Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 7811b9d555af81729d0021919ceac30c9394b534
parent c9461ecfc74d5a21a96964b39b1f68e48161c117
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 13 Sep 2026 11:09:06 -0400

plans: /channels rack redesign plan

Copy the approved rack redesign plan for the editor /channels page into
the repo so the implementation commits have a tracked reference.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Aplans/editor-channels-rack.md | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 301 insertions(+), 0 deletions(-)

diff --git a/plans/editor-channels-rack.md b/plans/editor-channels-rack.md @@ -0,0 +1,301 @@ +# `/channels` rethink: one instrument, one deck + +Pinned to `main` @ `a1058d0` (2026-09-13). + +## Context + +`/channels` is the editor's channel console: 67 rows, 16 columns, three bulk mechanisms and +a pipeline band per operation per row. Two features landed on it in the 2026-09 integration +without a design pass, and the operator's first use of the merged page found it messy: + +- **Two selection bars.** `ChannelBulkBar` (priority) is `sticky bottom-0` and floats at the + viewport edge; `ChannelStorageBulkBar` is a plain block after the 67-row table, reachable only + at the very bottom. Both say "N selected", both have Clear. On `<md` the sticky bar pins to + the table's `overflow-x-auto` box, not the screen. The storage bar's root input truncates and + its button label (`Move media to {root}`) is cut to "Move media to…". +- **Rows are ~90 px.** The Tier cell is a four-line stack (select, chips, held note, Advanced), + and with a corpus-wide focus active the orange `Held — focus: omnimirror` note repeats on 60+ + rows: the most repeated element carries the least per-row information. 67 rows ≈ 6,000 px. +- **The table overflows the viewport.** At 1440 px (1216 px of content beside the sidebar) the + six band columns and the Actions column sit off-screen; the wrapper is `overflow-visible` on + `md+`, so it spills rather than scrolls. +- **The header has no hierarchy.** Four dark pills ("Sync every channel", "Full sweep every + channel", "Update all reports", "New channel") share one weight. +- **The page's honesty lives in its footer.** The band legend, the Names·A/T explainer and the + report-age note (`data-testid="channels-freshness"`) are stacked in 11 px type under the + floating bar. + +Decisions taken with the operator 2026-09-13: full-page rethink; a corpus-wide focus is stated +**once** above the table and each held row shows a small chip with the full reason as +screen-reader text. + +## The design plan + +**Subject.** An operator's console for a 67-channel corpus. Its single job: see every channel's +state in one glance and act on a few. The vocabulary already in the product is the instrument: +"transit line", "stations", "bands", "rail/strip", "runner", "lane". The page should read as a +**rack** — a scrolling column of channel strips under a fixed meter-bridge header, with the +selection controls docked below like a console's master section — not a document with widgets +stapled to its top and bottom. + +**Colour.** No new colour. The editor runs the neutral base family from +`common/styles/tokens.css`; the page keeps it and keeps the band rule that `reachable` +(`--info` #2563eb) is the **only** saturated colour. The palette in use: + +| role | token | +|---|---| +| chrome (header, toolbar, deck) | `bg-card/95 backdrop-blur border-border` — the channel page's sticky-header idiom (`[slug]/layout.tsx:43`) | +| rack ground | `bg-background`, rows `border-t border-border`, meter bridge `bg-surface` (#f7f7f8 / #141416) | +| identity | `text-foreground` slug in `font-mono`; name in `text-muted-foreground` | +| the one accent | `--info`, via `StateBand` only | +| attention | `--warning` for held chips, stale/missing report; `--destructive` only for unreachable media | + +**Type.** Source Sans 3 body, Source Code Pro for slugs, counts and the editor's signature +micro-caps eyebrow (`font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground`, +already the nav-group and lane-strip label style). Eyebrows are the structural device on this +page: they name the *verb groups* in the deck (Tier · Focus · Media), the *corpus actions* in +the header, and the *pipeline* block in the table head. They encode something true: these are +groups of controls, not decoration. Tabular numerals (`tabular-nums`) on every count and date. + +**Layout.** On `md+` the page stops scrolling the document. It is a flex column the height of +the viewport: header → focus line → instrument bar → **a scroll region holding the table** → +deck (only with a selection). Inside the region the `<thead>` is `sticky top-0` and the +checkbox + Slug cells are `sticky left-0`, so the identity column and the meter bridge header +never leave the screen while 16 columns scroll horizontally. On `<md` the document scrolls as +today and the deck is `sticky bottom-0` **outside** the overflow wrapper, so it pins to the +screen. + +``` +┌ sidebar ┐┌────────────────────────────────────────────────────────────────────────┐ +│ ││ Channels EVERY CHANNEL │ +│ ││ 67 channels · oldest report Aug 27, 5:58 PM [Sync every] [Full sweep]│ +│ ││ (channels-freshness, moved up) [Update reports] ▮New ch.▮│ +│ ││────────────────────────────────────────────────────────────────────────│ +│ ││ FOCUS omnimirror · 61 held [site ▾] [Focus site] [End focus] │ +│ ││────────────────────────────────────────────────────────────────────────│ +│ ││ [x] Group by section PIPELINE ▪ done ▮ can run ░ blocked … │ +│ ││┌─────────────────────────────────────────────────────────────────────┐│ +│ │││☐ Slug Name Handling Build Tier Playlist Last sync … ││ sticky thead +│ │││☐ chibi-rev… Chibi R… youtube ●Incl. Normal▾ 9658 Sep 11 10:24 ││ +│ │││☑ chrissie-m Chrissie youtube ●Incl. Normal▾ [held] 3528 … ││ 40px rows +│ │││ … (scrolls vertically; ☐ + Slug sticky left; bands scroll right) ││ +│ ││└─────────────────────────────────────────────────────────────────────┘│ +│ ││┌ deck (docked, only with a selection) ───────────────────────────────┐│ +│ │││ 2 selected │ TIER [Normal▾] [Apply tier] │ FOCUS [Focus these] ││ +│ │││ │ MEDIA [/mnt/platter/archilyzer-media] [Move media] ││ +│ │││ Queued 1 · skipped 1 · view jobs [Clear] ││ +│ ││└─────────────────────────────────────────────────────────────────────┘│ +└─────────┘└────────────────────────────────────────────────────────────────────────┘ +``` + +Mobile (`<md`): + +``` +│ Channels │ +│ 67 channels · oldest report … │ +│ EVERY CHANNEL │ +│ [Sync every channel] │ +│ [Full sweep every channel] │ +│ [Update all reports] ▮New channel▮│ +│ FOCUS omnimirror · 61 held │ +│ [site ▾] [Focus site] [End focus] │ +│ ┌ table, overflow-x-auto ───────┐ │ +│ │☐ slug (sticky left) | … → │ │ +│ └───────────────────────────────┘ │ +│ ┌ deck sticky bottom-0 ─────────┐ │ ← outside the overflow wrapper +│ │ 2 selected [Clear] │ │ +│ │ TIER [Normal▾] [Apply tier] │ │ +│ │ FOCUS [Focus these] │ │ +│ │ MEDIA [root……] [Move media] │ │ +│ └───────────────────────────────┘ │ +``` + +**Signature.** The **meter bridge**: the six band columns are one block, not six loose grey +dashes. Their header cells share one eyebrow (`PIPELINE`) with the legend swatches inline; their +body cells sit on `bg-surface` with a 1 px `border-l border-border` on the first, so the +strips read as one instrument across the row. Nothing about `StateBand` changes (no new fill, +no animation at `strip` scale). This is the one place the page spends its boldness; the deck, +header and rows are quiet. + +**Self-critique before building.** The generic answer to "messy table page" is cards, a +search box, a filter chip row and a floating action button. None of that: the IA record rules +out cards (columns must stay locked for cross-group comparison), there is no search or filter +on the page today and none is asked for, and a FAB would be a third selection mechanism. The +rack-with-docked-deck is specific to this page's real problem (16 columns × 67 rows plus two +kinds of bulk verb) and reuses the product's own instrument vocabulary. The eyebrows are the +editor's existing signature move, not an import. The one risk taken (the bridge) is layout, +not colour, so it survives all eight token families. + +## Rules this must honour (from the plans, verified) + +- Every e2e accessible name on the page is **unchanged**; only DOM position and styling move. + The pinned set: `select all channels`, `select {slug}`, `channel priority bulk`, `bulk tier`, + `Apply tier`, `Focus these`, `channel focus`, `focus site`, `Focus site`, `End focus`, + `bulk media root`, `move media for selected channels`, `bulk media move result` (+ `title`), + `bulk media move error`, `priority bulk error`, `held reason for {slug}` (must **contain the + full text** `Held — focus: …`; `toHaveCount(0)` when not held), `focused-{slug}`, + `tier for {slug}`, `advanced priority for {slug}`, `{op} override for {slug}`, + `sync only for {slug}`, `report age for {slug}`, `playlist count for {slug}`, + `{op} count for {slug}` (sr-only text beside the band), `toggle build inclusion for {slug}`, + `sort by {label}` + `aria-sort` on the `<th>`, `group-name`, `rowheader`, `Group by section`, + `sync group {name}` etc., `sync every channel` / `sync all result`, `update all reports` / + `update all reports result`, `New channel` link, `channels-freshness`, `Active site`, the + h1 `Channels`, and the eight fixed header strings Slug, Name, Handling, Build, Tier, + Playlist, Last sync, Report. +- `channel priority bulk` and `bulk media root` must have **count 0 with an empty selection** + and the root box must carry `settings.storage.mediaRoot` as soon as anything is selected — + so the deck unmounts when empty and shows all three verb groups at once (no mode switch). +- The storage verb keeps **no preview gate** and one root for the batch + (`ChannelStorageBulkBar.tsx:8-20`); `Queued N · skipped M` with reasons on `title`. +- Tier is a `<select>`; the five per-operation overrides stay behind the `Advanced` disclosure; + one writer (`saveChannelPriorityAction`); focus is corpus-wide and needs no selection. +- `MediaLocationBadge` appearances unchanged (nothing on in-place rows). +- Row dimming keys off `excludeFromBuild || tier === "paused"` only. +- Selection is a `Set` of slugs intersected with what is on screen; survives sort/group toggle. +- `StateBand`: exactly one saturated colour, fill pattern first, no animation at `strip`. +- The freshness note is an obligation, not decoration; the Names·A/T explainer stays. +- No sort/group state in the URL. + +## Changes, by file + +All under `editor/app/channels/` unless noted. New components go in `components/`. Add by +path, commit per step, tsc green at every commit. + +### 1. `components/ChannelSelectionDeck.tsx` (new) — replaces both bars + +- Props: `slugs`, `onClear`, `defaultRoot`. Returns `null` when `slugs.length === 0`. +- One root element `aria-label="channel priority bulk"` (keeps the spec's handle; the storage + controls are inside it, addressed by their own labels). Chrome: + `border-t md:border md:rounded-md border-border bg-card/95 backdrop-blur shadow-lg + motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2`. +- Row 1, `flex flex-wrap items-center gap-x-4 gap-y-2`: `{n} selected` (`font-medium + tabular-nums`), then three groups each `flex items-center gap-2 pl-4 border-l border-border` + with an eyebrow (`TIER`, `FOCUS`, `MEDIA`): Tier = `<select aria-label="bulk tier">` + + **Apply tier**; Focus = **Focus these**; Media = `<input aria-label="bulk media root">` + (`font-mono w-72 md:w-80`, seeded from `defaultRoot`) + button `aria-label="move media for + selected channels"` whose **visible text is `Move media`** (the root is in the box beside it; + the old `Move media to {root}` label is what truncated) + `Queueing…` while pending; then + `ml-auto` **Clear**. +- Row 2 (only when there is something to say): the storage result span (`aria-label="bulk + media move result"`, `Queued N · skipped M · view jobs`, reasons on `title`), the storage + error (`role="alert" aria-label="bulk media move error"`), the priority error + (`role="alert" aria-label="priority bulk error"`). +- Behaviour moved verbatim from the two old components: `setChannelTierAction` and + `focusChannelsAction` call `onClear` on success; the storage move does not; the storage + header comment about "no preview gate" moves with it. +- Delete `ChannelStorageBulkBar.tsx` and the `ChannelBulkBar` export from `ChannelBulkBar.tsx` + (keep `ChannelFocusBar` there, or move it to its own file — see step 4). + +### 2. `components/ChannelsTable.tsx` — the rack + +- **Structure.** The outer wrapper becomes `flex flex-col min-h-0` (it is inside the page's + flex column, step 5). Order: instrument bar → scroll region → deck. The deck is a sibling of + the scroll region, never inside the `overflow` element. +- **Scroll region.** `div.relative.min-h-0.flex-1.overflow-auto.md:rounded-md.md:border + .border-border` (border-y only below `md`). Drop `md:overflow-hidden` from the `<table>` — + it would become the sticky ancestor and defeat the sticky header; rounded corners come from + the region. +- **Sticky header.** `<thead className="sticky top-0 z-20 bg-muted">`. Group header rows + (`ChannelGroupHeaderRow`) get `sticky top-[var(--thead-h)] z-10` on `md+` so a section's name + stays visible while its rows scroll (measure `--thead-h` once with a ref; fall back to a fixed + `top-9`). +- **Sticky identity.** The checkbox `<th>/<td>` `sticky left-0 z-10` and the Slug cells + `sticky left-8 z-10`, both with `bg-muted` (head) / `bg-background` (body) and a + `shadow-[1px_0_0_var(--color-border)]` right edge so they read as pinned when the rest + scrolls. Rows keep `border-t border-border`; selected rows get `bg-accent/60`. +- **Row density.** Cells `px-2 py-1.5 align-middle` (was `px-3 py-2`); target row height 40 px + with a closed Tier cell. `Td` for dates: `whitespace-nowrap tabular-nums text-xs + text-muted-foreground`, formatted `toLocaleString(undefined, { dateStyle: "medium", + timeStyle: "short" })` (`Sep 11, 2026, 10:24 AM`). Report cell keeps `stale`/`missing` in + `text-warning`. Playlist `text-right tabular-nums`. Handling becomes a mono micro-tag + (`font-mono text-[11px] text-muted-foreground`), same text. +- **Meter bridge.** The pipeline `<th>`s: a `<th colSpan={columns.length}>` is NOT added (it + would change `columnheader` semantics); instead each band header keeps its own sortable + button and the FIRST band header carries the eyebrow `PIPELINE` above its label and the + legend swatches render inline in the instrument bar (step 3). Body: each band `<td>` gets + `bg-surface`, the first `border-l border-border`, the last `border-r border-border`; width + `w-24 min-w-20`. `PipelineCell` unchanged (sr-only count + `StateBand size="strip"`). +- **Instrument bar** (the row above the region): `flex items-center justify-between gap-3 + px-1 pb-2 text-xs text-muted-foreground`: left = the existing `Group by section` checkbox + (when sections exist); right = `<BandLegend />` plus, behind a `<details>` labelled + `Names·A / Names·T`, the two-route explainer paragraph (text unchanged). +- **The deck** replaces lines 446–454; the legend block at 455–464 is dissolved into the + instrument bar. `colSpan` stays `10 + columns.length`. +- **`<md`.** The region is `overflow-x-auto -mx-4` as today (no fixed height; the document + scrolls); the deck sits after the wrapper with `sticky bottom-0 z-20`. + +### 3. `components/ChannelTierSelect.tsx` — one line + +- Root becomes `flex flex-wrap items-center gap-1.5` (not a vertical stack); `min-w-36` → + `min-w-32`. Order: `<select aria-label="tier for {slug}">` (h-7, text-xs), `Focused` chip + (`data-testid="focused-{slug}"`, unchanged), `N pinned` chip (unchanged), the **held chip**, + the `Advanced` `<details>` summary (unchanged label/aria; its open panel is + `absolute z-30 mt-1 rounded-md border border-border bg-popover p-2 shadow-md w-64` inside + a `relative` wrapper so opening it does not reflow the row — it overlays the rows below). +- **Held chip.** Replaces the orange `role="note"` line. Keeps `role="note"` and + `aria-label="held reason for {slug}"`; visible text is a warning pill + (`rounded-full border border-warning/30 bg-warning-soft text-warning px-1.5 text-[10px] + uppercase tracking-wide`) reading `held`, plus an `sr-only` span holding the **full reason + text** (`Held — focus: site Focusable`) and the same on `title`. `textContent` therefore still + contains the pinned substring. Rendered only when `heldReason` is set (so `toHaveCount(0)` + holds). +- The error span stays (`text-destructive text-xs`), wrapping to a second line only on error. + +### 4. Focus line — `ChannelFocusBar` (in `ChannelBulkBar.tsx`, or moved to `ChannelFocusBar.tsx`) + +- Keeps `aria-label="channel focus"`, the `focus site` select, **Focus site**, **End focus**, + `focus error`, and the `Focus: {label}` / `No focus` text (the spec asserts + `toContainText("Focus: site Focusable")` and `"No focus"`). +- New prop `heldCount` (computed in `ChannelsTable` as the number of rows whose + `priority.heldReason` is set): renders ` · {n} held` after the label when a focus is active. +- Chrome: a single line, `flex items-center gap-3 px-1 py-1.5 text-sm` with an eyebrow + `FOCUS` first; no card border (it is a statement, not a panel); `border-b border-border` + under it. Buttons `h-7 text-xs`. + +### 5. `page.tsx` — header hierarchy and the honesty line + +- Page root: `flex flex-col gap-3 md:h-[calc(100vh-3rem)] md:min-h-0` (main has `py-6`; + the sidebar is `md:h-screen`, so the content column matches). Below `md` no fixed height. +- Header: left = `<h1>` (unchanged) + the freshness paragraph **moved here** as the subtitle + (`data-testid="channels-freshness"`, same text, `text-xs text-muted-foreground`, prefixed + with `{count} channels · `). Right = a cluster: eyebrow `EVERY CHANNEL` above + `SyncAllChannelsButton` + `RefreshAllReportsButton` restyled as **outline** buttons (`h-8 + text-xs border border-border bg-transparent hover:bg-accent`), then the `New channel` link + as the **only** primary (`bg-primary text-primary-foreground`). Button names and result + spans unchanged (`sync all result`, `update all reports result`). +- The empty state (`No channels yet.`) unchanged. +- The freshness paragraph is removed from the bottom (it moved, not duplicated). + +### 6. Header buttons — `SyncAllChannelsButton.tsx`, `RefreshAllReportsButton.tsx` + +- Accept a `variant` or simply take the outline classes; the inline result span moves to a + `title`-less line under the cluster on `<md` (still the same element and label). + +### 7. Nothing else moves + +No server action, no `actions.ts`, no `common/` change, no e2e spec renamed. `MediaLocationBadge`, +`ChannelBuildToggle`, `ChannelGroupHeaderRow`, `ChannelGroupLine`, `PipelineCell`, `StateBand`, +`BandLegend` are reused as they are (only classes on their host cells change). + +## Verification + +1. `pnpm -r exec tsc --noEmit` clean; `pnpm --filter yt-dlp-transcript-common test` unchanged + (1159 on `main`); `pnpm --filter editor exec next build` clean. +2. Editor e2e, from a worktree (never the primary checkout), the channel set first: + `channels.spec.ts`, `channels-sort.spec.ts`, `channels-counts.spec.ts`, + `channels-actions.spec.ts`, `channel-priority.spec.ts`, `channel-storage.spec.ts`, + `channel-groups.spec.ts`, `channel-work.spec.ts`, `channel-build-toggle.spec.ts`, + `site-scope.spec.ts`, `navigation.spec.ts`, `perf-budget.spec.ts`; then the FULL suite + (533 on `main`; known flakes `video-page.spec.ts:216`, `backfill.spec.ts:457`). Detached + behind the queue lock, watched with Monitor. +3. Visual check against the "before" PNGs: re-run the screenshot script against the + worktree's dev server (its own port block; a copy of the e2e fixture corpus, never + `transcripts/`) at 1440×900 and 390×844, with and without a selection, plus one row's + Advanced open. Confirm: one deck; rows ≈ 40 px; header and identity column stay put while + the bands scroll; the deck reachable on mobile without scrolling the table; keyboard focus + rings visible on every control; `prefers-reduced-motion` disables the deck's entry animation. +4. Accessibility: tab order header → focus line → instrument bar → table → deck; the held + chip's accessible name reads the full reason; the sticky cells do not trap focus. +5. Record: `editor/CHANGELOG.md` entry; a "Shipped" section appended to this file with the + before/after screenshots' paths and the one-paragraph rationale (rack + docked deck + + meter bridge), and the e2e counts.