# Release 14 — the social icons within reach, and a clear screen until the first Search
`main` at `ac438bbc` (release 12 merged and not rolled out; release 13 is a parallel session's).
Plan: [`export-header-first-search.md`](export-header-first-search.md), written 2026-09-28, with
slice HP added to it on the operator's ruling of the same day. Rules:
`plans/tools/implementer-rules.md`, with the commit trailer this release's prompts give.
**The operator's standing choices** (the plan, "The operator's words"; not re-opened):
- **No copy** beside any social link: icons with accessible names only.
- **A link the operator adds is an entry in `settings.json` `socialLinks`, never code.** The earlier
tip-link branch is parked and not merged; nothing is taken from it.
- **No vendor file in the repository**, not even as a test fixture: the tracked tree is published
by the source mirror.
- **Nothing is edited in the primary checkout**; each slice has its own worktree, and the parent
merges with `git merge --no-ff` only on a clean tree.
## The slices
| Slice | Branch | What | Owns |
|---|---|---|---|
| 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` |
| Lows, chart gap, T1, H1 (H3 folded in), H2 | `r14/two-grounds-headers` | The final review's Lows; the charts' surface gap; two grounds and each site in its own accent; the export and hub headers carry the social row and the toggle as one group, with an Archilyzer link to the homepage's `#instances` in place of the sites dropdown and the hub link; Changelog to the footer; after its review, the narrow header keeps the name and shows only the marked links (every header) | `common/components/{ThemeProvider,ThemeScript,ThemeToggle,SocialScroll}.tsx` + `themeConfig.ts` (and the deleted `ThemeMenu`, `ThemeRadios`), `common/components/charts/{ChartView,CrossSiteChart,surfaceGap}`, `common/styles/tokens.css`, `common/lib/{brand,accent,siteColor,paths,project,socialSvg,siteSchema,settingsSchema}.ts` + tests, `scripts/next-build-trace.test.mjs`, `export/app/components/{Header,MobileMenu,Footer}.tsx` (and the deleted `SiblingSwitcher`), `export/app/{layout.tsx,globals.css,changelog/page.tsx,lib/brand.ts}`, `export/e2e{,-hub}/**` (the theme, header and branding specs), `export/playwright.config.ts`, `editor/app/{layout.tsx,globals.css,sites/components/SiteForm.tsx}`, `editor/e2e/theme.spec.ts`, `homepage/app/{page.tsx,layout.tsx,globals.css,lib/*,changelog/page.tsx,components/{Header,ArchiveGrowthChart,ArchiveCards}.tsx}`, `homepage/e2e/**`, `homepage/content/docs/operate.md`, `SETTINGS.md`, `SITE.md` |
| HS | `r14/hidden-sites` | Hidden sites: `site.json` `listed` (absent = listed). An unlisted site builds and deploys as before, and is left off the homepage (cards, chart, `/stats`), the hub (members, federated search, `corpus.json`, `llms.txt`), every other site's footer and the published id lists (`channel-sites.json`, the pooled `stats/`); the channels only it exposes count in no public total. One checkbox in the site form | `common/lib/{siteSchema,site,homepageSummary}.ts` + tests, `common/controller/{poolSummary,buildStats}.ts` + tests, `common/bin/{compose-homepage,compose-hub}.ts` + `compose-hub.test.ts`, `editor/app/sites/{actions.ts,components/SiteForm.tsx}`, `editor/e2e/{helpers.ts,sites-crud.spec.ts}`, `homepage/e2e/{fixture-summary.ts,unlisted-site.spec.ts}`, `SITE.md`, `plans/FACTS.md` (Naming hazards) |
| S1 | `r14/first-search` | A clear screen until the first Search, on every site's search page and the hub's: no results area until the visitor asks (a Search, a profile load, or a link that carries a query or a filter), with the bar's "Press Enter or click Search to apply" line meanwhile; two page-life flags, the hold of release 8 unchanged and the gate new | `common/components/{SearchSessionContext,SearchResults,SearchBar}.tsx`, `export/e2e/first-search.spec.ts` (new), `export/e2e/{browse-all,workspace-shell,charts,restore-no-refire,responsive,tag-chips}.spec.ts`, `export/e2e/helpers.ts` (`showAll`), `export/e2e-hub/federated-search.spec.ts`, `export/CHANGELOG.md`, `plans/export-header-first-search.md` |
| CF | `r14/chart-fold` | The homepage's growth chart folds two or more sites each under 5 % of its total into one Other band on top, in its own near-neutral grey (`--chart-other`); the kept sites keep their colours; the legend shows them and Other, the hover titles and the table every site | `homepage/app/lib/growthGaps.ts` + test, `homepage/app/components/ArchiveGrowthChart.tsx`, `common/styles/tokens.css` (`--chart-other`), `homepage/e2e/{fixture-summary.ts,growth-chart.spec.ts,instance-colours.spec.ts,marketing.spec.ts}`, `homepage/CHANGELOG.md`, `plans/FACTS.md` |
**Order:** HP → `r14/two-grounds-headers` → HS (`r14/hidden-sites`) and S1 (`r14/first-search`),
siblings → CF (`r14/chart-fold`), off `main` after HS, with `main` merged in after S1. The shared
files are the three changelogs' `[Unreleased]` sections and this record.
## Record
### Slice HP, as shipped — the homepage shows the social links where they are seen (2026-09-28)
Branch `homepage/social-visible` off `main` `ac438bbc`, worktree `~/Projects/homepage-social-visible`
(block #3: editor 3301, test 3311, homepage e2e 3340, homepage static 3331), one Opus implementer.
Scratch files `hp-*` in the job's `tmp`. The rulings, all 2026-09-28, built in this order on one
branch:
1. The homepage's social links more visible; a change to the social-link schema where it has a
reason.
2. The Changelog link moves to the footer; Base and Accent move behind a single options button with
a gear icon that opens a modal.
3. The gear sits in the social icons' rhythm, with no gap of its own; the narrow header carries the
icons and the gear in its bar.
4. On Official Instances, each site's name uses the bold-lead effect the sites' own headings use,
with a slight tint or underline in the site's accent colour.
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.
10. (After the merge, 2026-09-28.) The chart follows standard practice for light and dark grounds;
the foreground-coloured separator lines are withdrawn.
**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`. 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`.**
- Props `links`, `placement: "header" | "footer"`, `className` (on the `
`, which carries
`data-social-links=""`); nothing site-specific.
- Each link is a 36 px key (`size-9`) around the 20 px glyph, 44 px under a coarse pointer
(`pointer-coarse:size-11`, Tailwind v4's own variant). The glyph is `text-muted-foreground`,
`hover:text-foreground`, with a `hover:bg-muted` key, so a multi-colour icon has a hover state
too. Focus is `focus-visible:ring-2 focus-visible:ring-ring` with
`not-forced-colors:focus-visible:outline-none`: in forced colours a box-shadow is not drawn,
and the browser's own outline is left in place. Nothing is drawn at rest.
- `aria-label` and `title` are the label, `target="_blank" rel="noopener noreferrer"`, and the
link has no text.
- The header placement shows at most four (`headerSocialLinks`, below); the footer every link.
- Every icon passes the save-time check AGAIN before it is inlined (`safeSocialSvg`, below). One
that fails is not injected: the link shows its label as text.
- The ids inside each inlined icon are scoped per row and per link (`scopeSvgIds`, below).
- It uses only `useId`, so it works in a server or a client tree.
- **The header bound, `headerSocialLinks`** (`common/lib/socialLinks.ts`, pure). At most four: the
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 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 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 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
`siteSchema` use. It is still version 5: nothing reads the version to accept a file.
- The homepage's loader keeps a lead only when it is a proper prefix of the title
(`withCheckedLead`), so an older or hand-edited summary shows the plain title.
- `ArchiveCards` sets the title with the shared `Wordmark` (lead 720, suffix 380) at the card
title's size, and tints the lead in the site's own accent: `siteAccentColor`, `siteColor`'s
accent half. A site with a lead and no accent keeps the foreground. `Wordmark` gains
`leadStyle`.
- `headerTitle` is not carried: the card's accessible name is `siteTitle`, and every live site
has the two equal.
- **The social icon's SVG, checked by an allowlist** (`common/lib/socialSvg.ts`, pure;
`settingsSchema.ts` re-exports `normalizeSocialSvg` and `socialSvgProblem`). The allowlist was
chosen over the denylist: the element and attribute lists stay small, and every shape the
existing tests and icons use passes.
- The input is read tag by tag. It must be ONE well-formed `