feat(mantle): unified size prop for the button family + size/intent decision record - #1315
Conversation
🦋 Changeset detectedLatest commit: df57b89 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🚀 Deploy PreviewsUpdated 2026-07-14 01:53 UTC
|
There was a problem hiding this comment.
Pull request overview
Adds a unified, shared size scale across the button family and documents the associated design decisions (including the later priority → intent redesign plan), updating both implementation and docs in Mantle.
Changes:
- Introduces
ButtonSize(xs | sm | md | lg | xl) and applies it toButton,IconButton, andSplitButton.Root(withSplitButtonenforcing a single size across both halves via context). - Updates styling/DOM attributes so size affects box geometry (not typography), and is intentionally ignored for
Buttonwithappearance="link"(nodata-sizeemitted). - Adds/updates unit tests and documentation pages to cover the new size behavior and API surface.
Conventions pass: Confirmed class composition uses cx, size is modeled as a shared exported type, and SplitButton prevents mixed-size composites by omitting size from part props.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/mantle/src/components/split-button/split-button.tsx | Adds size on SplitButton.Root and shares it via context to both halves. |
| packages/mantle/src/components/split-button/split-button.test.tsx | Adds coverage ensuring Root size drives both halves and parts reject size at the type level. |
| packages/mantle/src/components/button/sizes.ts | Introduces shared ButtonSize type and documents the unified scale. |
| packages/mantle/src/components/button/index.ts | Re-exports ButtonSize from the @ngrok/mantle/button entrypoint. |
| packages/mantle/src/components/button/icon-button.tsx | Extends IconButton size variants to lg/xl and defaults size to md. |
| packages/mantle/src/components/button/icon-button.test.tsx | Adds IconButton size coverage, including asChild forwarding. |
| packages/mantle/src/components/button/button.tsx | Adds size variant, applies size-driven height/padding (excluding link), and generalizes icon-side padding per size. |
| packages/mantle/src/components/button/button.test.tsx | Adds Button size coverage, including link non-effect and override precedence. |
| decisions/2026-07-13-button-size-and-intent-api.md | Records the size and intent decisions, constraints, and migration plan. |
| apps/www/app/docs/components/actions/split-button.mdx | Documents SplitButton sizing and updates prop tables accordingly. |
| apps/www/app/docs/components/actions/icon-button.mdx | Documents IconButton’s expanded size scale and examples. |
| apps/www/app/docs/components/actions/button.mdx | Documents Button sizing behavior, including appearance="link" exclusion. |
| .changeset/button-family-size-prop.md | Publishes the API addition via changeset. |
| - **Phase 1 (`size`)**: implemented alongside this record; ships in the next `@ngrok/mantle` minor | ||
| (decoupled from the intent redesign so consumers can adopt `size` and `CenteredLayout` first). | ||
| - **Phase 2 (`intent` redesign)**: accepted, not yet implemented; ships in a later minor with its | ||
| own changeset and migration prompt. |
One PR for the first two stages of the button refresh (prior art: #1105 — design direction kept, diff not salvaged; it was 151 commits stale and predates the docs IA reorg, optional-
type,data-slot, and the button test suites).1. Decision record (
decisions/2026-07-13-button-size-and-intent-api.md)sizescale. OneButtonSizescale shared byButtonandIconButton;SplitButton.Rootownssizefor its halves; typography and icon glyphs deliberately do not scale for now (explicit follow-ups).priority→intent. Button values becomeaccent | danger | neutral;appearanceandintentbecome required (no defaults) — replacing the POC's silent accent→neutral default flip with deterministic compile errors;subtlemust meet ≥ 4.5:1 contrast in both themes before shipping (POC's light theme fails: accent ≈ 3.6–4.1:1, danger ≈ 3.7–3.9:1 from the actual oklch tokens); Badge keepscolor; no deprecation aliases — hard cutover via a codemod-style migration prompt driven by the record's explicit priority → (intent, appearance) mapping table; toastPriority→ToastIntent;data-priority→data-intent.priority=occurrences across 336 files; ~176 un-annotated<Button>s that would have silently restyled under a default flip.2.
sizeprop implementation (patch, purely additive)xsh-6 px-2size-6smh-7 px-2.5size-7md(default)h-9 px-3size-9lgh-10 px-3.5size-10xlh-12 px-4size-12sizeapplies via compound variants gated tofilled | ghost | outlined—appearance="link"is untouched (no box classes, no typography change, nodata-size), structurally avoiding the POC'stext-sm font-mediumleak onto link buttons. Icon-side padding generalizes the existing −0.125rem rule per size. NewButtonSizetype exported from@ngrok/mantle/button.lg/xlon the shared scale; now always emitsdata-size(previously absent when the prop was omitted).sizeonRoot, driven through context to both halves; the partsOmitit (pinned by a@ts-expect-errortest), so mixed-height composites aren't expressible in the type system.Verification
lint,fmt:check,typecheck, and the full test suite pass (including the agent-surface drift test — no snapshot regeneration needed).Adversarially reviewed: a 480-case old-vs-new render matrix (appearance × priority × isLoading × icon placement × className overrides) confirmed rendering is identical at the default
md— no visual change for any existing consumer.New tests pin the size contract: per-size height + padding, link exclusion (box, typography,
data-size), icon padding (start/end/default), consumerclassNameoverrides still winning,asChildforwarding, and SplitButton plumbing across all five sizes.Docs: new "Common mistakes" section on the Button page — icon-only buttons must be
IconButton(accessiblelabel, square box), notButtonwith aniconand no children.