feat: Enhance Content, Media, and Snippets dashboards

This commit is contained in:
Elio Struyf
2026-06-04 10:04:46 +02:00
parent 6f333ff430
commit 7a0d89fa4e
50 changed files with 1073 additions and 609 deletions
+2
View File
@@ -13,3 +13,5 @@ e2e/sample
localization.log
localization.md
.env
.claude
+6 -6
View File
@@ -25,7 +25,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[var(--vscode-list-hoverBackground)] data-[state=open]:bg-[var(--vscode-list-hoverBackground)]",
"flex cursor-default select-none items-center gap-2 rounded-[7px] px-2.5 py-2 text-[13px] leading-none text-[var(--vscode-editor-foreground)] outline-none transition-colors focus:bg-[var(--vscode-list-hoverBackground)] data-[state=open]:bg-[var(--vscode-list-hoverBackground)]",
inset && "pl-8",
className
)}
@@ -45,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[12rem] overflow-hidden rounded-[10px] border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1.5 text-[var(--vscode-editor-foreground)] shadow-[0_10px_30px_rgba(0,0,0,0.35)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
@@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] rounded border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-96 overflow-auto",
"z-50 min-w-[12rem] rounded-[10px] border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1.5 text-[var(--vscode-editor-foreground)] shadow-[0_10px_30px_rgba(0,0,0,0.35)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-96 overflow-auto",
className
)}
{...props}
@@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[var(--vscode-list-hoverBackground)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer disabled:opacity-50",
"relative flex select-none items-center gap-2 rounded-[7px] px-2.5 py-2 text-[13px] leading-none text-[var(--vscode-editor-foreground)] outline-none transition-colors focus:bg-[var(--vscode-list-hoverBackground)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer disabled:opacity-50",
inset && "pl-8",
className
)}
@@ -99,7 +99,7 @@ const DropdownMenuLabel = React.forwardRef<
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
"px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-widest text-[var(--vscode-descriptionForeground)]",
inset && "pl-8",
className
)}
@@ -114,7 +114,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-[var(--frontmatter-border)]", className)}
className={cn("-mx-1 my-1.5 h-px bg-[var(--frontmatter-border)]", className)}
{...props}
/>
))
@@ -1,4 +1,4 @@
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import { ChevronDownIcon, PlusIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
@@ -28,11 +28,12 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({
<span className="relative z-50 inline-flex shadow-sm rounded-md">
<button
type="button"
className={`inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium ${choices.length > 0 ? `rounded-l` : `rounded`
className={`inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium ${choices.length > 0 ? `rounded-l` : `rounded`
} text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}
onClick={onClick}
disabled={disabled}
>
<PlusIcon className="w-4 h-4 mr-2" />
{title}
</button>
@@ -40,7 +41,7 @@ export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({
{choices.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger
className='h-full inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium focus:outline-none rounded-r text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50'
className='inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium focus:outline-none rounded-r text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50'
disabled={disabled}>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardCommonChoiceButtonOpen)}</span>
<ChevronDownIcon className={`h-4 w-4`} aria-hidden="true" />
@@ -36,7 +36,10 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({
}
return (
<span className={`date__field ${className || ''} text-xs text-[var(--frontmatter-secondary-text)]`}>
<span
className={`date__field ${className || ''} text-xs`}
style={{ fontFamily: 'var(--fm-mono)', color: 'var(--fm-text-lo)' }}
>
{dateValue}
</span>
);
@@ -18,7 +18,7 @@ import { SelectedItemActionAtom, SettingsSelector } from '../../state';
import { COMMAND_NAME, GeneralCommands } from '../../../constants';
import { PinIcon } from '../Icons/PinIcon';
import { PinnedItemsAtom } from '../../state/atom/PinnedItems';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { RenameIcon } from '../../../components/icons/RenameIcon';
import { openOnWebsite } from '../../utils';
import { CustomActions } from './CustomActions';
@@ -39,6 +39,7 @@ export interface IContentActionsProps {
};
};
onOpen: () => void;
onMenuOpenChange?: (open: boolean) => void;
}
export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
@@ -50,7 +51,8 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
listView,
isDefaultLocale,
translations,
locale
locale,
onMenuOpenChange
}: React.PropsWithChildren<IContentActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const [pinnedItems, setPinnedItems] = useRecoilState(PinnedItemsAtom);
@@ -114,84 +116,84 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
return (
<>
<div
className={`${listView ? '' : 'group/card absolute top-6 right-2'
} flex flex-col space-y-4`}
className={`${listView ? '' : 'group/card absolute top-2 right-2 z-10'} flex`}
>
<div
className={`flex items-center border border-transparent rounded-full ${listView ? '' : 'p-1 -mt-3'
} group-hover/card:bg-[var(--vscode-sideBar-background)] group-hover/card:border-[var(--frontmatter-border)]`}
>
<div className={`relative flex text-left`}>
<DropdownMenu>
<DropdownMenuTrigger
className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] data-[state=open]:text-[var(--vscode-tab-activeForeground)] focus:outline-none'>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardContentsContentActionsActionMenuButtonTitle)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenu onOpenChange={onMenuOpenChange}>
<DropdownMenuTrigger
className={`relative flex h-8 w-8 items-center justify-center rounded-[8px] border transition-colors focus:outline-none ${listView
? 'border-transparent text-[var(--fm-text-lo)] bg-transparent hover:bg-[var(--fm-surface-3)] hover:text-[var(--fm-text-mid)]'
: 'border-[var(--fm-border)] text-[var(--fm-border)] bg-[var(--fm-surface-2)]/95 backdrop-blur-sm hover:bg-[var(--fm-surface-3)] hover:text-[var(--fm-text-hi)]'
} data-[state=open]:text-[var(--fm-text-hi)] data-[state=open]:bg-[var(--fm-surface-3)]`}
>
<span className="sr-only">{l10n.t(LocalizationKey.dashboardContentsContentActionsActionMenuButtonTitle)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => isPinned ? unpinItem(e) : pinItem(e)}>
<PinIcon className={`mr-2 h-4 w-4 ${isPinned ? "" : "-rotate-90"}`} aria-hidden={true} />
<span>{isPinned ? l10n.t(LocalizationKey.commonUnpin) : l10n.t(LocalizationKey.commonPin)}</span>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => isPinned ? unpinItem(e) : pinItem(e)}>
<PinIcon className={`h-4 w-4 ${isPinned ? "" : "-rotate-90"}`} aria-hidden={true} />
<span>{isPinned ? l10n.t(LocalizationKey.commonUnpin) : l10n.t(LocalizationKey.commonPin)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onView}>
<EyeIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onMove}>
<ArrowRightCircleIcon className={`h-4 w-4`} aria-hidden={true} />
<span>Move to folder</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onRename}>
<RenameIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonRename)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onSmartRename}>
<ArrowPathIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemSmartRename)}</span>
</DropdownMenuItem>
{
settings?.websiteUrl && (
<DropdownMenuItem onClick={onOpenWebsite}>
<GlobeEuropeAfricaIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
</DropdownMenuItem>
)
}
<DropdownMenuItem onClick={onView}>
<EyeIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
{
locale && (
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
<LanguageIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>
</DropdownMenuItem>
)
}
<DropdownMenuItem onClick={onMove}>
<ArrowRightCircleIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>Move to folder</span>
</DropdownMenuItem>
<TranslationMenu
isDefaultLocale={isDefaultLocale}
locale={locale}
translations={translations} />
<DropdownMenuItem onClick={onRename}>
<RenameIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonRename)}</span>
</DropdownMenuItem>
<CustomActions
filePath={path}
contentType={contentType}
scripts={scripts} />
<DropdownMenuItem onClick={onSmartRename}>
<ArrowPathIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemSmartRename)}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{
settings?.websiteUrl && (
<DropdownMenuItem onClick={onOpenWebsite}>
<GlobeEuropeAfricaIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
</DropdownMenuItem>
)
}
{
locale && (
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>
</DropdownMenuItem>
)
}
<TranslationMenu
isDefaultLocale={isDefaultLocale}
locale={locale}
translations={translations} />
<CustomActions
filePath={path}
contentType={contentType}
scripts={scripts} />
<DropdownMenuItem onClick={onDelete} className={`focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]`}>
<TrashIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<DropdownMenuItem
onClick={onDelete}
className="text-[var(--fm-status-danger)] focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]"
>
<TrashIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
@@ -17,6 +17,12 @@ import { Alert } from '../Modals/Alert';
import { MoveFileDialog } from '../Modals/MoveFileDialog';
import { LocalizationKey } from '../../../localization';
import { deletePage } from '../../utils';
import { GroupOption } from '../../constants/GroupOption';
import { GroupingSelector, ViewSelector } from '../../state';
import usePagination from '../../hooks/usePagination';
import { DashboardViewType } from '../../models';
import { Pagination } from '../Header/Pagination';
import { PaginationStatus } from '../Header/PaginationStatus';
export interface IContentsProps {
pages: Page[];
@@ -27,13 +33,21 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
}: React.PropsWithChildren<IContentsProps>) => {
const loading = useRecoilValue(LoadingAtom);
const settings = useRecoilValue(SettingsSelector);
const grouping = useRecoilValue(GroupingSelector);
const view = useRecoilValue(ViewSelector);
const { pageItems } = usePages(pages);
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
const [showDeletionAlert, setShowDeletionAlert] = React.useState(false);
const [showMoveDialog, setShowMoveDialog] = React.useState(false);
const [page, setPage] = useState<Page | undefined>(undefined);
const [selectedItemAction, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
const pageFolders = [...new Set(pageItems.map((page) => page.fmFolder))];
const showFooterPagination =
grouping === GroupOption.none &&
view !== DashboardViewType.Structure &&
pageSetNr > 0 &&
pageItems.length > pageSetNr;
const onDismiss = useCallback(() => {
setShowMoveDialog(false);
@@ -100,6 +114,12 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
topContent={showFooterPagination ? (
<div className='flex items-center justify-between gap-2'>
<PaginationStatus totalPages={pageItems.length} />
<Pagination totalPages={pageItems.length} />
</div>
) : undefined}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=content" alt="Content metrics" />
@@ -12,6 +12,7 @@ export interface ICustomActionsProps {
contentType: string;
scripts: CustomScript[] | undefined;
showTrigger?: boolean;
onMenuOpenChange?: (open: boolean) => void;
}
export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
@@ -19,6 +20,7 @@ export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
contentType,
scripts,
showTrigger = false,
onMenuOpenChange,
}: React.PropsWithChildren<ICustomActionsProps>) => {
const onRunCustomScript = React.useCallback(
@@ -57,7 +59,7 @@ export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
key={script.id || script.title}
title={script.title}
onClick={(e) => onRunCustomScript(e, script)}>
<CommandLineIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<CommandLineIcon className={`h-4 w-4`} aria-hidden={true} />
<span>{script.title}</span>
</DropdownMenuItem>
))
@@ -70,10 +72,10 @@ export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
if (showTrigger) {
return (
<DropdownMenu>
<DropdownMenu onOpenChange={onMenuOpenChange}>
<DropdownMenuTrigger
title={l10n.t(LocalizationKey.commonOpenCustomActions)}
className='px-2 text-[var(--frontmatter-secondary-text)] hover:text-[var(--frontmatter-button-hoverBackground)] focus-visible:outline-none'>
className='px-2 text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)] focus-visible:outline-none'>
<span className="sr-only">{l10n.t(LocalizationKey.commonOpenCustomActions)}</span>
<CommandLineIconSolid className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
@@ -14,53 +14,73 @@ export interface IFooterActionsProps {
contentType: string;
websiteUrl?: string;
scripts?: CustomScript[];
/** Render as an inline icon row without the full-width wrapper bar */
compact?: boolean;
onMenuOpenChange?: (open: boolean) => void;
}
export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
filePath,
contentType,
websiteUrl,
scripts
scripts,
compact,
onMenuOpenChange,
}: React.PropsWithChildren<IFooterActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)] rounded-b`}>
{/* <ItemSelection filePath={filePath} show /> */}
const actions = (
<>
<QuickAction
title={l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => openFile(filePath)}>
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={() => openFile(filePath)}
>
<span className={`sr-only`}>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
<EyeIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
{
websiteUrl && (
<QuickAction
title={l10n.t(LocalizationKey.commonOpenOnWebsite)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => openOnWebsite(websiteUrl, filePath)}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
<GlobeEuropeAfricaIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
{websiteUrl && (
<QuickAction
title={l10n.t(LocalizationKey.commonOpenOnWebsite)}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={() => openOnWebsite(websiteUrl, filePath)}
>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
<GlobeEuropeAfricaIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
)}
<CustomActions
filePath={filePath}
contentType={contentType}
scripts={scripts}
showTrigger />
showTrigger
onMenuOpenChange={onMenuOpenChange}
/>
<QuickAction
title={l10n.t(LocalizationKey.commonDelete)}
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={() => setSelectedItemAction({ path: filePath, action: 'delete' })}>
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-status-danger)]`}
onClick={() => setSelectedItemAction({ path: filePath, action: 'delete' })}
>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonDelete)}</span>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
<TrashIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
</>
);
if (compact) {
return (
<div className={`flex items-center gap-0.5`}>
{actions}
</div>
);
}
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)] rounded-b`}>
{actions}
</div>
);
};
};
+146 -92
View File
@@ -43,6 +43,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
});
const isSelected = useMemo(() => selectedFiles.includes(pageData.fmFilePath), [selectedFiles, pageData.fmFilePath]);
const [menuOpen, setMenuOpen] = React.useState(false);
const [tagsExpanded, setTagsExpanded] = React.useState(false);
const onOpenFile = React.useCallback(() => {
openFile(pageData.fmFilePath);
@@ -83,9 +85,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({
statusHtml ? (
<div dangerouslySetInnerHTML={{ __html: statusHtml }} />
) : (
cardFields?.state && draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" ? <Status draft={pageData[draftField.name]} published={pageData.fmPublished} /> : null
cardFields?.state && draftField && draftField.name && typeof pageData[draftField.name] !== 'undefined'
? <Status draft={pageData[draftField.name]} published={pageData.fmPublished} />
: null
)
)
);
}, [statusHtml, cardFields?.state, draftField, pageData]);
const datePlaceholder = useMemo(() => {
@@ -95,60 +99,64 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
dateHtml ? (
<div className='mr-6' dangerouslySetInnerHTML={{ __html: dateHtml }} />
<div dangerouslySetInnerHTML={{ __html: dateHtml }} />
) : (
cardFields?.date && pageData.date ? <DateField className={`mr-6`} value={pageData.date} format={pageData.fmDateFormat} /> : null
cardFields?.date && pageData.date
? <DateField value={pageData.date} format={pageData.fmDateFormat} />
: null
)
)
);
}, [dateHtml, cardFields?.date, pageData]);
const hasDraftOrDate = useMemo(() => {
return cardFields && (cardFields.state || cardFields.date);
}, [cardFields]);
if (view === DashboardViewType.Grid) {
return (
<li className="relative">
<div
className={cn(`group flex flex-col items-start content-start h-full w-full text-left shadow-md dark:shadow-none hover:shadow-xl border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`, isSelected && `border-[var(--frontmatter-border-active)]`)}
className={cn(
`group flex flex-col w-full text-left`,
`min-h-[342px] h-full`,
`rounded-[9px]`,
`overflow-hidden`,
`border`,
`shadow-[0_1px_2px_rgba(0,0,0,.3)]`,
`hover:shadow-[0_8px_24px_rgba(0,0,0,.35)]`,
`transform-gpu`,
`hover:-translate-y-0.5`,
`transition duration-150 ease-out`,
menuOpen && `shadow-[0_8px_24px_rgba(0,0,0,.35)] -translate-y-0.5`,
isSelected
? `border-[var(--fm-accent-line)]`
: cn(`border-[var(--fm-border)] hover:border-[var(--fm-border-hi)]`, menuOpen && `border-[var(--fm-border-hi)]`)
)}
style={{ backgroundColor: 'var(--fm-surface-2)' }}
>
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`relative rounded-t h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]`}
>
{
imageHtml ?
<div className="h-full w-full" dangerouslySetInnerHTML={{ __html: imageHtml }} /> :
pageData[PREVIEW_IMAGE_FIELD] ? (
<img
src={`${pageData[PREVIEW_IMAGE_FIELD]}`}
alt={escapedTitle || ""}
className="absolute inset-0 h-full w-full object-cover object-left-top group-hover:brightness-75"
loading="lazy"
/>
) : (
<div
className={`h-full flex items-center justify-center bg-[var(--vscode-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}
>
<MarkdownIcon className={`h-32 text-[var(--vscode-sideBarTitle-foreground)] opacity-80`} />
</div>
)
}
</button>
<ItemSelection filePath={pageData.fmFilePath} />
<div className="relative p-4 w-full grow">
{
(statusPlaceholder || datePlaceholder) && (
<div className={`space-y-2 ${hasDraftOrDate ? `mb-2` : ``}`}>
<div>{statusPlaceholder}</div>
<div>{datePlaceholder}</div>
{/* ── Cover ─────────────────────────────────────────── */}
<div className={`relative h-[120px] flex-shrink-0 overflow-hidden rounded-t-[9px]`}>
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`absolute inset-0 cursor-pointer`}
>
{imageHtml ? (
<div className="h-full w-full" dangerouslySetInnerHTML={{ __html: imageHtml }} />
) : pageData[PREVIEW_IMAGE_FIELD] ? (
<img
src={`${pageData[PREVIEW_IMAGE_FIELD]}`}
alt={escapedTitle || ''}
className="absolute inset-0 h-full w-full object-cover object-left-top group-hover:brightness-75 transition-[filter] duration-150"
loading="lazy"
/>
) : (
<div
className={`h-full flex items-center justify-center`}
style={{ backgroundColor: 'var(--fm-surface-3)' }}
>
<MarkdownIcon className={`h-24 opacity-20 text-[var(--fm-text-mid)]`} />
</div>
)
}
)}
</button>
{/* ⋮ context menu — comes after the button in DOM so it receives pointer events */}
<ContentActions
path={pageData.fmFilePath}
relPath={pageData.fmRelFileWsPath}
@@ -158,62 +166,102 @@ export const Item: React.FunctionComponent<IItemProps> = ({
translations={pageData.fmTranslations}
scripts={settings?.scripts}
onOpen={onOpenFile}
onMenuOpenChange={setMenuOpen}
/>
</div>
{/* Checkbox — absolutely positioned to li.relative, appears in cover area */}
<ItemSelection filePath={pageData.fmFilePath} />
{/* ── Body ──────────────────────────────────────────── */}
<div className={`flex flex-col flex-1 px-3 pt-3 pb-1 min-h-0 gap-1 ${tagsExpanded ? '' : 'overflow-hidden'}`}>
<I18nLabel page={pageData} />
{/* Status dot + date row */}
{(statusPlaceholder || datePlaceholder) && (
<div className="flex items-center justify-between gap-2 mb-2 flex-shrink-0">
<div>{statusPlaceholder}</div>
<div>{datePlaceholder}</div>
</div>
)}
{/* Title — hero text */}
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`text-left block`}>
{
titleHtml ? (
<div dangerouslySetInnerHTML={{ __html: titleHtml }} />
) : (
<h2 className="font-bold">
<span>{escapedTitle}</span>
</h2>
)
}
className={`text-left flex-shrink-0 mb-1`}
>
{titleHtml ? (
<div dangerouslySetInnerHTML={{ __html: titleHtml }} />
) : (
<h2
className="text-[15px] font-semibold leading-snug line-clamp-2"
style={{ color: 'var(--fm-text-hi)', fontWeight: 650 }}
>
{escapedTitle}
</h2>
)}
</button>
{
(escapedDescription || descriptionHtml) && (
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`mt-2 text-left block`}>
{
descriptionHtml ? (
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : (
<p className={`text-xs text-[var(--frontmatter-secondary-text)]`}>{escapedDescription}</p>
)
}
</button>
)
}
{/* Description */}
{(escapedDescription || descriptionHtml) && (
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={onOpenFile}
className={`text-left flex-shrink-0`}
>
{descriptionHtml ? (
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : (
<p className={`text-xs line-clamp-2`} style={{ color: 'var(--fm-text-mid)' }}>
{escapedDescription}
</p>
)}
</button>
)}
{
tagsHtml ? (
<div className="mt-2" dangerouslySetInnerHTML={{ __html: tagsHtml }} />
) : (
<Tags values={tags} tagField={settings?.dashboardState?.contents?.tags} />
)
}
{/* Spacer pushes tags to bottom of body */}
<div className="flex-1" />
{/* Tags — single row, no wrap */}
{tagsHtml ? (
<div dangerouslySetInnerHTML={{ __html: tagsHtml }} />
) : (
<Tags values={tags} tagField={settings?.dashboardState?.contents?.tags} onExpandChange={setTagsExpanded} />
)}
{/* Optional custom metadata from extensibility */}
{footerHtml && (
<div
className="mt-2 min-w-0 text-[0.7rem] leading-none overflow-hidden whitespace-nowrap"
style={{ color: 'var(--fm-text-lo)', fontFamily: 'var(--fm-mono)' }}
>
<div
className="placeholder__card__footer"
style={{ display: 'block', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
dangerouslySetInnerHTML={{ __html: footerHtml }}
/>
</div>
)}
</div>
{
footerHtml && (
<div className="placeholder__card__footer p-4 w-full" dangerouslySetInnerHTML={{ __html: footerHtml }} />
)
}
<FooterActions
filePath={pageData.fmFilePath}
contentType={pageData.fmContentType}
websiteUrl={settings?.websiteUrl}
scripts={settings?.scripts} />
{/* ── Footer ────────────────────────────────────────── */}
<div
className="flex items-center justify-end gap-2 px-3 py-2 flex-shrink-0 rounded-b-[9px] overflow-hidden"
style={{ borderTop: '1px solid var(--fm-border)' }}
>
{/* Right: quick actions — fade in on hover */}
<div className={cn("shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150", menuOpen && "opacity-100")}>
<FooterActions
filePath={pageData.fmFilePath}
contentType={pageData.fmContentType}
websiteUrl={settings?.websiteUrl}
scripts={settings?.scripts}
compact
onMenuOpenChange={setMenuOpen}
/>
</div>
</div>
</div>
</li>
);
@@ -221,9 +269,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
<li className="relative">
<div
className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b hover:bg-opacity-70 border-[var(--frontmatter-border)] hover:bg-[var(--vscode-sideBar-background)]`}
className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b hover:bg-opacity-70`}
style={{
borderColor: 'var(--fm-border)',
backgroundColor: 'transparent'
}}
onMouseEnter={(e) => ((e.currentTarget as HTMLDivElement).style.backgroundColor = 'var(--fm-surface-3)')}
onMouseLeave={(e) => ((e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent')}
>
<div className="col-span-8 font-bold truncate flex items-center space-x-4">
<div className="col-span-8 truncate flex items-center space-x-4" style={{ color: 'var(--fm-text-hi)', fontWeight: 600 }}>
<ItemSelection filePath={pageData.fmFilePath} show />
<button
@@ -241,7 +295,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
listView
/>
</div>
<div className="col-span-2">
<div className="col-span-2" style={{ fontFamily: 'var(--fm-mono)', fontSize: '0.75rem', color: 'var(--fm-text-lo)' }}>
<DateField value={pageData.date} />
</div>
<div className="col-span-2">
@@ -14,15 +14,20 @@ export const List: React.FunctionComponent<IListProps> = ({
let className = '';
if (view === DashboardViewType.Grid) {
className = `grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5 gap-4`;
className = `grid gap-4`;
// inline style is set on the element below via style prop
} else if (view === DashboardViewType.List) {
className = `-mx-4`;
} else if (view === DashboardViewType.Structure) {
className = `structure-view`;
}
const gridStyle = view === DashboardViewType.Grid
? { gridTemplateColumns: 'repeat(auto-fill, minmax(264px, 1fr))' }
: undefined;
return (
<ul role="list" className={className}>
<ul role="list" className={className} style={gridStyle}>
{view === DashboardViewType.List && (
<li className={`px-5 relative uppercase py-2 border-b text-[var(--vscode-editor-foreground)] border-[var(--frontmatter-border)]`}>
<div className={`grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8`}>
@@ -195,35 +195,41 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
}
return (
<div className='divide-y divide-[var(--frontmatter-border)]'>
{
pinnedPages.length > 0 && (
<div className='mb-8'>
<h1 className='text-xl flex space-x-2 items-center mb-4'>
<PinIcon className={`-rotate-45`} />
<span>{localize(LocalizationKey.dashboardContentsOverviewPinned)}</span>
<div className='divide-y divide-[var(--fm-border)]'>
{pinnedPages.length > 0 && (
<div className='mb-6'>
<h2
className='flex items-center gap-2 mb-3 text-xs font-semibold uppercase tracking-widest'
style={{ color: 'var(--fm-text-xlo)' }}
>
<PinIcon className={`-rotate-45 w-3 h-3`} />
<span>{localize(LocalizationKey.dashboardContentsOverviewPinned)}</span>
</h2>
</h1>
{view === DashboardViewType.List ? (
<List>
{pinnedPages.map((page, idx) => (
view === DashboardViewType.List ? (
<Item key={`${page.slug}-${idx}`} {...page} />
) : (
<PinnedItem key={`${page.slug}-${idx}`} {...page} />
)
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>
</div>
)
}
) : (
<ul role="list" className="flex flex-wrap gap-2">
{pinnedPages.map((page, idx) => (
<PinnedItem key={`${page.slug}-${idx}`} {...page} />
))}
</ul>
)}
</div>
)}
<div className={pinnedItems.length > 0 ? "pt-8" : ""}>
<div className={pinnedItems.length > 0 ? 'pt-6' : ''}>
<List>
{pagedPages.map((page, idx) => (
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>
</div>
</div>
);
};
@@ -26,39 +26,72 @@ export const PinnedItem: React.FunctionComponent<IPinnedItemProps> = ({
}, [pageData.fmFilePath]);
return (
<li className={cn(`group flex w-full border border-[var(--frontmatter-border)] rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] relative`, isSelected && `border-[var(--frontmatter-border-active)]`)}>
<button onClick={onOpenFile} className='relative h-full w-1/3'>
{
pageData["fmPreviewImage"] ? (
<img
src={`${pageData["fmPreviewImage"]}`}
alt={pageData.title || ""}
className="absolute inset-0 h-full w-full object-left-top object-cover group-hover:brightness-75"
loading="lazy"
/>
) : (
<div
className={`h-full flex items-center justify-center bg-[var(--vscode-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)] border-r border-[var(--frontmatter-border)]`}
>
<MarkdownIcon className={`h-8 text-[var(--vscode-sideBarTitle-foreground)] opacity-80`} />
</div>
)
}
<li
className={cn(
`group relative flex items-center h-14 w-52 flex-shrink-0`,
`rounded-[6px] border overflow-hidden`,
`transition duration-150 ease-out`,
isSelected
? `border-[var(--fm-accent-line)]`
: `border-[var(--fm-border)] hover:border-[var(--fm-border-hi)]`
)}
style={{ backgroundColor: 'var(--fm-surface-2)' }}
>
{/* Thumbnail */}
<button
onClick={onOpenFile}
className={`relative h-full w-12 flex-shrink-0 overflow-hidden`}
style={{ borderRight: '1px solid var(--fm-border)' }}
>
{pageData['fmPreviewImage'] ? (
<img
src={`${pageData['fmPreviewImage']}`}
alt={pageData.title || ''}
className="absolute inset-0 h-full w-full object-cover object-left-top group-hover:brightness-75 transition-[filter] duration-150"
loading="lazy"
/>
) : (
<div
className={`h-full flex items-center justify-center`}
style={{ backgroundColor: 'var(--fm-surface-3)' }}
>
<MarkdownIcon className={`h-5 opacity-25 text-[var(--fm-text-mid)]`} />
</div>
)}
</button>
{/* Text content */}
<button
onClick={onOpenFile}
className={`flex-1 px-2 text-left min-w-0 overflow-hidden`}
>
<p
className={`text-xs font-semibold truncate`}
style={{ color: 'var(--fm-text-hi)' }}
>
{escapedTitle}
</p>
{pageData.fmContentType && (
<p
className={`text-[0.65rem] truncate mt-0.5`}
style={{ color: 'var(--fm-text-xlo)' }}
>
{pageData.fmContentType}
</p>
)}
</button>
{/* Selection checkbox */}
<ItemSelection filePath={pageData.fmFilePath} />
<button onClick={onOpenFile} className='relative w-2/3 p-4 pr-6 text-left flex items-start'>
<p className='font-bold'>{escapedTitle}</p>
<ContentActions
path={pageData.fmFilePath}
relPath={pageData.fmRelFileWsPath}
contentType={pageData.fmContentType}
scripts={settings?.scripts}
onOpen={openFile}
/>
</button>
{/* Context menu */}
<ContentActions
path={pageData.fmFilePath}
relPath={pageData.fmRelFileWsPath}
contentType={pageData.fmContentType}
scripts={settings?.scripts}
onOpen={onOpenFile}
/>
</li>
);
};
};
@@ -33,10 +33,17 @@ export const Status: React.FunctionComponent<IStatusProps> = ({
if (settings?.draftField && settings.draftField.type === 'choice') {
if (draftValue) {
return (
<span
className={`inline-block px-[3px] py-[2px] rounded font-semibold uppercase tracking-wide text-[0.7rem] text-[var(--vscode-badge-foreground)] bg-[var(--vscode-badge-background)]`}
>
{draftValue}
<span className={`inline-flex items-center gap-1.5`}>
<span
className={`w-1.5 h-1.5 rounded-full flex-shrink-0`}
style={{ backgroundColor: 'var(--fm-accent)' }}
/>
<span
className={`text-[0.7rem] font-medium uppercase tracking-wide`}
style={{ color: 'var(--fm-text-mid)' }}
>
{draftValue as string}
</span>
</span>
);
} else {
@@ -48,25 +55,31 @@ export const Status: React.FunctionComponent<IStatusProps> = ({
return null;
}
const dotColor = draftValue
? 'var(--fm-status-draft)'
: isFuture
? 'var(--fm-status-scheduled)'
: 'var(--fm-status-published)';
const label = draftValue
? l10n.t(LocalizationKey.dashboardContentsStatusDraft)
: isFuture
? l10n.t(LocalizationKey.dashboardContentsStatusScheduled)
: l10n.t(LocalizationKey.dashboardContentsStatusPublished);
return (
<span
className={`draft__status
inline-block px-[3px] py-[2px] rounded font-semibold uppercase tracking-wide text-[0.7rem]
${draftValue ?
'bg-[var(--vscode-statusBarItem-errorBackground)] text-[var(--vscode-statusBarItem-errorForeground)]' :
isFuture ?
'bg-[var(--vscode-statusBarItem-warningBackground)] text-[var(--vscode-statusBarItem-warningForeground)]' :
'bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)]'
}`}
>
{
draftValue ?
l10n.t(LocalizationKey.dashboardContentsStatusDraft) : (
isFuture ?
l10n.t(LocalizationKey.dashboardContentsStatusScheduled) :
l10n.t(LocalizationKey.dashboardContentsStatusPublished)
)
}
<span className={`draft__status inline-flex items-center gap-1.5`}>
<span
className={`w-1.5 h-1.5 rounded-full flex-shrink-0`}
style={{ backgroundColor: dotColor }}
aria-hidden="true"
/>
<span
className={`text-[0.7rem] font-medium uppercase tracking-wide`}
style={{ color: dotColor }}
>
{label}
</span>
</span>
);
};
@@ -21,15 +21,28 @@ export const Tag: React.FunctionComponent<ITagProps> = ({
return (
<button
className={`inline-block mr-1 mt-1 text-xs text-[var(--vscode-button-secondaryForeground)] bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] border border-[var(--frontmatter-border)] rounded px-1 py-0.5`}
className={`flex-shrink-0 inline-block text-[0.65rem] font-medium px-1.5 py-0.5 rounded transition-colors duration-100`}
style={{
color: 'var(--fm-text-lo)',
backgroundColor: 'var(--fm-surface-3)',
border: '1px solid var(--fm-border)'
}}
title={l10n.t(LocalizationKey.commonFilterValue, value)}
onMouseEnter={(e) => {
(e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-mid)';
(e.currentTarget as HTMLButtonElement).style.backgroundColor = 'var(--fm-surface-4)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-lo)';
(e.currentTarget as HTMLButtonElement).style.backgroundColor = 'var(--fm-surface-3)';
}}
onClick={() => {
if (tagField) {
navigate(`${routePaths.contents}?taxonomy=${tagField}&value=${value}`);
}
}}
>
#{value}
{value}
</button>
);
};
};
@@ -4,26 +4,50 @@ import { Tag } from './Tag';
export interface ITagsProps {
values?: string[];
tagField?: string | null | undefined;
onExpandChange?: (expanded: boolean) => void;
}
const MAX_VISIBLE = 3;
export const Tags: React.FunctionComponent<ITagsProps> = ({
values,
tagField
tagField,
onExpandChange
}: React.PropsWithChildren<ITagsProps>) => {
const [expanded, setExpanded] = React.useState(false);
if (!values || values.length === 0) {
return null;
}
const filtered = values.filter(Boolean);
const visible = expanded ? filtered : filtered.slice(0, MAX_VISIBLE);
const overflow = filtered.length - MAX_VISIBLE;
const toggle = (e: React.MouseEvent) => {
e.stopPropagation();
const next = !expanded;
setExpanded(next);
onExpandChange?.(next);
};
return (
<div className="mt-2">
{values.map(
(tag, index) => tag && (
<Tag
key={index}
value={tag}
tagField={tagField} />
)
<div className={`flex items-center gap-1 flex-wrap`}>
{visible.map((tag, index) => (
<Tag key={index} value={tag} tagField={tagField} />
))}
{!expanded && overflow > 0 && (
<button
className="flex-shrink-0 inline-flex items-center text-[0.65rem] font-medium px-1.5 py-0.5 rounded-full"
style={{
color: 'var(--vscode-button-foreground)',
backgroundColor: 'var(--frontmatter-button-background)'
}}
onClick={toggle}
>
+{overflow}
</button>
)}
</div>
);
};
};
@@ -1,6 +1,5 @@
import * as React from 'react';
import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator } from '../../../components/shadcn/Dropdown';
import { LanguageIcon } from '@heroicons/react/24/outline';
import { MenuButton, MenuItem } from '../Menu';
import { useRecoilState, useRecoilValue } from 'recoil';
import { DEFAULT_LOCALE_STATE, LocaleAtom, LocalesAtom } from '../../state';
@@ -30,13 +29,9 @@ export const LanguageFilter: React.FunctionComponent<ILanguageFilterProps> = ()
return (
<DropdownMenu>
<MenuButton
label={
<>
<LanguageIcon className={`inline-block w-4 h-4 mr-2`} />
<span>{l10n.t(LocalizationKey.dashboardFiltersLanguageFilterLabel)}</span>
</>
}
label={l10n.t(LocalizationKey.dashboardFiltersLanguageFilterLabel)}
title={crntLocaleName || l10n.t(LocalizationKey.dashboardFiltersLanguageFilterAll)}
isActive={crntLocale !== DEFAULT_LOCALE_STATE && !!crntLocale}
/>
<DropdownMenuContent align='start'>
@@ -192,10 +192,15 @@ export const ActionsBar: React.FunctionComponent<IActionsBarProps> = ({
return null;
}, [view, settings?.scripts, selectedFiles]);
if (selectedFiles.length === 0) {
return null;
}
return (
<>
<div
className={`w-full flex items-center justify-between py-2 px-4 border-b bg-[var(--vscode-sideBar-background)] text-[var(--vscode-sideBar-foreground)] border-[var(--frontmatter-border)]`}
className={`w-full flex items-center justify-between py-2 px-4 border-b text-[var(--vscode-sideBar-foreground)] border-[var(--frontmatter-border)]`}
style={{ backgroundColor: 'var(--fm-accent-soft)', borderTop: '1px solid var(--fm-accent-line)' }}
aria-label="Item actions"
>
<div className='flex items-center space-x-6'>
@@ -80,12 +80,12 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (
}, [selectedFolder, settings]);
return (
<ol role="list" className="flex space-x-2 px-4 flex-1">
<ol role="list" className="flex items-center gap-1.5 min-w-0 overflow-x-auto whitespace-nowrap pr-2 flex-1">
<li className="flex">
<div className="flex items-center">
<button
onClick={() => updateMediaFolder(HOME_PAGE_NAVIGATION_ID)}
className={`text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]`}
className="text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]"
>
<HomeIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
<span className="sr-only">{l10n.t(LocalizationKey.dashboardHeaderBreadcrumbHome)}</span>
@@ -97,7 +97,7 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (
<li key={folder} className="flex">
<div className="flex items-center">
<svg
className={`flex-shrink-0 h-5 w-5 text-[var(--vscode-tab-inactiveForeground)]`}
className="flex-shrink-0 h-5 w-5 text-[var(--fm-text-xlo)]"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 20"
@@ -108,7 +108,7 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (
<button
onClick={() => updateMediaFolder(folder)}
className={`ml-2 text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]`}
className="ml-1.5 text-sm font-medium text-[var(--fm-text-lo)] hover:text-[var(--fm-text-hi)]"
>
{basename(folder)}
</button>
@@ -1,7 +1,6 @@
import * as React from 'react';
import { MenuButton, MenuItem } from '../Menu';
import * as l10n from '@vscode/l10n';
import { FunnelIcon } from '@heroicons/react/24/solid';
import { LocalizationKey } from '../../../localization';
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
@@ -27,13 +26,9 @@ export const Filter: React.FunctionComponent<IFilterProps> = ({
return (
<DropdownMenu>
<MenuButton
label={
<>
<FunnelIcon className={`inline-block w-4 h-4 mr-2`} />
<span>{label}</span>
</>
}
label={label}
title={activeItem || DEFAULT_VALUE}
isActive={!!activeItem}
/>
<DropdownMenuContent>
@@ -26,7 +26,11 @@ export const FoldersFilter: React.FunctionComponent<
return (
<DropdownMenu>
<MenuButton label={l10n.t(LocalizationKey.dashboardHeaderFoldersMenuButtonShowing)} title={crntFolder || DEFAULT_TYPE} />
<MenuButton
label={l10n.t(LocalizationKey.dashboardHeaderFoldersMenuButtonShowing)}
title={crntFolder || DEFAULT_TYPE}
isActive={!!crntFolder}
/>
<DropdownMenuContent>
<MenuItem
@@ -48,7 +48,11 @@ export const Grouping: React.FunctionComponent<
return (
<DropdownMenu>
<MenuButton label={localize(LocalizationKey.dashboardHeaderGroupingMenuButtonLabel)} title={crntGroup?.name || ''} />
<MenuButton
label={localize(LocalizationKey.dashboardHeaderGroupingMenuButtonLabel)}
title={crntGroup?.name || ''}
isActive={!!group && group !== GroupOption.none}
/>
<DropdownMenuContent>
{GROUP_OPTIONS.map((option) => (
@@ -6,7 +6,7 @@ import { DashboardMessage } from '../../DashboardMessage';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { GroupingSelector, MultiSelectedItemsAtom, SortingAtom } from '../../state';
import { AllPagesAtom, LastSyncAtom, MultiSelectedItemsAtom, SortingAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -19,10 +19,6 @@ import { HeartIcon } from '@heroicons/react/24/solid';
import { useLocation, useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import { useMemo } from 'react';
import { Pagination } from './Pagination';
import { GroupOption } from '../../constants/GroupOption';
import usePagination from '../../hooks/usePagination';
import { PaginationStatus } from './PaginationStatus';
import { Navigation } from './Navigation';
import { ProjectSwitcher } from './ProjectSwitcher';
import * as l10n from '@vscode/l10n';
@@ -33,6 +29,7 @@ import { COMMAND_NAME, GeneralCommands, SPONSOR_LINK } from '../../../constants'
import { Filters } from './Filters';
import { ActionsBar } from './ActionsBar';
import { RefreshDashboardData } from './RefreshDashboardData';
import { useRelativeTime } from '../../hooks/useRelativeTime';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -47,15 +44,15 @@ export interface IHeaderProps {
export const Header: React.FunctionComponent<IHeaderProps> = ({
header,
totalPages,
settings
}: React.PropsWithChildren<IHeaderProps>) => {
const grouping = useRecoilValue(GroupingSelector);
const resetSorting = useResetRecoilState(SortingAtom);
const resetSelectedItems = useResetRecoilState(MultiSelectedItemsAtom);
const location = useLocation();
const navigate = useNavigate();
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
const allPages = useRecoilValue(AllPagesAtom);
const lastSync = useRecoilValue(LastSyncAtom);
const syncLabel = useRelativeTime(lastSync);
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
@@ -173,55 +170,55 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
{location.pathname === routePaths.contents && (
<>
<div className={`px-4 mt-2 mb-2 flex items-center justify-between`}>
<div className={`flex items-center justify-start space-x-2 flex-1`}>
{/* Title row: heading + count/sync status | search + create */}
<div className={`px-4 py-2 flex items-center justify-between gap-4`}>
<div className={`flex flex-col min-w-0`}>
<h1 className={`text-lg font-semibold leading-tight`} style={{ color: 'var(--fm-text-hi)' }}>
{l10n.t(LocalizationKey.dashboardHeaderTabsContents)}
</h1>
<div className={`flex items-center gap-2 flex-wrap`} style={{ color: 'var(--fm-text-lo)' }}>
{allPages.length > 0 && (
<p className={`text-xs leading-tight mt-0.5`} style={{ fontFamily: 'var(--fm-mono)', color: 'var(--fm-text-lo)' }}>
{allPages.length} {allPages.length === 1 ? 'item' : 'items'}
{syncLabel ? <> &middot; synced {syncLabel}</> : null}
{` `}&middot;{` `}
</p>
)}
<RefreshDashboardData />
</div>
</div>
<div className={`flex items-center gap-2 flex-shrink-0`}>
<Searchbox placeholder={l10n.t(LocalizationKey.commonSearch) + ' content...'} />
<ChoiceButton
title={l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
choices={choiceOptions}
onClick={createContent}
disabled={!settings?.initialized}
/>
<RefreshDashboardData />
</div>
<Searchbox />
</div>
<div className={`px-4 flex flex-row items-center border-b justify-between border-[var(--frontmatter-border)]`}>
<div>
<Navigation totalPages={totalPages || 0} />
</div>
{/* Combined filter row: nav tabs | filters + grouping + sorting + view switch */}
<div className={`overflow-x-auto px-4 py-1.5 flex items-center justify-between gap-2 border-b border-[var(--frontmatter-border)]`}>
<Navigation />
<div className={`flex items-center gap-4 flex-shrink-0`}>
<ClearFilters />
<Filters />
<Grouping />
<Sorting view={NavigationType.Contents} />
<div className="h-5 w-px flex-shrink-0" style={{ backgroundColor: 'var(--frontmatter-border)' }} aria-hidden="true" />
<div>
<ViewSwitch />
</div>
</div>
<div
className={`overflow-x-auto py-2 px-4 w-full flex items-center justify-between lg:justify-end border-b space-x-4 lg:space-x-6 xl:space-x-8 bg-[var(--vscode-panel-background)] border-[var(--frontmatter-border)]`}
>
<ClearFilters />
<Filters />
<Grouping />
<Sorting view={NavigationType.Contents} />
</div>
{pageSetNr > 0 &&
(totalPages || 0) > pageSetNr &&
(!grouping || grouping === GroupOption.none) && (
<div
className={`px-4 flex justify-between py-2 border-b border-[var(--frontmatter-border)]`}
>
<PaginationStatus totalPages={totalPages || 0} />
<Pagination totalPages={totalPages || 0} />
</div>
)}
<ActionsBar view={NavigationType.Contents} />
</>
)}
@@ -5,39 +5,60 @@ import { SettingsAtom, TabAtom, TabInfoAtom } from '../../state';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
export interface INavigationProps {
totalPages: number;
}
export interface INavigationItemProps {
tabId: string;
isCrntTab: boolean;
count?: number;
onClick: () => void;
}
const NavigationItem: React.FunctionComponent<INavigationItemProps> = ({
tabId,
isCrntTab,
count,
onClick,
children
}: React.PropsWithChildren<INavigationItemProps>) => {
return (
<button
className={`${isCrntTab
?
`border-[var(--vscode-textLink-foreground)] text-[var(--vscode-textLink-foreground)]` :
`border-transparent text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-textLink-activeForeground)] hover:border-[var(--vscode-textLink-activeForeground)]`
} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[6px] text-sm font-medium whitespace-nowrap transition-colors duration-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1`}
style={{
backgroundColor: isCrntTab ? 'var(--fm-surface-4)' : 'transparent',
color: isCrntTab ? 'var(--fm-text-hi)' : 'var(--fm-text-lo)',
outlineColor: isCrntTab ? 'var(--fm-accent)' : undefined
}}
onMouseEnter={(e) => {
if (!isCrntTab) {
(e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-mid)';
(e.currentTarget as HTMLButtonElement).style.backgroundColor = 'var(--fm-surface-3)';
}
}}
onMouseLeave={(e) => {
if (!isCrntTab) {
(e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-lo)';
(e.currentTarget as HTMLButtonElement).style.backgroundColor = 'transparent';
}
}}
aria-current={isCrntTab ? 'page' : undefined}
onClick={onClick}
>
{children}
{count !== undefined && count > 0 && (
<span
className={`text-[0.65rem] font-medium px-1.5 py-0.5 rounded-full leading-none`}
style={{
backgroundColor: isCrntTab ? 'var(--fm-accent)' : 'var(--fm-surface-3)',
color: isCrntTab ? 'var(--fm-accent-ink)' : 'var(--fm-text-xlo)'
}}
>
{count}
</span>
)}
</button>
)
);
};
export const Navigation: React.FunctionComponent<INavigationProps> = () => {
export const Navigation: React.FunctionComponent = () => {
const [crntTab, setCrntTab] = useRecoilState(TabAtom);
const tabInfo = useRecoilValue(TabInfoAtom);
const settings = useRecoilValue(SettingsAtom);
@@ -61,16 +82,17 @@ export const Navigation: React.FunctionComponent<INavigationProps> = () => {
}, [settings?.draftField?.type, tabInfo]);
return (
<nav className="flex-1 -mb-px flex space-x-2 xl:space-x-4" aria-label="Tabs">
<nav className="flex items-center gap-1 flex-1" aria-label="Tabs">
{settings?.draftField?.type === 'boolean' ? (
tabs.map((tab) => (
<NavigationItem
tabId={tab.id}
isCrntTab={tab.id === crntTab}
count={tabInfo?.[tab.id]}
key={tab.name}
onClick={() => setCrntTab(tab.id)}>
onClick={() => setCrntTab(tab.id)}
>
{tab.name}
{tabInfo && tabInfo[tab.id] ? ` (${tabInfo[tab.id]})` : ''}
</NavigationItem>
))
) : (
@@ -78,9 +100,10 @@ export const Navigation: React.FunctionComponent<INavigationProps> = () => {
<NavigationItem
tabId={tabs[0].id}
isCrntTab={tabs[0].id === crntTab}
onClick={() => setCrntTab(tabs[0].id)}>
count={tabInfo?.[tabs[0].id]}
onClick={() => setCrntTab(tabs[0].id)}
>
{tabs[0].name}
{tabInfo && tabInfo[tabs[0].id] ? ` (${tabInfo[tabs[0].id]})` : ''}
</NavigationItem>
{settings?.draftField?.choices?.map((value, idx) => (
@@ -88,9 +111,10 @@ export const Navigation: React.FunctionComponent<INavigationProps> = () => {
key={`${value}-${idx}`}
tabId={value}
isCrntTab={value === crntTab}
onClick={() => setCrntTab(value)}>
count={tabInfo?.[value]}
onClick={() => setCrntTab(value)}
>
{value}
{tabInfo && tabInfo[value] ? ` (${tabInfo[value]})` : ''}
</NavigationItem>
))}
</>
@@ -28,26 +28,39 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
const buttons = useMemo((): JSX.Element[] => {
const maxButtons = 5;
const buttons: JSX.Element[] = [];
const start = page - maxButtons;
const end = page + maxButtons;
const start = Math.max(0, page - Math.floor(maxButtons / 2));
const end = Math.min(totalPagesNr, start + maxButtons - 1);
for (let i = start; i < end; i++) {
if (i >= 0 && i <= totalPagesNr) {
buttons.push(
<button
key={i}
disabled={i === page}
onClick={() => {
setPage(i);
}}
className={`max-h-8 rounded ${page === i
? `px-2 bg-[var(--vscode-list-activeSelectionBackground)] text-[var(--vscode-list-activeSelectionForeground)]`
: `text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}`}
>
{i + 1}
</button>
);
}
for (let i = start; i <= end; i++) {
const isActive = i === page;
buttons.push(
<button
key={i}
disabled={isActive}
onClick={() => setPage(i)}
className={`min-w-[28px] h-7 px-1.5 rounded-[6px] text-sm font-medium transition-colors duration-100`}
style={{
fontFamily: 'var(--fm-mono)',
backgroundColor: isActive ? 'var(--fm-accent)' : 'transparent',
color: isActive ? 'var(--fm-accent-ink)' : 'var(--fm-text-lo)',
cursor: isActive ? 'default' : 'pointer'
}}
onMouseEnter={(e) => {
if (!isActive) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = 'var(--fm-surface-3)';
(e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-mid)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = 'transparent';
(e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-lo)';
}
}}
>
{i + 1}
</button>
);
}
return buttons;
}, [page, totalPagesNr]);
@@ -65,17 +78,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
}
return (
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
<PaginationButton
title={l10n.t(LocalizationKey.dashboardHeaderPaginationFirst)}
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(0);
}
}}
/>
<div className="flex justify-between items-center sm:justify-end gap-1 text-sm">
<PaginationButton
title={l10n.t(LocalizationKey.dashboardHeaderPaginationPrevious)}
disabled={page === 0}
@@ -93,12 +96,6 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
disabled={page >= totalPagesNr}
onClick={() => setPage(page + 1)}
/>
<PaginationButton
title={l10n.t(LocalizationKey.dashboardHeaderPaginationLast)}
disabled={page >= totalPagesNr}
onClick={() => setPage(totalPagesNr)}
/>
</div>
);
};
@@ -26,19 +26,21 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
if (totalItems < items) {
return totalItems;
}
return totalItems;
return items;
}, [page, totalMedia, pageSetNr]);
return (
<div className="hidden sm:flex">
<p className={`text-sm text-[var(--vscode-tab-inactiveForeground)]`}>
{
l10n.t(LocalizationKey.dashboardHeaderPaginationStatusText,
(page * pageSetNr + 1),
totelItemsOnPage,
totalItems
)
}
<p
className={`text-xs`}
style={{ fontFamily: 'var(--fm-mono)', color: 'var(--fm-text-lo)' }}
>
{l10n.t(
LocalizationKey.dashboardHeaderPaginationStatusText,
page * pageSetNr + 1,
totelItemsOnPage,
totalItems
)}
</p>
</div>
);
@@ -64,7 +64,7 @@ export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardData
title={l10n.t(LocalizationKey.dashboardHeaderRefreshDashboardLabel)}
onClick={refresh}
>
<ArrowClockwiseIcon className={`h-5 w-5`} />
<ArrowClockwiseIcon className={`h-4 w-4`} />
<span className="sr-only">{l10n.t(LocalizationKey.dashboardHeaderRefreshDashboardLabel)}</span>
</button>
);
@@ -39,7 +39,7 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({
}, [debounceSearch]);
return (
<div className="flex justify-end space-x-4 flex-1">
<div className="flex justify-end">
<div className="min-w-0">
<label htmlFor="search" className="sr-only">
{l10n.t(LocalizationKey.commonSearch)}
@@ -182,6 +182,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({
label={l10n.t(LocalizationKey.dashboardHeaderSortingLabel)}
title={crntSort?.title || crntSort?.name || ''}
disabled={!!searchValue}
isActive={crntSorting !== null}
/>
<DropdownMenuContent>
@@ -19,7 +19,7 @@ export const Tab: React.FunctionComponent<ITabProps> = ({
<button
className={cn(`h-full flex items-center py-2 px-1 text-sm font-medium text-center border-b-2 border-transparent hover:text-[var(--vscode-tab-activeForeground)] ${location.pathname === `/${navigationType}`
?
`text-[var(--frontmatter-nav-active)] border-[var(--frontmatter-nav-active)]` :
`text-[var(--frontmatter-nav-active)] border-[var(--fm-accent)]` :
`text-[var(--frontmatter-nav-inactive)]`
}`)}
type="button"
@@ -27,43 +27,55 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
}
}, [settings?.pageViewType]);
const btnBase = `flex items-center px-2 py-1.5 transition-colors duration-100`;
const btnStyle = (isActive: boolean) => ({
backgroundColor: isActive ? 'var(--fm-accent)' : 'transparent',
color: isActive ? 'var(--fm-accent-ink)' : 'var(--fm-text-lo)'
});
return (
<div className={`flex rounded-sm lg:mb-1 bg-[var(--vscode-button-secondaryBackground)]`}>
<div
className={`flex rounded-[6px] overflow-hidden`}
style={{ border: '1px solid var(--fm-border)' }}
>
<button
className={`flex items-center px-2 py-1 rounded-l-sm ${view === DashboardViewType.Grid ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
}`}
className={`${btnBase} rounded-l-[5px]`}
style={btnStyle(view === DashboardViewType.Grid)}
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToGrid)}
type={`button`}
onMouseEnter={(e) => { if (view !== DashboardViewType.Grid) { (e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-mid)'; } }}
onMouseLeave={(e) => { if (view !== DashboardViewType.Grid) { (e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-lo)'; } }}
onClick={() => handleViewChange(DashboardViewType.Grid)}
>
<Squares2X2Icon className={`w-4 h-4`} />
<span className={`sr-only`}>
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToGrid)}
</span>
<span className={`sr-only`}>{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToGrid)}</span>
</button>
<button
className={`flex items-center px-2 py-1 ${view === DashboardViewType.List ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
}`}
className={`${btnBase}`}
style={{ ...btnStyle(view === DashboardViewType.List), borderLeft: '1px solid var(--fm-border)', borderRight: '1px solid var(--fm-border)' }}
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}
type={`button`}
onMouseEnter={(e) => { if (view !== DashboardViewType.List) { (e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-mid)'; } }}
onMouseLeave={(e) => { if (view !== DashboardViewType.List) { (e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-lo)'; } }}
onClick={() => handleViewChange(DashboardViewType.List)}
>
<Bars4Icon className={`w-4 h-4`} />
<span className={`sr-only`}>
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}
</span>
<span className={`sr-only`}>{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}</span>
</button>
<button
className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.Structure ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
}`}
className={`${btnBase} rounded-r-[5px]`}
style={btnStyle(view === DashboardViewType.Structure)}
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}
type={`button`}
onMouseEnter={(e) => { if (view !== DashboardViewType.Structure) { (e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-mid)'; } }}
onMouseLeave={(e) => { if (view !== DashboardViewType.Structure) { (e.currentTarget as HTMLButtonElement).style.color = 'var(--fm-text-lo)'; } }}
onClick={() => handleViewChange(DashboardViewType.Structure)}
>
<FolderIcon className={`w-4 h-4`} />
<span className={`sr-only`}>
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}
</span>
<span className={`sr-only`}>{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}</span>
</button>
</div>
);
@@ -9,6 +9,7 @@ export interface ISponsorMsgProps {
beta: boolean | undefined;
version: VersionInfo | undefined;
isBacker: boolean | undefined;
topContent?: React.ReactNode;
}
interface ISponsorLinkProps {
@@ -19,7 +20,7 @@ interface ISponsorLinkProps {
const SponsorLink: React.FunctionComponent<ISponsorLinkProps> = ({ title, href, children }: React.PropsWithChildren<ISponsorLinkProps>) => {
return (
<a
className={`group inline-flex justify-center items-center space-x-2 opacity-50 hover:opacity-100 text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-textLink-foreground)]]`}
className={`group inline-flex items-center gap-2 opacity-60 hover:opacity-100 text-[var(--fm-text-lo)] hover:text-[var(--vscode-textLink-foreground)] transition-opacity duration-150`}
href={href}
title={title}
>
@@ -31,38 +32,46 @@ const SponsorLink: React.FunctionComponent<ISponsorLinkProps> = ({ title, href,
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({
beta,
isBacker,
topContent,
version
}: React.PropsWithChildren<ISponsorMsgProps>) => {
return (
<footer
className={`w-full px-4 py-2 text-center space-x-8 flex items-center border-t ${isBacker ? 'justify-center' : 'justify-between'
} bg-[var(--vscode-editor-background)] text-[var(--frontmatter-secondary-text)] border-[var(--frontmatter-border)]`}
>
{isBacker ? (
<span>
Front Matter
{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}
</span>
) : (
<>
<SponsorLink
title={l10n.t(LocalizationKey.dashboardLayoutSponsorSupportMsg)}
href={SPONSOR_LINK}>
<span>{l10n.t(LocalizationKey.commonSupport)}</span>{` `}
<HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
</SponsorLink>
<div className='w-full border-t bg-[var(--vscode-editor-background)] border-[var(--frontmatter-border)]'>
{topContent && (
<div className='px-4 py-2 border-b border-[var(--frontmatter-border)]'>
{topContent}
</div>
)}
<footer
className={`w-full px-4 py-2 text-xs sm:text-sm flex items-center gap-4 ${isBacker ? 'justify-center' : 'justify-between'} text-[var(--fm-text-xlo)]`}
>
{isBacker ? (
<span>
Front Matter
{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}
</span>
<SponsorLink
title={l10n.t(LocalizationKey.dashboardLayoutSponsorReviewMsg)}
href={REVIEW_LINK}>
<StarIcon className={`h-5 w-5 group-hover:fill-current`} />{` `}
<span>{l10n.t(LocalizationKey.dashboardLayoutSponsorReviewLabel)}</span>
</SponsorLink>
</>
)}
</footer>
) : (
<>
<SponsorLink
title={l10n.t(LocalizationKey.dashboardLayoutSponsorSupportMsg)}
href={SPONSOR_LINK}>
<span>{l10n.t(LocalizationKey.commonSupport)}</span>
<HeartIcon className={`h-4 w-4 group-hover:fill-current`} />
</SponsorLink>
<span className='truncate'>
Front Matter
{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}
</span>
<SponsorLink
title={l10n.t(LocalizationKey.dashboardLayoutSponsorReviewMsg)}
href={REVIEW_LINK}>
<StarIcon className={`h-4 w-4 group-hover:fill-current`} />
<span>{l10n.t(LocalizationKey.dashboardLayoutSponsorReviewLabel)}</span>
</SponsorLink>
</>
)}
</footer>
</div>
);
};
@@ -11,12 +11,14 @@ export interface ICustomActionsProps {
filePath: string;
scripts?: CustomScript[];
showTrigger?: boolean;
onMenuOpenChange?: (open: boolean) => void;
}
export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
filePath,
scripts,
showTrigger = false,
onMenuOpenChange,
}: React.PropsWithChildren<ICustomActionsProps>) => {
const customActions = React.useMemo(() => {
@@ -27,7 +29,7 @@ export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
key={script.title}
onClick={() => runCustomScript(script, filePath)}
>
<CommandLineIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<CommandLineIcon className="h-4 w-4" aria-hidden={true} />
<span>{script.title}</span>
</DropdownMenuItem>
));
@@ -39,7 +41,7 @@ export const CustomActions: React.FunctionComponent<ICustomActionsProps> = ({
if (showTrigger) {
return (
<DropdownMenu>
<DropdownMenu onOpenChange={onMenuOpenChange}>
<DropdownMenuTrigger
title={l10n.t(LocalizationKey.commonOpenCustomActions)}
className='px-2 text-[var(--frontmatter-secondary-text)] hover:text-[var(--frontmatter-button-hoverBackground)] focus-visible:outline-none'>
@@ -18,7 +18,6 @@ import { parseWinPath } from '../../../helpers/parseWinPath';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import useMediaFolder from '../../hooks/useMediaFolder';
import { RefreshDashboardData } from '../Header/RefreshDashboardData';
export interface IFolderCreationProps { }
@@ -91,7 +90,7 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
if (scripts.length > 0) {
return (
<div className="flex flex-1 justify-start space-x-2">
<div className="flex items-center gap-2">
{renderPostAssetsButton}
<ChoiceButton
@@ -104,14 +103,12 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
onClick={onFolderCreation}
disabled={!settings?.initialized}
/>
<RefreshDashboardData />
</div>
);
}
return (
<div className="flex flex-1 justify-start space-x-2">
<div className="flex items-center gap-2">
{renderPostAssetsButton}
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium focus:outline-none rounded text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}
@@ -121,8 +118,6 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
<FolderPlusIcon className={`mr-2 h-6 w-6`} />
<span className={``}>{l10n.t(LocalizationKey.dashboardMediaFolderCreationFolderCreate)}</span>
</button>
<RefreshDashboardData />
</div>
);
};
@@ -18,6 +18,7 @@ export interface IFooterActionsProps {
insertIntoArticle: () => void;
insertSnippet: () => void;
onDelete: () => void;
onMenuOpenChange?: (open: boolean) => void;
}
export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
@@ -29,30 +30,31 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
insertIntoArticle,
insertSnippet,
onDelete,
onMenuOpenChange,
}: React.PropsWithChildren<IFooterActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}>
<div className="w-full flex items-center justify-end gap-0.5 px-2 py-2 border-t border-[var(--fm-border)]">
<QuickAction
title={localize(LocalizationKey.dashboardMediaItemMenuItemView)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={() => setSelectedItemAction({
path: media.fsPath,
action: 'view'
})}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
<EyeIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
<span className='sr-only'>{localize(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
</QuickAction>
<QuickAction
title={localize(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={() => setSelectedItemAction({
path: media.fsPath,
action: 'edit'
})}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
<PencilIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
<span className='sr-only'>{localize(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</QuickAction>
@@ -64,18 +66,18 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
? localize(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.fieldName)
: localize(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={insertIntoArticle}
>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
<PlusIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
{viewData?.position && snippets.length > 0 && (
<QuickAction
title={localize(LocalizationKey.commonInsertSnippet)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={insertSnippet}>
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
<CodeBracketIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
)}
</>
@@ -85,9 +87,9 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
relPath && (
<QuickAction
title={localize(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={() => copyToClipboard(parseWinPath(relPath) || '')}>
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
<ClipboardIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
)
}
@@ -97,13 +99,14 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
<CustomActions
filePath={media.fsPath}
scripts={scripts}
showTrigger />
showTrigger
onMenuOpenChange={onMenuOpenChange} />
<QuickAction
title={localize(LocalizationKey.dashboardMediaItemQuickActionDelete)}
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-status-danger)]`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
<TrashIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
</QuickAction>
</div>
);
+60 -53
View File
@@ -33,6 +33,7 @@ import { Snippet } from '../../../models';
import useMediaInfo from '../../hooks/useMediaInfo';
import { ItemSelection } from '../Common/ItemSelection';
import { FooterActions } from './FooterActions';
import { cn } from '../../../utils/cn';
export interface IItemProps {
media: MediaInfo;
@@ -53,6 +54,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const { mediaFolder, mediaDetails, isAudio, isImage, isVideo } = useMediaInfo(media);
const [menuOpen, setMenuOpen] = React.useState(false);
const relPath = useMemo(() => {
if (viewData?.data?.pageBundle && viewData?.data?.filePath) {
@@ -268,61 +270,61 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
<>
<li className={`group flex flex-col relative shadow-md hover:shadow-xl dark:shadow-none border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`}>
<button
className={`group/button relative block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImage ? 'cursor-pointer' : 'cursor-default'} border-b border-[var(--frontmatter-border)]`}
onClick={hasViewData ? undefined : openLightbox}
>
<div
className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}
<li className={cn("group relative flex flex-col w-full text-left rounded-[9px] overflow-hidden border border-[var(--fm-border)] bg-[var(--fm-surface-2)] shadow-[0_1px_2px_rgba(0,0,0,.3)] hover:border-[var(--fm-border-hi)] hover:shadow-[0_8px_24px_rgba(0,0,0,.35)] transform-gpu hover:-translate-y-0.5 transition duration-150 ease-out", menuOpen && "border-[var(--fm-border-hi)] shadow-[0_8px_24px_rgba(0,0,0,.35)] -translate-y-0.5")}>
<div className="relative">
<button
className={`group/button relative block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImage ? 'cursor-pointer' : 'cursor-default'} border-b border-[var(--fm-border)]`}
onClick={hasViewData ? undefined : openLightbox}
>
{renderMediaIcon}
</div>
<div
className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center group-hover:brightness-75`}
>
{renderMedia}
</div>
<ItemSelection filePath={media.fsPath} />
{hasViewData && (
<div
className={`hidden group-hover/button:flex absolute top-0 right-0 bottom-0 left-0 items-center justify-center bg-black bg-opacity-70`}
className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}
>
{renderMediaIcon}
</div>
<div
className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center group-hover:brightness-75`}
>
{renderMedia}
</div>
<ItemSelection filePath={media.fsPath} />
{hasViewData && (
<div
className={`h-full ${showMediaSnippet ? 'w-1/3' : 'w-full'
} flex items-center justify-center`}
className={`hidden group-hover/button:flex absolute top-0 right-0 bottom-0 left-0 items-center justify-center bg-black bg-opacity-70`}
>
<button
title={l10n.t(LocalizationKey.dashboardMediaItemButtomInsertImage)}
className={`h-1/3 text-white hover:text-[var(--vscode-button-background)]`}
onClick={insertIntoArticle}
<div
className={`h-full ${showMediaSnippet ? 'w-1/3' : 'w-full'
} flex items-center justify-center`}
>
<PlusIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
</button>
</div>
{viewData?.data?.position && mediaSnippets.length > 0 && (
<div className={`h-full w-1/3 flex items-center justify-center`}>
<button
title={l10n.t(LocalizationKey.dashboardMediaItemButtomInsertSnippet)}
title={l10n.t(LocalizationKey.dashboardMediaItemButtomInsertImage)}
className={`h-1/3 text-white hover:text-[var(--vscode-button-background)]`}
onClick={insertSnippet}
onClick={insertIntoArticle}
>
<CodeBracketIcon
className={`w-full h-full hover:drop-shadow-md `}
aria-hidden="true"
/>
<PlusIcon className={`w-full h-full hover:drop-shadow-md `} aria-hidden="true" />
</button>
</div>
)}
{viewData?.data?.position && mediaSnippets.length > 0 && (
<div className={`h-full w-1/3 flex items-center justify-center`}>
<button
title={l10n.t(LocalizationKey.dashboardMediaItemButtomInsertSnippet)}
className={`h-1/3 text-white hover:text-[var(--vscode-button-background)]`}
onClick={insertSnippet}
>
<CodeBracketIcon
className={`w-full h-full hover:drop-shadow-md `}
aria-hidden="true"
/>
</button>
</div>
)}
<ItemSelection filePath={media.fsPath} />
</div>
)}
</button>
<ItemSelection filePath={media.fsPath} />
</div>
)}
</button>
<div className={`relative py-4 pl-4 pr-12 grow`}>
<ItemMenu
media={media}
relPath={relPath}
@@ -340,41 +342,45 @@ export const Item: React.FunctionComponent<IItemProps> = ({
action: 'view'
})}
processSnippet={processSnippet}
onDelete={() => setShowAlert(true)} />
onDelete={() => setShowAlert(true)}
onMenuOpenChange={setMenuOpen} />
</div>
<p className={`text-sm font-bold pointer-events-none flex items-center break-all text-[var(--frontmatter-text)]`}>
<div className="relative flex-1 px-3 pt-3 pb-2 min-h-0 overflow-hidden">
<p className="text-sm font-semibold pointer-events-none flex items-center break-all text-[var(--fm-text-hi)]">
{basename(parseWinPath(media.fsPath) || '')}
</p>
{!isImage && media.metadata.title && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-2 text-[var(--frontmatter-text)]`}>
<b className="mr-2 text-[var(--fm-text-mid)]">
{l10n.t(LocalizationKey.dashboardMediaCommonTitle)}:
</b>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>{media.metadata.title}</span>
<span className="block mt-1 text-xs text-[var(--fm-text-lo)]">{media.metadata.title}</span>
</p>
)}
{media.metadata.caption && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-2 text-[var(--frontmatter-text)]`}>
<b className="mr-2 text-[var(--fm-text-mid)]">
{l10n.t(LocalizationKey.dashboardMediaCommonCaption)}:
</b>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>{media.metadata.caption}</span>
<span className="block mt-1 text-xs text-[var(--fm-text-lo)]">{media.metadata.caption}</span>
</p>
)}
{!media.metadata.caption && media.metadata.alt && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-2 text-[var(--frontmatter-text)]`}>
<b className="mr-2 text-[var(--fm-text-mid)]">
{l10n.t(LocalizationKey.dashboardMediaCommonAlt)}:
</b>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>{media.metadata.alt}</span>
<span className="block mt-1 text-xs text-[var(--fm-text-lo)]">{media.metadata.alt}</span>
</p>
)}
{(media?.size || media?.dimensions) && (
<p className={`mt-2 text-xs font-medium pointer-events-none flex flex-col items-start`}>
<b className={`mr-1 text-[var(--frontmatter-text)]`}>
<b className="mr-1 text-[var(--fm-text-mid)]">
{l10n.t(LocalizationKey.dashboardMediaCommonSize)}:
</b>
<span className={`block mt-1 text-xs text-[var(--frontmatter-secondary-text)]`}>
<span className="block mt-1 text-xs text-[var(--fm-text-lo)]">
{mediaDetails}
</span>
</p>
@@ -389,7 +395,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
scripts={settings?.scripts}
insertIntoArticle={insertIntoArticle}
insertSnippet={insertSnippet}
onDelete={() => setShowAlert(true)} />
onDelete={() => setShowAlert(true)}
onMenuOpenChange={setMenuOpen} />
</li>
{showSnippetSelection && (
@@ -22,6 +22,7 @@ export interface IItemMenuProps {
showMediaDetails: () => void;
processSnippet: (snippet: Snippet) => void;
onDelete: () => void;
onMenuOpenChange?: (open: boolean) => void;
}
export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
@@ -36,6 +37,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
showMediaDetails,
processSnippet,
onDelete,
onMenuOpenChange,
}: React.PropsWithChildren<IItemMenuProps>) => {
const onCopyToClipboard = React.useCallback(() => {
@@ -50,23 +52,21 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
}, [selectedFolder]);
return (
<div className={`group/actions absolute top-4 right-4 flex flex-col space-y-4`}>
<div className={`flex items-center border border-transparent rounded-full p-1 -mr-2 -mt-1 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
<div className="relative z-10 flex text-left">
<DropdownMenu>
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'>
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<div className="absolute top-2 right-2 z-10 flex">
<DropdownMenu onOpenChange={onMenuOpenChange}>
<DropdownMenuTrigger className="relative flex h-8 w-8 items-center justify-center rounded-[8px] border border-[var(--fm-border)] bg-[var(--fm-surface-2)]/95 text-[var(--fm-text-lo)] backdrop-blur-sm transition-colors hover:bg-[var(--fm-surface-3)] hover:text-[var(--fm-text-hi)] data-[state=open]:bg-[var(--fm-surface-3)] data-[state=open]:text-[var(--fm-text-hi)] focus:outline-none">
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={showMediaDetails}>
<EyeIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<EyeIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonView)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={showUpdateMedia}>
<PencilIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<PencilIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</DropdownMenuItem>
@@ -74,7 +74,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
viewData?.filePath ? (
<>
<DropdownMenuItem onClick={insertIntoArticle}>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<PlusIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemInsertImage)}</span>
</DropdownMenuItem>
@@ -84,7 +84,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
snippets.map((snippet, idx) => (
<DropdownMenuItem key={idx} onClick={() => processSnippet(snippet)}>
<CodeBracketIcon
className="mr-2 h-4 w-4"
className="h-4 w-4"
aria-hidden={true}
/>
<span>{snippet.title}</span>
@@ -94,7 +94,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
</>
) : (
<DropdownMenuItem onClick={onCopyToClipboard}>
<ClipboardIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<ClipboardIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}</span>
</DropdownMenuItem>
)
@@ -105,18 +105,16 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
scripts={scripts} />
<DropdownMenuItem onClick={revealMedia}>
<EyeIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<EyeIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemRevealMedia)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onDelete} className={`focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]`}>
<TrashIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<TrashIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonDelete)}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
@@ -5,6 +5,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import {
LoadingAtom,
MediaFoldersAtom,
MediaTotalSelector,
PagedItems,
SelectedMediaFolderAtom,
SettingsSelector,
@@ -22,6 +23,7 @@ import { DashboardMessage } from '../../DashboardMessage';
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
import { FolderItem } from './FolderItem';
import useMedia from '../../hooks/useMedia';
import usePagination from '../../hooks/usePagination';
import { STATIC_FOLDER_PLACEHOLDER, } from '../../../constants';
import { PageLayout } from '../Layout/PageLayout';
import { parseWinPath } from '../../../helpers/parseWinPath';
@@ -32,6 +34,8 @@ import { LocalizationKey } from '../../../localization';
import { MediaItemPanel } from './MediaItemPanel';
import { FilesProvider } from '../../providers/FilesProvider';
import { SortOption } from '../../constants/SortOption';
import { Pagination } from '../Header/Pagination';
import { PaginationStatus } from '../Header/PaginationStatus';
export interface IMediaProps { }
@@ -44,6 +48,9 @@ export const Media: React.FunctionComponent<IMediaProps> = () => {
const loading = useRecoilValue(LoadingAtom);
const crntSorting = useRecoilValue(SortingAtom);
const [, setPagedItems] = useRecoilState(PagedItems);
const totalMedia = useRecoilValue(MediaTotalSelector);
const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination);
const showFooterPagination = totalMedia > pageSetNr;
const currentStaticFolder = useMemo(() => {
if (settings?.staticFolder) {
@@ -278,6 +285,12 @@ export const Media: React.FunctionComponent<IMediaProps> = () => {
beta={settings?.beta}
version={settings?.versionInfo}
isBacker={settings?.isBacker}
topContent={showFooterPagination ? (
<div className='flex items-center justify-between gap-2'>
<PaginationStatus />
<Pagination />
</div>
) : undefined}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=media" alt="Media metrics" />
@@ -4,7 +4,6 @@ import { NavigationType } from '../../models/NavigationType';
import { SettingsAtom } from '../../state';
import { Sorting } from '../Header';
import { Breadcrumb } from '../Header/Breadcrumb';
import { Pagination } from '../Header/Pagination';
export interface IMediaHeaderBottomProps { }
@@ -19,14 +18,12 @@ export const MediaHeaderBottom: React.FunctionComponent<IMediaHeaderBottomProps>
return (
<nav
className={`w-full flex justify-between py-2 border-b bg-[var(--vscode-sideBar-background)] text-[var(--vscode-sideBar-foreground)] border-[var(--frontmatter-border)]`}
className="overflow-x-auto px-4 py-1.5 flex items-center justify-between gap-3 border-b border-[var(--frontmatter-border)]"
aria-label="Breadcrumb"
>
<Breadcrumb />
<Pagination />
<div className={`flex px-5 flex-1 justify-end`}>
<div className="flex items-center gap-4 flex-shrink-0">
<Sorting view={NavigationType.Media} disableCustomSorting />
</div>
</nav>
@@ -8,16 +8,17 @@ import { DashboardCommand } from '../../DashboardCommand';
import { DashboardMessage } from '../../DashboardMessage';
import {
LoadingAtom,
MediaTotalSelector,
PageAtom,
SelectedMediaFolderSelector,
SettingsSelector,
SortingSelector
} from '../../state';
import { Searchbox } from '../Header';
import { PaginationStatus } from '../Header/PaginationStatus';
import { FolderCreation } from './FolderCreation';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { RefreshDashboardData } from '../Header/RefreshDashboardData';
export interface IMediaHeaderTopProps { }
@@ -30,6 +31,7 @@ export const MediaHeaderTop: React.FunctionComponent<
const [, setLoading] = useRecoilState(LoadingAtom);
const [page, setPage] = useRecoilState(PageAtom);
const settings = useRecoilValue(SettingsSelector);
const totalMedia = useRecoilValue(MediaTotalSelector);
const debounceGetMedia = useDebounce<string | null>(lastUpdated, 200);
const prevSelectedFolder = usePrevious<string | null>(selectedFolder);
@@ -81,14 +83,29 @@ export const MediaHeaderTop: React.FunctionComponent<
return (
<nav
className={`py-2 px-4 flex items-center justify-between border-b border-[var(--frontmatter-border)]`}
className="px-4 py-2 flex items-center justify-between gap-4"
aria-label="Pagination"
>
<FolderCreation />
<div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold leading-tight" style={{ color: 'var(--fm-text-hi)' }}>
{l10n.t(LocalizationKey.dashboardHeaderTabsMedia)}
</h1>
<div className="mt-0.5 flex items-center gap-2 flex-wrap" style={{ color: 'var(--fm-text-lo)' }}>
{totalMedia > 0 && (
<p className="text-xs leading-tight mt-0.5" style={{ fontFamily: 'var(--fm-mono)', color: 'var(--fm-text-lo)' }}>
{totalMedia} {totalMedia === 1 ? 'item' : 'items'}{` `}&middot;{` `}
</p>
)}
<PaginationStatus />
<RefreshDashboardData />
</div>
</div>
<Searchbox placeholder={l10n.t(LocalizationKey.dashboardMediaMediaHeaderTopSearchboxPlaceholder)} />
<div className="flex items-center gap-2 flex-shrink-0">
<Searchbox placeholder={l10n.t(LocalizationKey.dashboardMediaMediaHeaderTopSearchboxPlaceholder)} />
<FolderCreation />
</div>
</nav>
);
};
@@ -10,6 +10,12 @@ export interface IMenuButtonProps {
className?: string;
labelClass?: string;
buttonClass?: string;
/**
* When true: show "Label Value" in a bordered box with value in accent color.
* When false: show just "Label " in muted gray with no box.
* When undefined: legacy behavior ("Label: Value " split across two elements).
*/
isActive?: boolean;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
@@ -19,7 +25,48 @@ export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
className,
labelClass,
buttonClass,
isActive,
}: React.PropsWithChildren<IMenuButtonProps>) => {
if (isActive === true) {
return (
<DropdownMenuTrigger
className={cn(
'flex items-center shrink-0 rounded-md px-2 py-1 gap-1 text-sm focus:outline-none',
disabled && 'opacity-50',
className
)}
style={{
border: '1px solid var(--frontmatter-border)',
backgroundColor: 'var(--vscode-dropdown-background)'
}}
disabled={disabled}
>
<span className={cn('font-medium', labelClass)} style={{ color: 'var(--fm-text-lo)' }}>
{label}
</span>
<span className="font-medium" style={{ color: 'var(--fm-accent)' }}>{title}</span>
<ChevronDownIcon className="h-4 w-4 shrink-0" aria-hidden="true" style={{ color: 'var(--fm-text-lo)' }} />
</DropdownMenuTrigger>
);
}
if (isActive === false) {
return (
<DropdownMenuTrigger
className={cn(
'flex items-center shrink-0 gap-1 text-sm font-medium focus:outline-none hover:text-[var(--vscode-tab-activeForeground)]',
disabled && 'opacity-50',
className
)}
style={{ color: 'var(--vscode-tab-inactiveForeground)' }}
disabled={disabled}
>
<span className={cn(labelClass)}>{label}</span>
<ChevronDownIcon className="h-4 w-4 shrink-0" aria-hidden="true" />
</DropdownMenuTrigger>
);
}
return (
<div className={cn(`group flex items-center shrink-0 ${disabled ? 'opacity-50' : ''} ${className || ""}`)}>
<div className={cn(`mr-2 font-medium flex items-center text-[var(--vscode-tab-inactiveForeground)] ${labelClass || ""}`)}>
@@ -31,13 +31,13 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
}
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)] z-50`}>
<div className="w-full flex items-center justify-end gap-0.5 px-2 py-2 border-t border-[var(--fm-border)] z-50">
{insertEnabled && (
<QuickAction
title={l10n.t(LocalizationKey.commonInsertSnippet)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={onInsert}>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
<PlusIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.commonInsertSnippet)}</span>
</QuickAction>
)}
@@ -49,9 +49,9 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
onEdit && (
<QuickAction
title={l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionEditSnippet)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={onEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
<PencilIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionEditSnippet)}</span>
</QuickAction>
)
@@ -61,9 +61,9 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
onDelete && (
<QuickAction
title={l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionDeleteSnippet)}
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-status-danger)]`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
<TrashIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionDeleteSnippet)}</span>
</QuickAction>
)
@@ -72,9 +72,9 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
) : (
<QuickAction
title={l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionViewSnippet)}
className={`text-[var(--frontmatter-secondary-text)]`}
className={`text-[var(--fm-text-lo)] hover:text-[var(--fm-text-mid)]`}
onClick={showFile}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
<EyeIcon className={`w-3.5 h-3.5`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionViewSnippet)}</span>
</QuickAction>
)
@@ -18,6 +18,7 @@ import SnippetForm, { SnippetFormHandle } from './SnippetForm';
import { LocalizationKey } from '../../../localization';
import { FooterActions } from './FooterActions';
import { ItemMenu } from './ItemMenu';
import { cn } from '../../../utils/cn';
import { SlideOver } from '../Modals/SlideOver';
import { DEFAULT_DASHBOARD_FEATURE_FLAGS } from '../../../constants/DefaultFeatureFlags';
@@ -36,6 +37,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
const [showInsertDialog, setShowInsertDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [showAlert, setShowAlert] = React.useState(false);
const [menuOpen, setMenuOpen] = React.useState(false);
const [snippetTitle, setSnippetTitle] = useState<string>('');
const [snippetDescription, setSnippetDescription] = useState<string>('');
@@ -152,10 +154,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
<>
<li className={`group flex flex-col relative overflow-hidden shadow-md hover:shadow-xl dark:shadow-none border space-y-2 rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] border-[var(--frontmatter-border)]`}>
<div className='p-4 grow space-y-2'>
<li className={cn("group relative flex flex-col w-full text-left rounded-[9px] overflow-hidden border border-[var(--fm-border)] bg-[var(--fm-surface-2)] shadow-[0_1px_2px_rgba(0,0,0,.3)] hover:border-[var(--fm-border-hi)] hover:shadow-[0_8px_24px_rgba(0,0,0,.35)] transform-gpu hover:-translate-y-0.5 transition duration-150 ease-out", menuOpen && "border-[var(--fm-border-hi)] shadow-[0_8px_24px_rgba(0,0,0,.35)] -translate-y-0.5")}>
<div className="relative flex-1 px-3 pt-3 pb-2 min-h-0 space-y-2 overflow-hidden">
<h2
className="font-bold flex items-center"
className="text-[15px] font-semibold leading-snug text-[var(--fm-text-hi)] flex items-center"
title={snippet.isMediaSnippet ? 'Media snippet' : 'Content snippet'}
>
<CodeBracketIcon className="w-5 h-5 mr-2" aria-hidden={true} />
@@ -170,7 +172,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
<ItemMenu
insertEnabled={!!(insertToContent && !snippet.isMediaSnippet)}
sourcePath={snippet.sourcePath}
onInsert={() => setShowInsertDialog(true)} />
onInsert={() => setShowInsertDialog(true)}
onMenuOpenChange={setMenuOpen} />
}
>
<ItemMenu
@@ -178,16 +181,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({
sourcePath={snippet.sourcePath}
onEdit={onOpenEdit}
onInsert={() => setShowInsertDialog(true)}
onDelete={() => setShowAlert(true)} />
onDelete={() => setShowAlert(true)}
onMenuOpenChange={setMenuOpen} />
</FeatureFlag>
<div className='inline-block mr-1 mt-1 text-xs text-[var(--vscode-button-secondaryForeground)] bg-[var(--vscode-button-secondaryBackground)] border border-[var(--frontmatter-border)] rounded px-1 py-0.5'>
<div className="inline-flex mr-1 mt-1 text-[0.65rem] text-[var(--fm-text-lo)] bg-[var(--fm-surface-3)] border border-[var(--fm-border)] rounded-[6px] px-1.5 py-0.5">
{
snippet.isMediaSnippet ? l10n.t(LocalizationKey.dashboardSnippetsViewItemTypeMedia) : l10n.t(LocalizationKey.dashboardSnippetsViewItemTypeContent)
}
</div>
<p className={`text-xs text-[var(--frontmatter-text)]`}>{snippet.description}</p>
<p className="text-xs text-[var(--fm-text-mid)]">{snippet.description}</p>
</div>
<FeatureFlag
@@ -13,6 +13,7 @@ export interface IItemMenuProps {
onEdit?: () => void;
onInsert: () => void;
onDelete?: () => void;
onMenuOpenChange?: (open: boolean) => void;
}
export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
@@ -21,6 +22,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
onEdit,
onInsert,
onDelete,
onMenuOpenChange,
}: React.PropsWithChildren<IItemMenuProps>) => {
const showFile = useCallback(() => {
@@ -32,22 +34,20 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
}
return (
<div className={`group/actions absolute top-4 right-4 flex flex-col space-y-4`}>
<div className={`flex items-center border border-transparent rounded-full p-1 -mr-2 -mt-3 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
<div className="relative z-10 flex text-left">
<DropdownMenu>
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'>
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<div className="absolute top-2 right-2 z-10 flex">
<DropdownMenu onOpenChange={onMenuOpenChange}>
<DropdownMenuTrigger className="relative flex h-8 w-8 items-center justify-center rounded-[8px] border border-[var(--fm-border)] bg-[var(--fm-surface-2)]/95 text-[var(--fm-text-lo)] backdrop-blur-sm transition-colors hover:bg-[var(--fm-surface-3)] hover:text-[var(--fm-text-hi)] data-[state=open]:bg-[var(--fm-surface-3)] data-[state=open]:text-[var(--fm-text-hi)] focus:outline-none">
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end">
{
insertEnabled && (
<DropdownMenuItem
title={l10n.t(LocalizationKey.commonInsertSnippet)}
onClick={onInsert}>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<PlusIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonInsertSnippet)}</span>
</DropdownMenuItem>
)
@@ -61,7 +61,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
<DropdownMenuItem
title={l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionEditSnippet)}
onClick={onEdit}>
<PencilIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<PencilIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionEditSnippet)}</span>
</DropdownMenuItem>
)
@@ -73,7 +73,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
title={l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionDeleteSnippet)}
onClick={onDelete}
className={`focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]`}>
<TrashIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<TrashIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionDeleteSnippet)}</span>
</DropdownMenuItem>
)
@@ -83,15 +83,13 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
<DropdownMenuItem
title={l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionViewSnippet)}
onClick={showFile}>
<EyeIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<EyeIcon className="h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardSnippetsViewItemQuickActionViewSnippet)}</span>
</DropdownMenuItem>
)
}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
@@ -94,25 +94,36 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = () => {
features={mode?.features || DEFAULT_DASHBOARD_FEATURE_FLAGS}
flag={FEATURE_FLAG.dashboard.snippets.manage}>
<div
className={`py-3 px-4 flex items-center justify-between border-b border-[var(--frontmatter-border)]`}
className={`px-4 py-2 flex items-center justify-between gap-4 border-b border-[var(--frontmatter-border)]`}
aria-label={l10n.t(LocalizationKey.dashboardSnippetsViewSnippetsAriaLabel)}
>
<FilterInput
placeholder={l10n.t(LocalizationKey.commonSearch)}
isReady={true}
autoFocus={(snippetKeys && snippetKeys.length > 0)}
value={snippetFilter}
onChange={(value: string) => setSnippetFilter(value)}
onReset={() => setSnippetFilter('')}
/>
<div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold leading-tight" style={{ color: 'var(--fm-text-hi)' }}>
{l10n.t(LocalizationKey.dashboardHeaderTabsSnippets)}
</h1>
{snippetKeys.length > 0 && (
<p className="text-xs leading-tight mt-0.5" style={{ fontFamily: 'var(--fm-mono)', color: 'var(--fm-text-lo)' }}>
{snippetKeys.length} {snippetKeys.length === 1 ? 'item' : 'items'}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<FilterInput
placeholder={l10n.t(LocalizationKey.commonSearch)}
isReady={true}
autoFocus={(snippetKeys && snippetKeys.length > 0)}
value={snippetFilter}
onChange={(value: string) => setSnippetFilter(value)}
onReset={() => setSnippetFilter('')}
/>
<div className="flex flex-1 justify-end">
<button
className={`inline-flex items-center px-3 py-1 rounded text-xs leading-4 font-medium focus:outline-none text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}
className={`inline-flex items-center px-3 py-2 rounded text-sm font-medium focus:outline-none text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}
title={l10n.t(LocalizationKey.dashboardSnippetsViewSnippetsButtonCreate)}
onClick={() => setShowCreateDialog(true)}
>
<PlusIcon className={`mr-2 h-6 w-6`} />
<PlusIcon className={`mr-2 h-4 w-4`} />
<span className={`text-sm`}>
{l10n.t(LocalizationKey.dashboardSnippetsViewSnippetsButtonCreate)}
</span>
+3
View File
@@ -17,6 +17,7 @@ import {
TabSelector,
TagSelector
} from '../state';
import { LastSyncAtom } from '../state';
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../DashboardMessage';
import { EventData } from '@estruyf/vscode/dist/models';
@@ -30,6 +31,7 @@ import { usePrevious } from '../../panelWebView/hooks/usePrevious';
export default function usePages(pages: Page[]) {
const [sortedPages, setSortedPages] = useState<Page[]>([]);
const [pageItems, setPageItems] = useRecoilState(AllPagesAtom);
const [, setLastSync] = useRecoilState(LastSyncAtom);
const [sorting, setSorting] = useRecoilState(SortingAtom);
const [tabInfo, setTabInfo] = useRecoilState(TabInfoAtom);
const [locales, setLocales] = useRecoilState(LocalesAtom);
@@ -226,6 +228,7 @@ export default function usePages(pages: Page[]) {
// Set the pages
setPageItems(crntPages);
setLastSync(Date.now());
},
[tab, tabInfo, settings, filters, locales, tabPrevious]
);
@@ -0,0 +1,37 @@
import { useState, useEffect } from 'react';
/**
* Returns a human-readable relative time string (e.g. "2m ago", "just now")
* that updates every 30 seconds.
*/
export function useRelativeTime(timestamp: number | null): string {
const [label, setLabel] = useState<string>('');
useEffect(() => {
if (timestamp === null) {
setLabel('');
return;
}
const compute = () => {
const diff = Math.floor((Date.now() - timestamp) / 1000);
if (diff < 10) {
setLabel('just now');
} else if (diff < 60) {
setLabel(`${diff}s ago`);
} else if (diff < 3600) {
setLabel(`${Math.floor(diff / 60)}m ago`);
} else if (diff < 86400) {
setLabel(`${Math.floor(diff / 3600)}h ago`);
} else {
setLabel(`${Math.floor(diff / 86400)}d ago`);
}
};
compute();
const id = setInterval(compute, 30_000);
return () => clearInterval(id);
}, [timestamp]);
return label;
}
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const LastSyncAtom = atom<number | null>({
key: 'LastSyncAtom',
default: null
});
+1
View File
@@ -8,6 +8,7 @@ export * from './FiltersAtom';
export * from './FolderAtom';
export * from './GroupingAtom';
export * from './LightboxAtom';
export * from './LastSyncAtom';
export * from './LoadingAtom';
export * from './LocaleAtom';
export * from './LocalesAtom';
+81
View File
@@ -3,6 +3,87 @@
@import 'tailwindcss/utilities';
:root {
/* FM accent tokens — driven by VS Code button color, never hardcoded */
--fm-accent: var(--vscode-button-background);
--fm-accent-ink: var(--vscode-button-foreground);
--fm-accent-hi: color-mix(in srgb, var(--fm-accent) 80%, white);
--fm-accent-soft: color-mix(in srgb, var(--fm-accent) 14%, transparent);
--fm-accent-line: color-mix(in srgb, var(--fm-accent) 32%, transparent);
/* Theme-driven surfaces */
--fm-bg: var(--vscode-editor-background);
--fm-surface-1: color-mix(
in srgb,
var(--vscode-editor-background) 88%,
var(--vscode-sideBar-background) 12%
);
--fm-surface-2: color-mix(
in srgb,
var(--vscode-editor-background) 76%,
var(--vscode-sideBar-background) 24%
);
--fm-surface-3: color-mix(
in srgb,
var(--vscode-editor-background) 62%,
var(--vscode-sideBar-background) 38%
);
--fm-surface-4: color-mix(
in srgb,
var(--vscode-editor-background) 48%,
var(--vscode-sideBar-background) 52%
);
/* When sidebar and editor share the same color the sidebar-mix above produces zero contrast.
Override per theme class to guarantee visible card lift using white (dark) or black (light). */
.vscode-dark {
--fm-surface-1: color-mix(in srgb, var(--vscode-sideBar-background) 96%, white);
--fm-surface-2: color-mix(in srgb, var(--vscode-sideBar-background) 90%, white);
--fm-surface-3: color-mix(in srgb, var(--vscode-sideBar-background) 84%, white);
--fm-surface-4: color-mix(in srgb, var(--vscode-sideBar-background) 78%, white);
}
.vscode-light {
--fm-surface-1: color-mix(in srgb, var(--vscode-editor-background) 97%, black);
--fm-surface-2: color-mix(in srgb, var(--vscode-editor-background) 93%, black);
--fm-surface-3: color-mix(in srgb, var(--vscode-editor-background) 89%, black);
--fm-surface-4: color-mix(in srgb, var(--vscode-editor-background) 85%, black);
}
/* Theme-driven borders */
--fm-border: var(--vscode-panel-border, var(--vscode-sideBar-border, rgba(127, 127, 127, 0.25)));
--fm-border-soft: color-mix(in srgb, var(--fm-border) 70%, transparent);
--fm-border-hi: color-mix(
in srgb,
var(--vscode-focusBorder, var(--fm-border)) 30%,
var(--fm-border) 70%
);
/* Theme-driven text hierarchy */
--fm-text-hi: var(--vscode-editor-foreground);
--fm-text-mid: color-mix(
in srgb,
var(--vscode-editor-foreground) 72%,
var(--vscode-descriptionForeground, var(--vscode-editor-foreground)) 28%
);
--fm-text-lo: var(
--vscode-descriptionForeground,
color-mix(in srgb, var(--vscode-editor-foreground) 55%, transparent)
);
--fm-text-xlo: color-mix(
in srgb,
var(--vscode-descriptionForeground, var(--vscode-editor-foreground)) 72%,
transparent
);
/* Theme-driven status colors */
--fm-status-published: var(--vscode-terminal-ansiGreen, #59b888);
--fm-status-draft: var(--vscode-terminal-ansiYellow, #c79a4b);
--fm-status-scheduled: var(--vscode-terminal-ansiBlue, #6b9fd4);
--fm-status-danger: var(--vscode-terminal-ansiRed, #e06c75);
/* Mono stack */
--fm-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, Consolas, monospace;
/* Bool field */
--frontmatter-toggle-background: #15c2cb;
--frontmatter-toggle-secondaryBackground: #adadad;
+1
View File
@@ -12,6 +12,7 @@ import { OpenOnWebsiteAction } from './Actions/OpenOnWebsiteAction';
import useContentType from '../../hooks/useContentType';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { CommandToCode } from '../CommandToCode';
import { PlusIcon } from '@heroicons/react/24/outline';
export interface IActionsProps {
metadata?: any;