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:
| Size | Before (icon side) | After |
|---|---|---|
xs | 6px | 8px |
sm | 6px | 10px |
default | 8px | 10px |
lg | 8px | 10px |
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.
FilePickerDropzonenow attachesinputProps.onCancelto the input as a nativecancel
listener, and removes it on unmount or when the handler changes.
inputProps.onCancelis 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-primarylayer andsidebar-accent-foreground
text.
- Action label: the label no longer hovers. The
SidebarGroupActionbutton 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-*-foregroundcolours (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).
ComboboxContentnow accepts and forwardscollisionAvoidanceandcollisionPaddingto
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)inlib/avatar-initials.ts, re-exported fromavatar.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.
AvatarFallbackaccepts an optionalname. 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
+Ncount in an xsAvatarGroupalso 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.tsxnow carries its matching--card-*-foregroundtoken: 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@themekey aliases them:--ds-radius-md: 8pxwith
--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.cssstill 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.cssis now@import "./tokens.css"+@import "./theme-core.css".theme-core.cssis
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 nosupports-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.
FilePickerfollows automatically — it reusesDialogOverlay.DrawerandSheetkeep 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.
FilePickerListforces 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
FilePickerSidebarhas 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.jsonsynced.
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.
SidebarMenuButtonandSidebarMenuSubButtongaineddata-[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 oneloader-trimkeyframe; theagmark adds staggeredloader-star+loader-basekeyframes. - Props:
variant,size(number → px, or CSS length; default 48),color(defaults tocurrentColorso 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 underprefers-reduced-motion: reduce). - Import:
@luminpdf/ds/components/ui/loaderin-repo,@luminpdf/ds/loaderornpx shadcn add @lumin/loaderfor 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--pdfand--pdf-foregroundflip themselves, the variant needs nodark:pair -- it is thedefaultvariant'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.tsandscripts/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 usualtext-/border-variants. - Dark-mode
--popovermoved from neutral/800 (#262626) to neutral/900 (#171717), so dropdowns, popovers, selects, and every otherbg-popoversurface 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.tsandapp/_icons/lazy-registry.tsviapnpm generate:icons— now 1775 icons (119 deprecated shims). AddsLmLogoAskAiandLmMagicPencil; nothing removed. - Docs-site only:
@luminpdf/iconsis a devDependency consumed by the/iconspage, 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,SidebarGroupActionandSidebarMenuActionall gained the layer; theirhover:/active:/data-active:bg-sidebar-accentrules are gone. Text colour (sidebar-accent-foreground),data-active:font-medium, focus rings and sizing are unchanged.- The
outlinemenu button keeps itsbg-backgroundand both box-shadows; the hover tint now sits on top of them instead of replacing the fill. SidebarGroupLabelgains 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 ofdata-active. React rendersdata-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 toSidebarMenuBadgeandSidebarMenuAction, whose text colour keyed off the same peer selector. SidebarGroupLabel's text is now the fullsidebar-foregroundtoken. It wastext-sidebar-foreground/70, an opacity Figma does not apply (node 5198:1428 fills the label withbase/sidebar-foregroundat 100%).- Focus outline widths follow Figma's border widths:
SidebarMenuSubButton,SidebarGroupActionandSidebarMenuActiondrop tofocus-visible:ring-1(Figmaborder-width/border),SidebarMenuButtonkeepsring-2(border-width/border-2). The ring colour was alreadysidebar-ring. - Focus outlines now follow Figma's shared
Ringcomponent (node299:17), which every focused sidebar variant instances at 50% opacity:focus-visible:inset-ring-sidebar-ring/50withinset-ring-2on the menu row andinset-ring-1on the sub row, group action and row action. The oldring-2/ring-1drew 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
Ringcomponent's own 2px outer shadow (focus-visible:ring-2 focus-visible:ring-black/5), which only the menu row overrides away in Figma. SidebarMenuSubButtongainsleading-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-mobileentry inexports: it pointed athooks/use-mobile.tsxbut the file ishooks/use-mobile.ts.sidebar.tsxis the only importer, so nothing caught it — the Next docs app resolves through the@luminpdf/ds/*tsconfig path and skips theexportsmap, 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.
buttonGroupVariantsnow applies the sameborder-0to[data-slot=dropdown-menu-trigger][data-variant]:not([data-variant=outline]). The[data-variant]guard keeps the rule to triggers actually rendered as aButton; a bare trigger is untouched.outlinetriggers keepborder-input, exactly likeoutlinebuttons.- Playground gained a "With a dropdown trigger" row (outline / default / secondary) and
e2e/button-group.spec.tsasserts 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-1inset fromSelectGrouponto theSelectContentviewport, matching howdropdown-menu,context-menu, andmenubaralready 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.
TooltipTriggernow 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
lgandicon-lgsizes now add[&_svg:not([class*='size-'])]:size-5, following the patternxs/smalready use. Height, gap, padding and radius are unchanged. - The
[class*='size-']exclusion is untouched, so an explicitsize-*on a consumer's icon still wins. Spinnernow carriesdata-slot="spinner", and the large sizes size it to 20px via[&_[data-slot=spinner]]:size-5—Spinnersets its ownsize-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"orsize="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
linkvariant now addsoverflow-visible, unsetting the baseoverflow-clip. It has no background and a transparent border, so there was never anything to clip. - Keeps the 8px radius, so the
focus-visiblering stays rounded and still matches Figma (node 60:244). Also holds up inside aButtonGroup, where the small sizes re-applyrounded-mdafter 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-foregroundgray, and an explicitclassNamestill overrides viatwMerge. - 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-hiddenfrom theAvatarroot (matching the design-parity spec'soverflow: visible). The image still self-clips to the circle via its ownrounded-[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.Indicatorrendered<CheckIcon />unconditionally, and every fill selector was scoped todata-[state=checked]only. Radix setsdata-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 uncontrolleddefaultChecked="indeterminate"), and mirror eachdata-[state=checked]fill selector (bg/border/text, incl.aria-invalid) with adata-[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-controlledopenprop combined withmodal={false}leavesdocument.body.style.pointerEventsstuck at'none'forever (vaul's own reset only fires throughDrawer.Trigger's internal state change, not a controlledopenprop). Now watches for this viaMutationObserverand 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 anoverlayprop (defaulttrue) 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). Addedoverflow-hiddenso 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 thenextadvisories.- Added range-scoped
pnpm.overridesfor 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-autoso a tall dialog scrolls inside itself rather than scrolling the page (dvhaccounts for mobile browser chrome). - Changed the mobile width guard from
max-w-[calc(100%-2rem)]tomax-w-[calc(100vw-2rem)]so the 16px side gutter is always viewport-relative (the previous100%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
DropdownMenuShortcutstay muted on hover/focus — dropped thefocus:**:text-accent-foregrounddescendant override and the shortcut'sgroup-focusoverride 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
ItemStatesStorybook 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:
useResolvedCssVarsre-read on the next-themesresolvedTheme, but the provider applies the.darkclass in an effect that runs after the consumer effect, sogetComputedStyleread one commit behind. - Fix: both hooks now re-read via a
MutationObserveron<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
currentColortoggle - 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,Escclears 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 newColorUtilityButtonscomponent - Swatch click still copies
var(--…); the--varlabel 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).
| Component | Sub-elements updated |
|---|---|
| Combobox | ComboboxItem |
| Command | CommandItem |
| ContextMenu | ContextMenuItem, ContextMenuSubTrigger, ContextMenuCheckboxItem, ContextMenuRadioItem |
| DropdownMenu | DropdownMenuItem, DropdownMenuSubTrigger, DropdownMenuCheckboxItem, DropdownMenuRadioItem |
| Menubar | MenubarItem, 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,MenubarSubTriggernode mappings toscripts/figma-registry.tsfor upcoming design-parity coverage.
- Renamed CSS variables from
--components-card-{hue}/--components-card-{hue}-foregroundto `--card-{hue}` / `--card-{hue}-foreground`. - Tailwind utilities are now
bg-card-{hue}andtext-card-{hue}-foreground(e.g.bg-card-blue,text-card-blue-foreground). Migrate anyvar(--components-card-…)orbg-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}-foregroundpairs (17 hues). Superseded in 0.2.4 by--card-{hue}naming. - Registered all 34 in
@theme inlineso Tailwind utilities likebg-components-card-bluework out of the box. Superseded in 0.2.4 bybg-card-blue.
- Added
ComponentCardsSectionto the tokens reference page, showing bg/fg swatch pairs for all 17 hues.
- Replaced Radix
AvatarPrimitivewith a single<img>element and localAvatarContextfor 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, and17375: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, and17089:45580.
- Added Field component Figma registry, docs, playground, Storybook, E2E, and design-parity wiring.
- Bumped version to 0.2.2. Updated
peerDependenciesand 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.cssand all consumer usages accordingly.
- Added
scripts/design-paritytooling for automated Figma-to-code comparison.
- Removed unused registry entries and standardised component registration format.
- Added
LOCAL_PUBLISH_GUIDE.mdwith 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-elementspackage with initial set of AI-focused UI primitives.
- Replaced inline SVG icons in AI Elements with
@luminpdf/iconsimports.
- 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-blurfrom Dialog overlay per updated Figma spec.
- Moved the consumer-facing guide to
README.mdso it appears on the npm package page.
- Final pre-publish cleanup: verified registry, exports, and peer dependencies.