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 qualityresult 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
mainneeds no automated direct-push bypass.3dd6a85
📦 npm
@vegastack/design→0.4.1(was0.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 themotion-indeterminateutility — the one sanctioned looping animation, the sweeping segment of an indeterminateProgress. Its keyframes open and close on the same resting frame, so the globalprefers-reduced-motionreset leaves a static 35% segment rather than a bar that reads as complete. docs ·7915a71 -
surfaceInteractiveandfillInteractive— the two hover/pressed recipes, exported from@vegastack/designso no component writes ahover: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. TheFillTonetype ships with them. docs ·b2c2e96 -
A layout scale, and
--chart-single.--layout-header-height,--sidebar-width-mobile,--layout-overlay-max-heightand--panel-width-sm|md|lggive the shell and the overlay family named dimensions instead of per-component literals, and--chart-singlenames 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 (
allValuesplus aparentchild 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 noCheckboxGroupItem— a child is a plainCheckboxwith avalue. docsuseInlineEdit— 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).FieldInlineandEditableCellhad each written it, and the copies had already drifted — only one re-armed the guard when a controlled host flippededitingon, 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 aSelectpopup with no text input at all. docs ·9fbeb65 -
28 new
lucide-animatedicons adopted — the mirror moves 439 → 467. Upstream had drifted 28 items ahead of the pinned manifest, and becausetooling/mirror-animated-icons.mjsfails closed on an unexpected upstream item count,--refreshcould not be run at all. The new items arebinary,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-downandwaves-arrow-up— eachshadcn add @vegastack/icon-<name>, each generated by the mirror rather than authored, and each inheriting the singlecreateAnimatedIconcontroller 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 ownlucide-reacticon module rather than to an alias of one already mirrored. Registry items go 568 → 596.tooling/verify-animated-icons.mjsgains 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-testgrows 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 ofaudio-player(which owned the video player's controls too). It carries the control surface anduseMediaShortcuts: ONE keyboard map for both players (Space/K play, J/L and arrows skip, M mute, F fullscreen), scopedsurfacevscontrolsso a shortcut can never steal a key from the focused control. TheassignRef/getMediaDuration/clampTimehelpers live here as well, instead of in a copy per player.audio-playerdrops from 1,431 lines to 305. docs ·aa5fa0d -
use-media-query— the system's onematchMediasubscription, onuseSyncExternalStorewith a caller-declaredserverFallback. Five files each hand-rolled the sameuseState(false)+useEffectshape, and every one of them reportedfalseon the server, so a phone rendered the DESKTOP branch of every JS-driven layout until an effect ran. ShipsusePrefersReducedMotionas its named reduced-motion reader;useIsMobileandusePlatform's touch half are now one-liners over it. docs ·3663f8f -
floating-surface— the shared floating-overlay module every anchored overlay now composes: onePortal → Positioner → Popup (→ Viewport)composer, one theme-scope hand-off across the portal boundary, one arrow, and four painted surfaces (panelat the 16px tier,menuat list density,tooltipas the inverted ink chip,navigationfor the morphing mega-menu). It also ownsmenuItemVariants— the one list-item recipe behind menu items, select options, combobox options and command rows — andPanelSearchFrame, 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,Toastand every part (ToastRoot,ToastContent,ToastTitle,ToastDescription,ToastAction,ToastClose,ToastViewport,ToastPortal,ToastPositioner,ToastArrow), plususeToastand the imperativetoast(). Stacking with expand-on-hover, swipe-to-dismiss,F6into the viewport landmark andEscapeon the focused toast come from the primitive; promise toasts and a customrendercome 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 writesloading/success/errorinpromise()itself, while the tokens follow the house families. The live-region policy is enforced in code:errorandwarningannounce urgently, everything else politely, derived from the type. docs ·d5e2de2 -
geo-dataanddrag-item— the first tworegistry:libitems: plain.tsmodules that install under a consumer'slibalias and are pulled in automatically as dependencies.geo-dataholds 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.json67 KB → 7.4 KB).drag-itemholds the one visual recipe for ause-drag-reorderitem — drop-edge hairlines, lift dim, pending shimmer — whichBoardandSortableListhad 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-widthpanel and an optional clear control.CountrySelectandRegionSelectare now thin data-fed wrappers over it. Two rules it exists to hold: selection runs through Base UI'svalue/onValueChangeand nothing else (the oldRegionSelectcomputed 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 chromeDataListandDataGridhad 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 throughregistryDependencies.SortableHeadandSortHeaderButtoncompose the systemButtoninstead of a hand-rolled<button>, and emitaria-sorton every sortable column ("none"included). The doctrinal split is unchanged —DataListstays presentational,DataGridkeeps 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 backsTruncatedText's clipping check, theTablescroll region's focusability,ComparisonMatrixandTerminal's command pane. docs ·6d25185 -
chip— THE labelled pill, and the only one.hue(10 decorative tag hues, or neutral) ×size(sm28px inline ·md32px control-scale) ×active(the neutral chip's promotion to thesurface-2selection rung), withonRemovemounting a round ghostIconButton size="xs"whose real border box is 24×24.Tag,FilterChip,ComboboxChipand ChipInput's chips are all this one primitive composed through Base UIrender. docs ·7915a71 -
use-announcer— the one polite live region. DestructureannounceandAnnouncerfromuseAnnouncer()and render theAnnouncerelement 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, andAnimatedIconComponenttypes its host asHTMLSpanElement. 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.2useReducedMotion()isuseState(prefersReducedMotion.current)— a one-shot read of a module singleton captured at first import, with a standingTODOabout not updating — anduseReducedMotionConfig()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()returnsfalseoutright when the context saysreducedMotion: "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 explicitreducedMotion="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, thesizeprop and thestartAnimation/stopAnimationhandle 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.
ItemTitlemoves from 12px totext-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 droppedhover:underline— underlining on hover is the link affordance — and tooksurfaceInteractivetogether 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-rolledrole="progressbar"next to the primitive that already draws one, so it now composesProgressIndicator segments; the primitive gainssegmentsFill(segments share the container width instead of a fixed bar width) and accepts a single segment. A navigableStepperlabel is now alink-variant Button instead of aghostButton with its height and padding stripped to imitate inline text. docs ·8c18d2b -
Base UI 1.6.0 → 1.8.0,
@shadcn/react0.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 newdata-pending-scrollattribute withinvisible, so a server-rendered transcript no longer paints the top of the thread for one frame before jumping to the bottom —visibility: hiddenrather thandisplay: noneon purpose, because the primitive measuresclientHeight/scrollHeightto decide where to scroll and a display-none viewport measures zero. A regression test asserts the attribute never sticks.message-scroller's declared@shadcn/reactrange moves to^0.3.1; it anddate-pickercarry new integrity hashes. Two user-visible upstream changes are kept as shipped: areadOnlySelect/Combobox now opens and browses (reached througheditable-cell), and start/end-aligned popups take their pop-in--transform-originfrom the aligned edge. docs ·5b03e3b -
One key chip, and the display leaves move onto role tokens.
TooltipKbdrendersKbd 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);mdreached the same 12px throughtext-sm, the same pixel size named twice, and the meaninglesspointer-events-noneon a<kbd>is gone.StatusIconsizes become the--icon-inline/default/action/featurerole tokens (14 / 16 / 20 / 24px), the ladder Spinner already uses, instead of rawsize-Nsteps spelling the same four values. ASkeletonline 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. Thebase.cssreset 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, soStaggeredTextRevealstill 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 —buttonVariantscould not be read from a server component. 84 client leaves in the registry became 72. docs ·7915a71 -
text-xsis mono-only. Seven sites across Attachment, AudioPlayer, Chart and ProgressIndicator were reaching 11px in Geist Sans for density; sans copy now floors attext-sm(12px). docs ·7915a71 -
TruncatedTextgainsfocusable, with aTruncationFocusProviderthat 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,TableCellTextandRelativeTimetake 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 onsurface-3so hovering it still moves. ComparisonMatrix and PricingSection stop usinginfo(blue) for the promoted column and the highlighted plan —infois 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— notvisibility: 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 nowIconButton, 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. NewiconButtonGeometry(size, shape)styles an icon-only link: navigation stays a real<a>(PageHeader's back affordance) instead of acquiringrole="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/fieldControlGroupin@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. docsCheckbox, 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
Labelisinline-flexby 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. docsPasswordInput's eye toggle is an
IconButtonin the ghost recipe, and the eye swap has no motion. It replayedmotion-pop-inbehind a guard whose only job was to stop the animation firing on first paint — a tell that the animation did not belong there. docsFieldInline 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. docsAutoSaveInput drops a
cn(className)no-op, sizes its status slot with--icon-defaultinstead of a rawsize-4, and marks its spinnerdecorative. docs ·9fbeb65 -
AttachmentTriggershows a real focus indicator. It droppedoutline-noneand 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
Slideropened 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 -
Imagelazy-loads and decodes off-thread by default.loading="lazy" decoding="async"are now the defaults, matching what MarkdownView already did for its images. Passloading="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-strongand--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 areIconButton variant="ghost" shape="round"on the scrim. docs ·aa5fa0d -
Media controls keep the system's focus outline. The
ring-2 ring-ring/50glow that media invented for itself, and the forced-colours carve-out beside it, are gone; the standard 2px:focus-visibleoutline applies, inset with-outline-offset-2so anoverflow-hiddenframe cannot clip it.tabIndex={0}now appears only on genuinely scrollable regions. docs ·aa5fa0d -
Slidergrows 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),marksandshowValue. Every[&_[data-slot=slider-*]]override in the players is deleted. docs ·aa5fa0d -
VideoPlayergainscontrolsVisible.truepins the overlay open andfalsekeeps 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
proserecipe. Both restated the same heading, paragraph, list, quote and code recipe in their own grammar and had already drifted (h4–h6, 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 ofCodeBlock'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+Tabout. It was arole="toolbar"of eight independent tab stops. - ActionBar is a Base UI
Toolbarwith newActionBarButtonandActionBarSeparatorparts. 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-outpair 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,outlinealready is a page-coloured face with the one hairline and the surface-ladder hover.2a3fc24
- MarkdownView and TextEdit share the
-
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-foregroundand lift totext-foregroundon 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, andwallColumnsis now a MAXIMUM over anauto-filltrack 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-3with its hairline tintedprimaryat--alpha-outline-border. A full-strengthborder-primaryread as the active/invalid state of a form control — the wrong signal on a card someone is being invited to choose. docs ·3663f8f -
SettingsSectiongainedtitleAs(h2…h6, defaulth3). 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
selectedChipVariantsrecipe from@vegastack/design, and a selected chip now hovers and presses again — it used to be excluded from both bynot-data-pressed:*/not-data-[active], so the one chip a user is most likely to click was the one that answered nothing. The Tabslinetrigger'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.SidebarProvidergainspersist(defaulttrue) around the cookie write:persist={false}keeps the component out ofdocument.cookieentirely andonOpenChangefires 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.AppShellContentandSidebarInsetgainlandmark="region", which renders a<div role="region">instead of a<main>for a shell embedded in a page that already owns one.SidebarTriggeris anIconButtonrather than a hand-rolleduseRenderbutton, so it inherits the one box, ink and hover/pressed grammar, andPageHeader's back affordance swaps its physical-ml-2for a logical-ms-2.PageHeader's active favourite star fills withforegroundinstead ofwarningink, 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 takesduration-base(200ms) because it resizes between items rather than simply appearing; the modal family — Dialog, AlertDialog, Sheet — isduration-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-heightinstead of a hand-written100dvhcalc.DialogContentand Command size throughsize. docs ·7915a71 -
Popover, HoverCard, Tooltip, DropdownMenu, ContextMenu, Select, Combobox, NavigationMenu — eight lookalike overlays became one module. Each now composes
floating-surfaceinstead of restating its own portal, positioner, popup surface, arrow and theme-scope plumbing.ContextMenuis bound to the same item parts asDropdownMenuthroughcreateMenuParts(Base UI'sContextMenunamespace re-exportsMenu'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
Inputinside a bordered popup drew two borders;PanelSearchFrameis 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-fulllike every other form control: the fixedw-56andw-72were the only fixed-width controls in the system and overflowed a 320px content area. The calendar caption is now a realauto 1fr autogrid (navLayout="around") instead of an absolutely positioned nav over apx-7clearance, 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 newcolumnMaxHeightprop. Dropzone paints the drag-over state on its own surface, so a dropzone wrapping anything other than anEmptyfinally shows one, and a newdragStateprop 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 (axescrollable-region-focusableon the Table page and the dashboard block), whileComparisonMatrixandTerminalcarried an unconditional tab stop that was dead on every instance that fits. Each viewport now measures itself throughuseOverflowand takes a tab stop only while it can actually scroll; name it withscrollLabel(falling back to the table'saria-label) and it is exposed asrole="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 andgrouprole stay unconditional.TableandTerminalboth 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.DataGridgainscolumnPicker(defaulttrue): 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.DataListandDataGridgain amonocolumn flag (mono numeral face plustabular-nums,nowrapby default) and anowrapflag; together with the shared chrome that is 249 fewer lines across the two files.TableRowno 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.PropertyListbecomes a container query: the label track was a fixed 112px regardless of the pane, and is now content-sized above an 80px floor at@xsand stacked below it, with values wrapping instead of truncating.Chartmoves axis labels from 11px to 12px (11px is reserved for mono) with the numerals on the monotext-code-smtier, 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'sPaginationLinktakes its hover and pressed steps fromsurfaceInteractiveinstead of restatedhover:bg-surface-2/active:bg-surface-3literals. docs ·7915a71 -
Tag, FilterChip, ComboboxChip, ChipInput, TagGroup — all now compose the
chipprimitive. Three consequences are visible: aTagis 28px rather than 20px and aFilterChipis a pill rather than a rounded rectangle (chips arerounded-fullby doctrine); the neutral chip rests onsurface-1and an applied filter sits onsurface-2instead of theaccentalias; and every remove control is the same 24×24 target. That last one fixesComboboxChipRemove, a bare 16px box with no hit-area expansion at all (a WCAG 2.5.8 failure), and retiresTag's::beforehit area, which a nested native<button>clipped and so never actually expanded anything. TagGroup's+Noverflow control is itself a chip, so the whole 28px pill is the pointer target and its hover/pressed steps come from the sharedsurfaceInteractiverecipe rather than a hand-written descendant selector. docs ·7915a71
🗑 Removed / renamed
-
trackis removed. The slider rail, progress track, skeleton and every well aresurface-1; the switch off-track issurface-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/80opacity dim),--alpha-input-hover(the dark-only input hover wash) and--alpha-surface-subtle(the outline button's hover tint, now--alpha-hoverin the family's own hue). docs ·b2c2e96 -
Button
glass, and the seven colour-in-the-name variants. The frostedglassvariant had no product consumer; media chrome uses the theme-invariant--media-*tokens instead.success,warning,info,destructive-outline,success-outline,warning-outlineandinfo-outlinebaked a colour into a name and are replaced by thetoneaxis. docs ·7915a71 -
Button
finishand the--shadow-littoken. 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. docsshakeSignalon Input, Checkbox, RadioGroupItem, OTPInput, NumberField and ChipInput — the prop lives onField, which owns the shake. docsSpinner label=""as the way to say "decorative" —decorativeis 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 -
mergeRefsno longer ships from theuse-animation-replayregistry item, andmedia-player-controlsno longer exportsassignRef. Both were spellings of the same merge; the one implementation now lives in@vegastack/designand every registry file imports it there alongsidecn.grep -rn 'typeof ref === "function"' packages/ui/registry/uiis 0. docs ·3663f8f -
AlertDialogContent intent— it wrote adata-intenthint and nothing else, leaving two props named for one concept with one of them inert.AlertDialogAction intentis the single owner of a confirmation's tone. docs ·7915a71 -
Hand-written
100dvhoverlay height calcs — replaced by--layout-overlay-max-heightand 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-visiblebox-shadow glow the design system bans everywhere else. Toasts are now@vegastack/toast. The toaster-mirror gate stays but points attoast.tsx— the private@vegastack/uipackage still needs a byte-identicalToaster— and thesonner-human-facing-aliascontract exemption is gone, because it existed only while the registry item was namedsonnerand its page was/docs/components/toast; the two now match. docs ·d5e2de2 -
TagHue— the hue vocabulary moved to the Chip primitive and is exported asChipHuefrom@vegastack/chip.Tag'shueprop 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-testthat proves each assertion still fails on its own defect.@vegastack/uiis private and neither published package changes.09b2107
🛠 CLI & tooling
-
Animated icons are one factory plus 439 data modules. Every mirrored
lucide-animatedicon 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 increateAnimatedIcon, exported from the new@vegastack/design/create-animated-iconsubpath, and each icon is acreateAnimatedIcon({ … })call describing only its geometry, its Motion variants, and (for 49 icons) its non-default start/stop steps.motionbecomes an OPTIONAL peer dependency — only an animated icon pulls it in, soIcon/BrandIconconsumers 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.mjsemits the data modules and fails closed on any upstream archetype it cannot model;tooling/verify-animated-icons.mjsasserts 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 inpackages/ui/animated-icon-sources.jsonso a hand-edited path or timing value is rejected outright, and carries a--self-testthat 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 aminimumReleaseAgeExcludeentry topnpm-workspace.yamland installs the too-new version anyway; explicit, the install fails withERR_PNPM_NO_MATURE_MATCHING_VERSIONand writes nothing. The two stalefumadocs-*@16.10.5excludes were written by pnpm that way, not by a human, and are deleted.@hookform/resolversmoves 5.4.0 → 5.9.1 in the same pass:zodResolver's overloads did not accept a schema built by zod 4.5, sofield-form.test.tsx— which exists to fail the typecheck when the RHF/Zod surface drifts — stopped compiling. It did its job.deploy.yml'swranglerVersionpin followsapps/docsto 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.
enablePrerenderSourceMapsandexperimental.turbopackFileSystemCacheForBuildare defaults in 16.3 and were deleted.next devin 16.3 writes a managed agent-rules block into anAGENTS.md/CLAUDE.mdin the Next app directory when it detects a coding agent;agentRules: falseturns 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/browser4.1.9 → 4.1.11 (critical: provider commands bypassedallowWrite),@tiptap/*3.27.4 → 3.31.3 (prototype pollution + paste XSS),postcss8.5.19 → 8.5.28 (source-map file read),style-dictionary5.5.0 → 5.5.2 (prototype pollution), plus theshadcn4.21 bump that clearspostcssundershadcn/tsup. Measured on the same advisory database on 2026-09-09,pnpm auditgoes 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-dictionaryis held at 5.5.2 deliberately: 5.5.3 touchescolor/cssalpha 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 buildoutput is byte-identical andverify-shadcn-consumestill passes against the real CLI. 4.21.0 makes thecnpackage the upstream default for generatedlib/utils; we did not adopt it —cn()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
playwrightdevDependency 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.mjsderives the required Linux job image (mcr.microsoft.com/playwright:v<version>-noble) from the singleplaywrightversion the lockfile resolves, soci.yml,release.ymlanddeploy.ymlnow pinv1.63.0-noble.auto-install-peers=truealso makes pnpm resolve Next's optional@playwright/testpeer even though no manifest declares it, and left alone it stayed on 1.61.0 — twoplaywrightversions in one lockfile, which that gate refuses. Apnpm-workspace.yamloverride pins the phantom peer to the same version, so the container tag keeps exactly one authority.3b37dde -
verify-component-contracts --write-data-attributesrecords each registry part'sdata-*attributes and CSS variables incomponent-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-testdrifts adataAttributesrecord 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}.cssimported only part of the layer set production ships, so every custom@utilitya fixture wore compiled to nothing and both lanes measured a page no user sees;verify-test-css-layersnow derives the required set from the shipped preset and fails closed.pnpm lintgains a repo-wideprettier --check. A changeset may no longer link a commit — the only sha it can name is a pre-merge one the squash orphans — andchangelog-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/testfinally type-checks,verify-component-contractsderives the inventory counts from the registry instead of hard-coding them, six token-vocabulary design-lint rules land with negative fixtures, andverify-token-referencesfails closed on a--tokenthat 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-setupdefaulted its bootstrap directory to~/setup-pnpm— which it deletes on every job, and whichPNPM_HOMEalso made the package store. Concurrent jobs raced that deletion intoENOTEMPTY(or a half-linkednode_modulesthat turbo reported asunable 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-pnpmand installs with--store-dir "$RUNNER_WORKSPACE/pnpm-store"— per-agent, and persistent — and the three jobs that enabledsetup-node's package-manager cache no longer do, since it cached the directory the next job deleted.verify-workflow-security.mjsasserts 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-npmfails closed instead of fail-open, anddocs/RELEASING.mdmatches reality. The registry probe spawnednpm viewwith cwd = the repo root, wherepackage.jsondeclaresdevEngines.runtimenode 24.20.0; npm enforces that field, does not honour pnpm'sonFail: download, and exitsEBADDEVENGINESon any other Node before it reaches the network. The oldstatus !== 0branch read that as "not published", so every push tomainreported both live public packages as unpublished, forcedpublish=true, ranquality-gate, and armed the OIDC-capablepublishjob on changeset-free pushes (release run 34323665258 showsunpublished: @vegastack/design (none) → 0.3.2with 0.3.2 live). The query now runs in an empty temporary directory carrying a copy of the repo.npmrc, out ofdevEngines' reach but still on the repo's own registry mapping, andnpm view --jsonis parsed into three outcomes: published, absent (a genuineE404— the only answer that may mean unpublished), and unknown (engine refusal, transport, auth, missing npm, unparseable output). An unknown never contributes topublish, is retried once, and exits non-zero when it was the only thing that could have setpublish— fail-closed and loud rather than afalsethat was never established. Nine cases intooling/test/release-detect.test.mjspin it against a stubbed npm; six of them fail against the previous script.docs/RELEASING.mdno 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 mergedbuild-sign-deployjob rather than the deleted three-job artifact split, and records that releases deliberately create no git tag or GitHub release.AGENTS.mdgains that tag decision and reconciles two deviations from the verification-rebuild plan.af61a44 -
design-lint --docs-shell --emitted-cssreads 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 gates —
pnpm verifycan no longer be neutered in a workflow. The workflow-security gate now matches the command as the ENTIRErun: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 bareexit 0), rejects aWEBKIT_LANE/SMOKE_WEBKIT/ cleared-CIenv:at workflow, job or step level (each silently droppedverify:releasefrom three browser engines to two), pins--ignore-scriptsper job in both directions — the npm-OIDCpublishjob now installs without lifecycle scripts, likeversion-pr— and checks--frozen-lockfileline by line. Eleven new negative cases, all observed rejecting. The hover/pressed recipes are now measured for real: the@sourceglob 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-detectno longer reads an unanswerablegit diffas "nothing changed",workspace-cleanrefuses 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-cssincluded.47ddb18 -
verify-docs-base-mirrordiffs the rule blocksapps/docs/app/global.csshand-copies frombase.css, which had no gate. It counts@applyas 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-exportfails 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(docslint) proves the agent export fails closed on the three failures that leave no artefact behind forverify-docs-exportto 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=requireinstead ofauto, 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-erroron the deploy'sverifyjob (a failed sweep still deployed to production), the same key on ci.yml'spnpm verifystep (a green check on a red suite), andif: falseonpnpm verify:release(the step stayed in the file and left the run). The gate now rejectscontinue-on-erroranywhere without a recorded allowlist entry, forbids anif:on any verification step and on the deploy's sweep job, asserts the Cloudflare command exactly (deploy --dry-rundeployed nothing and reported success), pinswranglerVersionagainstapps/docs/package.json, requires the publish-time lifecycle guard and the standalonenpm publishline (afalse &&prefix published nothing), requiresSITE_VISIBILITYin all three workflows,ref: ${{ github.sha }}on every outward checkout,--frozen-lockfileon every install, a concurrency group and a boundedtimeout-minuteson every job, andverify-macos's required steps including thechangeset statuspresence check.publishloses thecontents: writeandpull-requests: writeit never used, every job declares a timeout instead of inheriting the 360-minute default, ci.yml declares the sameSITE_VISIBILITYas 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/testover the docs export. No published package changes:@vegastack/uiis 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.mdrelease 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.mjsgroups those bodies into the## [x.y.z]entry at version time, insidepnpm run version-packages, andsync-changelogregenerates the docs page as before.tooling/changeset-lint.mjsrejects a body with no marker, two markers, or no text. Nobody hand-edits/CHANGELOG.mdbetween 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-mdx15.4.0,fumadocs-typescript5.4.0,@fumadocs/story1.3.0;fumadocs-twoslashtakes the TS 6-safe 3.3.1 patch rather than 4.0, which needs TypeScript 7) is two migrations rather than a bump.fumadocs-core16.14 replaced Orama with ZBSearch behind the same module path —oramaStaticClientis now a deprecated alias forstaticClient, the client builds its own database, and theinitOramafactory plus the direct@orama/oramadependency are gone.fumadocs-typescript5.4 swapped ts-morph for the native TypeScript 7 API, so the docs' own-props filter reads a property symbol'sdeclarations[].pathinstead ofgetDeclarations()[].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
dtheme hotkey is inert here —RootProvidermounts its window-levelkeydownlistener inside thetheme.enabled !== falsebranch 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 inactiveTabspanels, so aComponentPreview'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 noforceMountwas 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
LucideIcontype — 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 expandsaria-prohibited-attrandrole=image, and the browser axe lane reports no new violation. recharts 3.9.2 → 3.10.1 deprecatesLegend'salign/verticalAlignin favour ofposition/offset; noChartLegendcall site passes either andLegendstill injectsverticalAligninto custom content, soChartLegendContentkeeps reading it. Onlychart's registry item changes, and only because its documentation comment records that migration.3b37dde -
@tanstack/react-table8.21.3 → 9.2.4, indata-gridalone. v9 replacesuseReactTablewithuseTableand 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-griddeclares exactly one —rowSortingFeaturewithcreateSortedRowModel()— which turns the sanctioned-exception boundary from a claim into something the module reads back:columnVisibilityFeature,columnOrderingFeatureandrowSelectionFeatureall exist in v9 and none is adopted, because column visibility, column order and row selection aredata-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) andmanualPaginationwent withrowPaginationFeature, where it was already inert. The four built-in comparators v8 kept permanently in its registry —alphanumeric,basic,datetime,text— are registered explicitly in thesortFnsslot sogetAutoSortFnresolves the same comparator per column as it did under v8. No public prop, type or behaviour ofDataGridchanges;@tanstack/react-virtualis untouched. docs ·36ebf9d -
Vitest 5.
vitest4.1.11 → 5.0.0,@vitest/browser-playwright4.1.11 → 5.0.0 andvitest-browser-react2.2.0 → 2.3.0 across the workspace — the runner under every test in the repository, including the browser suitepnpm verifygates on. Vitest 5 matches locator text and accessible names whole-string by default and makestoHaveTextContentwhole-string equality (toMatchTextContentis 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 namedActivity3. Vitest 5 also consolidates every run artifact under one.vitestdirectory, soworkspace-clean.mjs's tree walk for the old.vitest-attachmentsdirectory 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.
motion12.42.2 → 13.2.0 (its only import site is the animated-icon factory; the 13.0 removal of the optional@emotion/is-prop-validdependency does not apply — no CSS-in-JS library wraps amotioncomponent here — anduseReducedMotion()is still the one-shotuseStateread the factory deliberately replaces with a liveuseSyncExternalStoresubscription).react-dropzone19.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-drop2.0.1 → 3.1.0 plus-hitbox2.0.0 → 2.2.0, whose 3.0.0 renamed every entry point:use-drag-reordernow imports from/adapter/element-adapter,/utils/combine,/closest-edge/attach-closest-edge,/closest-edge/extract-closest-edgeand/typesrather than the deprecated compatibility shims.@testing-library/jest-dom6.9.1 → 7.0.1, which makes@testing-library/doma required peer — now declared explicitly at 10.4.1.globals16.5.0 → 17.12.0, whose 17.0.0 split theaudioWorkletenvironment out ofbrowser; the shared ESLint config usesbrowser+nodeonly. 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/designexports the field-chrome recipes.fieldControlandfieldControlGroupare 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-inputat rest, the neutral--alpha-border-subtleink on hover, theringtint on focus. Hover is guarded bynot-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 baredata-field-groupattribute — that is what@vegastack/design-tokens'base.csshooks to paint the forced-colours focus outline on the group, whoseoverflow-hiddenwould otherwise clip the inner input's own.Also
surfaceInteractiveGroup— the group-scoped twin ofsurfaceInteractive, 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 themotion-dock-in/motion-dock-oututilities — the docked-control enter/exit pair for a control that stays mounted at a viewport edge and flipsdata-active(a bottom action bar, a floating scroll-to-edge button). 150ms in onease-emphasized, 100ms out onease-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 ordinarytranslate-*utilities, because it is per-dock geometry and atranslatedeclaration inside the utility would clobber a horizontally-centred bar's composed transform.2a3fc24 -
@vegastack/design— exports theproserecipe — 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.proseClassNameis the whole recipe;proseis the per-element record it composes from, keyed by theProseElementtype.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 composingprose, never by setting a class on the rendered element.2a3fc24 -
@vegastack/designexportsmergeRefs— fan one DOM node out to several refs (a forwardedrefprop plus one or more internal refs) as a single ref callback, handling both shapes React 19 accepts and skippingnull/undefinedentries. It was exported from theuse-animation-replayregistry item, an odd home for it, while nine registry files hand-inlined the sametypeof 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 Tabspill/chip,Segmented, and pressedToggle/ToggleGroup, which had drifted into four different selected looks (bg-background,bg-secondaryplus a hairline,bg-foreground/10). The track issurface-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 astrack,item, and two state literals:pressed(Base UIdata-pressed) andactive(Base UIdata-active). docs ·fdaed05 -
The design-system registry (
@vegastack/ui) bumps 0.6.0 → 0.7.0.
📚 Docs
-
The
inputandtextareasummaries 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 — butcomponent-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-markdownandremark-gfmare 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 shippedtypescript-eslintsupports TypeScript 7, andtw-animate-cssstays bundled inpreset.cssbecause 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
Emptycontainer axis, the group-licensedlistitemrole 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-pickerandnext-themesare now named sanctioned engines in AGENTS.md § Sanctioned dependency exceptions and indesign.md, with their one-file isolation written down. Both were already shipping and neither was on the list.react-day-pickerv10 is the calendar state machine behinddate-picker— Base UI ships no Calendar, and shadcn's Calendar is this package under every base;next-themesis the theme engine mounted only byprovider, whichsonnerreads throughuseTheme()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## Installationheading is now## Install, and the five generated sections — install steps, anatomy, API tables, states tested and the per-item changelog — are rendered fromregistry.json,component-contracts.jsonandCHANGELOG.mdon every page instead of being hand-typed on three.tooling/content-lint.mjsenforces the section vocabulary, the section order, "nothing after Do / Don't except the Changelog", and generated-not-typed, with a--self-testthat observes each rule failing;tooling/verify-docs-export.mjsadditionally requires a playground or Story explorer to render under the page's## Playgroundheading. Theregistryfrontmatter 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 wrongshadcn addtarget. TheAutoTypeTablealias forApiTableis 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
statusandsinceare now machine authorities, not hand-typed prose. Both are recorded per component inpackages/ui/component-contracts.json, written onto the page bypnpm design:derived, and compared against the contract bytooling/content-lint.mjs, so a page cannot claim a status or an origin version the contract does not hold.statuswas 116 identicalstablestrings with nothing behind them; every component is genuinelystable— 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.sincewas derived once fromgit log --followand 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 againstpackages/ui/registry.jsonas it stood at each release commit (0.1.0 82/82). That check corrected one page — MediaPlayerControls now readssince: 0.7.0, not 0.5.0:--followhad walked into theaudio-playersource it was extracted from, while the item itself has never shipped. A component authored between releases carries the next version, andtooling/version-sync.mjsre-stamps it at version time with the version actually being released, so a different bump than the author guessed cannot publish a wrongsince. docs ·c352ba9 -
API tables are flat and expanded, on every page at once. One table per exported part — name, the literal union (
"default" | "secondary" | …, notunion), the@defaultvalue, the description — instead of collapsed accordion rows. This one lands everywhere immediately: the renderer is registered under the legacyAutoTypeTablename 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 thedata-*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
borderedswitch and emitsdot={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 × tonegrid with the forbiddensolid+destructivecell 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 derivedforegroundalpha rather than a solid border; Buttonsecondaryas 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 asecondaryfill; and the switch off-track assurface-3rather than the removedtracktoken. The Sidebar page's active-row description matches the shippedsurface-3rest /surface-2hover 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-litwas corrected. docs ·7915a71 -
Fullscreen preview is the system
Dialog. The old overlay declaredrole="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 focusabledivs. accessibility ·7915a71 -
llms.txtcarries the registry roster and the skill roster — every installable item with its page and itsshadcn addtarget, 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
.mdroute andllms-full.txtpreviously emitted<AutoTypeTable …/>and<ComponentPreview …/>verbatim — 107 of 110 component pages and 260 occurrences inllms-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(theshadcn addcommand, the registry dependencies and the sanctioned engines), Anatomy from the contract's newdataAttributes, the states-tested table from the contract'sstates, 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 isdesign.md§Docs canon — the target shape, not a description of every page today. canon ·7915a71 -
Per-family variants are documented as eight tokens, not six —
subtle-hoverandsubtle-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 fromdesign.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 usedrounded-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 inheritedfont-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 inlinebridge. That alpha role was deleted with the ladder, so the example compiled to nothing; both now show thefillInteractiverecipe's real washes. docs ·b2c2e96 -
The shipped design-system skill is refreshed for Button's
variant × tonematrix, the singlexs · sm · md · lgsize vocabulary, andIconButtonas 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 thesizeexample it never had, NumberField gains its invalid fixture and folds its "Scope" table into Usage, and Label documentslayout. docs ·9fbeb65 -
Docs —
design.mdgains 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. Thecomponentskill's motion-mechanism matrix gains a fourth row for a control that stays mounted at a viewport edge and flipsdata-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, neverinfoand 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. Thecomponentskill §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
mainunprotected 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_requiredand never execute, so the publish is gated byquality-gateon 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-buttonas 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 apicommand 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.mjsactually enforces (hover-without-pressed), and added thesr-onlyaccessible-name separator to design.md § Accessibility as doctrine. verify-component-contractsno longer tells the reader to run a command that cannot fix a driftedexpectedCountskey.- 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
- 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
-
Docs —
AGENTS.mdis 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 todocs/ledger/operator-review.md, and thecomponent,review, andshipskills 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 andAppShellContentboth hard-coded the idmain-content, so a page holding more than one shell published that id twice and EVERY skip link resolved to the first region — measured on theapp-shelldocs 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.AppShellnow mints the id once withReact.useId()and shares it toAppShellContent, with a newcontentIdprop for when the id has to be known (a deep link, an externalaria-controls, a test harness). SettingidonAppShellContentmoves the element but does not rewire the link; that is whatcontentIdis for. Sharing a generated id is whyapp-shell.tsxnow carries a client boundary at the shell root —createContext/useContextare unavailable under thereact-servercondition — which is whereSidebarProvider's own client context already lives; page content passed aschildrenstill 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.
NotificationBellread 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 throughuseAnimationReplaywhen 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+"). TheTimelinehero fixture, which sat under the 24px pointer-target floor, is lifted off it. docs ·8c18d2b -
tiptap 3.31.3 loaded
prosemirror-modeltwice, and TextEdit's suite died on it.@tiptap/pm@3.31.3declaresprosemirror-modeldirectly and resolved to 1.25.11, while its siblings stayed on the 1.25.9 the pre-bump lockfile already carried —pnpm installonly 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 dedupecollapses the tree to a single 1.25.11 and removes 34 packages.packages/ui/vitest.config.tsalso stops pre-bundlingclsxandtailwind-merge, which are@vegastack/design's dependencies rather than@vegastack/ui's and printed a resolve failure on every run.5b03e3b -
The
providerunit 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 unscopedgetByRole("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
Progressno longer reads as 100% complete. Base UI writes no inline width whenvalueisnull, 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, andaria-valuenowis omitted. docs ·7915a71 -
The
prefers-reduced-motion: reducereset zeroes delays too. It gainsanimation-delay: 0s !importantandtransition-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 amotion-reduce:restatement of its own. docs ·7915a71 -
RelativeTimeno longer renders an empty first frame. A relative label needsDate.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-playpointerdown— 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-pressedwhile painting the opaquesurface-2/surface-3rungs, so it demonstrated the opposite of the twins' claim. It now paints the realforegroundcomposites 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 replacesborder-coloroutright, so a focused field showed no indicator at all. The token layer now paints a real2pxoutline underforced-colors: active, once, for every text-entry control. docs ·b2c2e96 -
Text on the soft media scrim is gated at AA —
media-foregroundonmedia-scrimwas 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. Themedia-scrim,media-scrim-strongandmedia-foregrounddescriptions 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-strongand--media-foregroundtokens keep overlay chrome dark-scrim + light-ink in both themes. docs ·b2c2e96 -
Four controls shipped with utilities silently destroyed, and a
Field > Textareawas 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 — measuredbackground-color: rgba(0, 0, 0, 0)andpadding: 0pxunchecked and checked, with on/off conveyed only by thumb position on abackground-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--foregroundwith no hover step. All four are repaired, anddesign-lintgained a structuralclass-gluerule that rejects the seam at the AST — the existing rules read one literal at a time and could not see it, which is whytransition-pairingpassed on an element with no ease token.<Field label="…"><Textarea /></Field>produced a textarea with noid, noaria-labelledby, noaria-describedbyand noaria-invalid: the<label for>pointed at nothing, the error message was not linked, and axe reportedlabelat critical.Textareanow renders through Base UI'sField.Control, like every sibling control, so the wiring and the destructive border tint arrive automatically. Standalone use is unchanged.Also fixed:
aria-invalidwas accepted and inert on a standaloneOTPInput(it landed on the root, never on the slots) and on a standaloneNumberField(the group's:has()selector cannot match the group's own attribute); a read-onlyEditableCellwith aselecteditor rendered the raw stored value where the editable cell rendered the option label; and aborderlessFieldshowed 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-hiddenso 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 baredata-field-groupattribute thatbase.csshooks) 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-invalidfield kept its destructive border when focused, and aField borderlesscontrol kept its transparent one, so both showed no focus indicator at all (WCAG 2.2 §2.4.7). The invalid tint infieldControl/fieldControlGroupand on TextEdit's container now stands down onfocus/focus-within,borderlessflattens only while unfocused, andInput'soutline-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
destructiveBubblewas the only place in the registry that used a solid FILL token as body text —text-destructiveoverbg-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/-activewithdestructive-textink — measuring 5.80 / 5.13 / 4.69 light and 6.14 / 5.02 / 4.61 dark, monotone in both. Thetintedvariant's pressed step, which composited to L 0.921 against asurface-3hover at 0.922, moves to--alpha-ink-tint-strongso a press is visible. ThectaButton painted its 0.75rem/400 mono label intext-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 shipsbrand-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 thelinkButton, every rendered rich-text link throughprose, and PropertyList) compositedsuccess/info/warningink to 4.03–4.11:1 in light at 80% and is now 88%, re-measured 4.74–4.83:1.--alpha-soft-hoverand--alpha-soft-surfaceare removed: their only consumer was that Bubble line, and--alpha-soft-hoverwas a second, 13pp different answer to the rolesd-hooks.mjs'sSUBTLE_HOVER_ALPHAalready owns.--font-display/--font-pixelare now bridged into@theme inline, so D17's sanctioned Geist Pixel flourish is reachable. Gates:contrast-check.mjsmeasuresbrand-textand the link-hover composite (both observed failing on the pre-fix theme);design-lintgainsfill-token-as-textandfield-group-pairing, both with negative fixtures;verify-token-referencesanddesign-lintnow coverpackages/design/src, where every shared recipe lives and where a bogus token previously exited 0; andverify-docs-base-mirrornow 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>usescontent-visibility: autofor render skipping, which brings paint containment with it, so anything a child paints outside the box is clipped and stops being hit-testable.timeline-contentdropped its bottom padding on the last item, and a trailingRelativeTime— whose 24px pointer target comes from a 4pxbefore:-inset-y-1overhang — 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 aMarkerrendered 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: aninertcontrol accepts no pointer action, and arole="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.MediaPlayerControlstakes aportalContainerandVideoPlayerpasses 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-1tied on specificity with the shared track'sh-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
onTimeChangeamong its dependencies;timeupdatefires ~4×/s and re-renders the controls, so a player given an inline callback re-applieddefaultPlaybackRateseveral 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 neverutilities.css, so every@utilityin the system —scroll-fade-*,scrollbar-thin,motion-pop-in, the newmotion-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 -
useIsMobilereportedfalseon 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 theserverFallbackthe caller declares.usePlatform'sisTouchwas 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 onisTouchhas 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--brandafter a toggle to dark until something forced a remount. It now reads the canvas's own resolvedcolorper 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
BreadcrumbEllipsisglyph 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::beforeexpansion (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 insideBreadcrumbList's 6px gap, so it never reaches into a neighbouring segment.BreadcrumbEllipsisis 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.
BreadcrumbPageannounced 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 viewports —ScrollArea's andMessageScroller'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'slandmark-no-duplicate-main; they renderlandmark="region". AndTabsContent,BreadcrumbCollapsed's trigger andAppShell's skip link each restated the global:focus-visiblerule — 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 — anitems-centerchild 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 takesself-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'sEXCLUDEDmap is deleted. docs ·45cde26 -
registry:libfiles are now modeled as shadcn-transformed in the shipped consume verifier.shadcn addremoves a JS/TS file's entire leading comment prologue as it writes it, for every file type it touches — butverify-registry-item.mjslisted onlyregistry:ui,registry:hook,registry:pageandregistry:component, so the firstregistry:libitems (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-updatesreads 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
Toasterunder the package provider'sToastProvider, and each toast module owns a module-scope manager — so the viewport listened to one store while every preview'stoast()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 aToasterwhose 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'sdata-dayhook is a stableYYYY-MM-DDinstead 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 explicitlocale. 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 currencySelectstops painting into the rule.1e439d9 -
A table's selection checkbox did not own its own 24px hit area.
TableHeadandTableCellcollapsed the trailing padding of a checkbox column tope-0, so the checkbox's 6px::beforeoverhang fell outside its own cell — the neighbouring header's sort control owned part of the centred 24×24 square. Both cells now usepe-2, the least that contains the target, and the sort control no longer carries a negative inline margin (SortableHeadnarrows 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'
accessibilityLayermakes the plot<svg>a tab stop, andChartContainer's own.recharts-surfaceoutline reset then poisoned--tw-outline-styleon the very element that takes focus, so even the global:focus-visiblerule resolved tooutline-style: none. The reset is now scoped to:not(:focus-visible). And theDataGridroving cell's ring was clipped, because the cell lives inside the table's scroll viewport, which clips its overflow — it is now inset.PropertyListvalues stop truncating for the same reason: theoverflow: hiddenthattruncateimplies 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 andscrollIntoViewIfNeeded, failing the sweep with "Element is not attached to the DOM" on a different subset of Chromium projects each run — on unmodifiedmainas 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 bothkey-circleandkey-square),RefreshCCWIconWIcon(onrefresh-cw),ActivityIconHandle(onsquare-activity) andZapHandlewere@deprecatedaliases left behind by upstream naming quirks; each of those icons still exports its<Name>IconHandleand only the alias is gone.chevron-firstis the different case and is a rename, not an alias removal: upstream had copy-pasted adisplayNamefrom another icon, so the primary interface was calledChevronsDownUpIconHandleandChevronFirstIconHandlewas the@deprecatedalias of it. The exported component symbol is authoritative, so the name that survives is the one that matches it —ChevronsDownUpIconHandle→ChevronFirstIconHandle. Consumers of the old name must rename; no compatibility alias is kept. docs ·b2c2e96 -
Alertisrole="status", notrole="alert", andItemtakesrole="listitem"only inside anItemGroup. 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 newliveprop for a banner raised by a user action, and adestructiveorwarningintent then escalates to the assertivealertrole.AnnouncementBannerdrops itsrole="status"at load and takes the sameliveprop. Tests assertinggetByRole("alert")on a static banner should readgetByRole("status"). Separately,Itemappliedrole="listitem"to every non-renderrow, so a standaloneItemwas an axearia-required-parentcritical andTimelinehad to documentrole="none"as a workaround;ItemGroupnow provides the context that licenses the role, and outside one a row carries no role at all. Remove anyrole="none"passed to work around the old default. docs ·8c18d2b -
Emptyhas one container axis.variant: plain | card | dashedreplaces theborderedflag crossed withsurface, a pair that could ask for a dashed card and resolved it only by letting tw-merge pick a winner. Rename map:bordered→variant="dashed",surface="card"→variant="card",surface="transparent"(the default) →variant="plain". Thedata-borderedanddata-surfaceattributes are replaced bydata-variant.EmptyTitlealso takes anasprop, so the hard-coded<h3>no longer guesses at the host page's heading outline — passas="h2"when the empty state replaces a page body, oras="p"when the surrounding card already carries the heading. docs ·8c18d2b -
Badge speaks the same variant vocabulary as Button.
variantis nowsolid · soft · outline · minimal:subtleis renamedsoft, with no alias — a stalevariant="subtle"is a type error. The three sizes become three REAL heights,sm16px ·md20px ·lg24px;smused to bemdwith 2px less horizontal padding, which is a padding value, not a size.minimalbecomes 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). Passdot={false}to opt out, or the newiconprop to take the dot's place. docs ·7915a71 -
Kbddefaults toos="other". It reads nonavigator— that is what keeps it server-safe — so the platform is the caller's to resolve: runusePlatform()and pass the answer down. The old default shipped mac glyphs to a Windows majority.TooltipKbdtakes the sameosprop. docs ·7915a71 -
borderandinputare translucent.borderis derived asforegroundat--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.
cardwasoklch(0.985)against a0.994page — a grey slab no reference system draws. It is now the page colour, separated by the hairline alone; dark keeps its one-step lift.popoveriscardin 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,mutedandaccentwere 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. Existingbg-muted/bg-accent/bg-sidebar-*utilities keep compiling and keep their rest appearance; onlyaccentmoves (one rung darker, because it is the hover rung). Name the rung in new code. docs ·b2c2e96 -
Buttonhas no icon size tier.size="icon"/icon-xs/icon-sm/icon-lgare gone; every icon-only action isIconButton, which makes the missingaria-labela type error and now ownsshape="square" | "round". docs ·7915a71 -
Button is two axes, not fifteen variants.
variantis now the SHAPE —solid·soft·outline·ghost·link·cta— and the newtoneprop 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:default→solid,secondary→soft,destructive→soft -
disabledisaria-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 assertingelement.disabledshould readaria-disabledinstead. docs ·7915a71 -
One size vocabulary:
xs · sm · md · lg. The tier every component calleddefaultis nowmd, 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
Fieldowns 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,NumberFieldandChipInputno longer takeshakeSignaland no longer shake on their own —Fielddoes, for every control it wraps, soTextareagains the behaviour it never had. A bare<Input aria-invalid>outside aFieldstill tints its border; wrap it in aFieldfor the motion, or moveshakeSignalonto theField. docsInline validation announces as
role="status", notrole="alert".FieldErrorandFieldInline's error are polite live regions: the person just typed or submitted and is looking at the field, andalertinterrupts whatever the screen reader was saying.alertstays reserved for something that arrives without being asked for. Tests assertinggetByRole("alert")on a field error should readgetByRole("status"). docsForm controls keep their pointer events when disabled.
disabled:pointer-events-noneis 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-allowedand the dim stay; Base UI suppresses activation either way. docs ·9fbeb65 -
MediaPlayerControlsmoved out ofaudio-playerinto its own registry item. It was exported fromaudio-playereven though the video player was its main consumer. Runshadcn add @vegastack/media-player-controlsand import from@/components/ui/media-player-controls;video-playerdeclares it as a registry dependency, so a freshshadcn add @vegastack/video-playerpulls it in automatically. docs ·aa5fa0d -
Slider's internals are no longer restyled from outside. Anything reaching into
[&_[data-slot=slider-track]]/-indicator/-thumbto build a media rail should passvariant="media"orvariant="overlay"withthumb="hover"instead. The internal slots keep theirdata-slotnames, but their rest appearance is now thevariant's to decide. docs ·aa5fa0d -
Menu items take
tone, notvariant.DropdownMenuItem,ContextMenuItemand their checkbox/radio siblings usetone="destructive", matching Button's tone axis. The state attribute moves with the prop: items exposedata-tone, notdata-variant. docs ·7915a71 -
In-panel search fields are a
searchbox. Every panel-search row renderstype="search", soShortcutOverlay's filter (and any other field inside the shared row) exposes thesearchboxrole rather than a generic textbox. Selecting one by role in a test or script must change with it. docs ·7915a71 -
Sheetruns on Base UI'sDrawer. It gains swipe-to-dismiss, snap points (snapPoints/snapPoint/onSnapPointChangepass straight through) andSheetVirtualKeyboardProviderfor bottom sheets containing fields.sidemoves fromSheetContentto theSheetroot, because it selects the dismiss gesture as well as the pinned edge, and asideon the content could disagree with the gesture.SheetContentnow sizes throughsize—sm · md · lg · fullfrom the shared--panel-width-*vocabulary replacesclassNamewidth overrides, and one tier means a width on aleft/rightsheet and a height on atop/bottomone. 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.sonneris removed from the system and the registry item is renamedsonner→toast.toast()now takes a title plus Base UI's options:action: { label, onClick }becomesactionProps: { children, onClick },durationbecomestimeout(and0, notInfinity, disables auto-dismiss), and ids are strings.toast.messageis gone — it wastoast().toast.customnow renders the toast BODY inside a real toast, so a custom notification keeps stacking, swipe-to-dismiss,Escapeand the live region instead of opting out of them. Resolving a loading toast istoast.update(id, …)rather than re-firing with the same id.Toasterloses sonner's props:positionvalues are logical (bottom-end, notbottom-right),expandis gone because the stack expands on hover by design, andoffset/mobileOffset/themeare gone — the viewport carries the safe-area insets itself and reads the theme from the cascade.VegaStackProvideralways mounts the toast context now:toaster={false}still suppresses the visible viewport — the part that must not mount twice — but the providertoast()writes into is unconditional, so a host rendering its own<Toaster />shares one queue.@vegastack/designgainsTIMINGS.tooltipOpenDelayMs/TIMINGS.tooltipCloseDelayMs, which the provider applies toTooltip.Providerso every tooltip in an app shares one rhythm.@vegastack/design-tokensgains 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/RegionSelectinternals.COUNTRIES,Country,getCountryByCode,REGIONS_BY_COUNTRY,Region,getRegionsByCountryandhasRegionsare no longer exported fromcountry-select/region-select, andregion-select-data.tsis gone — importCOUNTRIES,REGIONS,getCountryByCodeandgetRegionsfrom@/lib/geo-data(getRegionsByCountry→getRegions;hasRegions(c)→getRegions(c).length > 0).RegionSelectno longer clears by re-selecting the current state; clearing is the explicitclearablecontrol on the trigger, on by default. Both selects now render a wrapper, sodata-slot="country-select"/"region-select"is on the wrapper and the trigger carries the-triggersuffix. 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 useoverflow-wrap: anywhereover a--table-cell-min-widthfloor, and scrolling is reserved for tables that are genuinely wide. Two column shapes opt back out automatically —align="end"figures and the newmonocolumns — andDataListColumn.nowrap/DataGridColumn.nowrapoverride the inference either way. A layout that relied on single-line cells should setnowrap: true(orwhitespace-nowrapon a rawTableCell). Two further breaks land with it:DataGrid'smobileposture 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"; andTable'scontainerClassNameis removed, because it did exactly whatcontainerProps.classNamedoes. docs ·6d25185 -
useDragReorderanduseFileDropreturnAnnouncer, notgetLiveRegionProps(). 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 -
Paginationno longer hard-codes its accessible name. It renders a plain<nav>(norole="navigation") andaria-labeldefaults to "Pagination". A page with more than one pager must name each one — two identically named landmarks are an axelandmark-uniquefailure. 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 / durationreadout, 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 withelapsedanddurationpinned 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 (newonTranscriptClick, lucideaudio-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/design→0.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-tokensis 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-invalidremains 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/design→0.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-tokensis 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 }) forKbd'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/containerPropsforwarded to thedata-slot="table-container"scroll wrapper, the attachment point for sticky headers, fixed-height viewports, and virtualizers. docs - DataList —
DataListPropsnow extendsTableProps(the spreadsheet voice and container hooks type-check), columns gain a per-cellcellClassNamehook, andrenderreceives an optional thirdDataListCellContextargument. All additive. docs - FieldInline — additive controlled edit mode (
editing/onEditingChange) and atabIndexoverride for the display element, soEditableCelland grid hosts compose it instead of forking it. No behaviour change for existing consumers. docs - ColorPicker and EmojiPicker — internal refactor onto the shared
useListNavhook; 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-pxpress nudge is removed frombuttonVariants, so every component composing Button (IconButton, SplitButton, toolbars, pickers) loses it in one place. Pressed feedback is now colour-only via the existingactive:bg-*states, and the motion foundations doctrine records press-motion as a deliberate exclusion. docs
🛠 CLI & tooling
verify-registry-depsgains a fail-closed npm-range check: a registry item pin the installed version cannot satisfy now failsregistry:buildinstead of passing silently.
📚 Docs
- The npm dependency pins the installed versions could not satisfy are reconciled to
packages/ui/package.json:lucide-reactwas declared at both^1.20.0and^0.525.0across a major boundary (41 items), and@shadcn/reactat^0.1.0against an installed^0.2.1. table.mdxno longer claims the Table parts add no props;data-list.mdxdocuments that a columnrenderis 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-buttonrendertargets;button.mdxand the bundled design-system skill document the pattern, andmotion.mdxrecords 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-servercondition. docs ·6c60d53
🛠 CLI & tooling
vegastack-design skills installnow 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 ·6c60d53vegastack-design check-updatesnow 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/design0.2.0 — adds bundled public agent skills, the skills installer, CommonJS-compatible exports, and the explicit./theme-scopeclient subpath while preserving a server-safe root.6c60d53@vegastack/design-tokens0.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/uiadvances 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-writenow 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 thesonnerToaster 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) andpage.tsxships 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-updatesis 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≈ driftstatus (differs — upstream update or local edits);--fail-on-updatefails onupdate+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/design0.1.1 —tw-animate-cssis a real dependency (was an optional peer that pnpm never installed, breaking every fresh consumer build atpreset.css). Published via npm OIDC trusted publishing — the first fully-automated release.9532d42@vegastack/design-tokensstays 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 thedashboard-01starter block — all Base UI + Tailwind v4, semantic-token-only, WCAG 2.1 AA, distributed as copy-in via the private registry atdesign.vegastack.com/r/*(Cloudflare Access service-token auth, Sigstore-signed manifest). components ·8a5bb2a
📦 npm
@vegastack/design0.1.0 —cn(), the icon runtime (./icons), the Tailwind v4 preset (./preset.css), token CSS re-exports, and thevegastack-designCLI.@vegastack/design-tokens0.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 --overwriteStatus 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.