Changelog

Every notable change to Beaket UI, newest first.

@beaket/ui#

4.2.1#

Patch Changes#

  • #1170 368e504 Thanks @jihnma! - Describe the package as what it is: React + Tailwind components the CLI copies into your project, not only a CLI. The npm description dated from when the package was the CLI alone and was not updated when the registry joined it.

  • #1168 22dfe95 Thanks @jihnma! - DropdownMenu: a disabled SubTrigger now renders in fg-disabled ink and drops pointer affordance, like the disabled Item, CheckboxItem and RadioItem rows beside it.

    Root cause: Radix marks a disabled sub-trigger with data-disabled, but SubTrigger’s class list was the only menu row without the data-[disabled]:text-fg-disabled data-[disabled]:pointer-events-none pair, so a disabled submenu parent rendered in full ink and looked identical to an enabled one. No story rendered <DropdownMenu.SubTrigger disabled>, so neither the visual baselines nor the play tests could catch it.

  • #1174 70f4bc8 Thanks @jihnma! - Label and Field.Label: render the label of a disabled control in fg-disabled.

    DESIGN.md’s Dashed-Disabled Rule gives a disabled control fg-disabled text, and Quiet Ink names “disabled labels” as one of that token’s jobs, but Label and Field.Label set only text-fg and had no disabled variant. A disabled Checkbox, Radio or Switch therefore sat next to a label in full ink that still read as live. Label now dims when it follows a disabled peer control (Checkbox, Radio and Switch all carry peer) or wraps a disabled control. Field.Label dims when its Field’s control is :disabled or carries data-disabled (Radix roots such as RadioGroup). No props change.

  • #1172 d00458f Thanks @jihnma! - NavigationProgress now holds a full, static bar under prefers-reduced-motion: reduce. Root cause: the global reduced-motion rule collapses the sweep to a single 0.01ms run with no fill mode, so the bar fell back to its resting w-1/3 at the left of the track and read as “a third done” rather than indeterminate. The bar now adds motion-reduce:w-full motion-reduce:animate-none.

4.2.0#

Minor Changes#

  • #996 04e10b2 Thanks @jihnma! - Breaking change: rebuild all five palettes in OKLCH, as ten separately designed schemes.

    Light and dark are no longer one palette inverted. Every ramp is built on an even OKLab lightness step — only Solace was ever checked for progressivity, and the other four were violating it badly, with jumps of 0.17–0.20 at tone-3→4 against a 0.12 ceiling, which is why their mid-greys collapsed into each other. Nothing in the set reaches #ffffff or #000000 at either end.

    Each palette carries its own page, and they are spread on purpose. Porcelain #fdfdfe is the starkest ground; Solace #fdfcfa the near-white; Marigold #faf9fc a bright panel with a trace of its own violet; Eucalyptus #f4f7f9 a soft canvas that was never pure white; Tobacco #f6f3f0 an aged stock. Dark grounds are five different rooms rather than one: a slate night #0f171e, a neutral black [#151617](https://github.com/beaket/ui/issues/151617), a warm #1b150e, a violet-black #18141f, and a grey [#222527](https://github.com/beaket/ui/issues/222527) that deliberately sits well above black.

    A ramp may travel in hue as well as lightness. Solace runs warm paper under cool ink. That cast is interpolated through OKLab a/b rather than by rotating hue, because rotating drags the mid-tones around the colour wheel and straight through green.

    Signal hue follows convention; its treatment follows the palette. Green has to read as green, so the hue is not the palette’s to choose — but its chroma, lightness tier and temperature are. Tobacco is the quietest set and Marigold the loudest, measured rather than asserted.

    Role lightness is tiered on purpose. Hue cannot carry status separation alone: tritanopia collapses blue against green, and protanopia and deuteranopia collapse red against green — and against brown, which is why a warm accent could not coexist with a red danger. Each role sits on its own lightness step, and every palette clears the project’s colour-vision floors.

    Accent is never quieter than danger. The interaction colour marks what you can do; the destructive one marks a rare path. Tobacco’s accent takes the red its reference actually carries, with danger stepping down to a deeper oxblood.

    Migration. Re-run npx @beaket/ui theme <name> to pick up the palette block. Any consumer that hard-coded a palette value rather than reading a semantic token needs to re-read it; the 69 semantic names are unchanged.

  • #1010 edc2485 Thanks @jihnma! - Breaking change: return Solace’s info signal to the blue band, held apart from the accent by step rather than by hue.

    The regrade moved --signal-info to a plum at hue 315° and recorded the reason in DESIGN.md: “Plum is deliberately not a blue: the accent already owns the blue band, and an informational blue beside it would read as actionable.” The diagnosis was right and the remedy was not.

    The old blue’s problem was step, not hue. Solace light’s previous info #53628f measured L* 0.503, C 0.074, h 269.5 against an accent at L* 0.501, C 0.195, h 263.9 — the same lightness and effectively the same hue, separated by chroma alone. That is why it cleared the project’s colour-vision floor by only 0.0485 against a 0.04 minimum, and why it read as a duller version of the thing you can click rather than a different kind of mark.

    Rotating 45° into plum bought margin by abandoning the convention. It also made Solace the only palette whose info left the blue band: Porcelain, Eucalyptus and Tobacco all ship a blue info, and Porcelain does it beside a blue accent at 0.0407 — a thinner margin than the Solace blue that was replaced. The stated rule was never held system-wide.

    --signal-info is now [#275085](https://github.com/beaket/ui/issues/275085) light and #5f8bc6 dark: the regrade’s own lightness and chroma tiers for the role, unchanged (L* 0.430 / 0.630, C 0.100), with only the hue moved from 315° to 256°. Separation from the accent is now carried by a full lightness step and roughly half the chroma, which is the relationship Eucalyptus already ships between its blue accent and blue info — and it measures 0.0900 light and 0.0743 dark, better than both the old blue and Porcelain’s shipping pair. The role’s display name follows the value from Plum to Prussian Blue.

    Migration. Re-run npx @beaket/ui theme solace to pick up the palette block. Only Solace changes; the other four palettes are untouched. Any consumer that hard-coded #643d75 or #a076b3 rather than reading --color-info-* needs to re-read it.

  • #996 04e10b2 Thanks @jihnma! - Breaking change: retune the surface, type, and instrument layers so a drawn edge means “you can act on this”.

    Measured on the Storybook Overview, 21.7% of rendered elements carried a border while ink fill covered 0.46% of the page — the weight was never the black, it was the lines. Containers now read by surface and drawn shade; strokes are rationed to instruments.

    The page is now painted. --color-bg was defined and never applied — no body rule anywhere in the token layer or the CLI’s injected block. That was invisible while --color-bg was near-white and the host’s default page was white, and it makes every surface claim in this release conditional, so semantic.css now sets body { background-color; color } as an element selector any consumer rule overrides.

    Surfaces. Solace’s light ladder landed raised surfaces 1.02:1 from the page — inside the range of its own hover fills — so every container had to draw a border simply to be seen. The direction was never the problem; the step size was. Ladder steps are now large enough to read on their own (1.08:1 and 1.16:1 in Solace light, 1.13:1 and 1.28:1 in dark), and --shadow-size goes to 2px so the drawn shade, not a stroke, is what says “raised”. A ladder step means separation from the page; which way it travels is a per-theme decision, and half the light schemes now step darker than their page rather than climbing toward white. The palette values themselves are set by the accompanying regrade.

    Body ink. --color-fg moves from the ramp’s deepest step to --tone-10, taking body text from 17.7:1 to 15.2:1 against the page. The deepest step measured well over 2x the AAA threshold and read as a high-contrast accessibility mode rather than a printed page. --color-border-strong and --color-bg-emphasis stay deepest, so emphasis still wins; the One Ink, Three Pressures Rule now covers type as well as stroke, and --tone-10 leaves the reserved set (28 functional palette values, not 27, with only --tone-8 and --tone-9 held back).

    Type. --text-xs 11px → 12px and --text-sm 13px → 14px, with --text-xs--line-height and --text-sm--line-height pinned at 1.45 / 1.55 (Tailwind derives leading from its own scale, so overriding size alone left 14px text on a 1.43 ratio). Hangul packs two or three jamo into one em and kanji can carry a dozen strokes; both collapse below 12px where Latin does not.

    CJK. --font-sans gains CJK families — none of the Latin entries carry Hangul or Kana, so resolution was uncontrolled OS fallback that differed per platform. One flat list is not enough: fallback runs per codepoint and a Korean face such as Apple SD Gothic Neo also covers the CJK ideograph block, so whichever script is named first captures every ideograph and the other renders in the wrong national glyph forms. @layer base therefore re-declares font-family per :lang(ko|ja|zh|zh-Hant) — re-declares rather than swaps a variable, because preflight sets font-family once on html and descendants inherit the already-substituted value. :lang(zh-Hant) cannot infer the script from a region-only tag, so zh-TW, zh-HK and zh-MO are named alongside it — otherwise the Simplified rule stays active and Traditional text renders in Simplified glyph forms. :lang(ko) also sets word-break: keep-all, without which Korean wraps mid-eojeol (관리|하려면). All of this requires the host to set lang on the document. Alert.Title drops tracking-tight, which tightened glyphs that are already full-width. A new Design/CJK story renders Korean and Japanese screens so the floor has a visual baseline — nothing else in the set contained a single Hangul or Kana glyph.

    Instruments. Checkbox and Radio move from a 24px chassis to 16px, and Switch defaults to sm (28×16). Hit expanders are re-measured from the padding box, so the inset moves to -15px and every control still clears 44×44.

    Containers. Card, the DataTable container, the Table wrapper and Alert drop their resting borders. Alert becomes one 2px role rule down its leading edge. Table row seams move to the new --color-border-subtle (69th semantic name) and data cells can wrap again.

    Fixes. Fused-strip hit expanders overlapped their neighbours — elementFromPoint 2px inside a Navigation cell returned the adjacent one, and ~8px of every Tabs cell activated the wrong tab. Horizontal expansion is removed inside a strip and kept on its outer edges — keyed off the <li> in Navigation, where every link is alone in its own list item and first:/last: had matched all of them. A disabled Switch rendered track and thumb at the same grey (1.00:1) — unchecked, both landed on border-muted; the thumb now draws in fg-disabled, the disabled ink Radio’s indicator already uses, which clears both disabled track values (worst of the ten schemes: 1.79:1 unchecked, 3.34:1 checked). Taking it to bg-disabled instead would have moved the collapse onto the checked track rather than removed it — that track is bg-disabled too. Card.Title loses leading-none, which clipped its own descenders. A read-only Input now sits flush with the page instead of being materially identical to a writable one. ring-0 is removed from the Switch thumb — a live zero shadow in a system that refuses rings. A read-only Textarea gains the same treatment as Input, which had been split. Stacked bare instruments in the Checkbox, Radio and Switch stories move to a 44px pitch: a 16px chassis reserves a 44px target, so a tighter stack made the lower third of each control belong to its neighbour.

    Migration. Re-run npx @beaket/ui theme <name> to pick up the palette and foundation block. Consumers who copied components earlier keep their files; re-run add for the component changes. Anything that pinned a 24px checkbox or radio in its own layout, or relied on Switch defaulting to md, needs the size passed explicitly. Set lang on your document for the Korean line-breaking rule to apply.

Patch Changes#

  • #1020 3092754 Thanks @jihnma! - Breadcrumb: widen the gap around the separator from 4px to 8px.

    At gap-1 the separator sat 4px from the labels on both sides, which reads as one crowded string rather than a trail. DESIGN.md spends 4px on icon gaps and 8px on control gaps; a separator standing between two labels belongs to the latter. Breadcrumb.List and Breadcrumb.Item both move to gap-2, so the spacing is symmetric whether the separator is composed inside the item (as the shipped examples do) or as a sibling in the list.

  • #1011 a3b7a81 Thanks @jihnma! - Stop treating a commented-out @import "tailwindcss" as a real Tailwind import.

    init picks your Tailwind entry by looking for that import, but it tested the raw file while the setup check tested the file with comments stripped. The two disagreed, so a stylesheet whose only @import "tailwindcss" sat inside a /* */ comment could be chosen as the theme target and then reported as not importing Tailwind. Both paths now share the comment-aware check.

    Internal tidying with no visible change: one BOM-tolerant JSON reader instead of four, one package.json read for the Next.js check instead of two, and the component-level up-to-date/outdated status has been dropped from compareComponent — diff reports per-file status and never read it.

4.1.1#

Patch Changes#

  • #991 7cc7c5c Thanks @jihnma! - Polish sweep across the component set: one motion grammar, one focus grammar, and the removal of styling that never reached the browser.

    Classes that produced no CSS. tw-animate-css was never a dependency of this repo, so every animate-in / fade-* / zoom-* / slide-* class copied into Dialog, Sheet, Tooltip and DropdownMenu emitted nothing — overlays have always appeared instantly, which is what DESIGN.md already documents (“no entrance choreography for ordinary controls”). Those classes are gone, along with Sheet’s now-empty sideAnimations map. Consumers copying these files into a project that does have tw-animate-css installed lose an accidental fade.

    Radix height caps, written in Tailwind 3 syntax. max-h-radix-dropdown-menu-content-available-height and max-h-radix-select-content-available-height are not Tailwind 4 class names and generated no rule, so a long menu or select panel could run off the bottom of the viewport instead of scrolling. Both now use the v4 arbitrary-property form and measure correctly (max-height: 792px in a 885px viewport). DropdownMenu.SubContent gains the same cap. The two origin-radix-* classes were equally dead and, with no entrance animation to originate, are removed rather than repaired.

    Select.Trigger’s size prop did nothing. It set data-size and no rule consumed it; the trigger also carried no height at all, so it rendered 36.57px beside a 36px Input. size now selects from the documented height ladder — h-9 (36px) beside Input and a default Button, h-8 (32px) beside a small one.

    Sheet’s gap never applied. gap-4 sat on a display: block element, which is why Sheet.Header carried an mb-4 to compensate while nothing else in the panel was spaced. Sheet now stacks with flex flex-col gap-4 at 24px padding, and the header’s compensating margin is removed — measured parity with Dialog: 24px padding, 16px between sections, 32px before the footer. (Dialog’s own grid is deliberately not copied here: a left/right sheet is h-full, and a grid taller than its content stretches every row — it put the header, body and footer in thirds of the viewport.)

    One motion grammar. Button’s transition list omitted background-color, so its hover fill snapped while its edge faded; Switch’s thumb, Pagination’s cells and the Dialog/Sheet close buttons ran at Tailwind’s default 150ms, and Slider’s thumb had no transition at all. All are 100ms now, leaving Progress as the system’s single 150ms — which is what DESIGN.md’s Motion section claims.

    One focus grammar. Table’s scroll region was the only focus outline in the system without outline-offset-2.

    Consistency. Navigation and Tabs now share body weight (DESIGN.md calls them the identical grammar; Tabs was 500 and Navigation 400). Select.Label and DropdownMenu.Label are one role and now one treatment — Label type, 11px/500 in muted ink. The menu leading rule is named once in dropdown-menu.tsx instead of hand-written in five places. DropdownMenu.Shortcut no longer recolors to accent on a highlighted row, where the label is meant to stay ink. Alert hoists what all five variants share into its base. Pagination’s ellipsis is a real ellipsis, and icons use size-4 throughout.

  • #993 c7bc59f Thanks @jihnma! - Skeleton breathes in ink instead of fading.

    animate-pulse cycles opacity from 1 to 0.5, which is the one thing this system never does: opacity is not a styling device here, and a fading placeholder dims whatever sits behind it rather than re-inking the block itself. It was also the last component contradicting the Drawn-Not-Lit rule.

    The block now moves one step along the neutral ramp and back — bg-hover ↔ bg-active — on a new --animate-skeleton-pulse token in the semantic layer. The cadence is unchanged at 2s, so only the channel moved; the placeholder reads the same, it just does it in ink. Both ends are semantic names, so the pulse follows a palette swap and reads correctly in dark mode.

    Because the keyframes drive background-color, a className background is painted over while the animation runs. Recolor by editing the keyframes — the file is yours. animate-none will not stop it: tailwind-merge cannot dedupe a custom animate-* utility against it, the same gotcha that stops shadow-none opting a variant out of shadow-offset-action. style={{ animation: "none" }} works, and prefers-reduced-motion: reduce already collapses it globally.

4.1.0#

Minor Changes#

  • #974 5b1be75 Thanks @jihnma! - Breaking change: Remove DataTable’s inaccessible onRowClick prop. Compose DataTable.Row with a cell-level link or button instead.

4.0.2#

Patch Changes#

  • #976 df7567e Thanks @jihnma! - Refine Solace’s neutral palette, status signals, and component borders.

4.0.1#

Patch Changes#

  • #969 9d7020f Thanks @jihnma! - Fix opt-in component smoke tests when run with tsx in projects using the automatic JSX runtime.

  • #971 373ff8b Thanks @jihnma! - Install Node type definitions for opt-in component smoke tests in projects that restrict ambient types.

4.0.0#

Major Changes#

  • #906 0d51ce4 Thanks @jihnma! - Breaking: Tooltip no longer mounts its own TooltipProvider

    Tooltip wrapped every instance in its own TooltipPrimitive.Provider, so a TooltipProvider placed around a group was shadowed by the inner one and its delayDuration could never reach a tooltip. The provider is now what it is in Radix — required, and the single place the delay is configured.

    - <Tooltip delayDuration={700}>…</Tooltip>
    - <Tooltip delayDuration={700}>…</Tooltip>
    + <TooltipProvider delayDuration={700}>
    +   <Tooltip>…</Tooltip>
    +   <Tooltip>…</Tooltip>
    + </TooltipProvider>

    Every Tooltip must now sit inside a TooltipProvider; Radix throws otherwise. Mount one near the root of your app. delayDuration stays available on a single Tooltip as a per-tooltip override of the provider’s value, and the provider still defaults to 0.

Minor Changes#

  • #954 025f5c8 Thanks @jihnma! - Add ui list [query] to discover registry components with descriptions before initializing a project. Support the same explicit registry-ref/latest options as add and diff. Enforce the documented TypeScript-only contract before init or add writes files; plain-JavaScript projects receive actionable guidance instead of silently acquiring unconfigured TSX.

  • #952 e7eede1 Thanks @jihnma! - Contain Table overflow with a keyboard-accessible scroll container. Expose Dialog and Sheet content props, sizing and close labels, Button loading labels, and DataTable/Pagination localization without editing installed source.

    Add Navigation longest-prefix matching with route-segment boundaries and a custom matching callback, preserving exact matching and explicit active overrides.

  • #960 b6ad115 Thanks @jihnma! - Add Field with composable Label, Control, Hint and Error parts plus optional render-function sugar. Share generated IDs and validation state across native and Radix controls, track mounted descriptions, and leave validation and layout policy to the application.

  • #951 8325a00 Thanks @jihnma! - Generate light, dark and system theme preferences with persistent override examples. Add managed-block hashes, edit warnings and read-only theme previews. Honor reduced motion globally, remove unused 7px spacing tokens in favor of existing Tailwind spacing, and clarify generated CSS ownership.

  • #950 3fbca06 Thanks @jihnma! - Mark hook-based components as client modules and export all compound parts by name for Next.js App Router server pages. Existing compound APIs remain supported inside client components.

  • #966 fd212f3 Thanks @jihnma! - feat(cli): add component smoke tests with add --with-tests

  • #957 f417f8b Thanks @jihnma! - Add Radix-backed Progress for determinate/indeterminate task progress and Slider for single/multiple values, keyboard input, native forms, RTL and vertical layouts. Both expose named and compound parts plus a short default composition. Tracks use neutral ink-and-border styling; slider thumbs preserve a 44px hit area and keyboard focus. Progress is not a measurement meter.

  • #949 44a97c6 Thanks @jihnma! - Pin registry downloads to the CLI release tag. Use —registry-ref to select a tag or commit, or —latest to explicitly resolve main to a commit. Missing release tags fail with explicit fallback guidance instead of silently fetching main.

    Record the registry ref, SHA-256 hash and CLI version for each installed file in beaket.ui.json, preserving existing baselines when local changes are skipped. Diff compares the recorded baseline, local source and target release, verifies baseline hashes, and separates local customizations from upstream changes. Exit codes are 0 for clean/local-only, 1 for mergeable updates, 2 for conflicts, and 3 for unknown baselines or errors. Comparing changes on both sides requires Git. Overwrite remains an explicit replacement with backups; automatic merging is deferred.

  • #955 aad0a34 Thanks @jihnma! - Add the canonical danger variant to Button, Badge and Alert without removing destructive, error or caution. Alert also accepts info, success and accent as aliases for note, tip and important. Existing styles, icons, callout titles and defaults are unchanged. No new color scale or renamed consumer API.

  • #963 dd9d9b4 Thanks @jihnma! - feat(cli): merge recorded local component edits during add —overwrite

Patch Changes#

  • #956 dcf571d Thanks @jihnma! - Document the copied-source update lifecycle and migration from native HTML. Explain version-pinned diffs, numbered backups, wrapper-first customization, theme ownership, named server-component imports, Select sentinels, native form submission, route matching and mobile table scrolling. Add a browser check for the documented Radix FormData behavior.

  • #947 5ff6181 Thanks @jihnma! - Type development warnings without requiring Node globals in browser projects, remove DataTable’s unused generic, and generate documented React requirements from the registry. Remove the deprecated baseUrl option from installation instructions.

  • #959 91f7def Thanks @jihnma! - Align Checkbox and Radio at 24px and give Switch distinct 16/20/24px heights with proportional thumbs. Preserve fixed chassis and inner-part press travel. Correct border-aware hit expanders to measure at least 44px rather than the previous 42px.

  • #953 56188fb Thanks @jihnma! - Make Radix-hidden backgrounds inert while Select and modal DropdownMenu overlays are open. This absorbs the aria-hidden-focus audit defect still present in @radix-ui/react-select 2.3.7 and @radix-ui/react-dropdown-menu 2.1.24. Native inert is required; existing inert regions are preserved. Background focusability is restored when Radix releases the last overlay’s hiding marker. No axe rule suppression is needed for these overlays.

  • #906 0d51ce4 Thanks @jihnma! - Remove className redeclarations from six component prop types

    AvatarProps, CheckboxProps, RadioGroupProps, RadioItemProps, SwitchProps, and TextareaProps each redeclared className?: string inside an interface already extending a props type that provides it. It reached no consumer and no docs table — the props generator excludes className. Types are unchanged. CheckboxProps, RadioGroupProps, and RadioItemProps are now type aliases, matching LabelProps.

    PaginationBaseProps and DataTableProps keep theirs: neither extends a DOM props type, so the declaration is the only source.

  • #906 0d51ce4 Thanks @jihnma! - alert, badge, and switch no longer depend on class-variance-authority

    Each used cva for a single flat variant map with no compound variants — a lookup keyed by one prop. alert.tsx already spelled the same lookup twice as a plain object (variantIcons, variantTitles) directly beneath the cva call. The three now use a plain object too, so ui add alert|badge|switch installs one package fewer. button and card keep cva, where two axes and compound variants earn it.

    Rendered class strings are unchanged. AlertProps and SwitchProps drop the VariantProps<…> they extended, which restated literal unions the interfaces already declare.

  • #947 5ff6181 Thanks @jihnma! - Preserve local files in numbered backups before overwriting, label overwritten files explicitly, and recommend reviewing diffs before discarding customizations. Resolve transitive registry dependencies when adding a component. Theme updates now ask before replacing tokens, and re-running init preserves existing configuration and directs theme switches to the theme command.

  • #961 8fac889 Thanks @jihnma! - Check bundler aliases and Tailwind v4/plugin setup during init instead of reporting an unverified project as ready. Offer a backed-up, confirmed Vite 8 native paths edit for simple configs; preserve toolchain files under —yes and print manual instructions for unsupported or conflicting configurations.

3.1.0#

Minor Changes#

  • #901 4628023 Thanks @jihnma! - Drop four dependencies and one migration path.

    • Label and Separator render a plain <label> and <div role="separator"> instead of wrapping @radix-ui/react-label / @radix-ui/react-separator. Same DOM, same data-slot/data-orientation/aria-orientation, two fewer packages for a project that installs them. Radix Label’s one behavior beyond the element — suppressing text selection when a label is double-clicked — is gone with it.
    • The CLI drops fs-extra and picocolors for node:fs/promises and node:util.styleText, which raises its floor to Node 22.13 (now declared in engines).
    • ui theme and ui add no longer recognise a theme block written before the /* beaket:theme:start */ markers shipped (April 2026, #309). An unmarked block is now left alone and the current theme is appended after it; re-run npx @beaket/ui init if your CSS still carries one.

3.0.0#

Major Changes#

  • #890 22104a3 Thanks @jihnma! - Breaking: RadioGroup.Item, <Component>Props names, and closeWhen is gone

    Three breaking changes batched into one sign-off, because a major bump is a cost paid once (docs/git-rules.md).

    RadioItem → RadioGroup.Item#

    Radio was already a true compound component — it shares state through Radix’s context — and only the packaging disagreed with Select and Tabs.

    - import { RadioGroup, RadioItem } from "@/components/ui/radio";
    + import { RadioGroup } from "@/components/ui/radio";
    
      <RadioGroup defaultValue="a">
    -   <RadioItem value="a" />
    +   <RadioGroup.Item value="a" />
      </RadioGroup>

    RadioItemProps keeps its name and stays exported. data-slot="radio-item" is unchanged.

    Generic Props → <Component>Props, exported everywhere#

    Props lands in your codebase, where it is un-greppable and forces import { Props as ButtonProps }. Four exported names change:

    - import { Button, type Props } from "@/components/ui/button";
    + import { Button, type ButtonProps } from "@/components/ui/button";
    WasNow
    badge.tsx PropsBadgeProps
    button.tsx PropsButtonProps
    checkbox.tsx PropsCheckboxProps
    input.tsx PropsInputProps

    The rest is free: AvatarProps, DialogProps, SheetProps and TextareaProps were generic Props that nobody could import, and AlertProps, SelectTriggerProps, TooltipProps and TooltipProviderProps were named right but unexported. Exporting a props type is what lets you write a wrapper — the first thing anyone does with a copy-pasted component.

    closeWhen removed from Dialog and Sheet#

    - <Dialog open={open} onOpenChange={setOpen} closeWhen={result?.ok}>
    + <Dialog open={open} onOpenChange={setOpen}>

    Close it from the state you already own: useEffect(() => { if (result?.ok) setOpen(false); }, [result]).

    closeWhen was the only reason Dialog and Sheet hand-rolled controlled/uncontrolled state that Radix’s Root already implements. With it gone, open and onOpenChange pass straight to Radix, and the render-time ref write that served it disappears with it. The dev warning for open without onOpenChange is unchanged.

Minor Changes#

  • #876 b8015bb Thanks @jihnma! - Breadcrumb.Link and Navigation.Link accept asChild

    Both hardcoded a plain <a href>, so in Next.js or React Router a consumer had to either lose client-side navigation or rewrite the component — which, under Open Code, means forking the file forever. asChild renders the consumer’s own element instead:

    <Breadcrumb.Link asChild>
      <Link href="/docs">Docs</Link>
    </Breadcrumb.Link>

    Under asChild the component injects nothing: the child owns its tag, and Navigation.Link skips its press-travel wrapper the way Button skips its spinner. Both components now list @radix-ui/react-slot in the registry.

  • #887 005e77f Thanks @jihnma! - DataTable defers its global filter, and the render-time ref writes are gone

    F6 — typing in the search field re-ran filtering and re-rendered the whole table in the same commit. The input is now bound to the immediate value and the table receives a useDeferredValue of it: typing stays responsive and the expensive pass lags by design.

    F2 — data-table.tsx carried two hand-rolled “latest ref” writes during render (tableRef.current = table, onSelectionChangeRef.current = onSelectionChange) — a shape React tells you not to write and that React Compiler, which runs in your build, refuses to optimize past. useEffectEvent replaces both: it is the official answer for an Effect calling the freshest callback, and it closes over the freshest table as well, so the second ref disappears with the first.

    textarea.tsx had the same render-time write for its forwarded-ref merge. That is not an Effect callback, so useEffectEvent does not apply; the ref is simply gone and the merge depends on ref directly.

    data-table now declares "react": ">=19.2.0" in the registry (useEffectEvent); add warns if your project is below it. useDeferredValue is a React 18 API and needs no floor.

  • #877 a955418 Thanks @jihnma! - data-slot on every element DataTable renders

    data-table.tsx was the only component in the registry with zero data-slot attributes, so its entire DOM was unaddressable from outside — part of why it compensates with getRowClassName and a 20-prop interface.

    Hyrum’s Law says something becomes the contract whether we choose it or not. data-slot is the blessed observable: stable, named, free at runtime, and explicitly not the class names, which are ours to rewrite on every redesign.

    24 of the 28 rendered elements now carry a data-table-* slot — data-table, -toolbar, -search, -search-icon, -container, -table, -header, -header-row, -select-head, -head, -head-content, -sort-indicator, -sort-icon, -body, -row, -select-cell, -cell, -empty-row, -empty-cell, -empty, -footer, -summary.

    The other four are composed controls — Input, two Checkboxes and Pagination — which keep their own slots rather than being overwritten; they are addressable through the named container around each.

    Attributes only. No API change.

  • #885 8a6c2bc Thanks @jihnma! - Dialog.Trigger and Sheet.Trigger parts

    Dialog and Sheet accepted the trigger as a ReactNode prop and exposed no Trigger part, so the sugar was the API. A consumer who needed two triggers, or a conditional one, or a trigger that is not the first child, had nowhere to go — and a ReactNode prop is structurally invisible: nothing in the type says trigger must be focusable.

    <Dialog>
      <Dialog.Trigger>
        <Button>Open</Button>
      </Dialog.Trigger>
      <Dialog.Trigger>
        <Button variant="ghost">Also open</Button>
      </Dialog.Trigger>
      <Dialog.Title>…</Dialog.Title>
    </Dialog>

    Radix’s context was already mounted here; the wrapper simply did not expose the part. asChild defaults to true, matching Dialog.Close, so the natural nesting keeps the consumer’s own element.

    Additive: trigger keeps working unchanged and is now literally sugar that renders this part. A Trigger child is placed as a sibling of the portal rather than as content inside it, so triggers and content each land where they are legal.

  • #884 35d7666 Thanks @jihnma! - Button’s loading falls back to the form’s pending state

    React 19’s useFormStatus is documented as a design-system hook: a submit button nested in a <form> can read the form’s pending state directly, with no prop drilling and no wiring by the consumer. Until now every consumer wired loading by hand for the most common case there is.

    <form action={save}>
      <Button type="submit">Save</Button> {/* spins and disables on its own */}
    </form>

    loading remains the override — supplied, it wins, including loading={false}. The fallback applies only to a type="submit" button; an ordinary button is untouched, and under asChild the component still injects nothing at all.

    No new registry dependency: react-dom ships in lockstep with react, and adding it to a component’s dependencies would run npm install react-dom in your project.

  • #883 8510bab Thanks @jihnma! - add warns when your React is older than a component needs

    A registry component is copied into a project whose React version we do not control, and registry.json declared npm dependencies but never a React floor — so a component using a 19.2-only API would land in an 18.x project and fail at runtime with no warning.

    registry.json now carries a top-level "react" floor, and a component needing more can carry its own. add reads the React you actually have (node_modules/react/package.json, falling back to the declared range read as its minimum) and says so when the floor is unmet:

    ! tabs needs React >=19.2.0 — found 18.3.1.
      The files are still copied; they may fail at runtime until React is upgraded.

    A check, not an install. Putting "react" in a component’s dependencies would have run npm install react in your project — pulling React to latest as a side effect of add button — which is the opposite of declaring a minimum. The files are still written either way: you may be about to upgrade, and a copy-paste library has no business changing your React version. An unparseable floor, or a React version that cannot be determined, produces no warning rather than a guess.

  • #879 f5f23ce Thanks @jihnma! - Pagination is a flexible compound component: Pagination.Previous, .Item, .Ellipsis, .Next

    Pagination was one function with configuration props, a discriminated union with never guards, and hardcoded <a href>s — so the consumer’s router was unusable and the page-number algorithm was entangled with rendering.

    The root now holds page / totalPages / navigation mode in context and the parts read it, each taking asChild:

    <Pagination page={page} totalPages={10} buildPageUrl={(p) => `/page/${p}`}>
      <Pagination.Previous />
      <Pagination.Item page={1} />
      <Pagination.Ellipsis />
      <Pagination.Item page={10} asChild>
        <Link href="/page/10">10</Link>
      </Pagination.Item>
      <Pagination.Next />
    </Pagination>

    Additive: mode="link" / mode="button" keep working unchanged and are now sugar that lays the same parts out for you. PaginationBaseProps, PaginationLinkProps and PaginationButtonProps are exported alongside the new PaginationItemProps and PaginationEllipsisProps.

  • #880 75391da Thanks @jihnma! - Alert.Title and Alert.Description parts

    Alert took its title as a content prop — title?: string — which forced the props type to Omit<…, "title"> and could only ever be extended by another content prop: title invites titleIcon, then titleClassName, then renderTitle. A string title also cannot be reordered, wrapped, conditionally rendered or styled.

    <Alert variant="warning">
      <Alert.Title className="uppercase">
        Deploy blocked <Badge>3</Badge>
      </Alert.Title>
      <Alert.Description>Two required checks have not reported yet.</Alert.Description>
    </Alert>

    Namespacing, not context — the parts share no state, so a provider would be pure machinery. The variant icon stays on the root, where the variant lives.

    Additive: title keeps working as the sugar over the parts, including the variant-name default ("Warning", "Note", …), and bare children are still wrapped as the description. Parts must be direct children of Alert.

  • #876 b8015bb Thanks @jihnma! - Navigation takes a root value; Navigation.Link derives its own active state

    The consumer answered “is this the current page?” by hand on every single link. The root now holds that one answer and each link compares its own value:

    <Navigation value={pathname}>
      <Navigation.Link href="/docs" value="/docs">
        Docs
      </Navigation.Link>
    </Navigation>

    Additive: the root’s value is optional, an explicit active still overrides the derived state, and a Navigation.Link used with no root value behaves exactly as before. The context accessor deliberately does not throw when there is no provider — throwing would require more of existing callers.

  • #881 2f3064a Thanks @jihnma! - DataTable is a flexible compound over the TanStack instance

    DataTableProps exposes 20 configuration props — among them emptyMessage and emptyState; onRowClick, onRowMouseEnter, onRowMouseLeave; getRowClassName. That is the mechanical result of answering layout questions with props instead of with children, and by Ousterhout’s measure it makes DataTable a shallow logic component: real complexity behind a wide interface, so the consumer pays twice — once to learn it, again when the 21st need is not on the list.

    The root now builds the TanStack table object and hands it back; the parts read it:

    <DataTable columns={columns} data={data} searchable paginated>
      {(table) => (
        <>
          <DataTable.Toolbar searchPlaceholder="Filter people…" />
          <DataTable.Table>
            <DataTable.Head />
            <DataTable.Body>
              {table.getRowModel().rows.map((row) => (
                <DataTable.Row key={row.id} row={row} className={rowClass(row)} onMouseEnter={…} />
              ))}
            </DataTable.Body>
          </DataTable.Table>
          <DataTable.Pagination />
        </>
      )}
    </DataTable>

    emptyMessage/emptyState become DataTable.Empty; getRowClassName, onRowMouseEnter and onRowMouseLeave become ordinary props on your own <DataTable.Row>.

    Additive: all 20 props keep working and now lay out exactly these parts, so the sugar path and the composed path render the same DOM by construction. Removing any of the 20 is a separate breaking decision.

  • #875 77ea99f Thanks @jihnma! - ref now typechecks on Badge, Blockquote, Button, Dialog.Header/.Footer and Sheet.Header/.Footer

    These seven prop declarations extended React.HTMLAttributes / React.ButtonHTMLAttributes, which do not carry ref. React 19 moved ref into intrinsic element props, so React.ComponentProps<"button"> does. Passing a ref to any of the seven failed to typecheck for no reason anyone intended; they now extend React.ComponentProps<…> like the rest of the registry.

    Widened type only — nothing that compiled before stops compiling.

  • #886 352b6f5 Thanks @jihnma! - Tabs.Content takes keepMounted

    Radix unmounts inactive tab panels, so scroll position, uncommitted form input and any in-panel state are destroyed on every tab switch. forceMount was the only escape and it keeps the panel fully live — the opposite extreme.

    <Tabs.Content value="draft" keepMounted>
      <textarea /> {/* survives a trip to another tab */}
    </Tabs.Content>

    keepMounted is React 19.2’s <Activity mode="hidden">: state preserved, effects torn down, re-render deprioritized. Off by default — this adds a way out, it does not change what Tabs already does.

    Under keepMounted the panel is hidden with data-[state=inactive]:hidden rather than Radix’s hidden attribute, which forceMount disables — so an inactive panel leaves no empty box in the layout or the accessibility tree.

    Requires React >= 19.2, which the tabs entry in registry.json now declares; add warns if your project is below it.

Patch Changes#

  • #875 77ea99f Thanks @jihnma! - Avatar drops the module-level hydration guard

    avatar.tsx carried a let hydrated = false module global and a post-mount render gate to work around a React 19 SSR hydration mismatch (#291): Radix’s useIsHydrated, built on useSyncExternalStore, returned true during client hydration, so a cached image rendered <img> where the server had rendered the fallback <span>.

    Root cause is gone upstream. @radix-ui/react-avatar 1.2.6 no longer uses that mechanism — the image loading status is a plain useState("idle"), identical on the server and on the first client render. The guard cost every Avatar a useState, a useEffect, and one blank frame before the image appeared.

    A new SsrHydrationTest story renders the avatar with renderToString, hydrates it with a warmed image cache, and asserts React recovered from no errors — so a future Radix bump that reintroduces the mismatch fails loudly instead of silently.

  • #875 77ea99f Thanks @jihnma! - Dialog, Sheet, DropdownMenu, Table and Avatar attach their parts with one Object.assign

    These five files attached parts by post-hoc mutation (Dialog.Title = DialogTitle) while the other six compound components used a single Object.assign. One expression is the complete public surface, instead of a surface scattered down the file and dependent on statement order — and TypeScript’s expando-function support means a typo in Dialog.Titel = … quietly adds a property rather than failing.

    Identical public shape. Nothing to change when re-copying.

2.8.0#

Minor Changes#

  • #808 2e9f99a Thanks @jihnma! - Install the shared type, spacing, border, and radius foundation with semantic and palette tokens so copied components render with their intended scale and shapes.

Patch Changes#

  • #812 3d6db4d Thanks @jihnma! - Stop generating a $schema URL that does not resolve in new configuration files.

  • #810 aa9c1d8 Thanks @jihnma! - Handle dependency installation failures without crashing the add command.

  • #813 872eed2 Thanks @jihnma! - Detect component aliases and Tailwind CSS entry files from referenced TypeScript configs.

  • #814 73a7e07 Thanks @jihnma! - Show dependency installation progress before the add command begins installing packages.

2.7.0#

Minor Changes#

  • #774 6dce543 Thanks @jihnma! - Remove the non-functional --surface-brand and --shadow-size-active values from every built-in palette, reducing the authored theme contract from 32 to 30 values.

    • --surface-brand is removed because no semantic token or shipped component referenced it. No replacement is needed; use a semantic background role such as --color-bg-emphasis when styling a brand-emphasis surface.
    • --shadow-size-active is removed because active pressables intentionally drop their shadow and translate by 1px; all semantic shadow roles derive from --shadow-size.

    The dependency audit also identifies --tone-8 through --tone-10 as reserved rather than functional. They remain for compatibility with the public 12-step neutral ramp and future deep-ink roles; the other 27 palette values are all reachable from the semantic layer.

    Existing CSS produced by an older init remains valid because consumers own that CSS, but these two custom properties continue to have no effect. Run npx @beaket/ui theme to refresh the configured theme, or npx @beaket/ui theme --theme <preset> to switch presets and replace the managed block with the 30-value contract. Customized palettes can instead delete the two declarations manually.

  • #781 09d2097 Thanks @jihnma! - Add a dark Solace palette that follows prefers-color-scheme: dark, including complete generated documentation tokens and forced-dark Storybook support.

  • #782 b509d5f Thanks @jihnma! - Define and apply a system-wide accent hierarchy so keyboard focus, open ownership, hover intent, selection, persistent affordance, and content navigation no longer compete at equal weight.

    • Buttons and interactive cards are neutral at rest, reveal a thin accent edge on hover, and hold a grown edge only while they own an open overlay. Primary emphasis now comes entirely from its ink surface; the secondary button returns to neutral material.
    • Checkbox, Radio, Switch, Navigation, Tabs, and Pagination drop their standing accent edges. Choice controls reveal action on hover, while navigation and tabs reserve accent for the selected lens.
    • Writing fields retain their cap-off focus edge, Select retains its open-owner edge, and menu rows retain their inset active rule under an explicit channel-precedence policy.
    • Add dense Storybook coverage plus native and Radix-backed interaction checks for simultaneous focus, open, active-row, and selection states.

Patch Changes#

  • #711 21f1cd4 Thanks @renovate! - Migrate DataTable to the TanStack Table v9 feature and state APIs while preserving sorting, filtering, pagination, selection, and exported table types.

  • #780 5261301 Thanks @jihnma! - Re-cut the Solace semantic signals so small text, icons, borders, tints, and solid states remain distinguishable under simulated protanopia, deuteranopia, and tritanopia.

    • danger: #af5340 → #a44735
    • warning: #ce8042 → #d18b3f
    • success: #00896c → #3f8a55
    • info: #4c6bb6 → #53628f
    • info-alt: #008597 → #005f72, with the knockout switched to the paper endpoint
    • accent remains #2b5bff

    The browser-backed audit evaluates all 15 role pairs across six semantic forms and four vision modes using the Machado 2009 full-severity transforms and OKLab distance floors. Storybook now includes a dedicated small-mark comparison and reinforces semantic status examples with readable labels and distinct icon shapes.

  • #778 8d971e9 Thanks @jihnma! - Rebalance the Solace light neutral ramp into a monotonic OKLab progression while preserving its warm-paper and cool-ink endpoints.

    The source hex values produce these OKLab lightness values and adjacent differences (rounded to three decimals):

    ToneBefore LBefore ΔLAfter LAfter ΔL
    00.963—0.963—
    10.966-0.0030.9200.043
    20.9340.0320.8200.100
    30.8380.0960.7300.090
    40.6390.1990.6390.091
    50.6160.0230.5850.054
    60.5400.0760.5250.060
    70.4550.0850.4550.070
    80.3860.0700.3900.066
    90.3280.0580.3250.064
    100.2520.0750.2450.080
    110.1280.1240.1280.117

    The automated palette contract permits 0.04–0.12 OKLab L across the full ramp and tightens the functional tone-3 through tone-7 steps to 0.05–0.10. This keeps the paper and deepest-ink ends flexible while preventing functional roles from collapsing or jumping abruptly.

  • #773 51041c8 Thanks @jihnma! - Enforce browser-resolved contrast guarantees for every built-in theme palette. Correct role hover/active mixtures, knockout choices, and dark neutral values exposed by the new policy.

  • #777 eea28a5 Thanks @jihnma! - Restore visible depth between the Solace light page, raised, and overlay surfaces while preserving its restrained warm-paper character and existing contrast guarantees.

2.6.0#

Minor Changes#

  • #683 02bd7e9 Thanks @jihnma! - feat(cli): tell existing users when a component’s style has moved

    Copied components don’t auto-update, so a consumer on an older style had no way to know a component now differs from the registry — add only knew “a file is here” and asked to overwrite, learning nothing about whether their copy was actually out of sync.

    • diff command — npx @beaket/ui diff lists every installed component as matching the registry or differing from it; npx @beaket/ui diff <component> prints the line-by-line diff between your copy and the registry, then points you at add <component> --overwrite (or hand-merging if you’ve customized it). It never writes — you own the code. A difference may be an upstream restyle or your own edits; without installed-version tracking the CLI can’t tell them apart, so it frames it as a difference and leaves the choice to you.
    • Smarter add — an existing file is compared against the registry: files already matching are left untouched (no prompt), and only files that differ prompt to overwrite, with wording that says your copy is out of sync rather than just that a file exists. Skipped files point at diff to see what changed.

2.5.0#

Minor Changes#

  • #681 3655067 Thanks @jihnma! - Card is a surface, not a template.

    Reframed the Card around its material rather than an imposed structure. Its identity is now the drawn surface — square corners, a single-ink border, and the grey offset shade that marks a raised thing — and it imposes no header/body/footer: the root carries sensible padding and a column gap so you can pour any content straight in and it sits right. This is what pulls it away from reading like a Dialog, whose Cancel/confirm footer was the tell.

    • elevation prop (flat | shade | overlay, default shade) replaces the old shadow?: boolean. shade is the quiet grey offset that says “raised surface”; overlay lifts it onto bg-bg-overlay with the heavier offset; flat sits flush.
    • interactive prop — when the whole card is a link it earns the accent edge in place of the grey shade: thin at rest, grown on hover, dropped onto the edge when pressed. Pair with asChild to render a real <a> (the one place a surface carries the vivid voice).
    • Card.Section — an edge-to-edge helper that cancels the root padding so media and full-width rules reach the card’s edges.
    • Header / Title / Description / Action / Content / Footer remain as optional, padding-free layout helpers — never the required anatomy.

    Migration: replace <Card shadow> with <Card elevation="shade"> (now the default, so plain <Card> also lifts); a card with no shade is <Card elevation="flat">.

2.4.0#

Minor Changes#

  • #635 cff40cf Thanks @jihnma! - Accent becomes the interaction voice.

    The accent signal — the one vivid ink each theme reserves for “what you can act on” — now actually carries interaction across the system, resolving the long-standing “two blues” (accent vs. signal-info) in Solace:

    • Primary button: the ink fill gains a 1px accent-solid edge, with accent-warmed hover/active surfaces (previously a plain border-strong with no hover shift).
    • Secondary button: now an accent tint (accent-bg + accent-fg + accent-border) that deepens its border on hover, instead of a neutral grey surface.
    • Links & focus rings (--color-fg-link, --color-border-focus): repointed from signal-info to signal-accent, so signal-info returns to meaning only “info”. Solace’s primary focus is now a single blue (edge + ring) rather than two.

    Two new semantic tokens back the primary hover/active states — --color-bg-emphasis-hover and --color-bg-emphasis-active (accent-warmed mixes of bg-emphasis) — bringing the semantic layer to 66 names.

    Accessibility: because accent is now link/focus text (which needs 4.5:1 against the page), three palette accents were re-cut to clear that floor — Marigold (light), Porcelain (dark), and Tobacco (dark). The other themes already passed.

  • #636 fede846 Thanks @jihnma! - feat(theme): action shadow — a thin accent edge under what you can press

    The offset shadow splits into two voices. Surfaces (cards, overlays) keep the grey shade; pressables now carry the one vivid voice as a thin sharp edge.

    • New semantic tokens --shadow-offset-action / --shadow-offset-action-hover (68 shared names now: 62 color + 6 shadow) — drawn with --signal-accent
    • Button: rest = 1px accent edge → hover grows to 2px → active drops the button onto the edge (translate(1px,1px), shadow collapses); ghost/link/disabled stay flat, disabled keeps its dashed border
    • Solace palette thins --shadow-size 2px → 1px — softness of the paper, sharpness of the edge
  • #647 8729283 Thanks @jihnma! - feat(breadcrumb): a quiet trail in one ink, not a switcher

    A breadcrumb is a trail — a sentence read left to right — not the lens that navigation and tabs are, so it doesn’t join the fused-strip/glass-plate grammar. It stays in one ink: ancestor links now sit in muted ink (text-fg-muted) instead of standing accent, and the current page keeps full ink (text-fg font-medium). Pointing at a step darkens it from muted to full ink (hover:text-fg) rather than dropping the accent to ink as before, and the always-on underline is retired (no-underline) — the darkening is the affordance. The one accent mark is the keyboard focus ring; the vivid voice is kept for where you act, not spent on a standing row of links. No pressable edge (a link is not a key), no new tokens.

  • #649 3d36f73 Thanks @jihnma! - feat(button): primary wears a light rim that sheds on engage

    The primary button’s rest edge no longer doubles. Its border was the full accent (border-accent-solid) sitting directly over the full accent shadow — the same blue on both, so the two 1px lines merged into one thick 2px band on the bottom and right. The border now takes the lighter accent tone (border-accent-border), so at rest the light rim and the full-accent shadow read as two tones — depth, not a doubled line — with the strong voice living in the shadow where it grows and drops.

    On engage the rim sheds: hover:border-transparent (and data-[state=open] mirrors it) drops the rim into the ink fill so the accent consolidates into the one growing shadow rather than piling into a thick accent band. The border is added to the shared transition (transition-[box-shadow,translate,border-color]) so the rim fades with the shadow instead of snapping — which also smooths the hover border-color change on the other bordered variants. No new tokens; other variants and the rest/hover/press/held-open shadow grammar are unchanged.

  • #649 3d36f73 Thanks @jihnma! - feat(button): remove the mono prop

    The mono prop (monospace + wide tracking for CTA-style text) had no documented role in the Ink & Instrument vocabulary, was demonstrated only by an orphan MonoVariants composition the docs never rendered (plus a redundant single story), and was a Button-only flourish. Monospace lives on where it means something — code badges, numeric table cells — not as a per-button toggle.

    The mono prop is dropped from Button along with its Mono and MonoVariants stories. Consumers who want monospace CTA text can pass className="font-mono tracking-wide". Since components are copy-paste, existing copies are unaffected.

  • #649 3d36f73 Thanks @jihnma! - feat(button): remove the stark variant

    stark (a strong-bordered button that inverted to solid ink on hover) had no documented role in the Ink & Instrument vocabulary — it appeared nowhere in the design docs, was used only as a demo trigger in stories, and overlapped with outline (bordered neutral) and primary (solid ink). In a system where every mark is meant to be deliberate, an unarticulated variant is a cut, not a keeper.

    The variant union drops "stark"; the demo triggers that used it (dialog, sheet, dropdown-menu stories) move to outline. Since components are copy-paste, existing consumer copies are unaffected; new copies simply won’t include it.

  • #645 fc298f9 Thanks @jihnma! - feat(button): held-open triggers sustain the grown edge

    A Button acting as an overlay trigger (dropdown menu, popover) now holds its hover state while the overlay is open — the accent edge stays grown instead of springing back to rest. The trigger reads as the active owner of the menu it opened while remaining pressable: a lifted state, not a drop.

    The rule is variant-aware: data-[state=open]: mirrors each variant’s hover:, so edge-bearing variants grow and hold their edge, and ghost/link keep their surface tint / underline with no edge and no press-travel. Radix sets data-[state=open] only on asChild triggers, so the styles are inert on ordinary buttons; modal (dialog/sheet) triggers sit behind the scrim, so the held edge is effectively visible only for non-modal triggers like DropdownMenu.

  • #645 fc298f9 Thanks @jihnma! - feat(dropdown-menu): accent marks the highlighted row

    The highlighted/active menu row is no longer an ink stamp (full bg-emphasis slab). Instead the system’s one vivid voice — the accent — marks the row you’d activate: a faint accent-bg wash plus a 2px accent left-rule (the engaged-edge weight, matching a hovered button’s grown edge), with the ink text left at full density and the shortcut in accent-fg. Checkbox, radio, and sub-trigger rows adopt the same mark; the sub-trigger holds it while its submenu is open. Destructive rows swap accent → danger (danger-bg wash + danger rule).

    The menu panel and sub-menu panel now float on shadow-offset-overlay (the darker overlay shade) instead of the surface shadow-offset — a menu sits above cards, not among them.

  • #641 a13b651 Thanks @jihnma! - feat(input, textarea): cap-off focus — retire the ring, the field engages the action edge

    Writing fields are quiet at rest; focusing one is “cap-off”: the glowing focus ring is retired and a static accent edge (shadow-offset-action) appears under the field while engaged — no hover growth, no active drop, no transition. Invalid fields swap the edge to the new --shadow-offset-action-danger token (semantic tokens 68 → 69). The caret becomes the pen (caret-accent-solid) and selection uses the accent tint (selection:bg-accent-bg). Read-only fields gain dignity: the frame retreats to border-border-muted while the value stays full-ink, and focusing one shows the grey surface shadow-offset instead of the action edge — readable, not writable. Keyboard-nav rings on pressables (buttons, checkboxes, …) are unchanged.

  • #643 3dc0a0b Thanks @jihnma! - feat(navigation): the lens — navigation joins the instrument family

    Navigation was the last component on the legacy grey shadow ladder. It is now a fused hairline strip (cells share border-border-muted borders) carrying one static accent edge, and the current page is no longer stamped in ink — it sits under a glass lens plate: hairline top/left rim, ink bottom/right rim (ink gathers where every shadow in the system falls), filled with the faintest accent wash. The plate lies beneath the type, so the current label keeps full ink density. Pressing any other link travels its label 1px like an instrument key.

    Token changes (68 semantic names, was 69):

    • New: --color-accent-bg-subtle — accent-only faintest wash (8%), the lens fill
    • Removed: --shadow-offset-hover and --shadow-offset-active — the grey ladder had no users left; surfaces keep shadow-offset/-overlay, pressables keep the action edge

    Vertical layouts swap the fusion axis: flex-col [&>li+li]:ml-0 [&>li+li]:-mt-px on Navigation.List.

  • #637 e86c410 Thanks @jihnma! - feat(pagination): one instrument — fused strip with a single accent edge

    Pagination adopts the action shadow, but as one machine rather than a row of buttons: cells fuse into a single strip (shared borders, no gaps) carrying one static 1px accent edge.

    • Strip: new pagination-strip slot wraps all cells with shadow-offset-action
    • Press: the chassis stays still — the pressed key’s label travels 1px inside the frame while the cell darkens (active:bg-bg-active)
    • Current page: an ink-solid cell whose label stays held down 1px — the key locked at its landing point
    • Ellipsis becomes a bordered blank key; focus outline lifts above neighboring cell borders
  • #658 5b20afc Thanks @jihnma! - Select trigger becomes a field that opens.

    The select trigger is surfaced like a form field (input’s paper, input’s border) but #646 gave it the full Button edge grammar — a standing accent edge, a hover that grows — so in a real form it was the one control that behaved like a button. But the dropdown “trigger” is loud at rest only because it’s literally a <Button>; that rest-edge is Button-incidental, not trigger-essential.

    Select now stays quiet at rest among its field neighbors (no standing edge, no hover growth) and lifts the grown edge only while its menu is open — keeping the trigger-essence (data-[state=open]:shadow-offset-action-hover) and the keyboard focus ring. It’s the one pressable that carries a ring but no rest edge. Invalid and disabled are unchanged; the resting form now reads as one quiet material.

  • #646 4fee10c Thanks @jihnma! - feat(select): held-open trigger + accent marks the highlighted row

    The Select trigger is the first form control that is also an overlay trigger — press it and a menu opens, exactly like a Dropdown. It now takes the same held-open pressable grammar as a Button rather than a writing field’s cap-off: a thin accent edge (shadow-offset-action) at rest, grown on hover (shadow-offset-action-hover), held grown while the menu is open (data-[state=open]: — which Radix sets natively on SelectPrimitive.Trigger), dropped onto the edge on press (active: translate), and gone when disabled (disabled:shadow-none). Keyboard focus keeps the ring; invalid recolors the border and ring to danger while the pressable edge stays accent — role-agnostic, exactly as on a destructive Button. No new tokens.

    Select’s own menu joins the system too: highlighted rows drop the ink stamp (bg-emphasis slab) for the accent mark introduced on DropdownMenu in #645 — an accent-bg wash + a 2px accent left-rule (data-[highlighted]:shadow-[inset_2px_0_0_0_var(--color-accent-solid)]), ink text left at full density. The list panel now floats on shadow-offset-overlay (the darker overlay shade) instead of the surface shadow-offset.

  • #638 372c55f Thanks @jihnma! - feat(checkbox, switch, radio): instrument grammar — static accent edge on small controls

    Small controls adopt the action shadow as instruments: the chassis floats on a static 1px accent edge (shadow-offset-action, no hover growth, no drop), and press physics belong to the inner key instead — the checkbox indicator and the switch thumb travel 1px under the press. Hover feedback is a surface tint (bg-bg-hover unchecked, bg-bg-emphasis-hover checked); a checked radio gets no press affordance since it cannot be unchecked. Disabled removes the edge.

    Switch checked also moves from bg-success-solid to ink (bg-bg-emphasis), matching checkbox/radio — state is carried by thumb position + ink, and the success role returns to meaning outcomes, not “on”.

  • #630 fce6614 Thanks @jihnma! - New two-layer color token system + Solace theme.

    • Semantic layer (themes/semantic.css, new): the 64 names components use — --color-bg/-raised/-overlay/-input/-hover/-active/-disabled/-emphasis, --color-fg/-muted/-subtle/-disabled/-on-emphasis/-link, --color-border/-muted/-strong/-focus, six roles (danger, success, warning, info, info-alt, accent) × seven slots (-solid, -fg-on-solid, -solid-hover, -solid-active, -fg, -bg, -border), and --shadow-offset/-hover/-active/-overlay. Authored once, shared verbatim by every theme.
    • Palette layer: each theme now authors only 32 values — --surface-0..2, --surface-brand, --tone-0..11, six --signal-* inks, six --signal-*-on knockouts, and shadow size/color. Porcelain, Tobacco, Marigold, and Eucalyptus are re-cut to the new contract (signal hue/chroma kept; lightness moved only as far as the 4.5:1 knockout and 3:1 page floors require).
    • New theme: Solace (light-only, now the default) — warm paper, cool ink, equal-weight signals, one vivid blue reserved for action.
    • All components migrated to the semantic names; the old material names (paper, ink, chrome, steel, frost, branch, signal-*, surface-N, shadow-offset-dark) are removed. The new --color-{role}-fg-on-solid knockouts close the amber-on-button legibility gap.
    • init/theme now inject semantic.css + the chosen palette together; --theme solace supported and default.
    • The prefers-contrast: more override was removed: --color-fg is now the ramp’s deepest ink, so maximum-contrast text is the default.

    Migration for consumers who customized tokens: re-run npx @beaket/ui theme to re-inject, then re-apply customizations against the 32 palette values (the semantic layer follows automatically). Class mapping highlights: bg-paper→bg-bg/bg-bg-raised/bg-bg-overlay/bg-bg-input by surface role, text-ink→text-fg, text-steel→text-fg-muted, border-chrome→border-border, bg-branch/bg-ink→bg-bg-emphasis (+text-fg-on-emphasis), outline-signal-blue→outline-border-focus, bg-signal-red→bg-danger-solid (+text-danger-fg-on-solid), shadow-offset-dark→shadow-offset-overlay.

  • #644 6adaf80 Thanks @jihnma! - feat(tabs): the lens — tabs join the navigation layer’s glass

    The tray retires. Tabs.List is now a fused hairline strip under one static accent edge (shadow-offset-action), and the selected tab sits under a glass lens plate instead of an ink stamp — hairline top/left rim, ink bottom/right rim, the faintest accent wash, plate beneath the type so the label keeps full ink density. Hover is a quiet surface tint; the switch itself is snappy (Radix activates a tab on press, so selecting it is instant by design). The unused shadow prop on Tabs.List is removed.

  • #650 49dfdcb Thanks @jihnma! - Each theme’s accent gets its own taste — no longer five shades of the same purple.

    The accent is a theme’s one vivid voice: it carries the secondary tint, links, focus ring, pressable edge, nav lens, and menu rule. Yet four of five palettes shipped the same default purple --signal-accent, so every theme but Solace read identically in its most characteristic moment. Each theme now answers in a hue drawn from its own world — light and dark both re-cut:

    • Porcelain → cobalt, the blue-and-white of 청화백자: #1e40af / #7fa0f0.
    • Tobacco → a warm taupe from its cigar-box world, in place of purple: #6c5240 / #bda488.
    • Eucalyptus → a teal-blue — the trust of blue, kept a leaf’s-width green so it stays clear of the pure-blue themes and its own cyan info-alt: #175a84 / #4fb0d6.
    • Solace (electric blue) and Marigold (violet) are unchanged — Marigold is now the family’s one violet, not one of four.

    Accessibility: because accent is link/focus text (--color-fg-link), which needs 4.5:1 against the page, every new value was gated against its theme’s tone-0 in both light and dark before selection. A pale celadon and Marigold’s namesake gold were both rejected for failing that floor on light paper. Regenerated docs/src/data/theme-tokens.json and docs/public/theme-init.js.

Patch Changes#

  • #649 3d36f73 Thanks @jihnma! - fix(button): ghost and link no longer leak the accent edge

    Ghost and link were meant to have no accent edge, but the base applied shadow-offset-action to every variant and their shadow-none override never won — twMerge can’t dedupe a custom shadow utility against shadow-none, so both classes survived and the custom edge won the cascade. Ghost showed a blue 1px edge at rest (and grew it on hover); link wore a button edge instead of reading as a link.

    The accent edge now lives on the variants that carry it (via a shared edge string appended to primary/destructive/outline/secondary/success/stark/warning) rather than on the base, so ghost and link simply omit it — kept out, not overridden. Ghost is now fully quiet (ink text, grey hover fill, no edge); link reads as a link (accent text, hover underline, no edge). Edged variants are unchanged. No token or palette change.

  • #649 3d36f73 Thanks @jihnma! - fix(button): outline hovers on the accent edge, not a grey fill

    The outline button was airy at rest but filled with a grey wash on hover (hover:bg-bg-hover) on top of its accent edge growing — two signals at once, and the fill read as muddy against the paper. Outline now drops the grey fill on hover and held-open and leans on the accent edge it already grows (from the base grammar); the press keeps a faint grey settle (active:bg-bg-active) to confirm the drop. Ghost is unchanged: with no edge, its grey fill is its only hover signal. No token or palette change — bg-hover is a shared, already-light token; this was a grammar fix, not a color one.

  • #613 3287d19 Thanks @jihnma! - fix(cli): add a 10s timeout to registry fetches and preserve the original error

    fetchRegistry and fetchComponent called fetch() with no AbortController, so a slow or unreachable GitHub raw CDN hung the CLI until the OS-level TCP timeout fired (potentially minutes). Both catch blocks also discarded the underlying error, collapsing ENOTFOUND / ECONNREFUSED / proxy failures into the generic “Make sure the repository is public.” message.

    Each request now aborts after 10s and the thrown message includes the real cause (or “request timed out after 10s” when the timeout fires), so npx @beaket/ui add button against a degraded network fails fast with an actionable reason.

  • #656 a98d75b Thanks @jihnma! - Switch now renders an invalid state.

    Switch was the only form control with no aria-invalid styling — a required switch marked invalid (e.g. an unaccepted terms toggle) showed no visual change, while its instrument siblings Checkbox and Radio both recolor. It now follows the same instrument grammar: aria-invalid recolors the border and focus ring to danger while the accent edge stays (role-agnostic, exactly as on Checkbox/Radio and the Select trigger).

  • #659 d4cfb2c Thanks @jihnma! - Switch off-state now responds to hover.

    The switch was the one instrument with no hover feedback when off — its checked track warms on hover and its siblings Checkbox and Radio tint their surface, but the off-channel sat inert. It now darkens one token step on hover (border-muted → border), completing the instrument grammar: hover tints the surface, press travels the key (the switch already travels its thumb, so no active tint is added).

2.3.1#

Patch Changes#

  • #409 785955d Thanks @jihnma! - Align Input props type with the rest of the library: extends React.ComponentProps<"input"> instead of React.InputHTMLAttributes<HTMLInputElement>, matching Textarea. Removes redundant className and ref declarations (already inherited). No runtime behavior change. Closes #333.

2.3.0#

Minor Changes#

  • #339 28ca093 Thanks @jihnma! - Re-tune prose body text color across all four themes to eliminate long-reading eye fatigue while preserving brutalist identity.

    Problem. graphite on frost/paper was running at ~18–20.4:1 contrast — above the research-backed 10–15:1 sweet spot (Bauer & Cavonius, Lin & Huang) and higher than every major design system (Apple 16.7:1, GitHub 15.5:1, Linear 16.2:1, Notion 10.6:1, Medium 14:1). Excess contrast induces halation and is especially punishing on dense Japanese kanji strokes. graphite and ink were also nearly identical (ΔE<1) so the two tokens had collapsed semantically.

    Changes per theme (light / dark):

    • Porcelain: graphite #030508 → #1e2229 / #e6eaee → #c4cad4; ink #080b10 → #0a0d14 / #dce0e6 → #e6eaee; iron #282b2f → #15191f / #b4bcc6 → #aab2bd; dark paper #06080c → #0d1117 (reduces dark-mode halation + re-adaptation fatigue).
    • Eucalyptus: graphite #0a1025 → #1e2638 / #e8ecf4 → #c6cdde; ink [#162036](https://github.com/beaket/ui/issues/162036) → #0e1628 / #dce2ec → #e8ecf4; iron [#243250](https://github.com/beaket/ui/issues/243250) → #151d30 / #b0bace → #a6aec4; dark paper #060a14 → #0e1320.
    • Marigold: graphite #0a0a0a → #1f1f1f / #f5f5f5 → #d8d8d8; ink [#121212](https://github.com/beaket/ui/issues/121212) → #0a0a0a / #ececec → #f5f5f5; iron [#262626](https://github.com/beaket/ui/issues/262626) → [#141414](https://github.com/beaket/ui/issues/141414) / #d0d0d0 → #c0c0c0; dark paper #0a0a0a → [#101010](https://github.com/beaket/ui/issues/101010) (conservative — preserves neon identity). Shadow references rebound (--shadow-offset now uses iron; -dark variant uses ink) to keep the “ink-black heavy shadow” semantic intact after the ink/graphite value swap.
    • Tobacco: graphite [#111110](https://github.com/beaket/ui/issues/111110) → #26231e / #eceae0 → #cfcabc; ink #1a1a18 → #14130f / #e2e0d6 → #eceae0; iron #312f2c → #1c1a16 / #c0bcb2 → #b4b0a6; dark paper #0c0b0a → #14130f.

    Token semantics clarified (via values, not rename):

    • graphite = prose body (tuned for sustained reading, 13–14:1 light, ~12:1 dark).
    • ink = UI primary / strong interactive (kept punchy — buttons, bg-ink tooltips, strong borders).
    • iron = structural dark accent / shadow (moved below graphite on the dark axis to preserve scale distance now that graphite has softened).

    Other:

    • --color-border-strong rebound from graphite to ink so softening prose text does not weaken the brutalist border language.
    • New @media (prefers-contrast: more) block in styles.css restores the original near-black graphite for users who request maximum contrast at the OS level (accessibility escape hatch).
  • #338 15835aa Thanks @jihnma! - Add resizable prop to Textarea so it can auto-grow with content while also letting the user drag the handle to make it taller. The manually dragged height becomes a floor — content can still push it larger but won’t shrink it below the user’s chosen size. Closes #332.

Patch Changes#

  • #335 618f25b Thanks @jihnma! - Fix CSS token duplication between src/css-variables.css and src/themes/porcelain.css. The two files had drifted out of sync (different hex values for graphite, ink, signal-blue, signal-green, surface layers) and css-variables.css lacked the dark-mode block. Removed src/css-variables.css and made src/themes/porcelain.css the single source of truth — src/styles.css now imports it directly.

2.2.0#

Minor Changes#

  • #307 2603a9c Thanks @jihnma! - Add ref prop support to Input component for direct DOM access (e.g. focus management)

  • #309 4d95dec Thanks @jihnma! - Add theme token sync to CLI. Running npx @beaket/ui add -o now also updates outdated CSS design tokens. New npx @beaket/ui theme command for standalone theme sync and switching.

2.1.2#

Patch Changes#

  • #294 46aa6ec Thanks @jihnma! - fix: Avatar.Image hydration mismatch in React 19 SSR

    Added hydration guard to Avatar.Image that defers rendering until after mount. This prevents React hydration error #418 caused by @radix-ui/react-use-is-hydrated returning true during client hydration in React 19, which made cached images render <img> while the server rendered <span> (fallback).

  • #296 7cc8d36 Thanks @jihnma! - fix(porcelain): adjust warning (signal-amber) color from brownish #d88810 to orange-yellow #e09800 for better visual distinction

2.1.1#

Patch Changes#

  • #287 5c50154 Thanks @jihnma! - Brighten porcelain warning color from olive-toned #b58a00 to warm vintage amber #d89018. Add signal-amber-text token across all themes for accessible text/icon usage on light surfaces.

2.1.0#

Minor Changes#

  • #257 09debe5 Thanks @jihnma! - Add dark mode variants for all themes (Porcelain, Tobacco, Marigold, Eucalyptus) with automatic OS detection via prefers-color-scheme and manual light/dark toggle in docs

  • #260 2a240ce Thanks @jihnma! - Add button mode to Pagination component and refactor DataTable to use it

    Pagination now supports mode="button" with an onPageChange callback for client-side pagination, in addition to the existing mode="link" (default) with buildPageUrl for SSR-friendly navigation.

    DataTable’s inline pagination has been replaced with the shared Pagination component, gaining ellipsis support for large page counts and ensuring visual consistency.

  • #236 fa45480 Thanks @jihnma! - Remove 5 composed components in favor of primitive-first philosophy

    Removed: ConfirmationDialog, ErrorPage, SidebarLayout, BlankSlate, PageHeader

    These components were fixed-layout compositions of existing primitives (Dialog, Button, Alert, etc.) that restricted user freedom. Users should compose their own layouts using the primitive components directly.

  • #256 98083c3 Thanks @jihnma! - Redesign signal colors across all themes for distinct personalities. Default green shifted from teal (167°) to emerald (150°) for clearer success recognition. Tobacco signals now use earthy pigments (indigo ink, brick, forest, ochre, plum, verdigris). Marigold pushed to max saturation with WCAG green fix (2.1:1 → ~5:1). Eucalyptus moved from Tailwind defaults to formal cobalt/crimson/jade/brass palette.

  • #249 168128d Thanks @jihnma! - Add surface layer tokens (surface-0, surface-1, surface-2) for visual depth between page, cards, and elevated elements. Surface containers (Card, Dialog, Sheet, Alert, Table) use bg-surface-1; floating overlays (DropdownMenu, Select popup) use bg-surface-2.

  • #255 7e14e13 Thanks @jihnma! - Add four themes: Porcelain (cold precision, teal accent), Tobacco (warm pampas cream, terracotta), Marigold (loud poster-print signals, ink-black shadows), Eucalyptus (enterprise titanium blue-gray). Each transforms neutrals, signal colors, and shadow geometry. New --theme CLI flag. Interactive demo at /themes.

Patch Changes#

  • #239 7b6b49f Thanks @jihnma! - Fix accessibility and consistency issues across 14 components

    • DataTable: add keyboard support (Enter/Space) on clickable rows, optimize selection useEffect
    • Button: add data-slot and default type=“button”
    • Alert: remove line-clamp-1 from title
    • NavigationProgress: add aria-valuetext
    • Sheet: add hideCloseButton prop for Dialog API parity
    • Input, Textarea, Select: normalize focus indicators to focus-visible:outline
    • Radio: align border weight with Checkbox (border-graphite)
    • Tooltip: remove shadow-offset from dark surface
    • Blockquote: use border-l-2 to match border-width-medium token
    • Checkbox, Radio, Switch, Dialog/Sheet close: expand touch targets to 44px
    • Select: add disabled:border-chrome to match other form controls
  • #252 9d4f3e0 Thanks @jihnma! - fix: add missing shadow prop to Avatar for API consistency with Card and Table

  • #250 e9c59f6 Thanks @jihnma! - fix: add missing gap-2 to Card.Footer so children are spaced correctly

  • #266 04be083 Thanks @jihnma! - Improve component prop documentation: extract compound component props (Card, Navigation, Tabs, Select, DropdownMenu), show enum literal values instead of “enum”, filter className noise, fix ReactNode types showing as “any”, and add inline option listings to JSDoc comments

  • #269 8d8c31c Thanks @jihnma! - Fix accessibility issues in DataTable, Pagination, and Input components

  • #240 5beb672 Thanks @jihnma! - Add focus-visible indicator to DataTable clickable rows for keyboard navigation

  • #264 7add468 Thanks @jihnma! - fix: resolve React 19 ref collision in Textarea and stabilize effect dependencies in DataTable, Dialog, Sheet

  • #253 cd1de9a Thanks @jihnma! - fix: increase Switch sm size from 12×28px to 16×32px for better visibility

  • #251 5d86371 Thanks @jihnma! - fix: increase default Table cell padding from px-1.5 py-1 to px-4 py-2/py-2.5 for better readability

2.0.0#

Major Changes#

  • #230 5dda5cf Thanks @jihnma! - BREAKING: Migrate CSS variables to Tailwind v4 @theme convention with --color-* prefix
    • CSS tokens moved from :root to @theme block with --color-* prefix
    • Components now use clean Tailwind utilities (bg-paper instead of bg-[var(--paper)])
    • Unified color token values with Beaket app (chrome, platinum, signal-blue)
    • Added --color-muted (#737373) for WCAG AA-compliant muted text (replaces aluminum for text)
    • Added --color-signal-red-text (#b91c1c) for accessible red text on light backgrounds
    • Accessibility improvements across all components:
      • BlankSlate: decorative icons marked aria-hidden
      • Navigation: default aria-label on nav landmark
      • Button: spinner wrapped with aria-live="polite"
      • Switch: disabled state uses border-dashed pattern instead of opacity
      • DataTable: search input has aria-label
      • Checkbox: indeterminate state with Minus icon and ARIA support

Minor Changes#

  • #232 18ebbb6 Thanks @jihnma! - Add 7 new components extracted from Beaket app
    • Skeleton: Loading placeholder with pulse animation
    • ViewToggle: Generic toggle button group for switching between views
    • NavigationProgress: Indeterminate progress bar for page navigation
    • PageHeader: Page header with title, count, and action slot
    • ErrorPage: Full-page error display with code, message, and action
    • SidebarLayout: Responsive layout with content and sidebar slots
    • ConfirmationDialog: Pre-built confirmation dialog with warning and loading state

Patch Changes#

  • #233 c71d810 Thanks @jihnma! - Fix accessibility and UX issues across 9 components
    • DataTable: add aria-sort on sortable headers and scope="col" for screen reader table navigation
    • ViewToggle: make label required for icon-only buttons (a11y)
    • Skeleton: add role="status" and aria-label for screen readers
    • Select: fix focus ring to use focus-visible: consistently
    • Badge: add missing data-slot="badge"
    • Avatar: add dev warning when alt prop is missing on Avatar.Image
    • Radio: fix broken disabled indicator dot color using group-disabled pattern
    • Navigation: add transition-shadow, active:shadow-offset-active, suppress shadow/hover when active
    • Switch: fix disabled+checked state showing green, improve thumb contrast when disabled

1.9.1#

Patch Changes#

  • #194 8c73b0b Thanks @jihnma! - Improve focus visibility for Switch and Tabs components by using outline-based focus indicators instead of subtle border color changes

  • #192 6c2e1cd Thanks @jihnma! - Replace hardcoded white with palette tokens in Input, Table, and Badge components

1.9.0#

Minor Changes#

  • #113 4e8f921 Thanks @jihnma! - Add support for installing multiple components at once: npx @beaket/ui add alert button label

  • #109 028aa68 Thanks @jihnma! - ### Breaking Changes

    • Table: Migrated to compound component pattern
      • Before: import { Table, TableBody, TableCell, ... } from "@beaket/ui"
      • After: import { Table } from "@beaket/ui" and use Table.Body, Table.Cell, etc.

    New Features#

    • Input: Added prefix and suffix props for icon support
    • Sheet: Added fullScreen prop for full-width mobile navigation
    • Button: Hover/active states now use CSS variables for easier customization
      • Added --signal-green-hover, --signal-green-active
      • Added --signal-red-hover, --signal-red-active
      • Added --signal-amber-hover, --signal-amber-active

Patch Changes#

  • #111 df61ccd Thanks @jihnma! - Refactor CSS variables to use single source of truth
    • Extract core CSS variables to src/css-variables.css
    • CLI now imports from generated file instead of hardcoding
    • Add pnpm sync:css script to sync variables to CLI

1.8.0#

Minor Changes#

  • 6460127: Add -y flag to init command for non-interactive mode

Patch Changes#

  • dca002d: Fix bun detection to use bun.lock instead of bun.lockb

1.7.0#

Minor Changes#

  • ed42b5d: Improve Switch and Textarea components

    Switch:

    • Flatter, more horizontal proportions (reduced height by ~40%)
    • Uniform 2px padding on all sides
    • Corrected thumb translate values for symmetric left/right states

    Textarea:

    • Add autoResize prop (default: true) for automatic height adjustment based on content
    • Unify focus styling with Input component (ring-2 instead of border-only)

1.6.0#

Minor Changes#

  • 1b61c60: Auto-detect component path based on tsconfig alias configuration

Patch Changes#

  • 6ffa8c6: Fix CLI init to include @theme block with shadow utilities for Tailwind CSS 4

1.5.1#

Patch Changes#

  • 1d6083e: Fix consistent form control borders and navigation active text
    • Standardize border color to —graphite for Checkbox, Select, Textarea
    • Remove meaningless hover border changes
    • Add text-inverse utility for navigation active state
    • Fix docs global styles conflicting with component styles

1.5.0#

Minor Changes#

  • 2fa9067: Add offset shadow system and new component variants

    • Button: Add offset shadow states, warning variant, mono prop
    • Badge: Add warning and code variants
    • Table: Add shadow prop, TableSectionHeader component
    • Card, Tabs: Add optional shadow prop
    • Dialog, Sheet, Dropdown, Tooltip: Add offset shadows
  • df80cc1: Add Blockquote, Breadcrumb, and Navigation components

    • Blockquote: Styled quotation with author attribution support
    • Breadcrumb: Compound component for navigation hierarchy
    • Navigation: Primary site navigation with offset shadow styling

1.4.0#

Minor Changes#

  • 1b26167: Add Avatar component for displaying user profile images with fallback support

  • 69a50f4: Add form components: Label, Textarea, Select, and Switch

    • Label: Form label with accessibility support via @radix-ui/react-label
    • Textarea: Multi-line text input with validation states
    • Select: Dropdown select with grouped options using compound pattern (Select.Trigger, Select.Content, Select.Item, etc.)
    • Switch: Toggle switch with size variants (sm, md, lg) via @radix-ui/react-switch
  • 0a6eb92: Add Phase 2 components: Tooltip, Separator, Card, Tabs, Sheet, and Alert

    • Tooltip: Popup that displays information on hover or focus
    • Separator: Visual divider for horizontal or vertical separation
    • Card: Container component with Header, Title, Description, Action, Content, and Footer sub-components
    • Tabs: Tab navigation component for switching between content panels
    • Sheet: Slide-out panel from any edge of the screen (left, right, top, bottom)
    • Alert: Callout component with semantic variants (note, tip, important, warning, caution)
  • 21e60c3: Add Table and DataTable components

    • Table: Semantic HTML table components (TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption)
    • DataTable: TanStack Table-based component with sorting, filtering, pagination, and row selection
    • Update Button to use design tokens
    • Update CLAUDE.md with library architecture and design philosophy
  • 2304fb0: Add Dialog component with compound pattern, controlled/uncontrolled modes, and Storybook tests

  • 4028bf6: Add DropdownMenu component with compound pattern, checkbox/radio items, submenus, and keyboard shortcuts support

  • 27f74ac: Add Pagination and BlankSlate components (Phase 3 migration)

    • Pagination: Server-side pagination with ellipsis support for SSR-friendly navigation
    • BlankSlate: Empty state component with preset icons and custom icon support

Patch Changes#

  • 86722d0: Make Tooltip self-contained by including TooltipProvider internally

1.3.0#

Minor Changes#

  • 495bc3c: Add Input component with brutalist design
  • 2633dec: Add Radio component for single-choice selection

1.2.0#

Minor Changes#

  • 11f945c: Add Badge component with 6 variants: default, secondary, success, error, info, outline

1.1.1#

Patch Changes#

  • 6916e4e: Add usage field to registry.json for component documentation

1.1.0#

Minor Changes#

  • 4901272: Add Checkbox component with Radix UI primitives

1.0.0#

Major Changes#

  • 5dfc775: Simplify CLI and component structure

    Breaking changes:

    • beaket.json now only requires components path (removed tailwind, aliases, paths.utils)
    • Components are now single files (e.g., button.tsx instead of button/button.tsx)
    • cn utility is now inlined in each component

    New beaket.json format:

    {
      "components": "src/components/ui"
    }

    Migration:

    1. Update beaket.json to new format
    2. Add CSS variables manually (see docs)
    3. Re-add components with --overwrite flag

Minor Changes#

  • 12e1451: Add overwrite prompt for existing files in CLI add command
    • Add --overwrite (-o) flag to force overwrite without prompting
    • Prompt user for confirmation when a file already exists
    • Show skipped files with instructions to use --overwrite
    • Improve progress display with checkmarks

0.1.8#

Patch Changes#

  • 6ecf976: Update CLI package description

0.1.7#

Patch Changes#

  • e30519c: Add JSDoc comments to Button component props

0.1.6#

Patch Changes#

  • e453371: docs: add JSDoc comment to Button component
  • e180d0f: docs: add JSDoc comment to Button props
  • 14c4706: docs: add JSDoc comment to Spinner component
  • 51771f8: Rename ButtonProps to Props internally
  • 26470e9: fix: trigger auto-changeset only on PR open and use PR title
  • 7b1fb93: chore: apply code formatting and cleanup

0.1.5#

Patch Changes#

  • 04f0200: Test changeset for verification