Skip to content

feat(ui): add the @lody/ui Toggle, ToggleGroup and Toolbar - #727

Merged
wibus-wee merged 2 commits into
feat/ui-v2from
feat/ui-toggle-toolbar
Sep 15, 2026
Merged

wibus-wee merged 2 commits into
feat/ui-v2from
feat/ui-toggle-toolbar

Conversation

@wibus-wee

@wibus-wee wibus-wee commented Sep 15, 2026

Copy link
Copy Markdown
Member

Related issue

Same-repository branch; no intake Issue.

Problem / pressure

@lody/ui has a control for every value a form stores and none for the state an interface holds without storing it — bold, wrapped lines, this filter. So twenty-two files in packages/components each draw their own aria-pressed button, and the fills they chose to mean "on" run to thirteen unrelated washes: bg-muted, bg-muted/30, /40, /60, /80, bg-muted-foreground/20, bg-secondary, bg-secondary/50, bg-primary/10, /15, /[0.05], bg-white/10, bg-white/14 — and several that change nothing but the text colour. packages/components/src/ui/toggle.tsx (the Radix wrapper) had reached zero in-repo callers and was never deleted, so @radix-ui/react-toggle was still a production dependency of a package that never imported it.

Summary

  • packages/ui/src/toggle/: one family, one token group. toggle.tokens.stylex.ts (+ palette theme), surface.ts (shared appearance), set.ts (the size/shape context, in its own file so the two parts do not close an import cycle), toggle.tsx, toggle-group.tsx, toolbar.tsx.
  • On is the well, not ink. The rules give a stored state ink, and this is where that rule is read carefully rather than literally: ink is what a control that already sits in a well becomes when it is on — a Switch's off state occupies the well, so on has to leave it — and a toggle rests on nothing, so the well is still free. It also keeps a bar of eight from reading as eight primary buttons. Off is a ghost Button; the state is a fill and a label colour (secondaryLabellabel).
  • Toggle: mini/small/medium/large (24/28/32/36, taken from control, not from button), shape="pill", icon, and Base UI's pressed / onPressedChange / value. A real <button> with aria-pressed; it does not bob, it goes down and stays.
  • ToggleGroup: generic over the caller's own union, multiple, wrap, orientation; states the size and shape once for its members. Not a Tabs strip — two members can be pressed at once, which no sliding pill can say, so there is no track.
  • Toolbar: Root / Button / Group / Separator. Draws nothing at all, not even the line a table draws; it exists to make a row of controls one tab stop the arrow keys walk, skipping the disabled ones. Toolbar.Button is Base UI's, unstyled, like every trigger here. separator.tsx now exports its styles so the bar's line is the same one Separator draws.
  • Migrations: tasks/task-body-selection-toolbar.tsx (the whole bar: nine tab stops → one) and tasks/tasks-workspace.tsx (the "Show" chips → a wrapping multiple set). src/ui/toggle.tsx deleted, @radix-ui/react-toggle removed.
  • Board section, test/toggle.test.tsx (13 tests), test/gallery.test.tsx token coverage, RULES.md "Pressed", README rows and prose, an AGENTS.md bullet (existing bullets trimmed in wording only to stay under the 8 KiB gate — it was 8190 of 8192 bytes, now 7956; no rule changed), specs/ui-primitives.md (draft), and an implemented Agent Note.

Visual explanation

packages/ui/src/toggle/
  toggle.tokens.stylex.ts   the group + its palette theme
  surface.ts                base / sizes / rest / pressed / group / bar
  set.ts                    size + shape context (own file: no import cycle)
  toggle.tsx                Toggle          -> Base UI Toggle   (aria-pressed)
  toggle-group.tsx          ToggleGroup     -> Base UI ToggleGroup
  toolbar.tsx               Toolbar.{Root,Button,Group,Separator}
flowchart TD
  L["elevation ladder"] --> W["well: you can put something here"]
  L --> R["raised: you can press this"]
  W --> S["Switch / Checkbox off"]
  S --> I["on = ink (the well is taken)"]
  R --> G["ghost Button: no fill at all"]
  G --> T["Toggle off"]
  T --> P["on = the well (it is free)"]
Loading
Tabs strip  [ Board | List | Timeline ]   one control -> a track, one pill that slides
ToggleGroup  (Board) (List) (Timeline)    a value     -> no track, each member sinks
                ^on          ^on          two at once, which no pill can say

Before / after

Before After
Thirteen different fills mean "on" across twenty-two files One state pair: the well plus the label colour, in one token group
A pressed control is a <button> with aria-pressed and a bespoke cn(...) Toggle, with the size ladder, the pill shape, the ring and the disabled opacity
A formatting bar is nine tab stops on the way past a text selection One stop; the arrow keys walk it and step over what is disabled
The "Show" chips are a flex-wrap div of hand-styled buttons ToggleGroup multiple wrap, reporting every pressed member
A dead Radix toggle.tsx and an unused @radix-ui/react-toggle dependency Both removed

Test plan

  • pnpm --filter @lody/ui test: 21 files, 258 tests pass — 13 new in test/toggle.test.tsx (the button and its aria-pressed; controlled press; the rest/pressed class pair; the glyph box, including through Toolbar.Button render={…}; native disabled; size stated once on a set and overridden on a member; one-of and several-of; the set as one tab stop with arrow-key walking; the bar's role, its empty appearance, a toggle staying a toggle inside it, a disabled control stepped over, and the bar's line being Separator's turned ninety degrees) plus the board's token coverage.
  • pnpm --filter @lody/ui typecheck, root pnpm typecheck: clean.
  • NODE_ENV=development pnpm --filter @lody/components test: 445 files, 3306 tests pass.
  • pnpm lint: 0 errors (0 warnings on the new files). pnpm lint:i18n: clean after adding tasks.body.format.marks / .blocks to both locales. pnpm check:public-boundary, check:platform-boundaries, check:code-collab-imports: pass. pnpm run docs check: no errors.
  • Board read in Chromium at both palettes, values measured off the rendered nodes: pressed is rgb(232, 234, 237) / rgb(28, 28, 28) under shadow.inset; the bar paints rgba(0,0,0,0) with box-shadow: none, 8px between clusters and 4px inside one; its line is a 1px hairline stretched to the 28px row.
  • pnpm install --frozen-lockfile passes on the hand-edited lockfile.

Context handoff

Instructions for reviewing agents

  • Review focus: surface.ts (the ring composed onto the pressed edge, not replacing it), toggle-group.tsx (generic, so the ref is a prop), toolbar.tsx (Toolbar.Button unstyled; the separator reuses separatorStyles).
  • Decisions to challenge: on as the well rather than ink; a set with no track even when multiple is off; a Toolbar that draws nothing; omitting Toolbar.Input/Link; the AGENTS.md trims must not have changed a rule.
  • Plausible failures / evidence gaps: the two migrations are covered by type checks and the primitives' tests, not by driving the app. In the product's .dark theme --popover is hsl(220 54% 11.5%) and the well is hsl(0 0% 11%), so the "Show" chips lean on the label colour until that menu becomes Menu.

Authoring context

  • User goal / directives: keep building @lody/ui atoms on Base UI, strictly to the new design; per component: settle the tokens, write the primitive, add the board entry, then migrate the old callers. This round was a gap survey of what atoms are still missing.
  • Constraints / non-goals: @lody/ui depends only on React, Base UI and StyleX; packages/ui/AGENTS.md must stay strictly under 8192 bytes; one family per PR.
  • Risk-bearing decisions: three new public export paths; removing a production dependency from packages/components; hand-editing pnpm-lock.yaml rather than re-resolving (a full --lockfile-only run also re-resolves esbuild, better-auth and @stylexjs/unplugin for unrelated reasons — 137 lines for a 3-line removal).
  • Destructive or irreversible behavior: deletes packages/components/src/ui/toggle.tsx (zero in-repo callers before this PR).
  • Deliberately not done or tested: the other twenty aria-pressed files (most are selected rows or radios wearing a disguise, not toggles); the file viewer's action strip — its two toggles are the clearest case this family has, but moving two of eight 24px buttons puts two glyph sizes in one row, and the row cannot move together until its Save button's status-warning tone is decided (these rules give warning to an outcome, not to a control a person presses); the format bar's own popover surface; regenerating open-source-attributions.generated.ts (a 12,484-line diff of unrelated drift since 2026-09-04).
  • Unknowns / confidence: high for the primitives (tested, and read on the board in both palettes); medium for how the two migrated surfaces look in the running app, for the reason above.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T10:57:51.816193Z 147220f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

A control that stays pressed, a set of them answering to one value, and the
bar that holds them, sharing one `toggle` token group.

On is the well rather than ink. The rules give a stored state ink, and ink is
what a control that already sits in a well becomes when it is on — a Switch's
off state occupies the well, so on has to leave it. A toggle rests on nothing,
so the well is free, and sinking into it is the plainest thing this system can
say about a button that went down and stayed. It also keeps a bar of eight from
reading as eight primary buttons.

A set is not a Tabs strip with its track removed: a strip picks what you see,
so it is one control with one pill sliding across it, while a set stores what
is on and two of its members can be pressed at once. A bar draws nothing at all
— not even the line a table draws — and exists to make a row of controls one
tab stop the arrow keys walk.

Migrates the task body's format bar and the tasks view's "Show" chips, and
deletes the dead Radix `toggle.tsx` and its dependency.

Model: claude-opus-5[1m]
@wibus-wee
wibus-wee force-pushed the feat/ui-toggle-toolbar branch from 196109a to 147220f Compare September 15, 2026 10:53
@wibus-wee
wibus-wee changed the base branch from feat/ui-icon-set to feat/ui-v2 September 15, 2026 10:54
@wibus-wee
wibus-wee merged commit ce0a9b0 into feat/ui-v2 Sep 15, 2026
3 of 4 checks passed
@wibus-wee
wibus-wee deleted the feat/ui-toggle-toolbar branch September 15, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant