commit ad7f92994bbfda05d46c865ccb0f97aced87cc71
parent 2a4598de02cde54d3096e31493117892ea5f7595
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 20:14:57 -0400
plans: slice HP's record after the re-review — the chart, the narrow header, the toggle, R1–R10, the gates; the plan's H1, H2 and T1; STATE; the changelogs as the end state
release-14.md: rulings 6–9; the Options dialog built, then replaced by the
toggle; the header's thresholds per link count and pointer, and where the
scroll box takes over; the checker's read and write paths; the commits since
2f573c38 and both merges of main; the gates at 72438bd0 and after 2a4598de;
what bites; the re-review's R1–R10, each to its commit; found and left.
export-header-first-search.md: the rulings; H2 is the cycling toggle; T1
("two grounds") planned; A8, A9.
homepage/CHANGELOG.md, editor/CHANGELOG.md: worded as the end state.
export/CHANGELOG.md: the footer's read path and each icon clipped to its box.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
6 files changed, 293 insertions(+), 186 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -7,7 +7,7 @@
- **umtool reads the corpus from its checkout (or `TRANSCRIPTS_DIR`), and the song project's data defaults to `~/.local/share/archilyzer/song`.** If yours is elsewhere, link it there before restarting umtool: `mkdir -p ~/.local/share/archilyzer && ln -s <where the data is> ~/.local/share/archilyzer/song` (the data stays where it is). With no `CHANNELS_DIR`, umtool reads the corpus at `$TRANSCRIPTS_DIR/channels`, else the checkout's own `transcripts/channels`; it used to fall back to an absolute path that existed on one machine only. The song project's videos default to `~/reports/quartering-uh-song/videos`; `SONG_DIR` and `VIDEO_ROOT` still win. The song project's tracked manifests record their paths relative to the song folders, and the twenty one-off `umtool/song/*.sh` run logs, which only ever ran on the machine that wrote them, are gone.
- **umtool's production build no longer reads the corpus folder.** Since umtool began finding the corpus from its checkout (the bullet above), `next build` treated the checkout's whole `transcripts/channels` as files to bundle. On a real archive it ran out of memory and was killed, so umtool could not be rebuilt. The build now ignores that folder and finishes in about 25 s at under 1 GB, the same as a checkout with no corpus. Nothing changes when umtool runs.
- **A social icon pasted with only a width and height is accepted, and each social link can be shown in a header.** The social-link editors (Settings, a site's form) refused an SVG with no `viewBox`, so a vendor's logo file as downloaded, which often carries only its size, was refused. On save, a root with a numeric width and height (unitless or px) and no viewBox is now given `viewBox="0 0 W H"`; a percentage, `em`, or a missing or zero side is still refused. Each link has a **Show in header** checkbox, stored as `featured: true` only when checked, with the hint "With none checked, the header shows the last four.": a header shows at most four links, the checked ones when any is checked, else the last four (the homepage's header reads it). A file with neither is read and rendered as before. `SETTINGS.md` and `SITE.md` list `featured`.
-- **A social icon is checked by what it may contain, on save and every time it is shown, and a refused one says why.** An icon must be one well-formed `<svg>` of shapes, groups, gradients, clips, masks, filters, text and simple animation, with SVG presentation attributes: no script, `style` block, `foreignObject`, link, embedded image or HTML element; no event handler, however it is written; a reference only to something inside the icon, even when it is spelled with character references. Comments, a leading XML declaration and a plain DOCTYPE are removed. A refused save ends with the reason ("… has an invalid SVG: it has an event handler attribute.", "… it links to something outside the icon.", "… it has an element an icon has no use for (style).") and never repeats the markup. A stored icon that fails the check — a file edited by hand, or saved by an older build — is not shown: the homepage and every site's footer show the link's label instead. A file saved from Illustrator or Inkscape may need its `<style>` block, `<metadata>` or `inkscape:` attributes removed first.
+- **A social icon is checked by what it may contain, when it is saved new or edited and every time it is shown, and a refused one says why.** An icon must be one well-formed `<svg>` of shapes, groups, gradients, clips, masks, filters, text and simple animation, with SVG presentation attributes: no script, `style` block, `foreignObject`, link, embedded image, `title`/`desc` with anything but text (text-only ones are removed), or HTML element; no event handler, however it is written; a `style` attribute of presentation properties only; a reference only to something inside the icon, written plainly; and nothing that could load from elsewhere (a CSS escape or comment, `image-set(`, `image(`, `cross-fade(`, `element(`, `src(`, `paint(`, `@import`). Comments, a leading XML declaration and a plain DOCTYPE are removed. A refused save ends with the reason ("… has an invalid SVG: it has an event handler attribute.", "… it links to something outside the icon.") and never repeats the markup; for a drawing program's file it says to export it with presentation attributes rather than a style block (in Inkscape, save as Plain SVG). **Upgrading:** an icon an older build stored is kept as it is when a save does not change it — a pause, a priority or a title still saves — but a page shows it as its label until its SVG is replaced; `archilyzer doctor`'s new "social icons" line names every stored icon that fails, by file and label, with the reason.
## [0.10.0] - 2026-09-28
- **The homepage can be built and deployed from `/sites`.** Under a new **Homepage** section, after Hub, there is **Build homepage** (tick **Deploy after build** to ship it in the same job, only if the build succeeds) and **Deploy homepage**, which ships the build already in `homepage/out`. A **Preview branch** box beside them sends either deploy to a Cloudflare Pages preview of the `archilyzer` project instead of production, and shows the preview's address as you type; a name Cloudflare would refuse or rewrite, or `main`, greys the deploy buttons out and says why. A line under the buttons says what a deploy would ship: when `homepage/out` was built (or that it holds no build yet), and where it goes, with the live URL. Deploy homepage with nothing built is refused before any job starts. The homepage reads the search index as it stands, so run **Build index** first when its numbers should move. The jobs run the same code as `archilyzer build homepage` / `deploy homepage`, and show on `/jobs` as `build-homepage`, `deploy-homepage` and `build-deploy-homepage`. The Hub section no longer describes the homepage.
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -2,6 +2,7 @@
## [Unreleased]
- **The charts count every transcript, once the site is rebuilt.** A transcript that arrived after its video was first indexed was missing from the charts' transcript and cue counts and from "Transcribed over time", and a video with YouTube captions alone had no transcription date. Both are counted now, and a captioned video is dated by when its captions arrived.
+- **A social icon that fails the check is shown as its label, and every icon paints inside its box.** The footer inlines a social link's SVG only if it passes the same check a save runs (what an icon may contain is in `SITE.md`); otherwise the link shows its label as text, at most 10rem with an ellipsis. Each icon is clipped to its own box. Needs a rebuild and deploy of each site.
## [0.10.0] - 2026-09-28
- **A video whose recheck failed shows as possibly missing rather than available.** When a video drops out of its channel's listing it is marked "Missing?" until a recheck says why. A recheck that could not reach the video — a blocked request or a network error — used to clear the mark as if the video had been found. It now leaves "Missing?" in place until a recheck actually reaches the video. Needs a rebuild and deploy of every export site.
diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md
@@ -2,13 +2,13 @@
## [Unreleased]
-- **The social links are in the header, at every width, beside one Options gear.** The operator's social icons (`homepage.json`'s, else `settings.json`'s) now sit in the header's bar as well as in the footer's Elsewhere column, followed by the gear, all four spaced alike. From 768 px wide the bar is wordmark, nav, icons, gear; below 768 px it is wordmark, icons, gear, and the nav has the rule below to itself, where its four links fit without scrolling. The header shows at most four links: the ones marked **Show in header** when any is, else the last four. A narrower bar shows the last of those that fit: with a mouse three from 360 px and two below; on a touch screen three from 390 px, two from 340 px and one below. No width from 320 px up scrolls sideways. The footer shows them all. Each is an icon named by its label, with no text beside it.
-- **One Options button in place of the two theme buttons.** The header's theme menu and base toggle are one gear, **Options**, which opens a dialog with **Base** (System, Light, Sepia, Dark) and **Accent** (the seven named accents, Signal tagged *default*) as radio lists. It opens on the ground in force; a choice applies at once and the dialog stays open; Escape, Close or a click outside closes it.
+- **The social links are in the header, at every width, beside the theme toggle.** The operator's social icons (`homepage.json`'s, else `settings.json`'s) now sit in the header's bar as well as in the footer's Elsewhere column, followed by the theme toggle, all spaced alike. From 768 px wide the bar is wordmark, nav, icons, toggle; below 768 px it is wordmark, icons, toggle, and the nav has the rule below to itself, where its four links fit. The header shows at most four links: the ones marked **Show in header** when any is, else the last four; the footer shows them all. No link is hidden on a small screen: when the icons and the full wordmark do not fit side by side, the wordmark's text is dropped and its mark stays (with three links, the full wordmark shows from 348 px wide with a mouse and from 380 px on a touch screen; with four, from 384 and 424 px). Only on a screen narrower than that for the mark too, or at a much larger text size, do the icons scroll sideways in their own box, the last one in view first. Each is an icon named by its label, with no text beside it.
+- **One theme toggle in place of the two theme buttons.** The header's theme menu (Base and Accent) and its base toggle are one toggle, the last of the header's icons, that cycles the ground (System, Light, Sepia, Dark) and is named for the next one. There is no accent to pick: the homepage's is its own, Signal, and an accent stored by an earlier build is ignored here, with no flash.
- **Changelog is in the footer only.** The header's nav is Docs, Source, Downloads and Stats; the footer's Sections list and the 404 page keep Changelog.
- **Each Official Instances card names its site with the site's wordmark.** The first part of the name is set heavy in the site's own accent and the rest light, as the site's own header sets it (Jer·alyzer, Hasan·alyzer, …), at the card title's size; the accent is fitted to the ground in force and reads above 4:1 on the card on Light, Sepia and Dark. A site with no configured lead, or a summary built before this, shows its title plain as before. `homepage-summary.json` gains an optional `wordmarkLead` per site (still version 5).
- **The growth chart's strata are parted by lines in the text colour.** The line along each stratum's upper edge was drawn in the page's background colour, so it was light on Light and dark on Dark; it is now a 1 px line in the ground's foreground (dark on Light and Sepia, light on Dark, the system's text colour in high-contrast mode). The gridlines are unchanged.
-- **Larger social links, with a focus ring.** Each icon, in the header and the footer, is a 36 px target around its 20 px glyph (44 px on a touch screen), in the muted text colour and the text colour on hover; the footer's were 20 px, in the faint colour, with no ring. Keyboard focus draws a 2 px ring in the accent, and in high-contrast mode the browser's own focus outline. An icon of two or more colours keeps its colours.
-- **The e2e no longer reads the checkout's `settings.json`.** Its dev server reads `e2e/.e2e-settings.json` (`SETTINGS_FILE`), written by `e2e/fixture-social.ts`: three synthetic icons (a gradient, one colour, and a two-colour disc pasted with only its size), put through the same check a save runs, and `SITES_DIR` points at an empty directory, so no `homepage.json` in the checkout can override them. `e2e/social.spec.ts` covers the header and the footer rows (and that a stored icon that would run script is shown as its label and never runs), `e2e/options.spec.ts` the Options dialog, and `e2e/instance-wordmark.spec.ts` the cards' names; specs change the theme through one helper, `chooseTheme` (`e2e/helpers.ts`).
+- **Larger social links, with a focus ring.** Each icon, in the header and the footer, is a 36 px target around its 20 px glyph (44 px on a touch screen), in the muted text colour and the text colour on hover; the footer's were 20 px, in the faint colour, with no ring. Keyboard focus draws a 2 px ring in the accent, and in high-contrast mode the browser's own focus outline. An icon of two or more colours keeps its colours, and every icon paints inside its own box. A stored icon that fails the check a save runs is shown as its label (at most 10rem, with an ellipsis) instead.
+- **The e2e no longer reads the checkout's `settings.json` or `homepage.json`.** Its dev server reads `e2e/.e2e-settings.json` (`SETTINGS_FILE`), written by `e2e/fixture-social.ts`: three synthetic icons (a gradient with an outline, one colour, and a two-colour disc pasted with only its size), put through the same check a save runs; and `SITES_DIR` points at an empty directory. `e2e/social.spec.ts` covers the header and footer rows (at every width, with 1, 3 and 4 links, both pointers; the scroll fallback; hostile stored icons that must neither run nor fetch), `e2e/toggle.spec.ts` the theme toggle and the pinned accent, `e2e/svg-vectors.spec.ts` that every accepted icon stays inside its `<svg>` in a real parse, `e2e/growth-chart.spec.ts` the chart's lines, and `e2e/instance-wordmark.spec.ts` the cards' names; specs change the ground through one helper, `chooseTheme` (`e2e/helpers.ts`).
- **A site's card counts every transcript, and never shows 0 channels while it serves recordings.** A transcript that arrived after its video was first indexed, or a video with YouTube captions alone, could be left out of the family's numbers: one site served 1,889 recordings and its card said 0 transcripts, 0 channels and 0 hours. Such transcripts are counted now — in the card, the family totals and the archive-growth chart — and one with no transcription date is left off only what is placed by that date: the charts by transcription date, "this month" and the recent list. The official-instance figures on the hub move with them.
- **The source is on the site, with its history: `/source/`.** A new **Source** page (and nav entry) gives `git clone https://archilyzer.pages.dev/source/archilyzer.git`, a read-only mirror of the main branch regenerated with every deploy, with its head, the private commit it reflects, a link to browse every file raw at `/source/tree/`, and the tarball with its size and sha256. Commit ids differ from the private repository's, because machine paths are scrubbed on the way out, and the page says so. A build without a published source says "No source published in this build." instead of offering a clone. The Downloads tarball is now regenerated by every build (its commit is the mirror's), and the page points at the mirror for history. The docs that said there is no public repository (*Install*, the FAQ, *What is Archilyzer*) now say how to clone. Below `md` the header's nav drops to its own row, as it did below `sm`, because five labels no longer fit beside the wordmark. `_headers` serves the raw tree as plain text.
- **The docs' *Building several sites at once* page says what Build all does.** It called the container pipeline opt-in, turned on in the settings. Build all sites builds every site in parallel in containers whenever a container engine is available, and one after another when none is; there is nothing to switch on.
diff --git a/plans/STATE.md b/plans/STATE.md
@@ -18,8 +18,8 @@ holds the record, the review and the rollout. FACTS has "The stats cache key".
:3001 (until then, never press "Build stats dataset"). Then index, then one full stats pass of
10–30 min, then the homepage, the hub and the sites. The homepage deploy waits on release 12's
step 0: it runs the source publish.
-- **Merge note:** against `homepage/social-visible` (tip `afc642fd`) the only conflict is
- `homepage/CHANGELOG.md`'s `[Unreleased]`. Keep both sides. Against `main` there is none.
+- **Merge note:** `homepage/social-visible` merged `main` (`10cefd15`) at `4d11542c`; the one
+ conflict, `homepage/CHANGELOG.md`'s `[Unreleased]`, kept both sides.
- **FOLLOW-UP, its own slice: the index build still treats an unmounted drive as an empty
channel.** It drops that channel's index records, and the next site build publishes the channel
as gone.
@@ -172,19 +172,31 @@ changed at integration. Nothing was deployed, cut, pushed or restarted; :3001 st
shared tool probe) and two test-only variable names; O5's render changes already reach it from
disk (the live :3050 spawns `report-to-video/*`), and an unbranded render is byte-identical.
- **Release 14 (2026-09-28): slice HP built, not merged** — the social icons in the export header,
- one Options button for the theme, the Sites dropdown replaced by a link to the Archilyzer home,
- and a clear screen until the first Search (`plans/export-header-first-search.md`; slices HP, H3,
- H1, H2, S1, and S2 as a candidate). It waits on nothing: a link the operator adds is an entry in
+ one theme toggle, the Sites dropdown replaced by a link to the Archilyzer home, and a clear screen
+ until the first Search (`plans/export-header-first-search.md`; slices HP, H3, H1, H2, S1, T1
+ planned, and S2 as a candidate). It waits on nothing: a link the operator adds is an entry in
`settings.json` `socialLinks`, and the earlier tip-link branch is parked, not merged. **HP**
- (`homepage/social-visible`, record in `release-14.md`) puts the social row and one "Options"
- gear in the homepage's header at every width through shared `common/components/SocialLinks.tsx`
- and `OptionsDialog.tsx` + `ThemeRadios.tsx` (H1 and H2 adopt them), moves Changelog to the
- footer, names each Official Instances card with the site's wordmark (lead tinted in its accent;
- `wordmarkLead` in the summary), checks a social icon by an allowlist on save and at render
- (`common/lib/socialSvg.ts`), gives a sized SVG with no viewBox its viewBox, and adds `featured`
- ("Show in header") to a social link. Reviewed SHIP AFTER FIXES; the fixes are in, and the
- branch's history was rewritten once so no vendor file entered it. Merge order:
- `fix/stats-cache-key` → HP → H3 → H1+H2 → S1.
+ (`homepage/social-visible`, record in `release-14.md`):
+ - puts the social row and one theme toggle (`ThemeToggle variant="bare"`) in the homepage's
+ header at every width, through the shared `common/components/SocialLinks.tsx`; no icon is
+ hidden by width. On a very small screen the wordmark's text drops first (a container query per
+ link count and pointer), and the row scrolls in `SocialScroll.tsx`, end first, only when even
+ the mark does not leave room;
+ - pins the homepage's accent (`pinAccent`). The Options dialog was built and then deleted by
+ ruling; `ThemeRadios` stays for the export's menu;
+ - moves Changelog to the footer and names each Official Instances card with the site's wordmark
+ (the lead tinted in its accent; `wordmarkLead` in the summary);
+ - draws the growth chart's separators in the ground's foreground;
+ - checks a social icon by an allowlist (`common/lib/socialSvg.ts`) on a new or edited icon and at
+ render. An unchanged stored icon never blocks a save, and `archilyzer doctor` names the failing
+ ones;
+ - gives a sized SVG with no viewBox its viewBox, and adds `featured` ("Show in header") to a
+ social link.
+
+ Reviewed SHIP AFTER FIXES, then re-reviewed SHIP AFTER FIXES. Both sets of fixes are in, and
+ `main` (`10cefd15`) is merged in. The history was rewritten once, so no vendor file entered it.
+ The next review covers everything after `afc642fd`. **T1** ("two grounds", Sepia and the accent
+ picker removed) is planned and unassigned. Merge order: HP → H3 → H1+H2 → S1.
- **Next candidates:** one-core Phase 5 (projects join the core, `plans/one-core.md`); the Diagnostics
cards keeping their retry log (O3's found-and-left); `ChartView.tsx`'s five-slot cycle reaching
`--chart-6` (O2); O5's two wording lows in `svg-faces.mjs` / the README (kerning is not
diff --git a/plans/export-header-first-search.md b/plans/export-header-first-search.md
@@ -34,6 +34,11 @@ main...<branch>` is empty for the search and header components on all five).
Official Instances each site's name uses the bold-lead effect of the sites' own headings, with a
slight tint or underline in the site's accent.
- 2026-09-28, on the homepage chart: light mode has dark lines and dark mode has light lines.
+- 2026-09-28, on the homepage: on very small screens the header keeps its mark and drops the word;
+ the social icons scroll only when they still cannot fit.
+- 2026-09-28: the options menu is dropped for now in favour of a three-way toggle (slice HP on the
+ homepage; slice H2 in the export). A later slice drops the Sepia base in every app and removes
+ the accent picker from the export and editor headers (slice T1, below).
- Standing: **no copy** beside any social link: icons with accessible names only.
## Decisions and assumptions
@@ -51,23 +56,24 @@ ASSUMED by the planner (2026-09-28) — each is one line to reverse, and the ope
| A5 | The header's **Hub** link | Dropped with the Sites dropdown; the link to the Archilyzer home covers it. `hubUrl` still parses. |
| A6 | The footer's social row | Stays, as well as the header's — nothing disappears for a reader who looks there. |
| A7 | Phones | The icons are visible in the header at every width, not inside the slide-out menu. |
-| A8 | The options modal | Holds Base and Accent and nothing else. |
-| A9 | The homepage app's own header | Carries the social row and the Options gear since slice HP (built), as well as the footer's Elsewhere row; H3 adds only the anchor. |
+| A8 | The options modal | None: ruled 2026-09-28, a cycling toggle instead (H2); the accent picker is removed in T1. |
+| A9 | The homepage app's own header | Carries the social row and the theme toggle since slice HP (built), as well as the footer's Elsewhere row; H3 adds only the anchor. |
| A10 | Deferring the summaries fetch until the first Search | NOT in S1. Measured and written up as S2, a candidate, because `/ask` reads the same data. |
## Dependency graph
```
-HP (homepage social row, Options, cards + schema, BUILT) ──► H1 ──► H2
+HP (homepage social row, toggle, cards + schema, BUILT) ──► H1 ──► H2 ──► T1
H3 (homepage anchor) ── independent; H1's link needs it DEPLOYED to land on the list
S1 (clear screen) ── independent of H*; touches no header file
S2 (defer summaries) ── after S1, only on the operator's word
+T1 (two grounds) ── after HP merges, before the production deploy
```
H1 then H2 are stacked on one branch (both rewrite `Header.tsx` and `MobileMenu.tsx`), branched
after HP merges: H1 adopts HP's `SocialLinks` component. S1 and H3 run in parallel with them on
-their own branches. Merge order: HP → H3 → H1+H2 → S1. HP and H3 share only
-`homepage/CHANGELOG.md` (`[Unreleased]`).
+their own branches. Merge order: HP → H3 → H1+H2 → S1, and T1 after HP and before the production
+deploy. HP and H3 share only `homepage/CHANGELOG.md` (`[Unreleased]`).
## Verified facts the implementer must not re-derive
@@ -176,25 +182,23 @@ shipped". What it built, so H1 does not re-derive it:
`SocialLink.featured?: boolean` (stored only when true), with a "Show in header" checkbox in the
editor's `SocialLinksField`. `sizeSocialSvg` moved to `lib/socialLinks.ts` (re-exported from
`settingsSchema`).
-5. **`common/components/OptionsDialog.tsx`** — one gear button, accessible name "Options",
- dressed exactly as a social key (36 px, 44 px under a coarse pointer, no border, the same hover
- square and ring). It opens a modal on the shared `common/components/ui/dialog.tsx`, used as it
- is (the editor's command palette renders it too; OptionsDialog styles its own panel border and
- its own 36 px Close), titled "Options", opening on the checked Base radio, holding
- **`common/components/ThemeRadios.tsx`**: the Base and Accent native radiogroups, extracted from
- the export's `MobileMenu` unchanged — `MobileMenu` renders it now (`namePrefix="mobile-theme"`,
- `groupClassName="mt-4 px-2"`), and the export's `theme`, `theme-accent`, `responsive` and
- `brand` specs pass. A pick applies at once and the dialog stays open; Escape, Close and a click
- outside close it; focus is trapped and returns to the gear; the page does not scroll.
- `ThemeMenu` and `ThemeToggle` are unchanged and still rendered by the export and the editor.
-6. **The homepage's header:** one group — the social row, then the gear, boxes touching (glyph to
- glyph 16 px) — in the bar at every width. ≥ `md`: wordmark · nav · group, the nav 32 px before
- the group's first box. < `md`: wordmark · group, and the four nav links alone on the rule below
- (they fit 360 px; no fade). The bar shows the last n of the header's links by width and
- pointer — mouse: 4 from 768, 3 from 360, else 2; touch: 4 from 768, 3 from 390, 2 from 340,
- else 1 — one CSS rule per step on the row (`[&>li:nth-last-child(n)]:hidden`); nothing from
- 320 px up scrolls sideways. Changelog is in the footer only (`homepage/app/lib/nav.ts`:
- `HEADER_NAV`, `FOOTER_NAV`).
+5. **The theme control is one toggle**: `common/components/ThemeToggle.tsx` with `variant="bare"`
+ (dressed as a social key: 36 px, 44 px under a coarse pointer, no border, the same hover square
+ and ring, a 20 px glyph), cycling the base, named "Switch to {next}"; its default rendering is
+ unchanged. The homepage offers no accent control, so `ThemeScript` and `ThemeProvider` take
+ `pinAccent` there (the stored accent is neither read nor removed). An Options dialog was built
+ first and deleted on the operator's ruling; `common/components/ThemeRadios.tsx` stays (the
+ export's `MobileMenu` renders it).
+6. **The homepage's header:** one group — the social row, then the toggle, boxes touching (glyph
+ to glyph 16 px) — in the bar at every width. ≥ `md`: wordmark · nav · group, the nav 32 px
+ before the group's first box. < `md`: wordmark · group, and the four nav links alone on the
+ rule below (they fit 320 px). No link is hidden by width: when the bar (`@container/bar`) is
+ narrower than the full wordmark, a 12 px gap and the group need, the wordmark's text is hidden
+ and the mark stays — a rem threshold per link count and pointer (full wordmark from 348 px with
+ three links and a mouse, 380 px with touch; with four, 384 and 424 px). The last resort is
+ `common/components/SocialScroll.tsx`: the row scrolls in a box, its end first (rtl box, ltr
+ row), the toggle outside it, focus brings a link into view. Changelog is in the footer only
+ (`homepage/app/lib/nav.ts`: `HEADER_NAV`, `FOOTER_NAV`).
7. **The Official Instances cards** set each site's title with the shared `Wordmark` when the
summary carries its `wordmarkLead` (new, optional, still v5), the lead tinted in the site's own
accent (`siteAccentColor`). The hub's cards are not touched.
@@ -228,13 +232,12 @@ Owns `export/app/components/{Header,MobileMenu,Footer,SiblingSwitcher}.tsx`,
`--ring`, the browser's outline in forced colours. The export footer's icons change from
`hover:text-brand` to the component's `hover:text-foreground`; say so in the changelog.
3. **The header, wide:** brand · nav · `SocialLinks` · the Archilyzer link · the theme controls
- (H2). **Narrow:** the row visible without scrolling at 320 px (the reflow width, 400 % zoom).
- HP's measurement: in the homepage's bar beside a 148–152 px wordmark, three 36 px keys and the
- gear need 348–352 px, four do not fit at 360, and three at 44 px need 380 px — hence HP's
- width × pointer steps (mouse: 4 from 768, 3 from 360, else 2; touch: 4 from 768, 3 from 390,
- 2 from 340, else 1). Measure this header (a long `headerTitle`, the Options button, the menu
- trigger) and choose its own steps; record the numbers. The bound is HP's: at most four, the
- `featured` ones when any is marked, else the last four; the rest stay in the footer.
+ (H2). **Narrow:** adopt HP's approach — no link hidden by width; the header first makes room
+ (for a site's header, whatever of the brand can give way while the mark stays), and HP's
+ `SocialScroll` box is the last resort, the row's end shown first. Measure this header (a long
+ `headerTitle`, the toggle, the menu trigger) and record the widths at which the brand collapses
+ and at which anything scrolls. The bound is HP's: at most four links, the `featured` ones when
+ any is marked, else the last four; the footer shows them all.
4. **Sites dropdown → one link.** `SiblingSwitcher` is deleted. In its place a text link
"Archilyzer" to `INSTANCES_URL`, opening in the same tab, with an accessible name that says
where it goes ("Archilyzer — official instances"). Also in the slide-out menu, replacing the
@@ -255,31 +258,41 @@ Owns `export/app/components/{Header,MobileMenu,Footer,SiblingSwitcher}.tsx`,
`related-sites.spec.ts`; hub e2e `official-instances.spec.ts`; screenshots of the header at
360 / 390 / 768 / 1280 px on Light, Sepia and Dark to `~/reports/release-14/shots/`.
-## Slice H2 — one options button (same branch, stacked on H1)
+## Slice H2 — one theme toggle (same branch, stacked on H1)
Owns `export/app/components/{Header,MobileMenu}.tsx`, `export/e2e/{theme,theme-accent,responsive,
-brand}.spec.ts`, `export/CHANGELOG.md`. The editor keeps `ThemeMenu`/`ThemeToggle`
-(`editor/app/layout.tsx`).
-
-1. **Adopt HP's `OptionsDialog` and `ThemeRadios` in the export header** (both in
- `common/components/`, built by slice HP): one gear button, accessible name **"Options"**, 36 px,
- visible at every width, where the toggle is today, opening the "Options" dialog with the Base
- and Accent radiogroups. `MobileMenu` already renders `ThemeRadios` (HP). A change the export
- needs goes into the shared component, with the homepage's `options.spec.ts` re-run.
-2. `ThemeMenu` and `ThemeToggle` leave the export header. The slide-out menu keeps its radiogroups
- (one tap there is cheaper than opening a dialog from inside a sheet).
-3. As built: a choice applies at once and the dialog stays open; Escape, the close button and a
- click outside close it; focus is trapped and returns to the trigger.
-4. **The cost, recorded:** changing the ground is two actions where the toggle made it one. The
- fallback, if the operator asks, is the toggle kept beside the Options button — one line in
- `Header.tsx`.
-5. **Labels are contracts.** `theme.spec.ts` and `brand.spec.ts` drive the toggle by
- `/switch to/i`; `theme-accent.spec.ts` drives `Choose theme` and `menuitemradio`. Rewrite them
- to open Options and pick a `radio` by name; keep one helper, `chooseTheme(page, {base, accent})`,
- in `export/e2e/helpers.ts` (the homepage's is `homepage/e2e/helpers.ts`), and use it everywhere a
- spec changes theme through the UI. Specs that set `localStorage` directly are untouched. The
- no-flash assertions (`data-theme-ready`) are untouched.
-6. Gates: as H1, plus the homepage e2e `options.spec.ts` and `theme.spec.ts`.
+brand}.spec.ts`, `export/CHANGELOG.md`. The editor keeps its own header controls until T1.
+
+1. **The export header's theme control is the cycling toggle** (`ThemeToggle`, `variant="bare"`,
+ built by slice HP) in place of `ThemeMenu` + `ThemeToggle`: one key at the end of the header's
+ group, visible at every width. No options dialog. The accent picker is removed from the export
+ header (the export's `ThemeProvider`/`ThemeScript` take `pinAccent`, as the homepage's do).
+2. The slide-out menu keeps its Base radiogroup (`ThemeRadios`) until T1 drops Sepia and the accent
+ picker everywhere.
+3. **Labels are contracts.** `theme.spec.ts` and `brand.spec.ts` drive the toggle by
+ `/switch to/i`; `theme-accent.spec.ts` drives `Choose theme` and `menuitemradio` and changes
+ with the accent picker's removal. Keep one helper, `chooseTheme(page, { base })`, in
+ `export/e2e/helpers.ts` (the homepage's is `homepage/e2e/helpers.ts`: click the toggle until the
+ base is reached), deriving the cycle from `THEME_BASES` / `nextBase`. Specs that set
+ `localStorage` directly are untouched. The no-flash assertions (`data-theme-ready`) are
+ untouched.
+4. Gates: as H1, plus the homepage e2e `toggle.spec.ts` and `theme.spec.ts`.
+
+## Slice T1 — two grounds (planned; after HP merges, before the production deploy)
+
+Owns `common/components/theme*` (`themeConfig.ts`, `ThemeProvider.tsx`, `ThemeScript.tsx`,
+`ThemeToggle.tsx`, `ThemeMenu.tsx`, `ThemeRadios.tsx`), `common/styles/tokens.css`, the three apps'
+`globals.css`, the three headers and the export's `MobileMenu`, and the specs that mention Sepia.
+
+1. **Drop the Sepia base in every app:** the tokens, `THEME_BASES`, `nextBase`, the no-flash
+ script; the legacy `archive` → sepia migration now maps to light, and a stored `sepia` migrates
+ to `light`.
+2. **Remove the accent picker** from the export and editor headers and the slide-out menu; a stored
+ accent is ignored (as the homepage's `pinAccent` does).
+3. **Specs updated.** The parent's count of what mentions Sepia: 40 files, 154 lines, 16
+ specs/tests. The homepage's `toggle.spec.ts` derives its cycle from `THEME_BASES`, so it needs
+ no change beyond the base's removal.
+4. Gates: tsc; common tests; the three apps' builds; the three e2e suites.
## Slice S1 — a clear screen until the first Search (branch `r14/first-search`)
diff --git a/plans/release-14.md b/plans/release-14.md
@@ -18,7 +18,7 @@ slice HP added to it on the operator's ruling of the same day. Rules:
| Slice | Branch | What | Owns |
|---|---|---|---|
-| HP | `homepage/social-visible` | The homepage's social row and one Options gear in the header at every width, one shared `SocialLinks` component, larger keys with a focus ring; Changelog in the footer only; the instance cards' names as the site's wordmark; the social icon checked by an allowlist on save and at render; a sized SVG with no viewBox gets one; `featured` ("Show in header") on a social link | `common/components/{SocialLinks,OptionsDialog,ThemeRadios,Wordmark}.tsx`, `common/lib/{socialSvg,socialLinks}.ts` + tests, `common/lib/settingsSchema.ts` (the social-link type, parser, docs; the normalizer moved to `socialSvg.ts`), `common/lib/normalizeSocialSvg.test.ts`, `common/lib/{settings,site,homepage}.ts` (the save errors), `common/lib/{homepageSummary,siteColor}.ts`, `homepage/app/components/{Header,Footer,ArchiveCards}.tsx`, `homepage/app/lib/{nav,summary}.ts`, `homepage/app/not-found.tsx`, `homepage/e2e/**` (the fixtures, `helpers.ts`, the new and the rewritten specs), `homepage/playwright.config.ts`, `export/app/components/{MobileMenu,Footer}.tsx` (the `ThemeRadios` swap; the footer's read path), `editor/app/components/SocialLinksField.tsx` + `socialLinksJson{,.test}.ts`, `editor/app/{settings,sites}/actions.ts` (the save errors), `editor/e2e/settings.spec.ts`, `SETTINGS.md`, `SITE.md` |
+| HP | `homepage/social-visible` | The homepage's social row and one theme toggle in the header at every width, the wordmark's text dropped first on a very small screen and the row scrolling only as the last resort, one shared `SocialLinks` component, larger keys with a focus ring; Changelog in the footer only; the instance cards' names as the site's wordmark; the social icon checked by an allowlist on save and at render; a sized SVG with no viewBox gets one; `featured` ("Show in header") on a social link | `common/components/{SocialLinks,SocialScroll,ThemeRadios,ThemeToggle,ThemeScript,ThemeProvider,Wordmark}.tsx` + `themeConfig.ts`, `common/bin/doctor.ts`, the growth chart, `common/lib/{socialSvg,socialLinks}.ts` + tests, `common/lib/settingsSchema.ts` (the social-link type, parser, docs; the normalizer moved to `socialSvg.ts`), `common/lib/normalizeSocialSvg.test.ts`, `common/lib/{settings,site,homepage}.ts` (the save errors), `common/lib/{homepageSummary,siteColor}.ts`, `homepage/app/components/{Header,Footer,ArchiveCards}.tsx`, `homepage/app/lib/{nav,summary}.ts`, `homepage/app/not-found.tsx`, `homepage/e2e/**` (the fixtures, `helpers.ts`, the new and the rewritten specs), `homepage/playwright.config.ts`, `export/app/components/{MobileMenu,Footer}.tsx` (the `ThemeRadios` swap; the footer's read path), `editor/app/components/SocialLinksField.tsx` + `socialLinksJson{,.test}.ts`, `editor/app/{settings,sites}/actions.ts` (the save errors), `editor/e2e/settings.spec.ts`, `SETTINGS.md`, `SITE.md` |
| H3, H1, H2, S1 | per the plan | per the plan | per the plan |
**Order:** HP → H3 → H1+H2 → S1. The shared files are `editor/CHANGELOG.md`,
@@ -43,11 +43,21 @@ branch:
5. (The review, ruled by the parent.) No vendor file in the repository; the icon check hardened on
save and at render; the shared dialog wrapper unchanged.
6. On the homepage chart, light mode has dark lines and dark mode has light lines.
+7. On very small screens the header keeps its mark and drops the word; the social icons scroll only
+ when they still cannot fit (rather than hiding icons on small screens).
+8. The options menu is dropped for now in favour of a three-way toggle.
+9. (The re-review, ruled by the parent.) A title or desc can no longer reach a page; an icon loads
+ nothing from elsewhere; a stored icon the checker refuses does not block an unrelated save.
**The branch's history was rewritten once** (after the review, ruling 5): its first nine commits,
one of which added a vendor file as a test fixture, were replaced by the five commits below,
re-committed from the same tree in the same logical steps, each tsc-clean. No vendor file is in
-any commit of `main..HEAD`.
+any commit of `main..HEAD`. Everything after `afc642fd` is new commits on top, and `main`
+is merged in twice (`10cefd15`, then `918e5f85`).
+
+**The Options dialog was built, then replaced by the toggle** (ruling 8): `OptionsDialog` (a gear
+opening a modal with Base and Accent) shipped in `4d93dc84` and was deleted in `5da60518`, with
+`options.spec.ts` and its screenshots. `ThemeRadios` stays: the export's slide-out menu renders it.
**What shipped.**
- **One row, `common/components/SocialLinks.tsx`.**
@@ -70,30 +80,25 @@ any commit of `main..HEAD`.
links marked `featured` when any is marked, else all of them; of those, the last four. Marking is
choosing: one marked of three shows one.
- **The homepage header** (`homepage/app/components/Header.tsx`): one group — the social row, then
- the Options gear — in the bar at every width. The gear is dressed as a key and its box sits
+ the theme toggle — in the bar at every width. The toggle is dressed as a key and its box sits
directly after the last link's, so every glyph is 16 px from the next. From `md` (768 px) the bar
is wordmark · nav · group, the nav's last link 32 px before the group's first box (40 px before
its first glyph; the nav's own links are 24 px apart). Below `md` it is wordmark · group, and the
- four nav links have the rule below to themselves, where they fit at 360 px: no fade, nothing
- scrolls (`overflow-x-auto` stays as a guard for larger text). The bar shows the LAST n of the
- header's links, by width and pointer (see "The header layout").
+ four nav links have the rule below to themselves, where they fit at 320 px. No link is hidden by
+ width (ruling 7): on a very small screen the wordmark's text goes first, and the row scrolls only
+ as the last resort (see "The header layout").
- **Changelog is in the footer only** (ruling 2). `homepage/app/lib/nav.ts` declares `HEADER_NAV`
(Docs, Source, Downloads, Stats) and `FOOTER_NAV` (the same, then Changelog). The footer's
Sections and the 404 page read `FOOTER_NAV`.
-- **One Options gear** (ruling 2): `common/components/OptionsDialog.tsx`, built to slice H2 so the
- export can adopt it.
- - The trigger is named "Options", 36 px (44 px under a coarse pointer), dressed as a social key.
- - It opens a modal on the shared `common/components/ui/dialog.tsx`, used exactly as it is on
- `main` (the editor's command palette renders it too). OptionsDialog styles its own panel
- (`border-border`, because no app sets a base border colour and Tailwind v4's bare `border` is
- currentColor) and its own Close (a 36 px key with the ring; the wrapper's is turned off).
- - The dialog, titled "Options", holds `common/components/ThemeRadios.tsx`: the Base and Accent
- native radiogroups, extracted from the export's `MobileMenu` unchanged. `MobileMenu` renders
- them now; the swap kept every name, role and class.
- - It opens with focus on the CHECKED Base radio. A pick applies at once and the dialog stays
- open. Escape, Close and a click outside close it; focus is trapped and returns to the gear; the
- page does not scroll (Radix).
- - `ThemeMenu` and `ThemeToggle` are unchanged, and the export and the editor still render them.
+- **One theme toggle** (ruling 8, replacing ruling 2's dialog): `common/components/ThemeToggle.tsx`
+ with a new `variant="bare"` — dressed as a social key (36 px, 44 px under a coarse pointer, no
+ border, the hover square, the ring, a 20 px glyph), cycling the base in `nextBase`'s order and
+ named "Switch to {next}". Its default rendering, which the export and the editor use, is
+ unchanged.
+ - **The accent is pinned on the homepage**: with no accent control there, `ThemeScript` and
+ `ThemeProvider` take `pinAccent`, so the stored `ytdlp-tb:accent` is neither read nor removed
+ and the homepage keeps Signal, with no flash (the pre-paint script skips the read). Without the
+ prop both are unchanged (unit-tested: the script string is identical).
- **The instance cards' names** (ruling 4).
- `homepage-summary.json` `sites[]` gains an optional `wordmarkLead`: site.json's, resolved
against `siteTitle` by `lib/brand.ts` `wordmarkLeadFor`, the resolver the sites' header and
@@ -115,15 +120,22 @@ any commit of `main..HEAD`.
instruction. Comments, a leading XML declaration and a leading DOCTYPE with no internal subset
are removed first.
- Elements: shapes, groups, `defs`, `symbol`, `use`, gradients, `stop`, `pattern`, `clipPath`,
- `mask`, filters, `text`/`tspan`, `title`/`desc`, and `animate`/`animateTransform`/`set`. No
- `script`, `style`, `foreignObject`, `a`, `image` or HTML element.
+ `mask`, filters, `text`/`tspan`, and `animate`/`animateTransform`/`set`. No `script`, `style`,
+ `foreignObject`, `a`, `image`, `title`, `desc` or HTML element (R1: a title or desc is an HTML
+ integration point, where a child element left the icon unclosed around the page; one holding
+ text only is removed first).
- Attributes: the SVG presentation, geometry, filter and animation set, plus `aria-*`, `data-*`
and `xmlns:*`. No event handler, whatever separates it.
- Values: character references (numeric and named, with or without `;`) are decoded and the
whitespace a browser ignores in a URL dropped before the checks. No `javascript:` or
- `vbscript:`. `href`/`xlink:href` only `#id`; every `url(…)` only to `#id`; a `style` with no
- escape, `@import`, `expression(`, `behavior` or binding; an animation never targets `href` or a
- handler; ids are plain names (`^[A-Za-z_][\w.:-]*$`).
+ `vbscript:`; no backslash (a CSS escape) or CSS comment; no function that loads anything
+ (`image-set(`, `-webkit-image-set(`, `image(`, `cross-fade(`, `element(`, `src(`, `paint(`,
+ `@import`, `expression(`) — an animation's `to`/`from`/`values`/`by` included (R2).
+ `href`/`xlink:href` only a plain `#id` as written; every `url(…)` only to `#id`, spelled so the
+ render's scoping rewrites it (R6). A `style` holds presentation properties only (fill, stroke,
+ stop-color, the opacities, stroke width/caps/joins, fill-rule, clip-rule, display, visibility,
+ paint-order). An animation never targets anything named `href` or a handler (R5). Ids are
+ plain names (`^[A-Za-z_][\w.:-]*$`).
- The normalized OUTPUT is checked again, so no transform can assemble what the input check
refused.
- `socialSvgProblem` names the reason class. The editor's save error and the writers' errors
@@ -131,7 +143,16 @@ any commit of `main..HEAD`.
- `SETTINGS.md` and `SITE.md` (generated) say what an icon may contain.
- **The read path.** `safeSocialSvg` (`common/lib/socialLinks.ts`) runs the check again at render.
`SocialLinks` and the export footer inline only what passes; a link whose icon fails shows its
- label as text. Every `dangerouslySetInnerHTML` of a social SVG goes through it (there are two).
+ label as text, capped at 10rem with an ellipsis and the full label in `title` (R7). Every
+ `dangerouslySetInnerHTML` of a social SVG goes through it (there are two). Each key clips its
+ icon's paint (`overflow-hidden`, `contain: paint`; R4); the focus ring is the key's own shadow,
+ outside that clip.
+- **The write path** (R3): `socialLinksForSave` keeps a link whose `svg` is byte-identical to one
+ already stored exactly as it is, and checks only a new or edited icon — in `writeSettings`,
+ `writeSite`, `writeHomepageConfig` and the editor's settings and site actions. So a lane pause, a
+ priority or a title never fails on an icon an older build stored; the render shows such an icon
+ as its label, and `archilyzer doctor` names it ("social icons": file, label, reason class, never
+ the markup). In this worktree the doctor line reads "3 icons in 1 file pass the check".
- **The size rule** (ruling 1): a root with no viewBox but a numeric `width` and `height`
(unitless or px, decimals, either quote) gets `viewBox="0 0 W H"`, read from the root's own
attributes (a size spelled inside another attribute's value does not count). A percentage,
@@ -147,32 +168,48 @@ any commit of `main..HEAD`.
**The header layout** (built and screenshotted before each choice). Measured: the wordmark link is
148 px (152 px at 1× device scale), the four-link nav 276 px, a key 36 px (44 px under a coarse
-pointer), the gear the same.
-- **The rule** (one CSS rule per step on the row, stated in `Header.tsx`; the classes' `max-[Npx]`
- mean "width < N"):
+pointer), the toggle the same.
+- **No link is hidden by width** (ruling 7): the header shows its (at most four) links at every
+ width. The bar is a size container (`@container/bar`), and when it is narrower than the full
+ wordmark, a 12 px gap and the group need, the wordmark's TEXT is hidden and the mark stays (the
+ link is still "Archilyzer home"). The threshold is a class per link count and pointer, in rem so
+ it follows the text size (`WORDMARK_FITS`, measured, not guessed):
- | Width | Mouse | Touch (coarse pointer) | Why the step |
- |---|---|---|---|
- | ≥ 768 | 4 | 4 | the nav, four keys and the gear fit at 768 (348 px free with touch) |
- | 390–767 | 3 | 3 | four keys and the gear overflowed a 360 px bar by 20 px |
- | 360–389 | 3 | 2 | three 44 px keys and the gear need 380 px |
- | 340–359 | 2 | 2 | three 36 px keys and the gear need 348–352 px |
- | < 340 | 2 | 1 | two 44 px keys and the gear need 336 px |
+ | Links | Full wordmark with a mouse | Full wordmark with touch |
+ |---|---|---|
+ | 1 | from 276 px | from 292 px |
+ | 2 | from 312 px | from 336 px |
+ | 3 | from 348 px | from 380 px |
+ | 4 | from 384 px | from 424 px |
-- **Measured with the rule** (the fixture's three links; the gap between the wordmark and the
- group, of which 12 px is the minimum): mouse — 320: 20 px, 339: 39, 359: 59, 360: 24, 390: 54;
- touch — 320: 40, 340: 16, 360: 36, 389: 65, 390: 22. No width from 320 up scrolls sideways.
-- **Two rows at 360 px** (a mouse): the wordmark (20–168), 28 px, the group (196–340: three keys
- and the gear), the 20 px gutter; below, the four nav links (20–291), 49 px to spare.
+ Measured at the threshold the gap is exactly 12 px; 1 px below, the text is hidden.
+- **With the mark alone** (the fixture; the gap between the mark and the group): with a mouse, 3
+ links — 320: 108 px, 340: 128; 4 links — 320: 72, 340: 92, 360: 112. With touch, 3 links — 320:
+ 76, 340: 96, 360: 116; 4 links — 320: 32, 340: 52, 360: 72. Four 44 px links, the toggle and the
+ mark fit a 320 px screen.
+- **The last resort** (`common/components/SocialScroll.tsx`): the row
+ scrolls inside a box, its END shown first (the box is `direction: rtl`, the row `ltr` and
+ `w-max`, so the first scroll position is the right edge — no script, no change to the DOM or tab
+ order), no scrollbar, the toggle outside it, the header never wider than the screen. A link that
+ takes focus is scrolled into view with its ring (the one bit of script: Chromium does not bring
+ a partly hidden link into view inside an rtl box). From 300 px up, with default text, nothing
+ scrolls; it takes 280 px with four 44 px links, or 320 px at a 200 % text size. Checked in
+ Chromium and in an installed Firefox build (end first, nothing sideways); WebKit UNVERIFIED (the
+ installed build does not match this Playwright).
+- **What a visitor sees, by width** (default text size): the full wordmark from the table's width
+ up; the mark alone below it, down to 300 px; the scroll fallback only below that, or at a much
+ larger text size.
+- **Two rows at 360 px** (a mouse, three links): the wordmark (20–168), 28 px, the group (196–340:
+ three keys and the toggle), the 20 px gutter; below, the four nav links (20–291), 49 px to spare.
- **How it got here:**
- First build, with two 32 px theme buttons and five nav links: three keys in the bar made a
360 and a 390 px page scroll sideways by 36 px, so the keys were pinned at the end of the nav's
rule, the nav in that rule below `lg`, and the row rendered twice.
- Rulings 2 and 3 freed the bar: one gear, four links, and the group in the bar at every width.
- The nav is back in the bar from `md`.
- - The review found 320 px with a mouse scrolled sideways by 8 px (the gear cut). The 340/360
- steps and the touch step below 340 came from that; the classes now say `max-[390px]`, as the
- prose says "below 390".
+ - The review found 320 px with a mouse scrolled sideways by 8 px; a width × pointer rule then hid
+ links step by step (`6c500818`).
+ - Ruling 7 replaced that rule with the collapsing wordmark and the scroll fallback (`0b3189fb`),
+ and ruling 8 the gear with the toggle, the same size (`5da60518`).
**The cards' accent treatment** (ruling 4). Both treatments were built and screenshotted on the
three grounds at 390 and 1280 px, from a family-like summary: the six live titles, their site.json
@@ -257,70 +294,99 @@ operator's pasted icons do):
|---|---|
| `518dd272` | `common:` `lib/socialSvg.ts`, the allowlist checker on save and at render with reasons; the size rule; `featured`; `lib/socialLinks.ts` (header rule, `safeSocialSvg`, id scoping, `sizeSocialSvg`); the writers' errors; the tests; `SETTINGS.md` / `SITE.md` |
| `a9dfa3de` | `editor:` "Show in header" with its hint; `socialLinksJson` + unit test; the save errors name the reason; the settings spec reads `featured` back |
-| `4d93dc84` | `common:` `SocialLinks`, `OptionsDialog` (its own border and Close, focus on the checked radio), `ThemeRadios`; `export:` `MobileMenu` renders `ThemeRadios`, the footer inlines only through `safeSocialSvg` |
-| `6c500818` | `homepage:` the social row and the gear in the header at every width, the width × pointer rule; Changelog in the footer only; the e2e's own settings and sites; `social`, `options`, nav and theme specs |
+| `4d93dc84` | `common:` `SocialLinks`, `OptionsDialog` (later deleted), `ThemeRadios`; `export:` `MobileMenu` renders `ThemeRadios`, the footer inlines only through `safeSocialSvg` |
+| `6c500818` | `homepage:` the social row and the gear in the header at every width, the width × pointer rule (later replaced); Changelog in the footer only; the e2e's own settings and sites; the specs |
| `88d61908` | `homepage:` the cards' names as the site's wordmark, the lead tinted; `wordmarkLead` in the summary and its check on read; `siteAccentColor`; `Wordmark` `leadStyle`; `instance-wordmark.spec.ts` |
| `afc642fd` | `plans:` this record; the plan's slice HP, H1 and H2; STATE; the changelogs |
-| _this_ | `homepage:` the growth chart's separators in the ground's foreground, 1 px, full strength (ruling 6); `growth-chart.spec.ts`; the record, the plan, the changelog |
+| `031c2115` | `homepage:` the growth chart's separators in the ground's foreground, 1 px, full strength (ruling 6); `growth-chart.spec.ts`; the record, the plan, the changelog |
+| `0b3189fb` | `homepage:` the wordmark's text drops first on a very small screen (a container query per link count and pointer); `SocialScroll`, the last-resort scroll box, end first; no link hidden by width (ruling 7) |
+| `5da60518` | `homepage:` one toggle (`ThemeToggle variant="bare"`) in place of the Options dialog; the homepage's accent pinned (`pinAccent`); `OptionsDialog` and `options.spec.ts` deleted; `toggle.spec.ts` (ruling 8) |
+| `68ad1464` | `common:` the checker drops title/desc, refuses loading functions, escapes and comments, allows presentation styles only, plain references, reasons that say how to export; the review's vectors (re-review R1, R2, R5, R6, R8, R9) |
+| `0be068cf` | `common, editor:` `socialLinksForSave` — an unchanged stored icon never blocks a save; `archilyzer doctor` names stored icons that fail (R3) |
+| `4f7bc097` | `common, export, homepage:` each key clips its icon's paint; a refused icon's label bounded; `svg-vectors.spec.ts` (the parser invariant, nothing fetched); the hostile e2e checks no other origin (R1, R2, R4, R7) |
+| `4d11542c` | merge `main` (`10cefd15`, the stats fix); `homepage/CHANGELOG.md` both sides kept |
+| `fe9fb4b2` | merge `main` (`918e5f85`, slice Q's umtool build trace); `editor/CHANGELOG.md` both sides kept, `main`'s bullet under the one it refers to |
+| _this_ | `plans:` this record; the plan's slices HP, H1, H2 and the new T1; STATE; the changelogs |
-**Gates** (at `88d61908`; logs `$T/hp5-*.log`):
-- **tsc** was clean at each of the five commits (the rewrite ran it on each commit's own tree:
- 34–44 s).
-- **Unit:** common **2,179/2,179**; editor unit **86/86**; homepage unit **8/8**;
- `test:scripts` **185 + 1 skipped**; mcp **269/269**.
+**Gates** (at `4d11542c`, after the first merge of `main`; logs `$T/hp11-*.log`. The second
+merge, `fe9fb4b2`, brings only umtool, `scripts/`, plans and an editor changelog bullet; tsc and
+`test:scripts` were run again after it: see the last line):
+- **tsc** was clean before each commit since `afc642fd`, and before this one.
+- **Unit:** common **2,202/2,202**; editor unit **87/87**; homepage unit **8/8**; `test:scripts`
+ **185 + 1 skipped**; mcp **271/271**.
- **Docs:** `settings example --check`, `docs files --check` and `docs env --check` all exit **0**.
-- **Builds:** homepage **ok** (14 s, with the fixture icons), export **ok** (26 s), editor **ok**
- (40 s). The homepage gate is `pnpm --filter homepage exec next build`, not `archilyzer build
- homepage --no-source`, which would withdraw the published source the parent put in this worktree.
-- **`git diff main...HEAD -- common/components/ui/`** is empty: the editor's command palette is
- untouched.
-- **Homepage e2e, full suite:** **68 passed, 0 failed, 0 skipped (1.9 min)**, in the
- published-source state. That is 63 before the review plus 5 (the 320/339/359 px widths, six
- links under touch at 360, the hostile icons); the touch, Options and card specs were tightened in
- place.
-- **Export e2e** (`site-branding brand theme theme-accent responsive related-sites archives-off`,
- the specs on the header's theme controls, the slide-out menu and the footer's social row):
- **35 passed, 0 failed (1.5 min)**.
-- **Editor e2e** (`settings sites-crud export-search`, the specs on the social-links field in both
- forms and the export footer's links; there is no command-palette spec): **45 passed, 0 failed
- (2.1 min)**. The spec-order failure below did not arise: this worktree's
- `editor/test-settings.json` already existed.
-- **Earlier rounds** (the same checks, before the review): homepage e2e 36 → 50 → 59 → 63 passed;
- export theme/responsive/brand 23 passed after the `MobileMenu` swap; the first editor run 44
- passed and 1 failed on spec order (see "Found and left").
-- **Screenshots:** `~/reports/release-14/shots/`, from a production build of the worktree with the
- fixture icons (`SETTINGS_FILE`), served on 3331. **The worktree's `homepage/out` is that fixture
- build: never deploy it; rebuild first.**
- - `hp2/`: `header-{320,340,360,390,768,1024,1280}-{light,sepia,dark}.png`,
- `dialog-{360,1280}-{…}.png` (360×640 and 1280×800), `footer-{390,1280}-{…}.png` (with
- Changelog), `gear-focus-ring-{…}.png`, `forced-gear-{unfocused,focused}-{light,dark}.png`.
- - `hp3/`: `tint-` and `underline-{390,1280}-{light,sepia,dark}.png`, retaken with the current
- header.
- - `hp/`: the first round's shots, superseded.
+- **Doctor:** the new section prints `social icons` / `ok stored icons 3 icons in 1 file pass the
+ check` (the worktree's `settings.json`, the parent's copy; no other file there holds icons).
+- **Builds:** homepage **ok** (22 s, `pnpm --filter homepage exec next build` with the fixture
+ icons), export **ok** (34 s), editor **ok** (59 s).
+- **Homepage e2e, full suite:** **75 passed, 0 failed, 0 skipped (1.7 min)**: `social` 24,
+ `marketing` 10, `toggle` 9, `stats` 7, `source` 5, `brand` 4, `docs` 4, `downloads` 3, `theme` 3,
+ `growth-chart` 2, and one each in `svg-vectors`, `instance-wordmark`, `instance-colours` and
+ `no-data`. `options.spec.ts` went with the dialog.
+- **Export e2e** (`site-branding brand related-sites theme theme-accent responsive archives-off`):
+ **35 passed, 0 failed (1.6 min)**. The theme specs are unchanged: `ThemeToggle`'s default
+ rendering is the export's as before.
+- **Editor e2e** (`settings sites-crud`): **26 passed, 0 failed (1.8 min)**.
+- **Browsers:** the scroll box's end-first start and focus scrolling are covered in Chromium by the
+ suite and were checked by hand in Firefox at 240 px. **WebKit is unverified**: the installed
+ revision does not match this Playwright's.
+- **Earlier rounds:** at `88d61908`, common 2,179, editor unit 86, mcp 269, homepage e2e 68, export
+ e2e 35, editor e2e 45 (with `export-search`); before the review, homepage e2e 36 → 50 → 59 → 63.
+- **Screenshots** (`~/reports/release-14/shots/`, at 2×):
+ - `hp4/` (64), the narrow header, from a dev server on 3330 reading synthetic settings
+ (`SETTINGS_FILE`: the fixture's three icons, or six so the header shows four) and an empty
+ `SITES_DIR`:
+ - `header-{3,4}icons-{320,340,360,390}-{fine,coarse}-{light,sepia,dark}.png`;
+ - `header-{3,4}icons-{768,1280}-{light,sepia,dark}.png`;
+ - the wordmark is collapsed in every 320 and 340 px shot, at 360 px in all but three icons with
+ a mouse, and at 390 px with four icons under touch;
+ - `fallback-280-coarse-rest-dark.png`: four icons under touch at 280 px, the box's end in view;
+ - `fallback-280-coarse-start-dark.png`: the same after Tab reaches the first icon, the box at
+ its start;
+ - `fallback-320-200pct-text-{rest,start}-dark.png`: the same pair at 320 px with the text at
+ 200 %.
+ - `hp5/` (19), the toggle: `header-{320,360,390,768,1280}-{light,sepia,dark}.png` (three icons,
+ the toggle last) and `forced-toggle-{unfocused,focused}-{light,dark}.png`.
+ - `chart/`: the separators at 100, 60 and 35 %, per ground (the chart's section above).
+ - `hp3/`: the cards' tint and underline; the header work does not reach them.
+ - `hp2/` (the gear header) and `hp/` (the first round) are superseded; `hp2`'s dialog shots are
+ deleted.
+ - **The worktree's `homepage/out` is a fixture build: never deploy it; rebuild first.**
- **Numbers tool:** none.
+- **After the second merge** (`fe9fb4b2`; `$T/hp12-post.log`): tsc clean in every package (42 s);
+ `test:scripts` **188 + 1 skipped**, `main`'s new guard on umtool's build trace among them.
-**They bite:**
-- `social.spec.ts` with the id scoping, the header bound and `pointer-coarse:size-11` removed by
- hand: 5 failed (the per-copy ids test, both coarse-pointer tests, both six-link tests).
-- The hostile-icon spec fails without the read path: a stored `<svg/onload=…>` inlined as is runs
- when the page parses it.
-- The loader test fails without `withCheckedLead` (a whole-title lead is kept).
-- `instance-wordmark.spec.ts`'s "Fix" + "ture Three" fails if the wordmark's spans stop being
- adjacent inline text.
+**They bite** (each change made by hand in the worktree, the specs run, the change reverted):
+- The separators in `var(--background)`: `growth-chart.spec.ts`'s ground test fails (Light: the
+ stroke is the ground's own colour).
+- `WORDMARK_FITS` taken off the wordmark: 9 of `social.spec.ts` fail (320, 340 and 360 px with a
+ mouse; 320–390 px under touch; the 280 px fallback; the 200 % text).
+- `pinAccent` taken out of `layout.tsx`: `toggle.spec.ts`'s stored-accent test fails.
+- `title` and `desc` back in the allowlist: `svg-vectors.spec.ts` fails on `title_child_el` (the
+ page's `<main>` parsed inside the icon).
+- The loading-function check skipped: the hostile-icon test fails (an `image-set(` icon is inlined
+ instead of its label).
+- From the earlier rounds:
+ - `social.spec.ts` with the id scoping, the header bound and `pointer-coarse:size-11` removed:
+ 5 failed.
+ - The hostile-icon spec fails without the read path.
+ - The loader test fails without `withCheckedLead`.
+ - `instance-wordmark.spec.ts`'s "Fix" + "ture Three" fails if the wordmark's spans stop being
+ adjacent inline text.
#### Review (verdict SHIP AFTER FIXES; `$T/hp-review.md`)
| Finding | Fix |
|---|---|
-| M1: 320 px with a mouse scrolled sideways by 8 px | `6c500818`: the width × pointer rule (a mouse shows two below 360, touch one below 340); 320/339/359 and the touch steps in `social.spec.ts`; `max-[390px]` to match "below 390" |
-| M2: the `ui/dialog.tsx` change restyled the editor's command palette | `4d93dc84`: the wrapper is `main`'s, byte for byte; OptionsDialog styles its own panel and Close. The records' "first importer" is corrected |
+| M1: 320 px with a mouse scrolled sideways by 8 px | `6c500818`: the width × pointer rule, since replaced by ruling 7 (`0b3189fb`) |
+| M2: the `ui/dialog.tsx` change restyled the editor's command palette | `4d93dc84`: the wrapper is `main`'s, byte for byte (the dialog it served is deleted since) |
| M3: a vendor file as a test fixture would be published by the source mirror | ruled: no vendor file. The branch was rewritten so the file and its notice never entered it; D1 is tested with synthetic sized SVGs; the e2e disc is synthetic |
| M4: five inputs passed the normalizer and ran script | `518dd272` (the allowlist checker, the output re-check, the reasons), `4d93dc84` (the read path in both renderers); the review's five inputs and each class tested at the normalizer, the read path and in the browser |
| L1, L2: id scoping | `518dd272`: plain ids only (the checker refuses others; comments are removed), entity-quoted and case-varied references |
-| L3: tests that could pass broken | the loader checks the lead; a mid-word lead; the gear 36/44 px exactly; the glyph's own colour; six links under touch at 360; 320 px |
-| L4: "below 390" vs `max-[389px]` | `max-[390px]` |
+| L3: tests that could pass broken | the loader checks the lead; a mid-word lead; keys 36/44 px exactly; the glyph's own colour; six links under touch at 360; 320 px |
+| L4: "below 390" vs `max-[389px]` | superseded with the width rule (ruling 7) |
| L5: `homepage.json` could win in the e2e | `SITES_DIR` → an empty directory. The reused dev server is pre-existing and left |
-| L6: the dialog opened on the first radio | it opens on the checked Base radio (spec) |
+| L6: the dialog opened on the first radio | fixed in `4d93dc84`; the dialog is deleted since (ruling 8) |
| L7: `hp3` at 390 showed an old header | retaken |
| L8: `featured` in `/site.json` | accepted, recorded above |
| L9: the fixture build in `homepage/out` | recorded above: never deploy it |
@@ -329,11 +395,30 @@ operator's pasted icons do):
| L12: the fixture echoed an operator icon's colours | the fixture's colours changed; the pre-existing test file is outside this branch |
| L13: `label` said "Visible name" | reworded; `SETTINGS.md` / `SITE.md` regenerated |
-The answers, as ruled: the touch rule is kept, with the 320 step; the tint is kept; the footer
-colour `--muted-foreground` is kept; marking is choosing, kept, and said beside the checkbox.
+The answers, as ruled: the tint is kept; the footer colour `--muted-foreground` is kept; marking is
+choosing, kept, and said beside the checkbox. The touch rule was kept, then superseded by ruling 7.
+
+#### Re-review (verdict SHIP AFTER FIXES; `$T/hp-review.md`, "New findings")
+
+| Finding | Fix |
+|---|---|
+| R1: a `title` or `desc` with an element child left the page's parser inside the icon | `68ad1464`: both are out of the allowlist; one holding text only is removed before the check, any other is refused as an element. `4f7bc097`: `svg-vectors.spec.ts` parses every accepted input as the static page carries it and asserts the page after it stays outside the icon |
+| R2: an icon could load from another origin | `68ad1464`: `\` and `/*` refused in every value after decoding; `image-set(`, `image(`, `cross-fade(`, `element(`, `src(`, `paint(`, `@import` and `expression(` refused in every value; `style` holds presentation properties only (an allowlist); a `url(…)` count that differs raw and decoded is refused; the reviewer's eight inputs in the unit tests (`socialSvg.vectors.ts`). `4f7bc097`: the hostile e2e and `svg-vectors.spec.ts` fail on any request to another origin |
+| R3: a stored icon the allowlist refuses blocked every save | `0be068cf`: `socialLinksForSave` checks only a new or edited icon, and keeps an unchanged stored one byte for byte, in the settings, site and homepage writers; `archilyzer doctor`'s "social icons" line names each failing icon by file and label, with the reason; the editor changelog's upgrade text |
+| R4: an icon could paint and catch clicks outside its key | `4f7bc097`: each key, in the header and both footers, is `overflow-hidden` with `contain: paint`, so a `class` or `overflow` stays inside it; `68ad1464`'s style allowlist refuses `position`, `inset` and `z-index` |
+| R5: an `attributeName` naming an href | `68ad1464`: an `attributeName` containing `href`, or starting `on` after any prefix, is refused |
+| R6: an encoded or padded fragment passed the check but was not scoped | `68ad1464`: the raw value must be a plain fragment |
+| R7: a refused icon's label had no width bound | `4f7bc097`: at most 10rem (`max-w-40`) with an ellipsis, the whole label as its title, in the header and both footers |
+| R8: the comment on the echoed name | `68ad1464`: the echoed name is stripped to letters, digits and `_.:-`, at most 40 characters |
+| R9: "remove the style block" drops the colours; a DOCTYPE's reason | `68ad1464`: the reason says to export with presentation attributes rather than a style block (in Inkscape, save as Plain SVG); a DOCTYPE with an internal subset has its own reason; the editor changelog says the same |
+| R10: the branch moved during the review | nothing to fix: every commit after `afc642fd` is for the next review |
**Found and left:**
-- **The export's header and the rest of its footer** are slices H1 and H2.
+- **The export's header and the rest of its footer** are slices H1 and H2; **T1** ("two grounds")
+ is planned, not built.
+- **The shared charts' separators** (`/stats`, the export's charts) are still drawn in the page's
+ background colour; ruling 6 names the homepage chart. A follow-up for the operator to rule on.
+- **WebKit** is unverified for the scroll box (see Gates).
- **The Settings form's hint** still says the default links show "in every site's footer"; true of
the sites until H1. Operator-facing; left alone.
- **`export-search.spec.ts`'s "a site's own social links win over the global default" depends on
@@ -344,25 +429,21 @@ colour `--muted-foreground` is kept; marking is choosing, kept, and said beside
homepage dev server must be stopped before the suite runs; `reuseExistingServer` would otherwise
reuse one started without the fixture's environment (pre-existing).
- **A pasted Illustrator or Inkscape file** with a `<style>` block, `<metadata>` or `inkscape:*`
- attributes is now refused, with the element or attribute named in the error. Stripping those is
- the operator's step.
+ attributes is refused, with the element or attribute named and the export setting to use.
- **The hub's official cards** still show plain titles; ruling 4 names the homepage only.
-- **Merging after `fix/stats-cache-key`:** its `homepageSummary.ts` hunks (the `SiteMetricStat`
- comment, `withUndated`, the loop, `transcribed: withUndated(…)`) do not touch this branch's
- (the version comment, the `wordmarkLead` field at the end of `HomepageSummarySite`, the spread
- after `accentIdOf`), and its test is inserted mid-file where this branch's is appended: the merge
- is clean there. Both add an `editor/CHANGELOG.md` bullet: this branch's are at the END of
- `[Unreleased]`, theirs at the top, so that merges cleanly too.
+- **At a 200 % text size the homepage's own content** is wider than a 320 px screen; the header is
+ not (the e2e checks the header only).
**Changelog.**
-- `homepage/CHANGELOG.md` `[Unreleased]`: the row and the gear in the header; one Options button;
- Changelog in the footer only; the cards' wordmark names; the larger keys with a focus ring; the
- e2e's fixtures and helper.
+- `homepage/CHANGELOG.md` `[Unreleased]`, worded as the end state, after `main`'s stats bullet: the
+ icons in the header at every width beside the toggle (the wordmark's text drops first, the scroll
+ box is the last resort); one theme toggle and the homepage's own accent; Changelog in the footer
+ only; the cards' wordmark names; the chart's separators; the larger keys with a focus ring; the
+ e2e's fixtures and specs.
- `editor/CHANGELOG.md` `[Unreleased]`, at the end of the list: the size rule and "Show in header";
- the icon check with reasons, on save and at render.
-- `export/CHANGELOG.md` has no `[Unreleased]` heading on `main`; `fix/stats-cache-key` creates it.
- After that merges, one bullet is owed there: "**A social icon that fails the check is shown as
- its label.** The footer inlines a social link's SVG only if it passes the same check as a save
- (release 14); otherwise the link shows its label as text."
+ the icon check with reasons, on a new or edited icon and at render, and the upgrade note (an
+ unchanged stored icon is kept; `archilyzer doctor` names those that fail).
+- `export/CHANGELOG.md` `[Unreleased]` (created by `fix/stats-cache-key`): an icon that fails the
+ check is shown as its label, bounded, and every icon paints inside its box.
## Rollout