Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Changelog

Release history by design-system version — new and changed components with doc links and commit refs — plus how updates reach you.

Last updated

Release history for the whole system, versioned by the design-system (registry) version; npm package versions are listed per entry. This page is generated from the repo's root CHANGELOG.md — the two can never drift (CI enforces it).

[0.9.1] — September 15, 2026

🧩 New components

  • Selection controls — Give ToggleGroup, Segmented, and pill Tabs a persistent semantic boundary, and use Segmented for the documentation preview's device modes. 6ec9d54

🛠 CLI & tooling

  • Verification and release — Run deterministic affected component tests once on pull requests and automate the authorized publish-and-deploy chain. 6ec9d54
  • Version PR checks — Use GitHub's native approval gate for the bot-created pull-request run so its PR quality result satisfies protected-main status checks without a duplicate dispatch. 22cc795
  • Version PR verification — Route generated version metadata through static and positive output checks without misclassifying provenance-only registry rewrites as component edits. ed2aa8f
  • Release automation — Restore a generated Version Packages PR so protected main needs no automated direct-push bypass. 3dd6a85

📦 npm

  • @vegastack/design0.4.1 (was 0.4.0).
  • The design-system registry (@vegastack/ui) bumps 0.9.0 → 0.9.1.

[0.9.0] — September 15, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.8.2 → 0.9.0.

🐛 Fixed

  • Toast — center the copy and controls vertically in single- and two-line notifications, give the labelled action the same quiet ghost treatment as dismissal, and complete the docs Scope and part API sections. docs · 31e915d

[0.8.2] — September 12, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.8.1 → 0.8.2.

🐛 Fixed

  • Dropzone docs — keep the three-state drag preview within 320px by giving its narrow layout an explicit zero-minimum grid track before the three-column breakpoint. docs · c371d09

[0.8.1] — September 12, 2026

🛠 CLI & tooling

  • Geometry diagnostics — name the elements and computed paint geometry that own a horizontal overflow, so a cross-engine failure identifies its cause instead of reporting only the page width. 9390ce4

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.8.0 → 0.8.1.

[0.8.0] — September 12, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.7.5 → 0.8.0.

🐛 Fixed

  • Keep Dropzone drag feedback inside narrow WebKit viewports. The drag-state stroke now sits one stroke-width inside the surface and uses explicit border-box sizing. This avoids WebKit counting the stroke's two edges as horizontal scroll overflow at 320px while preserving the design system's independent focus-visible outline. docs · f4fec28

[0.7.5] — September 11, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.7.4 → 0.7.5.

🐛 Fixed

  • Dropzone — paint drag feedback with an inset overlay that cannot expand WebKit scrollable overflow. e5ba405

[0.7.4] — September 11, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.7.3 → 0.7.4.

🐛 Fixed

  • VideoPlayer WebKit contract — separate pointer-leave hiding from the intentional focus-retention behavior in the browser test. e45defd

[0.7.3] — September 11, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.7.2 → 0.7.3.

🐛 Fixed

  • WebKit release contracts — keep Dropzone drag outlines inside the viewport and make AudioPlayer media-time tests use an engine-independent writable clock. 7c5a9e8

[0.7.2] — September 11, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.7.1 → 0.7.2.

🐛 Fixed

  • Docs-shell verification — assert native modal isolation directly instead of requiring a platform-dependent focus-guard escape after the injected defect. 9a1c6ec

[0.7.1] — September 11, 2026

📦 npm

  • The design-system registry (@vegastack/ui) bumps 0.7.0 → 0.7.1.

🐛 Fixed

  • Docs-shell verification — make the modal native-inert negative proof remove every outside descendant and keep the injected defect active for the full focus walk. ae881cc

[0.7.0] — September 11, 2026

🧩 New components

  • --duration-indeterminate (1200ms) and the motion-indeterminate utility — the one sanctioned looping animation, the sweeping segment of an indeterminate Progress. Its keyframes open and close on the same resting frame, so the global prefers-reduced-motion reset leaves a static 35% segment rather than a bar that reads as complete. docs · 7915a71

  • surfaceInteractive and fillInteractive — the two hover/pressed recipes, exported from @vegastack/design so no component writes a hover:bg-* literal again. surfaceInteractive (hover:bg-surface-2 active:bg-surface-3) is for a control on a known ladder surface; fillInteractive.<tone> (hover:bg-<tone>/(--alpha-hover) active:bg-<tone>/(--alpha-pressed)) is for one on an unknown backdrop or hovering in its own hue. The FillTone type ships with them. docs · b2c2e96

  • A layout scale, and --chart-single. --layout-header-height, --sidebar-width-mobile, --layout-overlay-max-height and --panel-width-sm|md|lg give the shell and the overlay family named dimensions instead of per-component literals, and --chart-single names the one-series chart colour. docs · b2c2e96

  • CheckboxGroup — shared state for a set of checkboxes, with first-class "select all". Base UI ships the parent/child arithmetic (allValues plus a parent child gives the mixed state and the whole-set toggle) and the system had no wrapper for it, so DataGrid, DataList and every permissions block computed it by hand. There is no CheckboxGroupItem — a child is a plain Checkbox with a value. docs

    useInlineEdit — the click-to-edit machine: draft, commit, cancel, focus restoration and the double-commit guard (Enter closes the edit, which unmounts the input, which fires blur, which would commit a second time). FieldInline and EditableCell had each written it, and the copies had already drifted — only one re-armed the guard when a controlled host flipped editing on, and only one restored focus after a keyboard commit. It owns no DOM and no persistence, so it also serves a cell editor that is a Select popup with no text input at all. docs · 9fbeb65

  • 28 new lucide-animated icons adopted — the mirror moves 439 → 467. Upstream had drifted 28 items ahead of the pinned manifest, and because tooling/mirror-animated-icons.mjs fails closed on an unexpected upstream item count, --refresh could not be run at all. The new items are binary, cigarette, cigarette-off, circle-gauge, cloud-backup, cloud-sync, external-link, hat-glasses, leaf, leafy-green, link-2, monitor-cog, palette, plane-landing, plane-takeoff, server, server-cog, server-crash, ship-wheel, shredder, soup, spray-can, ticket, tree-deciduous, tree-pine, user-plus, waves-arrow-down and waves-arrow-up — each shadcn add @vegastack/icon-<name>, each generated by the mirror rather than authored, and each inheriting the single createAnimatedIcon controller with its reduced-motion gate. None replaces an existing icon: the refresh removed nothing, the 439 pinned upstream and generated-module hashes are byte-identical, and every new item maps to its own lucide-react icon module rather than to an alias of one already mirrored. Registry items go 568 → 596. tooling/verify-animated-icons.mjs gains two sanctioned Motion easings ([0.25, 1, 0.5, 1], [0.65, 0, 0.35, 1]), two durations (0.12s, 0.32s) and a narrow staggered-duration form whose base and per-index step are each held to a sanctioned vocabulary; its --self-test grows to seventeen mutations, two of them new — an off-vocabulary stagger step, and a one-digit glyph edit inside one of the newly adopted modules. docs · 2035023

  • media-player-controls — the shared media transport, lifted out of audio-player (which owned the video player's controls too). It carries the control surface and useMediaShortcuts: ONE keyboard map for both players (Space/K play, J/L and arrows skip, M mute, F fullscreen), scoped surface vs controls so a shortcut can never steal a key from the focused control. The assignRef / getMediaDuration / clampTime helpers live here as well, instead of in a copy per player. audio-player drops from 1,431 lines to 305. docs · aa5fa0d

  • use-media-query — the system's one matchMedia subscription, on useSyncExternalStore with a caller-declared serverFallback. Five files each hand-rolled the same useState(false) + useEffect shape, and every one of them reported false on the server, so a phone rendered the DESKTOP branch of every JS-driven layout until an effect ran. Ships usePrefersReducedMotion as its named reduced-motion reader; useIsMobile and usePlatform's touch half are now one-liners over it. docs · 3663f8f

  • floating-surface — the shared floating-overlay module every anchored overlay now composes: one Portal → Positioner → Popup (→ Viewport) composer, one theme-scope hand-off across the portal boundary, one arrow, and four painted surfaces (panel at the 16px tier, menu at list density, tooltip as the inverted ink chip, navigation for the morphing mega-menu). It also owns menuItemVariants — the one list-item recipe behind menu items, select options, combobox options and command rows — and PanelSearchFrame, the in-panel search row. docs · 7915a71

  • toast — the toast surface on Base UI's Toast primitive, replacing the sonner wrapper. It ships the whole vocabulary rather than a configured library root: ToastProvider, Toaster, Toast and every part (ToastRoot, ToastContent, ToastTitle, ToastDescription, ToastAction, ToastClose, ToastViewport, ToastPortal, ToastPositioner, ToastArrow), plus useToast and the imperative toast(). Stacking with expand-on-hover, swipe-to-dismiss, F6 into the viewport landmark and Escape on the focused toast come from the primitive; promise toasts and a custom render come with it. The surface is the floating-family recipe at 16px padding and a typed toast wears Alert's exact tint recipe, so the two status surfaces read as one design. Six types (default · success · error · warning · info · loading) — the strings follow the engine, which writes loading/success/error in promise() itself, while the tokens follow the house families. The live-region policy is enforced in code: error and warning announce urgently, everything else politely, derived from the type. docs · d5e2de2

  • geo-data and drag-item — the first two registry:lib items: plain .ts modules that install under a consumer's lib alias and are pulled in automatically as dependencies. geo-data holds the ISO 3166-1 country list, the states/provinces map and their two lookups, so a consumer installing both geography selects copies the data once (region-select.json 67 KB → 7.4 KB). drag-item holds the one visual recipe for a use-drag-reorder item — drop-edge hairlines, lift dim, pending shimmer — which Board and SortableList had each copied. docs · 45cde26

  • searchable-select — the one Select-shaped Combobox preset: a full-width trigger, the shared in-panel search row, a check on the selected row, a --anchor-width panel and an optional clear control. CountrySelect and RegionSelect are now thin data-fed wrappers over it. Two rules it exists to hold: selection runs through Base UI's value/onValueChange and nothing else (the old RegionSelect computed the value inside each row's click handler with the root deliberately unwired, so keyboard Enter and a pointer click reached it by two different paths), and the clear control is a SIBLING of the trigger, never a child, because an interactive control may not contain another. docs · 45cde26

  • data-table-parts — the chrome DataList and DataGrid had each grown separately: the sort header, the select-all / per-row selection arithmetic, the skeleton rows, the empty row and the column class rules, twice each. They now come from one registry item, installed automatically with either renderer through registryDependencies. SortableHead and SortHeaderButton compose the system Button instead of a hand-rolled <button>, and emit aria-sort on every sortable column ("none" included). The doctrinal split is unchanged — DataList stays presentational, DataGrid keeps its engines. docs · 6d25185

  • use-overflow — the system's one overflow measurement, live across resizes AND content growth: it observes the element and its children, so a table widening inside a fixed viewport is not missed. It backs TruncatedText's clipping check, the Table scroll region's focusability, ComparisonMatrix and Terminal's command pane. docs · 6d25185

  • chip — THE labelled pill, and the only one. hue (10 decorative tag hues, or neutral) × size (sm 28px inline · md 32px control-scale) × active (the neutral chip's promotion to the surface-2 selection rung), with onRemove mounting a round ghost IconButton size="xs" whose real border box is 24×24. Tag, FilterChip, ComboboxChip and ChipInput's chips are all this one primitive composed through Base UI render. docs · 7915a71

  • use-announcer — the one polite live region. Destructure announce and Announcer from useAnnouncer() and render the Announcer element once per component. It keeps the region mounted and observed from first paint, re-keys it per call so an identical consecutive announcement is still spoken, and holds its state outside the host so announcing no longer re-renders a whole DataGrid. docs · 7915a71

🔧 Changed components

  • Animated icons — the host element is now an inline-flex <span> rather than a block-level <div>, so an icon placed in a line of text no longer breaks the line box, and AnimatedIconComponent types its host as HTMLSpanElement. Reduced motion is now a live subscription to (prefers-reduced-motion: reduce), so turning the preference on settles every mounted icon immediately instead of only affecting icons mounted afterwards. Motion's own hooks cannot do this: in 12.42.2 useReducedMotion() is useState(prefersReducedMotion.current) — a one-shot read of a module singleton captured at first import, with a standing TODO about not updating — and useReducedMotionConfig() layers <MotionConfig> on that same one-shot value. Worse, the OS preference was never consulted at all unless the application happened to mount a <MotionConfig>: useReducedMotionConfig() returns false outright when the context says reducedMotion: "never", and "never" is precisely Motion's default context value. The factory now treats the preference as the base value and lets <MotionConfig reducedMotion="always"> add reduction on top; the override is one-way, because an explicit reducedMotion="never" is byte-identical to no provider at all and honouring it would switch reduced motion off for everyone who configured nothing. Public icon names, the size prop and the startAnimation/stopAnimation handle are unchanged. docs · b2c2e96

  • A list row is 14/500 over 12, disclosures hover with the row wash, and the checklist composes the progress primitive. ItemTitle moves from 12px to text-label — the same type Sidebar menu rows, DataList cells, menu items and Message rows already use, because a 12px Item title beside a 14px sidebar row read as two systems; size="sm" keeps the denser 12/12 pair. Accordion and Collapsible triggers dropped hover:underline — underlining on hover is the link affordance — and took surfaceInteractive together with the padding, inner radius and ≥4px hairline inset that the wash requires, with the accordion panel taking the same horizontal padding so the body stays aligned under its label and row heights unchanged. OnboardingChecklist's segmented bar was a second hand-rolled role="progressbar" next to the primitive that already draws one, so it now composes ProgressIndicator segments; the primitive gains segmentsFill (segments share the container width instead of a fixed bar width) and accepts a single segment. A navigable Stepper label is now a link-variant Button instead of a ghost Button with its height and padding stripped to imitate inline text. docs · 8c18d2b

  • Base UI 1.6.0 → 1.8.0, @shadcn/react 0.2.1 → 0.3.1. Fifteen upgrade deltas were executed and observed in a browser rather than assumed from a green suite; only MessageScroller needed a source change. Its viewport now answers the primitive's new data-pending-scroll attribute with invisible, so a server-rendered transcript no longer paints the top of the thread for one frame before jumping to the bottom — visibility: hidden rather than display: none on purpose, because the primitive measures clientHeight/scrollHeight to decide where to scroll and a display-none viewport measures zero. A regression test asserts the attribute never sticks. message-scroller's declared @shadcn/react range moves to ^0.3.1; it and date-picker carry new integrity hashes. Two user-visible upstream changes are kept as shipped: a readOnly Select/Combobox now opens and browses (reached through editable-cell), and start/end-aligned popups take their pop-in --transform-origin from the aligned edge. docs · 5b03e3b

  • One key chip, and the display leaves move onto role tokens. TooltipKbd renders Kbd size="xs" instead of restyling a second <kbd>, so a shortcut hint reads identically wherever it appears — and inherits the OS rewrite. Kbd's three sizes now use one type role (text-code-sm); md reached the same 12px through text-sm, the same pixel size named twice, and the meaningless pointer-events-none on a <kbd> is gone. StatusIcon sizes become the --icon-inline / default / action / feature role tokens (14 / 16 / 20 / 24px), the ladder Spinner already uses, instead of raw size-N steps spelling the same four values. A Skeleton line moves to the text radius (rounded-sm): 8px on a 16px bar reads as a pill, not as text. docs · 7915a71

  • Reduced motion is stated once, globally. All seventeen motion-reduce: copies across eleven components were deleted — the registry now carries zero. The base.css reset owns the rule with the one sanctioned !important, so a per-component restatement adds nothing and is a second copy that can drift. One copy looked load-bearing and exposed a hole in the reset instead: it zeroed animation duration but not delay, so StaggeredTextReveal still played its words out one by one over the full stagger window. docs · 7915a71

  • Twelve components are server-safe again. Avatar, Button, Collapsible, Field, Progress, Resizable, ScrollArea, Separator, Slider, Switch, Tabs and Toggle carried "use client" without touching a hook or a handler. A client module poisons every RSC importer downstream — buttonVariants could not be read from a server component. 84 client leaves in the registry became 72. docs · 7915a71

  • text-xs is mono-only. Seven sites across Attachment, AudioPlayer, Chart and ProgressIndicator were reaching 11px in Geist Sans for density; sans copy now floors at text-sm (12px). docs · 7915a71

  • TruncatedText gains focusable, with a TruncationFocusProvider that sets it for a whole region. Clipped text becomes a Tooltip trigger and takes a tab stop — in a 50-row table that is 50 extra tab stops layered on a grid's own roving focus, and CSS truncation never hides anything from a screen reader, so the tooltip only ever served sighted keyboard users. IconText, TableCellText and RelativeTime take the same prop. docs · 7915a71

  • Button, Select, Sidebar, Toggle, Tabs, Table, DataGrid, DataList, Board, Item, Pagination, NavigationMenu, Combobox, DatePicker, Dialog, Sheet, Popover, HoverCard, Segmented, TagGroup, Bubble, Card, AppShell, EmojiPicker, FieldInline, MessageScroller, NumberField, OnboardingChecklist, ShortcutOverlay, Sonner, Switch, ToolCallChip and the dashboard-01 block — every hover now climbs one rung and every control has a pressed step. Previously only the solid primary Button darkened on :active; a state probe found 268 elements where pressing changed nothing. Select's trigger hovered only in dark mode; it now hovers in both. The current sidebar row rests on surface-3 so hovering it still moves. ComparisonMatrix and PricingSection stop using info (blue) for the promoted column and the highlighted plan — info is links and informational UI only; promotion is a neutral ladder rung. docs · b2c2e96

  • A loading Button no longer changes width. The spinner is taken out of flow and stacked over the label, which keeps its box behind opacity-0 — not visibility: hidden, which would drop the label out of the accessibility tree and leave a pending button with no name; previously a "Save changes" button jumped about 20px the moment a request started. docs · 7915a71

  • IconButton everywhere. The dismiss, pager and toggle controls that were hand-rolled <button> elements in Alert, AnnouncementBanner, Dialog, Sheet, Pagination, OnboardingChecklist and FilterBar are now IconButton, and CopyButton, MessageScrollerButton, ColorPicker, EmojiPicker and SplitButton's chevron half compose it too — so they all inherit the matrix, the focus ring, the loading contract and the required accessible name. New iconButtonGeometry(size, shape) styles an icon-only link: navigation stays a real <a> (PageHeader's back affordance) instead of acquiring role="button". docs · 7915a71

  • One field chrome. The border/hover/focus/invalid/disabled grammar was copy-pasted in Input, Textarea and NumberField, restated a fourth time as slot overrides in Field, and again in Combobox and ChipInput. It is now fieldControl / fieldControlGroup in @vegastack/design, which Input, Textarea, NumberField, OTPInput, the Select trigger, the Combobox input and its input-group, and ChipInput all spread — so retuning the field is one edit. docs

    Checkbox, RadioGroup, Switch and the Select trigger hover in both themes, checked included. A state probe found no hover treatment at all on 31 checkbox/radio/switch fixtures, and a ticked control read dead under the cursor while an unticked one moved. They now step through the same neutral border rung every field wears, and a filled control steps through the solid's own darker rungs. docs

    NumberField's ± steppers no longer run their hover fill into the field border. The wash was full-bleed, so it met the field's hairline on three sides with a square inner corner against the rounded outer one. It is now an inset chip with its own radius; the button keeps the full pointer target. docs

    Label is inline-flex by default, so it composes into a sentence instead of breaking the line around itself; layout="block" is the explicit opt-in for the stacked form row. docs

    PasswordInput's eye toggle is an IconButton in the ghost recipe, and the eye swap has no motion. It replayed motion-pop-in behind a guard whose only job was to stop the animation firing on first paint — a tell that the animation did not belong there. docs

    FieldInline and EditableCell run on useInlineEdit, and FieldInline's rest hover is the shared interactive-surface recipe, so it is visible on a card rather than only on the page ground. docs

    AutoSaveInput drops a cn(className) no-op, sizes its status slot with --icon-default instead of a raw size-4, and marks its spinner decorative. docs · 9fbeb65

  • AttachmentTrigger shows a real focus indicator. It dropped outline-none and a border tint as its only keyboard cue; the standard outline is drawn inset so the card's radius does not clip it. A border tint is the text-entry treatment, not a button's. docs · aa5fa0d

  • AudioPlayer gains mute and a volume rail in both layouts. Audio previously had no visible volume control at all and mute was reachable only from the M key. The rail is a vertical Slider opened from the mute button, rendered inline rather than portaled — the video frame is the fullscreen element, so a portal to <body> would put the rail outside it. The seek thumb is now hidden at rest only where a pointer can hover; on touch it stays visible, because otherwise there is no scrub affordance at all. docs · aa5fa0d

  • Image lazy-loads and decodes off-thread by default. loading="lazy" decoding="async" are now the defaults, matching what MarkdownView already did for its images. Pass loading="eager" for an above-the-fold hero, where deferring the fetch delays LCP rather than saving it. docs · aa5fa0d

  • Media chrome is theme-invariant. The video overlay was built on primary, which flips with the theme — in dark it rendered a near-white scrim behind near-black icons. Scrim, pills and ink now come from --media-scrim, --media-scrim-strong and --media-foreground, which are the same values in both themes, and a compiled-CSS test pins scrim lightness under 0.3 with overlay ink over 0.85. Overlay controls are IconButton variant="ghost" shape="round" on the scrim. docs · aa5fa0d

  • Media controls keep the system's focus outline. The ring-2 ring-ring/50 glow that media invented for itself, and the forced-colours carve-out beside it, are gone; the standard 2px :focus-visible outline applies, inset with -outline-offset-2 so an overflow-hidden frame cannot clip it. tabIndex={0} now appears only on genuinely scrollable regions. docs · aa5fa0d

  • Slider grows the props the players were faking with descendant selectors. variant (default · media · overlay · bare), orientation (vertical is now supported and is how the volume rail is built), thumb (always · hover · none), marks and showValue. Every [&_[data-slot=slider-*]] override in the players is deleted. docs · aa5fa0d

  • VideoPlayer gains controlsVisible. true pins the overlay open and false keeps it closed, for kiosk players — and for static fixtures, since the auto-reveal never fires without a pointer and the docs page therefore never showed the video controls. docs · aa5fa0d

  • Rich text is one recipe, and the bars that promise arrow-key traversal now implement it.

    • MarkdownView and TextEdit share the prose recipe. Both restated the same heading, paragraph, list, quote and code recipe in their own grammar and had already drifted (h4h6, tables and images existed on one side only). They now wear the same class from @vegastack/design, so rendered markdown and edited rich text are one typography — asserted structurally: a unit test requires every rule of the recipe on both roots, and no typography of their own. TextEdit's fenced-code block stops being a third copy of CodeBlock's surface, and the editor's links no longer all light up when the editor itself is hovered.
    • TextEdit's formatting row is a Base UI Toolbar — one tab stop, arrow keys across three labelled groups, Shift+Tab out. It was a role="toolbar" of eight independent tab stops.
    • ActionBar is a Base UI Toolbar with new ActionBarButton and ActionBarSeparator parts. Compose the actions from them: a toolbar builds its single tab stop from the items that register with it, so a bare <Button> renders but keeps its own tab stop.
    • ActionBar and MessageScrollerButton use the shared motion-dock-in / motion-dock-out pair instead of two copies of a recipe that exited more slowly than it entered, with a scale on a bar that slides off its own edge.
    • MessageScrollerButton defaults to variant="outline" with no inline colour override — after the Button matrix, outline already is a page-coloured face with the one hairline and the surface-ladder hover. 2a3fc24
  • dashboard-01's KPI labels no longer truncate. Every stat label was cut at the 2-column width ("Active agen…", "Tasks compl…", "API calls (24…"). The label now wraps to two lines across the full header and the trend badge sits on the value row, beside a short mono figure; the header breadcrumb collapses at maxItems={2} so it stays one line at 320px. docs · 3663f8f

  • LogoRow shows marks, not links. The underline is gone — wordmarks rest in text-muted-foreground and lift to text-foreground on hover, because a wall of underlined text reads as a paragraph of links. Cell seams are logical (-ms-px border-s), so RTL keeps its inner rules instead of doubling the outer edge, and wallColumns is now a MAXIMUM over an auto-fill track with an 8rem cell floor: a 4-column wall at 320px gave 80px cells and clipped every mark. docs · 3663f8f

  • PlanCard's promoted plan is a ladder rung, not a border colour. It now lifts onto surface-3 with its hairline tinted primary at --alpha-outline-border. A full-strength border-primary read as the active/invalid state of a form control — the wrong signal on a card someone is being invited to choose. docs · 3663f8f

  • SettingsSection gained titleAs (h2h6, default h3). A settings page nests sections at different depths, and a hard-coded <h3> everywhere breaks heading navigation. The visual size is unchanged; only the document structure moves. docs · 3663f8f

  • StaggeredTextReveal waits for the scroll (whenVisible, on by default). A reveal below the fold used to finish before anyone scrolled to it. The gate only ever REMOVES the reveal — the server-rendered markup animates and the client pulls off-screen words back before the first paint — so a page whose JavaScript never runs still shows its text. docs · 3663f8f

  • Testimonial takes its quotation marks from CSS. The quote renders inside a <q>, so the browser inserts the pair the ACTIVE language uses („…“, « … », 「…」) instead of the hard-coded English “…” — which also kept the marks out of the copied text. docs · 3663f8f

  • Navigation and layout take one selection recipe and one hover geometry. Tabs, Segmented, Toggle and ToggleGroup move onto the shared selectedChipVariants recipe from @vegastack/design, and a selected chip now hovers and presses again — it used to be excluded from both by not-data-pressed:*/not-data-[active], so the one chip a user is most likely to click was the one that answered nothing. The Tabs line trigger's hover wash used to end exactly on the rule the underline indicator rides along, in both orientations; it is held one 4px step off it with a logical margin, so the vertical variant mirrors onto the inline-start rail and RTL follows for free. SidebarProvider gains persist (default true) around the cookie write: persist={false} keeps the component out of document.cookie entirely and onOpenChange fires either way, so a host under a consent regime persists the state itself and loses nothing — the docs section is renamed from "SSR persistence" to "Persistence". ScrollArea's viewport is a tab stop only once its content actually overflows, measured on mount and on resize; Board's column viewports inherit it. AppShellContent and SidebarInset gain landmark="region", which renders a <div role="region"> instead of a <main> for a shell embedded in a page that already owns one. SidebarTrigger is an IconButton rather than a hand-rolled useRender button, so it inherits the one box, ink and hover/pressed grammar, and PageHeader's back affordance swaps its physical -ml-2 for a logical -ms-2. PageHeader's active favourite star fills with foreground instead of warning ink, which read as caution on a control that means "I marked this". docs · fdaed05

  • Popover and Select document their modality. Both are modal by default so the page cannot scroll out from under an open panel; pass modal={false} for a lightweight one. Combobox stays non-modal and says why in its source. docs · 7915a71

  • Overlay motion follows one measured scale. Every floating surface enters and leaves at duration-fast (150ms); NavigationMenu takes duration-base (200ms) because it resizes between items rather than simply appearing; the modal family — Dialog, AlertDialog, Sheet — is duration-base. Timings were measured against Vercel and Linear rather than chosen. docs · 7915a71

  • Overlay padding has two tiers, not per-surface literals. 24px (p-6) for Dialog, AlertDialog and Sheet; 16px (p-4) for Popover and HoverCard; menus keep list density. Panel widths come from --panel-width-*, and a viewport-capped popup uses Base UI's --available-height instead of a hand-written 100dvh calc. DialogContent and Command size through size. docs · 7915a71

  • Popover, HoverCard, Tooltip, DropdownMenu, ContextMenu, Select, Combobox, NavigationMenu — eight lookalike overlays became one module. Each now composes floating-surface instead of restating its own portal, positioner, popup surface, arrow and theme-scope plumbing. ContextMenu is bound to the same item parts as DropdownMenu through createMenuParts (Base UI's ContextMenu namespace re-exports Menu's parts verbatim), so the two menus can no longer drift. docs · 7915a71

  • Command, Combobox, EmojiPicker, ShortcutOverlay — the in-panel search row is one recipe. A bordered Input inside a bordered popup drew two borders; PanelSearchFrame is a sticky, full-bleed header row with a leading glyph, no box of its own and a hairline below. docs · 7915a71

  • DatePicker, Board and Dropzone. DatePicker and DateRangePicker triggers are w-full like every other form control: the fixed w-56 and w-72 were the only fixed-width controls in the system and overflowed a 320px content area. The calendar caption is now a real auto 1fr auto grid (navLayout="around") instead of an absolutely positioned nav over a px-7 clearance, and the selected day carries a pressed rung (hover:bg-primary-hover active:bg-primary-active) instead of pinning its rest fill. Board's grab cursor appears only where a pointer drag can actually start and its column body height is the new columnMaxHeight prop. Dropzone paints the drag-over state on its own surface, so a dropzone wrapping anything other than an Empty finally shows one, and a new dragState prop paints either state for documentation and automated checks. docs · 45cde26

  • Table, DataList, DataGrid, ComparisonMatrix, Terminal — a scroll viewport is now a named, keyboard-reachable region, and it is measured rather than guessed. A wide table could previously only be scrolled with a pointer (axe scrollable-region-focusable on the Table page and the dashboard block), while ComparisonMatrix and Terminal carried an unconditional tab stop that was dead on every instance that fits. Each viewport now measures itself through useOverflow and takes a tab stop only while it can actually scroll; name it with scrollLabel (falling back to the table's aria-label) and it is exposed as role="region", unnamed it stays a plain focusable container. Its focus outline is inset, because the viewport clips its own overflow. Names and roles do not move with the measurement — Terminal's name and group role stay unconditional. Table and Terminal both stay server-safe: the measurement lives in a 'use client' leaf (table-scroll-region.tsx, terminal-body.tsx). docs · 6d25185

  • DataGrid, DataList, Table, PropertyList, Chart — the rest of the table-family pass. DataGrid gains columnPicker (default true): the "Columns" picker used to render whenever the grid had any columns — i.e. always, even for a three-column read-only grid — and it now sits in the toolbar's trailing slot beside the hidden-columns hint. DataList and DataGrid gain a mono column flag (mono numeral face plus tabular-nums, nowrap by default) and a nowrap flag; together with the shared chrome that is 249 fewer lines across the two files. TableRow no longer tints the header row on hover — it styles every row it renders, including the header row the two renderers build with it, so hovering a header washed it as if it were actionable. PropertyList becomes a container query: the label track was a fixed 112px regardless of the pane, and is now content-sized above an 80px floor at @xs and stacked below it, with values wrapping instead of truncating. Chart moves axis labels from 11px to 12px (11px is reserved for mono) with the numerals on the mono text-code-sm tier, and the tooltip follows. docs · 6d25185

  • EditableCell, ChipInput, DataGrid, CopyButton, SortableList, Board, Dropzone — all announce through use-announcer, one live region each, replacing five identical hand-rolled { text, seq } regions plus CopyButton's. EditableCell's visible status slot is no longer itself a live region, so it stops announcing its own icon swaps. Pagination's PaginationLink takes its hover and pressed steps from surfaceInteractive instead of restated hover:bg-surface-2 / active:bg-surface-3 literals. docs · 7915a71

  • Tag, FilterChip, ComboboxChip, ChipInput, TagGroup — all now compose the chip primitive. Three consequences are visible: a Tag is 28px rather than 20px and a FilterChip is a pill rather than a rounded rectangle (chips are rounded-full by doctrine); the neutral chip rests on surface-1 and an applied filter sits on surface-2 instead of the accent alias; and every remove control is the same 24×24 target. That last one fixes ComboboxChipRemove, a bare 16px box with no hit-area expansion at all (a WCAG 2.5.8 failure), and retires Tag's ::before hit area, which a nested native <button> clipped and so never actually expanded anything. TagGroup's +N overflow control is itself a chip, so the whole 28px pill is the pointer target and its hover/pressed steps come from the shared surfaceInteractive recipe rather than a hand-written descendant selector. docs · 7915a71

🗑 Removed / renamed

  • track is removed. The slider rail, progress track, skeleton and every well are surface-1; the switch off-track is surface-3, the pressed rung. Three alpha roles are removed with it, because the ladder is now the one hover mechanism and nothing references them: --alpha-fill-hover (the secondary button's /80 opacity dim), --alpha-input-hover (the dark-only input hover wash) and --alpha-surface-subtle (the outline button's hover tint, now --alpha-hover in the family's own hue). docs · b2c2e96

  • Button glass, and the seven colour-in-the-name variants. The frosted glass variant had no product consumer; media chrome uses the theme-invariant --media-* tokens instead. success, warning, info, destructive-outline, success-outline, warning-outline and info-outline baked a colour into a name and are replaced by the tone axis. docs · 7915a71

  • Button finish and the --shadow-lit token. The "lit" action finish is retired, so flat-by-default now has no exception at all and the system has exactly one shadow role, shadow-overlay. docs · 7915a71

  • selection:* in form controls — Input, Textarea, OTPInput, NumberField and the Combobox input repainted selected text near-black on near-white. Native selection is what users expect and it respects the OS and accessibility settings. docs

    shakeSignal on Input, Checkbox, RadioGroupItem, OTPInput, NumberField and ChipInput — the prop lives on Field, which owns the shake. docs

    Spinner label="" as the way to say "decorative"decorative is now the sanctioned spelling. label="" still means the same thing; it just says it by passing a value that reads as a mistake at the call site. docs · 9fbeb65

  • mergeRefs no longer ships from the use-animation-replay registry item, and media-player-controls no longer exports assignRef. Both were spellings of the same merge; the one implementation now lives in @vegastack/design and every registry file imports it there alongside cn. grep -rn 'typeof ref === "function"' packages/ui/registry/ui is 0. docs · 3663f8f

  • AlertDialogContent intent — it wrote a data-intent hint and nothing else, leaving two props named for one concept with one of them inert. AlertDialogAction intent is the single owner of a confirmation's tone. docs · 7915a71

  • Hand-written 100dvh overlay height calcs — replaced by --layout-overlay-max-height and Base UI's --available-height. docs · 7915a71

  • sonner — the registry item and the dependency. Removing it deletes a rendering engine, the CSS override layer that fought the library's internal greys, its z-index exemption and a :focus-visible box-shadow glow the design system bans everywhere else. Toasts are now @vegastack/toast. The toaster-mirror gate stays but points at toast.tsx — the private @vegastack/ui package still needs a byte-identical Toaster — and the sonner-human-facing-alias contract exemption is gone, because it existed only while the registry item was named sonner and its page was /docs/components/toast; the two now match. docs · d5e2de2

  • TagHue — the hue vocabulary moved to the Chip primitive and is exported as ChipHue from @vegastack/chip. Tag's hue prop is unchanged; only the type's name and home moved. docs · 7915a71

  • The attestation stack is deleted — the gate receipt and its guards, the change classifier, route scoping, the Playwright-over-the-docs-export contract and pixel lanes, and the cross-engine smoke selection. CI executes every browser lane instead of inspecting evidence about it. The five docs-shell contracts those lanes carried survive as tooling/verify-docs-shell.mjs, a release-stage script with a --self-test that proves each assertion still fails on its own defect. @vegastack/ui is private and neither published package changes. 09b2107

🛠 CLI & tooling

  • Animated icons are one factory plus 439 data modules. Every mirrored lucide-animated icon used to carry its own copy of the controller — the animation controls, the reduced-motion gate, five pointer/focus handlers, the imperative handle and a block-level host — so a change to any of that meant regenerating 439 files and trusting that all 439 agreed. The controller now lives once in createAnimatedIcon, exported from the new @vegastack/design/create-animated-icon subpath, and each icon is a createAnimatedIcon({ … }) call describing only its geometry, its Motion variants, and (for 49 icons) its non-default start/stop steps. motion becomes an OPTIONAL peer dependency — only an animated icon pulls it in, so Icon/BrandIcon consumers are unaffected. The corpus went from 79,078 lines to 12,951 (-84%) and from 2.06 MiB to 0.57 MiB of source; the served registry fell from 4.48 MiB to 2.92 MiB. tooling/mirror-animated-icons.mjs emits the data modules and fails closed on any upstream archetype it cannot model; tooling/verify-animated-icons.mjs asserts the controller contract once against the factory, holds every module to a schema whose central clause is that a data module contains no controller at all, pins each generated module by SHA-256 in packages/ui/animated-icon-sources.json so a hand-edited path or timing value is rejected outright, and carries a --self-test that proves seventeen distinct regressions are rejected. docs · 2035023

  • Patch/minor alignment across the workspace, and an explicit supply-chain floor. Seventeen packages moved to their current patch or minor, including the three Tailwind siblings brought to a matching 4.3.3. The release-age floor is now explicit (minimumReleaseAge: 1440 + minimumReleaseAgeStrict: true) rather than inherited. Measured on pnpm 11.7.0: inherited, pnpm appends a minimumReleaseAgeExclude entry to pnpm-workspace.yaml and installs the too-new version anyway; explicit, the install fails with ERR_PNPM_NO_MATURE_MATCHING_VERSION and writes nothing. The two stale fumadocs-*@16.10.5 excludes were written by pnpm that way, not by a human, and are deleted.

    @hookform/resolvers moves 5.4.0 → 5.9.1 in the same pass: zodResolver's overloads did not accept a schema built by zod 4.5, so field-form.test.tsx — which exists to fail the typecheck when the RHF/Zod surface drifts — stopped compiling. It did its job.

    deploy.yml's wranglerVersion pin follows apps/docs to 4.129.0. verify-workflow-security (#88) asserts the two agree, because the deploy action downloads the version named in the workflow and production would otherwise ship through a wrangler this repository never installs or tests against. 5b03e3b

  • Next 16.2.11 → 16.3.4 for the docs app. The 16.2 line stopped receiving patches in July and two criticals were never backported. enablePrerenderSourceMaps and experimental.turbopackFileSystemCacheForBuild are defaults in 16.3 and were deleted. next dev in 16.3 writes a managed agent-rules block into an AGENTS.md/CLAUDE.md in the Next app directory when it detects a coding agent; agentRules: false turns that off — this repo's agent instructions are authored and reviewed, and a tool-managed block inside a hand-authored file has no owner. 5b03e3b

  • Security patches across the toolchain. @vitest/browser 4.1.9 → 4.1.11 (critical: provider commands bypassed allowWrite), @tiptap/* 3.27.4 → 3.31.3 (prototype pollution + paste XSS), postcss 8.5.19 → 8.5.28 (source-map file read), style-dictionary 5.5.0 → 5.5.2 (prototype pollution), plus the shadcn 4.21 bump that clears postcss under shadcn/tsup. Measured on the same advisory database on 2026-09-09, pnpm audit goes from 59 findings (3 critical · 29 high · 25 moderate · 2 low) before to 47 (0 critical · 24 high · 21 moderate · 2 low) after. Every remaining finding is a transitive dev-tool dependency with no path into published output. style-dictionary is held at 5.5.2 deliberately: 5.5.3 touches color/css alpha precision, which is token CSS output, and 5.5.2 already carries the fix. 5b03e3b

  • shadcn CLI 4.13.0 → 4.21.0. 4.13.1 fixed three unadvised security bugs — custom registry headers leaking across cross-origin redirects, path traversal for registry items without an explicit target, and flag injection from registry-supplied dependency strings. No GHSA or CVE was ever filed, so no audit tool would have flagged the gap. shadcn build output is byte-identical and verify-shadcn-consume still passes against the real CLI. 4.21.0 makes the cn package the upstream default for generated lib/utils; we did not adopt itcn() keeps coming from @vegastack/design, which is a locked decision, and the change is registry content, not CLI behaviour. 5b03e3b

  • Playwright 1.61.0 → 1.63.0, and the Linux CI container tag follows it. The playwright devDependency moves in the root workspace and in @vegastack/ui; 1.63 ships Chromium 153.0.8010.12, Firefox 155.0 and WebKit 26.6, and drops Ubuntu 20.04 support. The audit's one API-removal concern, Locator.ariaRef(), is a non-issue: it is absent from the type surface of the 1.61.0 we came from as well as 1.63.0, and nothing in this repo names it. tooling/verify-workflow-security.mjs derives the required Linux job image (mcr.microsoft.com/playwright:v<version>-noble) from the single playwright version the lockfile resolves, so ci.yml, release.yml and deploy.yml now pin v1.63.0-noble.

    auto-install-peers=true also makes pnpm resolve Next's optional @playwright/test peer even though no manifest declares it, and left alone it stayed on 1.61.0 — two playwright versions in one lockfile, which that gate refuses. A pnpm-workspace.yaml override pins the phantom peer to the same version, so the container tag keeps exactly one authority. 3b37dde

  • verify-component-contracts --write-data-attributes records each registry part's data-* attributes and CSS variables in component-contracts.json, extracted from the canonical source through the TypeScript AST, so the docs API tables and the agent markdown export list them. The default mode fails when the contract drifts from the source, and a --self-test drifts a dataAttributes record in memory and requires the reconciliation to reject it — so the gate cannot pass by never having run. docs · 7915a71

  • Gate correctness (G1-b) — four fail-opens closed, and focus indication covered again. packages/ui/test/{contrast,stacking}.css imported only part of the layer set production ships, so every custom @utility a fixture wore compiled to nothing and both lanes measured a page no user sees; verify-test-css-layers now derives the required set from the shipped preset and fails closed. pnpm lint gains a repo-wide prettier --check. A changeset may no longer link a commit — the only sha it can name is a pre-merge one the squash orphans — and changelog-lint's own probe became reachability rather than mere object existence. Eleven cases in the workflow-security negative harness matched literals that can drift; one of them had never exercised its own rule. The geometry lane gains a focus-indicator assertion over all 541 fixtures that rejects the browser's own ring by name, packages/ui/test finally type-checks, verify-component-contracts derives the inventory counts from the registry instead of hard-coding them, six token-vocabulary design-lint rules land with negative fixtures, and verify-token-references fails closed on a --token that does not exist. b2c2e96

  • CI: the macOS runner class is ONE mac mini running two runner agents that share one home directory, and pnpm/action-setup defaulted its bootstrap directory to ~/setup-pnpm — which it deletes on every job, and which PNPM_HOME also made the package store. Concurrent jobs raced that deletion into ENOTEMPTY (or a half-linked node_modules that turbo reported as unable to spawn child process) before any repository code ran, and the store never survived a job, so every macOS install re-downloaded the whole dependency graph. Each mac-mini job now bootstraps into ${{ runner.temp }}/setup-pnpm and installs with --store-dir "$RUNNER_WORKSPACE/pnpm-store" — per-agent, and persistent — and the three jobs that enabled setup-node's package-manager cache no longer do, since it cached the directory the next job deleted. verify-workflow-security.mjs asserts all three, with negative-harness cases for each; two of its existing cases that matched the install by literal command line were rewritten to match by shape. c352ba9

  • release-detect --check-npm fails closed instead of fail-open, and docs/RELEASING.md matches reality. The registry probe spawned npm view with cwd = the repo root, where package.json declares devEngines.runtime node 24.20.0; npm enforces that field, does not honour pnpm's onFail: download, and exits EBADDEVENGINES on any other Node before it reaches the network. The old status !== 0 branch read that as "not published", so every push to main reported both live public packages as unpublished, forced publish=true, ran quality-gate, and armed the OIDC-capable publish job on changeset-free pushes (release run 34323665258 shows unpublished: @vegastack/design (none) → 0.3.2 with 0.3.2 live). The query now runs in an empty temporary directory carrying a copy of the repo .npmrc, out of devEngines' reach but still on the repo's own registry mapping, and npm view --json is parsed into three outcomes: published, absent (a genuine E404 — the only answer that may mean unpublished), and unknown (engine refusal, transport, auth, missing npm, unparseable output). An unknown never contributes to publish, is retried once, and exits non-zero when it was the only thing that could have set publish — fail-closed and loud rather than a false that was never established. Nine cases in tooling/test/release-detect.test.mjs pin it against a stubbed npm; six of them fail against the previous script. docs/RELEASING.md no longer calls this public repo private in four places, states the real reason provenance is off (npm rejects a self-hosted bundle with E422, not repository visibility), describes the merged build-sign-deploy job rather than the deleted three-job artifact split, and records that releases deliberately create no git tag or GitHub release. AGENTS.md gains that tag decision and reconciles two deviations from the verification-rebuild plan. af61a44

  • design-lint --docs-shell --emitted-css reads the BUILT stylesheet, because the shell's off-system values are compiled in by dependencies and never appear in this repo's source. It ships a negative self-test. 7915a71

  • CI gatespnpm verify can no longer be neutered in a workflow. The workflow-security gate now matches the command as the ENTIRE run: body (a block scalar that merely contained the line satisfied the presence check while escaping every effectiveness rule), rejects any run body that discards an exit code (set +e, || true, a bare exit 0), rejects a WEBKIT_LANE / SMOKE_WEBKIT / cleared-CI env: at workflow, job or step level (each silently dropped verify:release from three browser engines to two), pins --ignore-scripts per job in both directions — the npm-OIDC publish job now installs without lifecycle scripts, like version-pr — and checks --frozen-lockfile line by line. Eleven new negative cases, all observed rejecting. The hover/pressed recipes are now measured for real: the @source glob that compiles them was a no-op in both browser lanes, and the one assertion over it was reading its own file. Also: the secret scan's scope is stated honestly (it reads the working tree, never history), release-detect no longer reads an unanswerable git diff as "nothing changed", workspace-clean refuses a path when git cannot say whether it is tracked instead of assuming it is not, and every compiled-CSS lane now imports the same set production ships, tw-animate-css included. 47ddb18

  • verify-docs-base-mirror diffs the rule blocks apps/docs/app/global.css hand-copies from base.css, which had no gate. It counts @apply as a declaration: the focus ring is expressed only that way on both sides, so filtering @-prefixed lines compared that block as empty against empty and could never fail. It ships a negative self-test. 7915a71

  • verify-docs-export fails the docs build on any JSX tag surviving outside a code fence, any unresolved export placeholder, or any empty API table — the regression guard for the markdown export — and enforces that a page carries either a curated playground or the Story explorer, never both. It ships a negative self-test, so it cannot pass by never having run. 7915a71

  • verify-mdx-manifest (docs lint) proves the agent export fails closed on the three failures that leave no artefact behind for verify-docs-export to find: an MDX component no manifest entry classifies, a placeholder whose runtime renderer is missing (nested ones included), and a component registered in the MDX map but absent from the manifest. Before it, the first rendered to a single space and the second to its bare children. 7915a71

  • pnpm verify:release — discards the docs build cache before the first export so a stale Turbopack cache can no longer report the docs shell as off-system, and the registry idempotency check now compares before/after instead of demanding an empty tree. cb99022

  • Cross-engine lane — CI now defaults to WEBKIT_LANE=require instead of auto, so a WebKit that stops launching fails the release lane instead of silently reducing it to two engines. e5d7a7f

  • CI/CD: the workflow-security gate now asserts that the workflows DO something, not only that they are shaped correctly. An adversarial review built twelve mutations that the gate passed; three made a workflow report success while executing nothing — continue-on-error on the deploy's verify job (a failed sweep still deployed to production), the same key on ci.yml's pnpm verify step (a green check on a red suite), and if: false on pnpm verify:release (the step stayed in the file and left the run). The gate now rejects continue-on-error anywhere without a recorded allowlist entry, forbids an if: on any verification step and on the deploy's sweep job, asserts the Cloudflare command exactly (deploy --dry-run deployed nothing and reported success), pins wranglerVersion against apps/docs/package.json, requires the publish-time lifecycle guard and the standalone npm publish line (a false && prefix published nothing), requires SITE_VISIBILITY in all three workflows, ref: ${{ github.sha }} on every outward checkout, --frozen-lockfile on every install, a concurrency group and a bounded timeout-minutes on every job, and verify-macos's required steps including the changeset status presence check. publish loses the contents: write and pull-requests: write it never used, every job declares a timeout instead of inheriting the 360-minute default, ci.yml declares the same SITE_VISIBILITY as the other two so the turbo cache key genuinely matches, and the OIDC scope check is structural rather than a text count. The negative harness grows from 37 to 60 mutations, each of the new ones reproduced against the pre-fix gate first. 087015e

  • WP1 of the verification rebuild — the 320px reflow, RTL containment, and effective 24px pointer-target contracts now run as a vitest browser lane over the preview fixtures (packages/ui/test/geometry.browser.test.tsx) instead of @playwright/test over the docs export. No published package changes: @vegastack/ui is private, and the only files touched are its test lane and vitest config. 7915a71

  • WP2 of the verification rebuild — test-config and tooling only, no published package change. 7915a71

  • WP4 of the verification rebuild — the contract-derived docs files become build outputs rather than committed artefacts; no published package change. 7915a71

  • The root CHANGELOG.md release entry is assembled from the changesets. A PR's only changelog artefact is its changeset, whose body opens with one of the eight section emoji; tooling/changelog-assemble.mjs groups those bodies into the ## [x.y.z] entry at version time, inside pnpm run version-packages, and sync-changelog regenerates the docs page as before. tooling/changeset-lint.mjs rejects a body with no marker, two markers, or no text. Nobody hand-edits /CHANGELOG.md between releases — it was one list at the top of one file, and every branch collided on it. 7915a71

📦 npm

  • Dependency batches 5–6: the Fumadocs 16.15.8 family, lucide-react 1.42, axe-core 4.13, Playwright 1.63 and recharts 3.10.

    Fumadocs 16.11.5 → 16.15.8 (fumadocs-ui, fumadocs-core, fumadocs-mdx 15.4.0, fumadocs-typescript 5.4.0, @fumadocs/story 1.3.0; fumadocs-twoslash takes the TS 6-safe 3.3.1 patch rather than 4.0, which needs TypeScript 7) is two migrations rather than a bump. fumadocs-core 16.14 replaced Orama with ZBSearch behind the same module path — oramaStaticClient is now a deprecated alias for staticClient, the client builds its own database, and the initOrama factory plus the direct @orama/orama dependency are gone. fumadocs-typescript 5.4 swapped ts-morph for the native TypeScript 7 API, so the docs' own-props filter reads a property symbol's declarations[].path instead of getDeclarations()[].getSourceFile().getFilePath(); the generator cache is TypeScript-version bound and was cleared.

    Two Fumadocs behaviour changes were audited and deliberately left alone. 16.13's global d theme hotkey is inert here — RootProvider mounts its window-level keydown listener inside the theme.enabled !== false branch and this site disables fumadocs' theme provider outright, so nothing is registered; the reasoning is now recorded at the call site so re-enabling that provider cannot silently reintroduce a hotkey that swallows a letter on every interactive page. 16.12 stopped force-mounting inactive Tabs panels, so a ComponentPreview's hidden Code panel is no longer in the prerendered HTML; nothing depends on it, because the visual-surface contracts read the always- mounted Preview panel and the markdown export reads fixture source from disk, so no forceMount was added.

    Nothing in the component sources changed for the icon or chart bumps. The lucide 1.25 → 1.42 rename sweep is a no-op: all 126 distinct lucide names imported across the registry, the design package and the docs app — 125 icons plus the LucideIcon type — resolve against the installed 1.42.0 module, and lucide keeps every historical rename as a named alias, so there is nothing to sweep. The 439 animated-icon data modules regenerate byte-identical through the factory, with the 28 new upstream icons left unadopted. axe-core 4.12.1 → 4.13.0 expands aria-prohibited-attr and role=image, and the browser axe lane reports no new violation. recharts 3.9.2 → 3.10.1 deprecates Legend's align/verticalAlign in favour of position/offset; no ChartLegend call site passes either and Legend still injects verticalAlign into custom content, so ChartLegendContent keeps reading it. Only chart's registry item changes, and only because its documentation comment records that migration. 3b37dde

  • @tanstack/react-table 8.21.3 → 9.2.4, in data-grid alone. v9 replaces useReactTable with useTable and stops bundling every feature into every table: a table now declares the features it uses, and row models are feature slots rather than table options. data-grid declares exactly one — rowSortingFeature with createSortedRowModel() — which turns the sanctioned-exception boundary from a claim into something the module reads back: columnVisibilityFeature, columnOrderingFeature and rowSelectionFeature all exist in v9 and none is adopted, because column visibility, column order and row selection are data-grid's own state, and the APG grid keyboard layer (roving gridcell tabindex, Enter/F2 edit mode, Escape restore) is unchanged this-file-only code. getCoreRowModel() is gone (the core model is automatic) and manualPagination went with rowPaginationFeature, where it was already inert. The four built-in comparators v8 kept permanently in its registry — alphanumeric, basic, datetime, text — are registered explicitly in the sortFns slot so getAutoSortFn resolves the same comparator per column as it did under v8. No public prop, type or behaviour of DataGrid changes; @tanstack/react-virtual is untouched. docs · 36ebf9d

  • Vitest 5. vitest 4.1.11 → 5.0.0, @vitest/browser-playwright 4.1.11 → 5.0.0 and vitest-browser-react 2.2.0 → 2.3.0 across the workspace — the runner under every test in the repository, including the browser suite pnpm verify gates on. Vitest 5 matches locator text and accessible names whole-string by default and makes toHaveTextContent whole-string equality (toMatchTextContent is the partial/RegExp matcher), which turned ten of 2363 browser tests red. Every one was fixed at the call site rather than by restoring the old substring behaviour: eight now assert the FULL accessible name — a tab's trailing count badge, a link's sr-only "(opens in new tab)", a requirement row's sr-only "Met: " prefix — so a query proves what a screen-reader user hears. getByRole('tab', { name: 'A' }) had been resolving a tab named Activity3. Vitest 5 also consolidates every run artifact under one .vitest directory, so workspace-clean.mjs's tree walk for the old .vitest-attachments directory is deleted rather than kept. No component source, no published output and no registry item changes; item count stays at 568. 2625f2b

  • Five mechanical dependency majors. motion 12.42.2 → 13.2.0 (its only import site is the animated-icon factory; the 13.0 removal of the optional @emotion/is-prop-valid dependency does not apply — no CSS-in-JS library wraps a motion component here — and useReducedMotion() is still the one-shot useState read the factory deliberately replaces with a live useSyncExternalStore subscription). react-dropzone 19.1.1 → 20.1.1, whose only breaking change is a Node 22 floor (this repo pins Node 24.20.0). @atlaskit/pragmatic-drag-and-drop 2.0.1 → 3.1.0 plus -hitbox 2.0.0 → 2.2.0, whose 3.0.0 renamed every entry point: use-drag-reorder now imports from /adapter/element-adapter, /utils/combine, /closest-edge/attach-closest-edge, /closest-edge/extract-closest-edge and /types rather than the deprecated compatibility shims. @testing-library/jest-dom 6.9.1 → 7.0.1, which makes @testing-library/dom a required peer — now declared explicitly at 10.4.1. globals 16.5.0 → 17.12.0, whose 17.0.0 split the audioWorklet environment out of browser; the shared ESLint config uses browser + node only. Behaviour of the drag keyboard layer, the live-region announcements, the "Move to…" menu equivalents and the paste-acquisition path is unchanged — all of it is ours, not the engines'. docs · 452df99

  • @vegastack/design exports the field-chrome recipes. fieldControl and fieldControlGroup are the one border/hover/focus/invalid/disabled grammar every text-entry control wears, so Input, Textarea, NumberField, OTP slots, the Select trigger, the Combobox input and ChipInput can stop keeping private copies of it. Three border rungs and no more: border-input at rest, the neutral --alpha-border-subtle ink on hover, the ring tint on focus. Hover is guarded by not-disabled: because a disabled control keeps its pointer events so a Tooltip can explain it. Every element wearing the wrapper recipe must also carry a bare data-field-group attribute — that is what @vegastack/design-tokens' base.css hooks to paint the forced-colours focus outline on the group, whose overflow-hidden would otherwise clip the inner input's own.

    Also surfaceInteractiveGroup — the group-scoped twin of surfaceInteractive, for the one geometry where the two ladder rungs cannot sit on the interactive element itself: a wash painted by an inner chip inset from a container hairline (NumberField's ± steppers). The rungs stay written once. docs · 9fbeb65

  • @vegastack/design-tokens — adds the motion-dock-in / motion-dock-out utilities — the docked-control enter/exit pair for a control that stays mounted at a viewport edge and flips data-active (a bottom action bar, a floating scroll-to-edge button). 150ms in on ease-emphasized, 100ms out on ease-exit, translate and fade, no scale: an exit is never slower than its enter.

    The pair owns the timing, the fade and the parked pointer-events: none; the travel distance stays at the call site as ordinary translate-* utilities, because it is per-dock geometry and a translate declaration inside the utility would clobber a horizontally-centred bar's composed transform. 2a3fc24

  • @vegastack/design — exports the prose recipe — one token vocabulary for rendered rich text, so a surface the system did not author element by element (markdown, a contenteditable, CMS copy) is styled by one class on its root. proseClassName is the whole recipe; prose is the per-element record it composes from, keyed by the ProseElement type.

    It is expressed as descendant variants ([&_h1]:…) because neither consumer can put a class on the elements — ProseMirror owns the editor's DOM, and react-markdown's output is reachable only through an override map — and because an element-level class silently LOSES the cascade to a root-level descendant rule (specificity (0,1,0) against (0,1,1)). Restyle prose by composing prose, never by setting a class on the rendered element. 2a3fc24

  • @vegastack/design exports mergeRefs — fan one DOM node out to several refs (a forwarded ref prop plus one or more internal refs) as a single ref callback, handling both shapes React 19 accepts and skipping null/undefined entries. It was exported from the use-animation-replay registry item, an odd home for it, while nine registry files hand-inlined the same typeof ref === "function" merge; ref-as-prop makes "the component needs the node AND has to forward it" the normal case, so it belongs in the package. Typed against React's ref shapes with a type-only import, so the entry stays server-safe. 3663f8f

  • Exports selectedChipVariants — the one "raised chip on a muted track" recipe, shared by Tabs pill/chip, Segmented, and pressed Toggle/ToggleGroup, which had drifted into four different selected looks (bg-background, bg-secondary plus a hairline, bg-foreground/10). The track is surface-1; the chip is the pressed/selected rung in its alpha form (bg-foreground/(--alpha-ink-tint)), which is what lets a SELECTED chip keep stepping — it strengthens on hover and drops back to the resting tint on press. Ships as track, item, and two state literals: pressed (Base UI data-pressed) and active (Base UI data-active). docs · fdaed05

  • The design-system registry (@vegastack/ui) bumps 0.6.0 → 0.7.0.

📚 Docs

  • The input and textarea summaries no longer promise a focus ring they never had. Both text-entry controls signal focus with a border tint, not an outline — that is the rule in Accessibility and it is what the shared field recipe implements — but component-contracts.json, the machine authority that feeds the shipped design-system skill, still described "a focus-visible ring" for each. Both summaries are corrected, so an agent reading the packaged skill roster is told what the components actually do. The other 114 component summaries were audited for the same class of claim and hold.

    Doctrine, in the same pass: react-markdown and remark-gfm are now sanctioned renderer engines rather than an undocumented exception, and two version decisions are written down with their evidence — TypeScript stays at 6.0.3 while no shipped typescript-eslint supports TypeScript 7, and tw-animate-css stays bundled in preset.css because it is consumer-facing API that Quickstart and Troubleshooting both document. 78ed487

  • Alert, Empty, Item, Timeline, Collapsible, Accordion, Announcement Banner, Onboarding Checklist, Progress Indicator, Board and Dropzone — the pages now describe the polite live-region policy, the one Empty container axis, the group-licensed listitem role and the disclosure hover wash. Alert's "Strip variant" and Empty's "Illustration & value tiers" move above API Reference per the page canon, Alert and Announcement Banner gain a "Live announcements" section, and new fixtures cover Card, Announcement Banner, Onboarding Checklist and Timeline. docs · 8c18d2b

  • react-day-picker and next-themes are now named sanctioned engines in AGENTS.md § Sanctioned dependency exceptions and in design.md, with their one-file isolation written down. Both were already shipping and neither was on the list. react-day-picker v10 is the calendar state machine behind date-picker — Base UI ships no Calendar, and shadcn's Calendar is this package under every base; next-themes is the theme engine mounted only by provider, which sonner reads through useTheme() rather than mounting a second time. docs · 5b03e3b

  • Every component page now follows the page canon, and a gate says so. All 116 pages carry the canon's frontmatter (registry, status, since, a11y), the ## Installation heading is now ## Install, and the five generated sections — install steps, anatomy, API tables, states tested and the per-item changelog — are rendered from registry.json, component-contracts.json and CHANGELOG.md on every page instead of being hand-typed on three. tooling/content-lint.mjs enforces the section vocabulary, the section order, "nothing after Do / Don't except the Changelog", and generated-not-typed, with a --self-test that observes each rule failing; tooling/verify-docs-export.mjs additionally requires a playground or Story explorer to render under the page's ## Playground heading. The registry frontmatter field is required and is no longer inferred from the page slug, so a wrong or missing item name fails the build rather than composing the wrong shadcn add target. The AutoTypeTable alias for ApiTable is gone. On the Command page the live dialog demo moved to ⌘J, because the docs site itself owns ⌘K and both dialogs were opening at once. docs · 7153311

  • A component page's status and since are now machine authorities, not hand-typed prose. Both are recorded per component in packages/ui/component-contracts.json, written onto the page by pnpm design:derived, and compared against the contract by tooling/content-lint.mjs, so a page cannot claim a status or an origin version the contract does not hold. status was 116 identical stable strings with nothing behind them; every component is genuinely stable — each has a registry item, a docs page, a preview and required test coverage, and no ledger, changelog or source records a deprecation or a preview-quality component. since was derived once from git log --follow and is now pinned data, never computed: the values were verified against /CHANGELOG.md's enumerated release lists (0.2.0 1/1, 0.3.0 12/12, 0.4.0 12/12, 0.5.0 2/2) and against packages/ui/registry.json as it stood at each release commit (0.1.0 82/82). That check corrected one page — MediaPlayerControls now reads since: 0.7.0, not 0.5.0: --follow had walked into the audio-player source it was extracted from, while the item itself has never shipped. A component authored between releases carries the next version, and tooling/version-sync.mjs re-stamps it at version time with the version actually being released, so a different bump than the author guessed cannot publish a wrong since. docs · c352ba9

  • API tables are flat and expanded, on every page at once. One table per exported part — name, the literal union ("default" | "secondary" | …, not union), the @default value, the description — instead of collapsed accordion rows. This one lands everywhere immediately: the renderer is registered under the legacy AutoTypeTable name the 107 unmigrated pages author, so no page body had to change for it. Own props only, and a part with no own props of its own gets one sentence instead of the 138 "(no own props)" placeholder rows that filled 18 pages. A second table lists the data-* attributes and CSS variables the part exposes. example · 7915a71

  • Badge — a "Minimal — the dense-table treatment" section, real pixel sizes on the Sizes section, and a playground that gained the bordered switch and emits dot={false} when the switch disagrees with the variant's own default. docs · 7915a71

  • Button — the page is rebuilt around the matrix: a variant row, a tone row, and the full variant × tone grid with the forbidden solid + destructive cell called out. The "Lit finish" section is gone. IconButton gains Tones and Shape sections; SplitButton gains Tones. docs · 7915a71

  • Colors — a new surface-ladder specimen renders both themes side by side with the rungs and their alpha twins; the sidebar section now says the rail is aliases, not a second palette. docs · b2c2e96

  • Doctrine corrections in design.md — the hairline is described as the derived foreground alpha rather than a solid border; Button secondary as the rung-1 fill over a transparent base border rather than a card fill plus border; Input/Select/Textarea as transparent with a dark-only wash rather than a secondary fill; and the switch off-track as surface-3 rather than the removed track token. The Sidebar page's active-row description matches the shipped surface-3 rest / surface-2 hover behaviour. docs · b2c2e96

  • Progress, RelativeTime, TruncatedText, Kbd, StatusIcon, Skeleton — each page now explains the mechanism rather than the class name: why an indeterminate bar is a segment, why the first paint is an absolute date, why a grid turns tab stops off, and where reduced motion is actually handled. A duplicate empty heading on the Progress page is gone. docs · 7915a71

  • Elevation — "the two roles" is now "the one role"; every foundations page that named shadow-lit was corrected. docs · 7915a71

  • Fullscreen preview is the system Dialog. The old overlay declared role="dialog" aria-modal="true" and had no focus trap — Tab walked straight out into the hidden chrome behind it. Copy Prompt moved once into the page header (it was repeated six times on the Button page), the hero preview renders through the same frame as every other example, a skip link is now the first tab stop on every page, and the icon-gallery tile is a real labelled button instead of 439 nameless focusable divs. accessibility · 7915a71

  • llms.txt carries the registry roster and the skill roster — every installable item with its page and its shadcn add target, and the public agent skills — so an agent can go from "I need a data grid" to the page and the install command without scraping. guide · 7915a71

  • The markdown export is real markdown. The per-page .md route and llms-full.txt previously emitted <AutoTypeTable …/> and <ComponentPreview …/> verbatim — 107 of 110 component pages and 260 occurrences in llms-full.txt — so an agent reading the docs saw no props and no example code at all. Every MDX component now renders to markdown: the exact fixture source the Code tab shows, the flat prop tables, the install steps, the do/don't pairs. Browser-only surfaces are replaced by a one-line note rather than dropped silently. guide · 7915a71

  • One page canon, for humans and agents alike — the infrastructure, and three reference pages. The canon defines a fixed page shape whose machine-readable half is generated from the two authorities rather than typed: Install from registry.json (the shadcn add command, the registry dependencies and the sanctioned engines), Anatomy from the contract's new dataAttributes, the states-tested table from the contract's states, and a per-item Changelog filtered out of /CHANGELOG.md. This release ships those generated sections as MDX components and places them on three reference pages (button, dialog, data-grid); the remaining 107 pages keep their current bodies and are migrated to the canon in the next release. The canon table is design.md §Docs canon — the target shape, not a description of every page today. canon · 7915a71

  • Per-family variants are documented as eight tokens, not sixsubtle-hover and subtle-active (the soft fill's hover and pressed steps, precomposed at build time and AA-gated against -text) were missing from the Colors page, and from design.md, which called the ramp seven tokens. docs · b2c2e96

  • The docs shell obeys the design system it documents. Fumadocs' chrome and the typography plugin are compiled against Tailwind's stock theme, so headings, sidebar titles and prose <strong> rendered at weight 600–900 in a system whose ladder is 400/500, cards used rounded-xl, and popovers used the stock shadow ladder. All of it is remapped to system values once. Demos also sat on the 15px/28px prose base because the product type scope re-bound the --type-* vars but not the inherited font-size. foundations · 7915a71

  • Theming and Colors no longer teach a removed token — both pages used bg-primary/(--alpha-surface-subtle) as the worked example of an override flowing through the @theme inline bridge. That alpha role was deleted with the ladder, so the example compiled to nothing; both now show the fillInteractive recipe's real washes. docs · b2c2e96

  • The shipped design-system skill is refreshed for Button's variant × tone matrix, the single xs · sm · md · lg size vocabulary, and IconButton as the only icon-only path. No runtime code changed. guide · 7915a71

  • Field, Checkbox, FieldInline, Textarea, Motion — every page that described the error as role="alert", the shake as per-control, or the textarea focus treatment as a removed outline now says what the code does. Textarea gains the size example it never had, NumberField gains its invalid fixture and folds its "Scope" table into Usage, and Label documents layout. docs · 9fbeb65

  • Docsdesign.md gains a §Prose section (one recipe, why it is expressed as descendant rules, and the cascade trap that makes an element-level typography class a silent no-op inside a prose root) and its §Motion vocabulary gains the docked pair with the rule the pair encodes: an exit is never slower than its enter. The component skill's motion-mechanism matrix gains a fourth row for a control that stays mounted at a viewport edge and flips data-active. text-edit's "Scope" moves into its canon position and its keyboard table documents the toolbar's arrow-key traversal. docs · 2a3fc24

  • design.md §Charts and §Brand & marketing. A single series is drawn in foreground ink (--chart-single), not a categorical hue: the ramp encodes which series is this, a distinction that does not exist when there is only one — hue starts at two series. Two new §Brand & marketing subsections: Promotion is a ladder rung plus a tinted hairline, never info and never a full-strength chromatic border; Marketing leaves records the three conventions the leaf components encode — quotation marks from CSS rather than characters, a logo wall of marks rather than links, and a scroll reveal that fails open. The component skill §4 states the responsive ladder explicitly: container query → viewport breakpoint → useMediaQuery, and a JS media branch must declare its server answer. 3663f8f

  • Plan and ledger — withdrew the verification rebuild plan tooling line-count target, dropped WP3b with the measured reasoning, and recorded the post-rebuild audit including the decision to leave main unprotected while the audit epic merges. 5d2c705

  • Ship skill — corrected a false claim about CI on the Version PR: runs on the changesets bot branch sit at action_required and never execute, so the publish is gated by quality-gate on main before and after the merge, not by checks on that PR. 5aba886

  • Docs

    • Corrected the shipped 0.1.0 changelog entry: the initial release carried 83 components, not 75 (and the docs showcase had 99 pages, not 91). Recomputed against the release commit with the repository's own path-derived classifier, which counts icon-button as the component it is. The animated-icon and hook figures were already right.
    • Replaced the hand-maintained runner roster in the Linux CI runbook — and its two copies elsewhere — with the one gh api command that returns live truth, keeping only the per-host facts that command cannot return.
    • Reconciled the hover/pressed rule in AGENTS.md and design.md with what tooling/design-lint.mjs actually enforces (hover-without-pressed), and added the sr-only accessible-name separator to design.md § Accessibility as doctrine.
    • verify-component-contracts no longer tells the reader to run a command that cannot fix a drifted expectedCounts key.
    • Versioned the audit epic's orchestration briefs into the repo, and recorded the epic's review rounds — three Codex, the rest independent Opus reviewers — in the ledgers. c68a35b
  • DocsAGENTS.md is a rulebook again (150 lines): locked decisions as one-liners with dates, the three verification loops, and the four artefacts a PR carries. The CI history it used to carry moved to docs/ledger/operator-review.md, and the component, review, and ship skills now describe only machinery that exists. d65088b

🐛 Fixed

  • Animated icons — the reduced-motion effect ran after every render in all 439 icons, because it was written without a dependency array. It now runs when the preference changes, once, in the factory. docs · b2c2e96

  • AppShell's skip link now targets its own content region. The skip link and AppShellContent both hard-coded the id main-content, so a page holding more than one shell published that id twice and EVERY skip link resolved to the first region — measured on the app-shell docs page, where four embedded previews each carried it and the documented "Tab once, press Enter" flow landed the reader in the wrong preview from every frame but the first. AppShell now mints the id once with React.useId() and shares it to AppShellContent, with a new contentId prop for when the id has to be known (a deep link, an external aria-controls, a test harness). Setting id on AppShellContent moves the element but does not rewire the link; that is what contentId is for. Sharing a generated id is why app-shell.tsx now carries a client boundary at the shell root — createContext/useContext are unavailable under the react-server condition — which is where SidebarProvider's own client context already lives; page content passed as children still renders on the server. docs · 64ba720

  • Release readiness — close the modal focus leak across Dialog, AlertDialog and Sheet; isolate the animated-icon gallery to its own route; make the docs-shell and export checks fail closed over both fixes; and stabilize the complete cross-engine suite with trusted interactions, settled animation baselines, and sequential browser processes. 515da26

  • The notification badge pops for real activity only. NotificationBell read a mount ref during render and flipped it in an effect that scheduled no re-render, so the pop-in class first landed on whatever unrelated re-render happened next: a parent state change animated the badge with no new notification behind it. The previous count is now held in state and the cue is replayed through useAnimationReplay when the count rises after mount and the badge visibly changes — so it never fires on mount, never on a re-render, and never for 100 → 101 (both read "99+"). The Timeline hero fixture, which sat under the 24px pointer-target floor, is lifted off it. docs · 8c18d2b

  • tiptap 3.31.3 loaded prosemirror-model twice, and TextEdit's suite died on it. @tiptap/pm@3.31.3 declares prosemirror-model directly and resolved to 1.25.11, while its siblings stayed on the 1.25.9 the pre-bump lockfile already carried — pnpm install only re-resolves what it must. ProseMirror compares node types by object identity, so a fragment built by one copy is unconvertible by the other (RangeError: Can not convert <> to a Fragment). pnpm dedupe collapses the tree to a single 1.25.11 and removes 34 packages. packages/ui/vitest.config.ts also stops pre-bundling clsx and tailwind-merge, which are @vegastack/design's dependencies rather than @vegastack/ui's and printed a resolve failure on every run. 5b03e3b

  • The provider unit test no longer races sonner's toast timer. Sonner's toast store is a module singleton, so a toast fired by one test kept rendering — close button and all — into the next test in the same file for its full 4 s lifetime, and an unscoped getByRole("button") there became a strict-mode violation under load. The toast is now dismissed and drained where it is fired, and the theme probe is queried by accessible name. Test-only; no component changed. 5b03e3b

  • An indeterminate Progress no longer reads as 100% complete. Base UI writes no inline width when value is null, so an indicator styled only for the determinate case inherited the track's full width — an upload in progress looked finished. It is now a 35% segment sweeping the track on the one sanctioned looping utility, motion-indeterminate, whose keyframes rest on the same frame at both ends so reduced motion leaves a static segment rather than a full bar, and aria-valuenow is omitted. docs · 7915a71

  • The prefers-reduced-motion: reduce reset zeroes delays too. It gains animation-delay: 0s !important and transition-delay: 0s !important. Zeroing duration alone left a staggered entrance sequencing over its real-time delay window, which is motion; with the delay zeroed the whole sequence lands at once, and no component needs a motion-reduce: restatement of its own. docs · 7915a71

  • RelativeTime no longer renders an empty first frame. A relative label needs Date.now(), which the server cannot reproduce, so it used to render "" until hydration — a visible pop and a layout shift on every row of a list. Server and hydration render now agree on the absolute date ("Mar 15, 2025"), derived from the target instant alone, and the swap to the relative label is a text change inside a box that is already the right size. docs · 7915a71

  • Docs — the icon gallery was unusable by keyboard and inert on touch. Each tile was a focusable <div> with no role, so all 439 were reachable and announced as nothing; each is now a real <button>. A tile also drives its icon through a ref, and holding the ref suppresses every trigger the icon provides for itself, including its tap-to-play pointerdown — the tile replaced hover and focus but not that one, so on a phone nothing in the gallery ever moved. Tiles now carry the tap driver too, under the same pointer-type rules. docs · b2c2e96

  • The surface-ladder specimen showed the wrong swatches — the "alpha twins" panel labelled two swatches --alpha-hover / --alpha-pressed while painting the opaque surface-2 / surface-3 rungs, so it demonstrated the opposite of the twins' claim. It now paints the real foreground composites over three hosts (page, card, well) in both themes, with the opaque rung beside each wash for comparison. docs · b2c2e96

  • Text-entry focus under forced colours — Input, Textarea, NumberField, OTPInput and TextEdit signal focus with a border tint and outline-none. Windows High Contrast replaces border-color outright, so a focused field showed no indicator at all. The token layer now paints a real 2px outline under forced-colors: active, once, for every text-entry control. docs · b2c2e96

  • Text on the soft media scrim is gated at AAmedia-foreground on media-scrim was checked only against the 3:1 non-text floor while the token contract permitted labels on it, so the contract was wider than its enforcement. The pair is now gated at 4.5:1; it measures 5.22:1 over the white worst case, so nothing moves today and a future scrim retune that thins it under AA fails the build instead of silently demoting its labels. The media-scrim, media-scrim-strong and media-foreground descriptions state where text is allowed and which floor enforces it. No token value changes. docs · b2c2e96

  • Media chrome no longer inverts in dark — the video scrim and its controls were built from primary, which flips with the theme, so in dark the scrim rendered near-white with near-black icons. New theme-invariant --media-scrim, --media-scrim-strong and --media-foreground tokens keep overlay chrome dark-scrim + light-ink in both themes. docs · b2c2e96

  • Four controls shipped with utilities silently destroyed, and a Field > Textarea was unlabelled. In five places two adjacent class-string literals were concatenated with no separating space, so JavaScript welded them into one word and the utility on both sides of the seam vanished. The Switch had no track colour in either state — measured background-color: rgba(0, 0, 0, 0) and padding: 0px unchecked and checked, with on/off conveyed only by thumb position on a background-coloured thumb; only a hovered checked switch painted, so the control appeared under the cursor and nowhere else. The switch thumb ran on Chromium's default curve instead of --motion-ease-standard; a focused OTP slot wore the global 2px focus ring that text entry exists to suppress; and the NumberField steppers rendered at full --foreground with no hover step. All four are repaired, and design-lint gained a structural class-glue rule that rejects the seam at the AST — the existing rules read one literal at a time and could not see it, which is why transition-pairing passed on an element with no ease token.

    <Field label="…"><Textarea /></Field> produced a textarea with no id, no aria-labelledby, no aria-describedby and no aria-invalid: the <label for> pointed at nothing, the error message was not linked, and axe reported label at critical. Textarea now renders through Base UI's Field.Control, like every sibling control, so the wiring and the destructive border tint arrive automatically. Standalone use is unchanged.

    Also fixed: aria-invalid was accepted and inert on a standalone OTPInput (it landed on the root, never on the slots) and on a standalone NumberField (the group's :has() selector cannot match the group's own attribute); a read-only EditableCell with a select editor rendered the raw stored value where the editable cell rendered the option label; and a borderless Field showed no resting border when invalid. docs · 02ba364

  • Text-entry focus under forced colours, on a field with addons, where the outline was being clipped. A bordered field group — Input's prefix/suffix wrapper, NumberField's stepper group, ChipInput, the Combobox input-group — clips with overflow-hidden so its addons follow the rounded corner, and the inner input's outline is offset outward into that clip. It was drawn and then cut, so an addon field still had no visible focus under the forced palette. The group carries the outline now (via a bare data-field-group attribute that base.css hooks) and the control inside stands down, so the two never double-ring. docs · 9fbeb65

  • Text-entry focus — a text field's border is its only focus channel, and two other states were taking it. An aria-invalid field kept its destructive border when focused, and a Field borderless control kept its transparent one, so both showed no focus indicator at all (WCAG 2.2 §2.4.7). The invalid tint in fieldControl / fieldControlGroup and on TextEdit's container now stands down on focus/focus-within, borderless flattens only while unfocused, and Input's outline-hidden — lost to a missing space in a string concatenation — applies again. The focus tint is now contrast-gated as the composite users actually see: 4.04–4.51:1 light, 6.31–7.72:1 dark. docs · cff5ccb

  • Two shipped WCAG 1.4.3 failures in brand and status colour, and the three gates that could not see them. The destructive Bubble was the only place in the registry that used a solid FILL token as body text — text-destructive over bg-destructive/(--alpha-soft-surface) measured 5.24 / 4.31 / 4.44:1 in light and 2.56 / 2.37 / 1.78:1 in dark across rest/hover/pressed, and its light ladder inverted (hover L 0.874, pressed L 0.883) because the pressed step jumped to a precomposed token sitting on a different ground. It is now the same soft recipe the four soft Buttons use — bg-destructive-subtle / -hover / -active with destructive-text ink — measuring 5.80 / 5.13 / 4.69 light and 6.14 / 5.02 / 4.61 dark, monotone in both. The tinted variant's pressed step, which composited to L 0.921 against a surface-3 hover at 0.922, moves to --alpha-ink-tint-strong so a press is visible. The cta Button painted its 0.75rem/400 mono label in text-brand, a 3.5:1 MARKER value, measuring 3.41 / 3.33 / 3.21:1 in light on the public docs playground; the family now ships brand-text, the page-readable half every chromatic family already has, and the label re-measures 5.93 / 5.80 / 5.59 light and 11.41 / 10.90 / 10.13 dark. The hovered-link dim (--alpha-link-hover, on the link Button, every rendered rich-text link through prose, and PropertyList) composited success/info/warning ink to 4.03–4.11:1 in light at 80% and is now 88%, re-measured 4.74–4.83:1. --alpha-soft-hover and --alpha-soft-surface are removed: their only consumer was that Bubble line, and --alpha-soft-hover was a second, 13pp different answer to the role sd-hooks.mjs's SUBTLE_HOVER_ALPHA already owns. --font-display / --font-pixel are now bridged into @theme inline, so D17's sanctioned Geist Pixel flourish is reachable. Gates: contrast-check.mjs measures brand-text and the link-hover composite (both observed failing on the pre-fix theme); design-lint gains fill-token-as-text and field-group-pairing, both with negative fixtures; verify-token-references and design-lint now cover packages/design/src, where every shared recipe lives and where a bogus token previously exited 0; and verify-docs-base-mirror now mirrors the ::view-transition-* reduced-motion companion rule the docs copy had silently lost. docs · 18e2208

  • A Timeline's last row lost the bottom of its pointer target. TimelineItem's <li> uses content-visibility: auto for render skipping, which brings paint containment with it, so anything a child paints outside the box is clipped and stops being hit-testable. timeline-content dropped its bottom padding on the last item, and a trailing RelativeTime — whose 24px pointer target comes from a 4px before:-inset-y-1 overhang — had that overhang fall outside the clip: the effective target collapsed to the row's own 23px, under the WCAG 2.2 SC 2.5.8 floor. The last row now keeps 4px of bottom padding, exactly the depth of that hit area. Trailing whitespace under the final row grows by 4px; nothing else moves. docs · 6a5a49e

  • Pointer targets — nine WCAG 2.2 §2.5.8 misses recorded by the geometry lane are closed at the root. IconText's row and a Marker rendered as a link or a button each carry an invisible hit area, so a clipped row and an action marker measure 24px to the pointer and not one pixel differently to the eye. In the docs, the attachment demo composed its actions above the card trigger — the reverse of the documented order, which made the remove button unclickable — the scroll-fade demos now reflow at 320px instead of scrolling the page sideways, and the message-scroller outline entries sit on a target-sized pitch. The lane itself learned two facts it was missing: an inert control accepts no pointer action, and a role="tabpanel" is not a target. docs · 1b11aa1

  • The video player's tooltips and settings menu are visible in fullscreen. They portaled to <body>, which the browser does not paint inside a fullscreen element — the playback-speed and quality menus were unusable in fullscreen. MediaPlayerControls takes a portalContainer and VideoPlayer passes its frame; the volume panel already avoided this by rendering inline. docs · aa5fa0d

  • The video overlay's seek rail rests at its own thickness and thickens on engagement again. Its h-1 tied on specificity with the shared track's h-1.5, so Tailwind's sort order picked the default 6px rail and the hover/focus thickening had nothing to thicken from. docs · aa5fa0d

  • A viewer's chosen playback speed no longer resets while the media plays. The media-element effect listed the consumer's onTimeChange among its dependencies; timeupdate fires ~4×/s and re-renders the controls, so a player given an inline callback re-applied defaultPlaybackRate several times a second and 2× snapped back to 1×. docs · aa5fa0d

  • Fixed — the geometry contract lane (packages/ui/test/geometry.css) imported the token theme and base layers but never utilities.css, so every @utility in the system — scroll-fade-*, scrollbar-thin, motion-pop-in, the new motion-dock-* pair — compiled to nothing and all 523 fixtures were measured against a layout no user ever sees. A custom utility with no definition is an empty rule, not an error, so the gap only ever made the contracts weaker. The lane now imports the same layer production does. 2a3fc24

  • useIsMobile reported false on the server, so SSR rendered the desktop layout on a phone until the effect ran — Board enabled pointer drag and then disabled it. It now renders the serverFallback the caller declares. usePlatform's isTouch was frozen at the post-hydration value; the primary pointer can change mid-session — a 2-in-1 detaching its keyboard — and a drag affordance gated on isTouch has to follow it, so that half is now the live (pointer: coarse) query. docs · 3663f8f

  • ParticleField froze the brand colour of the theme it mounted in. The ink was read once, into a const, inside an effect keyed on nothing the theme touches — so a light-mounted field kept the light --brand after a toggle to dark until something forced a remount. It now reads the canvas's own resolved color per frame, and the single static reduced-motion frame repaints on a theme change too. docs · 3663f8f

  • The collapsed-breadcrumb trigger was a 20×20 pointer target. Its visible box was the 20px BreadcrumbEllipsis glyph and nothing expanded it, so the one control that reveals a trail's hidden segments sat under the WCAG 2.5.8 24×24 CSS px floor — measured 20.00×20.00 on all three breadcrumb fixtures. BreadcrumbCollapsed's trigger now carries a transparent ::before expansion (relative before:absolute before:-inset-0.5) that brings the EFFECTIVE target to exactly 24×24 with no change to the visible glyph and no change to the trail's line height; 2px per side stays inside BreadcrumbList's 6px gap, so it never reaches into a neighbouring segment. BreadcrumbEllipsis is decorative and stays 20px — its doc comment now says the wrapping trigger owns the target, and the manual-composition example demonstrates it. docs · fdaed05

  • Four navigation and layout accessibility defects. BreadcrumbPage announced the current page as a disabled link — it rendered <span role="link" aria-disabled="true">, so screen readers described a non-interactive segment as a dimmed link; it is a plain <span aria-current="page">, and the ARIA misuse was inherited from shadcn. Focus rings were clipped on scroll viewportsScrollArea's and MessageScroller's viewports and the sidebar rail offset their focus outline OUTWARD under a clipping ancestor, so the ring was cut in half or lost; all three inset it now. Every shell and sidebar docs fixture rendered a duplicate <main> inside the docs page's own, failing axe's landmark-no-duplicate-main; they render landmark="region". And TabsContent, BreadcrumbCollapsed's trigger and AppShell's skip link each restated the global :focus-visible rule — two copies of one rule can only drift, so the copies are gone. docs · fdaed05

  • The DatePicker caption dropdown was a 21px-tall pointer target. captionLayout="dropdown" renders a transparent <select> stretched over the caption root (absolute inset-0), and that root — an items-center child of a 32px row — collapsed to its 21px line box, so the real control measured 50.36×21.00 against the system's 24px effective-target floor (WCAG 2.2 §2.5.8). The root now takes self-stretch, handing the select the row's full height. Nothing in that root paints, so the month/year label and its chevron are pixel-identical. Its entry in the geometry lane's EXCLUDED map is deleted. docs · 45cde26

  • registry:lib files are now modeled as shadcn-transformed in the shipped consume verifier. shadcn add removes a JS/TS file's entire leading comment prologue as it writes it, for every file type it touches — but verify-registry-item.mjs listed only registry:ui, registry:hook, registry:page and registry:component, so the first registry:lib items (geo-data, drag-item) compared the copy-in against unstripped source and failed post-write verification with a line-count mismatch — i.e. the gate reported a TOCTOU signal for a transform the CLI is sanctioned to perform. check-updates reads the same set, so its diffs were affected identically. 45cde26

  • Toasts appear again, and three appearance-probe defects close. The docs site mounted the registry copy-in Toaster under the package provider's ToastProvider, and each toast module owns a module-scope manager — so the viewport listened to one store while every preview's toast() wrote to the other, and the Toast page had been silently dead since toasts moved to Base UI. The copy-in now brings its own provider, a gate refuses a Toaster whose provider comes from a different module, and a browser test pins both halves of the rule. Tabs' count badge stacks its ink wash on whatever the trigger paints, which put muted ink at 3.43:1 on a selected pill in dark; it takes body ink now, the token gate learned to check a wash painted on a ladder rung, and the rendered-contrast lane covers every variant in both themes. DatePicker's data-day hook is a stable YYYY-MM-DD instead of a locale-formatted string, which is what made a prerendered calendar throw a hydration error in every browser whose locale was not the build host's, and the page now says prerendering a formatted date needs an explicit locale. NumberField's addon slots hold an interactive control off the field's hairlines and keep its focus ring out of the clip, so the money recipe's currency Select stops painting into the rule. 1e439d9

  • A table's selection checkbox did not own its own 24px hit area. TableHead and TableCell collapsed the trailing padding of a checkbox column to pe-0, so the checkbox's 6px ::before overhang fell outside its own cell — the neighbouring header's sort control owned part of the centred 24×24 square. Both cells now use pe-2, the least that contains the target, and the sort control no longer carries a negative inline margin (SortableHead narrows its own cell padding instead, so the label alignment is unchanged). docs · 6d25185

  • Two focus rings that never painted. Charts were focusable with no visible ring — recharts' accessibilityLayer makes the plot <svg> a tab stop, and ChartContainer's own .recharts-surface outline reset then poisoned --tw-outline-style on the very element that takes focus, so even the global :focus-visible rule resolved to outline-style: none. The reset is now scoped to :not(:focus-visible). And the DataGrid roving cell's ring was clipped, because the cell lives inside the table's scroll viewport, which clips its overflow — it is now inset. PropertyList values stop truncating for the same reason: the overflow: hidden that truncate implies was clipping the focus ring of any link inside a value. docs · 6d25185

  • The 320px contract check no longer races a re-rendering fixture — a fixture that re-renders on its own timer (relative-time reschedules a setTimeout) could detach between the visibility assertion and scrollIntoViewIfNeeded, failing the sweep with "Element is not attached to the DOM" on a different subset of Chromium projects each run — on unmodified main as well. The scroll is now a bounded retry that re-resolves the locator; the assertions, the RTL and 24px target-floor checks, and the fixture selection are unchanged. b2c2e96

⚠️ Breaking

  • Seven animated icons drop a deprecated handle alias; an eighth renames its handle type. BotMessageSquareHandle, ConciergeBellHandle, KeyIconHandle (on both key-circle and key-square), RefreshCCWIconWIcon (on refresh-cw), ActivityIconHandle (on square-activity) and ZapHandle were @deprecated aliases left behind by upstream naming quirks; each of those icons still exports its <Name>IconHandle and only the alias is gone. chevron-first is the different case and is a rename, not an alias removal: upstream had copy-pasted a displayName from another icon, so the primary interface was called ChevronsDownUpIconHandle and ChevronFirstIconHandle was the @deprecated alias of it. The exported component symbol is authoritative, so the name that survives is the one that matches it — ChevronsDownUpIconHandleChevronFirstIconHandle. Consumers of the old name must rename; no compatibility alias is kept. docs · b2c2e96

  • Alert is role="status", not role="alert", and Item takes role="listitem" only inside an ItemGroup. Every alert used to be an assertive live region, so a page rendered with three static alerts interrupted a screen reader three times before the user had read anything. The banner is now polite for every intent; pass the new live prop for a banner raised by a user action, and a destructive or warning intent then escalates to the assertive alert role. AnnouncementBanner drops its role="status" at load and takes the same live prop. Tests asserting getByRole("alert") on a static banner should read getByRole("status"). Separately, Item applied role="listitem" to every non-render row, so a standalone Item was an axe aria-required-parent critical and Timeline had to document role="none" as a workaround; ItemGroup now provides the context that licenses the role, and outside one a row carries no role at all. Remove any role="none" passed to work around the old default. docs · 8c18d2b

  • Empty has one container axis. variant: plain | card | dashed replaces the bordered flag crossed with surface, a pair that could ask for a dashed card and resolved it only by letting tw-merge pick a winner. Rename map: borderedvariant="dashed", surface="card"variant="card", surface="transparent" (the default) → variant="plain". The data-bordered and data-surface attributes are replaced by data-variant. EmptyTitle also takes an as prop, so the hard-coded <h3> no longer guesses at the host page's heading outline — pass as="h2" when the empty state replaces a page body, or as="p" when the surrounding card already carries the heading. docs · 8c18d2b

  • Badge speaks the same variant vocabulary as Button. variant is now solid · soft · outline · minimal: subtle is renamed soft, with no alias — a stale variant="subtle" is a type error. The three sizes become three REAL heights, sm 16px · md 20px · lg 24px; sm used to be md with 2px less horizontal padding, which is a padding value, not a size. minimal becomes ink only — no fill, no border, no horizontal padding — so it sits flush in a table cell instead of faking a pill, and it now carries a leading dot by default, because a badge with no container has nothing but colour left to signal status with (WCAG 1.4.1). Pass dot={false} to opt out, or the new icon prop to take the dot's place. docs · 7915a71

  • Kbd defaults to os="other". It reads no navigator — that is what keeps it server-safe — so the platform is the caller's to resolve: run usePlatform() and pass the answer down. The old default shipped mac glyphs to a Windows majority. TooltipKbd takes the same os prop. docs · 7915a71

  • border and input are translucent. border is derived as foreground at --alpha-border (8% light / 14% dark) so one hairline reads on the page, on a card, inside a well and on a dark band. Anything that assumed an opaque border value should read the variable instead. docs · b2c2e96

  • Light cards are page-coloured. card was oklch(0.985) against a 0.994 page — a grey slab no reference system draws. It is now the page colour, separated by the hairline alone; dark keeps its one-step lift. popover is card in both themes. docs · b2c2e96

  • Surface tokens are now one ladder. surface-1 / surface-2 / surface-3 — the rest-fill/well, hover and pressed/selected rungs — arrive with the theme-invariant alpha twins --alpha-hover (7%) and --alpha-pressed (10%). secondary, muted and accent were a single OKLCH value under three names, so no hover or pressed state could be seen on a card. They are now aliases of ladder rungs and have no independent values: secondary = muted = surface-1, accent = sidebar-accent = surface-2, sidebar = card, sidebar-border = border, sidebar-ring = ring. Existing bg-muted / bg-accent / bg-sidebar-* utilities keep compiling and keep their rest appearance; only accent moves (one rung darker, because it is the hover rung). Name the rung in new code. docs · b2c2e96

  • Button has no icon size tier. size="icon" / icon-xs / icon-sm / icon-lg are gone; every icon-only action is IconButton, which makes the missing aria-label a type error and now owns shape="square" | "round". docs · 7915a71

  • Button is two axes, not fifteen variants. variant is now the SHAPE — solid · soft · outline · ghost · link · cta — and the new tone prop is the HUE — neutral (default) · destructive · success · warning · info. Every recipe is written once as ten class strings and reads the hue from --btn-* custom properties, so all thirty cells share one hover/pressed grammar. Rename map: defaultsolid, secondarysoft, destructivesoft

    • tone="destructive", success/warning/infosoft + the matching tone, {family}-outlineoutline + the matching tone; outline, ghost, link and cta keep their names. A destructive action is never a solid red button — tone="destructive" with variant="solid" does not type-check. docs · 7915a71
  • disabled is aria-disabled, not the native attribute. Button, IconButton and SplitButton keep their pointer events and stay focusable when disabled, so a Tooltip can explain why the action is unavailable. Base UI still suppresses activation. Code asserting element.disabled should read aria-disabled instead. docs · 7915a71

  • One size vocabulary: xs · sm · md · lg. The tier every component called default is now md, matching the --size-* tokens it was always built from. This is a rename across Button, IconButton, SplitButton, Badge, Input, Textarea, Select, Combobox, Avatar, Card, Item, Empty, Kbd, Dialog, Switch, Checkbox, RadioGroup, Toggle, ToggleGroup, Segmented, Stat, Spinner, StatusIcon, Progress, ProgressIndicator, OTPInput, NumberField, Attachment, ChipInput, Pagination and Sidebar. There is no alias — size="default" is a type error. docs · 7915a71

  • Helper text moved below the control, and Field owns validation feedback. A field's description now renders under the control with the error under that — above the control it pushed the input away from its own label, and a wrapped description put two lines of prose between the two things the eye pairs. The invalid shake moved with it: Input, Checkbox, RadioGroupItem, OTPInput, NumberField and ChipInput no longer take shakeSignal and no longer shake on their own — Field does, for every control it wraps, so Textarea gains the behaviour it never had. A bare <Input aria-invalid> outside a Field still tints its border; wrap it in a Field for the motion, or move shakeSignal onto the Field. docs

    Inline validation announces as role="status", not role="alert". FieldError and FieldInline's error are polite live regions: the person just typed or submitted and is looking at the field, and alert interrupts whatever the screen reader was saying. alert stays reserved for something that arrives without being asked for. Tests asserting getByRole("alert") on a field error should read getByRole("status"). docs

    Form controls keep their pointer events when disabled. disabled:pointer-events-none is gone from Input, Textarea, NumberField, OTPInput, Select, Combobox, ChipInput, Checkbox and RadioGroup, matching the Button contract — an unavailable control must stay hoverable so a Tooltip can say why. cursor-not-allowed and the dim stay; Base UI suppresses activation either way. docs · 9fbeb65

  • MediaPlayerControls moved out of audio-player into its own registry item. It was exported from audio-player even though the video player was its main consumer. Run shadcn add @vegastack/media-player-controls and import from @/components/ui/media-player-controls; video-player declares it as a registry dependency, so a fresh shadcn add @vegastack/video-player pulls it in automatically. docs · aa5fa0d

  • Slider's internals are no longer restyled from outside. Anything reaching into [&_[data-slot=slider-track]] / -indicator / -thumb to build a media rail should pass variant="media" or variant="overlay" with thumb="hover" instead. The internal slots keep their data-slot names, but their rest appearance is now the variant's to decide. docs · aa5fa0d

  • Menu items take tone, not variant. DropdownMenuItem, ContextMenuItem and their checkbox/radio siblings use tone="destructive", matching Button's tone axis. The state attribute moves with the prop: items expose data-tone, not data-variant. docs · 7915a71

  • In-panel search fields are a searchbox. Every panel-search row renders type="search", so ShortcutOverlay's filter (and any other field inside the shared row) exposes the searchbox role rather than a generic textbox. Selecting one by role in a test or script must change with it. docs · 7915a71

  • Sheet runs on Base UI's Drawer. It gains swipe-to-dismiss, snap points (snapPoints / snapPoint / onSnapPointChange pass straight through) and SheetVirtualKeyboardProvider for bottom sheets containing fields. side moves from SheetContent to the Sheet root, because it selects the dismiss gesture as well as the pinned edge, and a side on the content could disagree with the gesture. SheetContent now sizes through sizesm · md · lg · full from the shared --panel-width-* vocabulary replaces className width overrides, and one tier means a width on a left/right sheet and a height on a top/bottom one. Swipe is always an addition: Esc, the close button and a backdrop press still close the panel. docs · 7915a71

  • Toasts run on Base UI, and the toast() API changed with them. sonner is removed from the system and the registry item is renamed sonnertoast. toast() now takes a title plus Base UI's options: action: { label, onClick } becomes actionProps: { children, onClick }, duration becomes timeout (and 0, not Infinity, disables auto-dismiss), and ids are strings. toast.message is gone — it was toast(). toast.custom now renders the toast BODY inside a real toast, so a custom notification keeps stacking, swipe-to-dismiss, Escape and the live region instead of opting out of them. Resolving a loading toast is toast.update(id, …) rather than re-firing with the same id. Toaster loses sonner's props: position values are logical (bottom-end, not bottom-right), expand is gone because the stack expands on hover by design, and offset / mobileOffset / theme are gone — the viewport carries the safe-area insets itself and reads the theme from the cascade. VegaStackProvider always mounts the toast context now: toaster={false} still suppresses the visible viewport — the part that must not mount twice — but the provider toast() writes into is unconditional, so a host rendering its own <Toaster /> shares one queue. @vegastack/design gains TIMINGS.tooltipOpenDelayMs / TIMINGS.tooltipCloseDelayMs, which the provider applies to Tooltip.Provider so every tooltip in an app shares one rhythm. @vegastack/design-tokens gains a third z band, --z-toast (60): the toast viewport mounts with the app provider, before any dialog exists, so DOM order alone would put every later-opened dialog on top of it — and a toast fired from inside a modal must stay visible. Sonner supplied that from its own private z-index, which is why elevation doctrine carried a library-shaped exception; it is now a token with exactly one caller. docs · d5e2de2

  • CountrySelect / RegionSelect internals. COUNTRIES, Country, getCountryByCode, REGIONS_BY_COUNTRY, Region, getRegionsByCountry and hasRegions are no longer exported from country-select / region-select, and region-select-data.ts is gone — import COUNTRIES, REGIONS, getCountryByCode and getRegions from @/lib/geo-data (getRegionsByCountrygetRegions; hasRegions(c)getRegions(c).length > 0). RegionSelect no longer clears by re-selecting the current state; clearing is the explicit clearable control on the trigger, on by default. Both selects now render a wrapper, so data-slot="country-select" / "region-select" is on the wrapper and the trigger carries the -trigger suffix. docs · 45cde26

  • Table cells wrap by default. Every head and cell carried whitespace-nowrap, so one long value forced the whole table to scroll instead of wrapping at a word boundary. Body cells now use overflow-wrap: anywhere over a --table-cell-min-width floor, and scrolling is reserved for tables that are genuinely wide. Two column shapes opt back out automatically — align="end" figures and the new mono columns — and DataListColumn.nowrap / DataGridColumn.nowrap override the inference either way. A layout that relied on single-line cells should set nowrap: true (or whitespace-nowrap on a raw TableCell). Two further breaks land with it: DataGrid's mobile posture now defaults to "merge" rather than "hidden", so overflow columns stack into the primary cell instead of disappearing — mobile: "hidden" is still available, and when it drops anything the toolbar states "N columns hidden"; and Table's containerClassName is removed, because it did exactly what containerProps.className does. docs · 6d25185

  • useDragReorder and useFileDrop return Announcer, not getLiveRegionProps(). Render <reorder.Announcer /> / <drop.Announcer /> in place of <span {...reorder.getLiveRegionProps()} />. The props-getter shape could not keep the region mounted across an announcement, which is the property that makes it audible. docs · 7915a71

  • Pagination no longer hard-codes its accessible name. It renders a plain <nav> (no role="navigation") and aria-label defaults to "Pagination". A page with more than one pager must name each one — two identically named landmarks are an axe landmark-unique failure. docs · 7915a71

[0.6.0] — August 31, 2026

🔧 Changed components

  • AudioPlayer — rebuilt transport with a responsive, two-line mobile layout. On a wide player it is a single line: play/pause, rewind and forward (±15s), an elapsed / duration readout, a flexible seek, and a fixed-width tappable speed control that cycles the playback rates (1x → 1.25x → 1.5x → 2x → 0.5x). Audio carries no volume control — mute stays on the M key. On a narrow (mobile-width) player it reflows to two lines: the seek bar with elapsed and duration pinned to either edge in a smaller font on top, and a centred play/pause flanked by rewind and forward on the bottom, with an optional transcript control (new onTranscriptClick, lucide audio-lines) on the leading edge and the speed control on the trailing edge. The chrome and progress fill move from the brand-bold primary to a subdued secondary emphasis, and the waveform seek now fills continuously instead of one bar at a time. VideoPlayer's overlay controls are unchanged. docs · 43eb359

📦 npm

  • @vegastack/design0.3.2 — refreshes the shipped Audio Player skill reference for the reworked transport and its two-line mobile layout. No runtime code changed.
  • @vegastack/design-tokens is unchanged. The design-system registry (@vegastack/ui) bumps 0.5.0 → 0.6.0.

[0.5.0] — August 28, 2026

🧩 New components

  • AudioPlayer, VideoPlayer — media players sharing one custom transport: grouped play / seek / mute / settings controls, a 128-bar waveform seek on audio, a smoothly expanding video progress rail, a contained volume rocker, larger overlay actions, state-aware fullscreen, and keyboard shortcuts. docs · 334cb4c

🔧 Changed components

  • ProgressIndicator — new value-display variants render the percentage inline or contained within the glyph. docs · 334cb4c
  • Sidebar — the desktop sidebar and its footer now stay pinned to the viewport while page and navigation content scrolls. docs · 334cb4c
  • Switch — the invalid state no longer applies a destructive track border; aria-invalid remains the semantic cue and the wrapping Field owns the error message. Consumer-visible restyle. docs · 334cb4c

🐛 Fixed

  • DatePicker, ComparisonMatrix, PricingSection, Tabs, FieldInline — manual-QA remediation across range selection, compact navigation, overflow handling, and responsive pricing and comparison layouts. docs · 334cb4c

🛠 CLI & tooling

  • Cross-engine smoke and the full three-engine suite now treat WebKit as host-conditional — it runs where it can launch and is skipped with an auditable banner where it cannot (macOS 26.6.2 cannot launch Playwright's WebKit). Chromium + Firefox coverage is never blocked, and a Mac on macOS 26.2–26.5 still enforces WebKit. Control with WEBKIT_LANE=auto|off|require. b6b1bd0

📚 Docs

  • Improved the published examples for dropzones, menus, charts, mobile shells, and the dashboard starter. 334cb4c

📦 npm

  • @vegastack/design0.3.1 — ships the regenerated component-roster skill reference (the two new players, the ProgressIndicator variant note, updated counts). No runtime code changed.
  • @vegastack/design-tokens is unchanged. The design-system registry (@vegastack/ui) bumps 0.4.1 → 0.5.0.

[0.4.1] — July 28, 2026

🐛 Fixed

  • Stepper — horizontal step content is now width-constrained so long labels truncate instead of overlapping adjacent steps at the 320px responsive contract width. docs

[0.4.0] — July 27, 2026

🧩 New components

  • ActionBar, ChipInput, EditableCell, FilterBuilder (filter-bar-managed), NumberField, ShortcutOverlay, Stepper, and Timeline — eight additions from the CRM commission (plan 2026-07-26), each with complete docs, state coverage, accessibility tests, and registry integrity metadata. components
  • SortableList, Board, Dropzone, and DataGrid — the four dependency-gated commissions, unblocked by MK's 2026-07-27 sanction of four engines (Pragmatic drag and drop, react-dropzone, TanStack Table, TanStack Virtual). Reorderable rows with a lossless keyboard Move menu; a kanban board with cross-column card movement and per-move lock reasons; a paste-capable file drop surface; and the full-parity grid — multi-key sort, column picker with responsive revelation, collapsible grouping, keyboard-continuous load-more, opt-in virtualization, and APG cell navigation with inline editing. components
  • useListNav, usePlatform, useDragReorder, and useFileDrop — four new registry hooks: roving-tabindex keyboard navigation for lists and grids (RTL-aware arrows, homeEndScope, overlay suppression); SSR-safe platform detection ({ os, isTouch }) for Kbd's modifier rewriting and touch gating; the reorder seam over Pragmatic drag and drop (pointer drags, commit-per-step keyboard move mode, server-refusable moves); and the file-drop seam over react-dropzone (drop + paste + browse, typed rejection reasons). guide

🔧 Changed components

  • Kbd — mac modifier glyphs now pair the visual glyph with visually hidden spoken names ("Command", "Option", …), so screen readers no longer hear "place of interest sign" or nothing. docs
  • Table — new containerClassName / containerProps forwarded to the data-slot="table-container" scroll wrapper, the attachment point for sticky headers, fixed-height viewports, and virtualizers. docs
  • DataListDataListProps now extends TableProps (the spreadsheet voice and container hooks type-check), columns gain a per-cell cellClassName hook, and render receives an optional third DataListCellContext argument. All additive. docs
  • FieldInline — additive controlled edit mode (editing / onEditingChange) and a tabIndex override for the display element, so EditableCell and grid hosts compose it instead of forking it. No behaviour change for existing consumers. docs
  • ColorPicker and EmojiPicker — internal refactor onto the shared useListNav hook; the API and visuals are unchanged, with one behavioural correction riding along: EmojiPicker's horizontal arrow keys become RTL-aware, matching ColorPicker. Safe to take or skip. docs
  • Button — the active:translate-y-px press nudge is removed from buttonVariants, so every component composing Button (IconButton, SplitButton, toolbars, pickers) loses it in one place. Pressed feedback is now colour-only via the existing active:bg-* states, and the motion foundations doctrine records press-motion as a deliberate exclusion. docs

🛠 CLI & tooling

  • verify-registry-deps gains a fail-closed npm-range check: a registry item pin the installed version cannot satisfy now fails registry:build instead of passing silently.

📚 Docs

  • The npm dependency pins the installed versions could not satisfy are reconciled to packages/ui/package.json: lucide-react was declared at both ^1.20.0 and ^0.525.0 across a major boundary (41 items), and @shadcn/react at ^0.1.0 against an installed ^0.2.1.
  • table.mdx no longer claims the Table parts add no props; data-list.mdx documents that a column render is invoked as a plain function (hooks belong in a returned component element).
  • The docs homepage and 404 page now pass nativeButton={false} to every Button rendered as a link, matching the Base UI contract for non-button render targets; button.mdx and the bundled design-system skill document the pattern, and motion.mdx records the press-nudge removal.

[0.3.0] — July 24, 2026

🧩 New components

  • AnnouncementBanner, CodeBlock, ComparisonMatrix, NavigationMenu, OnboardingChecklist, PricingSection, PropertyList, RuledBand, Segmented, Stat, TagGroup, and ToolCallChip — twelve production-ready additions with complete docs, state coverage, accessibility tests, responsive previews, and registry integrity metadata. components · 6c60d53

🔧 Changed components

  • MarkdownView — remote image sources are now same-origin by default; explicitly allow trusted remote origins with allowedImageOrigins. The renderer and copied-file verifier also enforce contained paths and digest-pinned post-write checks. docs · 6c60d53
  • Provider and all portal-owning components now share an explicit theme-scope contract, while server-safe modules are verified under React's real react-server condition. docs · 6c60d53

🛠 CLI & tooling

  • vegastack-design skills install now bundles and safely installs the four public VegaStack agent skills for Claude Code and Codex, with atomic conflict handling, symlink containment, dry-run, and single-surface options. guide · 6c60d53
  • vegastack-design check-updates now verifies installed file bodies and the complete target set; matching provenance headers can no longer hide edited content, and removed or renamed targets fail --fail-on-update. guide · 6c60d53
  • Registry builds now prune stale artifacts, reconcile exact authority/index/manifest sets, and prove real shadcn consumption against locally packed npm artifacts before release. integrity · 6c60d53

📦 npm

  • @vegastack/design 0.2.0 — adds bundled public agent skills, the skills installer, CommonJS-compatible exports, and the explicit ./theme-scope client subpath while preserving a server-safe root. 6c60d53
  • @vegastack/design-tokens 0.2.0 — publishes the unified DTCG-backed doctrine, strong type and effect roles, exact dark/marketing parity, and verified ESM/CommonJS/package exports. 6c60d53
  • Private @vegastack/ui advances to 0.3.0 to stamp the 538-item registry; it remains private and is never published to npm. 6c60d53

📚 Docs

  • The public design doctrine, component contracts, homepage catalog, API reference, agent guidance, internal SSO corpus, metadata, and release/cutover runbooks now derive from current machine authorities and build successfully in both private and public visibility modes. design doctrine · 6c60d53

🐛 Fixed

  • State-specific accessibility coverage now exercises open, expanded, selected, disabled, highlighted, loading, empty, error, success, and removable states across the applicable component contracts; all token contrast pairs pass WCAG 2.2 AA gates in both themes. accessibility · 6c60d53
  • Release and deploy workflows now use the reviewed-merge/manual-dispatch approval model supported by the private GitHub Team repository, retain isolated OIDC and repository secrets, and probe the Cloudflare Access boundary after deployment. 6c60d53

⚠️ Breaking

  • vegastack-design verify --post-write now requires --expected-integrity; use the exact integrity-pinned command printed by the pre-write verification. MarkdownView also requires an explicit allowlist for cross-origin images. verification · MarkdownView · 6c60d53

[0.2.0] — July 19, 2026

🧩 New components

  • Provider — the app-root wrapper (theme, toasts, tooltip coordination, text direction); install once via shadcn add @vegastack/provider, composes the sonner Toaster item. docs · c7de692

🔧 Changed components

  • Checkbox, Switch — Story explorer controls narrowed to human-usable props (the raw Base UI prop graph serialized to ~24 MB per page, over Cloudflare's 25 MiB asset limit). No API change. checkbox · switch · 45c7cf8
  • dashboard-01 (block) — component files now target app/dashboard/components/ (the page's relative imports were broken on clean installs) and page.tsx ships the default export Next requires for route files. docs · ac6288a
  • use-animation-replay (hook) — stray duplicate header line removed (caused a false drift flag in check-updates). ac6288a

🛠 CLI & tooling

  • vegastack-design check-updates is header-optional: the shadcn CLI strips provenance headers on copy-in, so copies are now identified by filename against the registry index and compared by alias-normalized content. New ≈ drift status (differs — upstream update or local edits); --fail-on-update fails on update + drift. guide · ac6288a
  • Release workflow is path-routed: the container pixel gate runs only when component-visual code changes; the release job only when something is publishable. e8a8450

📦 npm

  • @vegastack/design 0.1.1tw-animate-css is a real dependency (was an optional peer that pnpm never installed, breaking every fresh consumer build at preset.css). Published via npm OIDC trusted publishing — the first fully-automated release. 9532d42
  • @vegastack/design-tokens stays 0.1.0 (no changes).

📚 Docs

  • New Guides section — Quickstart, Registry access & auth, Working with components, Provider setup, Theming, Production checklist, Troubleshooting. Every command executed for real against the reference starter before being written down; 29 adversarial-review findings applied. 7307231 · 11836b6
  • Sidebar sections labeled; doc pages carry a server-rendered "Last updated" stamp. a323cba
  • Lifecycle guides for both audiences: Internal projects (the two-speed update model: npm = global on dep bump, registry = frozen until reviewed pull; the token-ordering trap) and Client projects (delivery model, handover checklist, zero-lock-in dependency table). Repo also gains a maintainer README. The internal guide is now SSO-only and intentionally absent from the public corpus; the client-project guide remains public.

[0.1.0] — July 18, 2026

🧩 New components

  • Initial release: 83 components, 439 animated-icon items, 2 hooks (use-mobile, use-animation-replay), and the dashboard-01 starter block — all Base UI + Tailwind v4, semantic-token-only, WCAG 2.1 AA, distributed as copy-in via the private registry at design.vegastack.com/r/* (Cloudflare Access service-token auth, Sigstore-signed manifest). components · 8a5bb2a

📦 npm

  • @vegastack/design 0.1.0cn(), the icon runtime (./icons), the Tailwind v4 preset (./preset.css), token CSS re-exports, and the vegastack-design CLI.
  • @vegastack/design-tokens 0.1.0 — the zero-dependency DTCG token contract (theme.css, base.css, utilities.css, tokens.json).

📚 Docs

  • At the 0.1.0 release, the Fumadocs showcase had 99 pages, live previews rendering the real shipped source, and complete desktop/mobile VRT coverage; it was deployed behind Cloudflare Access SSO. Current route and baseline counts are verified dynamically rather than inferred from this historical release entry. 8a5bb2a

How you receive updates

Two delivery paths, because the system is hybrid — the full operating model per audience: Internal projects · Client projects.

Packages (@vegastack/design + @vegastack/design-tokens) are normal npm dependencies — bump them with your package manager when a new version publishes. They share a Changesets linked group: releases that touch both give them the same version; otherwise versions may differ (e.g. design 0.1.1 alongside design-tokens 0.1.0 — nothing changed in the tokens).

Components are pulled, never pushed. They are copied into your repo by the shadcn CLI, so an upstream change does not silently rewrite your files. You decide when to re-pull, review the diff, then overwrite.

# 1. See what changed upstream (content comparison against the live registry)
vegastack-design check-updates

# 2. Review the diff for a component before taking it
shadcn add @vegastack/button --diff

# 3. Pull the update in, overwriting your copy
shadcn add @vegastack/button --overwrite

Status is by content, not version number

check-updates identifies your copies by filename against the registry index and compares alias-normalized content (a provenance header, when present, is only a fast-path pin). A component reads up to date when the global version bumped but that component's content didn't change — you only re-pull what actually moved. ≈ differs means the bytes diverged — an upstream update or your local edits; add --diff disambiguates.

Every registry item carries a meta.integrity SHA-256 plus a provenance header in its registry content, and the consume flow supports a fail-closed verify (vegastack-design verify). If a hash doesn't match, the add is rejected rather than silently trusted.

Do
Run check-updates, then shadcn add --diff before --overwrite so you review every incoming change.
Don't
Assume a global version bump means a component changed — status is by content, not the version number.

On this page