From a2b76bd6346da8afeb9aed341477890becbf571b Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 25 Aug 2026 18:15:46 +0100 Subject: [PATCH] feat(web): distill shell rail --- DESIGN.md | 4 ++++ web/index.html | 29 ++++++++++++++++++----------- web/src/icons.ts | 6 ++++++ web/src/main.ts | 22 ++++++---------------- web/src/settings.ts | 1 + web/src/style.css | 28 ++++++++++++++-------------- 6 files changed, 49 insertions(+), 41 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index fed28ea..5752878 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -743,6 +743,10 @@ is healthy survives the move and no navigation is needed to get it. **The wordmark is a link home.** `arr` on the rail navigates to `/`. +**The rail composition is fixed.** From left to right it carries the master +lamp and wordmark, a warning mark only while queues need attention, the centred +search, the version readout and the settings cog. + ## 10. Persistence SQLite via `sqlx`, compile-time-checked queries, migrations in `arr-db`. diff --git a/web/index.html b/web/index.html index d915b03..ce5cb2b 100644 --- a/web/index.html +++ b/web/index.html @@ -76,17 +76,16 @@ arr - + diff --git a/web/src/icons.ts b/web/src/icons.ts index 45786e2..4c6cfd4 100644 --- a/web/src/icons.ts +++ b/web/src/icons.ts @@ -22,6 +22,12 @@ const ICON_PATHS = { "M11.2 2.4a1.5 1.5 0 0 1 2.1 0l.3.3a1.5 1.5 0 0 1 0 2.1L5.9 12.5l-3.2.8.8-3.2z", "M10.1 3.5l2.4 2.4", ], + cog: [ + "M8 5.2a2.8 2.8 0 1 0 0 5.6 2.8 2.8 0 0 0 0-5.6Z", + "M8 1.6v1.3M8 13.1v1.3M14.4 8h-1.3M2.9 8H1.6M12.5 3.5l-.9.9M4.4 11.6l-.9.9M12.5 12.5l-.9-.9M4.4 4.4l-.9-.9", + "M10.8 3.1l.7 1.7 1.7.7-1.7.7-.7 1.7-.7-1.7-1.7-.7 1.7-.7z", + ], + warning: ["M8 1.8l6.2 11H1.8z", "M8 5.3v3.4M8 11.2h.01"], } as const; /** Stroke glyph on the shared 16-grid; the control's aria-label carries the meaning. */ diff --git a/web/src/main.ts b/web/src/main.ts index 4dcd69d..ecf8625 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -325,7 +325,7 @@ function main() { library.open(); await movieDetail.open( route.movieId, - must("#nav-library"), + must("#wordmark"), must("#library"), { kind: "library", @@ -350,7 +350,7 @@ function main() { library.open(); await seriesDetail.open( route.seriesId, - must("#nav-library"), + must("#wordmark"), must("#library"), { kind: "library", @@ -396,7 +396,7 @@ function main() { library.open(); await seriesDetail.open( seriesId, - must("#nav-library"), + must("#wordmark"), must("#library"), { kind: "library" }, ); @@ -407,7 +407,7 @@ function main() { sub, seriesId, target, - origin: must("#nav-library"), + origin: must("#wordmark"), returnTo: must("#series"), parentRoute: { kind: "series", seriesId }, }); @@ -2532,7 +2532,6 @@ function libraryMain( ): LibraryView { const view = must("#library"); const deck = must("#deck"); - const nav = must("#nav-library"); const summary = must("#library-summary"); const toggle = must("#library-toggle"); const status = must("#library-status"); @@ -2677,25 +2676,14 @@ function libraryMain( } deck.hidden = true; view.hidden = false; - nav.setAttribute("aria-pressed", "true"); void load(); } function hide() { view.hidden = true; - nav.setAttribute("aria-pressed", "false"); sequence += 1; } - // §9.7: the library is the homepage — the bottom layer. There is nothing - // beneath it, so the nav control never toggles off and Esc has no handler. - nav.addEventListener("click", () => { - if (view.hidden) { - navigate({ kind: "library" }); - open(); - } - }); - toggle.addEventListener("click", () => { showAll = !showAll; render(); @@ -4128,6 +4116,7 @@ function queuesMain(movieDetail: MovieView, views: HideableView[], goHome: () => const view = must("#queues"); const deck = must("#deck"); const nav = must("#nav-queues"); + nav.prepend(icon("warning")); const badge = must("#queues-count"); const summary = must("#queues-summary"); const status = must("#queues-status"); @@ -4167,6 +4156,7 @@ function queuesMain(movieDetail: MovieView, views: HideableView[], goHome: () => } function paintBadge(total: number) { + nav.hidden = total === 0; badge.hidden = total === 0; badge.textContent = total === 0 ? "" : String(total); } diff --git a/web/src/settings.ts b/web/src/settings.ts index 1e5bdbf..b00be58 100644 --- a/web/src/settings.ts +++ b/web/src/settings.ts @@ -206,6 +206,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void): const view = must("#settings"); const deck = must("#deck"); const nav = must("#nav-settings"); + nav.append(icon("cog")); const summary = must("#settings-summary"); const status = must("#settings-status"); const rootsSection = must("#group-roots"); diff --git a/web/src/style.css b/web/src/style.css index 4980295..ca9df2f 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -142,6 +142,7 @@ body { /* contents share the deck's column; the panel itself stays full-bleed */ .rail-inner { + position: relative; display: flex; align-items: center; gap: var(--space-6); @@ -178,8 +179,6 @@ body { display: flex; align-items: center; gap: var(--space-4); - /* the verdict line used to absorb the slack; without it the readout - needs to claim the right edge itself */ margin-left: auto; } @@ -529,9 +528,11 @@ body { /* ---- unified search (§9.2) ------------------------------------------- */ .rail-search { - position: relative; - flex: 1; + position: absolute; + left: 50%; + width: min(30rem, calc(100% - 20rem)); max-width: 30rem; + transform: translateX(-50%); display: flex; align-items: center; } @@ -1337,10 +1338,6 @@ body { /* ---- library view (§4.2) ---------------------------------------------- */ -.rail-nav { - flex: none; -} - /* the library's master line: total readout left, the one §4.2 toggle right */ .library-bar { margin: 0 0 var(--space-6); @@ -1639,6 +1636,10 @@ body { color: var(--signal-warn); } +.rail #nav-queues[hidden] { + display: none; +} + .queue-note { margin: var(--space-2) var(--space-1) 0; font-size: var(--text-xs); @@ -1901,6 +1902,7 @@ body { @media (max-width: 46rem) { .rail-inner { + position: static; flex-wrap: wrap; gap: var(--space-3) var(--space-4); } @@ -1919,15 +1921,13 @@ body { } .rail-search { + position: relative; + left: auto; + width: auto; order: 4; flex-basis: 100%; max-width: none; - } - - /* deterministic first row: identity, views, then meta pushed right — - never re-wrapped by content or scrollbar changes */ - .rail-nav { - order: 2; + transform: none; } .rail-meta {