Archilyzer · Source

archilyzer

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

commit f5e8cc2fa223184ac96d355b206e45a6ab3e5718
parent 41bc8118433f6b88a4ac9d720678dcd7f69d103d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 20:43:18 -0400

plans: brand + themes — the Found-line mark, the subject-split wordmark, and base × accent reader themes (S0 → S1 ‖ S2 → S3)

Operator decisions of 2026-09-25 are final: mark D, ink dark base, Anilyzer
Sakura / Jasolyzer Green / Archilyzer keeps Signal, all five theme families
retire. Contrast-corrected light/sepia accents in this plan beat the canvas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Aplans/brand-and-themes.md | 414+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 414 insertions(+), 0 deletions(-)

diff --git a/plans/brand-and-themes.md b/plans/brand-and-themes.md @@ -0,0 +1,414 @@ +# Brand + themes: the Found-line mark, and base × accent + +Status: APPROVED 2026-09-25 — implementing on `brand/found-line` (worktree `../brand-found-line`, +pnpm wt block #2). Slices S1 and S2 branch from S0's tip as `brand/mark` and `brand/themes`. + +Design canvas: https://claude.ai/artifact/UsUxwgRkP5a3m4jZXucAvG (boards *Family*, *In context*, +*Themes · base × accent*, and the rejected directions A–C). Read it with Artifact +`action:"read"`; republish by staging under the primary checkout's gitignored +`node_modules/.brand-canvas/project/` (Artifact publish only accepts paths inside the repo). +**Where this plan and the canvas differ, the plan wins** — notably the contrast-corrected +on-light and on-sepia accent values below. + +## What the operator asked for (2026-09-25) + +1. One logo and brand for Archilyzer and every official child site. +2. A reader theme made of two independent choices: a **base** (Light / Sepia / Dark, plus + System) and an **accent**. Every site defaults to its own accent; a reader can pick any other. + +## Decisions (final) + +- **Mark D · Found line.** Four transcript lines on a rounded square; the second is lit and + carries a play head. +- **The wordmark splits on the subject's name,** not on "lyzer". +- **The dark base is warm ink,** not graphite. +- **Accent swaps:** Anilyzer takes Sakura and Jasolyzer takes Green, because Archilyzer keeps + its teal ("Signal"). +- **All five theme families retire:** base, archive, selenized, swiss and archilyzer. +- **No new dependencies expected.** `rsvg-convert`, `magick` and `inkscape` are on the machine, + and `next/og` bundles satori and resvg. sharp's native build failed in the worktree; nothing + here needs it. + +## The design (source of truth) + +### Mark + +Everything is in a 512 viewBox. The shapes, back to front: + +| Part | Shape | Colour | +|---|---|---| +| ground | rect, `rx=112` | ground | +| line 1 | rect x112 y128 w288 h44 rx22 | dim | +| play head | polygon `112,210 172,234 112,258` | lit | +| line 2 (the found line) | rect x188 y212 w212 h44 rx22 | lit | +| line 3 | rect x112 y296 w232 h44 rx22 | dim | +| line 4 | rect x112 y380 w152 h44 rx22 | dim | + +Icon palettes: +- **Child site:** ground `#0c0a08`, dim `#3b3327`, lit = the site accent's on-dark value. +- **Archilyzer** (homepage, hub, editor): ground `#151b20`, dim `#3f4c56`, lit `#e7edf1` + (bone). The parent mark is achromatic. + +Icon variants: +- `any`: `rx=112`. +- `maskable`: full-bleed ground, the mark scaled 0.8 about (256,256). +- `apple`: full-bleed ground at scale 1. + +### Wordmark + +Archivo at `font-stretch:118%`. The lead is weight 720 in `--foreground`; the suffix is weight +380 in `--muted-foreground`. Two adjacent spans with no whitespace between them; the link's +accessible name is the full title. + +| Site | Lead | Suffix | +|---|---|---| +| Archilyzer | Archi | lyzer | +| Jeralyzer | Jer | alyzer | +| Rekietalyzer | Rekieta | lyzer | +| Hasanalyzer | Hasan | alyzer | +| Anilyzer | Ani | lyzer | +| Bonnellyzer | Bonnell | yzer | +| Jasolyzer | Jaso | lyzer | + +### Accents + +The on-light and on-sepia values are contrast-corrected: each must reach ≥4.5:1 against its +ground and with white ink. A unit test enforces it. + +| id | Name | Site default | On dark | On light | On sepia | +|---|---|---|---|---|---| +| `signal` | Signal | Archilyzer (homepage, hub, editor) | `#5fa8a0` | `#2e7b73` | `#2b756e` | +| `brass` | Brass | jeralyzer | `#e3b15c` | `#95661a` | `#8e6119` | +| `vermilion` | Vermilion | rekietalyzer | `#ec7a52` | `#b3431f` | `#b3431f` | +| `violet` | Violet | hasanalyzer | `#b49cf2` | `#6a4bc4` | `#6a4bc4` | +| `sakura` | Sakura | anilyzer | `#ee8fb5` | `#a83a6a` | `#a83a6a` | +| `blue` | Blue | bonnellyzer | `#74a9f2` | `#2d5fb8` | `#2d5fb8` | +| `green` | Green | jasolyzer | `#7cc46a` | `#3f7a2c` | `#3d772b` | + +### Bases + +Each base declares all 48 colour tokens listed in `homepage/e2e/theme.spec.ts:63-112`, plus +`color-scheme`. + +| Base | Source | Values | +|---|---|---| +| Light | today's `archilyzer` light block | bg `#f3f6f7`, surface `#e8edef`, fg `#161c21`, muted-fg `#55646e`, border `#d2dade`, border-strong `#aeb9c0`, faint `#78868f` | +| Sepia | new | bg `#f4ecd8`, card `#faf4e6`, fg `#33281a`, muted-fg `#6b5c43`, border `#dccdaa` | +| Dark | today's `archive` dark (ink) | bg `#0c0a08`, surface `#141009`, fg `#efe7d8`, muted-fg `#a39a86`, border `rgba(233,220,197,.1)` | + +- **Sepia** derives its status, panel and chart values from the archive paper block, darkened + for a light ground. +- **Dark** adds the `--destructive-soft` and `--state-gone(-soft)` it lacks today. +- **Charts do not follow the accent:** each base has its own fixed `--chart-1..5`. Re-validate + chart-3 against `--state-gone` with the dataviz skill's validator (FACTS.md ~6442 records a + real past collision), and keep chart-1 ≠ Signal. + +### Type and radius + +Three faces everywhere: +- Archivo as `--font-display`, with the `wdth` axis. `.font-display` gets `font-stretch:112%`. +- IBM Plex Sans as `--font-sans`, weights 400–700, normal + italic. +- IBM Plex Mono as `--font-mono`. + +`--radius: 0.375rem` on `:root`. + +## Slices + +``` +S0 palette + data ──┬──> S1 brand (mark, icons, wordmark) ──┐ + └──> S2 themes (tokens, runtime, picker) ┴──> S3 integrate + gates ──> operator rollout +``` + +Cadence: the planner/reviewer orchestrates; one Opus general-purpose implementer per slice +writes the code, under [`tools/implementer-rules.md`](tools/implementer-rules.md). S1 and S2 run +as two parallel implementers, each in its own worktree cut from S0's tip +(`pnpm wt add brand/mark --from brand/found-line`, `pnpm wt add brand/themes --from brand/found-line`). +Review fixes go back to the same agent. Sonnet or Haiku do exploration and extraction. + +**Conflict avoidance between S1 and S2:** +- In `export/app/layout.tsx` and `homepage/app/layout.tsx`, S1 edits only the `icons` metadata; + S2 edits only the theme constants, `generateViewport` and the `RootLayout` `<html>`. +- `Header.tsx` and `Footer.tsx` belong to S1; `ThemeMenu`, `MobileMenu` and `ThemeToggle` belong + to S2. +- S1 colours the header mark with `var(--brand-mark, var(--brand))`, so it works before S2 lands. +- S1 puts new assertions in a new `export/e2e/brand.spec.ts`; S2 owns + `site-branding.spec.ts:75-86`. +- Whichever of S1/S2 merges second merges `main` and re-gates. +- Release 9 slice C2 (`one-core/c2-federated-search`) is in flight on the hub search + components; neither slice touches `SearchResults.tsx`, `FiltersPanel.tsx` or + `ArchiveShelf.tsx` (they receive hexes and keep working unchanged). + +### S0 — palette + data (`brand/found-line`, alone) + +**New `common/lib/brand.ts` (pure):** +- `ACCENT_IDS` and an `ACCENTS` table, `{id, name, onDark, onLight, onSepia}` per accent. +- `DEFAULT_ACCENT = "signal"`. +- `BASE_GROUNDS = {light: "#f3f6f7", sepia: "#f4ecd8", dark: "#0c0a08"}`. +- `ICON_PALETTES` for child sites and for Archilyzer. +- The `MARK` shape list and `markSvg(palette, {variant})`. +- `splitWordmark(title, lead?)`. + +`common/lib/project.ts` gains `PROJECT_WORDMARK_LEAD = "Archi"`. + +**`common/lib/accent.ts`:** +- Keep `parseAccent`. +- Add `parseAccentSetting(v)`: an accent id, a `"#rrggbb"`, or undefined. +- Add `resolveAccent(v)`: `{id | "custom", light, sepia, dark}`. A custom hex is darkened or + lightened per ground until it reaches 4.5:1. +- Add `accentHex(v)`. The published value is always a hex: an id becomes its on-dark value. +- Add `customAccentVars()`. +- `siteAccentVars` is deleted in S2. + +**`common/lib/siteSchema.ts`:** +- `accent` parses through `parseAccentSetting` (around :238, :302, :321), with new doc text + (:110). +- New optional `wordmarkLead`, after `headerTitle`. Stored only when it is a proper prefix of + `headerTitle`; needs type, `SITE_FIELD_DOCS`, schema and `siteToDisk` entries. +- Regenerate SITE.md: `pnpm --filter yt-dlp-transcript-common exec tsx bin/file-schemas-docs.ts`, + checked with `--check`. + +**The published accent stays a hex.** Third-party hubs read other sites' `/site.json`, so these +route through `accentHex`: +- `common/lib/siteDescriptor.ts:~87` +- `common/bin/compose-hub.ts:~94` +- `common/lib/homepageSummary.ts:~431` + +**Editor form** (`editor/app/sites/components/SiteForm.tsx:229-234`, +`editor/app/sites/actions.ts:~45-51,169-191`): +- "Brand accent" becomes a native radio group of the 7 swatches plus "Custom", with a hex field. +- A "Wordmark lead" field. The single writer is `writeSite` in `common/lib/site.ts`. +- `Field` puts the hint inside the `<label>`, so the new labels and hints must not contain + "header title", "site title", "site id" or "public url" — `sites-crud`'s `getByLabel` would hit + strict-mode violations. +- `hubSite()` (`export/app/lib/site.ts:~29`) sets `wordmarkLead: PROJECT_WORDMARK_LEAD`. + +**Tests:** `brand.test.ts` (contrast ≥4.5 everywhere, `markSvg` geometry); `accent.test.ts`; a +siteSchema round-trip with an id and `wordmarkLead`; siteDescriptor publishes a hex for an id; +`fileSchemaDocs.test.ts` passes on the regenerated SITE.md. + +**e2e:** editor `sites-crud` and `branding` (the root `pnpm e2e` is the editor suite); export +`site-branding`; `pnpm --filter export run e2e:hub`. + +### S1 — mark, icons, wordmark (`brand/mark`) + +**Step 1 is a spike, timeboxed to about an hour:** +1. Seed the `export/public` symlinks (implementer-rules.md, under `sh`). +2. Build a PNG icon via `app/icons/[file]/route.ts`. +3. `pnpm --filter export exec next build`. +4. Check `out/icons/icon-192.png`: PNG magic, a 192×192 IHDR, and look at it. +5. Check `out/favicon.ico` starts with `00 00 01 00`. +6. Repeat for the homepage. + +**If the spike fails:** +- The PNG route fails: call the same renderer from compose-site / compose-hub / + compose-homepage and write into `public/icons`. The last resort is `rsvg-convert`. +- Only the ICO fails: check in a `public/favicon.ico` family mark generated once by + `common/bin/brand-icons.ts`. Next reserves `/favicon.ico` as a metadata route + (`is-metadata-route.js:114`). + +**Static route handlers are safe under static export.** Under `output:"export"` a GET route +handler with `generateStaticParams` is copied byte-for-byte to its exact path +(`next/dist/export/index.js:~722-737`). `app/icon.tsx` is unusable: its URLs are always hashed. + +**`common/lib/brandIcons.ts`:** +- `ICON_FILES`: `icon.svg`, `maskable.svg`, `icon-32.png`, `icon-192.png`, `icon-512.png`, + `maskable-512.png`, `apple-touch-icon.png`. +- `renderIconPng()` — `ImageResponse` from `next/og` on the node runtime, with an `<img>` SVG + data URI via `createElement`. +- `pngToIco()` — a 6-byte header, 16-byte entries, then the PNG payloads. + +**Routes:** `export/app/icons/[file]/route.ts` and a homepage twin. +- `dynamic = "force-static"`, `dynamicParams = false`; never read the request. +- The palette is Archilyzer in hub mode (`instanceMode()`), otherwise the site's resolved accent + via `currentSite()`. +- Delete `export/public/icons/*`, `homepage/public/icons/*` and both `app/favicon.ico`. +- Metadata declares `icon.svg` (type `image/svg+xml`), `icon-32.png`, 192, 512 and apple. +- The homepage OG image keeps `/icons/icon-512.png`. + +**Manifest** (`export/app/manifest.ts`): `theme_color = BASE_GROUNDS.dark`; `background_color` is +the icon ground; add `icon.svg` with sizes "any". `pwa.spec` requires the 192/512/maskable +entries and the four PNG paths. + +**Service workers:** in `export/service-worker/site-sw.js:~28` and `sw-hub.js:~20`, rename +**only** the `SHELL` cache to `shell-v2`. Bumping `VERSION` would make activate delete readers' +offline channel downloads. + +**Components:** +- New `common/components/BrandMark.tsx`: inline SVG, `aria-hidden`, `focusable="false"`, colours + through `style`, not `fill="var()"`. +- New `common/components/Wordmark.tsx`. +- `export/app/components/Header.tsx:68-73`: the rotated square becomes an ink tile plus + `var(--brand-mark)` — the header follows the reader's accent while the favicon keeps the site + default. The Wordmark uses `site.wordmarkLead`. The hub reuses this header and gets the + Archilyzer mark. +- `homepage/app/components/Header.tsx:39-51`: the CSS triangle becomes the bone mark, and + "Archilyzer" becomes the split wordmark, no longer tracked uppercase. Keep + `aria-label="Archilyzer home"`. +- `export/app/components/Footer.tsx`: a small Archilyzer mark before the credit, outside the + link. The link's name stays exactly "Archilyzer". +- Editor sidebar (`editor/app/layout.tsx:~133-144`): a mark beside `adminTitle`, not split. +- Editor favicon: a static `editor/app/icon.svg`, with a parity test against `markSvg`. + +**Fixture:** `"wordmarkLead":"Fixture"` in `export/e2e/fixtures/sites/testsite/site.json`. + +**Tests:** new `export/e2e/brand.spec.ts` (split spans, link name, mark present, footer credit); +`pwa.spec.ts:30-42` adds content-type, PNG magic, `icon.svg` and `favicon.ico`; the hub's +`/icons/icon.svg` contains `#151b20` / `#e7edf1`; unit tests for the ICO header, and the PNG IHDR +if `next/og` runs under tsx. + +### S2 — base × accent (`brand/themes`) + +**`common/styles/tokens.css`:** +- Keep `@custom-variant dark (&:where(.dark, .dark *))` (:28). `.dark` is on `<html>` iff the + resolved base is dark, so the 47 `dark:` usages keep working and sepia styles as light. +- Delete every `[data-theme=…]` block, the `.dark{}` token block and the per-family font/radius + rules (:99-141). +- Base blocks, each declaring all 48 tokens: `:root, html[data-base="light"]`, + `html[data-base="sepia"]`, `html[data-base="dark"]`. Each also declares: + - `--swatch-<id>`: that ground's value for each of the 7 accents; + - `--swatch-custom: var(--accent-custom-<base>, var(--swatch-signal))`; + - `--brand: var(--swatch-signal)` as the default; + - `--brand-strong`: `color-mix` toward white on dark, toward black on light/sepia; + - `--brand-soft`: `color-mix` at 16% on dark, 12% on light/sepia; + - `--brand-ink`: `#0c0a08` on dark, `#fff` on light/sepia. +- **After** the base blocks (same specificity, so later wins): + `html[data-accent="<id>"]{--brand:var(--swatch-<id>); --brand-mark:<onDark>}` for each of the + 7, and `html[data-accent="custom"]{--brand:var(--swatch-custom); --brand-mark:var(--accent-custom-dark)}`. +- `--primary` stays **neutral**, not the accent: light `#202a31`, sepia `#33281a`, dark + `#efe7d8`, each with a ground-coloured foreground. `--ring: var(--brand)`. + +**`common/styles/fonts.ts`:** Archivo, Plex Sans and Plex Mono only — Source Serif, Source Sans, +Source Code and JetBrains Mono go. Update the comments in both `globals.css` files. + +**`common/components/themeConfig.ts`:** +- `BASE_KEY = "ytdlp-tb:base"`; `ACCENT_KEY` now stores an accent id; `LEGACY_THEME_KEY` / + `LEGACY_MODE_KEY` are the old theme/mode keys. +- `ThemeBase = light | sepia | dark | system`. +- `REQUIRED_TOKENS`, which the homepage spec imports. +- A pure `migrateLegacy({theme, mode})`, after which both legacy keys are deleted: + + | Stored mode | Result | + |---|---| + | light | `light`, or `sepia` when the old theme was `archive` | + | dark | `dark` | + | system | `system` | + | absent | nothing stored | + +- `buildThemeScript({defaultBase})`. + +**Pre-paint `ThemeScript.tsx`** (replacing :27-56), in this order: +1. Migrate if needed. +2. Validate the base, falling back to the default. +3. Set `data-base` to the resolved light/sepia/dark and toggle `.dark`. +4. Set `data-accent` only from a valid stored id; otherwise the server-rendered default stays. +5. Update `meta[name=theme-color]`. +6. Last: `data-theme-ready="1"`, the e2e no-flash marker. + +A unit test runs the script string in `node:vm` over the whole migration matrix. + +**`ThemeProvider.tsx`:** +- Props `{defaultBase = "system", siteAccent = DEFAULT_ACCENT}`. +- Context `{base, resolvedBase, isDark, accent, siteAccent, setBase, cycleBase, setAccent}`. +- A `useLayoutEffect` re-asserts `data-base`, `.dark`, `data-accent` and theme-color. Hydration + can reset `<html>` attributes; without it the site default overwrites a reader's pick. +- A live `systemDark` state from the `matchMedia` listener (today `isDark` goes stale after an + OS change). +- `setAccent(<site default>)` **removes** the stored key, so the reader follows the site default. +- `cycleBase`: system → light → sepia → dark. + +**Consumers:** +- `ThemeMenu.tsx`: keeps `aria-label="Choose theme"`. A "Base" radio group of `menuitemradio`s + named exactly System, Light, Sepia and Dark. An "Accent" radio group: a swatch dot + (`background: var(--swatch-<id>)`), the name, and a visible "default" tag on the site's own + option. A custom-hex site adds a first "Site colour" option. +- `ThemeToggle.tsx`: labels "Switch to …" (the `/switch to/i` selector keeps working); icons + Monitor / Sun / BookOpen / Moon. +- `export/app/components/MobileMenu.tsx:~121-141`: native radio groups "Base" and "Accent". +- `common/components/ui/sonner.tsx:17`: `theme={isDark ? "dark" : "light"}`. + +**Layout defaults:** + +| App | Base | Accent | Where | +|---|---|---|---| +| Export site | system | from site.json | `<html data-accent>` rendered server-side; a custom hex adds the inline `--accent-custom-*` style | +| Hub | dark | signal | export layout | +| Homepage | dark | signal | homepage layout | +| Editor | system | signal | no layout change | + +**Browser chrome colour** (`generateViewport`): sites get +`[{media: light, BASE_GROUNDS.light}, {media: dark, BASE_GROUNDS.dark}]`; the hub and homepage +get `BASE_GROUNDS.dark`. `FALLBACK_THEME_COLOR`, `HUB_THEME_COLOR` and the homepage +`THEME_COLOR` go. + +**Tests:** + +| Spec | Change | +|---|---| +| export `theme.spec.ts` | Rewrite: default `data-base`, live `emulateMedia`, cycling through 4 states, persistence across a `commit` reload plus `data-theme-ready`. | +| export `theme-family.spec.ts` | Delete; new `theme-accent.spec.ts`. Pick Violet: it persists and the computed `--brand` is correct. Picking the default removes the key. Also pick Sepia. | +| export `site-branding.spec.ts:75-86` | `data-accent="custom"`, and the computed `--brand` is `#cc3366` on light. | +| export `responsive.spec.ts:~95` | "Archive" becomes "Sepia". | +| homepage `theme.spec.ts` | Dark + signal defaults; `REQUIRED_TOKENS` complete across all 3 bases; the backgrounds differ. | +| editor `theme.spec.ts` | Seed `BASE_KEY`; migration cases selenized+dark → dark (legacy keys removed) and archive+light → sepia. | +| unit | Tokens completeness and parity: parse tokens.css; every base declares every token; `--swatch-*` / `--brand-mark` equal `ACCENTS`. | + +**Leftover copy:** `homepage/content/docs/operate.md:~84` and the comment at +`export/playwright.config.ts:~29` still describe the old accent and the committed icons. + +### S3 — integrate (after S1 + S2 merge to `main`) + +- Full gates. +- Verified facts appended to `plans/FACTS.md`; `plans/STATE.md` rewritten. +- `export/CHANGELOG.md` and `homepage/CHANGELOG.md` `[Unreleased]` bullets. +- The release recorded in the next `plans/release-N.md`. +- The operator runbook as **generated HTML** (operator rule). + +## Verification + +**Gates, every slice:** +- `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit`. +- `pnpm --filter yt-dlp-transcript-common test`. +- Editor unit: `pnpm exec tsx --test "app/**/*.test.ts"` in `editor/`. +- `pnpm test:scripts`; mcp tests. +- `pnpm --filter export exec next build`, the homepage `next build` and the editor `next build`. +- `ls out/icons` plus the PNG/ICO magic checks. + +**e2e** is queue-locked machine-wide; run it detached (`setsid nohup … &`). The parent session +holds a Monitor; a subagent waits in foreground loops of ≤100 s. +- `pnpm --filter export run e2e`, `e2e:hub`, `e2e:2origin`. +- `pnpm --filter homepage run e2e`. +- The root `pnpm e2e` (the editor suite). + +**Visual check:** build one site, serve `out/`, and screenshot the header, the picker open, and +each base × two accents at 390 px and 1280 px (use `localhost`, not 127.0.0.1). Compare with the +canvas's *In context* and *Themes* boards. + +**Caution:** `export/out` belongs to the checkout, and an e2e run can start a real deploy +(release-7 lessons). Never deploy from a worktree without the operator. + +## As shipped + +(Each slice appends its "### Slice S<n>, as shipped" record here.) + +## Operator rollout (after merge) + +1. Restart the live :3001 editor on the new `main`; it needs S0's form. +2. In the editor's site form (the one writer; never hand-edit `transcripts/`), set accent and + wordmark lead: + + | Site | Accent | Wordmark lead | + |---|---|---| + | jeralyzer | brass | Jer | + | rekietalyzer | vermilion | Rekieta | + | hasanalyzer | violet | Hasan | + | anilyzer | sakura | Ani | + | bonnellyzer | blue | Bonnell | + | jasolyzer | green | Jaso | + +3. `archilyzer deploy site anilyzer --preview brand` (or + `pnpm ops deploy-site --json '{"siteId":"anilyzer","preview":"brand"}' --wait`). On a phone, + check the favicon and installed-PWA icon update (SW `shell-v2`), the picker, and migration + from a stored old theme. +4. Build and deploy every site: `pnpm ops build-deploy --json '{"siteIds":[…]}' --wait`; then the + hub (`pnpm ops build-hub --json '{"deploy":true}' --wait`) and the homepage + (`archilyzer deploy homepage`).