Archilyzer · Source

archilyzer

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

commit c0e6bbbabb2c835d10460b6f93ed333ac16a2a3d
parent 9e1f99900dc3daa83b7a6b069003f021cf4959e7
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 21:11:53 -0400

plans: brand + themes — slice S0 as shipped (palette + data); editor CHANGELOG

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/brand-and-themes.md | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 168 insertions(+), 0 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **A site's accent is one of seven named colours or a custom one, and a site can split its wordmark.** The site form's **Brand accent** is now a row of swatches: Signal (the family default), Brass, Vermilion, Violet, Sakura, Blue and Green, plus **Custom**, whose colour goes in the **Custom hex** field (typing there picks Custom). `site.json` stores the accent's id (`"accent": "brass"`) or the hex. Picking Signal stores no key, because no key means Signal. A custom colour is darkened or lightened for each reading theme until it reads at 4.5:1; the seven named colours already do. A new **Wordmark lead** field names the heavy first part of the header wordmark, e.g. `Jer` for Jeralyzer. The form refuses a lead that is not how the header title starts, and `site.json` keeps `wordmarkLead` only when it is. What other hubs read does not change: `/site.json`, the hub's `hub-sites.json` and the homepage summary still carry a hex, and an id goes out as its colour on dark. `SITE.md` lists both keys. The header, icons and theme picker that use them come with the rest of the brand work; until then a site with a named accent is tinted with that colour, as a custom hex is today. - **The hub and the homepage drop their subtitles, and both carry a Ko-fi link.** The headings are in Title Case on both pages: **Official Instances**, **Archives You Added** (hub) and **What It Does** (homepage). The line "The archives I run. Anyone can run their own." is gone from both, and the official-instance cards no longer show the site's description under its name; the four figures stay. The footer of the homepage and of the hub has a plain link reading "Ko-fi" to `https://ko-fi.com/archilyzer`. A published site's footer does not: an archive someone else hosts never carries it. Needs a rebuild and deploy of the hub and the homepage. - **A job that waited in a queue no longer ends `failed` after doing its work.** A sync, download or other job queued behind another on the same platform ran its final page refresh outside any request, where Next refuses it, so the job read `failed` and `pnpm ops … --wait` exited 1 even though the work was done (the teamrcn sync on 2026-09-25). The refresh is now skipped there with one warning in the server log; the pages re-read disk on their next load anyway. - **Downloads no longer sleep after a video the download filter declined.** The "sleep between downloads" (30 s by default) ran after every video, including each one the channel's download filter declined before fetching anything. A filtered channel's download-missing slept 193 times for 14 downloads on 2026-09-25. It still sleeps after every real fetch and after every failure, per-video ones included. diff --git a/plans/brand-and-themes.md b/plans/brand-and-themes.md @@ -390,6 +390,173 @@ canvas's *In context* and *Themes* boards. (Each slice appends its "### Slice S<n>, as shipped" record here.) +### Slice S0, as shipped — palette + data (2026-09-25) + +Branch `brand/found-line` off the plan commit `d8dd98b8`. S0 lays the data S1 (mark, icons, wordmark) +and S2 (base × accent tokens, runtime, picker) build on: the palette and mark as pure data, the +`site.json` keys, the one mapping that keeps every *published* accent a hex, and the editor form that +writes both keys. No `common/styles/**`, `common/components/**`, layout, manifest, header, footer, +icon, service-worker or theme-spec file was touched, and nothing a reader sees changes until an +operator picks an accent or a lead. + +**`common/lib/brand.ts` (new, pure: zero imports).** +- `ACCENT_IDS` (picker order) and `ACCENTS: Record<AccentId, {id, name, onDark, onLight, onSepia}>`, + the plan's table verbatim; `DEFAULT_ACCENT = "signal"`; `isAccentId`. +- `BASE_GROUNDS` (+ `BASE_GROUND_IDS`), `ACCENT_INK` (`#ffffff` on light and sepia, the dark ground on + dark), `MIN_ACCENT_CONTRAST = 4.5`, and the WCAG `relativeLuminance` / `contrastRatio` the rule and + its tests use. **The rule as coded:** an accent's value on a base reaches 4.5:1 against that base's + ground AND its ink. On dark the ink is the ground, so it is one check there. Every value in the + plan's table passes: the tightest are Signal and Brass on light and on sepia (4.60–4.61) and Green on + sepia (4.61). `brand.test.ts` makes 42 checks (7 accents × 3 bases × 2). +- `MARK`: six shapes back to front, the ground first (`part`, `kind`, geometry, `tone` = + ground | dim | lit), plus `MARK_VIEWBOX`, `MARK_GROUND_RX` and `MARK_MASKABLE_SCALE`. + `markSvg(palette, {variant})` returns a standalone SVG string: + - `any`: ground `rx=112`; + - `maskable`: full-bleed ground (no rx), the lines inside `<g transform="translate(51.2 51.2) scale(0.8)">`, + which is 0.8 about (256,256); + - `apple`: full-bleed ground at scale 1. + + Every palette colour must be `#rrggbb` or it throws, so a stored value cannot inject markup into an + icon. +- `ICON_PALETTES` has `child` (ground `#0c0a08`, dim `#3b3327`) and `archilyzer` (`#151b20` / `#3f4c56` + / `#e7edf1`). `childIconPalette(lit)` adds the lit value: **S1 passes + `resolveAccent(site.accent).dark`.** brand.ts cannot import accent.ts; accent.ts imports brand.ts. +- `wordmarkLeadFor(title, lead)` returns the trimmed lead when it is a PROPER prefix of the title + (case-sensitive, non-empty, shorter than it), else undefined. `splitWordmark(title, lead?)` returns + `{lead, suffix}`, and without a usable lead that is `{lead: title, suffix: ""}`. There is no + heuristic: `splitWordmark("Rekietalyzer")` is the whole title, and the test pins that. The test + splits all seven family titles. +- `common/lib/project.ts`: `PROJECT_WORDMARK_LEAD = "Archi"`. + +**`common/lib/accent.ts`.** +- `parseAccent` is unchanged and stays hex-only. +- `parseAccentSetting(v)` takes an id (trimmed, any case, stored lowercase) or `parseAccent`'s hex, else + undefined. A hex equal to a named accent's value stays a custom hex and is never mapped to an id. +- `resolveAccent(v)` returns `{id | "custom", light, sepia, dark}`: + - a named id reads its row; + - absent or malformed reads Signal; + - a custom hex is fitted per base. It is kept if it already meets the rule; otherwise it is mixed in + sRGB toward black (light, sepia) or white (dark) in 1 % steps until it does. Full black or white + always passes, so the loop ends. + + The fixture's `#cc3366` resolves to light `#cc3366` (kept, 4.57), sepia `#c43162` (4.50) and dark + `#d14574` (4.53). **So S2's `site-branding` expectation, `--brand` = `#cc3366` on light, holds.** +- `accentHex(v)` is the published value: an id becomes its `onDark`, a custom hex goes out as stored + (normalized, not fitted), and absent or malformed gives undefined. +- `customAccentVars(v)` returns `{--accent-custom-light|sepia|dark}` for a custom hex, and null for an + id or no accent. It takes the setting as its argument; the plan wrote `customAccentVars()`. +- **`siteAccentVars` (legacy, which S2 deletes) now goes through `accentHex`.** A hex gives the same + output as before (the `site-branding` spec's `--brand: #cc3366` passes). An id paints its on-dark + hex in both modes, the old one-colour contract. + +**`site.json` (`common/lib/siteSchema.ts`).** +- `accent` parses through `parseAccentSetting`; the doc text is new. +- `wordmarkLead?` sits after `headerTitle` in the type, `SITE_FIELD_DOCS`, the schema (trim, else + undefined) and `siteToDisk`. +- The object step resolves it with `wordmarkLeadFor(headerTitle, …)`, beside `defaultGroupId`, so a + non-prefix lead reads as undefined. `siteToDisk` re-checks it, so a caller's stale lead after a + retitle is not written. +- `SITE.md` was regenerated and `--check` is clean. +- A stored `"signal"` round-trips as `"signal"`. The schema keeps any valid setting; only the FORM + stores the default as absent. + +**Published accents stay hex.** `siteDescriptor.ts` (public `/site.json`), `compose-hub.ts` +(`hub-sites.json`) and `homepageSummary.ts` (homepage summary, and through `toHubSummary` the hub's +`hub-summary.json`) emit `accentHex(site.accent)`, omitting the key when there is no accent, as before. +`wordmarkLead` is not added to the public descriptor, so the contract is unchanged. **Every reader of an +`accent`, and what it now receives:** + +| Reader | Receives | +|---|---| +| `common/lib/siteDescriptor.ts:~89` | hex (`accentHex`) | +| `common/bin/compose-hub.ts:~96` → `hub-sites.json` | hex (`accentHex`) | +| `common/lib/homepageSummary.ts:~433` → `homepage-summary.json` | hex (`accentHex`) | +| `common/lib/hubSummary.ts:67,122` | the homepage summary's value → hex | +| `common/components/siteRegistry.ts:144,206,305` | `hub-sites.json` / a member's `/site.json` → hex | +| `common/components/SearchDataContext.tsx:274,442`, `SearchSessionContext.tsx:899,926,960` | registry → hex | +| `common/components/SearchResults.tsx:573`, `FiltersPanel.tsx:324` (C2's files, untouched) | `group.accent` from the registry → hex | +| `export/app/components/hub/ArchiveShelf.tsx:69` (C2's, untouched), `HubHome.tsx:39`, `HubOfflineManager.tsx:97`, `export/app/ask/AskHub.tsx:33` | registry / hub summary → hex | +| `export/app/layout.tsx:86` `siteAccentVars(currentSite().accent)` | id or hex → hex via `accentHex` (legacy, S2 deletes) | +| **`export/app/layout.tsx:64`** `generateViewport` themeColor | `parseAccent(id)` = undefined → falls back to `#2563eb` (S2's file; S2 replaces it with `BASE_GROUNDS`) | +| **`export/app/manifest.ts:19`** `theme_color` | `parseAccent(id)` = undefined → falls back to `#2563eb` (S1's file; S1 sets `BASE_GROUNDS.dark`) | +| `editor/app/sites/actions.ts`, `SiteForm.tsx` | the setting (id or hex), by design | +| `common/lib/channelGroups.ts:28` | a hub-mode group's provenance accent from the registry → hex; not a `site.json` read | +| homepage `ArchiveCards.tsx` | does not read it (its comment says so) | +| mcp, umtool | none (umtool's `pal.accent` is a video palette, unrelated) | + +**Editor form** (`editor/app/sites/components/SiteForm.tsx`, `actions.ts`): +- **Brand accent** is a `<fieldset>`/`<legend>` group of 8 native radios named `accent`: the 7 named + (each with a dot of its on-light / on-dark value, and "Signal (default)") plus **Custom**. A + **Custom hex** text field (`accentCustom`) sits below; typing in it selects Custom. The radios are + controlled, so the choice survives the reset React does after the action. +- The action stores the chosen id, but **Signal is stored as absent** (the two read the same). Custom + requires a valid hex. A bare hex posted in `accent` (an old tab from before the restart) is taken as + custom. +- **Wordmark lead** is a `Field` after Header title. The action REFUSES a non-prefix lead with an error + rather than letting the schema drop it silently. +- `writeSite` is still the one writer. +- No new label or hint contains "header title", "site title", "site id" or "public url"; the lead's + hint says "header text". +- Shots: `$T/s0-accent-{light,dark}.png`, `$T/s0-form-{light,dark}.png`. + +**Changed locators: none.** No editor spec referenced "Brand accent" or the accent field. New test +`sites-crud.spec.ts` "brand accent radio group + wordmark lead round-trip to site.json" covers: +- the default checked, and 8 radios in the "Brand accent" group; +- Brass + lead `Brandy` → `"accent":"brass","wordmarkLead":"Brandy"`; +- typing `#CC3366` selects Custom and stores `#cc3366`; +- a lowercase lead is refused (alert) with the file unchanged; +- Signal + blank lead → both keys absent. + +**`hubSite()`** (`export/app/lib/site.ts`) sets `wordmarkLead: PROJECT_WORDMARK_LEAD`. The live +`homepage.json` header title is "Archilyzer", so it is kept. + +| sha | what | +|---|---| +| `e85c1fd2` | `common/lib/brand.ts` (+ test) and `accent.ts` (+ test): the palette, rule, mark, wordmark split; `parseAccentSetting` / `resolveAccent` / `accentHex` / `customAccentVars`; `siteAccentVars` through `accentHex`; `PROJECT_WORDMARK_LEAD` | +| `3b634248` | `siteSchema.ts`: `accent` id-or-hex, `wordmarkLead` (proper prefix only) + tests; `SITE.md` regenerated | +| `31ff873a` | `siteDescriptor` / `compose-hub` / `homepageSummary` publish `accentHex` + tests (new `siteDescriptor.test.ts`) | +| `17c874ca` | `export/app/lib/site.ts` `hubSite()` carries `wordmarkLead: "Archi"` | +| `4fce889d` | editor site form: the accent radio group + Custom hex, the Wordmark lead field, the action; the sites-crud test | +| `9775bd22` | sites-crud's new test filters its alert from Next's route announcer | + +**Gates.** +- tsc clean before each code commit (four runs), plus a fifth on the final tree, which covers `9775bd22`, a spec-only change. +- Unit and script tests: + - common **1,874/1,874**: C1b's 1,845 + 29 new — brand 12, accent 9, siteSchema 3, siteDescriptor 3, + homepageSummary 1, compose-hub 1; + - editor unit **78/78**; + - `test:scripts` **162 + 1 skip**; + - mcp **219/219**. +- `file-schemas-docs.ts --check` clean. +- Builds: + - `pnpm --filter editor exec next build` ok (50 s); + - `pnpm --filter export exec next build` ok (31 s, the known Turbopack warning), with the + `export/public` links seeded, `sw.js` a plain copy, and no dangling links; + - `pnpm --filter homepage exec next build` ok (16 s), with `homepage/public`'s four data entries + COPIED from the primary. +- e2e, no queue wait: + - editor `sites-crud.spec.ts branding.spec.ts`: run 1 **15 passed, 1 failed** (1.2 min). The failure + was the new test's own `getByRole("alert")`: Next's route announcer is a second, empty + `role=alert`. Run 2 on `9775bd22`: **16 passed, 0 failed**, 58.7 s. + - export `site-branding.spec.ts`: **7 passed**, 16.1 s. + - `e2e:hub`: **12 passed**, 28.4 s. +- The primary's `export/public/sw.js` was untouched: 10,027 B, mtime 20:47:37, before and after. +- Numbers tools: none. + +**Found and left.** +- **The browser-chrome colour falls back for a named accent until S1/S2.** + - `export/app/layout.tsx:64` and `export/app/manifest.ts:19` call the hex-only `parseAccent`, so a + site set to an id gets `#2563eb` there instead of its colour. + - Both files belong to S1/S2, which replace those lines with `BASE_GROUNDS`. They were left alone. + - No live site sets an accent (all six read `null`), so nothing live changes. +- **The legacy `siteAccentVars` paints an id's on-dark value in both modes.** On the light base that is + below 4.5:1 (Brass `#e3b15c` on `#f3f6f7` ≈ 1.9). This is transitional: S2 deletes it, and S0 is not + rolled out alone. +- **"Signal (default)" is only literally true once S2 lands.** At S0's tip an absent accent is still + the base family's blue. +- The worktree is port block **#1** (editor 3101, test 3111, export 3110), not #2: + `pnpm wt list` sorts, and `diet-series` is #2. + ## Operator rollout (after merge) 1. Restart the live :3001 editor on the new `main`; it needs S0's form.