Archilyzer · Source

archilyzer

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

commit bc5b8f9713050b4eb9f5974e2852655e31e21d5c
parent edb870090027d8bb9da3f7352281768e37117ce5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 16:11:29 -0400

Merge plans/export-header-first-search — release 14 planned: the export header carries the social row, one Options button, a link to the Archilyzer home in place of the Sites dropdown, a clear screen until the first Search; nothing built

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

Diffstat:
Mplans/STATE.md | 4++++
Aplans/export-header-first-search.md | 291++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 295 insertions(+), 0 deletions(-)

diff --git a/plans/STATE.md b/plans/STATE.md @@ -146,6 +146,10 @@ changed at integration. Nothing was deployed, cut, pushed or restarted; :3001 st 8. Optional: rebuild + restart umtool on :3050. Its app changed only in `lib/tools.mjs` (the 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. +- **Planned, not started (2026-09-28): release 14** — 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 H3, H1, H2, S1, + and S2 as a candidate). Waits on `export/gumroad-tip` merging. - **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 @@ -0,0 +1,291 @@ +# Release 14 — the social icons within reach, and a clear screen until the first Search + +Written 2026-09-28 against `main` `6f03805b`. Self-contained for a fresh session. Rules: +`plans/tools/implementer-rules.md` (one Opus implementer per slice in a worktree, one read-only +Opus review, the parent merges; no cut, no deploy and no restart inside a slice). Record file: +`plans/release-14.md` (NEW, created by the first slice to start, the shape of `release-12.md`). +Facts below were gathered by two read-only passes on 2026-09-28 and carry `file:line` anchors; +anything marked UNVERIFIED is a first step for the implementer, not a fact. + +**Why 14:** release 12 (the source mirror) is merged and release 13 (`release-13.md`) is a parallel +session's plan in flight, with its own slice table, merge order and integration gate. These slices +are not added to it. No `r13/*` branch touches any file this plan owns (checked: `git diff --stat +main...<branch>` is empty for the search and header components on all five). + +## The operator's words + +- 2026-09-25: "What if on the export we didn't load all search items until the user hits search, + preventing the possibility of an unexpected system overload on re-visit" — shipped in part as + release 8 slice E (`00fb567b`): a query restored from localStorage is held, not run. +- 2026-09-28: "the export feature … where the search doesn't fire / show results until the first + time hitting search after load", then: "I was expecting a clear screen (easy to see the footer) + when it's actually the default state of viewing an index of all videos like when you hit search + with no items." +- 2026-09-28: "I want the social icons (particularly gumroad) to be more accessible, I'm thinking + we move changelog link to footer, theme and dark/light behind a single options modal button, and + instead of sites dropdown just link to archilyzer home which lists official instances." +- Standing: **no copy** beside the tip link; the mark is vendored unmodified (`gumroad-tip-link.md`). + +## Decisions and assumptions + +Decided by the operator's words: the four header moves; the clear screen on load. + +ASSUMED by the planner (2026-09-28) — each is one line to reverse, and the operator has been told: + +| # | Question | Assumed | +|---|---|---| +| A1 | A link that carries a query (`qt=`, legacy `q=`) | Still runs on load and shows its results — "a shared link is the visitor asking" (release 8's rule, kept). | +| A2 | Leaving the search page and coming back within one visit | The listing is still there: the gate is once per page life, not once per mount. | +| A3 | Which pages get the clear screen | The single-site search page. The hub only if its home renders the same results component (S1 step 0). | +| A4 | A per-site switch | None. Every export site behaves the same. | +| 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 | Unchanged, except the anchor of H3. Its icons stay in the footer's Elsewhere row. | +| 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 + +``` +export/gumroad-tip (built 1c76cd95, in review) ──► H1 ──► H2 +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 +``` + +H1 then H2 are stacked on one branch (both rewrite `Header.tsx` and `MobileMenu.tsx`). S1 and H3 +run in parallel with them on their own branches. Merge order: gumroad → H3 → H1+H2 → S1. + +## Verified facts the implementer must not re-derive + +**The header** (`export/app/components/Header.tsx`): +- Sticky, `min-h-14` (56 px), one breakpoint at `md` (768 px) (`:68-72`; + `plans/export-responsive-redesign.md:55-62`). +- Wide: brand (`:73-80`), nav `hidden md:flex` from `navLinks` (`:50-56`, `:82-92`), then the right + cluster `hidden md:flex` (`:94-114`): `SiblingSwitcher` (`:96`), Hub backlink (`:97-106`, shown + when `isSite && resolvedHub !== site.siteUrl`, `:35-40`), Changelog (`:107-112`), `ThemeMenu` + (`:113`); then, at EVERY width, `ThemeToggle` (`:117`) and the `MobileMenu` trigger (`:118`). +- `MobileMenu.tsx` is a Radix `Sheet` from the right carrying nav + Offline + Changelog + (`menuLinks`, `Header.tsx:61-65`), the Hub backlink (`MobileMenu.tsx:83-92`), the Sites groups, + and Base/Accent as native radiogroups (`:124-147`) — it deliberately does not nest `ThemeMenu`'s + dropdown ("a focus-trap fight", `:30-35`). +- The Sites dropdown is **related sites, not official instances**: `SiblingSwitcher.tsx:21-62` over + `resolveRelatedSites(site, listSites())` (`common/lib/site.ts:146,203`). The footer renders the + same groups in `<nav aria-label="Related sites">` (`Footer.tsx:116-148`), so removing the + dropdown loses nothing and orphans no config. +- Theme: `ThemeMenu.tsx:28-86` — trigger `aria-label="Choose theme"` (`:34`), `menuitemradio` + items, two groups: Base (System/Light/Sepia/Dark, `themeConfig.ts:49-54`) and Accent (seven named + + the site's own, `siteSchema.ts:116`). `ThemeToggle.tsx:20-39` cycles Base only, + `aria-label="Switch to {next}"`. Keys `ytdlp-tb:base`, `ytdlp-tb:accent`; DOM `data-base`, + `.dark`, `data-accent` (`ThemeProvider.tsx:100-108`). +- The no-flash script (`ThemeScript.tsx`, `themeConfig.ts:197-228`) and `ThemeProvider` are + untouched by where the controls render: every control reads `useTheme()`. +- A full Radix dialog wrapper exists and has **no importer**: `common/components/ui/dialog.tsx`. + `radix-ui ^1.6.0` is already a dependency of `common`. +- Changelog route: `export/app/changelog/page.tsx:15-20`. Linked from the header twice (wide + cluster and `menuLinks`), from nowhere else. + +**The footer** (`export/app/components/Footer.tsx`): row 1 (`:32-75`) eyebrow links Downloads / +Offline / Use with AI (`:66-74`) — where Changelog goes; row 2 (`:76-114`) credit + social `<ul>` +(`:97-113`). Social links come from `resolveSocialLinks(site, getSettings())` (`:22`), inlined SVG +strings sized by `sizeSocialSvg` (`:108`); the Gumroad branch appends its `<li>` last, +unconditionally. + +**Accessibility, measured in code:** social icons are `w-5 h-5` (20 px) with no padding on the +link (`Footer.tsx:107`) — under WCAG 2.5.8's 24 px minimum; the links have no focus ring of their +own; header icon buttons are 32 px (`ThemeMenu.tsx:37`, `ThemeToggle.tsx:31`) and 36 px (the menu +trigger, `ui/button.tsx:28`). + +**The homepage:** Official Instances is `homepage/app/page.tsx:112-122`, a `<section>` with no +`id` (`:114`), rendered only when `sites.length > 0`. `PROJECT_URL` is `common/lib/project.ts:31`. + +**The results area:** +- `export/app/(workspace)/SiteWorkspace.tsx:22-57` mounts the providers, the search bar and + `WorkspaceView` once, above both `/` and `/ask`. `WorkspaceView.tsx:132` renders + `<SearchResults/>`, latched once mounted (`:113-116`). +- `common/components/SearchResults.tsx`: header text branches on `hasActiveQuery` (`:124-137`, + "Matching videos (N)" / "All videos (N)"); view toggle (`:155-177`), Copy for AI (`:178-189`), + selection toolbar (`:195-247`), chart view (`:249-273`), `browse-hint` (`:276-280`), zero states + (`:281-292`), the virtualized list (`:293-305`) whenever `resultGroups.length > 0`. +- `resultGroups` in browse mode lists every video passing the committed filters whenever + `!hasActiveQuery` (`SearchSessionContext.tsx:890-902`) — nothing asks whether the visitor searched. +- `ranThisPageLife` is a MODULE-level `let` (`SearchSessionContext.tsx:206`): set on a URL query at + hydrate (`:1277`), in `commitSearch` unconditionally — an EMPTY commit included (`:1393`) — and + in `applySnapshot` (`:1457`). Read once (`:1266`). It survives client-side navigation and resets + on reload. It is not reactive. +- `searchExecuted` is dead: the getter is discarded (`:463-471`), nothing reads it. +- `commitSearch` (`:1390-1426`) is the one choke point for Enter and the Search button + (`SearchBar.tsx:82-96,118-122`). +- The footer is a sibling of `<main>` in a column (`export/app/layout.tsx:123-138`): the list's + height is what pushes it off screen. +- The listing is client-rendered only (`SearchResults.tsx:1` `"use client"`; 0 rows in + `out/index.html`); the sitemap has four URLs and no per-video pages. The clear screen costs + nothing in static HTML or crawl. +- The editor imports none of these components. +- Specs that load `/` with no query and expect rows at once: `export/e2e/browse-all.spec.ts:32-44`, + `workspace-shell.spec.ts:37-39,57,67`, `charts.spec.ts:64-67,96-99,112-115,140-143`. There is no + shared "go home and wait for the list" helper. Specs that load with `qt=` or press Search first + are unaffected. + +**Specs on the header controls:** `theme-accent.spec.ts` (`:32` the trigger by name, `:45-72` +`menuitemradio`), `theme.spec.ts` (`:64-73` the toggle by `/switch to/i`), `responsive.spec.ts:85-97` +(the Sheet's `radio` "Sepia"), `brand.spec.ts:81` (`onBase`). No spec opens the Sites dropdown; no +spec names the Changelog link. On the Gumroad branch `expectGumroadMarkLast` +(`export/e2e/helpers.ts`) asserts the mark is the last `<li>` of the FOOTER's row. + +UNVERIFIED: whether `HubHome` renders `SearchResults` (`export/app/(workspace)/page.tsx:12-14` +branches to it and bypasses `SiteWorkspace`); Back-button scroll restoration into the virtualized +list; the summaries payload per site (FACTS and the 2026-09-25 session give different figures). + +## Slice H3 — the homepage's instances anchor (branch `r14/home-anchor`) + +Owns `homepage/app/page.tsx`, `homepage/e2e/marketing.spec.ts`, `common/lib/project.ts` (one +constant), `homepage/CHANGELOG.md`. + +1. `id="instances"` on the Official Instances `<section>`, with `scroll-mt` for the sticky header. +2. `INSTANCES_URL = ${PROJECT_URL}/#instances` beside `PROJECT_URL`. +3. e2e: `/#instances` scrolls the heading into view. When the section is absent (no sites) the + link lands on the top of the page; that is accepted and said in the record. +4. Gates: tsc, homepage unit, homepage e2e, `archilyzer build homepage --no-source`. + +## Slice H1 — the header carries the social row (branch `r14/header`, after gumroad merges) + +Owns `export/app/components/{Header,MobileMenu,Footer,SiblingSwitcher}.tsx`, a NEW +`common/components/SocialRow.tsx`, `export/e2e/{helpers.ts,site-branding.spec.ts,brand.spec.ts, +responsive.spec.ts}`, a NEW `export/e2e/header.spec.ts`, `export/e2e-hub/official-instances.spec.ts`, +`export/CHANGELOG.md`, `plans/export-responsive-redesign.md` (the header contract, `:258-260`). + +1. **One component, two places.** `SocialRow` renders the operator's links and then the Gumroad + mark, last. Props: `placement: "header" | "footer"`. The footer uses it as it renders today. +2. **Tap area and focus.** Each link is a 36 px box (`size-9`, the menu trigger's size) around the + 20 px glyph; on coarse pointers 44 px (`pointer-coarse:` literal classes). A + `focus-visible:ring-2 focus-visible:ring-ring` ring. The Gumroad ring and forced-colours outline + move with the mark, unchanged. +3. **The header, wide:** brand · nav · `SocialRow` · the Archilyzer link · the theme controls (H2). + **Narrow:** brand · `SocialRow` · the theme controls · the menu trigger. With three icons at + 36 px the narrow header is about 108 px of icons; if the operator configures more links than fit + at 360 px, the row keeps the LAST three (the mark is last) and the rest stay in the footer. + State the rule in a comment and test it with five links. +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 + Sites groups there. On the hub build the link is omitted (the hub lists the instances itself). +5. **Hub backlink removed** from the header and the slide-out menu (A5). `hubUrl` and + `resolveHubUrl` stay; an old `site.json` with the key still loads. +6. **Changelog** leaves both header places and joins the footer's row-1 links after Use with AI. +7. **Tests.** `expectGumroadMarkLast` takes the row as a parameter; assert it for the header row + and the footer row on a site page and on the hub. New `header.spec.ts`: the header row is + visible without scrolling at 360, 768 and 1280 px; each link's box is ≥ 24 px (and ≥ 44 px under + a coarse pointer emulation); the focus ring shows; the Archilyzer link's `href`; no `Sites` + button; Changelog is in the footer and not in the header; five configured links → three in the + header, five in the footer. `responsive.spec.ts`: the slide-out menu's contents as changed. +8. **No copy.** No text beside any icon; the accessible names are the services' names. +9. Gates: tsc; common tests; `pnpm --filter export exec next build` (site, fixture site, hub); + export e2e — the specs above plus `theme.spec.ts`, `theme-accent.spec.ts`, + `related-sites.spec.ts`; hub e2e `official-instances.spec.ts`; screenshots of the header at + 360 / 768 / 1280 px on Light, Sepia and Dark to `~/reports/release-14/shots/`. + +## Slice H2 — one options button (same branch, stacked on H1) + +Owns `common/components/{ThemeMenu,ThemeToggle}.tsx`, a NEW `common/components/OptionsDialog.tsx`, +`export/app/components/{Header,MobileMenu}.tsx`, `export/e2e/{theme,theme-accent,responsive, +brand}.spec.ts`, `export/CHANGELOG.md`. **Check first who else renders `ThemeMenu`/`ThemeToggle`** +(`homepage/app/components/Header.tsx:62-65` does; the editor may): those keep the old controls. +The new dialog is the export header's only. + +1. One icon button, accessible name **"Options"**, 36 px, visible at every width, where the toggle + is today. It opens `OptionsDialog` on `common/components/ui/dialog.tsx` (its first importer): + title "Options", two native radiogroups — **Base** and **Accent** — the markup `MobileMenu` + already uses (`:124-147`), extracted into one `ThemeRadios` component used by both. +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. A choice applies at once and the dialog stays open; Escape, the close button and a click + outside close it; focus 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`, 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 an axe-style check that the dialog traps focus and restores it, and the + homepage e2e `theme.spec.ts` to prove the homepage's controls did not move. + +## Slice S1 — a clear screen until the first Search (branch `r14/first-search`) + +Owns `common/components/{SearchSessionContext,SearchResults,SearchBar}.tsx`, +`export/app/(workspace)/WorkspaceView.tsx`, `export/e2e/{browse-all,workspace-shell,charts, +restore-no-refire}.spec.ts`, a NEW `export/e2e/first-search.spec.ts`, `export/e2e/helpers.ts` +(one helper), `export/CHANGELOG.md`. + +0. **First, settle the two unknowns** and write them in the record: does `HubHome` render + `SearchResults`; what does the Back button restore into the list. If the hub shares the + component, the behaviour applies there too and `e2e-hub` joins the gate; if not, the hub is + left and said so. +1. **One reactive flag.** `searchedThisPageLife` in the session context, initialised from the + module-level `ranThisPageLife` and set wherever it is set (`:1277`, `:1393`, `:1457`). Delete + the dead `searchExecuted` state. The module variable stays the source of truth across remounts + (A2). +2. **The gate is in the view, not the data.** `SearchResults` renders NOTHING until the flag is + true: no header count, no view toggle, no Copy for AI, no selection toolbar, no chart, no list, + no `browse-hint`. `resultGroups` is still computed (S2 is where the work is saved). The page's + own intro (`export/app/(workspace)/page.tsx:7-37`: the transcript count and New since last + visit) stays. +3. **What the visitor sees:** the search bar, the intro, the footer. The bar's existing line + "Press Enter or click Search to apply" is shown in this state too, so the screen says what to + do in words that already exist. No new copy. +4. **An empty Search** shows "All videos (N)" and the listing, exactly as today. A `qt=` URL runs + and shows on load (A1). A restored query stays held (release 8) — and now shows a clear screen + instead of the browse listing under it. +5. **Filters before the first Search:** changing a filter does not reveal the listing; the hint + line already covers it (`filtersDirty`). +6. **Tests.** New `first-search.spec.ts`: on load no `results-summary`, no `[data-card-header]`, + the footer in the viewport at 1280×800 and 390×844; Enter on an empty box shows "All videos"; + the Search button does too; `/` → `/ask` → `/` keeps the listing; a reload clears it; `qt=` + shows results on load; a restored query shows the clear screen and the filled form. The three + affected specs gain `showAll(page)` (one helper: press the Search button, wait for + `results-summary`) where they relied on the listing at load; `browse-all.spec.ts`'s first test + is rewritten to assert the clear screen and then the listing. +7. Gates: tsc; common tests; `pnpm --filter export exec next build`; the FULL export e2e suite + (the blast radius is a shared component — three specs are known, the suite finds the rest); + hub e2e if step 0 says so; `e2e:2origin` from the worktree only. + +## Slice S2 — CANDIDATE: fetch the summaries on the first Search + +Not scheduled. It is the other half of the 2026-09-25 request ("didn't load all search items +until the user hits search") and needs the operator's word, because it changes `/ask`. + +- `SingleSiteDataProvider` calls `useSummaries("")` on mount (`SearchDataContext.tsx:148-155`), and + `useSummaries` fans out EVERY page eagerly (`summariesCache.ts:19-71`, 1,000 records a page). +- The cheap manifest feeds the Filters channel list (`SearchDataContext.tsx:165-183`) and must stay + eager; the fallback at `:176-183` derives channel names from the pages when the manifest has none. +- `/ask` grounds on the same summaries (`useAskChat.ts:149-151`) under the same shell, so an + `/ask`-first visitor needs them without ever pressing Search. +- Step one of S2 is a measurement, per site, of the bytes and requests a plain visit costs today + and would cost after — the figures on record disagree. + +## Rollout (parent) + +Nothing here deploys inside a slice. After the merges: one export release cut, a rebuild and +deploy of every site and the hub (the header is in every build), and the homepage deployed FIRST +so `#instances` exists before any site links to it. The homepage deploy runs the source publish +step of release 12 — its denylist must be complete (`release-12.md`, "Rollout", step 0). Live +checks: the header row visible at 390 px on one site; the mark's `href`; the Archilyzer link lands +on Official Instances; a plain visit shows the footer without scrolling; a `qt=` link still shows +results. An HTML runbook at `~/reports/release-14/RUNBOOK.html`. + +## Risks + +- **Two taps for the ground** (H2.4). The fallback is named. +- **A first-time visitor sees no videos.** The site's front page becomes a search box over a + count. That is the operator's choice; the hint line is the only instruction. +- **Icons in a 360 px header** compete with the brand's title: a long `headerTitle` wraps + (`min-h-14` allows it). The screenshots at 360 px are the check; the rule of three keeps the row + bounded. +- **A trademark in the header** is more prominent than one in the footer. The mark stays + unmodified, unlabelled and only a link. +- **Specs outside the known three** may lean on the listing at load through a hydration wait; the + full-suite run in S1's gate is how they are found.