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
-
+
v —
+
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 {