Changelog

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

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.

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-5Spinner 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-containerpadding-inline: var(--responsive-container-padding-x)
  • py-sectionpadding-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.