mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-06 05:22:23 +02:00
Dashboard updates to support content creation + search
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -2359,6 +2359,12 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"fuse.js": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.6.tgz",
|
||||
"integrity": "sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw==",
|
||||
"dev": true
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
|
||||
@@ -346,10 +346,6 @@
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"when": "frontMatterCanOpenDashboard"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
@@ -392,6 +388,7 @@
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.23.0",
|
||||
"downshift": "6.0.6",
|
||||
"fuse.js": "6.4.6",
|
||||
"glob": "7.1.6",
|
||||
"gray-matter": "4.0.2",
|
||||
"html-loader": "1.3.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, SETTING_SEO_DESCRIPTION_FIELD } from './../constants/settings';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { join } from "path";
|
||||
import { commands, env, Uri, ViewColumn, Webview, WebviewOptions, WebviewPanel, WebviewPanelOptions, window, workspace } from "vscode";
|
||||
import { ColorThemeKind, commands, env, ThemeColor, Uri, ViewColumn, Webview, WebviewOptions, WebviewPanel, WebviewPanelOptions, window, workspace } from "vscode";
|
||||
import { SettingsHelper } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
@@ -12,10 +12,13 @@ import { DashboardCommand } from '../pagesView/DashboardCommand';
|
||||
import { DashboardMessage } from '../pagesView/DashboardMessage';
|
||||
import { Page } from '../pagesView/models/Page';
|
||||
import { openFileInEditor } from '../helpers/openFileInEditor';
|
||||
import { COMMAND_NAME } from '../constants/Extension';
|
||||
import { Template } from './Template';
|
||||
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
private static isDisposed: boolean = true;
|
||||
|
||||
/**
|
||||
* Init the dashboard
|
||||
@@ -24,6 +27,16 @@ export class Dashboard {
|
||||
const folders = Folders.get();
|
||||
await commands.executeCommand('setContext', CONTEXT.canOpenDashboard, folders && folders.length > 0);
|
||||
}
|
||||
|
||||
public static get isOpen(): boolean {
|
||||
return !Dashboard.isDisposed;
|
||||
}
|
||||
|
||||
public static reveal() {
|
||||
if (Dashboard.webview) {
|
||||
Dashboard.webview.reveal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the markdown preview in the editor
|
||||
@@ -37,9 +50,11 @@ export class Dashboard {
|
||||
ViewColumn.One,
|
||||
{
|
||||
enableScripts: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Dashboard.isDisposed = false;
|
||||
|
||||
Dashboard.webview.iconPath = {
|
||||
dark: Uri.file(join(extensionPath, 'assets/frontmatter-dark.svg')),
|
||||
light: Uri.file(join(extensionPath, 'assets/frontmatter.svg'))
|
||||
@@ -53,18 +68,35 @@ export class Dashboard {
|
||||
}
|
||||
});
|
||||
|
||||
Dashboard.webview.onDidDispose(() => {
|
||||
Dashboard.isDisposed = true;
|
||||
});
|
||||
|
||||
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
|
||||
switch(msg.command) {
|
||||
case DashboardMessage.getData:
|
||||
Dashboard.getSettings();
|
||||
Dashboard.getPages();
|
||||
break;
|
||||
case DashboardMessage.openFile:
|
||||
openFileInEditor(msg.data);
|
||||
break;
|
||||
case DashboardMessage.createContent:
|
||||
await commands.executeCommand(COMMAND_NAME.createContent);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async getSettings() {
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.settings,
|
||||
data: {
|
||||
folders: Folders.get(),
|
||||
initialized: await Template.isInitialized()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async getPages() {
|
||||
const config = SettingsHelper.getConfig();
|
||||
@@ -81,36 +113,38 @@ export class Dashboard {
|
||||
if (folderInfo) {
|
||||
for (const folder of folderInfo) {
|
||||
for (const file of folder.lastModified) {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.mdx`)) {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
|
||||
|
||||
if (article?.data.title) {
|
||||
const page: Page = {
|
||||
fmGroup: folder.title,
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
date: article?.data[dateField] || "",
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
if (article?.data.preview && crntWsFolder) {
|
||||
const previewPath = join(crntWsFolder.uri.fsPath, staticFolder || "", article?.data.preview);
|
||||
const previewUri = Uri.file(previewPath);
|
||||
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
|
||||
page.preview = preview?.toString() || "";
|
||||
if (article?.data.title) {
|
||||
const page: Page = {
|
||||
fmGroup: folder.title,
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
date: article?.data[dateField] || "",
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
if (article?.data.preview && crntWsFolder) {
|
||||
const previewPath = join(crntWsFolder.uri.fsPath, staticFolder || "", article?.data.preview);
|
||||
const previewUri = Uri.file(previewPath);
|
||||
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
|
||||
page.preview = preview?.toString() || "";
|
||||
}
|
||||
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
pages.push(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.data,
|
||||
command: DashboardCommand.pages,
|
||||
data: pages
|
||||
});
|
||||
}
|
||||
@@ -136,12 +170,12 @@ export class Dashboard {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://images.unsplash.com/ ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Front Matter</title>
|
||||
<title>Front Matter Dashboard</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;background:rgba(250, 250, 250, 1);">
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
|
||||
<div id="app" style="width:100%;height:100%;margin:0;padding:0;"></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759" alt="Daily usage" />
|
||||
|
||||
@@ -108,7 +108,6 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
|
||||
vscode.workspace.onDidChangeConfiguration(() => {
|
||||
Template.init();
|
||||
Preview.init();
|
||||
Dashboard.init();
|
||||
Folders.updateVsCodeCtx();
|
||||
|
||||
const exView = ExplorerView.getInstance();
|
||||
@@ -147,8 +146,13 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
|
||||
|
||||
// Pages dashboard
|
||||
Dashboard.init();
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => Dashboard.open(extensionPath) ));
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => {
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.reveal();
|
||||
} else {
|
||||
Dashboard.open(extensionPath);
|
||||
}
|
||||
}));
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(
|
||||
|
||||
28
src/hooks/useDarkMode.tsx
Normal file
28
src/hooks/useDarkMode.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function useDarkMode() {
|
||||
|
||||
const setTheme = (elm: HTMLElement) => {
|
||||
if (elm) {
|
||||
const darkMode = elm.classList.contains('vscode-dark');
|
||||
document.documentElement.classList.remove(`${darkMode ? "light" : "dark"}`);
|
||||
document.documentElement.classList.add(`${darkMode ? "dark" : "light"}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const mutationObserver = new MutationObserver((mutationsList, observer) => {
|
||||
const last = mutationsList.filter(item => item.type === "attributes" || item.attributeName === 'class').pop();
|
||||
setTheme(last?.target as HTMLElement);
|
||||
});
|
||||
|
||||
setTheme(document.body);
|
||||
|
||||
mutationObserver.observe(document.body, { childList: false, attributes: true })
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
}, ['']);
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useDebounce(value: string, delay: number) {
|
||||
export function useDebounce<T>(value: T, delay: number) {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum DashboardCommand {
|
||||
loading = "loading",
|
||||
data = "data"
|
||||
pages = "pages",
|
||||
settings = "settings"
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export enum DashboardMessage {
|
||||
getData = 'getData',
|
||||
openFile = 'openFile',
|
||||
getTheme = 'getTheme',
|
||||
createContent = 'createContent'
|
||||
}
|
||||
@@ -5,50 +5,87 @@ import { Overview } from './Overview';
|
||||
import { Header } from './Header';
|
||||
import { Tab } from '../constants/Tab';
|
||||
import { SortOption } from '../constants/SortOption';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Page } from '../models/Page';
|
||||
import useDarkMode from '../../hooks/useDarkMode';
|
||||
|
||||
export interface IDashboardProps {}
|
||||
|
||||
export const Dashboard: React.FunctionComponent<IDashboardProps> = (props: React.PropsWithChildren<IDashboardProps>) => {
|
||||
const { loading, pages } = useMessages();
|
||||
// TODO: Filter by tag / category
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
keys: [
|
||||
"title",
|
||||
"slug",
|
||||
"description",
|
||||
"fmFileName"
|
||||
]
|
||||
};
|
||||
|
||||
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({}: React.PropsWithChildren<IDashboardProps>) => {
|
||||
const { loading, pages, settings } = useMessages();
|
||||
const [ tab, setTab ] = React.useState(Tab.All);
|
||||
const [ sorting, setSorting ] = React.useState(SortOption.LastModified);
|
||||
const [ group, setGroup ] = React.useState<string | null>(null);
|
||||
const [ search, setSearch ] = React.useState<string | null>(null);
|
||||
const [ pageItems, setPageItems ] = React.useState<Page[]>([]);
|
||||
useDarkMode();
|
||||
|
||||
let pagesToShow = pages;
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = pages.filter(page => !page.draft);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = pages.filter(page => !!page.draft);
|
||||
} else {
|
||||
pagesToShow = pages;
|
||||
}
|
||||
React.useEffect(() => {
|
||||
// Check if search needs to be performed
|
||||
let searchedPages = pages;
|
||||
if (search) {
|
||||
const fuse = new Fuse(pages, fuseOptions);
|
||||
const results = fuse.search(search);
|
||||
searchedPages = results.map(page => page.item);
|
||||
}
|
||||
|
||||
let pagesSorted = pagesToShow;
|
||||
if (sorting === SortOption.FileNameAsc) {
|
||||
pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
|
||||
} else if (sorting === SortOption.FileNameDesc) {
|
||||
pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
|
||||
} else {
|
||||
pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
|
||||
}
|
||||
// Filter the pages
|
||||
let pagesToShow = searchedPages;
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page.draft);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page.draft);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
|
||||
// Show draft/published
|
||||
// Filter by draft
|
||||
// Filter by folder (if multiple)
|
||||
// TODO: Sort by last modified
|
||||
// Sort the pages
|
||||
let pagesSorted = pagesToShow;
|
||||
if (sorting === SortOption.FileNameAsc) {
|
||||
pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
|
||||
} else if (sorting === SortOption.FileNameDesc) {
|
||||
pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
|
||||
} else {
|
||||
pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
|
||||
}
|
||||
|
||||
if (group) {
|
||||
pagesSorted = pagesSorted.filter(page => page.fmGroup === group);
|
||||
}
|
||||
|
||||
setPageItems(pagesSorted);
|
||||
}, [ pages, tab, sorting, group, search ]);
|
||||
|
||||
const pageGroups = [...new Set(pages.map(page => page.fmGroup))];
|
||||
|
||||
return (
|
||||
<main className="h-full w-full">
|
||||
<main className={`h-full w-full`}>
|
||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<Header currentTab={tab}
|
||||
currentSorting={sorting}
|
||||
groups={pageGroups}
|
||||
crntGroup={group}
|
||||
totalPages={pageItems.length}
|
||||
switchTab={(tabId: Tab) => setTab(tabId)}
|
||||
switchSorting={(sortId: SortOption) => setSorting(sortId)}
|
||||
switchGroup={(groupId: string | null) => setGroup(groupId)}
|
||||
onSearch={(value: string | null) => setSearch(value)}
|
||||
settings={settings}
|
||||
/>
|
||||
|
||||
<Overview pages={pagesSorted} />
|
||||
</div>
|
||||
|
||||
{ loading ? <Spinner /> : null }
|
||||
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,6 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: Rea
|
||||
const dateString = format(parsedValue, 'yyyy-MM-dd');
|
||||
|
||||
return (
|
||||
<span className={`text-vulcan-100 text-xs`}>{dateString}</span>
|
||||
<span className={`text-vulcan-100 dark:text-whisper-900 text-xs`}>{dateString}</span>
|
||||
);
|
||||
};
|
||||
65
src/pagesView/components/Grouping.tsx
Normal file
65
src/pagesView/components/Grouping.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { MenuItem } from './MenuItem';
|
||||
|
||||
export interface IGroupingProps {
|
||||
groups: string[];
|
||||
crntGroup: string | null;
|
||||
switchGroup: (group: string | null) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_TYPE = "All types";
|
||||
|
||||
export const Grouping: React.FunctionComponent<IGroupingProps> = ({groups, crntGroup, switchGroup}: React.PropsWithChildren<IGroupingProps>) => {
|
||||
if (groups.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center ml-6">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<div>
|
||||
<span className={`text-gray-500 dark:text-whisper-700 mr-2 font-medium`}>Showing:</span>
|
||||
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600">
|
||||
{crntGroup || DEFAULT_TYPE}
|
||||
<ChevronDownIcon
|
||||
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm">
|
||||
<div className="py-1">
|
||||
<MenuItem
|
||||
title={DEFAULT_TYPE}
|
||||
value={null}
|
||||
isCurrent={!crntGroup}
|
||||
onClick={switchGroup} />
|
||||
|
||||
{groups.map((option) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
title={option}
|
||||
value={option}
|
||||
isCurrent={option === crntGroup}
|
||||
onClick={switchGroup} />
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,96 +1,62 @@
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { Tab } from '../constants/Tab';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { Fragment } from 'react';
|
||||
import { SortOption } from '../constants/SortOption';
|
||||
import { Navigation } from './Navigation';
|
||||
import { Sorting } from './Sorting';
|
||||
import { Grouping } from './Grouping';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
import { Searchbox } from './Searchbox';
|
||||
import { Settings } from '../models/Settings';
|
||||
|
||||
export interface IHeaderProps {
|
||||
settings: Settings;
|
||||
|
||||
// Navigation
|
||||
currentTab: Tab;
|
||||
currentSorting: SortOption;
|
||||
|
||||
totalPages: number;
|
||||
switchTab: (tabId: Tab) => void;
|
||||
|
||||
// Sorting
|
||||
currentSorting: SortOption;
|
||||
switchSorting: (sortId: SortOption) => void;
|
||||
|
||||
// Grouping
|
||||
groups: string[];
|
||||
crntGroup: string | null;
|
||||
switchGroup: (group: string | null) => void;
|
||||
|
||||
// Searching
|
||||
onSearch: (value: string | null) => void;
|
||||
}
|
||||
|
||||
function classNames(...classes: any[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, currentSorting, switchSorting, switchTab, totalPages, crntGroup, groups, switchGroup, onSearch, settings}: React.PropsWithChildren<IHeaderProps>) => {
|
||||
|
||||
export const tabs = [
|
||||
{ name: 'All articles', id: Tab.All},
|
||||
{ name: 'Published', id: Tab.Published },
|
||||
{ name: 'In draft', id: Tab.Draft }
|
||||
];
|
||||
|
||||
export const sortOptions = [
|
||||
{ name: "Last modified", id: SortOption.LastModified },
|
||||
{ name: "By filename (asc)", id: SortOption.FileNameAsc },
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc },
|
||||
];
|
||||
|
||||
export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, currentSorting, switchSorting, switchTab}: React.PropsWithChildren<IHeaderProps>) => {
|
||||
const createContent = () => {
|
||||
MessageHelper.sendMessage(DashboardMessage.createContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 flex items-center border-b border-gray-200 mb-8 sticky top-0 z-50 bg-gray-50 shadow-sm">
|
||||
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={classNames(
|
||||
tab.id === currentTab
|
||||
? 'border-teal-900 text-teal-900'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
||||
'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm'
|
||||
)}
|
||||
aria-current={tab.id === currentTab ? 'page' : undefined}
|
||||
onClick={() => switchTab(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className={`mb-6 sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
|
||||
<div className={`px-4 mb-2 flex items-center justify-between`}>
|
||||
<Searchbox onSearch={onSearch} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
|
||||
onClick={createContent}
|
||||
disabled={!settings.initialized}
|
||||
>
|
||||
Create content
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center ml-6">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-500 hover:text-gray-700">
|
||||
Sort
|
||||
<ChevronDownIcon
|
||||
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<div className="px-4 flex items-center border-b border-gray-200 dark:border-whisper-600">
|
||||
<Navigation currentTab={currentTab} totalPages={totalPages} switchTab={switchTab} />
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white ring-1 ring-vulcan-400 ring-opacity-5 focus:outline-none text-sm">
|
||||
<div className="py-1">
|
||||
{sortOptions.map((option) => (
|
||||
<Menu.Item key={option.id}>
|
||||
<button
|
||||
onClick={() => switchSorting(option.id)}
|
||||
className={classNames(
|
||||
option.id === currentSorting ? 'text-vulcan-500' : 'text-gray-500',
|
||||
'block px-4 py-2 text-sm font-medium w-full text-left hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
{option.name}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<Grouping crntGroup={crntGroup} groups={groups} switchGroup={switchGroup} />
|
||||
|
||||
<Sorting currentSorting={currentSorting} switchSorting={switchSorting} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { MarkdownIcon } from '../../viewpanel/components/Icons/MarkdownIcon';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
import { Page } from '../models/Page';
|
||||
import { DateField } from './DateField';
|
||||
@@ -15,19 +16,21 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
|
||||
return (
|
||||
<li className="relative">
|
||||
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full rounded-lg bg-gray-50 text-vulcan-500 text-left overflow-hidden shadow-md hover:shadow-xl`}
|
||||
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100`}
|
||||
onClick={openFile}>
|
||||
<div className="relative h-36 w-full overflow-hidden">
|
||||
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
|
||||
{
|
||||
preview ? (
|
||||
<img src={`${preview}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
) : (
|
||||
<img src={`https://images.unsplash.com/photo-1598620617148-c9e8ddee6711?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80`} alt={title} className="absolute inset-0 h-full w-full object-cover group-hover:opacity-95" loading="lazy" />
|
||||
<div className={`flex items-center justify-center bg-whisper-500 dark:bg-vulcan-200 dark:group-hover:bg-vulcan-100`}>
|
||||
<MarkdownIcon className={`h-32 text-vulcan-100 dark:text-whisper-100`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
<Status draft={!!draft} />
|
||||
|
||||
@@ -36,7 +39,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
|
||||
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
|
||||
|
||||
<p className="text-xs text-vulcan-200">{description}</p>
|
||||
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
22
src/pagesView/components/MenuItem.tsx
Normal file
22
src/pagesView/components/MenuItem.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMenuItemProps {
|
||||
title: string;
|
||||
value: any;
|
||||
isCurrent: boolean;
|
||||
onClick: (value: any) => void;
|
||||
}
|
||||
|
||||
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
|
||||
return (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => onClick(value)}
|
||||
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:text-gray-700 dark:hover:text-whisper-600`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
32
src/pagesView/components/Navigation.tsx
Normal file
32
src/pagesView/components/Navigation.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { Tab } from '../constants/Tab';
|
||||
|
||||
export interface INavigationProps {
|
||||
currentTab: Tab;
|
||||
totalPages: number;
|
||||
switchTab: (tabId: Tab) => void;
|
||||
}
|
||||
|
||||
export const tabs = [
|
||||
{ name: 'All articles', id: Tab.All},
|
||||
{ name: 'Published', id: Tab.Published },
|
||||
{ name: 'In draft', id: Tab.Draft }
|
||||
];
|
||||
|
||||
export const Navigation: React.FunctionComponent<INavigationProps> = ({currentTab, totalPages, switchTab}: React.PropsWithChildren<INavigationProps>) => {
|
||||
|
||||
return (
|
||||
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === currentTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === currentTab ? 'page' : undefined}
|
||||
onClick={() => switchTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === currentTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { MarkdownIcon } from '../../viewpanel/components/Icons/MarkdownIcon';
|
||||
import { Page } from '../models/Page';
|
||||
import { Settings } from '../models/Settings';
|
||||
import { Item } from './Item';
|
||||
import { List } from './List';
|
||||
|
||||
export interface IOverviewProps {
|
||||
pages: Page[];
|
||||
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages}: React.PropsWithChildren<IOverviewProps>) => {
|
||||
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settings}: React.PropsWithChildren<IOverviewProps>) => {
|
||||
|
||||
if (!pages || !pages.length) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center mt-16`}>
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<MarkdownIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto`} />
|
||||
{
|
||||
settings?.folders?.length > 0 ? (
|
||||
<p className={`text-xl font-medium`}>No Markdown to show</p>
|
||||
) : (
|
||||
<p className={`text-lg font-medium`}>Make sure you registered a content folder in your project to let Front Matter find the contents.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
|
||||
42
src/pagesView/components/Searchbox.tsx
Normal file
42
src/pagesView/components/Searchbox.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FilterIcon, SearchIcon } from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
|
||||
export interface ISearchboxProps {
|
||||
onSearch: (searchText: string) => void;
|
||||
}
|
||||
|
||||
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({onSearch}: React.PropsWithChildren<ISearchboxProps>) => {
|
||||
const [ value, setValue ] = React.useState('');
|
||||
const debounceSearch = useDebounce<string>(value, 500);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
onSearch(debounceSearch);
|
||||
}, [debounceSearch]);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<label htmlFor="search" className="sr-only">Search</label>
|
||||
<div className="relative flex justify-center">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
name="search"
|
||||
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`}
|
||||
placeholder="Search"
|
||||
value={value}
|
||||
autoFocus={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
63
src/pagesView/components/Sorting.tsx
Normal file
63
src/pagesView/components/Sorting.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { SortOption } from '../constants/SortOption';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { Fragment } from 'react';
|
||||
import { MenuItem } from './MenuItem';
|
||||
|
||||
export interface ISortingProps {
|
||||
currentSorting: SortOption;
|
||||
|
||||
switchSorting: (sortId: SortOption) => void;
|
||||
}
|
||||
|
||||
export const sortOptions = [
|
||||
{ name: "Last modified", id: SortOption.LastModified },
|
||||
{ name: "By filename (asc)", id: SortOption.FileNameAsc },
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc },
|
||||
];
|
||||
|
||||
export const Sorting: React.FunctionComponent<ISortingProps> = ({currentSorting, switchSorting}: React.PropsWithChildren<ISortingProps>) => {
|
||||
|
||||
const crntSort = sortOptions.find(x => x.id === currentSorting);
|
||||
|
||||
return (
|
||||
<div className="flex items-center ml-6">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<div>
|
||||
<span className={`text-gray-500 dark:text-whisper-700 mr-2 font-medium`}>Sort by:</span>
|
||||
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600">
|
||||
{crntSort?.name}
|
||||
<ChevronDownIcon
|
||||
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm">
|
||||
<div className="py-1">
|
||||
{sortOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
title={option.name}
|
||||
value={option.id}
|
||||
isCurrent={option.id === currentSorting}
|
||||
onClick={switchSorting} />
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ export interface ISpinnerProps {}
|
||||
|
||||
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
return (
|
||||
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-40`}>
|
||||
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
|
||||
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,6 @@ export interface IStatusProps {
|
||||
|
||||
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
|
||||
return (
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs ${draft ? "bg-red-500 text-whisper-200" : "bg-teal-500 text-whisper-500"}`}>{draft ? "Draft" : "Published"}</span>
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,29 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { ContentFolder } from '../../models';
|
||||
import { DashboardCommand } from '../DashboardCommand';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
import { Page } from '../models/Page';
|
||||
import { Settings } from '../models/Settings';
|
||||
|
||||
const vscode = MessageHelper.getVsCodeAPI();
|
||||
|
||||
export default function useMessages(options?: any) {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [pages, setPages] = useState<Page[]>([]);
|
||||
const [settings, setSettings] = useState<Settings>({} as any);
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
|
||||
switch (message.command) {
|
||||
case DashboardCommand.loading:
|
||||
setLoading(message.data);
|
||||
break;
|
||||
case DashboardCommand.data:
|
||||
case DashboardCommand.settings:
|
||||
setSettings(message.data);
|
||||
break;
|
||||
case DashboardCommand.pages:
|
||||
setPages(message.data);
|
||||
setLoading(false);
|
||||
break;
|
||||
@@ -26,11 +32,13 @@ export default function useMessages(options?: any) {
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
vscode.postMessage({ command: DashboardMessage.getTheme });
|
||||
vscode.postMessage({ command: DashboardMessage.getData });
|
||||
}, ['']);
|
||||
|
||||
return {
|
||||
loading,
|
||||
pages
|
||||
pages,
|
||||
settings
|
||||
};
|
||||
}
|
||||
6
src/pagesView/models/Settings.ts
Normal file
6
src/pagesView/models/Settings.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ContentFolder } from './../../models/ContentFolder';
|
||||
|
||||
export interface Settings {
|
||||
folders: ContentFolder[];
|
||||
initialized: boolean
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
.loader {
|
||||
border-top-color: var(--vscode-activityBar-activeBorder);;
|
||||
border-top-color: #15c2cb;
|
||||
animation: spinner 1.5s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,4 +24,5 @@ export enum CommandToCode {
|
||||
updatePreviewUrl = "update-preview-url",
|
||||
openInEditor = "open-in-editor",
|
||||
updateMetadata = "update-metadata",
|
||||
openDashboard = "open-dashboard",
|
||||
}
|
||||
@@ -16,6 +16,10 @@ export interface IBaseViewProps {
|
||||
|
||||
export const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndFiles}: React.PropsWithChildren<IBaseViewProps>) => {
|
||||
|
||||
const openDashboard = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openDashboard);
|
||||
};
|
||||
|
||||
const initProject = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.initProject);
|
||||
};
|
||||
@@ -32,6 +36,7 @@ export const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, fol
|
||||
|
||||
<Collapsible id={`base_actions`} title="Actions">
|
||||
<div className={`base__actions`}>
|
||||
<button onClick={openDashboard}>Open dashboard</button>
|
||||
<button onClick={initProject} disabled={settings?.isInitialized}>Initialize project</button>
|
||||
<button onClick={createContent} disabled={!settings?.isInitialized}>Create new content</button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { PanelSettings } from '../../models';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { MessageHelper } from '../../helpers/MessageHelper';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
import { Collapsible } from './Collapsible';
|
||||
import { VsCheckbox, VsLabel } from './VscodeComponents';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMarkdownIconProps {}
|
||||
export interface IMarkdownIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MarkdownIcon: React.FunctionComponent<IMarkdownIconProps> = (props: React.PropsWithChildren<IMarkdownIconProps>) => {
|
||||
export const MarkdownIcon: React.FunctionComponent<IMarkdownIconProps> = ({className}: React.PropsWithChildren<IMarkdownIconProps>) => {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M6.345 5h2.1v6.533H6.993l.055-5.31-1.774 5.31H4.072l-1.805-5.31c.04.644.06 5.31.06 5.31H1V5h2.156s1.528 4.493 1.577 4.807L6.345 5zm6.71 3.617v-3.5H11.11v3.5H9.166l2.917 2.916L15 8.617h-1.945z"/></svg>
|
||||
<svg className={className || ""} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M6.345 5h2.1v6.533H6.993l.055-5.31-1.774 5.31H4.072l-1.805-5.31c.04.644.06 5.31.06 5.31H1V5h2.156s1.528 4.493 1.577 4.807L6.345 5zm6.71 3.617v-3.5H11.11v3.5H9.166l2.917 2.916L15 8.617h-1.945z"/></svg>
|
||||
);
|
||||
};
|
||||
@@ -169,6 +169,9 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
case CommandToCode.openPreview:
|
||||
await commands.executeCommand(COMMAND_NAME.preview);
|
||||
break;
|
||||
case CommandToCode.openDashboard:
|
||||
await commands.executeCommand(COMMAND_NAME.dashboard);
|
||||
break;
|
||||
case CommandToCode.updatePreviewUrl:
|
||||
this.updatePreviewUrl(msg.data || "");
|
||||
break;
|
||||
@@ -516,7 +519,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* @param msg
|
||||
*/
|
||||
private postWebviewMessage(msg: { command: Command, data?: any }) {
|
||||
this.panel!.webview.postMessage(msg);
|
||||
this.panel?.webview?.postMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ const colors = require('tailwindcss/colors');
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
Reference in New Issue
Block a user