From cdb24b77758f0151da801c2011dff2620d91bdd5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 21 Aug 2026 10:45:05 +0000
Subject: [PATCH 1/5] chore(deps): bump the minor-and-patch group across 1
directory with 8 updates
Bumps the minor-and-patch group with 8 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
| [@tanstack/cli](https://github.com/TanStack/cli/tree/HEAD/packages/cli) | `0.69.6` | `0.70.2` |
| [incur](https://github.com/wevm/incur) | `0.4.26` | `0.5.1` |
| [@electric-sql/pglite](https://github.com/electric-sql/pglite/tree/HEAD/packages/pglite) | `0.5.3` | `0.5.5` |
| [@electric-sql/pglite-pgvector](https://github.com/electric-sql/pglite/tree/HEAD/packages/pglite-pgvector) | `0.0.4` | `0.0.6` |
| [@tanstack/solid-query](https://github.com/TanStack/query/tree/HEAD/packages/solid-query) | `5.99.2` | `5.101.4` |
| [@tanstack/devtools-vite](https://github.com/TanStack/devtools/tree/HEAD/packages/devtools-vite) | `0.7.2` | `0.8.3` |
| [@tanstack/db](https://github.com/TanStack/db/tree/HEAD/packages/db) | `0.6.17` | `0.7.2` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.43` | `0.2.6` |
Updates `@tanstack/cli` from 0.69.6 to 0.70.2
- [Release notes](https://github.com/TanStack/cli/releases)
- [Changelog](https://github.com/TanStack/cli/blob/main/packages/cli/CHANGELOG.md)
- [Commits](https://github.com/TanStack/cli/commits/@tanstack/cli@0.70.2/packages/cli)
Updates `incur` from 0.4.26 to 0.5.1
- [Release notes](https://github.com/wevm/incur/releases)
- [Changelog](https://github.com/wevm/incur/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wevm/incur/compare/incur@0.4.26...incur@0.5.1)
Updates `@electric-sql/pglite` from 0.5.3 to 0.5.5
- [Release notes](https://github.com/electric-sql/pglite/releases)
- [Changelog](https://github.com/electric-sql/pglite/blob/main/packages/pglite/CHANGELOG.md)
- [Commits](https://github.com/electric-sql/pglite/commits/@electric-sql/pglite@0.5.5/packages/pglite)
Updates `@electric-sql/pglite-pgvector` from 0.0.4 to 0.0.6
- [Release notes](https://github.com/electric-sql/pglite/releases)
- [Changelog](https://github.com/electric-sql/pglite/blob/main/packages/pglite-pgvector/CHANGELOG.md)
- [Commits](https://github.com/electric-sql/pglite/commits/@electric-sql/pglite-pgvector@0.0.6/packages/pglite-pgvector)
Updates `@tanstack/solid-query` from 5.99.2 to 5.101.4
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/solid-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/solid-query@5.101.4/packages/solid-query)
Updates `@tanstack/devtools-vite` from 0.7.2 to 0.8.3
- [Release notes](https://github.com/TanStack/devtools/releases)
- [Changelog](https://github.com/TanStack/devtools/blob/main/packages/devtools-vite/CHANGELOG.md)
- [Commits](https://github.com/TanStack/devtools/commits/@tanstack/devtools-vite@0.8.3/packages/devtools-vite)
Updates `@tanstack/db` from 0.6.17 to 0.7.2
- [Release notes](https://github.com/TanStack/db/releases)
- [Changelog](https://github.com/TanStack/db/blob/main/packages/db/CHANGELOG.md)
- [Commits](https://github.com/TanStack/db/commits/@tanstack/db@0.7.2/packages/db)
Updates `@types/chrome` from 0.1.43 to 0.2.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)
---
updated-dependencies:
- dependency-name: "@tanstack/cli"
dependency-version: 0.70.2
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: minor-and-patch
- dependency-name: incur
dependency-version: 0.5.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: minor-and-patch
- dependency-name: "@electric-sql/pglite"
dependency-version: 0.5.5
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: minor-and-patch
- dependency-name: "@electric-sql/pglite-pgvector"
dependency-version: 0.0.6
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: minor-and-patch
- dependency-name: "@tanstack/solid-query"
dependency-version: 5.101.4
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: minor-and-patch
- dependency-name: "@tanstack/devtools-vite"
dependency-version: 0.8.3
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: minor-and-patch
- dependency-name: "@tanstack/db"
dependency-version: 0.7.2
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: minor-and-patch
- dependency-name: "@types/chrome"
dependency-version: 0.2.6
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: minor-and-patch
...
Signed-off-by: dependabot[bot]
---
apps/cli/package.json | 2 +-
apps/server/package.json | 8 ++++----
apps/tauri/package.json | 4 ++--
apps/xtracter/package.json | 2 +-
package.json | 2 +-
packages/ui/package.json | 2 +-
6 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 31818015..1dd90deb 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 396d9727..cec03a67 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -33,8 +33,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",
@@ -49,7 +49,7 @@
"@solid-imager/ui": "workspace:*",
"@tanstack/router-plugin": "^1.168.14",
"@tanstack/solid-form": "^1.33.0",
- "@tanstack/solid-query": "5.99.2",
+ "@tanstack/solid-query": "5.101.4",
"@tanstack/solid-router": "1.170.16",
"@tanstack/solid-router-devtools": "^1.167.0",
"@tanstack/solid-router-ssr-query": "^1.167.1",
@@ -77,7 +77,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/tauri/package.json b/apps/tauri/package.json
index de41cf7e..f55a20d5 100644
--- a/apps/tauri/package.json
+++ b/apps/tauri/package.json
@@ -22,10 +22,10 @@
"@solid-imager/client": "workspace:*",
"@solid-imager/core": "workspace:*",
"@solid-imager/ui": "workspace:*",
- "@tanstack/db": "^0.6.8",
+ "@tanstack/db": "^0.7.2",
"@tanstack/query-db-collection": "^1.0.40",
"@tanstack/solid-db": "^0.2.22",
- "@tanstack/solid-query": "5.99.2",
+ "@tanstack/solid-query": "5.101.4",
"@tanstack/solid-router": "latest",
"@tanstack/tauri-db-sqlite-persistence": "^0.2.0",
"@tauri-apps/api": "^2.11.0",
diff --git a/apps/xtracter/package.json b/apps/xtracter/package.json
index 54961593..72614d89 100644
--- a/apps/xtracter/package.json
+++ b/apps/xtracter/package.json
@@ -19,7 +19,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@types/chrome": "^0.1.43",
+ "@types/chrome": "^0.2.6",
"@types/node": "^26.0.0",
"solid-js": "^1.9.13",
"typescript": "rc",
diff --git a/package.json b/package.json
index dad5a065..de745bd3 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"devDependencies": {
"@biomejs/biome": "^2.5.0",
"@google/design.md": "^0.4.0",
- "@tanstack/cli": "^0.69.4",
+ "@tanstack/cli": "^0.70.2",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"nitro": "npm:nitro-nightly@3.0.1-20260601-173724-a2597363",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 29672cd2..aa45615f 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -18,7 +18,7 @@
"@solid-imager/core": "workspace:*",
"@solidjs/router": "^0.16.1",
"@tanstack/solid-form": "^1.33.0",
- "@tanstack/solid-query": "5.99.2",
+ "@tanstack/solid-query": "5.101.4",
"@tanstack/solid-router": "1.170.16",
"@tanstack/solid-virtual": "^3.13.26",
"class-variance-authority": "^0.7.1",
From 4bb23aa0025df7413cae4d38aa8040a1772de5ba Mon Sep 17 00:00:00 2001
From: hmjn
Date: Mon, 24 Aug 2026 22:35:55 +0900
Subject: [PATCH 2/5] feat(v2): improve Eagle-style collection interactions
---
.../media/legacy-media-grid-item.tsx | 2 +
.../src/components/media/v2-media-actions.tsx | 95 ++-
.../components/media/v2-media-grid-item.tsx | 70 +-
.../server/src/components/v2/v2-app-shell.tsx | 83 ++-
.../src/components/v2/v2-command-center.tsx | 302 +++++++++
.../src/components/v2/v2-mobile-header.tsx | 18 +-
.../src/components/v2/v2-navigation.tsx | 23 +-
apps/server/src/components/v2/v2-sidebar.tsx | 44 +-
apps/server/src/routes/__root.tsx | 35 +-
.../components/source-media-page.tsx | 40 +-
.../components/v2-source-media-page.tsx | 17 +-
.../v2/components/v2-search-content.tsx | 41 +-
...-interface-interactions.responsive.spec.ts | 227 +++++++
apps/tauri/src/routes/__root.tsx | 5 +-
bun.lock | 5 +
packages/ui/package.json | 1 +
packages/ui/src/command.tsx | 28 +-
.../use-media-collection-selection.test.ts | 143 ++++
.../hooks/use-media-collection-selection.ts | 350 ++++++++++
.../ui/src/hooks/use-source-media-page.ts | 17 +-
.../ui/src/screens/search-screen.types.ts | 5 +
.../src/screens/source-media-screen.types.ts | 4 +
packages/ui/src/screens/v2-config-screen.tsx | 15 +
packages/ui/src/screens/v2-search-screen.tsx | 131 +++-
.../ui/src/screens/v2-source-media-screen.tsx | 81 ++-
.../ui/src/shortcuts/create-app-shortcut.ts | 62 ++
packages/ui/src/shortcuts/definitions.ts | 145 +++++
packages/ui/src/shortcuts/index.ts | 6 +
.../ui/src/shortcuts/preferences-provider.tsx | 222 +++++++
.../src/shortcuts/preferences-storage.test.ts | 117 ++++
.../ui/src/shortcuts/preferences-storage.ts | 224 +++++++
packages/ui/src/shortcuts/shortcut-kbd.tsx | 61 ++
.../src/shortcuts/shortcut-settings-panel.tsx | 374 +++++++++++
packages/ui/src/skeleton.tsx | 2 +-
packages/ui/src/source-media-grid.tsx | 612 ++++++++++++++----
packages/ui/src/source-media-page.tsx | 16 +-
packages/ui/src/styles/theme.css | 55 --
packages/ui/src/v2-import-review-modal.tsx | 44 +-
packages/ui/src/v2-media-grid-item.tsx | 6 +-
packages/ui/src/v2-media-viewer.tsx | 234 ++++++-
packages/ui/src/v2/collection-inspector.tsx | 185 ++++--
.../ui/src/v2/collection-navigation.test.ts | 85 +++
packages/ui/src/v2/collection-navigation.ts | 103 +++
packages/ui/src/v2/icons.tsx | 24 +
packages/ui/src/v2/search-composer.tsx | 8 +-
packages/ui/src/v2/search-toolbar.tsx | 42 +-
46 files changed, 4032 insertions(+), 377 deletions(-)
create mode 100644 apps/server/src/components/v2/v2-command-center.tsx
create mode 100644 apps/server/src/tests/e2e/v2-interface-interactions.responsive.spec.ts
create mode 100644 packages/ui/src/hooks/use-media-collection-selection.test.ts
create mode 100644 packages/ui/src/hooks/use-media-collection-selection.ts
create mode 100644 packages/ui/src/shortcuts/create-app-shortcut.ts
create mode 100644 packages/ui/src/shortcuts/definitions.ts
create mode 100644 packages/ui/src/shortcuts/index.ts
create mode 100644 packages/ui/src/shortcuts/preferences-provider.tsx
create mode 100644 packages/ui/src/shortcuts/preferences-storage.test.ts
create mode 100644 packages/ui/src/shortcuts/preferences-storage.ts
create mode 100644 packages/ui/src/shortcuts/shortcut-kbd.tsx
create mode 100644 packages/ui/src/shortcuts/shortcut-settings-panel.tsx
create mode 100644 packages/ui/src/v2/collection-navigation.test.ts
create mode 100644 packages/ui/src/v2/collection-navigation.ts
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 7d0c357a..807190c6 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 1ec14510..92d87b57 100644
--- a/apps/server/src/components/media/v2-media-actions.tsx
+++ b/apps/server/src/components/media/v2-media-actions.tsx
@@ -1,5 +1,15 @@
import type { MediaDetails } from "@solid-imager/core/domain/media/schemas";
import { getErrorMessage } from "@solid-imager/core/utils";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@solid-imager/ui/alert-dialog";
import { Button } from "@solid-imager/ui/button";
import {
Popover,
@@ -11,9 +21,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 +37,7 @@ import {
getCcipVectorStatus,
startCcipExtraction,
} from "~/infrastructure/api-clients/ai-api";
+import { deleteMedia } from "~/infrastructure/api-clients/media-api";
type MediaActionsProps = {
media: MediaDetails;
@@ -41,6 +54,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 +63,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 +117,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 +185,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 +244,11 @@ export function V2MediaActions(props: MediaActionsProps) {
Find similar
-
+
More actions
@@ -242,6 +288,25 @@ export function V2MediaActions(props: MediaActionsProps) {
>
CCIP status: {ccipStatus()}
+
+
+
@@ -264,6 +329,34 @@ 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
+
+ {
+ event.preventDefault();
+ void handleDelete();
+ }}
+ >
+ {isDeleting() ? "Deleting…" : "Delete"}
+
+
+
+
>
);
}
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 478211bb..45f7ef0f 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,81 @@ 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 +101,7 @@ export function V2MediaGridItem(props: V2ServerMediaGridItemProps) {
return (
(
@@ -65,6 +112,13 @@ 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 61afa272..0f9e79bb 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 00000000..22a49a1d
--- /dev/null
+++ b/apps/server/src/components/v2/v2-command-center.tsx
@@ -0,0 +1,302 @@
+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;
+};
+
+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 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 1be8d689..73e66fbc 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 643a8bd6..e0def02f 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 67b52ba9..5cce2373 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 3ab5579f..7bccdb48 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={}
+ >
+
+
+
+
+