Skip to content

Feat/grd 191 Move Studio's sidebar into Equality as the Nav parts - #205

Merged
matthewmr-eqty merged 19 commits into
mainfrom
feat/grd-191-menu-nav
Oct 27, 2026
Merged

matthewmr-eqty merged 19 commits into
mainfrom
feat/grd-191-menu-nav

Conversation

@matthewmr-eqty

@matthewmr-eqty matthewmr-eqty commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Need

Studio's sidebar and the docs sidebar should come from Equality, so they look and behave the same and Studio can delete its own copy (GRD-192). Part names and props freeze with this release.

Problem

The rows, the drill-in and the sidebar frame lived only in Studio, with their own colour variables. The motion and focus rules were spread across two Studio files.

Change

Eight parts, in two layers:

Part What it is
NavRail, NavRailItem The column of icon links on the left
NavPanel The panel beside it: a <nav> landmark with a header and a scrolling body
NavSwitcher Switches between items such as projects, in the panel's header
NavList, NavListGroup, NavListItem, NavListSeparator The rows, in groups; they work outside the sidebar too
NavDrill, NavDrillLevel Levels that slide in and out, each with a ‹ row back
  • Named NavList, not Menu: menus are for actions, which is DropdownMenu's job; nav lists are for going places.
  • One row size, 36px as in Studio today, growing to 44px on touch screens. Density for all of Equality is GRD-173.
  • NavDrill can follow the URL (value and onValueChange, as Tabs does), and slides only when the user asked for the change.
  • Departs from Henry's GRD-81 rule that suffix is only for the › caret: the › now comes from drill and is hidden from screen readers, and suffix holds badges and marks (a count, "New", ↗) that screen readers read with the label.

API choices

Every prop is used by Studio or the docs sidebar. Adding a prop later doesn't break anyone; removing one does.

  • No title on NavPanel: no Studio panel used one. A heading goes in header.
  • One action on NavSwitcher, as in Studio today: "Create Project" is an item in the list, because a separate button in the dropdown can't be reached by keyboard. A footer slot can be added later.
  • Studio's touch, back, view and motion are gone: touch sizing is automatic, levels draw their own ‹, and NavDrill owns the motion
nav-comparison-dark

Review focus

packages/ui/src/components/nav-list/nav-drill.tsx carries the risk: the slide between levels and where keyboard focus lands. The rest is mostly additive. Skip pnpm-lock.yaml.

Not covered

React 18, real browsers and screen readers. Density for all of Equality is GRD-173.
Closes GRD-191 · Part of GRD-81

matthewmr-eqty and others added 16 commits October 26, 2026 09:48
Need: the nav rows leaving Studio need sizes and fills that Equality owns, so both consumers read the same values.
Problem: Equality had no row sizes or current and hover fills; Studio kept its own copies.
Solution: theme/decisions.css declares the list, control and touch sizes as runtime variables, and the two fills become tokens with bg-list-item-current and bg-list-item-hover utilities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Studio's nav hands IconButton to Radix triggers and focus code that need the element.
Problem: IconButton dropped refs under React 18, and dropped them on the link under React 19 too.
Solution: the ref reaches whichever element renders, the button or the link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Studio's sidebar rows and the docs sidebar should be one component other screens can reuse, not a copy per app.
Problem: the rows lived in Studio as NavItem and NavSection, with a decorative-only suffix and links as the only row type.
Solution: Menu, MenuGroup, MenuList, MenuItem and MenuSeparator render links or buttons sized from the list decisions; a suffix is read out, and a drill row adds › and asks its MenuDrill to open a level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: a sidebar, popover or drawer should drill into a level and back with the same motion and focus rules Studio has.
Problem: Studio's panel cloned the old rows in the DOM, and each caller had to pass the motion and request focus by hand.
Solution: MenuDrill keeps the leaving level mounted, inert, while two animations slide it away, and only after a change it asked for; focus moves to ‹ after a drill and back to the row you came from after ‹.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Studio should import its sidebar frame from Equality so the copy in apps/studio can go.
Problem: the rail, panel and switcher lived in Studio, with their own colour variables and the drill-in built into the panel.
Solution: NavRail, NavRailItem, NavPanel, NavSwitcher and blockClosedTypeahead read Equality's tokens and size decisions; NavPanel is only the frame, and a panel that drills holds a MenuDrill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: the docs sidebar is the second consumer, and it has to test the Menu names before 4.15.0 freezes them.
Problem: nothing outside Studio had built a real tree on the parts, and the docs tests could not load TSX.
Solution: DocsNavMenu renders the docs tree on Menu at sm density, unwired, with a Vitest render test beside the node:test files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: teams building on Equality should find Menu and Nav, and agents in other repos should stop hand-rolling sidebars.
Problem: the new parts had no pages, no live demos and no entry in the skill's name table.
Solution: Menu and Nav pages with live demos, including a popover and a drawer that drill in, a Sizes table, and three rows in the name table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Studio's swap (GRD-192) and the docs sidebar (GRD-193) need the parts published.
Problem: the Menu, MenuDrill and Nav parts and their size and colour tokens existed only on this branch.
Solution: 4.15.0 publishes them, with a changelog entry naming the suffix rule that changes GRD-81.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed for

Need: page links must leave focus alone, and a drill-in must not slide or move focus on an unrelated later change.
Problem: a pending ‹ or drill request lived until the view changed, which Studio's page links often leave unchanged, so focus could be pulled into the nav; the drawer demo's levels were see-through mid-slide; React 18 warned on server renders.
Solution: anything the user does outside the drill drops the pending request, the layout effects go through useIsomorphicLayoutEffect, and the drawer demo puts Menu directly on the drawer's surface, as the docs now say to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…allers write

Need: Teams pick these parts from autocomplete, and the names freeze when 4.15.0 publishes.
Problem: MenuItem sat beside DropdownMenuItem with a different keyboard model, and callers placed a bare list and the ‹ row by hand.
Solution: The rows are NavList* and the drill-in NavDrill, controlled with value like Tabs. Groups draw their own list, levels their own ‹ row, and the leaving level no longer flashes after ‹.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Developers adopting the sidebar need one place that shows every part as Studio uses it.
Problem: The rows and the sidebar were split across two pages, with generic demos and drill-in examples filed as variants.
Solution: One Nav page with usage, variants, a drill-in section (on its own and following the URL) and examples, all on Studio's labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: The names and markup freeze when 4.15.0 publishes, and a fix after that is a breaking change.
Problem: Nesting a group gave invalid list HTML, ‹ didn't say "back" by default, a loading switcher was silent, and two row sizes waited on a density decision.
Solution: One 36px row size; nested groups and separators wrap themselves in list items; ‹ defaults to "{label}, back"; the loading switcher announces itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: The next person to edit the nav parts needs to know what not to change, not a narration of the code.
Problem: Several comments described layout and motion instead of the constraint behind them.
Solution: Those comments now name what breaks, descriptive ones are gone, and NavDrill's prop docs match the docs page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… action plainly

Need: Every prop in 4.15.0 is frozen, so each one should have a user.
Problem: NavPanel's title had none, and action's description read like a one-off.
Solution: title is gone (a heading goes in header), and action reads as a general command at the bottom of the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Someone reading the nav styles or inspecting the page should find the same naming as in every other component.
Problem: The nav modules used bare class names such as .item and .panel, and one variant written with a single dash.
Solution: Classes carry the component's name with -- for variants (.nav-list-item, .nav-list-item--back). The page renders identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthewmr-eqty
matthewmr-eqty marked this pull request as draft October 27, 2026 13:55
matthewmr-eqty and others added 2 commits October 27, 2026 10:01
Need: Everyone who installs 4.15.0 receives decisions.css, so it should hold rules, not a discussion.
Problem: The file opened with a commented-out draft of the scale debate's proposals.
Solution: The proposals move to GRD-173; one line stays, warning that a live @theme --spacing changes every component's compiled CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Need: Moving between nav levels should feel quick and quiet, the way Equality's other nav motion does.
Problem: The half-width push travelled far for a narrow panel, and a plain cross-fade showed both levels' rows at once as doubled text.
Solution: Both levels move 12px over 300ms with a quick start and long settle, and the leaving level fades out within the first third.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthewmr-eqty
matthewmr-eqty marked this pull request as ready for review October 27, 2026 14:23

@tmccoy14 tmccoy14 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Six things I think should be sorted before this merges, inline below. Smaller, non-blocking items are in a separate comment.


Generated by Claude Code

Comment thread packages/ui/src/components/nav/nav-rail.tsx
Comment thread packages/ui/src/components/icon-button/icon-button.tsx
Comment thread packages/docs/src/runtime/chrome/DocsNavList.tsx Outdated
Comment thread packages/ui/src/theme/theme.module.css Outdated
Comment thread packages/ui/src/theme/theme-base.css Outdated
Comment thread packages/ui/tests/setup.ts Outdated
@tmccoy14

Copy link
Copy Markdown
Contributor

Smaller, non-blocking items:

  1. Consistency: in components/index.ts the nav-list/* and nav/* exports sit between list-or-grid-view-toggle and loading-overlay; alphabetically they belong after motion-collapsible. NavRail and NavListSeparator are plain functions without forwardRef/displayName, unlike every other new part.
  2. CHANGELOG wording: "A row's suffix is now read by screen readers… changes the rule in GRD-81" describes a component that's new in this release, and contradicts "No existing component changes". It also cites an internal ticket and "Studio", which no earlier entry does. "Rows are 36px" leaves out the 44px under a coarse pointer that the docs mention.
  3. NavDrill re-renders: setRootRef is recreated every render, so a caller's callback ref gets null then the node each time (useCallback or Radix's compose-refs fixes it). The NavDrillContext.Provider values are fresh object literals, so every NavListItem re-renders whenever NavDrill's own state changes. Worth memoising.
  4. Production console.warn: the WCAG 2.5.3 backLabel warning in NavDrillLevel runs in production builds too. Wrap it in a development-only check?
  5. blockClosedTypeahead is public API but lives in nav/select-typeahead.ts; it's a Select helper and fits better under components/select/. The docs recommend it but leave it out of the Usage import list.
  6. Docs (nav.mdx) vs the create-documentation template:
    • The NavDrillLevel Required column says "With parent"; the template uses only ✅/❌, so the condition could go in the description.
    • The NavRailItem table is missing href.
    • The Prefix and Suffix example uses drill="eu-ai-act" outside a NavDrill, where the row is a button that does nothing.
  7. DocsNavList (only if it stays in this PR rather than moving to GRD-193):
    • A group node with no children falls through to a NavListItem with no href, which renders a button that does nothing.
    • Rows nests to any depth, but only [data-depth='1'] is styled.
    • External links open in a new tab with only an aria-hidden arrow, so screen readers aren't told.
    • key={section.label ?? index} mixes labels and indexes as keys.

Generated by Claude Code

Need: apps that install 4.15.0 get parts that reach every user, stay inside the Equality box and keep their names after release.
Problem: review found a rail item keyboards can't reach, a link IconButton that drops tooltip props, sizes leaking onto the host page and an unreleased docs draft riding along.
Solution: fix each point, prefix the sizes with --eq-, generate the scoped decisions file, and move the docs draft and blockClosedTypeahead to where they belong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthewmr-eqty

Copy link
Copy Markdown
Contributor Author

@tmccoy14 all addressed in 617c859:

Inline

  • A1 href is now required unless asChild (a union like NavDrillLevelProps), with a type test and the docs row.
  • A2 the link IconButton passes on ...props. There's a test with TooltipTrigger asChild that failed before the fix.
  • A3 DocsNavList is out of this PR. It moves to GRD-193 with your B7 points.
  • A4 the decisions now only land on .root in the scoped build. Sizes are prefixed --eq-. theme-decisions-scoped.css is generated from theme-decisions.css, the same way preflight is, and a test fails if it's stale.
  • A5 now oklch, and the changelog says the colours are mixed, not Pinwheel tokens. On dark mode, I kept one name that switches, the way all the Pinwheel colours work. control-edge has -dark twins only because it sits in @theme inline, which freezes its mix percentage. Moving it over is GRD-206, under GRD-173, which now frames the options and decisions layers.
  • A6 the stub now loads only in nav-drill.test.tsx. The full suite passes without it in the shared setup.

Non-blocking

  • B1 the exports are in A–Z order. NavRail and NavListSeparator now use forwardRef and have a displayName.
  • B2 changelog rewritten: no Studio or GRD references, the IconButton changes listed under Changed and Fixed, and the 44px touch rows mentioned.
  • B3 the ref callbacks and context values are now stable. The caller's ref fires once instead of on every render.
  • B4 the warning only runs outside production. I checked that a production bundle drops it.
  • B5 moved to select/, and the Select docs now have a "A select that navigates" note.
  • B6 all three fixed.
  • B7 moves with A3.

I also removed the CSS tests that only repeated the stylesheet's values. The leak and stale-file checks stay.

@tmccoy14 tmccoy14 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@matthewmr-eqty
matthewmr-eqty merged commit 2e0b40e into main Oct 27, 2026
1 check passed
@matthewmr-eqty
matthewmr-eqty deleted the feat/grd-191-menu-nav branch October 27, 2026 15:48
Shrinks99 added a commit that referenced this pull request Oct 28, 2026
Co-authored-by: matthewmr-eqty <matthew.rhodes@eqtylab.io>
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.

2 participants