+75
-66
@@ -63,12 +63,18 @@
|
||||
nothing needs them. The rail control carries an amber entry count,
|
||||
refreshed with the health poll; amber is attention, cyan stays the
|
||||
interaction hue.
|
||||
SHELL (§9.7, issue 173): the library is the homepage — "/" renders the
|
||||
library deck, and the wordmark on the rail is a link home. The signal
|
||||
chain above is no longer a route: it lives as the first section of
|
||||
/settings, its lamps still driven by the health poll, and the master
|
||||
lamp stays on the rail so the one-glance read survives the move. Where
|
||||
earlier notes here say "returns to the board", read "the library".
|
||||
-->
|
||||
<header class="rail">
|
||||
<div class="rail-inner">
|
||||
<div class="rail-id">
|
||||
<span class="lamp" id="master-lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h1 class="wordmark">arr</h1>
|
||||
<a class="wordmark" id="wordmark" href="/">arr</a>
|
||||
</div>
|
||||
<nav class="rail-nav" aria-label="views">
|
||||
<button type="button" class="control" id="nav-library" aria-pressed="false">
|
||||
@@ -361,6 +367,74 @@
|
||||
</header>
|
||||
<p class="deck-status readout" id="settings-status" role="status" hidden></p>
|
||||
|
||||
<section class="deck-group" id="group-chain" aria-labelledby="label-chain">
|
||||
<header class="deck-head">
|
||||
<h3 class="deck-label" id="label-chain">signal chain</h3>
|
||||
</header>
|
||||
<section class="chain" aria-label="signal chain">
|
||||
<article class="module area-tmdb" data-check="tmdb">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">tmdb</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">metadata in</p>
|
||||
<p class="module-detail readout" data-role="detail">titles, languages, release dates</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t1" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="tmdb" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-prowlarr" data-check="prowlarr">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">prowlarr</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">indexers in</p>
|
||||
<p class="module-detail readout" data-role="detail">torznab search and rss</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t2" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="prowlarr" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module module-master area-master" data-check="master">
|
||||
<header class="module-head">
|
||||
<span class="lamp lamp-big" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">arr</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">reconcile core</p>
|
||||
<p class="module-detail readout" data-role="detail">decide · find · fetch · import</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-out" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="master" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-transmission" data-check="transmission">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">transmission</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">torrents out</p>
|
||||
<p class="module-detail readout" data-role="detail">rpc · grab and seed</p>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="deck-group" id="group-roots" hidden aria-labelledby="label-roots">
|
||||
<header class="deck-head bucket-head">
|
||||
<h3 class="deck-label" id="label-roots">roots</h3>
|
||||
@@ -437,70 +511,5 @@
|
||||
<div id="tv-buckets"></div>
|
||||
</main>
|
||||
|
||||
<main class="board">
|
||||
<section class="chain" aria-label="signal chain">
|
||||
<article class="module area-tmdb" data-check="tmdb">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">tmdb</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">metadata in</p>
|
||||
<p class="module-detail readout" data-role="detail">titles, languages, release dates</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t1" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="tmdb" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-prowlarr" data-check="prowlarr">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">prowlarr</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">indexers in</p>
|
||||
<p class="module-detail readout" data-role="detail">torznab search and rss</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-t2" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="prowlarr" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module module-master area-master" data-check="master">
|
||||
<header class="module-head">
|
||||
<span class="lamp lamp-big" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">arr</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">reconcile core</p>
|
||||
<p class="module-detail readout" data-role="detail">decide · find · fetch · import</p>
|
||||
</article>
|
||||
|
||||
<div class="bus area-out" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="master" aria-hidden="true">
|
||||
<line x1="0" y1="4" x2="32" y2="4" />
|
||||
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<article class="module area-transmission" data-check="transmission">
|
||||
<header class="module-head">
|
||||
<span class="lamp" data-state="probing" aria-hidden="true"></span>
|
||||
<h2 class="module-name">transmission</h2>
|
||||
<span class="module-status readout" data-role="status">probing</span>
|
||||
</header>
|
||||
<p class="module-role">torrents out</p>
|
||||
<p class="module-detail readout" data-role="detail">rpc · grab and seed</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+47
-67
@@ -123,7 +123,7 @@ function moduleRefs(name: string): ModuleRefs {
|
||||
const status = root?.querySelector<HTMLElement>('[data-role="status"]');
|
||||
const detail = root?.querySelector<HTMLElement>('[data-role="detail"]');
|
||||
if (!lamp || !status || !detail) {
|
||||
throw new Error(`board is missing the ${name} module`);
|
||||
throw new Error(`signal chain is missing the ${name} module`);
|
||||
}
|
||||
return { lamp, status, detail, defaultDetail: detail.textContent ?? "" };
|
||||
}
|
||||
@@ -138,13 +138,13 @@ function setModule(refs: ModuleRefs, state: string, tone: Tone, word: string, de
|
||||
function must<T extends Element>(selector: string): T {
|
||||
const element = document.querySelector<T>(selector);
|
||||
if (!element) {
|
||||
throw new Error(`board markup is missing ${selector}`);
|
||||
throw new Error(`markup is missing ${selector}`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const board = must<HTMLElement>(".board");
|
||||
const chain = must<HTMLElement>(".chain");
|
||||
const masterLamp = must<HTMLElement>("#master-lamp");
|
||||
const version = must<HTMLElement>("#version");
|
||||
|
||||
@@ -185,13 +185,13 @@ function main() {
|
||||
function render(probe: Probe) {
|
||||
if (!powered) {
|
||||
powered = true;
|
||||
board.classList.add("powered");
|
||||
chain.classList.add("powered");
|
||||
}
|
||||
|
||||
if (probe.kind === "unreachable") {
|
||||
masterLamp.dataset.state = "unreachable";
|
||||
version.textContent = "v —";
|
||||
// the outage detail lives on the board's arr module — the header has no verdict line
|
||||
// the outage detail lives on the chain's arr module — the header has no verdict line
|
||||
setModule(master, "unreachable", "fault", "down", probe.detail);
|
||||
for (const refs of Object.values(checks)) {
|
||||
setModule(refs, "off", "idle", "no signal");
|
||||
@@ -249,13 +249,14 @@ function main() {
|
||||
// the tv deck registers its Escape handler before the series view does,
|
||||
// so one Esc always steps back from the innermost layer first
|
||||
const tvDeck = tvReleasesMain();
|
||||
const seriesDetail = seriesMain(board, tvDeck, views);
|
||||
const movieDetail = movieMain(board, views);
|
||||
const library = libraryMain(board, movieDetail, seriesDetail, views);
|
||||
const queues = queuesMain(board, movieDetail, views);
|
||||
const settings = settingsMain(board, views);
|
||||
const seriesDetail = seriesMain(tvDeck, views);
|
||||
const movieDetail = movieMain(views);
|
||||
const library = libraryMain(movieDetail, seriesDetail, views);
|
||||
const goHome = () => library.open();
|
||||
const queues = queuesMain(movieDetail, views, goHome);
|
||||
const settings = settingsMain(views, goHome);
|
||||
views.push(tvDeck, seriesDetail, movieDetail, library, queues, settings);
|
||||
const search = searchMain(board, movieDetail, seriesDetail, views);
|
||||
const search = searchMain(movieDetail, seriesDetail, views, goHome);
|
||||
// a removed title must not survive on the surface the page opened over
|
||||
movieDetail.setRemoved((parent) => {
|
||||
switch (parent.kind) {
|
||||
@@ -269,7 +270,7 @@ function main() {
|
||||
search.restore(parent.query);
|
||||
break;
|
||||
default:
|
||||
search.showIdle();
|
||||
library.open();
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -282,11 +283,10 @@ function main() {
|
||||
// and back/forward reach the same view a click would have.
|
||||
async function applyRoute(route: Route) {
|
||||
switch (route.kind) {
|
||||
case "board":
|
||||
search.showIdle();
|
||||
break;
|
||||
case "library":
|
||||
library.open();
|
||||
// §9.7: "/" is the library — clear any stale query so the rail
|
||||
// input matches the route on a cold load or back-navigation
|
||||
search.showIdle();
|
||||
break;
|
||||
case "queues":
|
||||
queues.open();
|
||||
@@ -401,6 +401,15 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// §9.7: the wordmark is a link home — intercepted so it navigates the
|
||||
// SPA instead of reloading, but still a real anchor for the keyboard
|
||||
const wordmark = must<HTMLAnchorElement>("#wordmark");
|
||||
wordmark.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
navigate({ kind: "library" });
|
||||
search.showIdle();
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
void applyRoute(currentRoute());
|
||||
});
|
||||
@@ -425,17 +434,17 @@ interface DeckRefs {
|
||||
}
|
||||
|
||||
interface SearchView {
|
||||
/** Restores the board with the search box empty — the "/" route. */
|
||||
/** Restores the library with the search box empty — the "/" route. */
|
||||
showIdle: () => void;
|
||||
/** Restores the deck for `query` — the "/search?q=" route. */
|
||||
restore: (query: string) => void;
|
||||
}
|
||||
|
||||
function searchMain(
|
||||
board: HTMLElement,
|
||||
movieDetail: MovieView,
|
||||
seriesDetail: SeriesView,
|
||||
views: HideableView[],
|
||||
goHome: () => void,
|
||||
): SearchView {
|
||||
const input = must<HTMLInputElement>("#search");
|
||||
const hint = must<HTMLElement>("#search-hint");
|
||||
@@ -492,21 +501,17 @@ function searchMain(
|
||||
let timer: number | undefined;
|
||||
let controller: AbortController | null = null;
|
||||
|
||||
function showBoard() {
|
||||
/** A cleared search lands on the library — the "/" route (§9.7). */
|
||||
function showHome() {
|
||||
controller?.abort();
|
||||
controller = null;
|
||||
for (const view of views) {
|
||||
view.hide();
|
||||
}
|
||||
refs.deck.hidden = true;
|
||||
board.hidden = false;
|
||||
goHome();
|
||||
}
|
||||
|
||||
function showDeck() {
|
||||
for (const view of views) {
|
||||
view.hide();
|
||||
}
|
||||
board.hidden = true;
|
||||
refs.deck.hidden = false;
|
||||
}
|
||||
|
||||
@@ -602,8 +607,8 @@ function searchMain(
|
||||
window.clearTimeout(timer);
|
||||
const query = input.value.trim();
|
||||
if (query === "") {
|
||||
navigate({ kind: "board" });
|
||||
showBoard();
|
||||
navigate({ kind: "library" });
|
||||
showHome();
|
||||
clearGroups();
|
||||
setStatus(null);
|
||||
return;
|
||||
@@ -632,8 +637,8 @@ function searchMain(
|
||||
}
|
||||
} else if (event.key === "Escape") {
|
||||
input.value = "";
|
||||
navigate({ kind: "board" });
|
||||
showBoard();
|
||||
navigate({ kind: "library" });
|
||||
showHome();
|
||||
clearGroups();
|
||||
setStatus(null);
|
||||
}
|
||||
@@ -658,8 +663,8 @@ function searchMain(
|
||||
input.select();
|
||||
} else if (event.key === "Escape" && !refs.deck.hidden) {
|
||||
input.value = "";
|
||||
navigate({ kind: "board" });
|
||||
showBoard();
|
||||
navigate({ kind: "library" });
|
||||
showHome();
|
||||
clearGroups();
|
||||
setStatus(null);
|
||||
input.focus();
|
||||
@@ -690,7 +695,7 @@ function searchMain(
|
||||
|
||||
function showIdle() {
|
||||
input.value = "";
|
||||
showBoard();
|
||||
showHome();
|
||||
clearGroups();
|
||||
setStatus(null);
|
||||
}
|
||||
@@ -1309,7 +1314,7 @@ function externalLink(label: string, href: string): HTMLAnchorElement {
|
||||
return link;
|
||||
}
|
||||
|
||||
function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
|
||||
function movieMain(views: HideableView[]): MovieView {
|
||||
const view = must<HTMLElement>("#movie");
|
||||
const deckEl = must<HTMLElement>("#deck");
|
||||
const back = must<HTMLButtonElement>("#movie-back");
|
||||
@@ -1360,7 +1365,7 @@ function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
|
||||
let removed: ((parent: Route) => void) | null = null;
|
||||
let origin: HTMLElement | null = null;
|
||||
let returnTo: HTMLElement | null = null;
|
||||
let parentRoute: Route = { kind: "board" };
|
||||
let parentRoute: Route = { kind: "library" };
|
||||
const dom: BucketsDom = { eligible, waived: collapsed.waived, rejected: collapsed.rejected };
|
||||
const actions: ReleaseActions = {
|
||||
reload: () => reloadReleases(),
|
||||
@@ -1907,7 +1912,6 @@ function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
|
||||
origin = from;
|
||||
returnTo = container;
|
||||
parentRoute = parent;
|
||||
board.hidden = true;
|
||||
deckEl.hidden = true;
|
||||
view.hidden = false;
|
||||
clearBuckets(dom);
|
||||
@@ -2373,7 +2377,6 @@ function loadLibraryShape(): LibraryShape {
|
||||
}
|
||||
|
||||
function libraryMain(
|
||||
board: HTMLElement,
|
||||
movieDetail: MovieView,
|
||||
seriesDetail: SeriesView,
|
||||
views: HideableView[],
|
||||
@@ -2523,7 +2526,6 @@ function libraryMain(
|
||||
for (const sibling of views) {
|
||||
sibling.hide();
|
||||
}
|
||||
board.hidden = true;
|
||||
deck.hidden = true;
|
||||
view.hidden = false;
|
||||
nav.setAttribute("aria-pressed", "true");
|
||||
@@ -2536,19 +2538,12 @@ function libraryMain(
|
||||
sequence += 1;
|
||||
}
|
||||
|
||||
function close() {
|
||||
hide();
|
||||
board.hidden = false;
|
||||
nav.focus();
|
||||
}
|
||||
|
||||
// §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();
|
||||
} else {
|
||||
navigate({ kind: "board" });
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2557,19 +2552,6 @@ function libraryMain(
|
||||
render();
|
||||
});
|
||||
|
||||
// capture, like the release deck: one Esc steps back one layer
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
(event) => {
|
||||
if (event.key === "Escape" && !view.hidden) {
|
||||
event.stopImmediatePropagation();
|
||||
navigate({ kind: "board" });
|
||||
close();
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return { hide, open };
|
||||
}
|
||||
|
||||
@@ -2951,7 +2933,7 @@ function tvReleasesMain(): TvReleasesView {
|
||||
|
||||
function close() {
|
||||
const current = request;
|
||||
navigate(current?.parentRoute ?? { kind: "board" });
|
||||
navigate(current?.parentRoute ?? { kind: "library" });
|
||||
hide();
|
||||
if (current) {
|
||||
current.returnTo.hidden = false;
|
||||
@@ -2992,7 +2974,7 @@ interface SeriesView {
|
||||
|
||||
const PAD_TWO = (value: number): string => String(value).padStart(2, "0");
|
||||
|
||||
function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
const view = must<HTMLElement>("#series");
|
||||
const deckEl = must<HTMLElement>("#deck");
|
||||
const back = must<HTMLButtonElement>("#series-back");
|
||||
@@ -3450,7 +3432,6 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
|
||||
parentRoute = parent;
|
||||
expanded = new Set();
|
||||
focusKey = null;
|
||||
board.hidden = true;
|
||||
deckEl.hidden = true;
|
||||
view.hidden = false;
|
||||
clearRichDetail();
|
||||
@@ -3507,7 +3488,7 @@ interface QueueGroup {
|
||||
rows: HTMLUListElement;
|
||||
}
|
||||
|
||||
function queuesMain(board: HTMLElement, movieDetail: MovieView, views: HideableView[]): QueuesView {
|
||||
function queuesMain(movieDetail: MovieView, views: HideableView[], goHome: () => void): QueuesView {
|
||||
const view = must<HTMLElement>("#queues");
|
||||
const deck = must<HTMLElement>("#deck");
|
||||
const nav = must<HTMLButtonElement>("#nav-queues");
|
||||
@@ -3649,7 +3630,6 @@ function queuesMain(board: HTMLElement, movieDetail: MovieView, views: HideableV
|
||||
for (const sibling of views) {
|
||||
sibling.hide();
|
||||
}
|
||||
board.hidden = true;
|
||||
deck.hidden = true;
|
||||
view.hidden = false;
|
||||
nav.setAttribute("aria-pressed", "true");
|
||||
@@ -3664,7 +3644,7 @@ function queuesMain(board: HTMLElement, movieDetail: MovieView, views: HideableV
|
||||
|
||||
function close() {
|
||||
hide();
|
||||
board.hidden = false;
|
||||
goHome();
|
||||
nav.focus();
|
||||
}
|
||||
|
||||
@@ -3673,7 +3653,7 @@ function queuesMain(board: HTMLElement, movieDetail: MovieView, views: HideableV
|
||||
navigate({ kind: "queues" });
|
||||
open();
|
||||
} else {
|
||||
navigate({ kind: "board" });
|
||||
navigate({ kind: "library" });
|
||||
close();
|
||||
}
|
||||
});
|
||||
@@ -3684,7 +3664,7 @@ function queuesMain(board: HTMLElement, movieDetail: MovieView, views: HideableV
|
||||
(event) => {
|
||||
if (event.key === "Escape" && !view.hidden) {
|
||||
event.stopImmediatePropagation();
|
||||
navigate({ kind: "board" });
|
||||
navigate({ kind: "library" });
|
||||
close();
|
||||
}
|
||||
},
|
||||
|
||||
+4
-9
@@ -3,7 +3,6 @@
|
||||
// state passed to pushState, so a manually edited or copy-pasted URL works.
|
||||
|
||||
export type Route =
|
||||
| { kind: "board" }
|
||||
| { kind: "library" }
|
||||
| { kind: "queues" }
|
||||
| { kind: "settings" }
|
||||
@@ -16,9 +15,6 @@ export type Route =
|
||||
|
||||
export function parseRoute(url: URL): Route {
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
if (segments.length === 1 && segments[0] === "library") {
|
||||
return { kind: "library" };
|
||||
}
|
||||
if (segments.length === 1 && segments[0] === "queues") {
|
||||
return { kind: "queues" };
|
||||
}
|
||||
@@ -27,7 +23,7 @@ export function parseRoute(url: URL): Route {
|
||||
}
|
||||
if (segments.length === 1 && segments[0] === "search") {
|
||||
const query = url.searchParams.get("q") ?? "";
|
||||
return query === "" ? { kind: "board" } : { kind: "search", query };
|
||||
return query === "" ? { kind: "library" } : { kind: "search", query };
|
||||
}
|
||||
if (segments.length === 2 && segments[0] === "movies") {
|
||||
const movieId = Number(segments[1]);
|
||||
@@ -70,15 +66,14 @@ export function parseRoute(url: URL): Route {
|
||||
return { kind: "episodeReleases", episodeId };
|
||||
}
|
||||
}
|
||||
return { kind: "board" };
|
||||
// §9.7: the library is the homepage — "/" and every unrecognised path land here
|
||||
return { kind: "library" };
|
||||
}
|
||||
|
||||
export function routePath(route: Route): string {
|
||||
switch (route.kind) {
|
||||
case "board":
|
||||
return "/";
|
||||
case "library":
|
||||
return "/library";
|
||||
return "/";
|
||||
case "queues":
|
||||
return "/queues";
|
||||
case "settings":
|
||||
|
||||
+4
-5
@@ -226,7 +226,7 @@ interface SettingsView {
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
export function settingsMain(board: HTMLElement, views: { hide: () => void }[]): SettingsView {
|
||||
export function settingsMain(views: { hide: () => void }[], goHome: () => void): SettingsView {
|
||||
const view = must<HTMLElement>("#settings");
|
||||
const deck = must<HTMLElement>("#deck");
|
||||
const nav = must<HTMLButtonElement>("#nav-settings");
|
||||
@@ -705,7 +705,6 @@ export function settingsMain(board: HTMLElement, views: { hide: () => void }[]):
|
||||
for (const sibling of views) {
|
||||
sibling.hide();
|
||||
}
|
||||
board.hidden = true;
|
||||
deck.hidden = true;
|
||||
view.hidden = false;
|
||||
nav.setAttribute("aria-pressed", "true");
|
||||
@@ -720,7 +719,7 @@ export function settingsMain(board: HTMLElement, views: { hide: () => void }[]):
|
||||
|
||||
function close() {
|
||||
hide();
|
||||
board.hidden = false;
|
||||
goHome();
|
||||
nav.focus();
|
||||
}
|
||||
|
||||
@@ -729,7 +728,7 @@ export function settingsMain(board: HTMLElement, views: { hide: () => void }[]):
|
||||
navigate({ kind: "settings" });
|
||||
open();
|
||||
} else {
|
||||
navigate({ kind: "board" });
|
||||
navigate({ kind: "library" });
|
||||
close();
|
||||
}
|
||||
});
|
||||
@@ -748,7 +747,7 @@ export function settingsMain(board: HTMLElement, views: { hide: () => void }[]):
|
||||
(event) => {
|
||||
if (event.key === "Escape" && !view.hidden) {
|
||||
event.stopImmediatePropagation();
|
||||
navigate({ kind: "board" });
|
||||
navigate({ kind: "library" });
|
||||
close();
|
||||
}
|
||||
},
|
||||
|
||||
+13
-26
@@ -156,6 +156,8 @@ body {
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* §9.7: the wordmark is a link home — quiet at rest, cyan on approach
|
||||
like every other interactive control */
|
||||
.wordmark {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
@@ -163,6 +165,13 @@ body {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 1;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wordmark:hover,
|
||||
.wordmark:focus-visible {
|
||||
color: var(--accent-bright);
|
||||
}
|
||||
|
||||
.rail-meta {
|
||||
@@ -247,44 +256,26 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.board.powered .lamp {
|
||||
.chain.powered .lamp {
|
||||
animation: lamp-strike 420ms var(--ease-out) both;
|
||||
animation-delay: calc(var(--strike, 0) * 120ms);
|
||||
}
|
||||
|
||||
.board.powered .lamp[data-state="off"] {
|
||||
.chain.powered .lamp[data-state="off"] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lamp[data-state="probing"],
|
||||
.board.powered .lamp {
|
||||
.chain.powered .lamp {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- board and chain ------------------------------------------------- */
|
||||
|
||||
.board {
|
||||
width: 100%;
|
||||
max-width: 68rem;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-12) var(--space-6);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* the deck swaps in over the board; flex would beat [hidden] otherwise */
|
||||
.board[hidden] {
|
||||
display: none;
|
||||
}
|
||||
/* ---- signal chain (§9.7: a settings section, no route of its own) ----- */
|
||||
|
||||
.chain {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chain {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2.5rem 1.2fr 2.5rem 1fr;
|
||||
grid-template-areas:
|
||||
@@ -1813,10 +1804,6 @@ body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.board {
|
||||
padding: var(--space-6) var(--space-4);
|
||||
}
|
||||
|
||||
.chain {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
|
||||
Reference in New Issue
Block a user