feat(ui): add ButtonGroup — a data-driven action bar#4339
Merged
Conversation
This was referenced Jun 5, 2026
Contributor
🔍 Visual review for your branch is published 🔍Here are the links to: |
ButtonGroup is a low-level layout primitive for arranging action buttons: alignment, gap, orientation, responsive stacking (viewport or container query), full-width-on-stack and reverseOnStack. It owns only arrangement — not footer/header chrome or button size. Also adds ButtonGroupSeparator (logical divider) and ButtonGroupOverflow (width-driven collapse into a "more" ellipsis menu, built on OverflowList), plus autodocs, an MDX page, and behaviors stories. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c4f4d80 to
6e507f3
Compare
Contributor
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
Contributor
📦 Alpha Package Version PublishedUse Use |
Contributor
✅ No breaking public API changesNo public exports were removed, renamed, or had existing props/types changed in a breaking way compared to Comparing |
Contributor
Coverage Report for packages/react
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||
rubenmoya
approved these changes
Jun 8, 2026
rubenmoya
left a comment
Contributor
There was a problem hiding this comment.
LGTM, just a minor comment
The hidden measurement copy is purely for layout sizing, so make it fully non-interactive and remove its children from the tab order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sauldom102
reviewed
Jun 8, 2026
Replace the children-based layout primitive with a typed, data-driven API addressing review feedback (@sauldom102): actions are passed as props, not arbitrary JSX, structurally guaranteeing invariants (≤1 primary). - primaryAction (solid, pinned) + secondaryActions (outline, or split buttons / inline separators / a single link) + otherActions (always in the "⋯" menu). - Secondaries shed into the "⋯" menu by width (useOverflowCalculation); splits and the primary are pinned and never collapse. - Responsive: stack into a full-width column below sm/md/container-md, with responsive size ({ base, md }) and a Dropdown↔MobileDropdown swap, all chosen in JS from the matching breakpoint so only one branch mounts. - Fold ButtonGroupOverflow into ButtonGroup (deleted); carry its inert measurement copy over, set via setAttribute since inert isn't a typed JSX prop in @types/react 18.3. - Rewrite stories + mdx to the prop API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose F0Button's critical (destructive/red) variant on action buttons, so authors can choose how guarded a destructive action is instead of the component enforcing one rule: - Guarded (default for permanent resources): critical in otherActions → behind the "⋯" menu, the extra click as a safety gate. - One-click (cheap/recoverable resources): a critical secondaryActions button renders inline in red with no extra click. critical is carried into the "⋯" menu when an inline critical secondary overflows. Documents the guard-by-default guideline in the mdx. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found by running Storybook and a comparative review against BaseHeader /
TimeLine/Actions / CardActions.
Bugs:
- Infinite render loop ("Maximum update depth exceeded"): the fresh
secondaryActions.filter() array changed useOverflowCalculation's callback
identity every render. Memoize it.
- Secondaries never measured when starting stacked: useOverflowCalculation's
ResizeObserver attached before the row DOM existed (usehooks-ts doesn't
re-observe when the ref element changes), so everything shed into the menu.
Split the row/stacked branches into separate keyed children so the overflow
hook mounts with its DOM present; keep the container-width observer on a
stable outer wrapper.
Review polish:
- useMediaQuery: explicit { initializeWithValue: false } (SSR-safe, matches the
repo-wide convention).
- Separator hairline: bg-f1-border-secondary (the house token) instead of the
unused bg-f1-background-secondary.
- Remove the dead `wrap` variant from variants.ts.
- Document why `id` is required (overflow identity), unlike index-keyed lists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merged
warcos-fact
added a commit
that referenced
this pull request
Jun 8, 2026
Replace the bespoke overflow/primary-secondary-other machinery in CardRowActions with a thin adapter over the data-driven ButtonGroup (#4339), which now owns the width-driven "⋯" overflow, the primary pinning, and the row/stacked layout. The card keeps its own concerns: stopping click propagation so actions don't trigger the row link, the stacked own-line footer hairline at the container breakpoint, and the confirm/reject icon variant (mapped to two pinned secondaries). The actions wrapper takes the remaining row space (flex-1) so ButtonGroup has a bound wider than its content to measure against; a shrink-to-fit container made it shed its tail into the menu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
ButtonGroup, a data-driven action bar for arranging a row of action buttons. Actions are passed as props, not children — which keeps the surface constrained and structurally guarantees invariants (e.g. at most one primary). This is the outcome of review feedback on the original children-based draft.primaryAction— a single solid button, pinned at the trailing edge (or a split button viatype: "split").secondaryActions— outline buttons, split buttons, or inline{ type: "separator" }hairlines (or a single link). Plain secondaries that don't fit shed into a "⋯" menu by width measurement; split buttons and the primary are pinned and never collapse.otherActions—DropdownItem[], always reachable through the "⋯" menu (supports separators andcritical).stack="sm" | "md" | "container-md"collapses into a full-width column (fullWidthOnStack,reverseOnStackto promote the primary to the top), with a responsivesize={{ base, md }}and an automaticDropdown↔MobileDropdownswap. The active branch is chosen in JS so only one mounts at a time (and the overflow machinery initializes with its DOM present).criticalrenders red. Guard it inotherActions(behind the ⋯) by default; mark an inline secondarycriticalfor a one-click delete when the resource is cheap to recreate.Notes
F0CardRowand is consistent with theprimaryAction/secondaryActions/otherActionstriplet already used byBaseHeaderandTimeLine.ButtonGroupOverflowintoButtonGroup(deleted) — width-driven overflow is now native viauseOverflowCalculation. No change to the public export barrel; there were no external consumers.Verification
Driven through Storybook across every pattern (Trailing, Split, Reflowing, width-overflow, critical) — which surfaced and fixed two bugs invisible to type-check/lint: an infinite render loop (unmemoized array into
useOverflowCalculation) and secondaries failing to measure when the group started stacked (observer attached before the row DOM existed). Type-check, lint, Chromatic, and the type build are green.🤖 Generated with Claude Code