# Changelog

<!-- FORMAT RULES — the docs site parses this file at runtime.
     • Entry heading:  ## version — YYYY-MM-DD — Title
     • Blank line between EVERY block (paragraph, heading, list, code fence)
     • Section headings: ### Heading  OR  **Bold line** on its own line after a blank line
     • Never write bold mid-paragraph as a heading substitute — it merges into surrounding text
-->

## 5.6.0 — 2026-08-09 — TopNav userMenu placement

### Added

- `TopNavUserMenu` gains `placement?: 'start' | 'end'` (default `'end'`, unchanged behavior) — set `'start'` to render the desktop `userMenu` dropdown before other `actions` in the cluster instead of after.

No breaking changes.

## 5.5.0 — 2026-08-09 — TopNav flyout: fix close-on-hover-out, pixel-perfect row alignment

### Fixed

- 5.4.0's Floating UI rebuild of the desktop flyout panel fixed the original hover-flicker bug, but introduced a worse one: moving the mouse from the trigger toward the panel to click a row closed the panel before the click could land, because Radix's `NavigationMenuPrimitive.Trigger` still runs its own hover-close timer on `pointerleave` — previously cancelled by its matching `Content` calling back in as the pointer entered it, which no longer exists. The trigger is now click-to-open only (its `onPointerEnter`/`onPointerMove`/`onPointerLeave` call `preventDefault()`, which skips Radix's internal hover handlers via `composeEventHandlers`'s check), matching how every other dropdown in this system already behaves — `DropdownMenu`, `Select`, `Combobox` are all click-triggered, not hover-triggered.

- The top-level nav row ("Overview", "Hosts", …) sat ~3px, then (after a first pass) exactly 1px, off the baseline of a flyout trigger like "Services" next to it. Root cause, in two parts: a bare `<a>` is a `display: inline` box, whose rendered height comes from font ascent/descent metrics rather than `line-height` — shorter than a `flex` trigger's line-height-driven height by a few px. Fixing that with `inline-flex` closed most of the gap but left exactly 1px, because `inline-flex` is still an *inline-level* box from its parent `<li>`'s perspective — the `<li>` wraps it in an inline formatting line box, adding asymmetric half-leading space a true block-level `flex` child never gets. Both are now `flex` with an explicit height, removing the ambiguity outright; verified pixel-identical (`top`/`bottom`/`height` matching to four decimal places).

- The mobile accordion for flyout groups (e.g. "Services" collapsed on the mobile sheet) indented its rows with a left guide-line (`border-l`, matching `SidebarMenuSub`), while `mobileMenuSections`/`userMenu`'s accordion sections used a plain flat style with no line. Reported live as visually inconsistent — unified both to the flat style; the guide-line is gone.

No breaking changes.

## 5.4.0 — 2026-08-09 — TopNav flyout groups rebuilt on Floating UI; collapsible mobile sections

### Fixed

- `TopNav`'s desktop flyout groups (`navItems` entries with `items`) are rebuilt on Floating UI — the same positioning engine behind `Combobox`/`Select`/`DatePicker` — instead of `@radix-ui/react-navigation-menu`'s own `Content`/`Viewport`. Reported live: hovering a trigger opened the panel, but it then animated "smaller and smaller" until it collapsed to a line and vanished, and a detached gray arrow below the trigger's text made it sit visibly higher than sibling nav items. Root cause: Radix's `Viewport` centers content under the *whole* nav bar rather than the trigger that opened it, so for any trigger not near the bar's center, the panel renders away from the cursor — the pointer leaves the trigger before reaching the panel, so Radix starts closing it mid-open-animation, and it never finishes opening before closing again. The `NavigationMenuIndicator` caret added in 5.2.0 to bridge that gap never reliably initialized (it needs a `ResizeObserver` callback to compute its position) and could render as a plain in-flow flex item instead, which is what pushed the trigger row's height. The panel is now anchored directly to its own trigger (Floating UI's `flip`/`shift`), which structurally removes the mismatch rather than papering over it — the `Indicator` bridge is no longer needed and has been removed. `Escape` and outside-click-to-close, previously handled by Radix's `Content`, are reimplemented directly (mirroring `Select`'s pattern) since they're no longer rendered.

- The flyout trigger, the `userMenu` dropdown trigger, and the mobile menu toggle button were all missing `cursor: pointer` (native `<button>`s don't get it by default, and `Button` is the only component in this codebase that sets it explicitly) — on top of the flyout bug above, this made the trigger look non-interactive, likely why hovering was the only thing tried before clicking. All three now set it.

### Added

- `TopNavSection` (used by both `mobileMenuSections` and `userMenu`) gains `collapsible?: boolean` (default `true`) and `defaultOpen?: boolean` (default `false`) — sections now collapse behind their label as a tap-to-expand accordion by default, the same treatment flyout groups already had, instead of always rendering fully expanded. A mobile sheet with a nav list plus a store-list section plus an account section was turning into a long wall of rows with no way to skim it; collapsing by default cuts the visible row count roughly in half without hiding anything. Set `collapsible: false` on a section to keep the old always-expanded behavior, or `defaultOpen: true` to start a specific section open (e.g. keep "Account" visible, collapse a longer "Recent stores" list).

No breaking changes — `TopNavItem`/`TopNavProps` types are unchanged; this is a visual fix plus additive section options.

## 5.3.0 — 2026-08-08 — TopNav userMenu — one data structure for desktop + mobile

### Added

- `TopNav` gains `userMenu?: TopNavUserMenu` — a single account/user menu rendered from one data structure on both surfaces: a `DropdownMenu` next to the desktop actions, and the same `sections` (reusing the `TopNavSection[]` shape from `mobileMenuSections`) appended to the mobile sheet automatically. Previously a desktop dropdown and its mobile equivalent had to be hand-built and kept in sync separately — found while reviewing Butikkpils' real usage, where the avatar dropdown (home stores, display name, settings, admin, sign out) was defined twice, once via `DropdownMenu`/`DropdownMenuItem` for desktop and again via `mobileMenuSections`/`mobileMenuExtra` for mobile.

```tsx
<TopNav
  userMenu={{
    trigger: <><span>Ada</span><Avatar name="Ada Lovelace" size="sm" /></>,
    triggerLabel: 'Account menu',
    sections: [
      { label: 'Account', items: [
        { label: 'Settings', href: '/settings', icon: <Settings size={15} /> },
        { label: 'Sign out', onClick: handleSignOut, icon: <LogOut size={15} /> },
      ] },
    ],
  }}
/>
```

`TopNavItem`'s existing `href`/`onClick`/`icon`/`external`/`active` fields all work inside `userMenu.sections` exactly as they do in `mobileMenuSections` — the desktop `DropdownMenuItem` renders a link or a `onSelect` action accordingly.

No breaking changes. `userMenu` is additive; existing `actions`/`mobileMenuSections`/`mobileMenuExtra` usage is unaffected and composes alongside it.

## 5.2.0 — 2026-08-08 — TopNav onClick rows, flyout indicator; dvh viewport fixes

### Added

- `TopNavItem` gains `onClick?: () => void`. When set without `href`, the row renders as a `<button>` instead of a link, with identical styling — for actions like sign out that were previously impossible to express as a first-class, styled row and forced consumers to hand-roll unstyled markup via `mobileMenuExtra`.

- `TopNav`'s desktop flyout groups now render a `NavigationMenuIndicator` — a small caret that tracks the currently open trigger's horizontal position and bridges the gap into the panel below it, so the dropdown reads as anchored to the button that opened it rather than a disconnected floating box centered under the whole nav bar.

- Added a live "Services" flyout-group demo to the `TopNav` docs page — previously the flyout-group feature (shipped since 4.19.0) had no demo anywhere in docs-app.

### Fixed

- `Sidebar` (`SidebarProvider`'s root and the desktop rail's `--sidebar-height` default) and `TopNav`'s mobile menu panel used `100vh`/`min-h-screen`. On mobile browsers, `vh` is computed against the viewport with the address bar collapsed — taller than what's actually visible whenever the bar is showing. This made pages taller than the visible window (an unwanted scroll even on short pages) and caused sticky elements to visibly jump as the address bar auto-hides/shows mid-scroll. Switched to `dvh` (dynamic viewport height) throughout.

No breaking changes. All additive/visual fixes; existing usage renders and behaves identically aside from the mobile viewport-height correction.

## 5.1.0 — 2026-08-08 — TopNav mobile menu rows + grouped sections; Sidebar mobile drawer width fix

### Added

- `TopNavItem` gains `icon?: ReactNode`, rendered in dropdown/mobile rows (desktop flyout panels, the mobile menu, and mobile accordion groups). Not rendered in the top bar itself — that stays plain text, matching the existing horizontal-nav convention.

- `TopNav` gains `mobileMenuSections?: TopNavSection[]` — labeled row groups appended below nav items/actions in the mobile sheet (e.g. recent items, an account section), styled consistently with the rest of the menu (same "muted uppercase eyebrow" label as `SidebarGroupLabel`, one divider, no ad-hoc markup needed). Previously the only way to add this kind of content was the freeform `mobileMenuExtra` slot, which forced every consumer to hand-roll their own dividers/labels/row styling — inconsistently. `mobileMenuExtra` is unchanged and still available for truly custom content, rendered after `mobileMenuSections`.

### Fixed

- `TopNav`'s mobile menu never got the button-row treatment `Sidebar`'s composable primitives shipped in 5.0.0 — rows were bare `<a>` tags with no hover background, no consistent height, and accordion group triggers explicitly suppressed their own hover state (`hover:bg-transparent`). All dropdown/list surfaces (desktop flyout content, mobile nav rows, mobile accordion children, and the new `mobileMenuSections`) now share one row component: 40px min-height, rounded hover/active surface, optional icon slot, and the same yellow inset-shadow active indicator `SidebarMenuButton` uses. Accordion group children are now indented under a left guide-line matching `SidebarMenuSub`.

- `Sidebar`'s mobile off-canvas panel was rendering at the wrong width — roughly 59% of the viewport instead of the intended 288px (`--sidebar-width-mobile`), because `w-[--sidebar-width-mobile]`/`max-w-[--sidebar-width-mobile]` used Tailwind v3's square-bracket CSS-variable shorthand, which this project's Tailwind v4 no longer supports (v4 requires either `w-(--foo)` or an explicit `w-[var(--foo)]`). The utility silently produced no `width` rule at all, so the panel just shrank to fit its content. Fixed with the explicit `var()` form, plus the width variable is now also set inline on the sheet content itself so it no longer depends on inheriting through the `SidebarProvider` div — a Radix `Dialog.Portal` renders the mobile sheet directly under `<body>`, outside that div's subtree, so the CSS custom property was never in scope there regardless of the Tailwind syntax issue.

- The docs site's own layout (`docs-app`) had a large empty strip on the right at wide viewports: `.ds-shell` is a flex item inside a `w-full` wrapper but had no `width: 100%` of its own, so it shrank to its content's natural width instead of filling the viewport. No consumer impact — internal to `docs-app`, not part of the published package.

No breaking changes. Existing `TopNav`/`Sidebar` usage is visually improved with no prop changes required; `icon`/`mobileMenuSections` are additive.

## 5.0.1 — 2026-08-01 — Sidebar height is now overridable; docs coverage added

### Fixed

- `Sidebar`'s desktop rail used a bare `h-screen` (always exactly 100vh, absolute and non-overridable). That's correct for the common case — a `Sidebar` filling a real page — but it silently broke the moment `Sidebar` was embedded in a shorter, bounded container (e.g. a docs preview box): the rail ignored the container entirely and rendered at full viewport height. It's now `h-[var(--sidebar-height,100vh)]` — same 100vh by default, but any ancestor can override it by setting the `--sidebar-height` custom property (CSS custom properties inherit, so no new prop is needed). This is the same bug that was already visible in this repo's own docs — see the next entry.

### Documentation

- Found and fixed while auditing this repo's own docs-app coverage: the `DashboardShell` demo on the docs site's Screen patterns page was silently rendering its embedded sidebar at full viewport height instead of the intended ~420px preview box, because of the `h-screen` issue above. Fixed via the new `--sidebar-height` override.

- `Sidebar` itself — a major new export surface from 5.0.0 — had no live demo anywhere in docs-app, despite the release-checklist requirement to add one for new/changed components. Added a dedicated "Sidebar" section to the Screen patterns page demonstrating the raw `SidebarProvider`/`Sidebar`/`SidebarMenu` composition and the `collapsible="icon"` mode via a real `SidebarTrigger`.

No breaking changes — `--sidebar-height` is a pure addition; every existing `Sidebar`/`DashboardShell` usage renders identically since it already implicitly resolved to 100vh.

## 5.0.0 — 2026-07-31 — Sidebar primitive family replaces SidebarNav

### Breaking

- `SidebarNav` is removed. In its place, a shadcn-style composable sidebar primitive family: `SidebarProvider`, `Sidebar`, `SidebarTrigger`, `SidebarInset`, `SidebarHeader`, `SidebarContent`, `SidebarFooter`, `SidebarGroup`, `SidebarGroupLabel`, `SidebarGroupContent`, `SidebarMenu`, `SidebarMenuItem`, `SidebarMenuButton`, `SidebarMenuAction`, `SidebarMenuBadge`, `SidebarMenuSub`, `SidebarMenuSubItem`, `SidebarMenuSubButton`, and the `useSidebar()` hook. `SidebarNavItem`/`SidebarNavGroup` **types** are unchanged and still exported (now from `./Sidebar`), so `DashboardShell`'s `sidebarGroups`/`sidebarItems` props stay source-compatible — `DashboardShell` now builds this composition internally instead of rendering the old `SidebarNav`.

  New capabilities over the old `SidebarNav`: a real mobile off-canvas panel built on `Sheet` (full focus-trap/Escape/`aria-modal`, replacing `DashboardShell`'s previous hand-rolled `translate-x-full` drawer, which had neither), `collapsible="icon"` mode (collapses to an icon-only rail with `SidebarMenuButton`'s `tooltip` prop shown on hover/focus), and a `Cmd`/`Ctrl+B` keyboard shortcut (`SidebarProvider`'s `keyboardShortcut` prop, default on).

  Migration for direct `SidebarNav` usage — see `AGENTS.md`'s "Sidebar primitive family replaces SidebarNav" version note for a full before/after example.

- `DashboardTopBar`'s `menuOpen`/`onMenuToggle` props are replaced by a single `menuTrigger?: ReactNode` slot (e.g. `<SidebarTrigger className="lg:hidden" />`). Only relevant if you use `DashboardTopBar` directly — `DashboardShell` wires this internally, no change needed there.

- New peer-adjacent internal dependencies: `@radix-ui/react-slot` and `@radix-ui/react-use-controllable-state` are used by `Sidebar` (both already peer/explicit dependencies from earlier releases — no new install required).

### Fixed

- `lib/bypass-modal-scroll-lock.ts` (the scroll-lock workaround `DropdownMenu` uses so its portalled content isn't swallowed by a Dialog/Sheet's document-level scroll lock) is rewritten as a ref callback instead of a `useLayoutEffect` keyed off a `useRef` object. The previous implementation only attempted to attach its listeners once, at the wrapper component's initial mount — but `DropdownMenuContent`'s actual DOM node doesn't exist until the dropdown first opens, which is almost always *after* that mount. In the common case (a dropdown that starts closed), the bypass silently never attached. It now reattaches correctly on every open via the ref callback, which React invokes exactly when the real node mounts and unmounts. `lib/floating-dropdown.tsx` (the shared engine behind `Combobox`/`Select`/`DatePicker`/`Segmented`) is updated to the same API; its own usage happened to work before by coincidence (it recomputed `isOpen` on every toggle, which incidentally re-triggered the old effect) but is more robust with the new callback-ref design regardless.

No compatibility shim is provided for `SidebarNav` — per this project's stated preference, this ships as a clean break with a documented migration path rather than a deprecated wrapper.

## 4.19.0 — 2026-07-31 — TopNav flyout groups + framer-motion mobile menu

### Added

- `TopNavItem` gains an optional `items?: TopNavItem[]`. Setting it turns an entry into a flyout group instead of a direct link: on desktop it renders as a `NavigationMenu` trigger + flyout panel (arrow-key/Home/End/Escape navigation, direction-aware slide animation, built on `@radix-ui/react-navigation-menu`); on mobile it renders as a collapsible accordion section inside the existing hamburger menu (reusing the `Accordion` component — no new mobile primitive). One level of nesting; a group item omits `href`.

**Requires a new peer dependency**: `@radix-ui/react-navigation-menu` (`^1.2.0`). Existing `TopNav` usage without any grouped `items` still works exactly as before, but the package now imports this primitive unconditionally, so it must be installed alongside the other Radix peers (`@radix-ui/react-dialog`, `@radix-ui/react-dropdown-menu`, etc.) — run `npm install @radix-ui/react-navigation-menu`.

### Changed

- `TopNav`'s mobile hamburger panel now animates with `framer-motion` (the same `forceMount` + `AnimatePresence` recipe as `Sheet`, see 4.18.1) instead of CSS `@keyframes`. Closing now unmounts asynchronously after the exit transition — the same test-timing note from 4.18.1 applies here too (`await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())`).

- The dead `.tollerud-topnav-menu-overlay[data-state]`/`.tollerud-topnav-menu-panel[data-state]` CSS keyframe rules in `globals-layers.css` were removed (superseded by framer-motion, same as Sheet's in 4.18.1). The static box-styling rules for those classes are unchanged.

No breaking changes to existing props — `items` is opt-in and everything else is source-compatible. The new required peer dependency is the one install-time action existing consumers need to take.

## 4.18.2 — 2026-07-31 — CommandMenu traps Tab focus

### Fixed

- `CommandMenu` now traps `Tab`/`Shift+Tab` inside the palette while it's open, via `@radix-ui/react-focus-scope`. Previously only `Escape` and click-outside closed the palette — `Tab` could move keyboard focus out into the page behind it while the palette was still visually open, which violated the "focus management matters" contract in `KEYBOARD.md` that every other overlay (`Dialog`, `Sheet`) already met via Radix. No prop or behavior change beyond the trap itself — auto-focus-on-open, arrow-key navigation, and close-on-Escape are unchanged.

No breaking changes.

## 4.18.1 — 2026-07-31 — Sheet now animates with framer-motion

### Changed

- `Sheet` (and `Drawer`, which wraps it) now animates its overlay and panel with `framer-motion` instead of plain CSS `@keyframes`, using the same `--motion-duration-*`/`--motion-ease-*` timings as before. `framer-motion` was already a peer dependency of `@tollerud/ui` but was unused until now.

- Closing a `Sheet`/`Drawer` now unmounts asynchronously, after its exit transition finishes, instead of synchronously on `onOpenChange(false)`. Consumers reading the DOM immediately after closing (e.g. in tests) should `await` the removal — see `Sheet.test.tsx`'s `closes on Escape` test for the pattern (`await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())`).

- `Sheet` respects `prefers-reduced-motion: reduce` the same as before — the animation is skipped (zero duration) rather than removed, so behavior is unchanged for reduced-motion users.

- The dead `.tollerud-sheet-overlay[data-state]`/`.tollerud-sheet-panel[data-state]` CSS keyframe rules in `globals-layers.css` were removed now that `Sheet` no longer drives its animation off `data-state` CSS selectors. `TopNav`'s mobile menu still uses the CSS-keyframe approach for now (planned to migrate in a follow-up release).

### Internal

- `@radix-ui/react-compose-refs`, `@radix-ui/react-focus-scope`, and `@radix-ui/react-use-controllable-state` are now declared as explicit `dependencies` — they were already resolving transitively via `@radix-ui/react-dialog`/`@radix-ui/react-dropdown-menu`, so this is a documentation fix with zero install-size impact, not a new footprint.

No breaking changes — every consumer-facing prop is unchanged; only the close-unmount timing and the animation engine changed.

## 4.18.0 — 2026-07-31 — PasswordInput labelAction

### Added

- `PasswordInput` gains a `labelAction?: ReactNode` prop, rendered at the right edge of the label row (e.g. a "Forgot?" link next to "Password"). Both docs demos (`docs-app/components/pages/page-auth.jsx`, `docs-app/components/pages/page-forms.jsx`) already passed this prop, but it was silently dropped — `PasswordInputProps` never declared it, so it fell through to `...props` on the underlying native `<input>` instead of rendering.

No breaking changes — this is an opt-in addition.

## 4.17.1 — 2026-07-30 — Fix dropdown-search autofocus in floating popovers

### Fixed

- `FloatingDropdownPortal` (shared by `Combobox`, `Select`, `DatePicker`, and `Segmented`) hid its panel before Floating UI finished positioning by setting `visibility: hidden`. Browsers treat `visibility: hidden` descendants as unfocusable, so any `.focus()` call made as the panel mounted — notably `Combobox`'s `searchPlacement="dropdown"` autofocus of its search input — silently failed, leaving focus stuck on the trigger. The panel is now hidden with `opacity: 0` + `pointer-events: none` instead, which hides it identically without blocking focus.

## 4.17.0 — 2026-07-26 — StructuredCard, StatCard secondary value, AuthSplitLayout

### Added

- `Card` gains an explicit `structured?: boolean` prop. Previously, whether `Card` renders shell padding or delegates it to `CardHeader`/`CardContent`/`CardFooter` was auto-detected from `child.type.displayName` — a mechanism that can fail to survive a Next.js Server/Client Component boundary. Set `structured` explicitly to bypass that detection.

- `StructuredCard` — new convenience component composing `Card` (with `structured` set explicitly) + `CardHeader` + `CardTitle` + `CardContent`, for the common "title + actions + body" card shape, safe to compose across a Server/Client boundary.

- `StatCard`'s `value` prop now accepts `ReactNode` instead of only `string | number`, so a consumer can pass their own `<Input>` for an inline-editable tile without the library owning save/loading state.

- `StatCard` gains `secondaryValue?: ReactNode` and `secondaryTone?: 'default' | 'accent' | 'success' | 'error' | 'warning' | 'info'` — renders a secondary value (e.g. a per-unit price) as a small `Badge` under the main value.

- `AuthSplitLayout` — new component composing `Monogram` + `PageHeader` (with `shimmer`) + optional `NoirGlowBackground` into the two-panel hero/form auth screen layout (sign-in, sign-up, reset-password). Takes `projectName`, `eyebrow?`, `title`, `highlight`, `description`, an optional `decoration` slot for a flourish element in the hero panel, and `glow?: boolean` (default `true`).

### Documentation

- `SKILL.md` and `AGENTS.md`'s "Color tokens" tables now list `tollerud-success`/`tollerud-warning`/`tollerud-error`/`tollerud-info` — these tokens already existed and were already used by `Badge`, `CardChange`, `StatusDot`, and `Table`, but were missing from the consumer-facing token reference.

No breaking changes — every addition above is opt-in and existing usage is unaffected.

## 4.16.0 — 2026-07-25 — Table primitives, per-region Card accent, StatCard tone

### Added

- `Table` — new static table primitives (`Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell`, `TableCaption`) for fixed comparison tables and small datasets with no sorting, filtering, search, or pagination. Use `DataTable` when that interactivity is needed; use `Table` for everything else. `TableRow` takes a `highlight` boolean to tint a row; `TableCell` takes `tone?: 'success' | 'error' | 'warning' | 'info' | 'accent'` to color a computed or changed value (e.g. a scaled quantity), matching the tone vocabulary already used by `StatCard` and `Meter`.

- `CardHeader`, `CardContent`, and `CardFooter` each gain their own optional `accent?: boolean | 'filled'`, independent of the parent `Card`'s `accent`. Left unset, a region keeps inheriting the parent `Card`'s accent exactly as before. Set explicitly (including `accent={false}`), it overrides the cascade for just that region — so a single header, content area, or footer can be tinted (or excluded) regardless of the rest of the card.

- `StatCard` gains a `tone?: 'success' | 'error' | 'warning' | 'info'` prop that colors the value text and border, independent of the existing `accent` boolean. Use it for a status-carrying figure — e.g. an over-budget total in red — without needing the yellow brand-accent treatment.

- `Timeline`'s `TimelineItemData.title` is now `ReactNode` instead of `string`, so an item can mix rich content (e.g. a bolded actor name) with plain text.

- `Timeline` gains a `variant?: 'connected' | 'flat'` prop. `'connected'` (default) is the existing behavior — a vertical line between dots, timestamp beside the title. `'flat'` drops the connector, divides rows with a hairline border, and puts the timestamp on its own line below the title — suited to activity/audit-log style lists.

- `Timeline` status dots support a new `info` status (blue), matching the addition to `StatusDot` below.

- `StatusDot` gains a new `info` status — a static blue dot (no pulse animation, like `idle`) using the existing `--tollerud-info` token.

No breaking changes — every addition above is opt-in and existing usage is unaffected.

## 4.15.0 — 2026-07-21 — SegmentBarChart + DataTable toolbar alignment

### Added

- `SegmentBarChart` — horizontal stacked proportion bar for category breakdowns (spend, budget, usage shares). One rounded bar with in-bar percentage labels (hidden below `minPercentLabel`, default 8%) and a responsive two-column legend (swatch + label + formatted value). Segment colors default to `SEGMENT_BAR_COLORS` — a yellow-on-noir intensity scale exported from the package barrel. Opt-in `interactive` makes segments focusable with tooltips and keyboard navigation (←/→, Home/End, Esc).

- `SEGMENT_BAR_COLORS` — five-step yellow intensity cycle (`tollerud-yellow` → warm gold → muted noir mixes) for monochrome proportion charts.

### Fixed

- `DataTable` — `toolbarRight` aligns to the end on desktop when search and filter are absent (previously sat flush left). Mobile stacked layout unchanged.

### Docs

- Servers page — new **Toolbar actions** demo with `ButtonGroup` in `toolbarRight`.

## 4.14.0 — 2026-07-21 — Combobox create-option support

### Added

- `Combobox` gains `onCreateOption` — when set, a `Create "<query>"` row appears at the end of the list whenever the search text has no exact (case-insensitive) label match among the options, so users can add a value that doesn't exist yet (e.g. a new category) without leaving the field. The row shows alongside partial matches, not just on a true empty result, matching the pattern used by MUI Autocomplete's `freeSolo` and react-select's `Creatable`.

- Selecting the create row calls `onCreateOption(label)` with the trimmed query. Return a string to use as the new option's value (e.g. a server-generated id); return nothing and the typed text is used as both label and value. The component tracks created options locally so the newly selected value displays its label correctly even before the consumer's `options`/`groups` prop catches up.

- New `createOptionLabel?: (query: string) => string` prop customizes the row's text. Defaults to `Create "<query>"`.

- The create row participates in arrow-key navigation and `aria-activedescendant` alongside regular options; Enter or click both commit it.

No API change to existing props — `onCreateOption` is opt-in.

## 4.13.3 — 2026-07-20 — Motion token consolidation

### Changed

- Every component now uses the `duration-fast`/`duration-normal`/`duration-slow` and `ease-out`/`ease-in`/`ease-in-out` Tailwind classes (mapped in the preset to `--motion-duration-*` / `--motion-ease-*`) instead of arbitrary values (`duration-[150ms]`), Tailwind's numeric scale (`duration-150`), or a bare `transition-*` class with no duration at all. Roughly 40 components were touched (`Button`, `Accordion`, `Dialog`, `Sheet`, `Tabs`, `Select`, `Switch`, `Progress`, `Meter`, and more).

- `globals-layers.css` — `--transition-fast/normal/slow` are now shorthand aliases of `--motion-duration-*` + `--motion-ease-in-out` instead of a second, independently hardcoded set of the same values.

- `tollerud-preset.cjs` — `transitionTimingFunction` now maps `out`/`in`/`in-out` to the same cubic-beziers as `--motion-ease-out/in/in-out`, so Tailwind's `ease-out`/`ease-in`/`ease-in-out` utilities match the CSS tokens instead of the browser defaults. `transitionDuration` (`fast`/`normal`/`slow`) already existed but was unused before this release.

- `StatusDot` — the framer-motion pulse transition now reads `motionDuration`/`motionEase` from the new `lib/motion.ts` (a hand-mirrored JS copy of the CSS motion tokens) instead of hardcoding `duration: 0.4, ease: 'easeOut'`.

- A handful of components' visual timing shifted slightly to land on a token: 75–100ms transitions now run at 150ms (`fast`), several 200ms/300ms transitions now run at 150ms or 350ms depending on which token they were closer to. Chart value-transitions (`BarChart`, `Gauge`) intentionally keep their own 500ms timing — see the new Motion tokens section in SKILL.md/AGENTS.md for the documented exception.

No API change.

## 4.13.2 — 2026-07-13 — Email dark-mode fixes: footer wordmark + receipt table

### Fixed

- `EmailFooter` — the "A Tollerud Project" link now carries the `muted` class so the `@media (prefers-color-scheme: dark)` override recolors it alongside "All rights reserved." In dark clients (Apple Mail / iOS) the link previously kept its light-mode `#52525B` while the surrounding text lightened to `#AAAAAA`, so the wordmark rendered in two colors. Light mode is unaffected.

- `ReceiptEmail` — the line items, "Total" label, and total value are hand-rolled `Text` cells that were missed by the 4.13.0 light-first migration: they had light-mode inline colors but no `t-*` class, so the dark-mode override never reached them and they rendered dark-on-dark (invisible) in Apple Mail / iOS. They now carry the `text` class.

- `ReceiptEmail` — the total value was painted with the yellow accent (`#FFFF00`), which is unreadable on the white card that every client (including Gmail) renders. It is now `textPrimary`, with emphasis from weight and size instead of color.

- `VerifyEmail` — the raw verification URL is now wrapped in a `word-break: break-all` span so a long token wraps inside the card instead of stretching it and forcing horizontal scroll.

### Changed

- `EmailFooter` — the fine-print line (`© YEAR · address · Unsubscribe`) is now center-aligned. No API change.

- `EmailText` — the `fine` tone now matches the footer's fine print: `xs` size, the `fine` class, and the muted text color. It was previously a shrunk `muted` tone (`sm` size, secondary color, `muted` class), so "fine" print rendered larger and a shade darker than the footer's. Templates that use `tone="fine"` (the verify link, expiry notes, the reset note) now render as true fine print.

### Added

- `packages/email/src/email.test.tsx` — a render-test suite (22 cases) covering every primitive and template. It guards the light-first dark-mode contract (every text-colored element carries a `t-*` class; every applied class has a matching dark-mode rule), the footer wordmark link's `muted` class, the `EmailText` tone map, and that `ReceiptEmail` never paints text with the yellow accent.

## 4.13.1 — 2026-07-13 — shaders-react peer → 0.0.77 (lockstep)

### Changed

- The optional `@paper-design/shaders-react` peer dependency (used by `NoirGlowBackground`) advances from `^0.0.76` to `^0.0.77`. It is pinned by this peer and must move in **lockstep** — the peer, docs-app, and the consumer fixtures are bumped together. Consumers should update `@paper-design/shaders-react` to `0.0.77` to match; don't bump it independently.

- Dependabot now **ignores** `@paper-design/shaders-react` in every directory so it is never bumped on its own — it is advanced manually alongside a `@tollerud/ui` release.

- Rolled the pending Dependabot consumer-fixture group into the fixture: `@radix-ui/react-dialog` 1.1.19, `@radix-ui/react-dropdown-menu` 2.1.20, `@radix-ui/react-progress` 1.1.12, `lucide-react` 1.24.0, `@types/node` 26.1.1.

## 4.13.0 — 2026-07-13 — Email: light-first theme + Gmail fixes

### Fixed

- `@tollerud/email` now renders correctly in **Gmail**, where the previous dark-first design broke (white background, mis-colored button, missing monogram). Gmail ignores `color-scheme`, applies its own color transforms to dark emails, and strips inline SVG — so the email is now **light by default**, which every client (Gmail included) renders predictably.

### Changed

- **Light-first theme with a dark-mode enhancement.** Inline styles are light (white card on a light-gray page, dark text); a `@media (prefers-color-scheme: dark)` `<style>` block restores the noir palette on clients that support it (Apple Mail, iOS Mail). The email carries `<meta name="color-scheme" content="light dark">` and `bgcolor` attributes on its surfaces.

- **Monogram is now a hosted PNG**, not inline SVG (which Gmail/Outlook strip). `BrandMark` renders the dark monogram on light backgrounds and swaps to the yellow monogram in dark mode. Hosted at `design.tollerud.dev/brand/email-monogram-{dark,yellow}.png`; regenerate with `node scripts/gen-email-monogram.mjs`. Override with `logoSrc` for your own hosted image.

- The yellow accent **button keeps `#FFFF00` with black text in both modes** (it reads on light and dark), so it no longer gets mangled by Gmail dark mode. Accent lines (dividers, the footer underline) use the warmer `#E8D500` so they stay visible on white.

### Breaking

- `@tollerud/email` — the `color` prop was removed from `BrandMark`, `EmailHeader`, and `EmailFooter`, and the `BrandMarkColor` type is no longer exported. The monogram color is now chosen automatically by color-scheme (dark on light, yellow on dark). Remove any `color="…"` on these components; pass `logoSrc` instead to supply your own mark.

## 4.12.1 — 2026-07-12 — Email footer polish

### Changed

- `@tollerud/email` `EmailFooter` — the wordmark line is now right-aligned with the monogram kept on the left (monogram left, "A Tollerud Project. All rights reserved." right). The wordmark reads "A Tollerud Project." with a period, and the yellow underline under the tollerud.no link is now 2px thick. No API change — `labels` / `monogram` / `address` / `unsubscribeUrl` / `links` are unchanged.

## 4.12.0 — 2026-07-12 — Email: overridable copy (i18n) + style escape hatches

### Added

- `@tollerud/email` templates gain an overridable `copy` prop. Each template exports a `*Copy` interface (`WelcomeEmailCopy`, `VerifyEmailCopy`, `PasswordResetEmailCopy`, `ReceiptEmailCopy`) whose dynamic lines are functions (so interpolated values still flow through) and static lines are strings. Pass any subset to reword — or to localize (e.g. Norwegian) — without forking the template. Defaults are unchanged.

- `@tollerud/email` primitives gain an optional `style` escape hatch — `EmailLayout` (content card), `EmailHeader`, `EmailFooter`, `EmailButton`, `EmailHeading`, `EmailText`, `EmailDivider`, and `BrandMark`. Inline styles are merged **last**, so they override the token defaults for one-off tweaks. The brand tokens remain the defaults.

### Notes

- Both additions are non-breaking — existing usage renders identically. The visual design stays token-locked by default; `style` is a deliberate per-instance opt-out, not a theme.

## 4.11.0 — 2026-07-12 — Email branded header + Tollerud footer

### Added

- `@tollerud/email` `EmailHeader` — optional branded header ("logo lockup"): the Tollerud monogram beside the project name in large display type, with an optional hairline divider. Props: `productName`, `monogram` (default `true`), `logoSrc`/`logoAlt` (hosted-image escape hatch), `color`, `align` (`'left' | 'center'`), `divider`.

- `@tollerud/email` `BrandMark` — the Tollerud monogram for email. Inline SVG by default (renders in Apple Mail / iOS Mail / some webmail); pass `src` for a hosted image where clients strip SVG (Outlook desktop, Gmail). `color` (`'yellow' | 'white' | 'black'`), `height`.

- All four templates (`WelcomeEmail`, `VerifyEmail`, `PasswordResetEmail`, `ReceiptEmail`) gain an optional `header?: EmailHeaderProps` — pass it to render the branded header at the top.

### Changed

- `@tollerud/email` `EmailFooter` now renders the real Tollerud footer — the monogram plus the "A Tollerud Project" wordmark linking to tollerud.no, mirroring the web `Footer`. The monogram geometry is synced from `components/monogram-geometry.ts` (single source of truth, shared with `@tollerud/footer`).

### Breaking

- `@tollerud/email` `EmailFooter` props changed. The generic `brandName` prop was removed in favor of `labels` (mirroring the web `Footer` `FooterLabels`: `tollerudProject`, `attribution?`, `allRightsReserved`), plus `monogram` / `logoSrc` / `color`. `address`, `unsubscribeUrl`, and `links` are unchanged. Migrate `footer={{ brandName: 'X', … }}` to `footer={{ labels: { tollerudProject: 'A Tollerud Project' }, … }}`.

## 4.10.0 — 2026-07-12 — @tollerud/email package + shared token module

### Added

- `@tollerud/email` — a new sibling package for building on-brand, client-tested HTML emails with React Email. Ships email-safe primitives (`EmailLayout`, `EmailButton`, `EmailHeading`, `EmailText`, `EmailDivider`, `EmailFooter`) and ready-made templates (`WelcomeEmail`, `VerifyEmail`, `PasswordResetEmail`, `ReceiptEmail`), plus a re-exported `render`.

- `lib/tokens.ts` — a generated JS token module (79 concrete values) parsed from the `:root` block of `tokens.css`. It exists so non-CSS consumers can read literal token values; `tokens.css` remains the author-facing source of truth. Email needs this because `var(--tollerud-*)` does not resolve in mail clients — every value is inlined.

### Why email is a separate package

Email is a different render target from the browser: table-based layout, inline-only styles, no CSS custom properties, and aggressive client quirks (Outlook, Gmail). Shipping the web `@tollerud/ui` components into inboxes would break in many clients, so `@tollerud/email` shares the *design tokens* rather than the components. Each project composes its own templates from the shared primitives.

### Tooling

- `npm run gen:tokens` regenerates `lib/tokens.ts`; `verify:tokens` drift-checks it in `validate`.

- `npm run sync:email` syncs the email package's tokens + version; `verify:email-sync` enforces lockstep in `validate`. `sync:registry` now version-locks both `@tollerud/footer` and `@tollerud/email`.

### Dark-mode note

The email primitives carry explicit `bgcolor` + `color-scheme` meta so the noir palette survives clients that force color inversion. See `packages/email/README.md`.

## 4.9.9 — 2026-07-10 — CardChange flat (unchanged) state

### Changed

- `CardChange` — `direction: "flat"` for unchanged metrics: minus icon, default `text-tollerud-info` (blue), label defaults to `—` when `value` is omitted. `StatCard` `change.direction` accepts `flat` as well.

## 4.9.8 — 2026-07-10 — Subtle accent tint on Card header/footer bands

### Changed

- `Card` — `accent={true}` on structured cards now adds a subtle yellow tint to `CardHeader` / `CardFooter` bands (~4% yellow into raised surface), matching the yellow border without a full `accent="filled"` callout. Body stays `surface-raised`.

## 4.9.7 — 2026-07-10 — CardChange and CardHeader actions

### New

- `CardChange` — shared up/down delta chip (`value`, `direction`, optional `tone`). Used by `StatCard` and available on any structured card header.
- `CardHeader` — `actions?: ReactNode` slot for trailing header content (change chip, buttons, badge).

### Changed

- `StatCard` — change indicator now renders via `CardChange` (same look, shared primitive).

## 4.9.6 — 2026-07-10 — Softer Card header/footer bands

### Changed

- `Card` — `CardHeader` and `CardFooter` use a subtle `color-mix` darken of the raised surface instead of page-level `noir-950`, so section bands no longer read as black trays.

## 4.9.5 — 2026-07-10 — Fix PriceDisplay size classes and CardContent background

### Fixed

- `PriceDisplay` — `size` now applies literal Tailwind classes so the primary value scales correctly in production builds (dynamic class maps were purged).
- `Card` — `CardContent` explicitly uses `bg-tollerud-surface-raised` so the body band stays visible between darker header/footer regions.

## 4.9.4 — 2026-07-10 — Card header/footer bands and DataTable selection borders

### Changed

- `Card` — optional compound parts: `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`. Header and footer use a darker `noir-950` band (yellow-tinted on `accent="filled"`) with a single seam border — no doubled lines between regions. Plain `<Card>…</Card>` keeps default `p-6` padding.
- `DataTable` — row selection uses a uniform opaque background instead of per-cell inset rings, fixing double borders between adjacent selected cells.
- `PriceDisplay` — `size?: "sm" | "md" | "lg"` (default `"md"`) scales the primary value and secondary badge together for dense table cells or prominent list rows.

## 4.9.3 — 2026-07-09 — ScrollRail item wrappers stretch for h-full children

### Changed

- `ScrollRail` item wrappers use `flex h-full flex-col` so children with `h-full` or `flex-1` fill the slot height without consumer boilerplate.

## 4.9.2 — 2026-07-09 — ScrollRail visibleCount for fill-row-then-scroll layouts

### Changed

- `ScrollRail` peek padding and peek width inset now apply **only when content overflows** — no empty gutter when all items fit in the row.
- Scrollport sets `container-type: inline-size` and exposes `--scroll-rail-gap`, `--scroll-rail-peek`, and `--scroll-rail-peek-inset` CSS variables aligned with `gap` / `peek` props.

### New

- `ScrollRail` **`visibleCount`** — number of items visible in the scrollport at once. When child count ≤ `visibleCount`, items expand to fill the row evenly; when child count exceeds it, each slot locks to `1/visibleCount` of the scrollport (minus gaps and peek) and the rest scroll horizontally. Replaces consumer `@container` + `calc(100cqw …)` workarounds for achievement rails and product rows.

## 4.9.1 — 2026-07-08 — Dialog layout, sizes, and DialogPanel

### Changed

- `DialogContent` default width is now `md` (`max-w-xl`, 576px) instead of `max-w-lg` (512px). Pass `size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'` for confirmations, forms, and wide detail views.
- `DialogHeader` and `DialogFooter` now use bordered regions (matching `Drawer` / `Sheet` polish). Body content scrolls inside the panel when it overflows (`max-h-[min(85dvh,900px)]`).
- `DialogContent` partitions children into header, scrollable body, and footer automatically.

### New

- `DialogBody` — explicit scrollable body slot for long forms and log viewers.
- `DialogPanel` — controlled helper with `open`, `onClose`, `title`, `description`, `footer`, and `size` (same ergonomics as `Drawer`).

## 4.9.0 — 2026-07-08 — Add ScrollRail horizontal scroll primitive

### New components

- `ScrollRail` — horizontal scroll rail for card rows, image strips, and any overflow content. Continuous scroll with configurable **peek** (`sm` / `md` / `lg`) so the next item shows a sliver, **fadeEdges** (default on) at scroll boundaries, and optional **controls** (`true` or `'auto'` when content overflows). Uniform `itemWidth` for product-card rows; omit for intrinsic child widths. Keyboard ←/→ when controls are shown; does not use `touch-pan-x`, so vertical page scroll works when a swipe starts on the rail.

## 4.8.57 — 2026-07-08 — Fix: DataTable no longer blocks vertical page scroll on touch

### Fixed

`DataTable`'s horizontal scroll wrapper used Tailwind `touch-pan-x` (`touch-action: pan-x`), which told the browser to handle only horizontal gestures on the table. On mobile, when a swipe started on table rows or headers, vertical page scroll was blocked — the page felt stuck unless the touch began outside the table. Removed `touch-pan-x`; `overflow-x-auto` and `overscroll-x-contain` still handle horizontal scroll for wide tables with pinned columns.

## 4.8.56 — 2026-07-07 — Portalled dropdowns now positioned by Floating UI

### Changed

Replaced the hand-rolled positioning in `FloatingDropdownPortal` (`Combobox`, `Select`, `DatePicker`, `Segmented`) with **Floating UI** — the same engine Radix Popover and shadcn/ui use. `autoUpdate` keeps the panel glued to its trigger across scroll, resize, layout shifts, and mobile viewport changes (iOS keyboard, zoom, address bar), and the `flip` / `shift` / `size` middleware handle top/bottom flipping, staying in view, and height clamping. This resolves the iOS Combobox saga from 4.8.53–4.8.55 (auto-zoom, floating, top/bottom flip, detachment) with a battle-tested library instead of bespoke scroll/touch heuristics.

Behaviour change: on touch devices the dropdowns now **stay open and reposition** while the page scrolls, matching Radix Popover / shadcn, instead of closing on scroll (the workaround added in 4.8.26). Outside-click and Escape still close them.

### Removed

Deleted the internal `lib/dropdown-placement.ts` module (`getDropdownPlacement`, `getFloatingDropdownCoords`, `useDropdownPlacement`, `dropdownPlacementClasses`) and the `onOutsideScroll` prop on `FloatingDropdownPortal` — both superseded by Floating UI. These were internal; no public component API changed.

### Internal

Added `mergeRefs` to `lib/utils`. Added `@floating-ui/react-dom` as a dev dependency (bundled into the package output; not a new peer dependency).

## 4.8.55 — 2026-07-07 — Fix: mobile dropdown detached from its trigger after opening

### Fixed

Follow-up to 4.8.54. Placing the panel once and never repositioning left it stranded away from its trigger on iOS: after opening, the page scrolls to lift the focused search input above the keyboard (and the address bar collapses), moving the anchor while the panel stayed put.

`useFloatingDropdownCoords` now tells the two kinds of mobile scroll apart. A programmatic settle scroll (keyboard/address-bar/focus, no finger drag) repositions the panel so it stays glued to the trigger; a user fling and its momentum tail dismiss it. The distinction is armed on `touchmove` and held through the inertial tail, so a plain tap-to-open repositions while a fling closes. Repositions are coalesced to one per animation frame, and `visualViewport` resize (keyboard/zoom/address bar, which fires no `scroll` event) is also handled, so the panel tracks the field without visibly floating.

## 4.8.54 — 2026-07-07 — Fix: portalled dropdowns floating/flipping on mobile scroll

### Fixed

Follow-up to 4.8.53. Two mobile placement defects in the portalled dropdowns (`Combobox`, `Select`, `DatePicker`, `Segmented`):

The panel floated around while scrolling on iOS. 4.8.53 repositioned the panel on every non-drag scroll event, and iOS fires a storm of them — momentum and rubber-band scrolling, address-bar collapse, and the programmatic scroll from focusing the search input. `useFloatingDropdownCoords` no longer repositions on window scroll/resize on touch devices; it places the panel once and keeps it stable, dismissing only on a genuine finger drag. Desktop still tracks the anchor on scroll/resize.

The panel flipped between top and bottom placement while filtering. Narrowing a `Combobox` to a single result shrank the popover, and placement was recomputed from scratch each time, so a top-placed panel could snap to the bottom mid-type. Placement is now sticky (`getDropdownPlacement` accepts the current side): once open, the panel keeps its side as long as it still fits.

## 4.8.53 — 2026-07-07 — Fix: Combobox unusable on iOS Safari + touch dropdowns dismissing on focus/zoom scroll

### Fixed

`Combobox` with `searchPlacement="dropdown"` was unusable on iOS Safari. Two compounding defects are addressed.

The in-dropdown search input was `text-sm` (14px). iOS Safari auto-zooms any focused input below 16px, and the input is auto-focused on open. It now renders at ≥16px on coarse-pointer devices (`pointer-coarse:text-base`) so focusing it no longer triggers a zoom. Desktop sizing is unchanged (14px).

Portalled dropdowns (`Combobox`, `Select`, `DatePicker`, `Segmented`) closed on *any* scroll behind them on touch devices — including the programmatic scroll iOS emits when auto-zooming a focused input or scrolling a focused field above the on-screen keyboard. `useFloatingDropdownCoords` now dismisses only on a genuine user touch drag (gated on `touchstart`/`touchmove`); focus- and zoom-induced scrolls reposition the panel instead of closing it.

Net effect: `searchPlacement="dropdown"` now opens, stays open, and is searchable on iOS Safari, and non-search dropdowns no longer dismiss unexpectedly when the page scrolls to reveal a focused field.

## 4.8.52 — 2026-07-07 — Fix: chart SR data table inflated page height

### Fixed

- `ChartSrTable` (the visually-hidden data table behind `TimeSeriesChart`, `AreaChart`, and `Heatmap`) put the `sr-only` class directly on the `<table>`. A `display: table` element treats `height: 1px` as a **minimum**, so the table kept its full content height while absolutely positioned — inflating the document's `scrollHeight` and leaving a large empty area below the page. Most visible on `/charts` with the 71-row `Heatmap` table (page height ballooned to ~19,600px). Fixed by moving `sr-only` to a wrapping `<div>` (a block element collapses to 1px and clips the table with `overflow: hidden`). The table stays fully accessible; no API change.

## 4.8.51 — 2026-07-07 — New: Heatmap (calendar activity grid)

### Added

- `Heatmap` — a calendar activity heatmap (GitHub-contributions style): `data` of `{ date, value }` renders as week columns × weekday rows, cells colored by intensity buckets (a yellow-on-noir scale, overridable via `colors`). Hover shows a date + value tooltip; a visually-hidden data table (`srTable`, default on) gives screen readers every valued day. Props: `startDate`/`endDate` (default from data range), `weekStartsOn` (0 Sun / 1 Mon, default Mon), `formatValue`/`formatDate`/`locale`, `showLegend` (Less→More scale), `cellSize`/`cellGap`, `ariaLabel`. New `Heatmap`/`HeatmapDatum` exports.

This completes the chart set expansion: stacked/grouped `BarChart` (4.8.49), `Gauge` (4.8.50), and `Heatmap` (4.8.51).

## 4.8.50 — 2026-07-07 — New: Gauge (radial dial)

### Added

- `Gauge` — a new radial gauge for a single metric (disk %, load, quota). A 270° dial with a bottom gap, filling from bottom-left clockwise. Props: `value`, `min`/`max` (default 0–100), `label`, `formatValue`, `tone` (`default` yellow / `success` / `warning` / `error`, token-backed like `Meter`), `size`, `thickness`, and `fluid` (scales to the container, stays circular). Exposes `role="meter"` with `aria-valuenow`/`min`/`max` and an accessible name; the arc animation respects `prefers-reduced-motion`. Complements the linear `Meter`.

## 4.8.49 — 2026-07-07 — Stacked & grouped BarChart

### Added

- `BarChart` — new `series` prop (`BarChartSeries[]` = `{ label, values, color? }[]`) with `categories` renders multiple series as **grouped** bars (side by side per category) or **stacked** bars (`stacked`) — one bar per category with a segment per series. Colors cycle the `--chart-1…5` palette unless set explicitly. A swatch legend renders above (`showLegend`, default on). When `interactive`: grouped bars are individually focusable (roving tabindex, one aria-label per category·series); stacked columns are focusable per category with a tooltip and aria-label listing every series. New `BarChartSeries` type exported.

### Changed

- `BarChart` single-series usage (the `data` prop) is unchanged — same markup, values, accent bar, tooltips, and keyboard behavior as 4.8.48.

## 4.8.48 — 2026-07-07 — Fluid Sparkline & Donut

Completes responsive charts — every chart can now flex to its container.

### Added

- `Sparkline` — new `fluid` prop fills the container width instead of a fixed `width`. It measures the real width with a `ResizeObserver` and plots in pixels, so the dots stay round. `height` still applies. Off by default, so table-cell sparklines keep their fixed micro size.

- `Donut` — new `fluid` prop scales the ring down to fit narrow containers (capped at `size`, so it never grows past it). The square viewBox scales uniformly, so the ring stays perfectly circular with no distortion and no `ResizeObserver`; the legend wraps below the ring when space is tight. Off by default.

Together with `TimeSeriesChart`, `AreaChart`, `BarChart`, and `Meter` (already fluid-width), all chart components are now responsive.

## 4.8.47 — 2026-07-06 — Fix AreaChart aspect-ratio distortion

### Fixed

- `AreaChart` — point markers rendered as ellipses and stroke widths looked stretched on containers wider than 520px. The SVG used a fixed `viewBox="0 0 520 h"` with `preserveAspectRatio="none"`, so filling the container width scaled the coordinate system non-uniformly. It now measures its real rendered width with a `ResizeObserver` and plots in pixels (the same approach as `TimeSeriesChart`), so circles stay round and strokes uniform at any width — still fully responsive. The interactive tooltip anchors in pixels rather than percentages.

- `lib/chart-interaction.tsx` (internal) — removed the now-unused `viewBoxWidth` option from `useChartInteraction` (it existed only to rescale pointer coordinates for AreaChart's stretched viewBox).

## 4.8.46 — 2026-07-06 — Multi-series TimeSeriesChart

Phase 6 (final) of the charts plan ([docs/CHARTS_PLAN.md](docs/archive/CHARTS_PLAN.md)) — the parked stretch item, now shipped.

### Added

- `TimeSeriesChart` — new `series` prop (`TimeSeriesSeries[]` = `{ label, points, color? }[]`) renders multiple lines on one chart. Colors cycle the `--chart-1…5` palette unless given explicitly. A single shared crosshair spans all series with a dot per line, and the tooltip stacks each series' value (color swatch + label + value) at the active point. Keyboard navigation, the live-region announcement, and the SR data table all cover every series — the table gains one value column per series. A swatch legend renders above the chart (`showLegend`, default on). Series should be index-aligned (share dates by position).

### Changed

- `TimeSeriesChart` single-series usage (the `data` prop) is unchanged — same area fill, gradient, latest-value badge, tooltip, and output as 4.8.45. Multi-series mode omits the area fill and latest-value badge (single-series concepts); `renderTooltip` applies to single-series only.

- `lib/chart-interaction.tsx` (internal) — `ChartTooltip` gained optional colored `rows` for stacked multi-series tooltips; `ChartSrTable` now takes `columns` + per-row `cells` so it can render one column per series (`AreaChart` and single-series `TimeSeriesChart` updated to the new shape, output identical).

## 4.8.45 — 2026-07-06 — Screen-reader data tables for line & area charts

Phase 5 (final interactive phase) of the charts plan ([docs/CHARTS_PLAN.md](docs/archive/CHARTS_PLAN.md)): screen-reader users get the actual numbers behind the SVG, not just "Time series chart".

### Added

- `TimeSeriesChart` — renders a visually-hidden data table (one row per point, date + formatted value) so screen readers can browse the data. New `srTable` prop, default `true`; set `false` to opt out. This complements the live-region announcements from keyboard navigation.

- `AreaChart` — same visually-hidden data table via a new `srTable` prop, defaulting to `interactive` (the SVG is `aria-hidden` when static, so no table then). Rows use each point's `label` when provided, otherwise "Point N".

- `lib/chart-interaction.tsx` (internal) — new `ChartSrTable` component. Deliberately not wired into `BarChart` or `Donut`: their data is already exposed as accessible text (per-bar `aria-label`s, the semantic legend list), so a table would duplicate what assistive tech already reads. `Sparkline` is excluded as a micro-chart.

### Changed

- The accessibility test suite (`components/a11y.test.tsx`) now covers all five interactive charts (`TimeSeriesChart`, `AreaChart`, `BarChart`, `Donut`, `Sparkline`) with zero axe violations, including the new SR data tables.

## 4.8.44 — 2026-07-06 — BarChart focusable bars + Donut palette defaults and interactive legend

Phases 3 and 4 of the charts plan ([docs/CHARTS_PLAN.md](docs/archive/CHARTS_PLAN.md)). Unlike the SVG-crosshair charts, these two move real focus between labeled elements, so screen readers announce each bar/legend row natively — no live region needed.

### Added

- `BarChart` — new `interactive` prop makes each bar a focusable target with a roving tabindex: Tab reaches the chart, ←/→ move between bars, Home/End jump, Esc dismisses (consumed only while a bar is focused). Hover or focus shows a tooltip; each bar carries an aria-label ("Oslo: 420 kr"). New `formatValue` (formats the visible value labels, tooltips, and aria-labels — default unchanged raw numbers) and `ariaLabel` (group name) props.

- `Donut` — `segment.color` is now optional: omitted colors cycle the `--chart-1…5` palette tokens, which were defined in `globals-layers.css` but previously unused by any component. The cycle is exported as `CHART_SERIES_COLORS` from the package barrel for consumer reuse. New `interactive` prop makes the legend rows focusable (roving tabindex, ↑/↓ or ←/→, Home/End, Esc): the active row highlights its arc, dims the others to 35 % opacity, and reveals the raw value next to the percentage. Rows carry aria-labels ("Diesel: 420, 42%"). New `ariaLabel` names the legend list.

### Changed

- `BarChart` and `Donut` are now client components (`'use client'`), consistent with the other interactive charts. Static usage renders identical output to 4.8.43 — no new tab stops, no roles, no visual changes.

- `BarChart` bar-height and `Meter` fill-width transitions now respect `prefers-reduced-motion` (`motion-reduce:transition-none`).

- `Donut` legend markup is now a semantic `<ul>`/`<li>` list (visually identical).

## 4.8.43 — 2026-07-05 — AreaChart & Sparkline: interactive tooltips + keyboard navigation

Phase 2 of the charts plan ([docs/CHARTS_PLAN.md](docs/archive/CHARTS_PLAN.md)) — both charts adopt the shared interaction core from 4.8.42.

### Added

- `AreaChart` — new `interactive` prop adds a crosshair, hover dot, tooltip, touch support, and the full chart keyboard contract (Tab focuses → latest point, ←/→ step, Home/End jump, Esc clears without closing a surrounding Dialog) with polite screen-reader announcements. New supporting props: `formatValue`, `renderTooltip(point, index, formattedValue)`, `ariaLabel`. `data` now also accepts labeled points (`{ value, label }[]`) so tooltips and announcements can name each point — plain `number[]` keeps working unchanged.

- `Sparkline` — the existing `interactive` prop now also shows a tooltip and supports the same keyboard contract and announcements. New `formatValue` and `ariaLabel` props. Idle interactive sparklines keep marking the latest point with a dot, exactly as before.

### Changed

- `AreaChart` is now a client component (`'use client'`), matching `Sparkline` and `TimeSeriesChart`. It still server-renders and imports fine from Server Components; static (non-`interactive`) usage renders the identical decorative `aria-hidden` SVG as 4.8.42.

- `lib/chart-interaction.tsx` (internal) — `useChartInteraction` gains a `viewBoxWidth` option that scales padding from viewBox units to client pixels for stretched-viewBox charts (AreaChart's 520-unit canvas), and `ChartTooltipLayer` accepts CSS length/percentage positions.

## 4.8.42 — 2026-07-05 — TimeSeriesChart keyboard navigation + shared chart interaction core

First phase of the charts plan ([docs/CHARTS_PLAN.md](docs/archive/CHARTS_PLAN.md)): a shared interaction layer that later phases reuse for AreaChart, Sparkline, BarChart, and Donut.

### Added

- `TimeSeriesChart` — full keyboard support. Tab focuses the chart (yellow focus ring) and activates the latest point; ArrowLeft/ArrowRight step through points with the crosshair + tooltip following; Home/End jump to the first/last point; Escape clears the active point. Escape is consumed only while a point is active, so a surrounding Dialog stays open (same rule as `Combobox`/`Select`). Keyboard-selected points are announced to screen readers via a visually-hidden polite live region.

- `lib/chart-interaction.tsx` (internal for now) — `useChartInteraction` (one active-index state shared by pointer, touch, and keyboard), `ChartTooltip` / `ChartTooltipLayer` (the tooltip bubble and clamped positioning extracted from TimeSeriesChart), `ChartLiveRegion`, and `clampTooltipX`. Not yet exported from the package barrel; later chart phases consume it, and it may be promoted to public API when the shape settles.

### Changed

- `TimeSeriesChart` — hover/touch behavior is unchanged, but the crosshair, hover dot, and tooltip now also render for keyboard-selected points. The latest-value badge hides while any point is active (previously only while hovering). Mouse, touch, and visual output are otherwise identical to 4.8.41.

## 4.8.41 — 2026-07-05 — Fix position:sticky broken for all PageShell descendants

### Fixed

- `PageShell` — the root element used `overflow-hidden`, which establishes a scroll container. Per spec, `position: sticky` elements stick to their nearest scrolling ancestor — so every sticky descendant (`DashboardShell` sidebar, `TopNav sticky`, `DashboardTopBar`, any consumer sticky element) silently behaved as `position: relative` because the shell root itself never scrolls. Replaced with `overflow-clip`, which clips the decorative grid/glow layers identically but does **not** create a scroll container (supported in all evergreen browsers, Chrome 90+ / Firefox 81+ / Safari 16+).

- `DashboardShell` — the sticky sidebar wrapper is a child of a `flex min-h-screen` row, so default `align-items: stretch` stretched it to the full content column height, leaving a sticky element zero travel room even with the `PageShell` fix. Added `lg:self-start` so the wrapper collapses to the sidebar's `h-screen` height and can stick. The mobile drawer is unaffected — below `lg` the wrapper is `position: fixed`, where `align-self` has no layout effect.

Both fixes are required for the `DashboardShell` sidebar; `TopNav sticky`, `DashboardTopBar`, and consumer sticky elements only needed the `PageShell` fix. Reported and verified live in the Butikkpils consumer app. Regression tests assert `overflow-clip` on all `PageShell` backgrounds, no `overflow-hidden` ancestor above a sticky `TopNav`, and `lg:self-start` on the sidebar wrapper.

Audited the rest of the library for the same pattern: all other `overflow-hidden` uses are on self-contained widgets (cards, progress tracks, dropdown panels) that never wrap page-level sticky content, and `DataTable`'s pinned columns stick within their own scroll region by design.

## 4.8.40 — 2026-07-05 — Keyboard & screen-reader pass: aria-activedescendant, sortable header buttons, tokenized focus rings, Checkbox indeterminate

### Breaking

- `DataTable` — `Column.render` is now always `(value, row) => ReactNode`. The single-parameter `(row) => …` form is removed: it was auto-detected from the function's declared parameter count (`fn.length`), which silently broke with default or rest parameters (`(value = '—', row) => …` has length 0 and was treated as row-only).

**Migration** — prepend `_v, ` to row-only callbacks:

```tsx
// Before
{ key: 'status', render: (row) => <Badge>{row.status}</Badge> }

// After
{ key: 'status', render: (_v, row) => <Badge>{row.status}</Badge> }
```

TypeScript surfaces every un-migrated callback as a compile error (a typed row parameter is not assignable to `unknown`). In plain JS/JSX, an un-migrated callback silently receives the cell **value** as its first argument instead of the row — grep for `render: (` and check each single-parameter arrow.

### Added

- `Checkbox` — new `indeterminate` prop for mixed states (e.g. a select-all with only some rows selected). Sets the native `indeterminate` property (announced as "mixed" by screen readers) and shows a dash indicator instead of the checkmark. Cleared automatically when the user clicks the checkbox.

- `DataTable` — the select-all header checkbox now shows the indeterminate state when only some rows on the current page are selected.

### Fixed

- `Combobox`, `Select`, `CommandMenu` — arrow-key highlight is now announced to screen readers. Options carry stable `id`s and the focused control sets `aria-activedescendant` to the highlighted option. `Select`'s trigger additionally gains `role="combobox"` and `aria-controls` (ARIA 1.2 select-only combobox pattern).

- `DataTable` — sortable column headers now render a real `<button>` inside the `<th>`, so keyboard users can Tab to the header and sort with Enter/Space. Previously the `onClick` sat directly on the `<th>` and sorting was mouse-only. `aria-sort` behaviour is unchanged.

- `DataTable` — the "select all on page" checkbox state was computed with a hardcoded row index. With index-fallback row keys (rows without an `id`/`key` field and no `rowKey` prop), selecting just the first row made the header checkbox report every row as selected.

- `Combobox` — the highlighted option now scrolls into view while arrowing through lists taller than the dropdown (parity with `Select`).

- `Combobox` / `Select` — pressing Escape while the dropdown is open no longer closes a surrounding Radix `Dialog`. The open dropdown consumes the event (innermost layer wins); with the dropdown closed, Escape propagates normally.

- `CommandMenu` — keyboard navigation and Enter now operate on the *filtered* results. Previously the key handler indexed into the unfiltered item list, so with an active search query Enter could run a different command than the one highlighted. The highlighted row also scrolls into view in long lists.

### Changed

- Focus rings on `Input`, `Textarea`, `PasswordInput`, `Select`, `Combobox`, `DatePicker`, and `TagInput`, plus the `AreaChart` stroke and gradient, now use `var(--tollerud-yellow-warm)` (with `#E8D500` fallback) instead of a hardcoded hex — re-themed consumers get correctly tinted focus rings and charts. `Select`'s error-state focus ring uses `var(--tollerud-error)`.

- `DataTable` — internal: the resolved search-keys array is memoized, so the filter pipeline no longer recomputes on every render when `searchKeys` is omitted.

## 4.8.39 — 2026-07-03 — Fix Combobox Dialog focus: stop focusout interception

### Fixed

- `FloatingDropdownPortal` `useDialogEscapeHatch` — Radix `FocusScope` has a second focus-redirection handler: a bubble-phase `focusout` listener on `document`. When focus moves from inside a Dialog to the portal search input, `focusout` fires first with `relatedTarget = portal input`. Since `container.contains(relatedTarget)` is false, Radix calls `focus(lastFocusedElement)` and steals focus back before `focusin` ever fires — making the v4.8.38 `focusin` escape hatch useless. Fixed by adding a capture-phase `focusout` listener on `document` that calls `stopPropagation()` when `relatedTarget` is inside the portal element. Capture phase fires before Radix's bubble-phase handler, so Radix never sees the event.

## 4.8.38 — 2026-07-03 — Fix Combobox dropdown search auto-focus inside Dialog

### Fixed

- `Combobox` `searchPlacement="dropdown"` — auto-focus on the search input now works correctly when the combobox is rendered inside a Radix `Dialog`. The previous `useEffect` on `[open, searchPlacement]` fired one render too early: `FloatingDropdownPortal` returns `null` on the first render after `open` becomes `true` (it needs a `useLayoutEffect` to compute coords first), so `dropdownSearchRef.current` was `null` when the effect ran. The fix uses a callback ref on the search input that calls `queueMicrotask(() => node.focus())` as soon as the input mounts. The microtask defers focus until after React's layout-effect phase, when `FloatingDropdownPortal`'s `useDialogEscapeHatch` (now upgraded from `useEffect` to `useLayoutEffect`) has already attached its `focusin` `stopPropagation` listener — so Radix Dialog's `FocusScope` never sees the focus event and cannot redirect it.

- `FloatingDropdownPortal` — `useDialogEscapeHatch` changed from `useEffect` to `useLayoutEffect`. This guarantees the `pointerdown`/`focusin` stop-propagation listeners are attached synchronously during React's commit phase, before any microtask-deferred focus can fire. The `requestAnimationFrame` fallback for late-mounting refs is removed — `popoverRef.current` is always set by the time layout effects run (refs are updated before layout effects in React's commit order).

## 4.8.37 — 2026-07-03 — Combobox: replace setTimeout focus with useEffect

### Fixed

- `Combobox` `searchPlacement="dropdown"` — replaced `setTimeout(() => dropdownSearchRef.current?.focus(), 0)` with a `useEffect` that runs when `open` and `searchPlacement` change. `setTimeout` is unreliable here: the element may not be in the DOM when the timer fires, and in the browser's event loop there is no guarantee about ordering relative to Radix's focus processing. `useEffect` runs after React has committed the render, guaranteeing the element exists. The `FloatingDropdownPortal` `focusin` stop-propagation added in v4.8.36 remains the essential piece that prevents Radix Dialog's FocusScope from redirecting focus away from the portalled input.

## 4.8.36 — 2026-07-03 — Fix Combobox dropdown unusable inside Dialog

### Fixed

- `Combobox` (and all portalled dropdowns: `Select`, `DatePicker`) — when rendered inside a Radix `Dialog`, two problems made the portalled dropdown non-interactive:
  1. **Dialog closed on click** — Radix's `DismissableLayer` registered a `pointerdown` listener on `document` (bubble phase). Clicking inside the portalled dropdown bubbled to `document` where Radix saw it as an "outside click" and closed the dialog.
  2. **Focus trap blocked typing** — Radix's `FocusScope` (`trapped={true}`) registered a `focusin` listener on `document` (bubble phase). When focus moved to the portalled search input, Radix immediately redirected it back into `DialogContent`, making the input unreachable.

  Fixed in `lib/floating-dropdown.tsx` by attaching native `pointerdown` and `focusin` listeners (bubble phase) to the portal element. These fire before the document-level handlers Radix registers, and `stopPropagation()` prevents Radix from seeing either event. The fix is automatic — no consumer API changes needed.

## 4.8.35 — 2026-07-03 — Fix GlowCard glow visibility

### Fixed

- `GlowCard` — glow was invisible when wrapping components with a solid background (e.g. `Card`). The overlay was rendered **behind** the content and blocked by the child's background color. Fixed by moving the overlay above the content in DOM order and adding `mix-blend-mode: screen`, so the radial gradient blends on top of whatever the children render. On the library's dark surfaces, `screen` adds luminosity without affecting pointer events.

## 4.8.34 — 2026-07-02 — First-class a11y: aria wiring, Button loading, Tooltip focus, Card asChild, forwardRef

### Added

- `Button` — new `loading` prop renders an inline `Spinner`, sets `disabled` and `aria-busy="true"` while active. Width stays stable so the layout does not shift.

- `Card` — new `asChild` prop (via Radix Slot) lets the card render as any element while keeping all visual classes.

- `Tooltip` / `TooltipTrigger` — now opens on keyboard focus (`onFocus`) and closes on blur (`onBlur`), satisfying WCAG 2.1 SC 1.4.13. Mobile click/touch behaviour unchanged.

### Changed

- All form fields with an `error` prop (`Input`, `Textarea`, `PasswordInput`, `Select`, `Combobox`, `FileUpload`, `TagInput`, `DatePicker`, `RadioGroup`) — the error message element now carries a stable `id`, and the control receives `aria-describedby` and `aria-invalid="true"` when an error is present.

- All form fields now accept a `required` prop (or pass it through from `HTMLAttributes`). Setting `required` adds `aria-required="true"` on the control and a red `*` marker next to the label (hidden from assistive technology via `aria-hidden`).

- `FileUpload` and `Combobox` — wrapped with `forwardRef`. `FileUpload` forwards to the outer wrapper `<div>`; `Combobox` forwards to the root `<div>`.

## 4.8.33 — 2026-07-02 — Fix Card density, GlowCard glow, remove duplicate PasswordStrength doc

### Fixed

- `Card` — `density="compact"` now reduces padding to `p-3`. The `[[data-density=compact]_&]:p-3` Tailwind selector also makes all `Card`s inside a `data-density="compact"` wrapper compact without needing the prop on each card.
- `GlowCard` — `intensity` now controls the **opacity** of the glow overlay (default 0.15) rather than a gradient color stop position. The previous implementation produced a small hard-edged circle of full-brightness yellow instead of a soft bloom. The gradient now always fades from full color at the cursor to transparent at 70% of the 600px radius, with `intensity` modulating how bright the entire overlay appears.

### Changed (docs only)

- `PasswordStrength` section removed from the Components page — it is documented on the Forms page only.

## 4.8.32 — 2026-07-02 — Translate all docs demos to English

### Changed (docs only)

- All demo content in `docs-app/` is now in English. Affected sections: `PromoSection` (title, description, buttons, eyebrow), `PriceDisplay` (store names, currency changed to `$`), `ListCard` (beer names changed to English brands, prices in `$`), `Segmented` sort options (labels and values in English), `TimeSeriesChart` value suffix demos (`kr/l` → `$/gal`, `nb-NO` locale changed to `en-US`).

## 4.8.31 — 2026-07-02 — Multi-word shimmer for PromoSection and PageHeader

### Changed

- `PromoSection` — `shimmer` now accepts `string | string[]`. Pass an array to accent multiple words or phrases independently. Single-string behaviour is unchanged.
- `PageHeader` — `shimmer` / `titleAccent` now accept `string | string[]`. Same multi-accent behaviour. Overlapping matches are silently skipped; non-matching entries are ignored. No migration needed for existing single-string consumers.

## 4.8.30 — 2026-07-02 — PromoSection shimmer and PageHeader-style eyebrow

### Changed

- `PromoSection` — eyebrow now renders as `font-mono text-xs uppercase tracking-[0.22em] text-tollerud-yellow`, matching `PageHeader`. The previous `Pill` rendering is removed.

### Added

- `PromoSection` — `shimmer?: string` wraps the first matching substring of `title` in `.tollerud-display-shimmer`. Behaves identically to `PageHeader`'s `shimmer` prop. Only applies when `title` is a plain string.

## 4.8.29 — 2026-07-02 — Fix PromoSection mobile layout; add contentWidth prop

### Fixed

- `PromoSection` — two-column grid now collapses to single-column on mobile (`grid-cols-1` below `sm:`). Text block is always first on mobile regardless of `visualPlacement`; the visual slot renders below it via CSS `order`.
- `PromoSection` — visual slot now has `overflow-hidden` to prevent horizontal overflow on small viewports.
- `PromoSection` — `background="raised"` now uses `border-y` instead of `border` so the block can go full-bleed edge-to-edge without visible corner borders.

### Added

- `PromoSection` — `contentWidth?: "sm" | "md" | "lg" | "xl" | "full"` (default `"xl"`) caps the inner content at a max-width while the outer wrapper can span the full viewport. Pair with `className="w-screen -mx-6"` or similar on the host page for a true full-bleed section.

## 4.8.28 — 2026-07-02 — Add PromoSection marketing block

### Added

- `PromoSection` — two-column marketing block with a text column and a consumer-controlled visual slot. Props: `eyebrow`, `title`, `description`, `actions`, `visual`, `visualPlacement?: "right" | "left"` (default `"right"`), `background?: "default" | "raised"` (default `"default"`), `textWidth?: "narrow" | "balanced" | "wide"` (default `"wide"`). Collapses to single-column on small viewports.

## 4.8.27 — 2026-07-02 — Add PriceDisplay and ListCard; extend Card accent; fix Sparkline clipping

### Added

- `PriceDisplay` — compact price display with a primary value and an optional secondary `Badge`. `highlight="cheapest"` switches to success coloring. `align?: "left" | "right"` (default `"right"`) for list row and table cell use.
- `ListCard` — hover card shell for list and grid items. Renders as `<a>` when `href` is provided. `highlight="cheapest"` applies a yellow border tint. Children are consumer-controlled.

### Changed

- `Card` — `accent` now accepts `true | "filled" | false`. `accent="filled"` adds `bg-tollerud-yellow/5` in addition to the yellow border tint, for callout boxes and cheapest-item highlights. Existing `accent={true}` behaviour is unchanged.

### Fixed

- `Sparkline` — added `viewBox` and `overflow="hidden"` to the SVG so stroke bleed from `strokeLinecap="round"` no longer escapes the element bounds at small sizes (e.g. `84×26` with `curve="step"` and `fill`).

## 4.8.26 — 2026-06-23 — Fix mobile scroll lag on portalled dropdowns

### Fixed

- `FloatingDropdownPortal` — on touch devices (`pointer: coarse`), outside scroll now closes the dropdown instead of repositioning it. Repositioning via React state updates caused a visible one-frame lag on every scroll tick on mobile. Affects `Combobox`, `Select`, `DatePicker`, and `Segmented` (collapsed mobile mode).

## 4.8.25 — 2026-06-22 — Add searchPlacement prop to Combobox

### Added

- `Combobox` — `searchPlacement?: 'trigger' | 'dropdown'` (default `'trigger'`). In `'dropdown'` mode the trigger becomes a button showing the selected value (like `Select`), and the search input moves inside the popover with a search icon. Useful when a cleaner trigger is preferred or when the combobox sits next to other Select fields.

## 4.8.24 — 2026-06-18 — Add mobileMenuExtra slot to TopNav

### Added

- `TopNav` — `mobileMenuExtra?: ReactNode` renders at the bottom of the mobile nav sheet, below nav items and actions, separated by a divider. Consumer controls all markup — `TopNav` just provides the slot.

## 4.8.23 — 2026-06-18 — Fix PageShell inner wrapper flex chain

### Fixed

- `PageShell` — inner content wrapper (`relative z-10`) now always applies `flex flex-col flex-1` so a `flex flex-col min-h-screen` outer shell correctly stretches content to fill the viewport. Added `contentClassName?: string` to customise the inner wrapper when needed.

## 4.8.22 — 2026-06-18 — Add showMobileLogo prop to DashboardTopBar and DashboardShell

### Added

- `DashboardTopBar` — `showMobileLogo?: boolean` (default `true`) gates the mobile monogram link. Pass `false` when the consumer renders its own logo in the top bar.
- `DashboardShell` — threads `showMobileLogo` through to `DashboardTopBar`.

## 4.8.21 — 2026-06-18 — Add PasswordStrength component

### Added

- `PasswordStrength` — strength bar + rule checklist for signup and change-password flows. Accepts `value: string` and optional `rules?: PasswordRule[]` to override the defaults. Default rules: min 8 chars, uppercase, lowercase, number, special character. Strength level (weak / fair / good / strong) is derived from the fraction of rules passed and uses the existing error/warning/info/success tokens. Also exports `passwordRules` (the default rule array) for composing custom rule sets.

## 4.8.20 — 2026-06-18 — Add StatCard icon prop

### Added

- `StatCard` — `icon?: ReactNode` renders an icon alongside the label in the top row. Pass any icon element (e.g. a Lucide icon).

## 4.8.19 — 2026-06-18 — Fix StatCard arrow direction

### Fixed

- `StatCard` — `direction: 'up'` now shows an up arrow and `direction: 'down'` shows a down arrow. The `rotate-180` transform was applied to the wrong condition — the SVG path draws a down arrow by default, so the rotation was inverted.

## 4.8.18 — 2026-06-18 — Use text-base across all form field inputs

### Fixed

- `Input`, `PasswordInput`, `Combobox`, `DatePicker`, `Textarea`, `Select` — all form field triggers now use `text-base` (16px). Previous releases mixed `text-sm` and `text-base`; 4.8.17 incorrectly standardised on `text-sm`. `text-base` is correct for form inputs (readability, prevents iOS auto-zoom on focus).

## 4.8.17 — 2026-06-18 — Align form input height across all field components

### Fixed

- `Input`, `PasswordInput`, `Combobox`, `DatePicker`, `Textarea` — all form fields now use `text-sm` (14px) and `py-2.5` padding, matching `Select`. Previously `Input`, `PasswordInput`, `DatePicker`, and `Textarea` used `text-base` (16px) with `py-2`, and `Combobox` used `py-2` with `text-sm`, causing inconsistent heights when mixing field types in the same form.

## 4.8.16 — 2026-06-18 — Fix SidebarNav scroll when nav items overflow

### Fixed

- `SidebarNav` — nav content area now scrolls independently when items overflow the viewport height. Added `min-h-0` alongside the existing `flex-1 overflow-y-auto` so the flex child actually creates a scroll context (classic flex `min-height: auto` bug).

## 4.8.15 — 2026-06-18 — StatCard change tone override

### Added

- `StatCard` — `change.tone?: 'success' | 'error' | 'warning' | 'info' | 'accent'` decouples the badge color from arrow direction. Useful when the semantic meaning of a change differs from its direction (e.g. a price drop is good). Omitting `tone` preserves the default: `up` = success (green), `down` = error (red).

## 4.8.14 — 2026-06-18 — Fix Combobox input font size

### Fixed

- `Combobox` — input now uses `text-sm` (14px) to match dropdown items (was `text-base` / 16px)

## 4.8.13 — 2026-06-16 — Drawer dropdown scroll and click fix

Replace competing RemoveScroll shards with native scroll-lock bypass on portalled menus.

### Fixed

- `FloatingDropdownPortal` / `DropdownMenu` — native `wheel` / `touchmove` listeners (`stopImmediatePropagation` in capture + `stopPropagation` in bubble) so react-remove-scroll on Radix Dialog/Sheet does not block list scroll
- Portalled menus — `pointer-events-auto` so items stay clickable when `body` is scroll-locked
- `Sheet` / `Dialog` — restore Radix `Dialog.Overlay` only (removes second `RemoveScroll` from `ModalScrollLockProvider` that broke clicks in 4.8.12)

### Removed

- `ModalScrollLockProvider` and context shard registry — superseded by native bypass (4.8.10–4.8.12 approach)

## 4.8.12 — 2026-06-16 — Drawer dropdown scroll via context shards

Wire portalled Select/Combobox lists into the active Sheet/Dialog RemoveScroll instance.

### Fixed

- `ModalScrollLockProvider` — React context bridge: wraps sheet/dialog content, owns `RemoveScroll` shards state, renders scrim
- `FloatingDropdownPortal` / `DropdownMenu` — register portalled DOM nodes with the nearest provider on mount (React portals preserve context)
- Replaces module-level shard registry that could not reach `RemoveScroll` when the overlay was only a sibling of content

## 4.8.11 — 2026-06-16 — Fix stale sheet overlay on navigation

Prevent `tollerud-sheet-overlay` from staying in the DOM and blocking clicks after closing a drawer or switching routes.

### Fixed

- `Sheet` — wrap overlay and content in `DialogPrimitive.Portal` so Radix `Presence` unmounts `ModalScrollLockOverlay` when the sheet closes
- `ModalScrollLockOverlay` — disable `RemoveScroll` when dialog content is not `data-state=open`
- Closed sheet overlay — `pointer-events: none` during exit animation

## 4.8.10 — 2026-06-16 — Drawer dropdown scroll (RemoveScroll shards)

Fix portalled Select/Combobox lists that still could not scroll inside Drawer/Sheet after v4.8.9.

### Fixed

- Register portalled menu DOM nodes as `react-remove-scroll` shards (native document listeners ignore React `stopPropagation`)
- `Sheet` / `Dialog` — replace Radix `Dialog.Overlay` scroll-lock with `ModalScrollLockOverlay` that shards dialog content plus open portalled menus
- `FloatingDropdownPortal` / `DropdownMenu` — auto-register while open

## 4.8.9 — 2026-06-16 — Scrollable portalled menus inside Drawer/Sheet

Fix Select, Combobox, and DropdownMenu lists that could not scroll when opened inside a modal drawer.

### Fixed

- `FloatingDropdownPortal` — stop wheel/touch propagation so react-remove-scroll (Radix Dialog/Sheet) does not block list scroll on portalled panels
- `DropdownMenu` — same scroll-lock compatibility for Radix-portalled menus inside `Drawer` / `Sheet` / `Dialog`

## 4.8.8 — 2026-06-16 — Portalled dropdown shadow consistency

Standard elevation for floating menus so portalled panels read clearly over tables and cards.

### Changed

- `FloatingDropdownPortal` — default `shadow-lg` (`--shadow-lg`) on all portalled panels
- `Select`, `Combobox`, `DatePicker`, `Segmented` — removed one-off shadow utilities; inherit portal default
- `DropdownMenu` — `shadow-lg` (was `shadow-md`) to match other popovers

## 4.8.7 — 2026-06-16 — FileUpload i18n labels

Configurable drop-zone CTA copy for custom text and translations.

### Added

- `FileUpload` — `clickLabel?` and `dragLabel?` override the default “Click to upload” / “or drag and drop” prompt (`dragLabel=""` hides the drag hint)

## 4.8.6 — 2026-06-16 — Toast visibility and settings nav spacing

Larger toast chrome, longer default duration, and clearer spacing in settings section nav.

### Changed

- `Toast` / `useToast` — larger padding, type, and icons; default duration 4.5s (was 3.8s)
- `Toaster` (Sonner) — matching larger text/padding; default duration 4.5s
- `SettingsLayout` — `gap-xs` between sidebar nav items
- `Stack` — `as="nav"` for semantic nav stacks

## 4.8.5 — 2026-06-16 — ButtonGroup fusion and TopNav lockup

Fix fused button chrome in groups and slightly enlarge the TopNav project title.

### Fixed

- `ButtonGroup` — child buttons no longer keep standalone border-radius/border from `.tollerud-btn` layer CSS; dividers use inset shadow so middle segments fuse cleanly with text labels

### Changed

- `TopNav` — project name uses `text-base`; desktop nav links get a little extra left margin after the monogram lockup

## 4.8.4 — 2026-06-16 — Button ghost semantic variants

Ghost buttons that tint on hover for success, warning, and info — same pattern as `ghost-destructive`.

### Added

- `Button` — `variant="ghost-success"`, `ghost-warning`, `ghost-info` (ghost at rest; semantic text, background tint, and border on hover/focus)
- `DataTable` bulk actions accept the new ghost semantic variants

## 4.8.3 — 2026-06-16 — Button ghost-destructive variant

Softer destructive button for dense toolbars — ghost at rest, red tint on hover.

### Added

- `Button` — `variant="ghost-destructive"` for archive/deactivate actions in `ButtonGroup` and table toolbars where permanent red chrome is too heavy

## 4.8.2 — 2026-06-16 — Portalled form dropdowns

Select, Combobox, DatePicker, and Segmented mobile menus render in a fixed portal so they are not clipped inside scroll or overflow containers (for example `DataTable`).

### Fixed

- `Select` — options list portals to `document.body` with viewport positioning
- `Combobox` — suggestion list portals to `document.body`
- `DatePicker` — calendar panel portals to `document.body`
- `Segmented` — `collapseMobile` dropdown portals to `document.body`

### Added

- `FloatingDropdownPortal` / `useFloatingDropdownCoords` — shared portalled menu helper (`lib/floating-dropdown.tsx`)
- `getFloatingDropdownCoords` — fixed-position placement helper on `lib/dropdown-placement.ts`

## 4.8.1 — 2026-06-16 — Segmented mobile dropdown

`collapseMobile` now opens options in a floating dropdown instead of expanding inline.

### Changed

- `Segmented` — mobile `collapseMobile` uses an absolute dropdown overlay (matches Select/Combobox), not inline expansion

## 4.8.0 — 2026-06-16 — Segmented collapseMobile

Mobile-friendly segmented control that collapses to the selected option on narrow viewports and opens a dropdown overlay.

### Added

- `Segmented` — `collapseMobile?` shows only the active option below `md`; tap to open dropdown overlay, select to collapse
- `useIsMobile` — internal hook for the `md` breakpoint (`lib/use-mobile.ts`, not exported)

## 4.7.4 — 2026-06-16 — Form field border radius

Align `Select` and `FileUpload` field radii with `Input`, `Combobox`, and `DatePicker`.

### Fixed

- `Select` — trigger uses `rounded` (was `rounded-lg`); dropdown panel unchanged
- `FileUpload` — drop zone and file rows use `rounded` (was `rounded-lg` / `rounded-md`)

## 4.7.3 — 2026-06-16 — Chart value prefix and suffix

Optional `valuePrefix` / `valueSuffix` on `TimeSeriesChart` for tooltip, axis, and badge formatting without a custom callback.

### Added

- `TimeSeriesChart` — `valuePrefix?` and `valueSuffix?` wrap locale-formatted numbers (ignored when `formatValue` is set)

## 4.7.2 — 2026-06-16 — Chart value formatting

`formatValue` docs, `formatChartDecimal` helper, Segmented key fix, and `renderTooltip` formatted-value arg.

### Added

- `formatChartDecimal` — decimal formatter with optional suffix (e.g. `57,0 kr/l` via `formatValue` + `locale="nb-NO"`)
- `TimeSeriesChart` — `renderTooltip` third argument `formattedValue` (from `formatValue` or locale default)

### Fixed

- `Segmented` — stable React keys when mapping options (fixes warning in `TimeSeriesChart` range toolbar)

### Docs

- Charts page — `formatValue` live demo, tooltip snippet uses `formattedValue`

## 4.7.1 — 2026-06-16 — English chart presets

`TIME_SERIES_PRESETS` and default `locale` now ship in English.

### Changed

- `TIME_SERIES_PRESETS` — labels: 3 mo · 6 mo · 1 yr · 2 yr · All (was nb-NO)
- `TimeSeriesChart` — default `locale` is `en-US`; chart formatters default to `en-US` (`nb-NO` still appends ` ,-` to values)

### Migration

Norwegian apps: pass custom `ranges` (e.g. `3 mnd`, `Alt`) and `locale="nb-NO"` on `TimeSeriesChart`.

## 4.7.0 — 2026-06-16 — Interactive time series charts

Stepped area charts with hover, range selection, and enhanced sparklines — pure SVG, no Recharts.

### Added

- `TimeSeriesChart` — wide vector chart with `curve="step"`, crosshair hover, tooltip, Y-axis labels, and optional `ranges` + `Segmented` toolbar
- `TIME_SERIES_PRESETS` — English range labels: 3 mo · 6 mo · 1 yr · 2 yr · All (`durationMs` filters from latest point). Norwegian UI: pass custom `ranges` with `locale="nb-NO"`.
- `lib/chart-series` — shared path, scale, and format helpers (used by charts)

### Changed

- `Sparkline` — `curve`, `fill`, and `interactive` props for stepped micro charts with hover dot

### Migration

Nothing breaking. Use `TimeSeriesChart` for price history; keep `AreaChart` for simple static series.

## 4.6.16 — 2026-06-16 — Magnetic button glow

Ship pointer-following glow for primary and terminal buttons as a first-class package export.

### Added

- `initButtonGlow()` — mount once at the app root; tracks pointer position on `.tollerud-btn--primary`, `.tollerud-btn--terminal`, and opt-in `.tollerud-btn-glow`
- `BUTTON_GLOW_SELECTORS` and `ButtonGlowOptions` — configure selector scope and event root
- `@tollerud/ui/button-glow` subpath export for tree-shaking
- Magnetic glow CSS in `globals-layers.css` (`is-glowing`, `--glow-x` / `--glow-y`); disabled under `prefers-reduced-motion`
- `examples/next-starter` — `ButtonGlowRoot` client helper wired in root layout

### Docs

- Foundations → Motion and Components → Button document `initButtonGlow()` usage
- Docs site delegates glow init to the package export (removed duplicate CSS in `docs.css`)

### Migration

Nothing breaking. Call `initButtonGlow()` once if you want the docs-style cursor glow; static hover glow on terminal still works without it.

## 4.6.15 — 2026-06-16 — PageHeader shimmer alias

`shimmer` prop alias for mid-sentence accent words in page titles.

### Added

- `PageHeader` — `shimmer` alias for `titleAccent` (same behavior: first matching substring in a string `title`)

### Migration

Nothing breaking. Prefer `shimmer="honest"` with `title="Keep beer prices honest."` for one highlighted word mid-sentence.

## 4.6.14 — 2026-06-16 — PageHeader shimmer accents

Selective display shimmer on page titles.

### Added

- `PageHeader` — `titleAccent` wraps the first matching substring in `.tollerud-display-shimmer` when `title` is a string
- `PageHeader` — `titleShimmer` renders an optional second title line with display secondary + shimmer styles
- `PageHeaderShimmer` — inline shimmer span for manual `title` composition

### Migration

Nothing breaking. `title` still accepts `ReactNode` for custom markup.

## 4.6.13 — 2026-06-16 — DataTable full-row hover

Row hover now applies to every cell, not only pinned columns.

### Fixed

- `DataTable` — opaque `group-hover/tr:bg-tollerud-noir-800` on all body cells so the entire row lightens on hover while pinned columns stay scroll-safe

### Migration

Nothing breaking.

## 4.6.12 — 2026-06-16 — DataTable mobile toolbar layout

Search stacks above filter and toolbar actions on narrow viewports.

### Fixed

- `DataTable` — toolbar uses a column layout on mobile: search on top, filter and `toolbarRight` stay paired on one row below (no wrap orphaning the action button)

### Migration

Nothing breaking. Drop-in layout fix for rich-mode tables with `searchable`, `filter`, and `toolbarRight`.

## 4.6.11 — 2026-06-16 — TopNav a11y, DataTable keys, Sheet title

Radix dialog warnings, DataTable React keys, pinned-column scroll bleed, and TopNav mobile menu polish.

### Fixed

- `TopNav` — mobile menu uses `DialogTitle` / `DialogDescription` with shipped `tollerud-sr-only`; `DialogTrigger` wraps the hamburger (no duplicate toggle state)
- `TopNav` — mobile menu scrim starts below the header (`tollerud-topnav-menu-overlay`) so the bar stays at full brightness when open
- `DataTable` — column list items use stable `column.key` + index React keys (no object-key warnings)
- `DataTable` — pinned columns use opaque row/hover backgrounds so horizontal scroll does not show bleed-through on hovered rows
- `Sheet` / `Drawer` — `SheetContent` injects a visually hidden `SheetTitle` when children omit one (Radix dialog a11y requirement)
- `tollerud-sr-only` utility in `globals-layers.css` — works without Tailwind `sr-only` in consumer apps
- Docs `CopyButton` — clipboard failures no longer log unhandled rejections

### Migration

Nothing breaking. `SheetContent` accepts optional `title` for the screen reader label when no visible `SheetTitle` is present (default: `Panel`). `TopNav` accepts optional `mobileMenuTitle` (default: `Navigation menu`).

## 4.6.10 — 2026-06-16 — TopNav overlay mobile menu

Mobile navigation now uses a modal overlay with backdrop, focus trap, and selective inline actions.

### Added

- `TopNavAction` — wrap `actions` children with `mobile?: 'inline' | 'menu' | 'hidden'` (default `menu`) to keep a primary CTA in the header bar while other actions collapse into the menu
- `TopNav` — mobile menu is a Radix Dialog overlay with scrim, Esc / backdrop dismiss, and body scroll lock; nav links and menu actions render in the panel below the bar

### Changed

- Unwrapped `actions` children now default to the mobile menu (previously stayed inline in the header). Wrap in `<TopNavAction mobile="inline">` to restore inline placement.

### Migration

Nothing breaking for the `actions` prop API. If you relied on all actions staying visible in the mobile header, wrap them in `TopNavAction mobile="inline"`.

## 4.6.9 — 2026-06-16 — TopNav responsive menu and max width

Top navigation now collapses links on narrow viewports and can align with Container width.

### Added

- `TopNav` — mobile menu toggle below `lg` when `navItems` are set; `maxWidth` prop (`default` | `wide` | `full` | `false`) constrains inner content to match `Container` / `MainContent` widths

### Migration

Nothing breaking. `maxWidth` defaults to `false` (full-bleed). Pass `maxWidth="default"` to cap at 1100px.

## 4.6.8 — 2026-06-16 — Sheet and Drawer slide animation

Slide-over panels now animate with shipped CSS keyframes instead of undefined Tailwind animate utilities.

### Fixed

- `Sheet` / `Drawer` — backdrop fade and panel slide in/out (250ms); `tollerud-sheet-*` classes in `globals-layers.css`
- Respects `prefers-reduced-motion: reduce` (instant open/close)

### Migration

Nothing breaking. Ensure consumer apps import `@tollerud/ui/globals.css` (and `source.css` for Tailwind v4).

## 4.6.7 — 2026-06-16 — DataTable height matches actual rows

Partial pages no longer pad with empty spacer rows below the last data row.

### Fixed

- `DataTable` — removed spacer rows that filled unused `pageSize` capacity; footer sits flush under the last row when fewer items than `pageSize` (e.g. 11 items with `pageSize={25}`)

### Migration

Nothing breaking. Table body height now reflects rendered row count.

## 4.6.6 — 2026-06-16 — Flip dropdowns upward near viewport edge

Select, Combobox, and DatePicker open above the trigger when there is not enough space below.

### Added

- `lib/dropdown-placement` — shared viewport-aware placement hook (`useDropdownPlacement`, `getDropdownPlacement`)

### Fixed

- `Select` — menu flips to `bottom-full` when the footer or bottom of the viewport is tight (DataTable rows selector)
- `Combobox` — listbox opens upward when needed; no longer closes on window resize
- `DatePicker` — calendar panel opens upward when needed; no longer closes on window resize
- `DropdownMenu` — `collisionPadding={8}` for Radix flip behavior at screen edges

### Migration

Nothing breaking. Placement is automatic.

## 4.6.5 — 2026-06-16 — Compact DataTable footer and js-yaml security fix

Tighter rows-per-page control in the table footer; dependency override clears Dependabot alert #10.

### Fixed

- `DataTable` — footer **Rows** selector uses inline `Select` (`layout="inline"`, `size="sm"`) so the footer stays one row tall
- `DataTable` — rows-per-page dropdown no longer clips behind the table (`overflow-hidden` removed from shell; footer stacking raised)
- `Select` — `layout` and `size` props for dense toolbar/footer use; menu `z-50`

### Security

- Override `@manypkg/get-packages` to `>=3.1.0` — drops vulnerable transitive `js-yaml@3.14.2` from `@changesets/cli` (CVE-2026-53550)

### Migration

Nothing breaking. `Select` defaults unchanged (`layout="stacked"`, `size="md"`).

## 4.6.3 — 2026-06-16 — DataTable rows-per-page selector

Users can change how many rows appear per page when `pageSizeOptions` is set.

### Added

- `DataTable` — `pageSizeOptions?: number[]` renders a footer **Rows** `Select`; changing size resets to page 1. Initial value from `pageSize` or the first option.

### Docs

- Data Table docs — pagination section covers fixed `pageSize` and `pageSizeOptions`; Servers canonical snippet updated
- `COMPONENTS.md` — fixed props table layout; documents `pageSizeOptions`

### Migration

Nothing breaking. Add `pageSizeOptions={[10, 25, 50]}` alongside `pageSize` to enable the selector.

## 4.6.2 — 2026-06-16 — Fix DataTable full width and document pagination

Tables in rich mode now stretch to the container width on desktop while keeping horizontal scroll when columns need more space.

### Fixed

- `DataTable` — `<table>` uses `w-full` instead of `w-max` so the table fills its container (e.g. inside a capped `Section`) instead of shrinking to content width

### Docs

- Data Table docs — new **Pagination** section (`pageSize`, internal page state, footer copy, cross-page selection)
- `COMPONENTS.md` and `SKILL.md` — pagination contract documented

### Migration

Nothing breaking. Drop-in width fix for tables inside max-width layouts.

## 4.6.1 — 2026-06-16 — Fuse DataTable bulk actions in ButtonGroup

Multiple `bulkActions` on `DataTable` now render as a fused `ButtonGroup` instead of separate spaced buttons.

### Changed

- `DataTable` — two or more bulk actions wrap in `ButtonGroup` (`size="sm"`); single action unchanged

### Migration

Nothing breaking. Bulk action bars look tighter with shared borders when multiple actions are defined.

## 4.6.0 — 2026-06-16 — Add ButtonGroup and first-class DataTable

New fused action button row and a production-ready data table with the full Servers example feature set built into `@tollerud/ui`.

### New components

- `ButtonGroup` — wraps `<Button>` children with shared borders, internal dividers, default `size`, and `orientation?: 'horizontal' | 'vertical'`

### DataTable

Rich mode ships search, segmented filter, row selection, bulk-action bar, sortable headers (`aria-sort`), row menus, pagination footer, loading skeletons, custom empty states, and a focusable horizontal scroll region with pinned anchor columns on narrow viewports (`pinColumns`, default on in rich mode).

- Column `header` is an alias for `label`
- `render` accepts `(row) => …` or `(value, row) => …`
- `striped` — alternating row backgrounds in rich mode
- `pinColumns` — pin first column and row ⋮ menu during horizontal scroll (default on in rich mode)
- `footer` — extra slot in the table footer bar
- `filter.variant` — `segmented` (default) or `combobox` for the rich-mode column filter
- Row hover in rich mode; bulk-action icons spaced correctly; `aria-sort` on sortable headers; focusable horizontal scroll region on mobile

### When to use

- `ButtonGroup` — adjacent actions (Deploy, Cancel, ⋯)
- `Segmented` — single selected option (sort mode, list/grid view)
- `DataTable` — config-driven tables with optional search, filters, selection, and pagination

### Migration

Nothing breaking. `header` and row-only `render` work on the npm component directly — the docs adapter shim is no longer required for column config.

## 4.5.2 — 2026-06-16 — Fix Button height for icon-only labels

Button sizes now use fixed heights so text and icon-only buttons align when placed in the same toolbar row.

### Fixed

- `Button` — `tollerud-btn--sm` / `--md` / `--lg` use fixed heights with centered flex layout; icon-only buttons no longer render shorter than labeled buttons
- CSS layer + token button sizes updated to match the React component

### Migration

Nothing breaking. Drop-in fix for mixed text/icon button rows.

## 4.5.1 — 2026-06-16 — Fix Segmented height for icon labels

Segment buttons now use a fixed height so text and icon labels align when multiple controls sit side by side.

### Fixed

- `Segmented` — `h-8` / `h-7` segment heights with centered flex layout; icon-only segments no longer render shorter than text segments

### Migration

Nothing breaking. Drop-in fix for mixed text/icon `Segmented` rows.

## 4.5.0 — 2026-06-16 — Grouped Combobox sections

`Combobox` now supports searchable dropdowns with section titles via an optional `groups` prop.

### Changed

- `Combobox` — optional `groups: { label, options }[]` for titled sections inside the list; flat `options` still works unchanged
- Exported `ComboboxGroup` type

### Migration

Nothing breaking. Existing flat `options` usage is unchanged. Pass `groups` when you need section headers in the dropdown.

## 4.4.1 — 2026-06-15 — Trademark notice and brand asset license

Clarifies that MIT applies to source code only. Tollerud trademarks, the monogram, avatars, and files under `brand/` remain proprietary.

### Added

- Trademark and brand asset notice appended to root `LICENSE`
- `brand/LICENSE` — terms for logo, monogram, and avatar assets
- `packages/footer/LICENSE` — MIT for footer code with pointer to full trademark terms
- README license section at the top (also visible on the npm package page)

### Changed

- Copyright holder on `LICENSE` — Mathias Tollerud
- `LICENSE` included in npm tarball `files` for `@tollerud/ui` and `@tollerud/footer`

## 4.4.0 — 2026-06-15 — Align DashboardShell with docs app shell

`DashboardShell` now defaults to the same sidebar-first layout used on the docs site: brand lockup in the left rail, structured sidebar navigation, and a context top bar instead of duplicating the lockup horizontally.

### New components

- `SidebarNav` — sidebar brand lockup with grouped nav links, icons, and active states
- `DashboardTopBar` — context top bar with breadcrumb, page title, mobile menu toggle, and actions

### Changed

- `DashboardShell` — default `variant="sidebar"` matches the docs shell; `variant="topnav"` keeps the previous horizontal TopNav layout
- New props: `sidebarGroups`, `sidebarItems`, `projectSubtitle`, `breadcrumb`, `pageTitle`
- `navItems` still works and maps into the sidebar when using the default variant
- `SettingsLayout` — `onNavSelect` for client-side section switching; `tone="danger"` on nav items
- Settings recipe uses package primitives; Settings example keeps its polished docs shell (`ds-settings`)
- Docs Screen patterns and Recipes demos updated to the aligned shell

### Migration

Existing apps using horizontal top navigation should pass `variant="topnav"` to preserve the previous layout. Apps that already pass `navItems` get sidebar navigation automatically with the new default.

## 4.3.0 — 2026-06-14 — Add screen patterns for component-first pages

Minor release: adds common page and section compositions so agents can build full Tollerud screens without recreating branded layout, navigation, form, list, detail, or empty-state structure with raw Tailwind.

### New components

- `PageHeader` — title block with eyebrow, description, metadata, and actions
- `TopNav` — branded monogram/project lockup with nav links and actions
- `DashboardShell` — app shell with top nav, optional sidebar, header, and main content
- `SettingsLayout` — settings page with section navigation and content panel
- `FormPanel` — titled form surface with body, action, and footer slots
- `ResourceList` — list/table page wrapper with header, filters, count, actions, and empty state
- `DetailPage` — detail page with header, primary content, and optional aside
- `EmptyPage` — full-page empty state on a Tollerud shell
- `FeatureSection` — feature grid section built from `PageHeader`, `CardGrid`, and `FeatureCard`
- `StatsSection` — metric section built from `PageHeader`, `Grid`, and `StatCard`

### Changed

- Docs app adds a Screen patterns page and search/deep links for the new exports.
- Docs app adds a **Recipes** page (`/recipes/`) with component-first copy-paste screen compositions for agents; each recipe links to an existing interactive example where one exists.
- Ships `tollerud-ui-audit` (`npx tollerud-ui-audit`) — lightweight consumer styling drift checker (missing `source.css`, copied `components/ui`, hardcoded brand hex, Button/Link nesting). Documented with full error-code reference, `--warn-only` flag, and alternative script invocation in `GETTING_STARTED.md`, `README.md`, `COMPONENTS.md`, and docs Guides.
- Docs and `GETTING_STARTED.md` add a consumer project checklist, anti-pattern table, and semantic feature-component example.
- `examples/next-starter` and `fixtures/consumer` use layout primitives (`PageShell`, `Section`, `Stack`, `PageHeader`, `CardGrid`) as the component-first reference implementation.
- `layout-patterns.test.ts` smoke-tests all layout and screen-pattern exports from the package barrel.
- Component demos and roadmap metadata now reference the actual screen-pattern APIs.
- Removed obsolete low-level `.tollerud-glass` and `.tollerud-section` utilities now covered by `TopNav` and `Section`.

### Migration

Replace `.tollerud-glass` nav usage with `TopNav`, and `.tollerud-section` wrappers with `Section`. No public projects are using these utilities yet.

## 4.2.0 — 2026-06-14 — Add layout primitives for component-first consumer apps

Minor release: adds semantic layout primitives so consumer apps and agents can build Tollerud-shaped pages without recreating branded structure with raw Tailwind utilities.

### New components

- `PageShell` — full-page shell with noir, grid, or glow background options
- `Section` — semantic page section with consistent spacing and width presets
- `Stack` — vertical layout primitive with finite gap and alignment options
- `Cluster` — wrapping horizontal layout for actions, badges, and toolbars
- `Grid` — responsive grid primitive with constrained column presets
- `CardGrid` — card collection grid with Tollerud spacing defaults
- `Split` — responsive two-column content/aside layout
- `MainContent` — main content wrapper with width, spacing, and density presets

### Changed

- Docs app adds a dedicated Layout page and deep links for each new primitive.
- `SKILL.md`, `AGENTS.md`, `GETTING_STARTED.md`, `README.md`, `COMPONENTS.md`, and `BACKGROUNDS.md` now reinforce component-first consumer styling.

### Migration

Nothing breaking. Existing Tailwind glue still works; prefer these primitives for repeated branded page structure.

## 4.1.1 — 2026-06-10 — Fix: missing `@theme` registration broke all `tollerud-*` color utilities

Critical fix. `globals.css` imported `tokens.css` (plain `--tollerud-*` CSS custom properties) but never registered them with Tailwind v4 via `@theme`. Tailwind v4 only generates color utilities for colors declared as `--color-*` theme variables, so every `bg-tollerud-*`, `text-tollerud-*`, and `border-tollerud-*` class used across the 52 component dist files resolved to nothing — breaking the entire visual identity (yellow accents, noir surfaces, borders, state colors) in any consumer app.

### Fix

Added a `@theme` block to `globals.css` mapping the full `tollerud-*` palette (brand yellows, noir scale, surfaces, text, borders, state colors) to `--color-tollerud-*` theme variables, referencing the existing `--tollerud-*` tokens. No change to token values — `bg-tollerud-yellow`, `text-tollerud-noir-400`, etc. now generate correctly with no extra config.

No API changes. Consumers should pick this up automatically on `npm update @tollerud/ui` — no code changes needed.

## 4.1.0 — 2026-06-11 — Ship Spinner, Drawer, EmptyState, and useToast

Minor release: four docs-site-only components move into `@tollerud/ui` with matching CSS in `globals-layers.css`.

### New components

- `Spinner` — inline loading indicator with reduced-motion support
- `Drawer` — controlled slide-over API (`open`, `onClose`, `footer`) built on `Sheet`
- `EmptyState` — prop-driven empty state with built-in Lucide icon names
- `ToastProvider` / `useToast` — context-based toast stack (alternative to Sonner `Toaster`)

### Changed

- Docs app imports the four components from `@tollerud/ui` instead of local adapters
- `COMPONENTS.md`, `SKILL.md`, `AGENTS.md` — export catalog and usage docs updated
- `registry.json` — four new entries; subpath exports: `@tollerud/ui/spinner`, `/drawer`, `/empty-state`, `/toast`

### Migration

Nothing breaking. `Empty` compound component and Sonner `Toaster` remain available.

---

## 4.0.5 — 2026-06-10 — Starter template and DX docs

Patch release: human-facing Next.js starter, migration guide, and footer package tooling alignment. No breaking API changes.

### Added

- `examples/next-starter/` — copy-paste Next.js 16 + Tailwind v4 reference app (`source.css`, `Toaster`, sample page)
- `GETTING_STARTED.md` — “Migrating from copied components” section (grep recipe, prop drift checklist, link to `SKILL.md`)

### Changed

- `@tollerud/footer` — TypeScript 6.x in devDependencies; `sync-footer-package.mjs` preserves `tsup.config.ts` build and `publishConfig`
- `GETTING_STARTED.md` / `README.md` — footer self-contained dependency model documented; starter template linked
- `packages/footer/tsconfig.json` — `ignoreDeprecations: "6.0"` for DTS emit under TS 6

### Migration

Nothing breaking. New apps can copy `examples/next-starter/` instead of wiring from scratch.

---

## 4.0.4 — 2026-06-10 — Export verification and source.css

Patch release: verifies all subpath exports in CI, adds package-owned Tailwind scanning, and expands install docs.

### Added

- `@tollerud/ui/source.css` — package-owned `@source` for `dist` scanning (npm, pnpm, workspaces, Bun)
- `test:subpath` now checks all 70 manifest entries (`dist/{name}.js` + `.d.ts`)
- `test:package` runs attw against every public subpath export

### Changed

- Recommended Tailwind v4 setup: `@import "@tollerud/ui/source.css"` after `globals.css`
- `GETTING_STARTED.md` — monorepo `@source` path table, footer-only minimal install
- `tailwind.css` re-exports `source.css` for one-import convenience
- Docs site getting-started page updated for `source.css`

### Migration

Replace manual `@source "../node_modules/@tollerud/ui/dist"` with:

```css
@import "@tollerud/ui/globals.css";
@import "@tollerud/ui/source.css";
```

---

## 4.0.3 — 2026-06-09 — Publish pipeline hardening

Patch release: aligns npm publish with `validate`, fixes preset export shape, enables provenance via OIDC, and fixes `@tollerud/footer` CI build.

### Changed

- `prepublishOnly` and `publish-npm.yml` now run `verify:footer-sync` and `test:consumer` before publish
- `publish-npm.yml` auto-builds and publishes `@tollerud/footer` alongside `@tollerud/ui`
- `tollerud-preset.js` renamed to `tollerud-preset.cjs` — fixes publint CJS-in-ESM warning; import via `@tollerud/ui/preset`
- `@tollerud/ui/utils` subpath no longer ships `'use client'` — `cn` is safe to import from Server Components
- Removed `engines.node` from `package.json` (contributor Node/npm guidance stays in `CONTRIBUTING.md`)
- Publish uses npm Trusted Publishers (OIDC) with `--provenance` instead of `NPM_TOKEN`
- `@tollerud/footer` ships its own `tsup.config.ts` — stops inheriting root TS 6 `tsconfig.build.json` during DTS emit

### Docs

- `README.md`, `GETTING_STARTED.md`, `SKILL.md`, `AGENTS.md` — preset import path updated
- Added `NPM_PACKAGE_PLAN.md` — npm hardening audit and task list

### Migration

Nothing breaking. If you copied `tollerud-preset.js` locally, rename to `tollerud-preset.cjs` or switch to `import preset from '@tollerud/ui/preset'`.

---

## 4.0.2 — 2026-06-10 — Repo layout and publish surface cleanup

Patch release: consolidates docs and CI fixtures, stops shipping internal manifests, and clarifies docs copy after the npm-only pivot.

### Changed

- Tarball smoke test moved to `fixtures/consumer/` (was `examples/consumer/`)
- Docs chrome consolidated under `docs-app/styles/docs.css` (removed top-level `docs/`)
- `registry.json` kept in the repo for `npm run test:drift` — no longer published in the npm tarball
- Docs copy: semantic tokens described without shadcn install-path framing

### Removed

- `components.json` — unused after copy-via-shadcn removal

---

## 4.0.1 — 2026-06-10 — npm-only install path

Patch release: drops copy-via-shadcn registry tooling. Install from the package — barrel or subpath imports.

### Removed

- `npm run test:registry-cli`, `npm run build:registry`, and `examples/registry-consumer/`
- `registry-dist/` build output (never shipped to npm in v4.0.0)
- Public shadcn `npx shadcn add` install docs — no consumers use copy-into-repo flow

### Docs

- Getting started leads with `npm install @tollerud/ui` and subpath imports (`@tollerud/ui/button`)
- `registry.json` remains for internal drift checks (`npm run test:drift`) only

### Migration

Use the package directly:

```tsx
import { Button } from '@tollerud/ui'
// or tree-shake:
import { Button } from '@tollerud/ui/button'
```

Do not copy component source via shadcn CLI — that path is unsupported.

---

## 4.0.0 — 2026-06-10 — Ecosystem hardening and globals-v4 removal

Major release: completes the post-v3 roadmap (light gallery, registry CLI, footer lockstep), reorganizes brand assets, and drops deprecated CSS entrypoints.

### Breaking changes

- Removed `@tollerud/ui/globals-v4.css` — it was an alias for `globals.css`. Tailwind v4 projects should import `@tollerud/ui/globals.css` only.
- Brand assets moved under `@tollerud/ui/brand/*` — e.g. `@tollerud/ui/brand/tollerud-logo.svg` (not package-root paths).

### Ecosystem

- `@tollerud/footer` — `packages/footer/` synced from `components/Footer.tsx` via `npm run sync:footer`; `npm run verify:footer-sync` in `validate`
- Changesets linked `@tollerud/ui` and `@tollerud/footer` for joint version bumps
- Registry drift checks via `registry.json` (`npm run test:drift`) — internal manifest, not a public shadcn install path

### Docs site

- Light-mode gallery parity — docs-only Tailwind preset maps `tollerud-*` utilities to CSS variables so npm previews flip in `data-theme="light"`
- Docs icons migrated to `lucide-react` (custom GitHub mark retained)
- Playwright coverage — forms page, command palette, theme toggle, light-mode card surfaces
- Brand assets canonical in `brand/`; synced to docs via `scripts/sync-brand-assets.mjs`
- Homepage and live docs URL → `https://design.tollerud.dev/`

### Tooling

- CI and dev tooling on Node 24 + npm 11.16.0 (`.nvmrc`, lockfile guardrails)
- Dependabot for `docs-app/` and `fixtures/consumer/` (moved from `examples/consumer/` in v4.0.2)
- Removed legacy `preview.html`, completed planning docs, and stale docs artifacts
- Consumer smoke test auto-syncs tarball version in `fixtures/consumer/package.json` (path at release: `examples/consumer/`)

### Migration

**globals-v4.css** — replace:

```css
@import "@tollerud/ui/globals-v4.css";
```

with:

```css
@import "@tollerud/ui/globals.css";
@source "../node_modules/@tollerud/ui/dist";
```

**Brand assets** — replace root imports:

```ts
import logo from '@tollerud/ui/tollerud-logo.svg'
```

with:

```ts
import logo from '@tollerud/ui/brand/tollerud-logo.svg'
```

No component API changes. `@tollerud/ui` barrel and subpath imports unchanged.

---

## 3.1.1 — 2026-06-09 — Display shimmer, form indicators, and button fixes

Patch release: ships hero text shimmer for consumer apps, fixes secondary/checkbox/radio styling, and polishes docs layout components.

### New utilities

- `.tollerud-display-shimmer` — animated yellow gradient clipped to text; respects `prefers-reduced-motion` (static `var(--primary)` fallback)

### Fixes

- `Button` — secondary variant restores raised surface and border (theme-aware CSS vars); all variants apply layer classes again
- `Checkbox` — checkmark visible on `defaultChecked` and click via `peer-checked` on the custom indicator
- `RadioGroup` — wires `value`, `onChange`, and `name` to children; inner dot shows when selected
- `CTABand` — title and description centered in the band
- `BentoDashboard` — section spacing and label alignment
- Docs `PageTOC` — restores `jumpToSection` import for in-page scroll

### Registry

- Top-level `name` → `Tollerud User Interface`; updated description and component metadata for blocks

### Docs

- Overview, Backgrounds, and Foundations Typography use `.tollerud-display-shimmer` (replaces docs-only `.ds-shimmer`)
- Light theme shimmer and secondary-button token overrides

### Migration

Drop-in. Replace any copied `.ds-shimmer` with `.tollerud-display-shimmer` from `@tollerud/ui/globals.css`.

---

## 3.1.0 — 2026-06-09 — Monogram component and docs fixes

Restores component styling in the docs site, ships the monogram as an npm component, and renames brand avatar assets.

### New components

- `Monogram` — inline SVG with `color`: `yellow` | `black` | `white`, optional `size` and `title`

### Fixes

- `Button` — `terminal` variant uses layer classes again
- `Pill`, `Avatar`, `Skeleton`, `Timeline`, `Switch`, `Slider`, `FormRow` — layer-class / prop adapter fixes
- `DatePicker` — calendar popover `z-50`
- `DataTable` — explicit `text-left` on column headers
- `CTABand` — inline accent bar margin
- `BentoDashboard` — real infra cards instead of placeholders
- `Footer` — uses `<Monogram color="yellow" />`
- `NoirGlowBackground` — `scale` and `offsetX` for edge-biased shader placement

### Brand assets

- `tia-full-figure.svg` renamed to `tollerud-avatar-full.svg` (plus PNG export)
- npm exports: `@tollerud/ui/tollerud-avatar-full.svg` and `@tollerud/ui/tollerud-avatar-full.png`

### Docs

- Docs-only brand layer: `Monogram`, `TiaPortrait`, `TollerudAvatarFull`, `NavLockup` under `@/components/brand`
- Tailwind `@source` fix, light-mode monogram via `currentColor`, onboarding/auth/foundations page updates

### Migration

Drop-in. Replace any copied `tia-full-figure` paths with `tollerud-avatar-full`. Use `<Monogram />` instead of inline SVG or `<img src={logo}>` where you need theme-aware fill.

---

## 3.0.0 — 2026-06-09 — ESM-only and rich DataTable

Ships the full table pattern in npm, drops CommonJS builds, and adds release/props tooling.

### Breaking change

- Package is **ESM-only** — `require('@tollerud/ui')` and `.cjs` subpath bundles are removed. Use `import` in apps and bundlers that support ES modules.

### New features

- `DataTable` — search, segmented filter, row selection, bulk actions, per-row menus, pagination, loading skeletons, and custom empty states (optional; simple sort/filter mode unchanged)
- `npm run docs:props` — generates `PROPS.generated.md` from component `*Props` interfaces
- `npm run test:props` — drift check in `validate` / `prepublishOnly`
- Changesets — `npm run changeset` and `npm run version:release` (runs `sync:registry`)

### Docs

- Retired docs-only `rich-datatable.jsx`; docs `DataTable` is an adapter over npm `DataTable`
- `PackageDataTable` remains the direct npm import alias on the components page

### Migration

Replace `require('@tollerud/ui')` with ESM imports. For rich tables, pass the new optional props on `DataTable` instead of copying docs-only table code.

---

## 2.0.0 — 2026-06-09 — Peer dependency model

Radix, Lucide, Framer Motion, and Sonner move to peer dependencies so consumer apps do not bundle duplicate copies.

### Breaking change

Install peers explicitly alongside `@tollerud/ui`:

```bash
npm install @tollerud/ui clsx tailwind-merge tailwindcss@4 \
  @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-progress \
  @radix-ui/react-slot @radix-ui/react-tabs @radix-ui/react-tooltip \
  lucide-react framer-motion sonner
```

### New features

- `@tollerud/ui/tailwind.css` — convenience import with documented `@source` hint
- `npm run test:package` — publint + `@arethetypeswrong/cli` on the package
- `npm run test:size` — size-limit budget on `dist/button.js` and `dist/index.js`
- `npm run sync:registry` — align `registry.json` version with `package.json` before publish

### Docs

- Retired docs-only `grain-gl.jsx`; backgrounds and overview use npm `NoirGlowBackground`
- Publish workflow runs drift, package quality, size budget, docs build, and Playwright E2E

### Migration

If you already had Radix/Lucide/Motion/Sonner in your app, add them to `package.json` if npm no longer hoists them from `@tollerud/ui`. No component API changes.

## 1.4.0 — 2026-06-09 — Charts and marketing blocks

Palette-aware charts and landing-page blocks ship in the npm package. Docs site reorganized into `pages/`, `kit/`, `blocks/`, and `backgrounds/`.

### New components

- `BarChart` — vertical bars with optional yellow accent series
- `AreaChart` — gradient area/line chart with grid lines
- `Donut` — donut chart with segment legend
- `Sparkline` — compact inline trend line
- `HeroBlock` — landing hero on noir glow (`intense` uses `NoirGlowBackground`)
- `FeatureCard` — icon chip + title + description
- `CTABand` — closing CTA with optional accent bar

### Docs

- Charts and marketing import from `@tollerud/ui` (no duplicate `charts.jsx` / `marketing.jsx`)
- `docs-app/components/` taxonomy: routable `pages/page-*.jsx`, `kit/`, `blocks/rich-datatable.jsx`, `backgrounds/grain-gl.jsx`

### Migration

Nothing breaking. Import charts and blocks from `@tollerud/ui` as named exports.

## 1.3.0 — 2026-06-09 — Tailwind v4 as default CSS entry

`@tollerud/ui/globals.css` is now the Tailwind v4 bundle (tokens + component layers + `@import "tailwindcss"`). v3 projects move to `@tollerud/ui/globals-v3.css`.

### Breaking change

If you were on Tailwind v3 and importing `@tollerud/ui/globals.css`, switch to `@tollerud/ui/globals-v3.css` and keep your v3 `tailwind.config.ts` preset setup.

### Migration (v4 — recommended)

```css
@import "@tollerud/ui/globals.css";
@source "../node_modules/@tollerud/ui/dist";
```

`@tollerud/ui/globals-v4.css` remains as an alias for `globals.css`.

### Docs

Install instructions, README, GETTING_STARTED, AGENTS.md, and SKILL.md now lead with Tailwind v4.

## 1.2.0 — 2026-06-09 — Subpath exports, Tailwind v4 CSS, Playwright E2E

Per-component subpath imports, a dedicated Tailwind v4 stylesheet, expanded unit tests, and docs-site E2E smoke tests.

### New features

- Subpath exports — `@tollerud/ui/button`, `@tollerud/ui/dialog`, `@tollerud/ui/utils`, and one entry per component (61 total)
- `@tollerud/ui/globals-v4.css` — single import for Tailwind v4 + tokens + component layers
- `@tollerud/ui/globals-layers.css` — shared component CSS layers (also imported by v3 `globals.css`)
- `npm run changelog:draft` — draft a CHANGELOG entry from commits since the latest version

### Tests & CI

- Vitest coverage for `Dialog`, `DataTable`, and `CommandMenu`
- Playwright E2E smoke tests for the docs site (`npm run test:e2e`)
- CI verifies subpath bundles and `globals-v4.css` in the npm tarball

### Migration

Nothing breaking. Existing `@tollerud/ui` barrel imports continue to work. For tree-shaking, switch to subpath imports. Tailwind v4 projects should prefer `@import "@tollerud/ui/globals-v4.css"`.

## 1.1.5 — 2026-06-09 — Fix Tailwind preset color namespace

### Bug fix

The Tailwind preset now exposes design-system colors under `tollerud.*`, matching the shipped component classes and documentation (`text-tollerud-yellow`, `bg-tollerud-noir-900`, `border-tollerud-border`, etc.).

Previously the preset exposed the same palette under `tia.*`, so consumer projects following the docs could miss generated `tollerud-*` utilities.

### Details

- Renamed the preset color namespace from `tia` to `tollerud`
- Renamed the default package shadow token from `shadow-tia` to `shadow-tollerud`
- Added missing documented/component color aliases: `tollerud.accent`, `tollerud.foreground`, `tollerud.black`, `tollerud.white`, `tollerud.noir-950`, and `tollerud.noir-850`

### Migration

Replace any `text-tia-*`, `bg-tia-*`, `border-tia-*`, or `shadow-tia` utilities with their `tollerud` equivalents.

## 1.1.4 — 2026-06-09 — Fix: Alert tone colors missing in Tailwind v4

### Bug fix

Alert `tone` prop colors (`danger`, `info`, `success`) were invisible in Tailwind v4 consumer projects when the `@source` path in `globals.css` pointed to the wrong `node_modules` location.

**Root cause:** Tailwind v4 resolves `@source` relative to the CSS file. When `globals.css` lives in `app/`, the path `../../node_modules/@tollerud/ui/dist/**` resolves to `app/node_modules/...` (which doesn't exist) instead of the root `node_modules`. The 9 tone utility classes were never scanned and therefore never generated.

**Fix:** Added an explicit `@layer utilities` block to `globals.css` that defines all 9 Alert tone classes unconditionally, bypassing scanning entirely. Classes are always emitted regardless of `@source` path configuration.

Classes added to safelist: `bg-red-500/5`, `bg-blue-500/5`, `bg-green-500/5`, `border-red-500/30`, `border-blue-500/30`, `border-green-500/30`, `text-red-400`, `text-blue-400`, `text-green-400`

No API changes.

## 1.1.3 — 2026-06-09 — Fix: registry deps, source 'use client', React 19 devdep, docs drift

No component API changes. Six quality fixes from a review audit:

**1. registry.json — missing runtime dependencies**
All icon-using components now list `lucide-react` in their registry entry; `button` lists `@radix-ui/react-slot`; `status-dot` lists `framer-motion`; `dialog` lists `lucide-react`. Affects manual/registry-copy installs only — the npm bundle was already correct.

Entries updated: `button`, `status-dot`, `accordion`, `breadcrumb`, `combobox`, `date-picker`, `dialog`, `file-upload`, `pagination`, `password-input`, `pricing-card`, `stepper`, `tag-input`

**2. Source components — added `'use client'` directive**
12 hook-using source files were missing the directive. The bundled package was protected by the tsup post-build injection, but copied source files (registry/manual flow) would fail in Next.js App Router.

Added `'use client'` to: `Accordion`, `Avatar`, `Checkbox`, `Combobox`, `DatePicker`, `FileUpload`, `FormRow`, `PasswordInput`, `RadioGroup`, `Slider`, `Switch`, `TagInput`

**3. package.json — aligned React 19 devDependencies**
`react-dom` dev dep bumped from `^18.3.1` → `^19.2.7` to match `react: ^19.2.7`, eliminating the `ELSPROBLEMS` peer conflict in local dev.

**4. docs Getting Started page — rewritten to npm-package-first**
Was: manual file-copy instructions, wrong token value (`--tollerud-yellow` = `#E8D500`), old component list (29 components).
Now: `npm install @tollerud/ui`, Tailwind v3 + v4 snippets, full 61-component import block, correct yellow token docs, RSC safety note.

**5. docs Brand page — corrected monogram color**
`#FFF200` → `#FFFF00` (two references: description text and inline style). This now matches `BRAND.md`, `SKILL.md`, and the package tokens.

**6. .gitignore — added `*.tsbuildinfo`**
`examples/docs-nextjs/tsconfig.json` has `"incremental": true`, generating a `.tsbuildinfo` file that was untracked. Suppressed globally.

## 1.1.2 — 2026-06-09 — Ship AGENTS.md + SKILL.md inside the npm package

`AGENTS.md` and `SKILL.md` are now included in the published package (`files` in `package.json`). After `npm install @tollerud/ui`, both files are available at:

- `node_modules/@tollerud/ui/AGENTS.md`
- `node_modules/@tollerud/ui/SKILL.md`

This lets Claude Code (and other agents) read them directly without needing a separate `curl` or a GitHub URL.

## 1.1.1 — 2026-06-09 — Docs: AGENTS.md package update + migration instructions

No component or API changes. Documentation only.

- `AGENTS.md` — added "Updating the npm package" checklist (component checklist, version bump rules, required file updates, build/push steps) and "Fixing copy/paste component patterns" guide (detection, migration, prop drift checks, common patterns table) for agents working in consumer projects

## 1.1.0 — 2026-06-09 — Fix: Combobox + DatePicker close on window resize

`Combobox` and `DatePicker` rendered their popover as `position: absolute` with no awareness of window resize — if the viewport changed while a popover was open it would stay in place, misaligned from its trigger. Both now close on `window resize`, consistent with the existing close-on-scroll behaviour.

`DropdownMenu` was unaffected (Radix handles this internally).

**Migration:** no API changes — behaviour only.

## 1.0.9 — 2026-06-08 — Ship the 19 components that only existed in the docs site

Closes the long-standing gap between the marketing/docs site and the installable
`@tollerud/ui` package — every component previously listed under "still missing"
in `COMPLETENESS_ROADMAP.md` now ships from `components/index.ts`:

- **New primitives:** `Divider`, `Pill`, `Avatar` / `AvatarGroup`, `Breadcrumb`, `Pagination`, `Segmented`, `Stepper`
- **New layout/display:** `Panel`, `Meter`, `FormRow`, `PricingCard`
- **New form controls:** `Accordion` (+ `AccordionItem`/`AccordionTrigger`/`AccordionContent`), `Slider`, `PasswordInput`, `Combobox`, `DatePicker`, `FileUpload`, `TagInput`

All built from scratch as accessible, theme-aware components following existing
conventions (`forwardRef`, `cn`, `tollerud-*` design tokens) — no new runtime
dependencies were added.

## 1.0.8 — 2026-06-08 — Fix: mark package as Client Components for RSC/SSR

**Fixes a breaking issue introduced in earlier versions:** importing *anything* from `@tollerud/ui` — even a plain helper like `buttonVariants` — into a Next.js Server Component crashed at build/runtime. The package is bundled into a single `dist/index.js`/`.cjs` file, and esbuild silently drops module-level `"use client"` directives during bundling, so the bundle was never marked as client code even though it's full of components using hooks (`useState`, `useEffect`, etc.).

- `dist/index.js` and `dist/index.cjs` now start with `'use client'` (injected via a post-build step in `tsup.config.ts`, since esbuild rejects it as a bundling banner) — this correctly tells Next.js's RSC bundler that the whole package is client code
- Added missing `'use client'` directives to `ActionDiff`, `AlertInbox`, `Select`, and `LogViewer` source files (they used hooks without declaring the boundary — harmless pre-bundling, but good hygiene and required if these are ever built unbundled)

**Migration:** just update to `1.0.8` — no code changes required. Server Components can now safely import from `@tollerud/ui` (you'll just be importing client-bundled code, which is fine for things like `buttonVariants` that are plain functions).

## 1.0.7 — 2026-06-08 — Button `asChild` + `buttonVariants`

- `Button` now supports an `asChild` prop (via `@radix-ui/react-slot`) — renders its single child element instead of a `<button>`, merging Button's classes/props onto it. Lets you style a `<Link>` (or any other element) as a button without invalid `<a>`-in-`<button>` nesting: `<Button asChild variant="primary"><Link href="/foo">Go</Link></Button>`
- Exported `buttonVariants({ variant, size, className })` — returns the Button class string directly, for cases where wrapping with `asChild` is awkward
- Exported `ButtonVariantProps` type
- Added `@radix-ui/react-slot` as a direct dependency

## 1.0.6 — 2026-06-08 — Fix brand color docs

- Fixed brand color swatches in `ds/page-foundations.jsx` — "Yellow" now correctly shows `#FFFF00` / `--tollerud-yellow`, "Yellow warm" shows `#E8D500` / `--tollerud-yellow-warm`
- Updated `BRAND.md` — monogram color corrected to `#FFFF00`

## 1.0.5 — 2026-06-08 — Yellow token rename + AGENTS.md

**Breaking token changes:**
- `--tollerud-yellow` is now `#FFFF00` (was `#E8D500`) — the brighter, high-voltage yellow is now the primary accent
- `--tollerud-yellow-bright` removed — replaced by `--tollerud-yellow-warm: #E8D500` for the warmer secondary yellow
- Tailwind: `tollerud.yellow` → `#FFFF00`, `tollerud.yellow-bright` → renamed to `tollerud.yellow-warm: #E8D500`
- All glow `rgba` values updated from `rgba(232,213,0,...)` to `rgba(255,255,0,...)`
- Semantic tokens `--primary`, `--ring`, `--chart-1`, `--border-accent` updated to `#FFFF00`

**Migration:** replace `tollerud-yellow-bright` → `tollerud-yellow`, and `tollerud-yellow` → `tollerud-yellow-warm` wherever you relied on the old warm `#E8D500` value.

**New files:**
- Added `AGENTS.md` — cross-tool AI agent guide (Claude Code, Cursor, Copilot, Codex)
- Added `.github/copilot-instructions.md` — GitHub Copilot native instructions

## 2026-05-26 — Form Primitives + Footer

- Added **Textarea** — multiline input with label/error support, same pattern as Input
- Added **Select** — styled native `<select>` with placeholder, label/error, custom chevron
- Added **Checkbox** — custom-styled checkbox with checkmark SVG, label, focus-visible ring
- Added **Switch** — toggle switch with role="switch", animated thumb, label
- Added **RadioGroup / Radio** — fieldset-based radio group with custom dot indicator, label/error
- Added **Footer** — ported from `@tollerud/footer` (v1.1.2), uses Tollerud UI design tokens, supports `accent` variant, responsive/row layouts, unstyled mode
- 6 new components → total **29 components** now

## 2026-05-26 — Phase 5: Docs App

- Created `examples/docs-nextjs/` — a full Geist-inspired documentation site:
  - Foundations: Color, Typography, Motion, Accessibility
  - Components: Catalog with all 23 components organized by category
  - Patterns: Dashboard and Approval Flow templates
  - Brand: Tia avatar, voice, and Tollerud glow guide
  - Changelog: Version history timeline
- Docs use the same Tollerud UI components for consistent preview
- Dark theme with sidebar navigation, responsive layout

## 2026-05-26 — Phase 4: shadcn Registry Compatibility

- Added `components.json` — shadcn UI registry format for tooling compatibility
- Added `registry.json` — component registry with all 23 components, dependencies, and metadata
- Added `GETTING_STARTED.md` — one-command install guide with Tailwind setup and component import examples
- Portable import paths documented: `@/components/ui` and `@/lib/utils`

## 2026-05-26 — Phase 3: Homelab Operational Components

11 new homelab-specific components for infrastructure management:

**Health & Monitoring**
- `ServiceHealthCard` — service status card with uptime, response time, version
- `HostCard` — server/VM card with CPU, RAM, disk, containers, IP
- `DockerStackCard` — Docker Compose stack overview with per-service health
- `IncidentCard` — severity-graded incident/alert card (critical→info)

**Actions & Approval**
- `ApprovalCard` — approve/reject card for pending operations
- `ActionDiff` — unified diff viewer with line numbers, add/remove/context
- `RollbackPlan` — ordered rollback steps with execution status

**Logs & Alerts**
- `LogViewer` — terminal-style scrollable log viewer with search, live follow, level coloring
- `AlertInbox` — alert feed with count badges, severity filter, acknowledge action

**Feed & History**
- `Timeline` — vertical timeline with status dots, connecting lines, metadata badges
- `BackupStatusPanel` — backup job overview with per-job status and schedule

## 2026-05-26 — Phase 2: Command-First Shell

- **Kbd** — Raycast-style keyboard shortcut chip (`⌘K`, `⌘⇧S`, etc.), 2 sizes.
- **ActionRow** — Command/action item row with icon, label, description, shortcut, keyboard navigation (`highlighted` prop).
- **CommandMenu** — Full command palette: search, groups, arrow key nav, `Enter`/`Esc`, auto-focus, body scroll lock, footer hints, custom filter support.
- **CSS classes**: `.tollerud-kbd`, `.tollerud-action-row`, `.tollerud-cmd`, `.tollerud-cmd-overlay`, `.tollerud-cmd__*` — in both `globals.css` and `tokens.css`.
- **KEYBOARD.md** — Keyboard contract document: global shortcuts, component contracts, accessibility requirements, implementation rules.

## 2026-05-26 — NoirGlowBackground

- Ported the real Tollerud.no background source from `MathiasOki/tollerud-landing`.
- Added `components/NoirGlowBackground.tsx` using `@paper-design/shaders-react` / `GrainGradient`.
- Added CSS fallback classes: `.tollerud-noir-glow-root`, `.tollerud-noir-glow-bg`, `.tollerud-noir-glow-vignette`, `.tollerud-noir-noise`.
- Added acid-yellow token `--tollerud-acid` / `tollerud-acid` for Tollerud voltage.
- Updated `preview.html` and the Next.js example hero to use the glow background.
- Added `BACKGROUNDS.md` documentation.

## 2026-05-25 — v1.0 Next.js Release

- **Tailwind preset** (`tollerud-preset.js`) — drop into any Next.js project
- **Globals.css** with shadcn-compatible semantic tokens (`--background`, `--primary`, `--ring`, etc.)
- **React components** — Button, Card, Badge, StatusDot, Input, CodeBlock, StatCard, Container
- **ACCESSIBILITY.md** — contrast ratios, focus rings, touch targets, reduced motion
- **COMPONENTS.md** — usage matrix for all component variants
- **VOICE.md** — copy guidelines, terminal-style CTAs, tone rules
- **CHANGELOG.md** — this file
- **Graphify-inspired additions**: grid backgrounds, glass nav, terminal CTAs, gradient accents, pills, tight display typography
- **Motion tokens**: duration, easing, reduced-motion support
- **Chart tokens**: accessible color palette for data viz

### What shipped

```
design-system/
├── package.json
├── README.md
├── CHANGELOG.md
├── ACCESSIBILITY.md
├── COMPONENTS.md
├── VOICE.md
├── COMPLETENESS_ROADMAP.md
├── tollerud-preset.js           # ← drop-in Tailwind preset
├── tailwind.config.js      # (backward compat)
├── tokens.css              # (backward compat)
├── globals.css             # ← full semantic tokens + components
├── preview.html
├── tollerud-avatar.svg
├── components/
│   ├── index.ts
│   ├── Button.tsx
│   ├── Card.tsx
│   ├── Badge.tsx
│   ├── StatusDot.tsx
│   ├── Input.tsx
│   ├── CodeBlock.tsx
│   ├── StatCard.tsx
│   └── Container.tsx
├── examples/
│   └── nextjs/
│       └── tailwind.config.ts
└── components.css
```
