Changelog

Release history for @luminpdf/ds. Click a version heading or section to collapse it.

Figma's SidebarMenuButton shows a 20px leading icon and a 16px trailing icon; the code drew

every icon at 16px. Icons inside a menu button are now 18px by default (20px read too large in

review), at any nesting depth. Mark a trailing icon with data-icon="inline-end" (the Button

convention) to keep it at 16px.

Leading icons that set size-4 grow to 18px anyway (the rule outranks a plain class), so drop it;

use data-icon="inline-end" for an icon that must stay at 16px.

In an icon-collapsed sidebar (collapsible="icon") the button padding drops from 8px to 7px so the

18px icon stays centred in the 32px button.

SidebarProvider takes a new keyboardShortcut prop, default true. Pass false when the provider

only exists so SidebarMenuButton can read its context and no collapsible Sidebar is rendered; the

provider then stops swallowing Cmd/Ctrl+B and no longer writes the sidebar_state cookie on that

shortcut. No change for existing callers.

Figma's DropdownMenu / Item (node 336:4988) has a 20px leading icon and an 8px gap; the code drew

16px with 6px. DropdownMenuItem now sizes an unsized icon to 18px (matching the sidebar) with an

8px gap, and inset moves to pl-8 so its label still lines up. DropdownMenuSubTrigger matches

the item (18px leading icon, 8px gap, pl-8 inset) so submenu labels line up with the rows around

them; its caret is fixed at 16px. An icon with its own size-* class keeps that size, so drop

size-4 at call sites that should follow the default.

FilePickerSidebarItem draws its icon at 18px (was 16px), matching the sidebar menu button. An

iconClassName with its own size still wins.

default and icon buttons draw an unsized icon, and the loading spinner, at 18px (was 16px),

matching the 18px sidebar, menu and file picker icons. lg / icon-lg keep 20px and the smaller

sizes are unchanged. An icon with its own size-* class keeps that size.

The sidebar's trailing-icon rule (data-icon="inline-end" → 16px) is now !important, so a ⋯

icon inside an icon Button in a sidebar row stays 16px whatever the stylesheet order.

ContextMenuItem and MenubarItem match DropdownMenuItem: an unsized icon is 18px with an 8px

gap, and inset is pl-8. Their sub-triggers match too (18px leading icon, pl-8 inset, fixed

16px caret), so submenu labels line up with the item labels.

Checkbox and radio items line their text up with the new 32px offset: inset is pl-8 on all three menus,

and menubar checkbox / radio items (indicator on the left) move from pl-7 to pl-8.

A button with a leading or trailing icon (data-icon="inline-start" / "inline-end") shrank its

padding on that side, which Figma never does. The padding now stays the same with or without an icon:

SizeBefore (icon side)After
xs6px8px
sm6px10px
default8px10px
lg8px10px

No API change. Call sites that added has-data-[icon=inline-start]:pl-2.5 (or similar) to match

Figma can drop it.

FilePickerDropzone spread inputProps.onCancel onto its hidden <input type="file">, but React 19

only listens for cancel on <dialog>. Closing the system file dialog without picking a file never

called the handler.

  • FilePickerDropzone now attaches inputProps.onCancel to the input as a native cancel

listener, and removes it on unmount or when the handler changes.

  • inputProps.onCancel is typed () => void, since it no longer receives a React synthetic event.

No visual change. Consumers that already pass onCancel, such as the Global creation upload tabs,

start getting the call without any change on their side.

A plain group label such as "Workspace storage" is a static heading, but it showed the hover tint,

which made it look clickable. SidebarGroupLabel's hover rules now apply only when the label is an

enabled form control, which is what the collapsible pattern

(<SidebarGroupLabel asChild><CollapsibleTrigger>) renders.

  • Default label: no hover.
  • Collapsible label: unchanged, with a 5% sidebar-primary layer and sidebar-accent-foreground

text.

  • Action label: the label no longer hovers. The SidebarGroupAction button keeps its own hover.

No API change. This reverses the EXP-175 decision that gave every label type a hover, after the

designer clarified that the Default and Action hover variants in Figma were built in error.

The Sonner status toasts now match the Figma Sonner page (DS-shadcn-000 Foundations, node 118:2756).

  • Status icons use the filled weight. Error switches from an X-circle to the exclamation-circle

(WarningCircle), as in Figma.

  • The warning icon uses --card-amber-foreground (#D97706) instead of

--card-orange-foreground.

  • Loading uses sonner's native bar loader instead of the spinning SpinnerIcon.
  • The toast description uses text-muted-foreground. The class was documented on 2026-05-07 but

missing from the code.

  • The title and description line-heights are now 20px. sonner's own title/description rules were

outranking leading-5 and rendering 21px / 19.6px.

  • The close button is painted from DS tokens (bg-popover, text-popover-foreground,

border-border). Before, the X disappeared when sonner's theme and the app's DS theme disagreed.

  • Dark-mode status icons keep the dark --card-*-foreground colours (EXP-258), although Figma

shows them white.

No API changes. Visual change only, and it applies to every app that renders the DS <Toaster>.

ComboboxContent only forwarded side / align / sideOffset / alignOffset / anchor to the

Base UI Combobox.Positioner; the remaining props spread onto Combobox.Popup, so there was no way

to reach the positioner's collision handling — a consumer could not disable flipping or pin the

listbox to a fixed side (the Base UI equivalent of Radix's avoidCollisions).

  • ComboboxContent now accepts and forwards collisionAvoidance and collisionPadding to

Combobox.Positioner, bringing the Base UI Combobox to parity with the Radix Select /

DropdownMenu, which already accept avoidCollisions / collisionPadding.

Additive API: both props are optional and default to undefined, so positioning is unchanged for

every existing consumer.

The same space showed different initials in different views, for example "al" in one sidebar and

"A" in another. Each consumer built its own initials: some kept the typed case, some took only the

first letter. AvatarFallback seeds its colour from the text it receives, so different initials

also meant a different colour.

  • Added getAvatarInitials(name) in lib/avatar-initials.ts, re-exported from avatar.tsx. It

returns 1–2 uppercase letters: the first letter of the first word and of the last word. It

ignores extra whitespace and never splits an emoji or surrogate pair. The file has no React

dependency, so utilities can import it without pulling in the component.

  • AvatarFallback accepts an optional name. When no children are passed, it renders

getAvatarInitials(name), and the colour is seeded from those same initials, so a name always

gets the same letters and colour.

  • The xs fallback text is now 10px with a 16px line height (was 12px), matching Figma

17379:199118. sm stays at 12px.

  • The +N count in an xs AvatarGroup also uses 10px / 16px, so it matches the xs initials.

Additive API: fallbacks with explicit children render exactly as before. Visual change: xs initials

are smaller.

toast.warning and the other status toasts rendered their icon in the neutral text color instead

of the variant color — a warning icon should be orange. The custom icons passed to Sonner carried

only size-5 and render fill="currentColor", so they inherited the toast's text color

(--popover-foreground without richColors, Sonner's own palette with it) rather than a

per-variant color.

  • Each variant icon in sonner.tsx now carries its matching --card-*-foreground token: success →

--card-green-foreground, info → --card-blue-foreground, warning → --card-orange-foreground,

error → --card-red-foreground. The class sets color on the SVG so fill: currentColor wins

over inheritance, so the icon is colored with richColors on or off, in light and dark, with no

new tokens. loading stays neutral.

  • The docs playground and the icon-variants code example dropped their redundant per-call icon

overrides, which were passing uncolored icons and masking the DS default.

No API changes. Visual change only: status-toast icons now match their variant.

A @theme key and the token it reads must never share a name. Two token groups broke that rule,

in opposite directions, and both failed the same way: the value resolved on the page and not

inside a micro-frontend, which scopes design-system CSS onto its own root.

The scales aliased themselves. --radius-md read --radius-md, because Tailwind owns the

--radius-* namespace and derives rounded-md from it, and the raw token was given the same

name. A self-reference resolves only where the literal sits on the same element; anywhere else

all 94 affected keys computed to the guaranteed-invalid value and shadowed the page instead of

inheriting from it. Radius, text size, shadow and blur silently resolved to nothing.

The palette was never registered with Tailwind at all. All 245 stops were declared on :root

in tokens.css and held only because an unlayered declaration beats @layer theme — a cascade

override, not a theme. Tailwind kept generating .bg-red-500 against its own default, so the

prefixer emitted that default onto the micro-frontend root: .bg-red-500 rendered #fb2c36 in

the embedded window against #EF4444 on the page around it. 33 of the 44 stops in use differed.

  • Raw tokens are now --ds-* and the @theme key aliases them: --ds-radius-md: 8px with

--radius-md: var(--ds-radius-md), --ds-color-red-500: #EF4444 with

--color-red-500: var(--ds-color-red-500). 339 names in total — 94 scales across

--spacing-*, --text-*, --radius-*, --shadow-*, --blur-*, --drop-shadow-* and

--inset-shadow-*, plus the 245-stop palette. --font-sans and --font-mono are excluded:

theme-core.css declares those itself as a composed stack, and the key reads that declaration.

  • The key names are unchanged, so every utility and every var(--radius-md) in product code

still resolves. Consumers that run Tailwind see no difference at all.

  • tokens.css still emits the old names, aliased to the new ones. Utilities read the --ds- name

directly, but a plain var(--radius-md) in CSS or an inline style resolves only through that

block, so it stays: removing it would be a breaking change.

  • theme.css is now @import "./tokens.css" + @import "./theme-core.css". theme-core.css is

everything except the token values — the @theme block, the variants, the @source paths — so

a consumer can take the Tailwind wiring without the values and inherit those from the page it

renders in.

An alert dialog opened over a dialog blurred it instead of dimming it: the two overlays had

been carrying different recipes since the initial shadcn import.

  • Both overlays are now bg-scrim, with no blur and no supports-backdrop-filter: gate.

--scrim is the Figma custom/scrim variable: #00000033 (20%) in light, #000000CC

(80%) in dark, so the backdrop follows the theme instead of being one flat black.

  • FilePicker follows automatically — it reuses DialogOverlay.
  • Drawer and Sheet keep their own values.

A file whose name has no spaces — Non-Disclosure-Agreement-Template-AAAA… — ran past the list and

was cut mid-letter at the panel edge, with no ellipsis and no tooltip affordance, even though

FilePickerItem labels carry truncate.

Radix wraps a ScrollArea's children in its own <div style="display: table; min-width: 100%">.

A table box is shrink-to-fit, so it grows to the widest label; the item's w-full then resolves to

that width rather than the viewport's, the label never overflows its own box, and truncate has

nothing to do.

  • FilePickerList forces that wrapper back to a block box:

[&_[data-slot=scroll-area-viewport]>div]:!block. !important is required because Radix sets

display as an inline style.

  • This is the rule FilePickerSidebar has always carried, which is why sidebar rows truncated

correctly while every list — the template list, the ChooseFileV2 document lists, the AG

create-document modal — did not.

  • Registry snapshot public/r/file-picker.json synced.

No API changes. Items wider than the list now end in an ellipsis (and show the full label on hover

where enableTooltip is set) instead of being clipped. A FilePickerList can no longer scroll

horizontally, which nothing in the component relied on.

A selected sidebar row did not react to hover. Both rules target the same ::before opacity, and data-[active=true]:before:opacity-10 is emitted after hover:before:opacity-5, so the selected value simply won and the row stayed at 10% under the cursor.

The state layer is meant to accumulate: the selected tint is what the row already carries, and hover adds its 5% on top of it.

  • SidebarMenuButton and SidebarMenuSubButton gained data-[active=true]:hover:before:opacity-15. The stacked variant is emitted after both single variants, so it wins on source order as well as specificity.
  • Measured on the rendered component: 0 default, 0.05 hover, 0.10 selected, 0.15 selected + hover.
  • Consumers that render rows through these components — including the PDF workspace sidebar — pick this up with no change of their own.

No API changes. Visual change only while hovering a row that is already selected.

Added Loader, a single component exposing the four animated brand marks (variant="lumin" | "pdf" | "sign" | "ag") so every team pulls one shared loader instead of copy-pasting markup.

  • Self-contained inline SVG + CSS stroke-trim animation (2.5s linear loop) — no JS deps, no external assets, animates at first paint (works in loading.tsx / pre-hydration). The three single-path marks (lumin, pdf, sign) share one loader-trim keyframe; the ag mark adds staggered loader-star + loader-base keyframes.
  • Props: variant, size (number → px, or CSS length; default 48), color (defaults to currentColor so it themes with surrounding text / dark mode). Standard <div> props pass through.
  • Accessible (role="status", aria-label="Loading") and reduced-motion aware (freezes to the fully-drawn mark under prefers-reduced-motion: reduce).
  • Import: @luminpdf/ds/components/ui/loader in-repo, @luminpdf/ds/loader or npx shadcn add @lumin/loader for external consumers.

Figma added a seventh row to the Button component set, painted in the PDF product blue on white text. buttonVariants had no entry that produced it, so the colour had to be hand-written at every call site.

  • Added variant="pdf": bg-pdf text-pdf-foreground hover:bg-pdf/80, on the semantic tokens 0.2.17 added. Because --pdf and --pdf-foreground flip themselves, the variant needs no dark: pair -- it is the default variant's shape with a different token.
  • Every other state (focus ring, disabled, pressed, loading) is what the base classes already do. The Figma row binds the same variables as the Default row for all of them, so only the fill and text colour differ.
  • Enumerations updated so the docs site, Storybook and the parity tooling all list the new variant: registry.json, app/components/_data/component-registry.ts, app/components/_data/code-examples.ts, components/design-system-showcase.tsx, app/_playground/button-playground.tsx, stories/lumin/button.stories.tsx, scripts/design-parity/button-code-spec.ts and scripts/figma-registry.ts (48 new node IDs -- 6 states x 8 sizes).

Purely additive: no existing variant, size or prop changed. The docs-site Button section now renders a seventh row, so the button-section.png Playwright baseline needs regenerating.

Two things design should be aware of. The Button component set and the Foundations Theme collection disagree in dark mode: the Button row binds custom/blue-500 dark:blue-600 (blue-600 fill, white text) while --pdf / --pdf-foreground resolve to blue-200 on blue-950. The token wins here, so a dark-mode PDF button is the pale blue, not the saturated one. Separately, Figma names the row inconsistently -- 47 of its 48 nodes read Variant=Brand and one reads Variant=PDF; EXP-320 asks for "PDF", so the code value is pdf.

Synced the Foundations color tokens from Figma (DS-shadcn-000 — Foundations, node 1:434).

  • New semantic colors --pdf / --pdf-foreground (the PDF brand accent and the text that sits on it) and --scrim (the overlay behind modals and drawers), each with a Light and a Dark value. Tailwind utilities: bg-pdf, text-pdf-foreground, bg-scrim, and the usual text- / border- variants.
  • Dark-mode --popover moved from neutral/800 (#262626) to neutral/900 (#171717), so dropdowns, popovers, selects, and every other bg-popover surface are one step darker in dark mode. Light mode is unchanged.

No API changes. bg-popover consumers get the new dark value automatically.

Bumped the docs site's @luminpdf/icons devDependency from 0.5.0 to 0.5.3 (the version the monorepo catalog already pins) so the /icons playground shows the current icon set.

  • Regenerated app/_icons/icon-names.ts and app/_icons/lazy-registry.ts via pnpm generate:icons — now 1775 icons (119 deprecated shims). Adds LmLogoAskAi and LmMagicPencil; nothing removed.
  • Docs-site only: @luminpdf/icons is a devDependency consumed by the /icons page, so the published component library surface is unchanged.

Every interactive sidebar part painted its hover/pressed/selected feedback as a solid bg-sidebar-accent fill (#E5E5E5 in light), which overwrote whatever background sat underneath — so the outline menu button lost its own bg-background, and a consumer that colours a row itself had that colour replaced rather than tinted.

Each part now carries a state layer: a ::before overlay filled with bg-sidebar-primary at opacity-0, raised to 5% on hover and 10% on pressed/selected, matching Figma (DS-shadcn-000 Foundations, node 5143:200). isolate + before:-z-10 keeps the layer above the element's own background and below its icon and label, exactly where the Figma "State-layer" child sits.

  • SidebarMenuButton, SidebarMenuSubButton, SidebarGroupLabel, SidebarGroupAction and SidebarMenuAction all gained the layer; their hover:/active:/data-active: bg-sidebar-accent rules are gone. Text colour (sidebar-accent-foreground), data-active:font-medium, focus rings and sizing are unchanged.
  • The outline menu button keeps its bg-background and both box-shadows; the hover tint now sits on top of them instead of replacing the fill.
  • SidebarGroupLabel gains a hover state it did not have before (5% layer + sidebar-accent-foreground), per the Hover variant Figma defines for all three label types.
  • Selected styling is now keyed on data-[active=true] instead of data-active. React renders data-active="false" as a present attribute, so the old presence selector matched every menu button — an unselected row carried the selected background, which made the Default state impossible to see. This also applies to SidebarMenuBadge and SidebarMenuAction, whose text colour keyed off the same peer selector.
  • SidebarGroupLabel's text is now the full sidebar-foreground token. It was text-sidebar-foreground/70, an opacity Figma does not apply (node 5198:1428 fills the label with base/sidebar-foreground at 100%).
  • Focus outline widths follow Figma's border widths: SidebarMenuSubButton, SidebarGroupAction and SidebarMenuAction drop to focus-visible:ring-1 (Figma border-width/border), SidebarMenuButton keeps ring-2 (border-width/border-2). The ring colour was already sidebar-ring.
  • Focus outlines now follow Figma's shared Ring component (node 299:17), which every focused sidebar variant instances at 50% opacity: focus-visible:inset-ring-sidebar-ring/50 with inset-ring-2 on the menu row and inset-ring-1 on the sub row, group action and row action. The old ring-2/ring-1 drew a fully opaque outline *outside* the row; Figma draws it inside the row's bounds (inset-0), so a focused row no longer grows by 4px.
  • The sub row and action buttons also regain the Ring component's own 2px outer shadow (focus-visible:ring-2 focus-visible:ring-black/5), which only the menu row overrides away in Figma.
  • SidebarMenuSubButton gains leading-none, matching the sub row's label in Figma. Verified by pixel diff: the row now renders with zero differing pixels against the design value, where before the text sat about half a pixel low.
  • Added stories/ui/sidebar.stories.tsx — Storybook had no sidebar story, so the states had nowhere to be reviewed. Four stories: a full sidebar, a state reference, a row with its own background, and the collapsed rail.
  • Fixed the ./hooks/use-mobile entry in exports: it pointed at hooks/use-mobile.tsx but the file is hooks/use-mobile.ts. sidebar.tsx is the only importer, so nothing caught it — the Next docs app resolves through the @luminpdf/ds/* tsconfig path and skips the exports map, but Storybook's Vite resolver does not, and neither would a published consumer.

No API changes. Visual change for every sidebar state: hover is lighter than the old solid accent, selected reads about the same depth, and the tint now composes over whatever background is beneath it.

A solid-style Button used as a DropdownMenuTrigger inside a ButtonGroup kept the 1px transparent border every other button in the group had removed, so the trigger's content box was 2px narrower than its siblings and the seam next to the separator sat off by a pixel.

DropdownMenuTrigger passes its own data-slot="dropdown-menu-trigger" down through asChild, and Button spreads incoming props after its own attributes — so the rendered element loses data-slot="button", which is what the group's border-0 rule selects on.

  • buttonGroupVariants now applies the same border-0 to [data-slot=dropdown-menu-trigger][data-variant]:not([data-variant=outline]). The [data-variant] guard keeps the rule to triggers actually rendered as a Button; a bare trigger is untouched.
  • outline triggers keep border-input, exactly like outline buttons.
  • Playground gained a "With a dropdown trigger" row (outline / default / secondary) and e2e/button-group.spec.ts asserts the border widths for the secondary and outline triggers.

No API changes. Visual change only for solid-style dropdown triggers inside a ButtonGroup, which now match their sibling buttons.

SelectContent's popover had no inset of its own — the 4px padding lived on SelectGroup instead. Any Select built without a SelectGroup wrapper (e.g. a flat list of SelectItems) rendered flush against the popover edge, missing the 4px inset Figma specifies (node 118:2540, DS-101-1 Foundations).

  • Moved the p-1 inset from SelectGroup onto the SelectContent viewport, matching how dropdown-menu, context-menu, and menubar already pad their content instead of their groups.
  • Grouped menus (SelectGroup + SelectLabel) render pixel-identical to before, minus an erroneous 8px seam between adjacent groups that came from double-padding (4px from each group).

No breaking changes — ungrouped menus gain the missing 4px inset; grouped menus only lose the incorrect extra gap between groups.

A tooltip anchored to a trigger inside a dialog, menu, or popover could get stuck open after that overlay closed. Closing an overlay restores focus to its trigger, and Radix's TooltipTrigger opens on any focus event — including that programmatic restore — so the tooltip reopened right as the user was leaving.

  • TooltipTrigger now gates its focus-open on :focus-visible (mirrors MUI): real keyboard-driven focus still opens the tooltip, but focus restored programmatically by a closing overlay no longer does.
  • Added e2e coverage for both focus modalities in e2e/tooltip.spec.ts.

No breaking changes — tooltips still open on keyboard focus and hover; only the phantom re-open on programmatic focus restore is gone.

Icons in a large button now render at 20px instead of 16px, matching Figma (node 34:6). The cva base sets a 16px icon for every size and only the *smaller* sizes overrode it, so a 36px-tall lg button carried the same glyph as the 32px default — visibly under-weighted.

  • The lg and icon-lg sizes now add [&_svg:not([class*='size-'])]:size-5, following the pattern xs/sm already use. Height, gap, padding and radius are unchanged.
  • The [class*='size-'] exclusion is untouched, so an explicit size-* on a consumer's icon still wins.
  • Spinner now carries data-slot="spinner", and the large sizes size it to 20px via [&_[data-slot=spinner]]:size-5 — Spinner sets its own size-4, which the icon selector deliberately skips, so a loading large button would otherwise have shown a 16px spinner beside 20px icons.
  • No API changes, but this is a visual change: any existing size="lg" or size="icon-lg" button that doesn't set its own icon size will grow its glyph by 4px.

<Button variant="link"> had the ends of its underline shaved into a wedge. overflow: clip clips to the padding box honoring border-radius, and underline-offset-4 puts the underline within ~1px of the bottom edge — right where the 8px corner arcs cut inward. Most visible at call sites that collapse the box onto the text with h-auto p-0.

  • The link variant now adds overflow-visible, unsetting the base overflow-clip. It has no background and a transparent border, so there was never anything to clip.
  • Keeps the 8px radius, so the focus-visible ring stays rounded and still matches Figma (node 60:244). Also holds up inside a ButtonGroup, where the small sizes re-apply rounded-md after the variant classes.
  • No breaking changes — the box geometry is unchanged; only clipping is relaxed.

AvatarFallback no longer renders a flat gray. When it shows text (initials) it now picks a card-<hue> background + card-<hue>-foreground text from the 17-hue design-token ramp, deterministically hashed from the initials, so members without a photo get distinct, on-brand colors instead of the washed-out bg-muted gray.

  • Text fallbacks resolve to one of the 17 card-* hues via a char-code hash of the initials (same initials → same color every render).
  • Icon-only fallbacks (non-text children) keep the previous bg-muted / text-muted-foreground gray, and an explicit className still overrides via twMerge.
  • Hues are listed as literal class strings so Tailwind's source scan emits every one.
  • Added shuffled-fallback demos (distinct-initials grid, stable-across-sizes row, fallback-only group) to the docs playground.

No breaking changes — text fallbacks recolor; icon-only fallbacks and className overrides are unchanged.

AvatarBadge (status dot / icon badge) sits at the avatar's bottom-right edge and was being clipped by overflow-hidden on the Avatar root.

  • Dropped overflow-hidden from the Avatar root (matching the design-parity spec's overflow: visible). The image still self-clips to the circle via its own rounded-[inherit], so the avatar shape is unchanged — only the badge is now fully visible.

An indeterminate checkbox now renders a filled MinusIcon (a dash) matching the checked state, instead of an unfilled CheckIcon, per the Figma spec (node 23174:7682).

  • Root cause: CheckboxPrimitive.Indicator rendered <CheckIcon /> unconditionally, and every fill selector was scoped to data-[state=checked] only. Radix sets data-state="indeterminate" for that state, so it showed a checkmark on a transparent box.
  • Fix: render both icons and toggle via the indicator's own data-state (works for controlled and uncontrolled defaultChecked="indeterminate"), and mirror each data-[state=checked] fill selector (bg/border/text, incl. aria-invalid) with a data-[state=indeterminate] equivalent so the box is filled like the checked state.
  • No breaking changes.

Drawer/DrawerContent now work correctly for a non-modal bottom sheet that must leave the rest of the page interactive (e.g. a resizable preview pane).

  • Drawer — Fixed a vaul bug where a fully-controlled open prop combined with modal={false} leaves document.body.style.pointerEvents stuck at 'none' forever (vaul's own reset only fires through Drawer.Trigger's internal state change, not a controlled open prop). Now watches for this via MutationObserver and reverts it back to 'auto' for as long as such a drawer is open. See emilkowalski/vaul#509 (open as of this writing).
  • DrawerContent — Added an overlay prop (default true) to optionally skip the dimming backdrop for non-modal use. Forwards its ref, so callers can measure/drive the sheet's DOM node directly (e.g. resizing it without a re-render). Added overflow-hidden so square-cornered content flush against an edge no longer visually pokes past the sheet's rounded corners.
  • No breaking changes — both additions are opt-in and default to prior behavior.

Cleared all 59 advisories (33 high, 21 moderate, 5 low) reported by pnpm audit --ignore-workspace against the standalone lockfile.

  • next: lockfile refreshed to 15.5.19 (was stale at 15.5.9), resolving the bulk of the next advisories.
  • Added range-scoped pnpm.overrides for transitive deps, each capped to its current major to avoid breaking bumps: @modelcontextprotocol/sdk (1.26.0), tar, path-to-regexp, qs, ajv, js-yaml, body-parser, postcss, @babel/core, diff, flatted, minimatch, picomatch, brace-expansion, @isaacs/brace-expansion, uuid.
  • No runtime or component changes.

DialogContent now stays within the viewport instead of overflowing on short screens.

  • Added max-h-[calc(100dvh-2rem)] + overflow-y-auto so a tall dialog scrolls inside itself rather than scrolling the page (dvh accounts for mobile browser chrome).
  • Changed the mobile width guard from max-w-[calc(100%-2rem)] to max-w-[calc(100vw-2rem)] so the 16px side gutter is always viewport-relative (the previous 100% resolved against the portal container, letting the modal touch the screen edge on mobile/tablet).

Matched the Figma "DropdownMenu / Item" spec (node 336:4988).

  • The leading icon now defaults to muted-foreground (gray) instead of inheriting the foreground color.
  • The leading icon and DropdownMenuShortcut stay muted on hover/focus — dropped the focus:**:text-accent-foreground descendant override and the shortcut's group-focus override so only the item label takes the accent/destructive color.
  • Destructive variant unchanged (icon + label stay destructive).
  • Added an "Item states & icon color" demo to the docs playground and an ItemStates Storybook story.

Color tokens on /tokens showed the previous theme's value after toggling light/dark (e.g. dark mode displayed rgba(255,255,255,.898) instead of rgba(10,10,10,.898)).

  • Root cause: useResolvedCssVars re-read on the next-themes resolvedTheme, but the provider applies the .dark class in an effect that runs after the consumer effect, so getComputedStyle read one commit behind.
  • Fix: both hooks now re-read via a MutationObserver on <html> (class/style), so values resolve after the DOM reflects the theme. Fixes every token section via the shared hook.

Added a new /icons page to browse all 1627 icons from @luminpdf/icons.

  • Search — Real-time filter by icon name
  • Weight selector — Switch between thin, light, regular, bold, fill, duotone
  • Size slider — Adjust preview size from 16–80 px
  • Color picker — Custom color with a currentColor toggle
  • Infinite scroll — Grid lazy-loads 240 icons at a time
  • Click-to-copy — Clicking any icon copies the import snippet (e.g. import { AgreementGenIcon } from "@luminpdf/icons") with a toast confirmation
  • Keyboard shortcuts — / focuses search, Esc clears it
  • Icon names displayed as-is (e.g. AgreementGen) without word-splitting

All 1627 icons are code-split into individual chunks for webpack safety. The copy handler falls back to execCommand('copy') in non-secure contexts (e.g. LAN IP).

Added a /changelog page with a lightweight Markdown renderer (headings, lists, tables, inline code, bold, links), sidebar navigation, collapsible sections, and active version tracking.

Upgraded the /tokens reference page so every token swatch now exposes its Tailwind utility alongside the CSS variable.

Color palette & semantic colors:

  • Each swatch now shows three one-click copy buttons — bg-*, text-*, border-* — via a new ColorUtilityButtons component
  • Swatch click still copies var(--…); the --var label is now a separate clickable button
  • Checkerboard background behind swatches to expose transparency

Radius section:

  • Card click copies rounded-* Tailwind class (e.g. rounded-lg)
  • Separate inner button to copy var(--radius-*) CSS variable

Responsive section — Expanded with usage guidance:

  • Breakpoints table now includes the Tailwind responsive prefix (e.g. sm:*) and a description of when it activates
  • Container tokens now show the corresponding max-w-* class and a plain-language usage example per size
  • Responsive spacing, section padding, and title-gap tokens show live resolved values and copy buttons for both Tailwind utility and CSS variable

New custom Tailwind utilities added to app/theme.css:

  • px-container → padding-inline: var(--responsive-container-padding-x)
  • py-section → padding-block: var(--responsive-section-padding-y)
  • gap-section-title-xl/lg/md/sm → corresponding --responsive-section-title-gap-* variables

New `useResolvedCssVarsResponsive` hook — like useResolvedCssVars but also re-reads on window.resize, for tokens whose values change at breakpoints.

New `copyTailwindClass` helper — writes a Tailwind class name to clipboard with a toast.

Tokens layout (app/tokens/layout.tsx) — wraps the tokens subtree in <TooltipProvider>.

Added scripts/generate-icon-names.ts and a generate:icons npm script to regenerate app/_icons/icon-names.ts from the installed @luminpdf/icons package.

Added min-h-8 (32px) to every interactive item in the five menu-family components so that single-line rows maintain a consistent touch target regardless of content, matching the Figma DS spec (EXP-70).

ComponentSub-elements updated
ComboboxComboboxItem
CommandCommandItem
ContextMenuContextMenuItem, ContextMenuSubTrigger, ContextMenuCheckboxItem, ContextMenuRadioItem
DropdownMenuDropdownMenuItem, DropdownMenuSubTrigger, DropdownMenuCheckboxItem, DropdownMenuRadioItem
MenubarMenubarItem, MenubarSubTrigger, MenubarCheckboxItem, MenubarRadioItem

No API changes — min-h-8 is purely additive and does not override explicit h-* classes passed via className.

  • Added MenubarItem, MenubarCheckboxItem, MenubarRadioItem, MenubarSubTrigger node mappings to scripts/figma-registry.ts for upcoming design-parity coverage.
  • Renamed CSS variables from --components-card-{hue} / --components-card-{hue}-foreground to `--card-{hue}` / `--card-{hue}-foreground`.
  • Tailwind utilities are now bg-card-{hue} and text-card-{hue}-foreground (e.g. bg-card-blue, text-card-blue-foreground). Migrate any var(--components-card-…) or bg-components-card-* usages.
  • Semantic shadcn tokens `--card` / `--card-foreground` and utilities `bg-card` / `text-card-foreground` are unchanged.
  • Added 34 --components-card-{hue} / --components-card-{hue}-foreground pairs (17 hues). Superseded in 0.2.4 by --card-{hue} naming.
  • Registered all 34 in @theme inline so Tailwind utilities like bg-components-card-blue work out of the box. Superseded in 0.2.4 by bg-card-blue.
  • Added ComponentCardsSection to the tokens reference page, showing bg/fg swatch pairs for all 17 hues.
  • Replaced Radix AvatarPrimitive with a single <img> element and local AvatarContext for loading-status coordination, preventing duplicate requests for uncacheable URLs (e.g. signed S3 URLs).
  • Updated surface tokens, max width, typography, arrow color, and Kbd shortcut styling to match DS-shadcn-001 node 17103:809.
  • Updated tooltip/legend styling to match DS-shadcn-001 node 533:8856; added registry, docs, playground, Storybook, E2E, and design-parity wiring.
  • Registered Toggle node mappings for DS-shadcn-001 node 21133:11461; added playground/docs/story/E2E/design-parity wiring.
  • Added Figma state registry mappings for DS-shadcn-001 node 18684:85575, plus docs, playground, Storybook, E2E, and design-parity wiring.
  • Updated toast container, action/cancel controls, and icon sizing to match DS-shadcn-001 nodes 18482:48401, 17375:198044, and 17375:198096.
  • Updated backdrop, side dimensions, spacing, and close icon states to match DS-shadcn-001 node 220:4633.
  • Updated Table header/cell API for Figma state/variant parity; replaced Data Table node mappings with 190:901, 23177:2258, and 17089:45580.
  • Added Field component Figma registry, docs, playground, Storybook, E2E, and design-parity wiring.
  • Bumped version to 0.2.2. Updated peerDependencies and registry metadata.
  • Initial 0.2.x publish with updated registry and peer dependency declarations.
  • Added AI Elements section to the main navigation and documentation layout.
  • Added initial AI Elements component library with example patterns for AI-powered UIs.
  • Improved registry build pipeline and component export verification.
  • Renamed all custom design token CSS variables to use the --custom-* prefix.
  • Updated theme.css and all consumer usages accordingly.
  • Added scripts/design-parity tooling for automated Figma-to-code comparison.
  • Removed unused registry entries and standardised component registration format.
  • Added LOCAL_PUBLISH_GUIDE.md with step-by-step instructions for local package publishing.
  • Registry now exports all UI components via a wildcard pattern for easier consumption.
  • Added 24 previously unregistered components to the registry.
  • Introduced the ai-elements package with initial set of AI-focused UI primitives.
  • Replaced inline SVG icons in AI Elements with @luminpdf/icons imports.
  • Configured AI Elements for npm publishing and registry inclusion.
  • Fixed tab line indicator position regression introduced in 0.1.0.
  • Updated Tabs styling to match Figma DS-shadcn-001 specification.
  • Removed backdrop-blur from Dialog overlay per updated Figma spec.
  • Moved the consumer-facing guide to README.md so it appears on the npm package page.
  • Final pre-publish cleanup: verified registry, exports, and peer dependencies.