From d87141ffc2fb0c39455d6c29728644673e023d26 Mon Sep 17 00:00:00 2001 From: Fernando <52652158+rene-roid@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:45:09 +0200 Subject: [PATCH 1/4] Add Liquid Glass interface style with a mobile dock A new Appearance setting switches between the original look and a frosted "Liquid Glass" style modelled on the Umbrel App Store. Shared borders, surfaces and radii are CSS tokens that [data-ui='glass'] swaps; component-specific differences use a `glass:` Tailwind variant. The choice is stored server-side (ui_style) and mirrored to localStorage so the right style paints before settings load. Glass mode adds a dashboard banner with a close button; it can be turned back on from Settings (dashboard_hero). Navigation no longer overflows on narrow screens: under md it moves to a bottom dock, and nav links collapse to icons until xl. Sticky bars and full-height panes read the navbar's measured height from --nav-h instead of assuming 60px. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/server/src/lib/settings.ts | 4 + apps/server/src/routes/settings.ts | 5 + apps/web/index.html | 4 +- apps/web/src/App.tsx | 7 +- apps/web/src/components/BulkConfirmDialog.tsx | 12 +- apps/web/src/components/BulkRenameDialog.tsx | 16 +- apps/web/src/components/BulkTagDialog.tsx | 10 +- apps/web/src/components/ConfirmDialog.tsx | 6 +- apps/web/src/components/FilterToolbar.tsx | 12 +- apps/web/src/components/LoginGate.tsx | 4 +- apps/web/src/components/MediaCard.tsx | 27 +- apps/web/src/components/MediaViewer.tsx | 16 +- apps/web/src/components/Menu.tsx | 2 +- apps/web/src/components/Navbar.tsx | 248 ++++++++++------ .../web/src/components/ReleaseNotesDialog.tsx | 8 +- apps/web/src/components/SelectionBar.tsx | 20 +- apps/web/src/components/TagSearchInput.tsx | 10 +- apps/web/src/components/TagSidebar.tsx | 10 +- apps/web/src/components/Toast.tsx | 2 +- apps/web/src/components/UpdateToast.tsx | 2 +- apps/web/src/hooks/useUiStyle.ts | 21 ++ apps/web/src/index.css | 92 ++++++ apps/web/src/main.tsx | 3 + apps/web/src/routes/Boards.tsx | 16 +- apps/web/src/routes/Dashboard.tsx | 269 +++++++++++++++--- apps/web/src/routes/Discover.tsx | 10 +- apps/web/src/routes/Explore.tsx | 4 +- apps/web/src/routes/LibraryView.tsx | 4 +- .../routes/downloader/DownloaderConsole.tsx | 12 +- .../src/routes/downloader/DownloaderPage.tsx | 38 +-- .../web/src/routes/settings/DuplicatesTab.tsx | 16 +- .../src/routes/settings/JobsConfigureTab.tsx | 20 +- .../src/routes/settings/JobsHistoryTab.tsx | 2 +- apps/web/src/routes/settings/JobsTab.tsx | 2 +- apps/web/src/routes/settings/LibrariesTab.tsx | 40 +-- apps/web/src/routes/settings/MiscTabs.tsx | 151 ++++++++-- apps/web/src/routes/settings/ReleasesTab.tsx | 2 +- apps/web/src/routes/settings/TaggingTab.tsx | 18 +- apps/web/src/routes/settings/UploadsTab.tsx | 6 +- apps/web/src/routes/settings/index.tsx | 14 +- packages/shared/src/index.ts | 4 + 41 files changed, 822 insertions(+), 347 deletions(-) create mode 100644 apps/web/src/hooks/useUiStyle.ts diff --git a/apps/server/src/lib/settings.ts b/apps/server/src/lib/settings.ts index 5698785..b3b7e4c 100644 --- a/apps/server/src/lib/settings.ts +++ b/apps/server/src/lib/settings.ts @@ -18,6 +18,10 @@ const DEFAULTS: Record = { // Off until switched on in Settings. Engagement tracking runs regardless, so the taste profile // is already warm the first time it's turned on. discover_enabled: '0', + // 'glass' (Liquid Glass) or 'classic' (the original look). + ui_style: 'glass', + // The Liquid Glass dashboard banner; its close button turns this off. + dashboard_hero: '1', }; export function getSetting(key: string): string { diff --git a/apps/server/src/routes/settings.ts b/apps/server/src/routes/settings.ts index ce8ce99..79dfb4e 100644 --- a/apps/server/src/routes/settings.ts +++ b/apps/server/src/routes/settings.ts @@ -30,8 +30,12 @@ const EDITABLE_KEYS = new Set([ 'gifs_as_videos', 'video_transcode_enabled', 'discover_enabled', + 'ui_style', + 'dashboard_hero', ]); +const UI_STYLES = new Set(['glass', 'classic']); + settingsRouter.get( '/api/settings', wrap(async (_req, res) => { @@ -45,6 +49,7 @@ settingsRouter.patch( const body = z.record(z.string()).parse(req.body); for (const [key, value] of Object.entries(body)) { if (!EDITABLE_KEYS.has(key)) return res.status(400).json({ error: `Setting not editable: ${key}` }); + if (key === 'ui_style' && !UI_STYLES.has(value)) return res.status(400).json({ error: `Unknown ui_style: ${value}` }); if (key === 'gifs_as_videos' && value !== getSetting(key)) { enqueueGifReclassifyJob(value === '1'); } diff --git a/apps/web/index.html b/apps/web/index.html index e1075de..5acf06c 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -2,11 +2,11 @@ - + Sakuya - +
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index a2448b3..b68b4a1 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -7,6 +7,7 @@ import { ReleaseNotesDialog } from './components/ReleaseNotesDialog'; import { UpdateToast } from './components/UpdateToast'; import { LoginGate } from './components/LoginGate'; import { useAuth } from './hooks/useAuth'; +import { applyUiStyle } from './hooks/useUiStyle'; import { JobsProvider } from './hooks/useJobs'; import { DownloaderProvider } from './hooks/useDownloader'; import { Dashboard } from './routes/Dashboard'; @@ -32,6 +33,10 @@ export function App() { } }, [settings?.accent_color]); + useEffect(() => { + if (settings?.ui_style) applyUiStyle(settings.ui_style); + }, [settings?.ui_style]); + if (loading) return null; // Only gate when the server says login is on. An unreachable server has no auth status at all, // and showing a password prompt for it sends people hunting for a password that doesn't exist. @@ -40,7 +45,7 @@ export function App() { return ( -
+
diff --git a/apps/web/src/components/BulkConfirmDialog.tsx b/apps/web/src/components/BulkConfirmDialog.tsx index f193dae..feafa9a 100644 --- a/apps/web/src/components/BulkConfirmDialog.tsx +++ b/apps/web/src/components/BulkConfirmDialog.tsx @@ -49,7 +49,7 @@ export function BulkConfirmDialog({ onClick={busy ? undefined : onCancel} >
e.stopPropagation()} >
{title}
@@ -64,11 +64,11 @@ export function BulkConfirmDialog({
)} -
+
{shown.map((row) => (
@@ -76,7 +76,7 @@ export function BulkConfirmDialog({ src={thumbUrl(row.id)} alt="" loading="lazy" - className="h-7 w-7 flex-none rounded border border-zinc-800 object-cover" + className="h-7 w-7 flex-none rounded border border-line object-cover" />
@@ -101,14 +101,14 @@ export function BulkConfirmDialog({
-
+
{plan.map((row) => (
@@ -183,14 +183,14 @@ export function BulkRenameDialog({
diff --git a/apps/web/src/components/BulkTagDialog.tsx b/apps/web/src/components/BulkTagDialog.tsx index 271fc26..23b07a3 100644 --- a/apps/web/src/components/BulkTagDialog.tsx +++ b/apps/web/src/components/BulkTagDialog.tsx @@ -90,7 +90,7 @@ export function BulkTagDialog({ onClick={onCancel} >
e.stopPropagation()} >
Edit tags on {ids.length} files
@@ -125,7 +125,7 @@ export function BulkTagDialog({ />
REMOVE
-
+
{(present ?? []).length === 0 && (
No tags on the selected files.
)} @@ -141,7 +141,7 @@ export function BulkTagDialog({ className={`flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11.5px] font-semibold ${ staged ? 'border-rose-500/50 bg-rose-500/15 text-rose-300 line-through' - : 'border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-600' + : 'border-line-strong bg-zinc-800 text-zinc-300 hover:border-line-hover' }`} > {tag.name} @@ -156,14 +156,14 @@ export function BulkTagDialog({
diff --git a/apps/web/src/components/ConfirmDialog.tsx b/apps/web/src/components/ConfirmDialog.tsx index 2063446..bbce3f6 100644 --- a/apps/web/src/components/ConfirmDialog.tsx +++ b/apps/web/src/components/ConfirmDialog.tsx @@ -24,7 +24,7 @@ export function ConfirmDialog({ onClick={onCancel} >
e.stopPropagation()} >
{title}
@@ -32,13 +32,13 @@ export function ConfirmDialog({