mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-06-21 02:24:43 +02:00
feat: Enhance Content, Media, and Snippets dashboards
This commit is contained in:
@@ -13,3 +13,5 @@ e2e/sample
|
||||
localization.log
|
||||
localization.md
|
||||
.env
|
||||
|
||||
.claude
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 ? <> · synced {syncLabel}</> : null}
|
||||
{` `}·{` `}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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'}{` `}·{` `}
|
||||
</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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user