review-chrome¶
The ONE shared paged-review contract and page chrome both review surfaces render — request-layer slices, GitHub ListView query/section/facet/pagination chrome, structured anchor rows, shared state visuals, and the standalone DetailShell — so
raw source¶
Evals and Issues are GitHub-style list/detail pairs built from ONE component set. The old master-detail copies proved the drift risk. Shared ListView/query/facet/row/state primitives and DetailShell live here; domain-only behavior stays in its page. No empty abstraction or page-local near-copy is allowed.
expanded spec¶
- ONE visible, editable token query is the whole list state (review-query is the engine). The
32px combobox shows the raw text — Issues defaults to
is:issue state:open, Evals tois:eval— and every control is only a query BUILDER over the COMMITTED text: section tabs and low-cardinality facet menus perform token surgery and PUSH, so a pick is always visible as text and no control owns private filter state. The committed text replays as a CONTINUABLE edit: the visible value is the trimmed tokens plus exactly ONE trailing ASCII space with the caret parked after it, so the next keystroke starts the next token; a builder pick, a hand submit, and a Back/Forward replay hand focus to the input, while a cold mount — a page load, or arriving from another page — parks the caret without stealing page focus (the list keys stay live). The trailing space is display-only — submit trims the outer whitespace, and the address never carries it. Syntax highlight is a GitHub-style aria-hidden overlay behind the native input (native caret/selection, never contenteditable), mirroring the value glyph-for-glyph including the trailing space; recognized qualifiers color, unknown ones stay plain and run to the honest filtered zero. An emptied submit refills the default — visibly too, even though the bare address does not change. High-cardinality dimensions (author/filer/node/scope) get NO enumerating dropdown: hand-typed or picked from the input's inline combobox+listbox autocomplete, whose candidates are bounded — values present in the data,scope:only sessions on the current board — a key pick completes in place, a value pick completes the token and executes; unknown or historical values still submit verbatim.scope:<id>is the worktree data source andsession:present|missingthe source-session presence — two axes, never conflated, and a detail address carries only the scope token. The initial/reset page-1 view omitspage; any non-default query starts?q=<raw text>, and pagination appends&page=<n>afterq(or starts?page=<n>for the default query), INCLUDINGpage=1when a pagination anchor returned there. Back restores text, the exact page-address form, results, and the list scroll position level by level. Omitted page and explicitpage=1are intentional action/history states, not a dual-address bug. ListPageis the measured GitHub ListView skeleton. A quiet title/action and the 32px query precede ONE bordered list. Its 48px header has counted section tabs left, invisible facet buttons right, and, whenever at least one REAL low-frequency/width-displaced facet has usable options (or an active value needs its off-switch), ONE semantic secondary Filters trigger — the low-cardinality set only (state, verdict, freshness, evidence, store, source-session presence); this menu never houses commands or non-filter actions. The trigger is the same visual family as a facet button: the icon system's filter/funnel glyph, localized Filters text, and chevron-down — never a kebab, ellipsis, or "More actions" affordance. A stable badge counts ACTIVE filter GROUPS currently housed in that menu (not matching rows): desktop excludes a facet still visible beside the trigger, while 390px includes every displaced group; zero has no badge. Its accessible name includes the same active count. Tab counts are computed under the REST of the query. A section may also carry ONE quieter count SUFFIX beside its number when its domain splits that section (review-filters; Evals' stale remeasurement debt): it is part of the same button — its label, its accessible name, never a second control or a second pill of equal weight. The page supplies it as a full text plus its COMPACT face, and the phone width shows the compact one while the accessible name and tooltip keep the full text — the same condense-the-face rule a direct facet follows. What condenses is wording; a count this chrome was given is never hidden at any width. When no secondary group has real options and none is active, the trigger is absent: no real options means no fake control. An ACTIVE value whose menu option vanished keeps a cheap All off-switch (the visible text is the canonical release). Issues uses the left controls as its exhaustive Open/Closed tablist. Evals uses the SAME slot and rhythm as a named Fail/Pass/Unmeasured pressed-button quick-filter group because the axis is non-exhaustive; no button is pressed on the honest all-verdict default. Menu open focuses the checked/first radio; Arrow/Home/End rove; Escape restores the trigger, while a SELECTION releases like every builder — into the query input, its trigger keeping focus only when the pick changed nothing — and outside click keeps clicked focus. Each secondary facet is its own named radio group inside the menu, never one mixed set with several checked items. Menus use the ONE LIFO Escape stack. The named horizontal tablist exposes one roving tab stop; tabs control one labelled results panel and only Left/Right/Home/End switch it, leaving Up/Down to normal page scrolling — and an arrow activation IS a builder pick, so it too releases focus into the query input (the roving stop is where Tab-return resumes). Every query, section, or facet action PUSHES canonical hash state; Back replays it. At 390px the query input keeps its full width and highlight; displaced facets join the one secondary Filters menu without widening the page. A low-cardinality facet deliberately kept direct may condense its visible face to the selected value while its accessible name stays fully qualified; section, direct-facet, and secondary-filter controls never overlap even when both filters are active. The header there takes only the lines its OWN content needs — one 49px row when everything fits, exactly two contained lines when it does not (a split count pushes Evals over; Issues stays at one) — and its rendered content always fits inside its own box. It never clips a control against that box, drops one, or lets the page scroll sideways: when the width budget runs out the header grows DOWNWARD, the list starts lower, and every control keeps its ≥44px target.- ONE paged-review data contract sits below both pages. A review request carries the committed token
query, positive integer page, and fixed product page size (25, matching the live GitHub Issues
observation). The server applies source selection, query/filter semantics, stable merge/sort, and count
BEFORE slicing, then returns only
{ items, page, perPage, total, sourceTotal, pageCount, prev, next, revision, counts, facets, section }:itemsis at most one page;sourceTotaldistinguishes a vacant source from a filtered-zero view; counts and facet options describe the complete filtered population under their documented rest-of-query rules. The two pages, trunk and scoped Evals, and every issue store consume this same shape. No frontend receives a full collection in order to slice or hide it, and no endpoint wraps a full collection beside a cosmetic page. The revision names the stable source snapshot used for count and slice. Local/aggregate sources define a deterministic merge order and revision; forge adapters push page/per-page/filter/sort to native host pagination when the host can preserve this contract, following the host's pagination links rather than synthesizing an N+1 crawl. A source that cannot provide exact global facets/counts in one request must make that cost explicit at its adapter boundary, never silently fetch every issue per browser request. - Pagination is address state and real navigation. The shared
Paginationprimitive renders one named navigation landmark with real page/Previous/Next anchors,aria-current="page"on the active page, and browser-default Enter/middle-click/copy behavior. Clicking a page PUSHES. Editing/submitting the query or changing an Open/Closed, Fail/Pass/Unmeasured, or facet builder resets to page 1 and PUSHES an address withoutpage; a pagination anchor returning to page 1 mintspage=1. Direct open, refresh, Back, and Forward preserve whichever page-1 form history contains and re-request/replay the full query+page state. Opening a row detail PUSHES; browser Back restores query, page, and the review scrollport offset. The detail URL stays independent of list filters, so the explicit DetailShell back anchor still targets the canonical default list. A positive page beyondpageCountis not clamped or redirected: the address and requested page remain,itemsis empty, and the filtered-empty face renders. Previous continues to page-1 and Next to page+1, matching the measured GitHub Web behavior; an in-range last page disables Next. Invalid/non-positive page input repairs to the reset page-1 form withoutpage. Loading and source failure keep the chrome stable and distinguish themselves from an honest empty page. At 390px the same controls wrap without horizontal overflow; no mobile-only pagination dialect exists. - Matching is review-filters, not page code. The canonical ListViews bridge their ONE parsed token text into that shared Issue/Eval engine and render its data-derived options; work-pane and eval-tab project the same adapters into one extremely compact embedded control with popup-local state. This node owns the presentations and canonical address behavior — never a second parser or a second field predicate.
- Rows use ONE two-level information grammar. Rows arrive as data and remain REAL
<a>anchors, but their content is structured through the shared row primitive: a fixed state-icon box, a wrapping title, secondary identity/author/time metadata, then real right-side facts such as comments, store, evidence kind, or scope. Desktop rows have GitHub's ~64px rhythm; at 390px the same markup grows vertically, moves trailing facts under the title, allows long titles to wrap, and never widens the page.j/kstill move a visual cursor and row-contextEnteropens its href. Inputs/textareas/selects yield no list keys; buttons keep native Enter/Space while allowingj/k; a focused anchor's Enter follows its OWN href, not the cursor. Blind rows stay inert. One shared empty state distinguishes a vacant dataset from a non-empty dataset whose current view matches nothing. - State is one data-driven primitive. The shared mapping owns
icon + label + tonefor eval verdicts (fresh/stale pass/fail and unmeasured/legacy) and issue lifecycle (open vs every concluded state). Evals list leading marks, detail status, and every A/B reading selector consume it; Issues list/detail and the compactIssueCardentries consume the same issue half. In list rows the shared primitive fixes one optical box, rendered size, stroke weight, and alignment for every issue/eval state, so switching domain or state never shifts the row; detail contexts may still request their own explicit size. Glyphs come only from icon-system — no page-local SVG, CSS dot, raw status pill, Unicode check/cross, or Eval-only alignment patch. Small overview surfaces may add counts beside the primitive, but never mint another state mapping. DetailShellfollows GitHub's issue grammar: title/meta HEADER, STATUS band, MAIN content with an optional docked composer, and a metadata SIDE rail. It is the shell for every STANDALONE page in these families, not only the details: Issues' compose page (issues-view) is the same header + main + rail with no status band, so writing an issue and reading one cannot drift into two skeletons. The header row leads with a compact back anchor when the page supplies one (backHref/backLabel): a REAL<a href>wearing the icon-system left-arrow glyph with its localized tooltip + accessible name — Enter follows the href natively, and it never callshistory.back(the page derives the href from the detail's canonical address, address-routing). Browser history remains the finer return path beside it. The header row carries ONE geometry contract: the row and the h1 declare the SAME type-scale tokens (the typography law bansinherit, so the metric is one pair of shared tokens, not one declaration), and the back anchor derives its vertical offset from the row's first-line box, so the anchor's center coincides with the title FIRST line's visual center — at every width and language, and a wrapping title keeps the anchor tied to line one, never re-centered against the whole block. No page or breakpoint may add its own pixel offset, and the anchor keeps its ≥24px hit target and focus ring. The header has no generic trailing action slot: detail-local exits belong in their actual content contract, and scoped Evals deliberately expose only the list-return arrow here (evals-view). Source failure and honest not-found are distinct faces. On desktop the SIDE rail is sticky in its grid column (neverposition:fixed): while a long main column scrolls, the rail pins near the scrollport top so the metadata stays on screen — grid containment keeps it off the header (which scrolls away normally) and the page bottom, and it adds no permanent nested scrollbar: only a rail taller than the viewport scrolls internally, keeping every section reachable. At phone width the SAME themed markup becomes one column with side metadata above main content — NOT sticky, plain document flow.- Side-rail metadata is ONE value primitive. Every rail row's value on BOTH detail pages renders
through the shared
SideValue— a plain span, a REAL anchor, or a button sharing one markup shape and one truncation contract: the text container is min-width:0, single-line, shrinkable, ellipsizing when it exceeds the rail, the full text kept reachable through the shared tooltip/accessible name, so a session UUID or long slug never stretches the rail or the page. Link/button variants keep a pointer, hover underline, and a visible focus ring; identity skins (the originator liveness chip's dot + tint) layer OVER the primitive, never beside it as a parallel span/anchor/tooltip variant. Information type is explicit: an issue's own id wears a localized Issue label, a spec-node reference wears the localized spec-node label and its real focus/navigate behavior — no bare#slugleft to be guessed at. A rail row that takes INPUT — the compose page's store picker (issues-view) — is a control, not a value, and stays outside the primitive; every value the rails SHOW still goes through it. List rows keep their own one compact meta grammar — two densities, each a single implementation. - Both components read only the shared theme/typography tokens (the
styles.cssvars) — the pages contribute content, never layout forks. A change to list rhythm or detail geometry lands HERE once and both pages move together; that is the component boundary this node exists to hold. ListPage and DetailShell consume the ONE page-scroll primitive: they never own overflow, gutter, track inset, or restoration themselves. Optional route-leading content is the scrollport's first child, never a sibling above it. Their sticky header, side rail, and composer pin inside that shared scrollport; Graph canvas, Session terminal, and bounded pane scrollers stay outside it.