diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ae38d015b..b8249e7b8 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -2,6 +2,7 @@ version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
+ target-branch: "develop"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 31818015b..1dd90deb6 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -17,7 +17,7 @@
"@orpc/client": "^1.14.4",
"@solid-imager/client": "workspace:*",
"@solid-imager/core": "workspace:*",
- "incur": "^0.4.8",
+ "incur": "^0.5.1",
"zod": "^4.4.3"
},
"devDependencies": {
diff --git a/apps/server/package.json b/apps/server/package.json
index 5621a2a26..bf832c119 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -35,8 +35,8 @@
"docs:generate": "typedoc"
},
"dependencies": {
- "@electric-sql/pglite": "0.5.3",
- "@electric-sql/pglite-pgvector": "0.0.4",
+ "@electric-sql/pglite": "0.5.5",
+ "@electric-sql/pglite-pgvector": "0.0.6",
"@orpc/client": "^1.14.4",
"@orpc/contract": "^1.14.4",
"@orpc/experimental-pino": "^1.14.4",
@@ -79,7 +79,7 @@
"@orpc/openapi": "^1.14.4",
"@playwright/test": "^1.60.0",
"@tailwindcss/vite": "^4.3.0",
- "@tanstack/devtools-vite": "^0.7.0",
+ "@tanstack/devtools-vite": "^0.8.3",
"@types/archiver": "^8.0.0",
"@types/ffmpeg-static": "^5.1.0",
"@types/fluent-ffmpeg": "^2.1.28",
diff --git a/apps/server/src/components/media/legacy-media-grid-item.tsx b/apps/server/src/components/media/legacy-media-grid-item.tsx
index 7d0c357a9..807190c66 100644
--- a/apps/server/src/components/media/legacy-media-grid-item.tsx
+++ b/apps/server/src/components/media/legacy-media-grid-item.tsx
@@ -16,10 +16,12 @@ export type ServerMediaGridItemProps = {
priority?: boolean;
sourceRootPath?: string;
isBulkSelectMode?: boolean;
+ isPreviewSelected?: boolean;
isSelected?: boolean;
onToggleSelect?: () => void;
onPrepareMediaDetail?: () => void;
onPreviewSelect?: () => void;
+ onSelectGesture?: (event: MouseEvent | KeyboardEvent) => void;
};
export function LegacyMediaGridItem(props: ServerMediaGridItemProps) {
diff --git a/apps/server/src/components/media/v2-media-actions.tsx b/apps/server/src/components/media/v2-media-actions.tsx
index 1ec14510b..3f8b651f6 100644
--- a/apps/server/src/components/media/v2-media-actions.tsx
+++ b/apps/server/src/components/media/v2-media-actions.tsx
@@ -1,5 +1,14 @@
import type { MediaDetails } from "@solid-imager/core/domain/media/schemas";
import { getErrorMessage } from "@solid-imager/core/utils";
+import {
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@solid-imager/ui/alert-dialog";
import { Button } from "@solid-imager/ui/button";
import {
Popover,
@@ -11,9 +20,11 @@ import { toast } from "@solid-imager/ui/toast";
import {
Binary,
ChevronDown,
+ Download,
Scan,
ScanSearch,
Sparkles,
+ Trash2,
} from "@solid-imager/ui/v2/icons";
import { useNavigate } from "@tanstack/solid-router";
import { createEffect, createSignal, on, onCleanup } from "solid-js";
@@ -25,6 +36,7 @@ import {
getCcipVectorStatus,
startCcipExtraction,
} from "~/infrastructure/api-clients/ai-api";
+import { deleteMedia } from "~/infrastructure/api-clients/media-api";
type MediaActionsProps = {
media: MediaDetails;
@@ -41,6 +53,7 @@ export function V2MediaActions(props: MediaActionsProps) {
createSignal(false);
const [isCharacterCropModalOpen, setIsCharacterCropModalOpen] =
createSignal(false);
+ const [moreActionsOpen, setMoreActionsOpen] = createSignal(false);
const [ccipStatus, setCcipStatus] = createSignal<
"missing" | "processing" | "ready" | "stale" | "failed"
>("missing");
@@ -49,6 +62,8 @@ export function V2MediaActions(props: MediaActionsProps) {
);
const [isCcipJobPending, setIsCcipJobPending] = createSignal(false);
const [isExtractingCcip, setIsExtractingCcip] = createSignal(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = createSignal(false);
+ const [isDeleting, setIsDeleting] = createSignal(false);
const [ccipMissingStatusCount, setCcipMissingStatusCount] = createSignal(0);
let ccipStatusRequestId = 0;
@@ -101,6 +116,7 @@ export function V2MediaActions(props: MediaActionsProps) {
);
const handleCcipExtraction = async () => {
+ setMoreActionsOpen(false);
setIsExtractingCcip(true);
const currentMediaId = props.media.id;
const currentMediaSourceId = props.media.mediaSourceId;
@@ -168,6 +184,31 @@ export function V2MediaActions(props: MediaActionsProps) {
activateVectorSearch(props.media.id, { surface: "v2" });
void navigate({ to: "/v2/search" });
};
+ const handleDownload = () => {
+ setMoreActionsOpen(false);
+ const anchor = document.createElement("a");
+ anchor.href = `/api/sources/${encodeURIComponent(props.media.mediaSourceId)}/${encodeURIComponent(props.media.id)}`;
+ anchor.download = props.media.fileName;
+ anchor.click();
+ };
+ const handleDelete = async () => {
+ if (isDeleting()) return;
+ setIsDeleting(true);
+ try {
+ await deleteMedia(props.media.mediaSourceId, props.media.id);
+ setIsDeleteDialogOpen(false);
+ toast.success("Media deleted");
+ await navigate({
+ params: { mediaSourceId: props.media.mediaSourceId },
+ replace: true,
+ to: "/v2/sources/$mediaSourceId",
+ });
+ } catch (error) {
+ toast.error(`Failed to delete media: ${getErrorMessage(error)}`);
+ } finally {
+ setIsDeleting(false);
+ }
+ };
const ccipActionLabel = () => {
if (ccipStatus() === "ready" || ccipStatus() === "stale") {
return "Re-extract CCIP vector";
@@ -202,7 +243,11 @@ export function V2MediaActions(props: MediaActionsProps) {
Find similar
-
+
More actions
@@ -242,6 +287,25 @@ export function V2MediaActions(props: MediaActionsProps) {
>
CCIP status: {ccipStatus()}
+
+
+
@@ -264,6 +328,32 @@ export function V2MediaActions(props: MediaActionsProps) {
media={props.media}
onClose={() => setIsCharacterCropModalOpen(false)}
/>
+
+
+
+ Delete this media?
+
+ {props.media.fileName} will be permanently removed from its
+ source. This action cannot be undone.
+
+
+
+
+ Cancel
+
+
+
+
+
>
);
}
diff --git a/apps/server/src/components/media/v2-media-grid-item.tsx b/apps/server/src/components/media/v2-media-grid-item.tsx
index 478211bb7..9238edce2 100644
--- a/apps/server/src/components/media/v2-media-grid-item.tsx
+++ b/apps/server/src/components/media/v2-media-grid-item.tsx
@@ -3,7 +3,7 @@ import {
type MediaGridLinkProps,
V2MediaGridItem as SharedV2MediaGridItem,
} from "@solid-imager/ui/v2-media-grid-item";
-import { Link, useLocation } from "@tanstack/solid-router";
+import { Link } from "@tanstack/solid-router";
import { Show } from "solid-js";
import type { ServerMediaGridItemProps } from "./legacy-media-grid-item";
import { ThumbnailImage } from "./thumbnail-image";
@@ -13,35 +13,82 @@ export type V2ServerMediaGridItemProps = Omit<
"imageLoadPolicy"
> & {
imageLoadPolicy?: MediaGridImageLoadPolicy;
+ onOpenMediaDetail?: () => void;
};
export function V2MediaGridItem(props: V2ServerMediaGridItemProps) {
- const location = useLocation();
+ const isPlainPrimaryClick = (event: MouseEvent) =>
+ event.button === 0 &&
+ !event.metaKey &&
+ !event.ctrlKey &&
+ !event.shiftKey &&
+ !event.altKey;
+ const hasFinePointer = () => window.matchMedia("(pointer: fine)").matches;
+ const isModifiedSelectionClick = (event: MouseEvent) =>
+ event.button === 0 &&
+ !event.altKey &&
+ (event.metaKey || event.ctrlKey || event.shiftKey);
+
const detailLink = (linkProps: MediaGridLinkProps) => (
{
if (
- props.onPreviewSelect &&
- window.matchMedia("(min-width: 1536px)").matches
+ props.onSelectGesture &&
+ hasFinePointer() &&
+ isModifiedSelectionClick(event)
) {
+ event.preventDefault();
+ props.onSelectGesture(event);
+ return;
+ }
+ if (!isPlainPrimaryClick(event)) return;
+
+ if (props.onPreviewSelect && hasFinePointer()) {
event.preventDefault();
props.onPreviewSelect();
return;
}
+
+ props.onPrepareMediaDetail?.();
+ }}
+ onContextMenu={linkProps.onContextMenu}
+ onDblClick={(event: MouseEvent) => {
if (
- event.button === 0 &&
+ !isPlainPrimaryClick(event) ||
+ !hasFinePointer() ||
+ !props.onOpenMediaDetail
+ ) {
+ return;
+ }
+
+ event.preventDefault();
+ props.onPrepareMediaDetail?.();
+ props.onOpenMediaDetail();
+ }}
+ onKeyDown={(event: KeyboardEvent) => {
+ if (
+ event.key === "Enter" &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey &&
- !event.altKey
+ !event.altKey &&
+ props.onOpenMediaDetail
) {
+ event.preventDefault();
props.onPrepareMediaDetail?.();
- sessionStorage.setItem("v2:media-return", location().href);
+ props.onOpenMediaDetail();
+ return;
+ }
+
+ if (event.key === " " && props.onPreviewSelect) {
+ event.preventDefault();
+ props.onPreviewSelect();
}
}}
- onContextMenu={linkProps.onContextMenu}
params={{
mediaId: props.media.id,
mediaSourceId: props.media.mediaSourceId,
@@ -55,6 +102,7 @@ export function V2MediaGridItem(props: V2ServerMediaGridItemProps) {
return (
(
@@ -65,6 +113,10 @@ export function V2MediaGridItem(props: V2ServerMediaGridItemProps) {
data-media-id={linkProps["data-media-id"]}
onClick={(event) => {
event.preventDefault();
+ if (props.onSelectGesture && isModifiedSelectionClick(event)) {
+ props.onSelectGesture(event);
+ return;
+ }
props.onToggleSelect?.();
}}
onContextMenu={linkProps.onContextMenu}
diff --git a/apps/server/src/components/v2/v2-app-shell.tsx b/apps/server/src/components/v2/v2-app-shell.tsx
index 61afa272e..0f9e79bba 100644
--- a/apps/server/src/components/v2/v2-app-shell.tsx
+++ b/apps/server/src/components/v2/v2-app-shell.tsx
@@ -12,11 +12,13 @@ import {
} from "@solid-imager/ui/dialog";
import type { RawEventHandler } from "@solid-imager/ui/hooks/use-sources-events";
import { useSourcesPage } from "@solid-imager/ui/hooks/use-sources-page";
+import { createAppShortcut } from "@solid-imager/ui/shortcuts/index";
import { SourceDeleteModal } from "@solid-imager/ui/source-delete-modal";
import { V2SourceFormModal } from "@solid-imager/ui/v2-source-form-modal";
import { createQuery, useQueryClient } from "@tanstack/solid-query";
+import { useNavigate } from "@tanstack/solid-router";
import type { JSX, ParentProps } from "solid-js";
-import { createSignal } from "solid-js";
+import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
import { createServerTransport } from "~/hooks/use-media-source-events";
import { mediaSourcesQueryOptions } from "~/infrastructure/api-clients/queries";
import {
@@ -25,6 +27,7 @@ import {
syncMediaSources,
updateMediaSource,
} from "~/infrastructure/api-clients/sources-api";
+import { V2CommandCenter } from "./v2-command-center";
import { V2MobileHeader } from "./v2-mobile-header";
import { V2Sidebar, type V2SidebarProps } from "./v2-sidebar";
@@ -32,8 +35,11 @@ type V2AppShellProps = ParentProps<{
statusIndicator?: JSX.Element;
}>;
+const SIDEBAR_PREFERENCE_KEY = "solid-imager:v2-sidebar-expanded";
+
export function V2AppShell(props: V2AppShellProps) {
const queryClient = useQueryClient();
+ const navigate = useNavigate();
const mediaSources = createQuery(mediaSourcesQueryOptions);
const sourceEventTransport = createServerTransport(() => "*", {
onResumeFromIdle: () => {
@@ -45,7 +51,32 @@ export function V2AppShell(props: V2AppShellProps) {
const registerSourceEvents = (handler: RawEventHandler) =>
sourceEventTransport.listen(handler);
const [sidebarExpanded, setSidebarExpanded] = createSignal(true);
+ const [sidebarPreferenceReady, setSidebarPreferenceReady] =
+ createSignal(false);
const [mobileMenuOpen, setMobileMenuOpen] = createSignal(false);
+ const [commandPaletteOpen, setCommandPaletteOpen] = createSignal(false);
+ const [shortcutHelpOpen, setShortcutHelpOpen] = createSignal(false);
+ let commandPaletteReturnFocus: HTMLElement | null = null;
+ let focusRestoreFrame: number | undefined;
+ const updateCommandPaletteOpen = (open: boolean) => {
+ if (open && !commandPaletteOpen()) {
+ commandPaletteReturnFocus =
+ document.activeElement instanceof HTMLElement
+ ? document.activeElement
+ : null;
+ }
+ setCommandPaletteOpen(open);
+ if (open || !commandPaletteReturnFocus) return;
+ const returnFocus = commandPaletteReturnFocus;
+ commandPaletteReturnFocus = null;
+ if (focusRestoreFrame !== undefined) {
+ cancelAnimationFrame(focusRestoreFrame);
+ }
+ focusRestoreFrame = requestAnimationFrame(() => {
+ focusRestoreFrame = undefined;
+ if (returnFocus.isConnected) returnFocus.focus({ preventScroll: true });
+ });
+ };
const sourceData = () => mediaSources.data ?? [];
const sourcePage = useSourcesPage({
actions: {
@@ -69,8 +100,45 @@ export function V2AppShell(props: V2AppShellProps) {
onCollapseToggle: () => setSidebarExpanded((expanded) => !expanded),
onDeleteSource: sourcePage.handleDeleteSource,
onEditSource: sourcePage.handleEditSource,
+ onOpenCommandPalette: () => updateCommandPaletteOpen(true),
onSyncSource: (source) => void sourcePage.handleSyncSource(source),
});
+ createAppShortcut(
+ "commandPalette",
+ () => updateCommandPaletteOpen(!commandPaletteOpen()),
+ { ignoreInputs: false },
+ );
+ createAppShortcut("shortcutHelp", () => setShortcutHelpOpen(true));
+ createAppShortcut("toggleSidebar", () =>
+ setSidebarExpanded((expanded) => !expanded),
+ );
+ createAppShortcut("goLibrary", () => {
+ void navigate({ to: "/v2/search" });
+ });
+ createAppShortcut("goManager", () => {
+ void navigate({ to: "/v2/manager" });
+ });
+ createAppShortcut("goJobs", () => {
+ void navigate({ to: "/v2/jobs" });
+ });
+ createAppShortcut("goSettings", () => {
+ void navigate({ to: "/v2/config" });
+ });
+ onMount(() => {
+ const storedPreference = localStorage.getItem(SIDEBAR_PREFERENCE_KEY);
+ if (storedPreference === "false") setSidebarExpanded(false);
+ if (storedPreference === "true") setSidebarExpanded(true);
+ setSidebarPreferenceReady(true);
+ });
+ onCleanup(() => {
+ if (focusRestoreFrame !== undefined) {
+ cancelAnimationFrame(focusRestoreFrame);
+ }
+ });
+ createEffect(() => {
+ if (!sidebarPreferenceReady()) return;
+ localStorage.setItem(SIDEBAR_PREFERENCE_KEY, String(sidebarExpanded()));
+ });
return (
- setMobileMenuOpen(true)} />
+ updateCommandPaletteOpen(true)}
+ onOpenMenu={() => setMobileMenuOpen(true)}
+ />
{props.statusIndicator}
+ setSidebarExpanded((expanded) => !expanded)}
+ paletteOpen={commandPaletteOpen()}
+ />
);
}
diff --git a/apps/server/src/components/v2/v2-command-center.tsx b/apps/server/src/components/v2/v2-command-center.tsx
new file mode 100644
index 000000000..9679eb50a
--- /dev/null
+++ b/apps/server/src/components/v2/v2-command-center.tsx
@@ -0,0 +1,333 @@
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@solid-imager/ui/dialog";
+import {
+ getShortcutDefinitionsForGroup,
+ SHORTCUT_GROUPS,
+ type ShortcutId,
+ ShortcutKbd,
+} from "@solid-imager/ui/shortcuts/index";
+import {
+ BriefcaseBusiness,
+ CircleHelp,
+ Clock3,
+ Library,
+ PanelLeftClose,
+ Plus,
+ Search,
+ Settings,
+} from "@solid-imager/ui/v2/icons";
+import { useNavigate } from "@tanstack/solid-router";
+import {
+ type Component,
+ createEffect,
+ createMemo,
+ createSignal,
+ createUniqueId,
+ For,
+ Show,
+} from "solid-js";
+
+export type V2CommandCenterProps = {
+ helpOpen: boolean;
+ onAddSource: () => void;
+ onHelpOpenChange: (open: boolean) => void;
+ onPaletteOpenChange: (open: boolean) => void;
+ onToggleSidebar: () => void;
+ paletteOpen: boolean;
+};
+
+type PaletteAction = {
+ group: "Actions" | "Navigate";
+ icon: Component<{ class?: string; size?: number | string }>;
+ keywords: string;
+ label: string;
+ run: () => void;
+ shortcutId?: ShortcutId;
+};
+
+type PaletteActionGroup = {
+ actions: PaletteAction[];
+ group: PaletteAction["group"];
+ startIndex: number;
+};
+
+export function V2CommandCenter(props: V2CommandCenterProps) {
+ const navigate = useNavigate();
+ const listId = createUniqueId();
+ const [query, setQuery] = createSignal("");
+ const [activeIndex, setActiveIndex] = createSignal(0);
+ const actions = createMemo
(() => [
+ {
+ group: "Navigate",
+ icon: Library,
+ keywords: "media images search browse",
+ label: "Library",
+ run: () => void navigate({ to: "/v2/search" }),
+ shortcutId: "goLibrary",
+ },
+ {
+ group: "Navigate",
+ icon: BriefcaseBusiness,
+ keywords: "projects characters ips bulk organize",
+ label: "Manager",
+ run: () => void navigate({ to: "/v2/manager" }),
+ shortcutId: "goManager",
+ },
+ {
+ group: "Navigate",
+ icon: Clock3,
+ keywords: "background activity downloads queue",
+ label: "Jobs",
+ run: () => void navigate({ to: "/v2/jobs" }),
+ shortcutId: "goJobs",
+ },
+ {
+ group: "Navigate",
+ icon: Settings,
+ keywords: "preferences keyboard key bindings",
+ label: "Settings",
+ run: () => void navigate({ to: "/v2/config" }),
+ shortcutId: "goSettings",
+ },
+ {
+ group: "Actions",
+ icon: Plus,
+ keywords: "import folder library",
+ label: "Add media source",
+ run: props.onAddSource,
+ },
+ {
+ group: "Actions",
+ icon: PanelLeftClose,
+ keywords: "collapse expand navigation",
+ label: "Toggle sidebar",
+ run: props.onToggleSidebar,
+ shortcutId: "toggleSidebar",
+ },
+ {
+ group: "Actions",
+ icon: CircleHelp,
+ keywords: "keyboard keys help reference",
+ label: "Keyboard shortcuts",
+ run: () => props.onHelpOpenChange(true),
+ shortcutId: "shortcutHelp",
+ },
+ ]);
+ const filteredActions = createMemo(() => {
+ const normalizedQuery = query().trim().toLocaleLowerCase();
+ if (!normalizedQuery) return actions();
+ return actions().filter((action) =>
+ `${action.label} ${action.keywords}`
+ .toLocaleLowerCase()
+ .includes(normalizedQuery),
+ );
+ });
+ const groupedActions = createMemo(() => {
+ const groups: PaletteActionGroup[] = [];
+ for (const [index, action] of filteredActions().entries()) {
+ const lastGroup = groups[groups.length - 1];
+ if (!lastGroup || lastGroup.group !== action.group) {
+ groups.push({
+ actions: [action],
+ group: action.group,
+ startIndex: index,
+ });
+ continue;
+ }
+ lastGroup.actions.push(action);
+ }
+ return groups;
+ });
+ const itemId = (index: number) => `${listId}-option-${index}`;
+ const runAction = (action: PaletteAction) => {
+ props.onPaletteOpenChange(false);
+ setQuery("");
+ action.run();
+ };
+ const setPaletteOpen = (open: boolean) => {
+ if (!open) setQuery("");
+ props.onPaletteOpenChange(open);
+ };
+ const moveActive = (nextIndex: number) => {
+ const count = filteredActions().length;
+ if (count === 0) return;
+ setActiveIndex((nextIndex + count) % count);
+ };
+
+ createEffect(() => {
+ query();
+ filteredActions().length;
+ setActiveIndex(0);
+ });
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/server/src/components/v2/v2-mobile-header.tsx b/apps/server/src/components/v2/v2-mobile-header.tsx
index 1be8d6895..73e66fbc7 100644
--- a/apps/server/src/components/v2/v2-mobile-header.tsx
+++ b/apps/server/src/components/v2/v2-mobile-header.tsx
@@ -1,8 +1,11 @@
import { Button } from "@solid-imager/ui/button";
-import { Menu } from "@solid-imager/ui/v2/icons";
+import { Menu, Search } from "@solid-imager/ui/v2/icons";
import { V2PendingDownloadsIndicator } from "~/components/imports/v2-pending-downloads-indicator";
-export function V2MobileHeader(props: { onOpenMenu: () => void }) {
+export function V2MobileHeader(props: {
+ onOpenCommandPalette?: () => void;
+ onOpenMenu: () => void;
+}) {
return (
);
diff --git a/apps/server/src/components/v2/v2-navigation.tsx b/apps/server/src/components/v2/v2-navigation.tsx
index 643a8bd65..e0def02f3 100644
--- a/apps/server/src/components/v2/v2-navigation.tsx
+++ b/apps/server/src/components/v2/v2-navigation.tsx
@@ -9,10 +9,25 @@ import type { JSX } from "solid-js";
import { Show } from "solid-js";
export const V2_NAVIGATION_ITEMS = [
- { icon: Library, label: "Library", to: "/v2/search" },
- { icon: BriefcaseBusiness, label: "Manager", to: "/v2/manager" },
- { icon: Clock3, label: "Jobs", to: "/v2/jobs" },
- { icon: Settings, label: "Settings", to: "/v2/config" },
+ {
+ icon: Library,
+ label: "Library",
+ shortcutId: "goLibrary",
+ to: "/v2/search",
+ },
+ {
+ icon: BriefcaseBusiness,
+ label: "Manager",
+ shortcutId: "goManager",
+ to: "/v2/manager",
+ },
+ { icon: Clock3, label: "Jobs", shortcutId: "goJobs", to: "/v2/jobs" },
+ {
+ icon: Settings,
+ label: "Settings",
+ shortcutId: "goSettings",
+ to: "/v2/config",
+ },
] as const;
export function V2NavigationItem(props: {
diff --git a/apps/server/src/components/v2/v2-sidebar.tsx b/apps/server/src/components/v2/v2-sidebar.tsx
index 67b52ba99..5cce23733 100644
--- a/apps/server/src/components/v2/v2-sidebar.tsx
+++ b/apps/server/src/components/v2/v2-sidebar.tsx
@@ -1,11 +1,13 @@
import type { SafeMediaSource } from "@solid-imager/core/domain/sources/schemas";
import { Button } from "@solid-imager/ui/button";
+import { ShortcutKbd } from "@solid-imager/ui/shortcuts/index";
import {
CircleHelp,
FileText,
Image,
PanelLeftClose,
PanelLeftOpen,
+ Search,
} from "@solid-imager/ui/v2/icons";
import { Link } from "@tanstack/solid-router";
import { For, Show } from "solid-js";
@@ -21,6 +23,7 @@ export type V2SidebarProps = {
onDeleteSource: (source: SafeMediaSource) => void;
onEditSource: (source: SafeMediaSource) => void;
onNavigate?: () => void;
+ onOpenCommandPalette?: () => void;
onSyncSource: (source: SafeMediaSource) => void;
};
@@ -71,6 +74,29 @@ export function V2Sidebar(props: V2SidebarProps) {
+
+ {(onOpenCommandPalette) => (
+
+
+
+
+ Quick actions
+
+
+
+
+ )}
+
+
+ >
+
+
)}
diff --git a/apps/server/src/routes/__root.tsx b/apps/server/src/routes/__root.tsx
index 3ab5579fd..7bccdb480 100644
--- a/apps/server/src/routes/__root.tsx
+++ b/apps/server/src/routes/__root.tsx
@@ -1,5 +1,6 @@
import { AppShell } from "@solid-imager/ui/layouts/app-shell";
import { RouteTransitionIndicator } from "@solid-imager/ui/router-status";
+import { ShortcutPreferencesProvider } from "@solid-imager/ui/shortcuts/index";
import { Toaster } from "@solid-imager/ui/toast";
import type { QueryClient } from "@tanstack/solid-query";
import {
@@ -54,23 +55,25 @@ function RootComponent() {
-
- }
- when={
- location().pathname !== "/design-lab" &&
- !location().pathname.startsWith("/design-lab/") &&
- !isV2Route()
- }
- >
- }
- statusIndicator={}
+
+
+ }
+ when={
+ location().pathname !== "/design-lab" &&
+ !location().pathname.startsWith("/design-lab/") &&
+ !isV2Route()
+ }
>
-
-
-
-
+ }
+ statusIndicator={}
+ >
+
+
+
+
+