Skip to content

Release 2.6.0 - #37

Merged
mixedrays merged 21 commits into
mainfrom
develop
Oct 16, 2026
Merged

mixedrays merged 21 commits into
mainfrom
develop

Conversation

@mixedrays

Copy link
Copy Markdown
Owner

No description provided.

mixedrays and others added 21 commits October 15, 2026 21:16
Every handler built its group from options itself and passed the readings
into shallow positional helpers, so one roving-stop rule was spread over
four modules and re-tested through five suites.

- Add a Group module (`readGroups` in group.ts). The four readings (root,
  items, skip, roving) resolve once per options object, with the attribute
  defaults inside. `at(target, listener)` reads a group from an event and
  `of(root)` from a root. A group answers lazily for its items, focused
  item, own items, roving items and stop, and holds the one navigable-item
  rule: neither skipped nor disabled.
- `group.move()` lands a move and decides which item the stop is carried
  from; `group.cross()` lands an exit or enter with the destination group's
  own stop. `stopSource`, `stopHolder` and focus landing are private to the
  module, and no handler computes the stop's source.
- keyRove, rove, createTypeahead, followFocus and initRovingTabindex keep
  only their own logic. crossBoundary calls the Group module and returns
  the group it lands in, and resolveTarget takes the group's navigable test.
- A group is one class instance with its getters on the prototype, and
  options that name none of the readings share one reader, so keydown cost
  stays within noise against HEAD (`pnpm bench:headless`).
- Test the group rules once, in a new group suite, and drop the handler
  cases that re-tested them, keeping one end-to-end case per rule.

Public exports, types and behaviour are unchanged.
refactor: resolve each group once and let it carry the roving stop
A demo's behaviour was spread over five tables keyed by its name in three
files, and the runtime checked the DOM again for what the build had decided.

- build/demo-definitions.ts holds one entry per demo: group options, extra
  handlers and icons. The build stamps icons from it and src/demos.ts wires
  the demo from it; HISTORY, BAND, ITEMS, EXTRAS and CONFIGS are gone.
- Every demo is a panel: the unused history and line logs, their markup
  and src/demos.css are removed.
- src/readout.ts is the one readout for the panels and the hero, with its
  initial markup, nameOf and isMoveResult written once. The hero keeps its
  easter eggs behind a flag and uses the panels' format.
- Placeholders parse in any attribute order, and a malformed one fails the
  build. hasDemos is removed.
- The page's first live demo takes focus, so the hero no longer has to
  mount before the panels.
Three callers chained demo expansion, meta expansion and rendering by hand,
and search allocated heading ids with a second parser over a different
expansion, so a search result could link to a heading the page did not have.

- build/page-render.ts owns the pipeline. renderPage(page, demos) parses
  the page once and returns the rail headings, the search sections and the
  .md twin, plus tree(), which renders the same tokens through Shiki on its
  first call and keeps the result.
- Search reads the html-mode demo expansion, which gives the same section
  text on every page, so its ids come from the parse the page renders.
  searchParser and collectSearchSections are gone.
- markdown.ts builds its renderer synchronously and loads Shiki on the
  first render, so parsing never waits for it. The resolveHref hook, which
  the only caller passed as the identity, is removed.
- The content plugin parses every page when the site loads and serves the
  page modules, the search index, the twins and llms-full.txt from those
  renders; its own page-module cache is gone. toSearchIndex and
  toLlmsFullTxt take rendered pages, and toMarkdown moves into the module.
- tests/ is type-checked. The search tests call renderPage instead of
  copying the pipeline, and a new test pins the twin's shape.

dist/client is unchanged apart from the timestamp the router writes into
each prerendered page.
Six hand-written walks decided which group an element is in, which is
around a nested root and which is nested in an item, each with its own
start and stop rule. A new internal nesting module answers the three
questions with one walk up the tree and one down it, and applies the
listener's reach in one place.

- nesting.ts owns rootAt, ownerRoot, outerRoot, innerRoot and
  listenerElement, tested at its interface.
- GroupReader gains `around` and `inside` and drops `isRoot`, so no
  handler walks the tree itself.
- The group an event is in still looks past the listener's element: a
  wrapper root above it wins. Documented under Roots and pinned.
- Every walk from a group or item to the group around it now stops at
  the listener's element. The focus key's group and followFocus no
  longer reach a root above the listener, and followFocus reaches a
  shadow-root listener's group from a top-level nested root, as exit
  already did.
refactor: walk group nesting in one module
refactor(docs): define each demo in one place
refactor(docs): render every page projection from one parse
The markers src/components/markdown.tsx swaps for React and the elements
build/html-tree.ts keeps opaque were two lists with nothing linking them,
and the build stamped widget markup that React threw away.

- build/placeholders.ts lists every placeholder: its marker (a data-*
  attribute or a class, with a tag where needed) and whether its
  contents stay opaque. html-tree.ts and markdown.tsx both read it
  through placeholderOf, and neither keeps a list of its own.
- markdown.tsx draws each swapped placeholder from DRAW, a table keyed
  by the registry's names, so an entry with no component does not
  compile. CodeTabs finds its copy button through the registry too.
- Code tabs are stamped with their labels and which one is selected,
  and copy buttons with their label. The tabs' ids, aria-controls,
  tabindex, data-keyrove-* and hidden, the copy button's SVGs, type and
  data-copy-code are gone, and so is the per-page id counter.
- tests/placeholders.test.ts renders every page and checks that each
  registry entry is written on one, and that every data-* hook is a
  placeholder's marker or named as not one. A page-render test pins
  the code-tab and copy-button stamps.
- The twin test's demo placeholder gets the data-demo-label that #31
  made required; the test failed on develop.

Every non-asset file in dist/client is unchanged apart from the
router's timestamp and the chunk hashes, and the client's JS is 64 kB
smaller.
Absolute URLs were put together in four places, the twin routes rebuilt
the .md rule by hand, and adding a generated file meant editing four.

- build/addresses.ts replaces build/paths.ts. It owns the page and twin
  paths, the landing page's /index.md included, and turns a site path
  into an href under the deploy base or a URL on the site. head.ts and
  plaintext.ts take its addresses instead of a bare base.
- GENERATED_FILES declares each file's path and type once. The
  prerender list and the served files both read it, and a file declared
  without a body does not typecheck.
- Files are keyed by site path, and every file route, the twins
  included, serves its handler's pathname, so no route builds a file key.
- The browser shares one base-only instance for links, the twin fetch,
  the search index, the favicon and the llms.txt links. markdownHref,
  routeToPath and createHrefResolver are gone.

dist/client is unchanged for the default build and DOCS_BASE=/keyrove/,
apart from the router's timestamps and the JS chunks that carry the
address code.
CodeTabs reads a pattern tab's label and aria-selected, and
CopyCommandButton reads the command and its label. The ids,
aria-controls, tabindex, data-keyrove-* and hidden on the hand-written
pattern tabs, and type on the install buttons, never reached the page.
content/index.md now writes the same markup build/markdown.ts stamps.

The rendered pages are unchanged; only the landing page's .md twin is.
The sidebar, the drawer and the Toc each put the same rail together from
a ref, useRovingStop, useNavShortcut, aria-keyshortcuts, the keyRove and
followFocus handlers and a KeysHint. The shortcut also found the search
dialog by an id set in search-dialog.tsx.

- keyboard-rail.tsx (was nav-keys.tsx) exports useKeyboardRail. A rail
  declares its links, its shortcut and the link its stop rests on; the
  hook returns the root's props, the hint and findStop.
- The stop moves whenever the link it rests on changes, and is set as a
  rail's links arrive, so the drawer needs no code of its own for it.
  It is still left alone while focus is inside.
- The shortcut is off while search is open, read from search's context
  instead of the dialog's id.
- The drawer now opens itself on the chord below `lg`: Sidebar loses
  onReveal, SidebarDrawer gains onOpen, and the drawer carries
  aria-keyshortcuts.
- navGroup is removed.
refactor(docs): build the sidebars' keyboard rails from one module
refactor(docs): keep the placeholder registry in one place
refactor(docs): answer every site address in one module
Remove unused docs and benchmark assets, dependency, and exports.
Import the docs stylesheet directly and fix the demo test fixture.
packages/ now holds only what is published to npm. The docs site and
the benchmark, which are not published, move to apps/. The workspace
lists both folders, and the docs deploy uploads apps/docs/dist/client.

The apps are renamed @mixedrays/keyrove-docs and
@mixedrays/keyrove-bench. A scoped package name allows one slash, so
pnpm read @mixedrays/keyrove/docs as a folder inside the library and
wrote a broken docs symlink into packages/keyrove, which is removed.

Every docs page's sitemap lastmod will date from this commit:
build/git.ts finds dates by path and does not follow renames.

Also bumps the docs version to 3.1.0 and documents the branch flow in
CONTRIBUTING.md.
Releases the behaviour change in 7fd93e4: the focus key's group and followFocus no longer reach a root above the listener, and followFocus reaches a shadow-root listener's group from a top-level nested root.
@mixedrays
mixedrays merged commit 2102de1 into main Oct 16, 2026
1 check passed
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