Skip to content

Reference page fixes: sticky headings, parameters, tmux links and alignment - #157

Open
tony wants to merge 51 commits into
mainfrom
sticky-member-headings
Open

tony wants to merge 51 commits into
mainfrom
sticky-member-headings

Conversation

@tony

@tony tony commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes from the 2026-10-18 review of the reference pages, on one branch so they share one CI run, one preview and one deploy. #158 to #163 were folded in here; each closed PR keeps its full discussion and evidence. Later fixes from the same review land on this branch too.

Sticky member and group headings

Long member lists scrolled the Members heading and the active group heading out of view, so a reader lost track of which group a member belonged to. Both headings now stay stuck below the site header and section row, and release at the end of the Members section. This was P0 6, held back from #156 until review found the problems below.

What a reader gets

  • The Members bar and the current group bar stay visible while scrolling a long member list.
  • A link or typed hash to a group lands at the start of that group, not its end.
  • Keyboard focus is never fully hidden under the bars (WCAG 2.2 SC 2.4.11). A focused bar returns to its place in the page and stays above the strip that covers the gap.
  • Heading IDs, permalinks, heading levels, print, no-JavaScript and reduced-motion behaviour are unchanged.

Fixed after the first review

  • Focus could sit fully under the bars when tabbing backward through a group.
  • In Firefox, a first click to an earlier group could land at the group's end, and a repeated same-hash click overshot.
  • A typed hash for a group above the viewport landed at the group's end in every engine.
  • A strip of scrolled text showed between the section row and the bars, and the strip's cover then painted over a focused Members bar.

Verification

  • node site/scripts/check-dev.mjs --api-sticky runs focus, link, repeated-link and typed-hash scenarios in Chromium, Firefox and WebKit at 1800 and 390px. Its controls fail in each engine without the focus clearance, without the typed-hash handling, or with an unpositioned Members bar.
  • site/test/api-page-contents.test.ts: 69 tests, including the new cases that fail on the unfixed sources.
  • An independent re-review approved it with nits.

Known gaps

  • Firefox scrolls a focused control only when none of it is visible, so a control can stay partly under a bar. The gate requires "not entirely hidden" there, which is the WCAG AA rule.
  • Without the Navigation API, a typed hash still lands at the group's end; clicks are handled either way.
  • CI installs Chromium only, so the Firefox-only cases run locally.

Keyword-argument literals stay unlinked (was #158)

A prose literal shaped like a keyword argument (name: or name: value) stays plain in HTML and Markdown exports. The Ruby Server reference had linked cancel: to Cancellation#cancel, clients: true to Snapshot#clients and where: to Selection#where. A bare name such as cancel still links. Test: inline-reference-appearance.test.ts, which fails without the rule.

Every parameter listed in every port (was #159)

Python, Swift, Ruby and Lua listed only the parameters a doc comment described, so Ruby Server.start showed no Parameters section. Every port now lists each declared parameter with its type, and each parameter name in the signature links to its entry.

  • Python lists the implementation's parameters, not each @overload stub's.
  • An undocumented leading self (or Python cls, or Lua self) is the receiver and is neither listed nor linked.

tmux CLI Manual and C source reference in the sidebar (was #160)

The tmux sidebar ends with CLI Manual and C source reference for the selected tmux version, and the overview cards use the same names as the documentation picker. Test: documentation-navigation.test.ts.

tmux commands, options, hooks and format variables badged and previewed (was #161)

tmux names in guides, port prose and API doc comments render like API mentions: a kind badge ($ command, # format, o option, h hook), a hover card from the pinned tmux manual, and a link to the manual entry. tmuxEntityHref(kind, name, version) in tmux-links.ts resolves all four kinds.

  • On the Rust concepts page, list-clients, session_attached and destroy-unattached now carry badges.
  • session_name still links to the Rust API field, because library symbols outrank tmux names.
  • Markdown exports keep plain links.

Tests: tmux-mentions.test.ts (42) and link-preview.test.ts. Screenshots are in #161; delete the tmux-mention-badges-screenshots branch after review.

Declaration controls level with the first signature line (was #162)

The badges, [source] and permalink beside a declaration are now level with the centre of the signature's first line, also when the signature wraps. On TS Server.sessions they sat 4.5px high, because the formatted signature's padding was counted. The fix is in site/src/styles/api.css; the vendored gp-sphinx CSS is untouched.

Gate: checkDeclarationControlAlignment in check-native-layout.mjs, run by check-dev.mjs --reference-layout. It measures 48 headers across TS, Go, Python, Julia, C#, Kotlin, Scala and the Rust MCP page at 1440, 768, 500 and 390px. It fails on main (-4.50px) and passes here (worst 0.70px, on MCP type headings).

Table row permalinks trail the cell text (was #163)

A prose table row's now sits right after the first cell's last text line, hidden until the row is hovered, focused or targeted. Markdown table cells share a 0.75rem inline inset, so the first column no longer touches the table edge.

Gate: checkRowPermalinks in check-dev-sample.mjs checks the Rust, tmux and Ruby CLI tables in light and dark at 1440, 768 and 390px. It fails on main ("permalink starts 39.8px after its text") and passes here.

Type parameters documented and linked

Model in Selection.where(criteria: WhereOf<Model>): Selection<Model> was plain text, because the TypeScript extractor never recorded the type parameters of interfaces, classes or aliases. Extractors now record them, and one linker binds every occurrence to the declaring page's new Type parameters section. Member pages say which declaration owns the parameter, and Markdown exports carry the section.

  • Recorded: names, bounds, defaults and docs for TS, Rust, Go, Java and C#; Kotlin and Scala add variance. Python covers PEP 695 lists, Generic[...] bases and TypeVar bounds. C++ covers templates, concepts and \tparam. Julia and Ruby are read from their recorded parameters.
  • Gaps: the Swift extractor is updated but the Swift model is not regenerated, because swift build fails on this host (missing libxml2.so.2). F# models carry no generics, so 'T stays unlinked.
  • Data: regenerating the older committed models also reorders some keys in Rust and Java, and picks up one C# doc comment and one Java record accessor. Only the type-parameter fields are merged into cxx.json, because the local Doxygen differs from the one that produced it.
  • Tests: 27 new per-port tests fail on main. check-dev.mjs --type-parameters follows the links to the declaring anchor on ts, py, rs, go and java, and fails on the old ts.json.

tmux key sequences render as keyboard keys

The tmux manual and guides now render key sequences such as C-b c as <kbd> caps (<kbd><kbd>C-b</kbd> <kbd>c</kbd></kbd>) instead of code.

  • What counts as a key: detection follows tmux's own grammar in key-string.c (site/src/lib/tmux-keys.ts). Modifiers C-, M- and S-, ^X, and the key names the manual spells (Enter, Escape, BSpace, F1–F12, the keypad and so on) count.
  • Where: modified keys convert anywhere. Bare key names convert only in key contexts: key tables, the default-bindings list, the bind-key/send-keys descriptions, or "press"/"the ‘c’ key". Format strings, option values, command names and paths stay code.
  • Manual data: converted at render time, so the committed manual data is unchanged. Markdown twins and exports keep backticks.
  • Counts: 329 key literals in the 3.2a manual and 258 in 3.7c.
  • Tests: tmux-keys.test.ts (95 cases) and the manual and plugin tests. check-dev.mjs --tmux-keys asserts the bind-key page's DOM.

Kind badges: no dotted underline, centred, one gap; cards are not previews

  • Underline: a decoration on an anchor reaches every in-flow child, so a leading badge inherited the preview's dotted underline (F# concepts M/C). The name now draws the underline and the badge draws none. Non-preview badge links draw their solid underline on the name only.
  • Centring and gap: badges centre on the text line (align-self: center) with a .25em gap everywhere. The gap had been anywhere from .2em to .67em, and badges sat 1.5px to 8px low in body text, headings and tables. Nav and sidebar rows were already centred and keep their 4px gap.
  • Cards: the Python workspace "Runtime support" card links into reference/, so the preview script marked it and painted a dotted underline across the whole card on hover. Links that wrap blocks, and .doc-card, are no longer preview candidates.
  • Gates:
    • checkKindBadgeLinks asserts one dotted underline on the name, the badge centre within 1px and a gap of about .25em.
    • checkBlockLinksUndecorated hovers every block link on the workspace, port and MCP overview pages, with a prose page as the inline control.
    • Both fail on main. The unit tests inline-reference-appearance, link-preview-paint and mcp-reference also fail on the old source.

Member bodies: padding alone sets the inset

On the Go Server.ConfigFile page a member body had 20px above its first line and 12px below its last. Elsewhere it was 12px above and 20px below. The cause was a top or bottom margin on the first or last child (a paragraph, a field list or an example) added to the body's 12px padding. One rule in site/src/styles/api.css replaces the earlier narrow fixes: edge children of dl.py > dd, down to three levels, lose their outer margin. A field list that opens the body also loses its divider and top padding. Spacing between siblings is unchanged.

Gate: checkMemberBodyInsets (check-dev.mjs --member-bodies) measures 17 pages across Go, Julia, Ruby, Python, TS, Swift, C#, Java, PowerShell and MCP at 1440 and 390px. It requires the top inset, bottom inset and padding to agree within 1px. Before the fix it failed on 12 pages (76 body lines); now it passes on 54 bodies.

Workspace commands, options and keys link to their reference

On the Python workspace topics page, tmuxp load, tmuxp freeze and shell_command_before were plain code. Workspace names never reached a resolver, and where one did link it often went to the wrong target: freeze to a library function, search to the Python standard library, session_name to a tmux format.

  • scripts/gen-workspace-entities.mjs generates site/src/data/workspace-entities.json from each port's CLI Manual commands and option and key tables. The anchors are shared with the rendered rows. --check runs in the test loops.
  • <program> <command> (e.g. tmuxp load, tmux-workspace load) links on any page of a port that ships that program. A bare command, option or key links on user-facing workspace pages, and the page's own port and product win over library, standard-library and tmux names. Internals and API-index pages keep their library links.
  • Commands get the $ badge, options o, and keys a new : badge. MCP tool and prompt names inside longer code runs now link without a badge. Markdown exports keep plain links.
  • Across nine ports, unlinked literals went from 178 to 0. workspace-mentions.test.ts walks every workspace and MCP page through the real pipeline against an independent oracle, and fails on the reported page without the linker.
  • Known gap: ports other than Python document only session-level keys in a table, so window- and pane-level keys link to the session row.

MCP tool pages: rendered descriptions and API-style arguments

On the Python create_session page, the environment argument showed a raw :func:libtmux_mcp._utils._coerce_dict_arg. -eKEY=VALUE was unstyled, show-environment was not linked, and the Arguments list looked unlike the API pages.

  • Descriptions run through the shared prose pipeline.
    • Sphinx roles become a link when the symbol has a page, and plain code otherwise. The private _coerce_dict_arg has no page.
    • Inline literals get the code style.
    • .. warning:: renders as an aside.
    • tmux commands, options, hooks and formats link with badges.
    • A symbols: false flag stops an argument named like a library attribute from linking to it.
  • Arguments use the API parameter-list style: name? = default (type) #.
    • Nullable unions fold into the type line instead of separate Variant blocks, and Record<string, T> shows as a map.
    • A closed object reads "No other arguments are accepted."
    • All 79 element ids on create_session are unchanged, and folded variant anchors stay as aliases.
  • Counts across ten ports:
    • Sphinx roles: 36 to 0.
    • Unlinked tmux names: 77 to 0.
    • Unstyled literals: 618 to 0.
  • Tests: mcp-descriptions.outer.test.ts sweeps every port's MCP descriptions and fails on main's rendering, with clean controls.

tmux examples build up to their scripts

The native tmux example pages opened with the complete script. Each page now starts with a short concept paragraph and works through numbered steps, one tmux command or idea at a time, each with a small block and links to the manual. "The complete script" comes last and is tested unchanged.

  • Step counts: attach and send keys has 11, capture pane output has 8, and workspace from file has 8.
  • Step blocks are anchored excerpts of the tested script. A fence names excerpt="<title>" or file="<path>" plus from and to text anchors. The build fails if the from anchor matches no line or more than one, or if to is missing or above from. Markdown exports resolve the same excerpts. 29 tests cover this, including mutation checks on each guard.
  • Not yet: the check that fails on a hand-typed block no test covers, and the Go pilot.

Indentation kept in Javadoc examples

The Java TmuxEnvironment example rendered flush left. The Javadoc extractor stripped the * margin and every space after it, so the model stored try (…) {\nSession mine = …. It now strips only the margin, so relative indentation inside <pre>{@code} survives, and the Java model is regenerated.

  • Six Java symbols had flattened examples. Only their examples[].code changed in the regenerated model.
  • The Doxygen extractor had the same bug: it collapsed a \\code listing onto one line. No committed C++ comment uses \\code yet.
  • scripts/check-example-indentation.mjs compares each comment-derived example in every committed model against its source, and runs in test-all.sh. A negative script plants the Java shape.

tmux options, hooks and formats catalog

/tmux/<version>/catalog/ adds options/, hooks/ and formats/ indexes and a page per entry, in the C reference's layout, with "Used by" lists and hover cards. tmux 3.2a has 104 options, 61 hooks and 200 formats; 3.7c has 153, 68 and 258.

  • tmuxEntityHref now sends options, hooks and formats to catalog pages. Commands stay in the CLI Manual.
  • Across guides and API docs there are 1,791 option, hook and format mentions. 1,677 now reach a catalog page, 18 reach a library symbol, and 96 stay plain: keyword arguments, parameter names and doctest output.
  • scripts/gen-tmux-catalog.mjs --check and its negative script run in the test loops. A crawl of 1,335 catalog pages found no broken links.

Verification of the combined branch

  • The 13 test files these changes touch pass together on the combined tree: 446 tests.
  • pnpm run lint exits 0. The two remaining warnings are in files this branch does not change.
  • Each folded PR's own browser gates passed on its branch, as recorded in that PR. CI on this branch runs them together.

tony added 4 commits October 18, 2026 01:20
why: Long member lists scroll the Members heading and the active
subgroup heading out of view, so a reader loses which group a member
belongs to. The section row already clears the site header; the member
headings need the same measured clearance without moving their IDs,
permalinks or heading levels.

what:
- Wrap an owner's Members or Fields section in `.api-members` and stack
  its real h2 and group h3 below the measured header and section row.
  Both release at the section end, before inherited and related content.
- Derive offsets, scroll margins and activation from the measured
  heading heights, so wrapped and enlarged text keep clearance. Stay in
  flow when the viewport cannot keep a reading area beside the bars.
- Return a link request for a stuck owner or group heading to its source
  boundary, including a repeated hash. The anchor handler and the
  browser keep startup arrival, history traversal, typed hashes,
  modified clicks, new tabs and cross-page links.
- Keep the bars in flow until the page is revealed, so the anchor
  handler's startup scroll is not skewed by a sticky heading in Firefox.
- Return a keyboard-focused heading to its source box, but keep it
  sticky under a pointer press so the permalink click still lands.
- Tint the opaque bar for the arrival cue instead of replacing its
  surface, including under reduced motion.
- Confine sticky rules to screen media; leave scalar, single-group and
  empty owners without added headings.
- Cover grouped, single, scalar and empty owners, release, wrapped
  headings, link requests, history, startup, the focus and arrival
  styles and the pending state in the api-page-contents tests.
why: A sticky box is aligned where it is stuck, not where its group
starts. A link or typed hash for a group above the viewport landed at
the end of that group in every engine, and Firefox did the same for a
first link click when a frame rendered before the URL changed. The
scroll padding counted only the header and section row, so Shift+Tab
left focused links entirely under the Members and group bars. A strip
of scrolled text also showed between the section row and the bars.

what:
- Hold the targeted Members or group heading in flow while a link click
  or a same-document fragment navigation scrolls, so the browser and
  the anchor handler align its source box. The Navigation API
  `navigate` event starts the hold before the browser scrolls a typed
  or scripted hash; a click in the layout starts it in browsers
  without that API. Animation frames end the hold, and every
  navigation signal restarts it.
- Give focusable controls in the owner section a scroll margin equal to
  the measured bars above them: the Members bar, plus the control's own
  group bar. A heading permalink takes only the bars above its heading.
- Cover the gap between the section row and the bars with the page
  background, using a sticky pseudo-element that adds no height.
- Test the hold, its restart and release, the Navigation API entry
  point, the focus clearance and the cover in api-page-contents.test.ts.

Verified: with the Swift Server page at 1800x1000, backward Shift+Tab
left 2 of 14 stops under a bar in Chromium and Firefox and 7 in WebKit;
none are now. A typed hash for the first group landed at scrollY 3609,
3723 and 3608 (Chromium, Firefox, WebKit), with the group box ending at
207, 94 and 208; it lands at 2677, 2678 and 2678 with the group
starting at the reading line. A rail click to the first group from a
later one ended at the group end in Firefox in 3 of 24 runs, and in 18
of 24 on a loaded machine; it ended there in 0 of 24 runs, twice, now.
why: The api-page-contents tests mock geometry in a headless DOM, so
none of the focus, landing or typed hash failures of the sticky bars
could fail them, and each engine aligns a sticky heading differently.

what:
- Add checkApiStickyHeadings. On the Swift Server page, at desktop and
  phone widths, it tabs backward through a stuck group, clicks a link
  to an earlier group once normally and once with the main thread busy,
  clicks a heading's link again from mid-group and from the group end,
  and types a hash for a group above the viewport. Each arrival must
  sit at the reading line, at the start of its group, uncovered, and
  reached by one scroll that never paints the group end.
- Add checkApiStickyHeadingsFail. It removes the focus clearance and
  the Navigation API from a page and requires the check to reject, so
  the check cannot pass without them.
- Run both in the default browser matrix, and in Chromium, Firefox and
  WebKit with `check-dev.mjs --api-sticky`. Document the mode in
  CONTRIBUTING.md.

Verified: against the sticky commit alone, check-dev.mjs --api-sticky
fails in Chromium and Firefox at "backward tab stop 11 is not entirely
hidden by the sticky bars" and in WebKit at "backward tab stop 9 starts
below the sticky bars". Run per scenario, a typed hash fails in all
three engines. In Firefox a link on a busy page lands with the heading
at 48 under the header (scrollY 3723), and a repeated link overshoots
to 3915 instead of 3687. With the fix the command passes in all three
engines, and the scenarios passed in three consecutive runs each.
why: Keyboard focus, the arrival hold and pending fonts put a member bar
back in flow with position: static. The sticky cover that closes the
gap under the section row is positioned, so it painted over the top of
the static Members bar, its permalink and its focus outline.

what:
- Hold in-flow bars with position: relative and top: auto, so their
  z-index keeps them above the cover
- Check in real browsers that a focused Members permalink is not covered,
  with a control that fails when the bar drops to static
@tony
tony deployed to docs-preview October 18, 2026 08:03 — with GitHub Actions Active
tony added 6 commits October 18, 2026 06:23
why: Prose code literals link every name that resolves to one member.
A keyword argument names a parameter of the call being described, so
Ruby's `cancel:` linked to Cancellation#cancel, `clients: true` to
Snapshot#clients, and `action:` to an unrelated Workspace member.

what:
- Add SymbolIndex.linkLiteral, which leaves `name:` and `name: value`
  plain and links other literals as before
- Use it for HTML prose, and skip keyword arguments in Markdown export
- Test that keyword arguments stay plain while the bare name still links
why: Python, Swift, Ruby and Lua listed only the parameters a doc comment
described, so a Ruby Server.start page showed no Parameters section for
executable:, config:, timeout: and cancel:, while C# listed its
parameters with their types whether documented or not.

what:
- List each declared parameter with its type in every port
- Link every parameter name in the signature to its entry
- For Python, list the implementation's parameters, not each @overload
  stub's, and leave out an undocumented self or cls receiver; leave out
  Lua's self receiver too
- Update the tests that pinned documented-only lists
why: The documentation picker named the tmux CLI Manual and C source
reference, but the tmux sidebar listed only Guides, Concepts, Examples
and Topics, and the overview cards called them Manual and Reference.

what:
- Add CLI Manual and C source reference links to the tmux sidebar,
  after its sections and before the notices
- Title the overview cards CLI Manual and C source reference
- Test the sidebar links for a pinned tmux version
…s, formats

why: A tmux command such as `list-clients` rendered as a plain link,
with no kind badge or hover card, while the API names beside it carried
both. `session_attached` and `destroy-unattached`, which the tmux manual
documents, were not linked at all.

what:
- Read options, hooks and format variables from the pinned manual per
  release, anchored on the same headings the full manual page renders
- Resolve every command, option, hook and format-variable link through
  tmuxEntityHref, so the destination of every kind changes in one place
- Link option and hook names written in code, and format variables that
  are a whole code span or sit inside a #{...} expression; names the
  release does not document, URI templates and calls stay plain
- Keep library symbols ahead of options, hooks and format variables in
  HTML, prose Markdown and symbol Markdown, so `pane_id` still reaches a
  Rust field where the page documents one
- Render each lone mention with the API kind badge (command $, option o,
  hook h, format #) and add the four kinds to the badge palette in the
  light, system dark and explicit dark themes; several names in one code
  run stay links without badges, and Markdown keeps plain links
- Serve preview cards for command pages and for each manual entry by
  anchor, with an "Open manual" action, and make manual links eligible
  for hover previews outside the command index and heading permalinks
- Load the highlighter only when the manual is highlighted, so link
  resolution no longer imports it
- Test the three mention kinds, the hover-card data and unknown names,
  and accept manual entry anchors in the native prose destination check
why: The badges, [source] link and permalink beside a declaration were
top-aligned with its code. A formatted native signature pads its code by
0.25rem, so on a one-line declaration such as TypeScript's
Server.sessions the controls sat 4.5px above the line's center. Badges
aligned by their own baseline sat 0.5px high on every other header, and
a heading with a taller line box drifted by more.

what:
- Align the row's first baselines for native headers at every width and
  for Python and MCP headers above the 36rem container breakpoint, so
  controls follow line one of a wrapped signature and ignore the code's
  padding; center the badge slot on the line the text controls share
- Leave the vendored column layout, header height and wrapping as they
  were; a control group on a row of its own is not moved
- Add checkDeclarationControlAlignment to check-native-layout.mjs. It
  loads TypeScript, Go, Python, Julia, C#, Kotlin, Scala and MCP pages at
  1440, 768, 500 and 390px and requires every control beside the code to
  sit within 1px of the center of the first signature line, and
  controls that wrap onto their own rows to be level within each row.
  check-dev.mjs runs it with the signature layouts and the sample loop
  checks the API probe header
- Replace the top-edge assertion in checkNativeSignatureGeometry with a
  center assertion, and replay the observed hosted Scala geometry as the
  rejected case

Largest control offset from the first line's center, origin/main then
this change, measured in Chromium against the dev server:
- TypeScript, Go, Julia and C# formatted methods: 4.50px, then 0.00px at
  1440 and 768px; TypeScript client.toString at 390px: 4.00px, then 0.00px
- Python method and class, Kotlin type: 0.50px, then 0.00px
- MCP tool header: 0.50px, then 0.00px; its type definition headings,
  whose 600-weight face makes a 25px line: 1.80px, then 0.70px
- Header heights, code boxes and the stacked layouts are unchanged
why: A prose table row's permalink sat at its first cell's right edge,
far from the label it links, and appeared inconsistently with the
section permalink. Typography also zeroes the outer padding of the
first and last columns, so labels touched the table edge and the row's
hover background.

what:
- Keep the row permalink at the end of its first cell's last line, out
  of flow so it neither widens the column nor wraps onto its own line
- Hide it until the row is hovered, focused or targeted, with the
  section permalink's theme colors
- Give markdown table cells a 0.75rem inline inset, first and last
  columns included; component tables keep their own padding
- Add checkRowPermalinks to check-dev-sample.mjs and run it from the
  sample and full check-dev flows. Rust, tmux and Ruby tables at 1440,
  768 and 390 px in light and dark must put the permalink within 0.5em
  of the last text line, share one inset across columns, stay quiet at
  rest, and appear legibly on hover, focus and target
tony added 12 commits October 18, 2026 06:34
why: `Selection.where(criteria: WhereOf<Model>): Selection<Model>` showed
`Model` as plain text. The TypeScript extractor never recorded the
parameters of an interface, class or alias, so the linker had nothing to
bind, and every port that did record them left the name unlinked or
resolved it against a global of the same name.

what:
- Add `TypeParameter` (name, modifier, kind, bound, default, doc) and
  `typeParameters` on symbols and signatures. `typeParametersOf` returns
  the stored list or reads `typeParams` apart per language, so models
  extracted earlier keep working.
- Record container and alias parameters in the TypeScript, Rust, Go,
  Java and C# specs, fold Rust and C# `where` bounds into the parameter
  they restrict, and read `@typeParam`, `@template`, `@tparam`, Javadoc
  `@param <T>` and C# `<typeparam>` onto the parameter.
- Extract parameters for Kotlin and Scala (bounds, variance, `where`),
  Python (PEP 695 lists, `Generic[...]` bases, module TypeVars and the
  variables a function binds), C++ (class and member template lists,
  `\tparam`) and Swift (generic parameters and constraints from the
  symbol graph).
- Bind names in one place, `SymbolIndex.typeParameterBindings`: the
  signature, then the enclosing declarations, honouring Java's static
  boundary, Rust impl self types and Go receivers by position. Signature
  and prose links share it, so a bound name links to the anchor
  `<owner>.type-parameter.<name>` on its declaring page.
- `<typeparamref>` becomes a `typeparam` reference that never falls back
  to a global lookup.
- Guard against the C# grammar reading a base type's arguments as a
  parameter list after a parse error.
- `inheritedTypeParameters` lists the enclosing parameters a member's
  types use, from the same links.
why: A reader of `Selection.where` could not tell what `Model` was. Type
parameters were neither documented nor linked, in any port.

what:
- List a symbol's own parameters under "Type parameters", each with a
  stable anchor, its bound or constraint, default and documentation. A
  member page lists the enclosing parameters its signature uses, linked
  to the owner's anchor and naming the declaring type.
- Link each occurrence in native, formatted-layout and unformatted
  signatures, in bounds, and in prose code literals. Layout ranges that
  keep a bound name out of the global lookup now link it to its
  declaration.
- Add the section to the on-page contents and to the Markdown export,
  built from the same bindings as the HTML.
- Give type-parameter links `api-type-parameter-name` and a title naming
  the declaring type, so parameter-name checks keep counting parameters.
- Add `check-dev.mjs --type-parameters`: it follows the links on member
  pages in five ports to the owner's anchor, and compares the Markdown
  twin. The default browser run includes it.
- Update the tests that expected a bound name to be unlinked.
why: The extractors now record type parameters, and the page tests that
compare a fresh extraction with the committed model require the two to
agree.

what:
- Regenerate ts, rs, go, csharp, java, kotlin, scala and py from the
  revisions each model already records, with `gen-api-model.mjs`. The
  existing models were produced by older extractor versions, so a few
  unrelated fields move: signature key order in Rust and Java, one C#
  doc comment and one Java record accessor.
- Merge only the type-parameter fields into cxx.json: the local Doxygen
  is 1.14 and rewrites inline code in other doc comments.
- Leave swift.json: `swift build` does not run here (missing
  libxml2.so.2). Ruby, Julia, Lua, PowerShell and F# come from native
  artifacts or sources that were not rebuilt; Ruby and Julia are read
  from their recorded `typeParams`.
why: The manual page for bind-key showed `C-b c` and `c` as code, the
same as `new-window` or a flag. A reader could not tell the key to press
from the command it runs, and the default binding tables listed keys as
bold text.

what:
- Add tmux-keys.ts with the key grammar from tmux's key-string.c and a
  policy for how much context a literal needs: modified keys qualify on
  their spelling, key names in the manual, and every key in a key table,
  the default binding list or the bind-key, unbind-key, send-keys,
  send-prefix and list-keys descriptions
- Add markTmuxManualKeys, which turns those literals into kbd, nests one
  kbd per key inside a kbd for a sequence such as C-b c, keeps permalink
  ids, and drops mandoc's quotation marks around a cap
- Leave format strings, option values, command names and bare letters
  elsewhere in the manual as code
- Draw a key cap in the prose theme from the surface and edge tokens, so
  light and dark share one rule; the layer is utilities because the
  typography plugin's own kbd rule sits there
- Test the grammar and the contexts, the bind-key sequence, the key
  tables of both pinned versions, command pages against the full manual,
  and the plain-text Markdown export
why: Guides name keys such as C-c, Enter and Up in inline code, and the
attaching guide spelled the detach binding as bold Ctrl-b and d. They
read as code beside the manual's key caps.

what:
- Add a rehype plugin, before the API linker, that marks inline code as
  kbd when tmux-keys.ts says it names a key: modified notation, a list
  that holds one, or a name beside "key" or "press"
- Keep the word Enter typed into a pane as code
- Write the detach binding as `C-b d` with the keys spelled out
- Document the convention in WRITING.md; Markdown twins keep backticks
- Test the plugin on fragments and on the key mentions of the real
  guides, topics and port pages
why: Unit tests read the transformed HTML. They do not show that the
rendered bind-key page nests the caps, keeps new-window a link, and draws
a legible cap in both colour schemes.

what:
- Add check-tmux-keys.mjs: bind-key renders C-b c as a kbd around two
  kbd, no key stays code, the command links remain; caps have an outline
  and raised edge, use the code face, fit 1440 and 390 px and reach 4.5:1
  contrast in light and dark
- Read the default bindings, the KEY BINDINGS modifiers and two guides,
  including the word Enter that stays code
- Run it in the sample and the full browser matrix, with its pages
  compiled during setup, and alone with `check-dev.mjs --tmux-keys`
why: A preview link that leads with a kind badge ([M], [C]) drew its
dotted underline under the badge letter as well as the name. A text
decoration on an ancestor reaches every in-flow descendant, and the
badge cannot cancel it, so the line has to sit on the name instead.

what:
- Put the dotted line on the link's name (.api-symbol-link__name, or
  the code name in "In other ports") and leave the anchor undecorated
  when a badge leads it
- Drop the prose underline from badge links that are not previews, and
  let their name draw it
- Wrap MCP description link names in .api-symbol-link__name, as prose
  links already do
- Check in a real browser that each badge's rendered box matches the
  same page drawn without decorations, at rest and hovered, and that the
  name has exactly one dotted underline; cover prose, headings, tables,
  MCP, API equivalents, related lists, sidebar, tree and search results
- Extend the happy-dom decoration test to the badge link shapes
why: A badge aligned to its name's baseline, so its centre sat below the
text centre and the gap to the name varied by context. Measured in
Chromium before the change: the centre was 3px low beside body text and
a code chip, 8px low in an h2, 5.5px in an h3, 1.5px in a table, 2.5px
in related cards and 1.3px in search results. The gap was .3em in prose
(4.8px to the chip, 10.8px to its text), .35em in MCP cards, .37em in
API equivalents, .6em in related cards, .67em in related contents and
.2em in search results.

what:
- Centre the badge on the link's name with align-self while the name
  keeps the baseline, so the link's height and its text do not move
- Set one .25em gap between a badge and the name in prose, MCP, related,
  equivalent and search links; navigation and sidebar rows already
  centre and keep their 4px
- Lay out search result names as a flex row so the badge and name share
  the same gap
- Move the related-card summary indent to the new badge and gap width
- Assert in the browser check that each badge centre is within 1px of its
  text centre, the gap is about .25em, and a one-line link stays within
  its line height
why: Hovering the "Runtime support" card on the workspace overview drew
the preview's dotted underline under its title and body. The card links
to reference/compatibility/, which the preview policy accepts as a
reference page, so the whole block link was marked and
a[data-link-preview] painted the line through its heading and paragraph.
Cards that link to guides were not matched, which is why only this one
showed it. A card already summarizes its destination, and only an inline
text run carries the dotted line.

what:
- Make a link that wraps blocks, or a .doc-card, ineligible for a
  preview, and remove a mark the parser added before its blocks arrived
- Check in a real browser, after hovering every block link on the
  workspace, port and MCP overview pages, that each dotted link line
  sits on an inline link without block children, with a prose page as
  the control that inline preview links keep theirs
- Cover the card and block-wrapping link shapes in the preview policy
  and decoration tests
- Test the badge check's dotted line with endsWith
why: A member body (`dl.py > dd`) is padded .75rem, but its first and
last children carried their own margins on top of that. A body holding
only "In other ports" opened 20px below its header and closed 12px
above its border; a trailing description paragraph or example did the
same at the bottom, and a leading parameter list added a divider and
padding before its label. Each case was patched on its own (the
Methods heading, then the Description and Examples headings), so the
next shape of body repeated the bug.

what:
- Trim the top margin of a body's first child, and the bottom margin of
  its last, through up to three wrappers, in one rule set in api.css
- Drop the divider and padding of a field list that opens the body
- Drop the bottom margin of the last parameter heading, which sits in
  a grid item the chain cannot reach
- Remove the narrower rules this replaces from global.css and api.css
- Add checkMemberBodyInsets to check-native-layout.mjs: it opens
  member bodies on Go, Julia, Ruby, Python, TypeScript, Swift, C#,
  Java, PowerShell and MCP pages at 1440 and 390px, finds the first
  and last painted content, and requires both insets to match the
  padding and each other within 1px
- Run it from check-dev.mjs, alone with --member-bodies
- Rewrite declaration-body-spacing.test.ts around the new rules,
  including the sibling spacing they must leave alone
…es in code

why: On /py/latest/workspace/topics/, `tmuxp load`, `tmuxp freeze` and
`shell_command_before` rendered as plain code, although the CLI Manual
and the Configuration reference have an entry for each. The prose
linker only knew library symbols, MCP names on MCP pages and tmux
names, so workspace names never reached a resolver. Where a workspace
name was linked, it went to the wrong place: `freeze` and `search` led
to a library function and the Python standard library, `session_name`
to the tmux format variable, and Ruby's `load` to its API.

what:
- Index each port's CLI Manual commands, option rows and configuration
  key rows from the workspace pages (scripts/gen-workspace-entities.mjs
  into site/src/data/workspace-entities.json), anchored on the rows the
  rendered tables serve; createRowAnchors now numbers rows for the page
  and the indexer alike
- Resolve `<program> <command>` on any page of the port that ships the
  program, and a bare command, option or key on a user-facing workspace
  page, through the mention pipeline that badges tmux names: the same
  badge markup, inline links without a badge inside a longer code run,
  plain links in Markdown exports
- Let the page's own product and port win: a workspace name outranks a
  library symbol, a standard-library name and a tmux name, while
  Internals pages and the language API index keep their library symbols
- Leave a heading, the first cell of the row that defines a name, and a
  command's own page plain rather than sending them elsewhere
- Prefer a key's entry on the page it is written on, then the first in
  reference order; a name that is both a command and a key follows its
  section, or its quotes
- Link tools and prompts named inside a longer MCP code run, and resource
  URIs on workspace pages, which a library symbol never shares
- Add a key badge (colon mark, property palette) beside the tmux kinds
- Share the linked-version rule between the prose linker and the new
  links as linkedVersionFor
- Check the index in the medium loop and the publication audit, and
  walk every workspace and MCP page of every port in a test that fails
  on any code literal naming a command, option, key, tool or resource
  that renders plain; its negative case renders the maintainer's page
  without linking and names the three reported literals

Measured on the original pipeline: 178 such literals rendered plain
across the nine ports with workspace pages, and none do now.
…iptions

why: The Python server publishes its docstrings verbatim, so
create_session printed `:func:` role text, -eKEY=VALUE and
show-environment rendered as unstyled code, and show-environment had no
link. The description renderer only knew Markdown and MCP tool names,
so every port left its tmux commands, options, hooks and format
variables unlinked, and meta descriptions carried raw backticks.

what:
- Rewrite roles and directives before Markdown renders them: a role
  becomes a link to the API reference page or MCP tool page that
  documents its target, or clean code when none exists (a private
  helper has no page and no extracted source line); :doc: links the
  upstream docs site; a warning directive becomes the site aside
- Run descriptions through the prose mention pipeline so tmux commands,
  options, hooks and format variables link through tmuxEntityHref with
  the same badges and previews as prose pages, and drop its links to the
  object being described
- Add a symbols opt-out to that pipeline: an argument named
  logical_name is not evidence for the library attribute of that name
- Give inline literals the API literal chip instead of bare code
- Use plain-text summaries for meta descriptions, search and previews
- Move the admonition kind table next to the aside it feeds
- Sweep every port's tool, prompt and resource descriptions and schema
  prose through the real renderer, failing on raw roles, directives,
  backticks, unstyled literals or unlinked tmux names, with negative
  cases on the origin/main rendering and clean controls
- Export a described tmux name as a plain Markdown link with no badge
tony added 29 commits October 18, 2026 07:19
why: Arguments rendered as a bold additionalProperties row, name: type
headings with an Optional tag and a permalink pushed away by flex
wrapping, and a Variant block with its own # for every member of
string | null, so a seven-argument tool ran to screens of rules and
gaps that no other API page has.

what:
- List each field as the API reference does: bold name, ? when optional,
  = default, the type in parentheses, the # permalink beside it, then
  the description at 14px; prompt arguments and URI variables use the
  same markup
- Write a map as Record<string, T>, and keep a union member, array item
  or map value in the type line unless it carries prose, constraints or
  fields; folded pointers stay as in-page anchors so every old fragment
  still lands on its field
- Say a closed object rejects other fields once, in prose, instead of
  in an additionalProperties fact row
- Label external type links as the API pages do (rel, JSON Schema title)
- Cover the folding rules, anchors and rendered structure in tests
why: An example page opened with a whole tested program, and a step
that explained one command had to be a hand-typed copy of part of it.
A copy drifts from the program the page runs. The pipeline included
only whole files: `region=` needs marker comments in the port source,
and the Go quickstart marks its lines with `docs:` comments the
region reader does not match.

what:
- Add excerpt fences: `excerpt="<title>"` quotes the titled block of
  the same page and `file="<path>"` quotes the port's cached file,
  each with `from` and `to` anchors that name text on the first and
  last line
- Resolve at build time and in the Markdown and LLM exports from one
  function, and drop the anchors from the rendered and exported fence
- Fail the build, naming the page, when `from` matches no line or
  several, when `to` matches nothing after `from`, when the range is
  reversed, and when the titled source block is missing or duplicated
- End a range at the first `to` match after `from`, so a block can end
  at a closing `fi` or `}` that the file repeats
- Accept single-quoted attribute values, so an anchor can hold double
  quotes
- Test the resolver, the three failures against a clean control, and
  the HTML and export paths on a page, including the port cache
why: Each native example opened with its whole script, so a reader met
the private socket, the trap, the pane-ID filter and the polling loop
at once, with no account of the tmux command behind each part.

what:
- Restructure attach-and-send-keys, capture-pane-output and
  workspace-from-file as a short concept paragraph and numbered steps,
  each introducing one tmux command or idea with a small block and what
  to observe, then "The complete script" with the unchanged tested file
- Take every step block from the page's own tested file through excerpt
  anchors, so the build fails if a step stops matching the script
- Name commands, options and format variables in inline code so the
  mention pipeline links and badges them; link the guide and concept
  pages each step relies on; mark keys with the key-cap rule
- Keep the old section anchors as ids
- List the capture and workspace pages in guide-examples.json so
  check-example-prose.py runs them; their file hashes are unchanged
- Accept a command linked by the mention pipeline in the native-page
  link test, resolve excerpts before comparing exported fences and
  prose, and update the key-cap expectation for the new steps
- Describe the step blocks on the examples overview

Verified: check-example-prose.py --example guide passed for all three
pages on tmux 3.7d; 355 tests passed in the focused site files.
why: The TmuxEnvironment example on the Java reference showed `Session
mine = ...` and `.filter(...)` flush against the left margin. The
doc-comment margin was already gone when `javadocToMarkdown` rewrote a
`<pre>{@code ... }</pre>` block, and it then trimmed every line's
leading whitespace, which was the code's own indentation.

what:
- Remove only the block's common margin from a `<pre>` body, line by
  line, and keep blank lines blank.
- Stop the opening pattern from consuming the first line's indentation.
- Test the converter and a Java comment through extraction; three of the
  new tests fail against the previous implementation, and prose
  paragraphs still reflow.
why: Six Java examples stored their code with every leading space
removed.

what:
- Regenerate java.json with gen-api-model.mjs at the revision the model
  records. Only `examples[].code` changes, in six symbols:
  TmuxEnvironment, EventSubscription.stream, Notification, Pane.run,
  WindowLayout and FakeTmux.
…riables

why: Prose across the site names tmux options, hooks and format variables
that neither the CLI Manual nor the C source reference indexes. A catalog
needs each name's type, scope, default and source line for every pinned
release, read from the tmux source rather than retyped.

what:
- Read options-table.c and format.c at each pinned commit: scope, type,
  default, range, choices, array and style flags, accepted spellings,
  the literal calls that fire each hook, and the object a format
  callback reads
- Take descriptions from the tmux.1 snapshot, whose digest must match
  tmux.1 at the commit; share a description between consecutive entries
  that have none of their own
- Refuse an unresolved table cell, a variable with no source location
  and a snapshot from another release instead of guessing
- Add --check, which verifies digests and shape offline and regenerates
  and compares with --source, plus a negative script, in the medium loop
  and the publication audit
- Commit site/src/data/tmux/catalog/<version>.json

Measured: tmux 3.2a has 104 options, 61 hooks and 200 format variables;
tmux 3.7c has 153, 68 and 258.
why: A reader who meets `destroy-unattached` or `session_attached` has
nowhere to look it up. The CLI Manual indexes commands and the C source
reference indexes declarations; neither answers what an option, hook or
format variable is, which scope it has, or how it changed between
releases.

what:
- Add /tmux/<version>/catalog/ with a section page per kind and an entry
  page per name, in the C reference's layout: sidebar tree, search with
  All, Options, Hooks and Formats tabs, kind badges and an entry page
- Show scope, type, default, values, release history, the manual text,
  the source line, the C declarations behind it and the library members
  whose documentation names it
- Serve tree.json for the sidebar and <entry>.preview.json for hover cards
- Add "Options, hooks and formats" to the tmux picker, sidebar, overview,
  version switcher, docs.json and llms.txt; keep older releases out of
  the sitemap
- Resolve a name's catalog page by release, and keep the matching logic
  for single words, shared names and command names in one module
why: Docstrings, guides, the CLI Manual and the Markdown exports already
badge tmux names, but an option, hook or format variable led to a manual
anchor. The catalog answers what a name means and which release has it,
so every mention should reach it, and the C declarations and library
members that wrap these names should reach it too.

what:
- Retarget tmuxEntityHref: commands stay in the CLI Manual; options,
  hooks and format variables resolve to the catalog page of the selected
  release, and an unknown or absent name has none. Manual hover cards
  keep their manual destination through tmuxManualEntityHref
- Read a whole literal ("session-created", after-new-window[0]), a
  separate hyphenated word of a code run, `#{...}` variables with their
  modifiers, quoted names in prose, and the argument of set-option and
  set-hook; link a single word or a shared name only beside the word
  option, hook or variable
- Keep a language API symbol ahead of a catalog name in HTML and Markdown
- Link names in the CLI Manual text, and each option, hook and variable
  it defines to its entry
- Show "tmux catalog" on library members that wrap options or hooks, on
  the C declarations of the option, format and notify tables, and in
  their Markdown exports
- Count mentions across guides and API docs in an outer-loop test

Measured: of 1791 option, hook and format mentions across the guides and
API docs, 1677 now reach a catalog page, 18 reach a library symbol and
96 stay plain (keyword arguments, parameter names, doctest output).
Before, none could reach a catalog page.
why: The extraction rules, URL scheme and linking order are decisions a
maintainer will want to change, and they live across a script, a library
and a page.

what:
- Record the entry kinds and the tmux table each one comes from
- Record the extraction guarantees, the URL scheme and the sidebar label
- Record the order in which a mention resolves, and what stays plain
why: Probing every extractor with an indented example showed the C++
one flattening a Doxygen `\code` block. `textOf` folded runs of spaces
and turned each `<codeline>` into running text, so `a(); b(); c();`
reached the page on one line. No committed C++ comment uses `\code`
today, so no model changes.

what:
- Render each `<programlisting>` as a fenced `cpp` block, one line per
  `<codeline>` with `<sp/>` as a space, and hold it aside while the
  surrounding text is folded.
- Share entity decoding with `codeOf`.
- Test indentation, entities and the text around the block; the test
  fails against the previous extractor.
why: Six Java examples were stored with every leading space removed
and nothing noticed: the code still parsed and every link resolved.

what:
- `check-example-indentation.mjs` finds each multi-line, comment-derived
  example in its declaration's source file at the revision the model
  records and compares relative indentation line by line. A block the
  source does not hold verbatim is counted as unlocated, a port without
  its checkout as skipped.
- `check-example-indentation.negative.mjs` plants the Java shape as a
  git checkout and model: the flattened example fails and names the
  line, the intact one and a clean control pass.
- Run both in `test-all.sh` after the source-link check.
- Measured across the committed models: the six Java examples fail
  before the fix and none fail in any port after it. Complete examples
  and console blocks are generated, not comments, so they are not judged.
why: The badge underline check failed in CI and in a local full run on
"the badge is underlined while hovered", though no decoration reached
the badge: the computed decorations and the pointer-free paint agreed.
The two screenshots of the badge strip differed in ten pixels at the
rounded corners, by one channel unit each (122 against 123), because the
badge sits at a fractional offset and a repaint, such as the one a first
hover preview triggers, rasterizes its antialiased corners a unit apart.
With the preview answering 150 to 350ms after the hover, 4 of 25 runs of
the sample failed this way. A real underline moves whole pixels: the
largest channel change was 222.

what:
- Compare the with and without decoration screenshots with pixelmatch at
  its default threshold, so antialiasing noise passes and an underline
  fails; restoring the old anchor rule still fails every variant, at
  rest and hovered
- Add the Python attaching-to-tmux guide, whose C and $ badges sit in
  prose links, to the sample
why: The parameter list leaves out an undocumented `self` receiver, but
the Rust signature still linked it, so every by-value `self` method
(Session::kill, ServerBuilder::build and others) pointed at a
`.parameter.self` anchor that no longer exists and the assembled-site
link check failed.

what:
- Render an undocumented `self` receiver as a keyword in Rust
  signatures, using the same rule that drops it from the parameter list
- Keep `&self` and documented receivers linked, since the list keeps them
- Test that a by-value `self` method links only the parameters it lists
why: The "Choose a solution" menu and the documentation picker printed a
two-sentence support-policy paragraph under every concept product, and
the concept badge's tooltip repeated it. A menu is for choosing; a
reader scanning it got a block of policy text instead of a label.

what:
- Show concept products in both menus with the same one-line summary
  other products use, "Working example of the core library", beside
  the concept badge, and drop the paragraph under an expanded group
- Shorten the concept description to "A working example of the core
  library. It can change without notice." for page notices and data,
  and use the one-line summary for the badge tooltip and screen readers
- Say it in one clause on the MCP overview, linking the concept page
  for the full explanation
- Make the navigation check refuse a paragraph in an expanded menu
  group and require the one-line summary
why: A kind badge centred its mark's em box, so a capital such as the S
of a struct sat about half a pixel high and a lowercase o or m about
half a pixel low inside the chip.

what:
- Trim each badge label to the height of its ink with text-box: cap
  height for capitals and symbols, x-height for the x-height letters
  (a, m, o, p, r), so the grid centres what the reader sees
- Tag x-height marks with data-mark="x" wherever a badge is built: API
  badges, the reference tree, prose and tmux mentions, and MCP links
- Browsers without text-box support keep the centred em box
why: The GitHub and Packages menus drew Core Library, MCP and Workspace
Manager with the same icon, two-line layout and spacing as the links
beneath them, so a section name read as another clickable row. The
names were never links; they only looked like them.

what:
- Render each product as a small muted uppercase label with no icon or
  summary line, no pointer cursor, and no hover, focus or active state
- Inset a product's rows under its label, with a larger gap before each
  label than after it
- Name each group by its label with aria-labelledby, using ids that
  carry the placement so the bar and Menu copies stay unique
- Give a menu of one product no heading, since there is nothing to tell
  apart
- Add pickerSections() to shape a menu's rows by product, and test its
  order, coverage and which menus carry headings
- Read each row's group from data-group instead of a relabelled
  aria-label in the picker tests
why: Nothing measured whether a product heading in the GitHub and
Packages menus stayed a label. A heading that took a tab stop, a
pointer cursor, a hover paint or an arrow-key stop, or that survived a
filter with no rows under it, would pass every existing check.

what:
- Add check-header-menu-sections.mjs and its --header-menu-sections
  flag, and run it in the sample and the full browser run
- On Python, Java and .NET, assert each heading is not focusable, has
  no pointer cursor or hover or press change, labels its group by id,
  differs from item typography in size, case and colour at 4.5:1
  contrast, and that Tab and the arrow keys never land on one
- Assert filtering hides a heading with its group, restores it on
  clear, and removes the group name from the accessibility tree
- Check Kotlin, a single-product port, has no heading or group role
- Check light and dark at 1440 and 390px, with the menu inside the
  viewport as the phone sheet
- Add a negative control: the check rejects the origin/main markup and
  CSS, and each of a focusable, pointer, hover, plain-typography, flush
  and unfiltered heading
why: The picker badges a product with its release stage, but the
registry data had no stage for Python's MCP server or tmuxp. Python
declares its stages on PyPI, where libtmux-mcp is Pre-Alpha while
libtmux and tmuxp are Production/Stable, so a version string alone
cannot say it: 0.1.0a22 reads as alpha.

what:
- Probe libtmux, libtmux-mcp and tmuxp on PyPI and record each
  project's Development Status classifier as its maturity
- Leave libtmux's git tag off the two projects released from other
  repositories
- Add the Python entry to registry.json; the other ports' entries are
  unchanged
- Add releaseMaturity() to read a stage from the declaration, the
  version or the tag, and fail on a stage it cannot read
- Add productStatus() to give a product its badge: concept for another
  port's companion, otherwise its release stage
- Add one-sentence summaries per badge and the guide section that
  explains them, and test every port's products against them
why: Core Library carried no badge while MCP and Workspace Manager
carried concept, and a footer line plus a paragraph under each
companion explained both. A menu is for choosing, so the explanation
belongs on the badge's hover card, with the full text on the guide. A
focusable badge inside a row's summary or link would nest one control in
another, so the badge has to be the row's sibling.

what:
- Replace ConceptBadge with StatusBadge: concept or a release stage,
  from the same data as the install commands, on every product row of
  the documentation picker, the solution menu and the MCP overview list
- Place the badge beside the product name with CSS anchoring, and at
  the row's end where a browser lacks it, as a sibling of the row's
  control
- Open a card on hover, keyboard focus and tap with the stage as its
  title, one line of about eight words, and a Read more link to the
  concept and alpha section; keep it open while the pointer or focus
  is inside, and close it on Escape before the menu sees the key
- Say what each product does in every row, from the picker summaries
  and the same on every port: API reference and guides, Tools for
  connected assistants, Repeatable development environments
- Remove the footer status line, the paragraph under an expanded
  concept group and the concept summary constant
- Keep the trigger's badge a plain label, because the trigger is itself
  a control; its name and description carry the status
- Add the release stages to the guide's concept and alpha section
- Read the trigger and group names without the badge in the checks
…rowser

why: Nothing measured that every product row carries its stage badge,
that a menu row stays a label and one line, or that a badge explains
itself without nesting a control. A missing library badge, a paragraph
under a row, a card the keyboard cannot reach, an Escape that closes the
menu first, or a badge button inside a summary would pass every
existing check.

what:
- Add check-status-badges.mjs and its --status-badges flag, and run it
  in the sample and the full browser run
- On Rust, Go, Python, Julia and Java, in the picker and the solution
  menu, assert every product row has exactly one badge beside its name
  holding the stage its data gives, Core Library included
- Assert each row shows its label and one picker summary line, the same
  on every port, with no paragraph, and that the footer line is gone
- Assert no interactive element sits inside another, in both menus
- Assert the card opens on focus and hover, is titled with the stage,
  holds one line under sixty characters and a Read more link that Tab
  reaches and that resolves to the guide's section, and closes on
  Escape with focus back on the badge and the menu still open
- Assert a phone tap opens and a second tap closes the card
- Check light and dark at 1440 and 390px
- Add a negative control: the check rejects the pre-badge markup for
  both menus, which fails on Core Library, and each of a second or
  wrong badge, a distant badge, a paragraph or long summary, a long
  card line, the footer line, a nested badge, a card that never shows,
  no hover, no Escape, an unreachable link and a broken section link
why: A menu row is a label and one line. Nothing stopped a paragraph
from reaching the GitHub or Packages menus the way it reached the
documentation picker.

what:
- Assert each row of the header menus has one short line under its
  label and no paragraph, and no text block in a menu runs past a
  short line
- Add a negative control that puts the long concept paragraph in a menu
- Load a page again once when the development server replaces it while
  optimizing dependencies
why: The text-box trim centred each mark on its ink in theory, but at 1x
the result still rounded: an S sat half a pixel high, and an m moved from
half a pixel low to half a pixel high, with more space under it than
over it. A 17px badge leaves 15px inside its border, an odd height, so
no whole-pixel position centres a mark.

what:
- Grow the compact kind badge from 17px to 18px, leaving an even 16px
  inside the border; capitals, ascender letters and symbols now sit
  exactly centred at 1x and within a quarter pixel at 2x
- Drop the text-box trim and lift the x-height marks (a, m, o, p, r and
  the colon) by one pixel, which centres them the same way; only p,
  whose descender hangs below, stays a pixel low
why: The homepage opens on its default language and switches to the
?port= language once its script runs. After a development-server reload
the badge check read the default language's menu and failed with the
wrong port.

what:
- Select the solution menu by its port and wait for it instead of
  asserting the language right after the page loads
why: Each product row in the homepage solution menu now carries a status
badge whose card links to the shared concept page. The launcher check
read every link in the menu and required each to point into the chosen
port, so the card's link failed it for every port.

what:
- Read only the menu's destination links when checking that a port's
  solutions stay within that port
why: The concept guide explained libtmux.org's own badges inside the
tmux guides, and parity and translations sat loose at the locale root.
None of them is tmux documentation, and each loose /en/<page>/ spends
the root namespace.

what:
- Publish "Support and release stages" at /meta/support/, shortened to
  four sections. It keeps the #concept-and-alpha section that the status
  badge cards, the concept notice and the MCP overview link to.
- Move parity and translations to /meta/parity/ and /meta/translations/
  and update the locale switcher, the untranslated-page notice, the MCP
  overview, the Japanese home page, the table check and the page-port
  link list.
- Remove the guide from the tmux collection, so the tmux sidebar and
  the tmux exports no longer carry it.
- Redirect /tmux/guides/concept-examples/, /parity/ and /translations/
  with the existing RedirectPage (canonical link, script replace and a
  noscript http-equiv refresh). The catch-all route emits them in the
  root build of every locale, and the sitemap omits them.
- Add meta-pages.test.ts: the redirect table, no tmux sidebar entry
  (the old entry restored as the negative control), the badge fragment
  resolving on the support page, and an allowlist of what may sit
  directly under the locale root (planted pages as the negative
  control). mcp and third-party-notices are still loose and listed as
  undecided.
- Point the status-badge gate at /meta/support/.

Verification: focused vitest files pass (567 tests, 124 skipped for the
absent assembled tree); astro check reports 0 errors; the new test
fails with the old guide restored or a page planted at the root. The
status-badge gate timed out at its 10 s cap on its first run at load
near 25 and passed on the second. A full shell build emitted the three
meta pages, then hit a 25 minute limit in the tmux reference pages
before it reached the redirect routes.
why: Pages about libtmux.org live under /meta/, and the notices page
was the last of them still loose at the locale root.

what:
- Move the notices page to /meta/third-party-notices/ and redirect
  /third-party-notices/ there in both locales, as for the other moved
  pages.
- Point the footer on every page and the shell footer that the Sphinx
  pages load at the new URL.
- Skip the meta section when grouping a port's documentation menu, as
  the notices page was skipped before.
- Count the notices page with the other meta pages in the root
  allowlist test, which now leaves only mcp undecided.
why: /mcp/ repeated what each port's MCP page and the homepage solution
menu already say: the install widget for one server, a port list and an
in-development note. The maintainer chose to drop the root page rather
than keep a second overview.

what:
- Remove the /mcp/ route and its Overview component. The old URL
  redirects to the site home in both locales, where the solution menu
  lists each port's MCP server. The redirect target may now be empty,
  meaning the build's home.
- Point the header menu's MCP chip at the current port's MCP home, and
  omit it on pages with no port or a port without an MCP server.
- Do the same in the shell the Sphinx, rustdoc and DocC pages load,
  which now reads MCP availability from the generated port table.
- Send the tool catalogue's install sentence to the Python MCP page, or
  the building port's.
- Drop comments that described the retired page.
- Test the redirect, the removed route, the header chips and the shell
  chips, each with a planted regression as the negative control. The
  root allowlist now leaves only /mcp/tools/ undecided.
why: The tmux sidebar's CLI Manual, catalog and C source reference links
took the docs build's version slug. A local or production build is
`latest`, which happens to be a tmux release too, but a pull-request
preview is `pr-157`, so every tmux page in English and Japanese linked
/tmux/pr-157/manual/, a page that does not exist, and the preview's
link check failed.

what:
- Give the sidebar the version the page's documentation context already
  uses: the port's version on a port page, the tmux release on a tmux
  page
- Let the glossary navigation harness swap in a stub for a child
  template
- Render the tmux sidebar under a pull-request slug and require tmux
  release links

This branch was successfully deployed

1 active (outdated) deployment
docs-preview — 4bba3c4c Deployed Oct 18, 2026 by tony via publish-preview / publish #813
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant