@rfdtech/components 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +156 -54
  2. package/dist/components/app-layout/AppSidebar.d.ts +1 -1
  3. package/dist/components/app-layout/AppSidebar.d.ts.map +1 -1
  4. package/dist/components/bulk-import-modal/BulkImportModal.d.ts.map +1 -1
  5. package/dist/components/card/Card.d.ts.map +1 -1
  6. package/dist/components/combobox/Combobox.d.ts.map +1 -1
  7. package/dist/components/date-selector/DateSelector.d.ts.map +1 -1
  8. package/dist/components/launchpad/Launchpad.d.ts.map +1 -1
  9. package/dist/components/lottie/ClientLottie.d.ts +6 -0
  10. package/dist/components/lottie/ClientLottie.d.ts.map +1 -0
  11. package/dist/components/page-section/PageSection.d.ts +4 -0
  12. package/dist/components/page-section/PageSection.d.ts.map +1 -0
  13. package/dist/components/page-section/PageSection.test.d.ts +2 -0
  14. package/dist/components/page-section/PageSection.test.d.ts.map +1 -0
  15. package/dist/components/page-section/index.d.ts +2 -0
  16. package/dist/components/page-section/index.d.ts.map +1 -0
  17. package/dist/components/quick-actions/QuickActions.d.ts +4 -0
  18. package/dist/components/quick-actions/QuickActions.d.ts.map +1 -0
  19. package/dist/components/quick-actions/QuickActions.test.d.ts +2 -0
  20. package/dist/components/quick-actions/QuickActions.test.d.ts.map +1 -0
  21. package/dist/components/quick-actions/index.d.ts +2 -0
  22. package/dist/components/quick-actions/index.d.ts.map +1 -0
  23. package/dist/components/sidebar/Sidebar.d.ts.map +1 -1
  24. package/dist/components/table/ProgressModal.d.ts.map +1 -1
  25. package/dist/components/table/Table.d.ts.map +1 -1
  26. package/dist/generated/components.theme.d.ts +1 -0
  27. package/dist/generated/components.theme.d.ts.map +1 -1
  28. package/dist/index.cjs +28 -28
  29. package/dist/index.css +1 -1
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +6353 -6000
  33. package/dist/mcp/generated/components.json +187 -20
  34. package/dist/mcp/generated/examples.json +37 -1
  35. package/dist/mcp/generated/index.meta.json +212 -202
  36. package/dist/mcp/generated/rules.json +13 -4
  37. package/dist/mcp/generated/search.json +88 -2
  38. package/dist/mcp/generated/tokens.json +10 -4
  39. package/dist/mcp/installers/codex.js +2 -1
  40. package/dist/mcp/installers/opencode.js +6 -2
  41. package/dist/mcp/installers/setup.js +1 -1
  42. package/dist/mcp/installers/util.js +14 -1
  43. package/dist/next.cjs +89 -99
  44. package/dist/next.css +1 -1
  45. package/dist/next.js +16695 -24578
  46. package/dist/types/card.d.ts +2 -0
  47. package/dist/types/card.d.ts.map +1 -1
  48. package/dist/types/combobox.d.ts +3 -0
  49. package/dist/types/combobox.d.ts.map +1 -1
  50. package/dist/types/date-selector.d.ts +4 -0
  51. package/dist/types/date-selector.d.ts.map +1 -1
  52. package/dist/types/launchpad.d.ts +5 -2
  53. package/dist/types/launchpad.d.ts.map +1 -1
  54. package/dist/types/metric-card.d.ts +6 -2
  55. package/dist/types/metric-card.d.ts.map +1 -1
  56. package/dist/types/page-section.d.ts +9 -0
  57. package/dist/types/page-section.d.ts.map +1 -0
  58. package/dist/types/quick-actions.d.ts +35 -0
  59. package/dist/types/quick-actions.d.ts.map +1 -0
  60. package/dist/types/sidebar.d.ts +10 -0
  61. package/dist/types/sidebar.d.ts.map +1 -1
  62. package/dist/types/table.d.ts +2 -0
  63. package/dist/types/table.d.ts.map +1 -1
  64. package/package.json +1 -1
@@ -492,7 +492,7 @@
492
492
  ],
493
493
  "import": "@rfdtech/components"
494
494
  },
495
- "docs": "# Card\n\nSurface card wrapper for grouping content. Uses `--clet-surface-card` for background and `--clet-card-padding` for spacing — both overridable via CSS custom properties or `className`.\n\n## Example\n\n<ExampleTabs title=\"Card variants\" preview={<CardPreview />} code={cardSource} />\n\n## Card\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Card content |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## CardHeader\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Header content |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## CardTitle\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Title text or element |\n\nAlso accepts remaining `HTMLAttributes<HTMLHeadingElement>`.\n\n## CardActions\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Action items (buttons, links) |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## Design tokens\n\n| Token | Fallback | Used for |\n|-------|----------|----------|\n| `--clet-surface-card` | `--clet-bg` | Card background |\n| `--clet-card-padding` | `20px` | Inner padding (16px on mobile) |\n| `--clet-card-font` | `--clet-font-body` | Card font family |\n| `--clet-card-title-font` | `--clet-font-header` | `CardTitle` font family |\n\nOverride padding via `--clet-card-padding` or tailwind padding classes through `className`.\n\n## Notes\n\n- On mobile (`<= 768px`), padding reduces to 16px via `--clet-card-padding`.\n- Use `className` to override background, border-radius, shadow, or any root style.\n\n## Do\n\n- For scrollable card content, use `Card className=\"flex flex-col\"` with an inner `flex-1 overflow-y-auto min-h-0` region — `min-h-0` is required for the flex-overflow scroll to work.\n\n## Don't\n\n- Add explicit padding (`px-6`, `py-4`) inside Card content — Card has a built-in `--clet-card-padding` token.\n- Add `border-t` on card footers — keep the flow borderless.",
495
+ "docs": "# Card\n\nSurface card wrapper for grouping content. Uses `--clet-surface-card` for background and `--clet-card-padding` for spacing — both overridable via CSS custom properties or `className`.\n\n## Example\n\n<ExampleTabs title=\"Card variants\" preview={<CardPreview />} code={cardSource} />\n\n## Card\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Card content |\n| `bordered` | `boolean` | `false` | Adds a 1px solid border using the `--clet-border` token |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## CardHeader\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Header content |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## CardTitle\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Title text or element |\n\nAlso accepts remaining `HTMLAttributes<HTMLHeadingElement>`.\n\n## CardActions\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Action items (buttons, links) |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## Design tokens\n\n| Token | Fallback | Used for |\n|-------|----------|----------|\n| `--clet-surface-card` | `--clet-bg` | Card background |\n| `--clet-card-padding` | `20px` | Inner padding (16px on mobile) |\n| `--clet-card-font` | `--clet-font-body` | Card font family |\n| `--clet-card-title-font` | `--clet-font-header` | `CardTitle` font family |\n\nOverride padding via `--clet-card-padding` or tailwind padding classes through `className`.\n\n## Notes\n\n- On mobile (`<= 768px`), padding reduces to 16px via `--clet-card-padding`.\n- Use `className` to override background, border-radius, shadow, or any root style.\n\n## Do\n\n- For scrollable card content, use `Card className=\"flex flex-col\"` with an inner `flex-1 overflow-y-auto min-h-0` region — `min-h-0` is required for the flex-overflow scroll to work.\n\n## Don't\n\n- Add explicit padding (`px-6`, `py-4`) inside Card content — Card has a built-in `--clet-card-padding` token.\n- Add `border-t` on card footers — keep the flow borderless.",
496
496
  "headings": [
497
497
  "Card",
498
498
  "Example",
@@ -505,7 +505,7 @@
505
505
  "Do",
506
506
  "Don't"
507
507
  ],
508
- "types": "import type { HTMLAttributes, ReactNode } from \"react\";\n\nexport interface CardProps extends HTMLAttributes<HTMLDivElement> {\n children?: ReactNode;\n}\n\nexport interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {\n children?: ReactNode;\n}\n\nexport interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {\n children?: ReactNode;\n}\n\nexport interface CardActionsProps extends HTMLAttributes<HTMLDivElement> {\n children?: ReactNode;\n}\n",
508
+ "types": "import type { HTMLAttributes, ReactNode } from \"react\";\n\nexport interface CardProps extends HTMLAttributes<HTMLDivElement> {\n /** Adds a 1px border around the card, using the same border color as other bordered surfaces. */\n bordered?: boolean;\n children?: ReactNode;\n}\n\nexport interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {\n children?: ReactNode;\n}\n\nexport interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {\n children?: ReactNode;\n}\n\nexport interface CardActionsProps extends HTMLAttributes<HTMLDivElement> {\n children?: ReactNode;\n}\n",
509
509
  "examples": [
510
510
  {
511
511
  "file": "card.example.tsx",
@@ -546,10 +546,18 @@
546
546
  "migration-v2"
547
547
  ]
548
548
  },
549
- "docs": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\nUpgrading from `1.22.0`? See the [migration guide](/docs/migration-v2) for a step-by-step walkthrough of every breaking change below.\n\n## [Unreleased]\n\n### Added\n\n- **New auto-installed `image-to-components` skill** (alongside `rfdtech-ui`, via `rfdui setup`): given a screenshot, image, mockup, or textual description of a screen to build, decompose it and map every element to an existing `@rfdtech/components` component/variant via the MCP (`search_components`, `get_component_types`, `get_component_examples`, `get_rules`) before writing any markup. Composes pages exclusively from matched components; if an element has no match, it stops and asks the user rather than hand-rolling custom UI — custom UI is only built when explicitly requested. Installed to `.claude/skills/image-to-components/`, `.cursor/rules/image-to-components.mdc`, and `.ai/image-to-components.md` for Claude Code, Cursor, and other tools respectively.\n- **`Switch`**: new binary on/off toggle built on `@radix-ui/react-switch` (new peer dependency), matching the `Checkbox` authoring pattern (plain function component, `label`/`labelPosition` for a \"Switch + Text\" variant, `classNames` slot map for `root`/`track`/`thumb`/`label`). 44×24 track, checked state uses `--gsl-primary`, all colors/sizes tokenized (`--gsl-switch-*`).\n- **`Notice`**: new boxed, persistent callout (`variant`: `default`/`info`/`success`/`warning`/`error`, `title`, `icon`, `leftBorder`, `dashed`, `color` escape hatch for a custom accent). No new brand colors — variants reuse `--gsl-primary`/`--gsl-success`/`--gsl-warning`/`--gsl-error`, tinted via `color-mix()` like `Badge`. `role` defaults to `\"alert\"` for the `error` variant, `\"status\"` otherwise.\n- Docs: `role-select.mdx` now has a real runnable preview (was a static, non-interactive code block only) and its `title` prop default is corrected to `\"View as\"` (was incorrectly documented as `\"Role\"`). `dropdown.mdx` gained a preview for the `disabled` state and turned its `formatOption` custom-rendering sample into a runnable example instead of a static snippet. `popup.mdx`'s variants example now matches the actual design spec — title/description/body combinations × row/stack footer layouts (title+description with row or stack buttons, an optional input body, description-only, title-only with a single button) — replacing an earlier example that didn't reflect the real variant set. `upload-field.mdx` gained a `multiple` mode preview.\n- **`UploadField`**: new optional `fileStatuses` prop overlays a per-file `\"uploading\"` (progress bar + cancel), `\"completed\"` (green check + existing trash icon), or `\"failed\"` (red-bordered card + \"Try Again\") state, index-aligned with `value` — omit it entirely for the unchanged default attach/remove look. New `onCancel`/`onRetry` callbacks. The file-type icon logic is now also exported standalone as `FileFormatIcon`. Fixed the inline PDF icon's hardcoded `#dc2626` to `var(--gsl-error)`.\n- **`PhoneNumberInput`**: flags are now real SVG icons from the new `country-flag-icons` dependency (each rendered with `role=\"img\"` + a `\"{country} flag\"` aria-label), replacing the previous emoji flags — consistent rendering across OSes/browsers instead of relying on platform emoji support. `CountrySelector`'s emoji flags are unchanged (see its deprecation note below).\n- **`DateRangeSelector`**: new optional `presets` prop — renders a left rail of quick-select presets (`{ label, getRange }`) and a \"Range: ...\" footer summary. Selecting a preset previews it on the calendar (Apply is still required to commit, same as manual day selection); the matching preset is highlighted, and clears when a day is clicked manually afterward. Both the rail and summary are opt-in — omitting `presets` keeps the existing layout unchanged.\n- **`Combobox`**: new searchable option list built on `cmdk` (following the `Command` component's inline-Popover pattern), opened from its own trigger button rather than Radix `Select`. Supports single selection (closes on pick), multi-selection with per-row checkboxes and a \"Selected: N options\" trigger summary (stays open while toggling), a per-option leading icon adornment, and `clearable`. Search matches each option's `label` via cmdk `keywords` while `value` stays the stable identifier.\n- A global text-sizing scale: `--gsl-text-size-xs/sm/base/md/lg/xl` in `src/styles/theme/base.css`, mapped to Tailwind `text-xs…xl` utilities via `--text-*` in the `@theme` block. (Named `text-size`, not `font-size`, to avoid a `classify()` ordering collision in `scripts/generate-theme-tokens.mjs` where `font` matches before `size`.)\n- Closed several gaps in the Tailwind `@theme` mapping (`src/styles/theme/tokens.css`): `--color-primary-light`, `--color-brand-gold` (the actual gold `--gsl-secondary`, kept separate from the existing `--color-secondary` → `surface-dark` mapping), `--color-text-muted`, `--color-border-strong`/`--color-border-subtle`, `--color-destructive-bg`, `--color-panel`, `--radius-xl`/`--radius-2xl`. A coverage-checklist comment at the bottom of `tokens.css` documents every global token that's intentionally not mapped (z-index tokens, overlay/shimmer) and why.\n- **Launchpad**: new sibling to `AppSwitcher` — a constrained, low-config 9-app launcher (fixed 3×3 grid) with a built-in bordered \"See all\" button and a role switcher rendered via `children` (only a `RoleSelect` element is accepted) below the grid, no manual `footer` composition needed. \"See all\" isn't a callback — it always opens a borderless `--gsl-radius-2xl` modal (50px padding, top-right close) showing every app in `apps` uncapped, in a 5-column 120px-icon grid — a scaled-up version of the same tile system. Both the popover and the modal title are a fixed \"Launchpad\" heading (`--gsl-font-header`, 600 weight); the trigger is a fixed round 9-dot icon with a hover tooltip reading \"Open Launchpad\" (also its accessible name) — none of these have a label/override prop. Loading state is spinner-only, no text. Both the popover panel and the modal's scrollable grid fade at the top/bottom edge (`mask-image`) instead of hard-clipping, and every app name reserves a fixed 2-line height so grid rows stay aligned regardless of name length. Icons render on a fixed palette of 9 brand gradients and 9 decorative overlay shapes (5% opacity) via `SystemLaunchpadIcon`/`LaunchpadIconTile` — `LaunchpadApp.icon` only type-checks as one of those two components, so a raw image URL, emoji, or arbitrary node can't opt an app out of the shared tile design. 50×50 icons, `--gsl-radius-base` corners. Fully independent from `AppSwitcher` (separate types, CSS classes, hook) — adopting it doesn't touch any existing `AppSwitcher` usage. Also exported: `LaunchpadItem`, `LaunchpadGridIcon`, `useLaunchpad`.\n- **ExportButton**: new button that exports tabular `data` to CSV, Excel (`.xlsx`), or a printable PDF report. Accepts all `Button` props (renders `<Button>` as its trigger) plus `columns` (`header` + `accessor`), `title`, optional `filename`/`filtersDescription` (both folded into the generated filename), and `formats` to limit which options appear in the dropdown. `Button` now forwards its ref so it can be used as a Radix Popover trigger. The underlying `exportToCsv`/`exportToXlsx`/`exportToPdf`/`formatFilenameTimestamp` functions (plus the `ExportMeta` type) are now exported from the package root too, for building a custom trigger instead of using `ExportButton`'s styling.\n- The PDF export's \"Generated\" timestamp now renders as `date.toLocaleString(\"en-US\", { dateStyle: \"medium\", timeStyle: \"short\" })` (e.g. `\"Jul 13, 2026, 2:05 PM\"`) instead of a hand-rolled `YYYY-MM-DD HH:MM` string. The filename timestamp (`formatFilenameTimestamp`) is unchanged — still `YYYY-MM-DD_HHmm`, kept filesystem-safe on purpose.\n- **Dropdown**: `name` (plus `required`/`form`) prop — Dropdown now participates in native `<form>`/`FormData` submission on its own, backed by Radix's hidden bubble `<select>`. Inside `TableFilter`, pass `name=\"role\"` directly instead of pairing a hand-written `<input type=\"hidden\" name=\"role\" value={roleValue} />` alongside it.\n- **`TableContent`**: `onRowClick` prop — called with `(row, event)` on row click, in addition to (not instead of) selection toggling when `selectable` is set. Use it to wire up row-click behavior (e.g. opening a detail view) without hand-rolling an `onClick` per cell.\n- **`TableContent`**: `bulkActions` prop (requires `selectable`) — surfaces bulk actions inside the row-actions menu (kebab click or right-click) as a \"Bulk actions\" section: always a Select all/Deselect all toggle, with the rest of the list appearing once one or more rows are selected. New `bulkActionsFooter` prop also renders the `TableBulkActions` bar at the bottom automatically, wired to the same `bulkActions`/`selectedIds`, instead of placing `<TableBulkActions>` yourself as a sibling. New `TableContentClassNames.actionsSectionLabel` key for the section label.\n\n### Breaking\n\n- **`AppItem.icon` (AppSwitcher) is now a closed set: `<SystemAppIcon>` or `<AppIconTile>` only.** Was `ReactNode | string` — a raw image URL, emoji, or arbitrary node is no longer accepted, even with `as any` bypassing the type it won't render (icons are inserted directly, no more string/URL handling in `AppSwitcherItem`). Every app icon renders on the shared 9-gradient / 9-overlay tile system by construction; there's no per-app opt-out. Replace `icon: \"https://...\"` with `icon: <SystemAppIcon name=\"App Name\" />` (or `<AppIconTile name=\"...\">` for a custom glyph).\n- **`AppHeaderProfile` is removed — merged into `ProfilePopover`.** They were the same menu with two names and two prop surfaces; now there's one component. `ProfilePopover` gained `user`/`variant` props that build the same compact avatar + name/role + chevron trigger `AppHeaderProfile` used to, alongside its existing `fullName`/`email`/`profilePhoto` mode. Replace `<AppHeaderProfile user={...} variant=\"avatar\">` with `<ProfilePopover user={...} variant=\"avatar\">` — same `items`, `loading`/`loadingLabel`, `onSignOut`, and `children` props carry over unchanged. `onProfileClick`/`onSettingsClick`/`onHelpClick` are gone; pass an `items` array instead (nothing hardcodes \"My Profile\" / \"Account Settings\" / \"Help & Support\" anymore — nothing renders in that section unless you pass `items`).\n- **`headerAction` is removed.** The popover header always shows a built-in light/dark theme toggle when a `ThemeProvider` is present in the tree, and nothing when it isn't — this is no longer a prop you can pass, override, or opt out of.\n- **`TableContent`: the kebab actions column no longer renders when `selectable` is true but no `rowActions` are passed.** Previously that combination still rendered a kebab column containing only a Select/Deselect toggle; selection is already handled by the checkbox column, so the empty kebab column was removed. If you relied on that Select/Deselect entry, pass an explicit `rowActions` entry for it instead.\n\n### Deprecated\n\n- **`AppSwitcher` is deprecated and no longer maintained.** Use [`Launchpad`](/docs/launchpad) for new work. `AppSwitcher` stays exported and won't be removed without its own breaking-change entry, but it receives no further features or design updates — `Launchpad` doesn't yet cover every `AppSwitcher` capability (flexible column count, an uncapped/differently-capped grid, a fully custom `footer`), so migrate only once it does, or if you don't need those.\n- **`CountrySelector` is deprecated in favor of `Combobox` (general country selection) or `PhoneNumberInput`'s own built-in country picker (phone numbers).** It was already unused anywhere else in the library — a fully standalone implementation duplicating what `PhoneNumberInput` builds inline. `CountrySelector` stays exported and won't be removed without its own breaking-change entry, but it receives no further features or design updates. See the [migration guide](/docs/migration-v2) for the replacement pattern.\n- **`NetworkOperator` is deprecated in favor of `Combobox` (build an operator list with `options`).** It was already unused anywhere else in the library — a standalone dropdown duplicating what `Combobox` covers generically. `NetworkOperator` stays exported and won't be removed without its own breaking-change entry, but it receives no further features or design updates. See the [migration guide](/docs/migration-v2) for the replacement pattern.\n\n### Changed\n\n- **MCP (`get_component`/`get_tokens`/`get_rules`/etc.) now serves `--clet-*` exclusively for every color token** — docs prose, type source, example source, and rule bodies are rewritten at index-build time (`mcp/src/indexer.ts`) so an agent reading through the MCP never encounters the legacy `--gsl-*` spelling for a color token; non-color tokens (radius/shadow/font/etc.) are unaffected. This also fixed a real data bug: `get_tokens` was previously showing the *literal* declared value of `--gsl-<name>` for color tokens — since that's now `var(--clet-<name>))`, agents were seeing a self-referential string instead of the real color; the indexer now resolves and serves the actual `--clet-<name>` value. The human docs site (rendered directly from the same `.mdx` source, not through this index) is unaffected and still explains both names. Also synced a duplicate `classify()`-style helper in `mcp/src/indexer.ts` that had drifted from `scripts/generate-theme-tokens.mjs` (missing the `secondary` fix from the surrounding token work).\n- **Every color token now has a `--clet-*` alias alongside its `--gsl-*` name**, both resolving to the identical value (`--gsl-<name>` is now defined as `var(--clet-<name>)`, not a literal value — existing `--gsl-*` overrides keep working unchanged). Covers all 46 color-classified tokens, global and component-scoped (e.g. `--clet-primary`, `--clet-notice-accent`, `--clet-sidebar-bg`). `--clet-*` is the new preferred name in docs/examples going forward; non-color tokens (radius, shadow, font, spacing, z-index) are unaffected and only have a `--gsl-*` name. Also fixed a `classify()` gap in `scripts/generate-theme-tokens.mjs` where `--gsl-secondary` (the gold brand token) wasn't recognized as a color (the regex checked for `primary` but not `secondary`), so it was previously mistyped as a generic string in `gslTheme()` instead of a proper color value.\n- **Rebranded to \"Clet\"**: \"Ghana School of Law\" references in prose (package description, README, `getting-started.mdx`) now read \"Clet\". Docs site header, README title, AGENTS.md title, and the component-authoring cursor rule title also updated. The `@rfdtech/components` package name and the MCP server id (`gsl-components-docs`) are unchanged.\n- **Every `.gsl-*` CSS class, `--gsl-*` custom property, `data-gsl-theme` attribute, `.gsl-theme` root class, and the `gslTheme()` theming API is renamed to its `.clet-*`/`--clet-*`/`data-clet-theme`/`.clet-theme`/`cletTheme()` counterpart — but every old name keeps working, unchanged, with no migration required.** Confirmed by test (`npm test`), typecheck, `lint:css`, and `build` all passing after the rename, plus a direct render assertion that a component carries both classes and `ThemeProvider` sets both attributes on the same element. The compatibility mechanism, layer by layer:\n - **Classes**: every rendered element now carries both classes (e.g. `class=\"clet-sidebar gsl-sidebar\"`) — old `.gsl-*` selectors in your own CSS keep matching without any change.\n - **CSS custom properties**: every internal `var(--clet-<name>)` read is now `var(--gsl-<name>, var(--clet-<name>))` — overriding either `--gsl-<name>` or `--clet-<name>` (globally or per-component) works identically; `--gsl-<name>` wins if both are set.\n - **Theme attribute/root class**: `ThemeProvider` sets both `data-clet-theme` and `data-gsl-theme` on `<html>` and its wrapper `<div>`, and the wrapper carries both `clet-theme` and `gsl-theme` classes — `light.css`/`dark.css`/`base.css` selectors match either spelling.\n - **JS API**: `gslTheme()` is now an alias of `cletTheme()` (same function, same behavior). Legacy type names (`GslTheme`, `GslThemeConfig`, `GslComponentThemeConfig`, `GslComponentThemeOverrides`, `ResolvedGslTheme`, `GslColorValue`, `GslComponentTokenMap`, `GslGlobalTokens`, `GslLengthValue`, `GslOpacityValue`, `GslShadowValue`, `GslStringValue`, `GslZIndexValue`) are re-exported as aliases of their `Clet*` equivalents.\n - `clet-*` is the preferred spelling in new docs, examples, and internal source going forward; `gsl-*` is a permanent compatibility alias, not a deprecated one — it isn't scheduled for removal.\n- **Default brand color**: `--gsl-primary` (light theme) changed from `#051b2c` to `#083755`. `--gsl-focus` and every component that reads `--gsl-primary` pick up the new value automatically; dark-theme `--gsl-primary` (`#4a7fa8`) and the static Launchpad brand-gradient palette are unaffected.\n- **`--gsl-page-bg`** (light theme) changed from `#fff` to `#fafafa`. **`AppLayout`**'s `\"stacked\"` variant content area now explicitly reads `--gsl-page-bg` instead of `transparent` (so it reads flush with the page instead of showing whatever's behind it); **`MetricCard`**'s `\"outline\"` variant now uses `--gsl-page-bg` instead of `transparent` for the same reason — both previously relied on transparency inheriting an ambient background rather than an explicit token, which broke down whenever they weren't sitting directly on the page (e.g. an outline `MetricCard` inside `AppLayout`'s `\"default\"` panel would show through as the panel's gray, not the page's tone).\n- **`DateSelector` / `DateRangeSelector` / `Popup`**: hardcoded font sizes (`14px`/`13px`/`12px`) replaced with the new `--gsl-text-size-*` scale, and hardcoded trigger/calendar/popup dimensions (heights, padding, min-widths) promoted to component-scoped `--gsl-*` tokens — no visual change, but every size is now overridable via `gslTheme()`.\n- **AppSwitcher / AppIconTile**: tile icons are now 40×40 (was 32px) with no explicit grid gap (spacing comes from each item's own 8px padding), and generated tile skins (`SystemAppIcon`/`AppIconTile`) switched from a procedural HSL gradient + drawn pattern to a fixed palette of 9 brand gradients and 9 decorative overlay shapes, each independently hashed from `name` (same name always renders the same tile; gradient and overlay vary independently). The `footer` slot now lays out as a stretched flex column, and the panel scrolls (`max-height` + `overflow-y: auto`) — enables composing a bordered \"See all\" `<Button>` and a `<RoleSelect>` stacked below the grid.\n- **ProfilePopover**: removed the divider between `items` and `children` (e.g. a `RoleSelect`) — they now render flush together in one content block. Dividers still separate the header from that block, and that block from **Sign Out**.\n- `ProfilePopoverItem` and `AppUser` are now exported from the package root (previously `AppUser` lived under `AppHeaderProfileProps` and `ProfilePopoverItem` wasn't exported at all).\n- **`AppHeaderBranding`**: `title` now renders in the heading font at `20px` (new `--gsl-branding-title-font` token, `--gsl-branding-title-size` default raised from `14px`) instead of inheriting the block's font. Everything else in the branding block — the `subtitle` included — now defaults to the body font (`--gsl-branding-font` default changed from `--gsl-font-header` to `--gsl-font-body`); the old `\"plain\"`-variant-only body-font override was removed since it's now the default everywhere.\n\n### Fixed\n\n- **`Button`**: `variant=\"secondary\"` now actually has a transparent background (only a `--gsl-border` outline, filling with `--gsl-hover` on hover) — it had no dedicated base rule and was silently inheriting the root `.gsl-button` rule's opaque `var(--gsl-bg)` background, making it look filled instead of outlined like `ghost`/`destructive`.\n- **`Sidebar`**: the default (non-`\"plain\"`) desktop card panel now genuinely uses its own `--gsl-sidebar-bg` token — previously that token was referenced (with a fallback) in CSS but never actually declared anywhere, so it wasn't overridable via `gslTheme()` and didn't appear in `get_tokens(\"sidebar\")`, even though the docs already listed it. Visual default is unchanged (`var(--gsl-panel-bg, var(--gsl-surface-subtle))`), but it's now a real, themeable token.\n- **`UploadField`**: removing a file in `multiple` mode now removes only that file from the array; previously the remove button on any card cleared the entire selection back to `null`.\n- **TableContent**: right-clicking a row to open its `rowActions` menu now opens the menu at the exact cursor position instead of snapping it to the kebab column at the end of the row. Opening via the kebab button itself is unchanged — still anchored to, and right-aligned under, that button. Right-clicking the same row again while its menu is already open now closes it, instead of re-opening at the new cursor position.\n- **TableContent**: the empty state (no `data`, no custom `children` override) now keeps the column header row visible above the \"No results\" message, instead of replacing the whole table with just the empty-state div. Matches the loading skeleton, which already kept the header.\n- **TableFilter**: `variant=\"spread\"` fields (e.g. `Dropdown`) now actually lay out side by side in a row. Each field previously inherited `width: 100%` from its stacked/popover styling, which — as a flex-row child — made it claim the entire row and force every other field onto its own line. The fix's own field-width override no longer forces a size onto Radix's visually-hidden bubble `<select>` (rendered when a field uses `Dropdown`'s `name` prop).\n- **Table**: the header toolbar (`TableSearch`/`TableFilter`/`TableActions`), both `TableFilter` variants, `TablePagination`, and the bulk-actions bar now stack and wrap at `768px` and below instead of overflowing or clipping — matches the breakpoint `Sidebar` already uses for its mobile drawer.\n- **AppHeader**: the notification bell (`AppHeaderNotifications`) and any other button built on `.gsl-app-header__notif-btn` (e.g. a docs-link icon button in `AppHeaderActions`) now keep `flex-shrink: 0`, so they stay a fixed 36px circle instead of getting squeezed narrower than tall when the header runs out of room.\n\n## [2.0.0] - 2026-07-11\n\n### Breaking\n\n- **AppHeaderProfile**: `variant` no longer accepts `\"basic\"` — only `\"full\"` and `\"avatar\"` remain. `children` type narrowed from `ReactNode` to a single `RoleSelect` element, matching `ProfilePopover`'s `children` slot.\n- Default brand tokens changed: `--gsl-primary`/`--gsl-primary-light` moved from red (`#dc2626` / `#ef4444`) to navy (`#051b2c` / `#4a7fa8`), and a new `--gsl-secondary` (gold `#c8a24b`) token was added and is now the default `SidebarLink` active-link color. Apps that don't set their own `--gsl-*` overrides will see the new navy/gold branding instead of the old red.\n- Default typography changed: new `--gsl-font-header` (Roboto Serif) and `--gsl-font-body` (Lato) tokens, loaded via new `@fontsource/lato`/`@fontsource/roboto-serif` dependencies, now back nearly every component's heading/body text (each component defines its own `--gsl-<component>-font`/`-title-font`/`-value-font` variable defaulting to one of the two). Previously every component inherited the single system-font stack (`--gsl-font`). Apps that don't override these tokens will see the new typefaces everywhere; the CSS bundle also grows by the bundled font files.\n- **AppSwitcher**: new `maxItems` prop defaults to `6` — grids that previously rendered every app now silently truncate past the 6th unless `maxItems` is passed explicitly (e.g. `maxItems={apps.length}`).\n\n### Added\n\n- **Stepper**: horizontal step indicator for multi-step flows. `Stepper` holds the active step value and derives each `Step`'s `complete` / `active` / `upcoming` state. Each `Step` renders a numbered circle that flips to an animated check when done and a connector line that fills between completed steps; `StepLabel` supplies the step's text. Optional `clickable` mode turns steps into buttons firing `onValueChange`, with per-step `disabled`. All colors resolve from existing `--gsl-*` tokens through overridable internal variables (`--gsl-stepper-accent` defaults to `--gsl-primary`, plus `--gsl-stepper-marker-size`, `--gsl-stepper-track`, etc.), and animations respect `prefers-reduced-motion`. Also available as `Stepper.Step`, `Stepper.StepLabel` for compound-style imports.\n- Sidebar: `SidebarGroup` `collapsible` prop — the first `SidebarGroupLabel` child becomes a button toggle. The label keeps the same visual styling as the static `<p>` (same font, size, color, padding); clicking it fades the content open/closed while reserving the group's space in the layout. Supports uncontrolled (`defaultExpanded`) and controlled (`expanded` + `onExpandedChange`) state. Content has `inert` + `visibility: hidden` when folded, and the transition respects `prefers-reduced-motion`. New `SidebarGroupClassNames` keys: `groupToggle`, `groupContent`.\n- **ProfilePopover**: new user-menu popover — avatar/name/email trigger and header, `items` rows (`icon`, `label`, `onClick`, `danger`), `onSignOut`, `headerAction` slot (e.g. a theme toggle), a `loading` skeleton state with `loadingLabel`, and a `children` slot that only accepts a `RoleSelect`. `AppHeaderProfile` is now built on top of it.\n- **RoleSelect**: dropdown for switching between roles — confirms via a \"Confirm Role Switch\" dialog by default (skip with `noConfirm`), radio-style selected state, per-role `icon`/`disabled`. Composes into `ProfilePopover` via its `children` slot.\n- `gslTheme()` runtime theme API (`src/components/theme/gslTheme.ts`) — pass camelCase `--gsl-*` token overrides per light/dark mode, globally and/or per component; injects a `<style>` tag matched to the library's own selector specificity so overrides win by source order. Backed by a generated typed token surface (`src/generated/components.theme.ts`) built by the new `scripts/generate-theme-tokens.mjs` (`npm run generate:tokens`), which scans `--gsl-*` custom property definitions across `src/styles/theme` and every component's CSS.\n- MCP docs server + `rfdui` CLI (`mcp/`) — ships inside `@rfdtech/components`'s `dist/mcp` and exposes `list_components`, `search_components`, `get_component`, `get_component_examples`, `get_component_types`, `get_rules`, `search_rules`, `get_tokens`, and `search_docs` tools generated from the existing MDX docs, `src/types/*.ts`, and new `demo/docs/rules/*.md` design-rule files, so AI coding agents can look up real component usage instead of guessing.\n- MetricCard: `loading` prop — shimmering skeleton placeholders for icon/label/value/trend/description, with `loadingLabel` for the announced status text.\n- SidebarLink: `loading` prop — shimmering skeleton placeholder for icon/label, with `loadingLabel` for the announced status text.\n- TableFilter: `variant=\"spread\"` — removes the popover and lays filter fields out inline as a flex row with a consistent gap, with just a \"clear\" action (fields auto-apply as soon as their value changes — no Apply button). Same `onApply`/`onReset` and URL behavior as the default `\"popover\"` variant otherwise.\n- **AppHeaderNotificationItem**: new notification row component — unread dot (or a subtle read background), message `text`, and `time` string. Becomes keyboard-focusable (Enter/Space) only when `onClick` is passed. Replaces hand-rolled `.gsl-notif-popover__*` divs as the documented way to render `AppHeaderNotifications` children.\n- AppSwitcher: `maxItems` prop to cap the visible app grid, and a `children` slot rendered below the grid (e.g. a \"manage apps\" link).\n- Avatar: `background` and `backgroundVar` props to override the initials gradient with a solid color or CSS variable (falls back to the existing gradient).\n\n### Changed\n\n- AppHeaderProfile: rebuilt on top of `ProfilePopover` — adds `headerAction`, `loading`/`loadingLabel`, and `onProfileClick`/`onSettingsClick`/`onHelpClick`/`onSignOut` props with a default \"My Profile\" / \"Account Settings\" / \"Help & Support\" menu.\n\n## [1.22.0] - 2026-07-07\n\n### Added\n\n- **Timeline**: composable vertical timeline with `Timeline` + `TimelineItem` primitives. Four status variants (`complete`, `current`, `warning`, `error`), optional icon override in the dot, staggered connector draw and dot pop-in animations, and part-level `classNames`. Render any markup as the content column. Connector animations respect `prefers-reduced-motion`.\n- **RouterAdapter**: framework-agnostic router abstraction that replaces direct `react-router-dom` imports across 7 hooks and components. Library now ships two entry points:\n - `@rfdtech/components` — default, backward compatible (react-router-dom)\n - `@rfdtech/components/next` — Next.js App Router support via `<RouterAdapterProvider>`\n- New exports: `RouterAdapterProvider`, `setRouterAdapter`, `getRouterAdapter`, `useNextRouterAdapter`\n- RouterAdapter: SidebarLink, AppLayoutInner, table hooks (useTableFilter, useTablePagination, useTableState), TableSearch, TableFilter, and TablePagination no longer import react-router-dom directly — they consume the adapter from context, making them portable to other routers\n- TableContent: `emptyIcon` and `emptyText` props for a customizable empty state when `data` is empty; renders in both regular and virtualized paths\n- TableContent: default empty icon (Inbox) shown when no `emptyIcon` is provided\n- Table: `classNames` API added to TableContent, TableHeader, TableFooter, TableSearch, TableFilter, TableActions, and TablePagination for deep inner styling\n- Tabs: `pill` variant with connected pill-style triggers and underline indicator\n\n- Theme: Tailwind v4 `@theme` token integration — all `--gsl-*` design tokens are registered as Tailwind utility classes for consumers using Tailwind v4; dark mode selectors now support `.dark` and `.light` class-based toggling alongside `data-gsl-theme`\n- ProgressModal: non-dismissable processing overlay with Lottie animation and determinate progress bar\n- BulkImportModal: chunked processing for datasets over 1000 rows — file parsing, row mapping, and validation run in 1000-row batches with a progress overlay to prevent main-thread freezes\n- BulkImportModal: CSV files stream-parsed in chunks for smooth progress; XLSX files parse in one shot with chunked normalize afterward\n- BulkImportModal: incremental validation — editing a cell in step 4 only re-validates that row instead of the entire dataset\n- BulkImportModal: loading state replaces step content during chunked processing — step title, Lottie animation, progress bar, and row counter (e.g. \"5,000 / 50,000 rows parsed\")\n- BulkImportModal: tested against 1M row CSV files\n- New exports: `mapDataRows`, `normalizeRows`, `filterEmptyRows`, `isCsv`, `parseCsvText`, `validateBatch`\n\n### Changed\n\n- BulkImportModal: chunk sizes, max file size, and accepted extensions consolidated into shared `constants.ts` — all files now import from the single source of truth; each processing phase (parse, remap, initial validation, background validation) uses its own named constant\n- BulkImportModal: SelectHeaderRowStep now shows only the first 20 rows for header selection\n- BulkImportModal: ValidateDataStep overscan increased to 10 rows for smoother virtual scrolling\n- Deps: added `lottie-react` for Lottie animation rendering\n\n### Fixed\n\n- BulkImportModal: ValidateDataStep now correctly imports `BACKGROUND_VALIDATION_CHUNK_SIZE` — was `undefined` at runtime, silently falling back to `DEFAULT_CHUNK_SIZE`\n- Modal: confirm dialog overlay portaled to document.body — escapes transform containing block, covers full viewport\n- Modal: discard button uses DialogPrimitive.Close — no longer requires consumers to pass onOpenChange to ModalContent\n- Modal: consumer onInteractOutside and onEscapeKeyDown handlers no longer override internal preventClose confirm flow\n- Modal: added pointer-events: auto to confirm elements to prevent Radix body-level pointer-events: none from disabling them\n- Modal: removed stray debugger statement\n- Tooltip: rebuilt on Radix Popover primitive — content renders in portal, escaping overflow:hidden clipping from parent containers; Radix handles collision detection and viewport boundary awareness\n- BulkImportModal: loading state replaces step content inline instead of separate ProgressModal or portaled overlay\n\n### Removed\n\n- Modal: onOpenChange prop removed from ModalContentProps\n\n## [1.20.0] - 2026-06-28\n\n### Added\n\n- Table: `rowActions` prop — per-row kebab popover rendered internally by `TableContent`. Actions accept `variant` (`\"default\"` | `\"destructive\"`), `condition` filter per row, and `onClick` receives the row object. Popover opens on both kebab click and row right-click\n- Table: Select/Deselect as first item in the row actions popover when `selectable` is true\n- Table: `TableRowAction<T>` type for row-level popover actions\n\n### Changed\n\n- Table: selection is now fully controlled via `selectedIds` prop (renamed from `defaultSelectedIds`). Pass alongside `onSelectionChange` — both `TableContent` and `TableBulkActions` read from the same source\n- Table: `rowKey` now optional — falls back to row index when not provided\n- Table: checkbox column always rendered (collapsed to 0 width when empty, slides open on first selection)\n- Table: removed indeterminate visual from select-all checkbox; partial selection shows unchecked, clicking selects all\n\n### Fixed\n\n- Table: internal selection state never updated — `handleToggleRow`/`handleSelectAll` now properly call `setSelectedIds` via controlled `selectedIds` prop\n- Table: `handleSelectAll` checked type widened to accept Radix `boolean` (removed `\"indeterminate\"` case)\n- Table: `onClear` on `TableBulkActions` now properly resets internal selection via controlled `selectedIds` sync\n- Table: checkbox column animation — added `min-width` to transition for smooth entry\n\n### Removed\n\n- Table: `TableContentInnerProps` — merged into public `TableContentProps`\n- Table: internal selection `useState` — now fully controlled via `selectedIds` prop\n- Docs: removed hallucinated `rowActions`, `bulkActions`, `onRowClick`, `onRowContextMenu` props from TableContent\n- Docs: removed hallucinated right-click context menu section and `TableRowAction` type (previously listed but never existed)\n\n## [1.19.0] - 2026-06-26\n\n### Added\n\n- Table: right-click context menu on rows with built-in Select action and custom `rowActions` prop\n- Table: bulk action support in right-click popover via `bulkActions` prop (shown when multiple rows selected)\n- Table: hidden select column — checkbox column appears only after first row selection\n- Table: `onRowClick` for left-click, `onRowContextMenu` for custom right-click handling\n- Table: `TableRowAction` type for context menu actions\n- Dropdown: rebuilt on Radix Popover (fixes modal + dropdown interaction bug where clicking outside the dropdown inside a modal closed the modal)\n- Command: clear button (XCircle) in input bar when text is entered, matching TableSearch pattern\n\n### Changed\n\n- Table: hover/selected row states via CSS classes (`gsl-table__row--selected`, `gsl-table__row--context-open`)\n- Table: row height uses `var(--gsl-table-row-height, 44px)` token\n- Popover: border-radius uses `var(--gsl-radius-base)` instead of `var(--gsl-radius-xl)`\n\n### Fixed\n\n- Dropdown: clicking outside the dropdown popover inside a Modal no longer closes the Modal — switched from Radix Select to Radix Popover\n\n## [1.18.1] - 2026-06-22\n\n### Fixed\n\n- **AppLayout**: tests now wrap renders in `MemoryRouter` to fix `useSearchParams` is not defined error\n- **Command**: test queries use `document.querySelector` with cmdk `data-value` attribute instead of `value` attribute\n- **Lint**: removed unused imports across 15+ files; replaced `as any` casts with proper TypeScript types across all source files\n\n### Changed\n\n- **DateRangeSelector**: wrapped `range` computation in `useMemo` to prevent unstable reference\n- **Table**: wrapped `columns` and `data` in `useMemo` to avoid new‑array‑on‑every‑render\n- **Sortable**: wrapped `style` object in `useMemo`\n- **useTableState**: wrapped `sort` computation in `useMemo`\n- **useBulkImportFlow**: removed unnecessary dependencies from `canGoNext` dependency array\n\n## [1.18.0] - 2026-06-21\n\n### Breaking\n\n- **AppLayout**: Now wraps `SidebarProvider` and `BreadcrumbProvider` internally. Consumers no longer need to add these providers. Removed `AppBreadcrumb` slot component. Breadcrumbs are context driven, call `useBreadcrumbs()` from any page to set them.\n- **Card**: Removed `header` prop and `CardClassNames`. Use `CardHeader`, `CardTitle`, and `CardActions` sub-components instead.\n\n### Added\n\n- `BreadcrumbProvider`, `useBreadcrumbs`, `useBreadcrumbContext`, `BreadcrumbEntry` exports\n- `CardHeader`, `CardTitle`, `CardActions` sub-components\n- `useConfirmBeforeUnload` hook for browser `beforeunload` confirmation\n- BulkImportModal: browser confirmation prompt when a file has been uploaded but not confirmed\n- BulkImportModal: reset discarded rows button on validate data page\n\n### Changed\n\n- **ValidateDataStep**: replaced `<Table />` dependency with an internal virtualized table using `@tanstack/react-virtual`. Error counts correctly exclude discarded rows.\n- Minor CSS improvements across sidebar, button, modal, field, and table components\n\n## [1.17.0] - 2026-06-20\n\n### Added\n\n- ModalContent `onOpenChange` prop for direct close handler passing\n- BulkImportModal `defaultState` prop (`BulkImportFlowDefaultState`) for flow state preservation across open/close cycles\n- BulkImportModal flow state types exported\n- AppHeader: simplified wrapper with `as` prop for semantic flexibility\n- AppLayout: className and extra props passthrough on layout wrappers\n- SelectHeaderRowStep: responsive single-column header row selection with radio buttons on mobile\n- Table: `TableHeaderCell` with `sortable`, `sortDirection`, `onSort` and sort icon; sticky `TableHeader`\n\n### Changed\n\n- ModalContent: removed nested AlertDialog for close-confirmation (all alert() debug calls cleaned out)\n- UploadField: action button uses `<Button variant=\"primary\">` instead of raw `<button>`\n- BulkImportModal stepper: nav-based step indicator with clickable completed steps; footer/body use ModalFooter/ModalBody\n- UploadField: action button height aligned with Button component sizing (36px to 40px)\n\n### Fixed\n\n- ModalContent: no nested Radix Dialog (caused focus-trap / z-index conflicts)\n- BulkImportModal CSS lint: padding shorthand, rgba to rgb modern notation, ::last-child to :last-child\n- ValidateDataStep memoization\n- Modal test: close-button test updated\n\n## [1.16.1] - 2026-06-19\n\n### Added\n\n- SidebarLink `to` prop: renders as react-router `<Link>` for client-side navigation\n\n### Fixed\n\n- **Peer dependency externalization** — `react-router-dom` and `lucide-react` were bundled inside the library, causing `useLocation()` crash when consuming app ran its own `BrowserRouter`. Both are now externalized from the build and declared as peer dependencies, so the library and app share a single copy from the consumer's node_modules.\n- Tooltip arrow borders now face the correct direction per placement (top, bottom, left, right)\n\n### Changed\n\n- Moved `lucide-react` from `dependencies` to `peerDependencies` (consumer likely has it installed)\n\n## [1.16.0] - 2026-06-19\n\n### Added\n\n- Tooltip component with top/right/bottom/left positioning, arrow indicator, and pure-CSS hover reveal\n- SidebarLink tooltip on collapsed rail: shows label text on hover via Tooltip\n- Modal size variants (`sm`, `md`, `lg`, `xl`, `2xl`) with independently customizable `--gsl-modal-max-width-*` tokens\n- Modal `preventClose` prop: intercepts X, overlay click, and Escape; shows confirmation dialog via AlertDialog\n- Modal centered layout with popover-style border and shadow\n- AppBreadcrumb slot for breadcrumbs in AppLayout\n- Avatar component with initials/name display and configurable size\n- SidebarBadge now uses primary color tokens with overridable CSS variables\n\n### Changed\n\n- BreadcrumbLink: calls preventDefault on click for SPA safety, preserves consumer onClick\n- BulkImportModal exit confirm: uses Button component instead of raw elements (fixes Cancel dismissal)\n- Modal, Popover, App-switcher, Command dialog, Dropdown: unified border-radius to `--gsl-radius-xl` and box-shadow to `--gsl-shadow-md`\n- AppLayout: passes className and extra props through to all layout wrappers\n- AppHeader: simplified to passthrough container\n- AppSidebar/AppBody: simplified to passthrough, layout classes moved to AppLayout wrappers\n- Theme: adds --gsl-z-header token and body background\n\n### Fixed\n\n- Command inline popover: removed Portal wrapper so popover renders in-flow; adds pointer-events: none when closed to prevent blocking clicks\n\n## [1.15.2] - 2026-06-19\n\n### Added\n\n- ThemeProvider `storageKey` prop: persists theme to localStorage across sessions\n\n## [1.15.1] - 2026-06-19\n\n### Changed\n\n- Command: inline list now renders via Radix Popover portal, escaping all parent stacking contexts; z-index and positioning handled by Popover instead of absolute CSS\n\n### Fixed\n\n- Malformed CSS in table checkbox-cell and network-operator rules causing PostCSS \"Unknown word\" errors in consuming projects\n\n### Added\n\n- stylelint with `lint:css` / `lint:css:fix` scripts; wired into `prepublishOnly` to catch CSS syntax errors before publish\n\n## [1.14.0] - 2026-06-18\n\n### Changed\n\n- CommandDialog overlay now fades in/out with animation, matching Dialog and Modal component patterns\n\n## [1.13.2] - 2026-06-18\n\n### Changed\n\n- Toast: restyled with popover background, border, and shadow; action button uses Button secondary sm classes; close button is transparent ghost; title text matches semantic variant color; border-radius xl; glass background with backdrop blur\n\n## [1.13.1] - 2026-06-18\n\n### Added\n\n- `PhoneNumberInput` Zod validation form example with success/failure dialog\n- `UploadField` Zod validation form example with file type, min/max size, and name length checks\n\n### Changed\n\n- CommandDialog: border radius changed from xl to base; inline Command input uses xl\n- CommandDialog input wrapper: focus outline now follows border radius\n- UploadField Zod validation made stricter with minimum file size and filename length checks\n\n### Cleanups\n\n- Shortened all MDX page meta descriptions to 3--5 words\n\n### Fixes\n\n- UploadField Zod form example: dialog styling matches PhoneNumberInput form pattern\n\n## [1.13.0] - 2026-06-18\n\n### Added\n\n- `DateRangeSelector` year/month dropdown selectors for quick navigation\n- `DateRangeSelector` Apply/Cancel confirmation buttons (pending range committed on Apply)\n- `DateRangeSelector` side-by-side two-month calendar layout\n- `DateRangeSelectorClassNames` keys: `calendarFooter`, `applyButton`, `cancelButton`\n\n### Changed\n\n- `DateRangeSelector` uses project `Dropdown` and `Button` components\n- `--gsl-z-select` bumped from 1200 to 1400\n\n## [1.12.0] - 2026-06-18\n\n### Fixed\n\n- TypeScript strict errors in `CodeFigure`, `PopoverExample`, `SidebarExample`, `SortableExample`, and `DateRangeSelector.test`\n- Export `DateRangeValue` type from `DateRangeSelector` component module\n- `onReorder` callback type compatibility in `Sortable` example\n- Union type destructuring for optional `destructive` and `badge` props in docs examples\n\n### Changed\n\n- Replace native inputs with internal `Input` component across `Table`, `ValidateDataStep`, `ThemeToggle`\n- Rename `--gsl-rounded-base` token back to `--gsl-radius-base`\n- Date pickers use `--gsl-z-dropdown` token instead of dedicated `--gsl-z-datepicker`\n- Refactor `DateRangeSelector` and `UploadField` design\n- Add uncontrolled/RHF tests for all input components\n\n## [1.11.0] - 2026-06-18\n\n### Added\n\n- UploadField docs: Zod file validation section with `z.instanceof(File)`, `.refine()` for type/size, and multi-file array example\n\n## [1.10.0] - 2026-06-18\n\n## [1.9.0] - 2026-06-17\n\n### Added\n\n- `AppHeader` compound primitives (`AppHeader`, `AppHeaderActions`, `AppHeaderSearch`, `AppHeaderNotifications`, `AppHeaderProfile`)\n- `AppLayout`, `AppSidebar`, `AppBody` layout container with auto-positioning by `componentId`\n- `Table` compound primitives (`Table`, `TableHeader`, `TableSearch`, `TableFilter`, `TableContent`, `TableFooter`, `TablePagination`) with URL-driven state; `paramPrefix` required; pagination shows \"Showing {'{'}start{'}'} to {'{'}select{'}'} of {'{'}total{'}'}\"\n- `CountrySelector`, `DateSelector`, `DateRangeSelector`, `MetricCard`, `NetworkOperator`, `OtpInput`, `PhoneNumberInput`, `UploadField` input components\n- `Card` surface wrapper (`header`, `body`, `--gsl-surface-card`, `--gsl-card-padding`)\n- `SidebarBrand` sub-component for collapsed-aware brand area\n- `useTablePagination`, `useTableState`, `useTableFilter`, `useDebounce` hooks\n- `DocsLayout` for documentation pages\n- `countries` utility: country list with name, flag emoji, dial code, ISO code\n- CSS tokens `--gsl-surface-dark`, `--gsl-surface-card`, `--gsl-rounded-base`\n- Documentation pages for all new components with interactive examples\n\n### Changed\n\n- Sidebar uses `--gsl-radius-2xl` and `--gsl-surface-dark` for active states; scrollbar hidden; scroll hint internalized\n- CommandGroup loading replaced with skeleton rows\n- `--gsl-radius` renamed to `--gsl-rounded-base`\n- Added `invalid` prop and `aria-invalid` to `Dropdown`\n- `DemoLayout` and demo pages refactored for new components\n- `DateRangeSelector` refactored to single-trigger display with two-click range selection; start always kept before end (auto-swap); self-contained CSS; simplified `placeholder` prop from `{'{'} start, end {'}'}` object to `string`\n\n## [1.8.0] - 2026-06-15\n\n### Added\n\n- `Field` compound primitives (`Field`, `FieldLabel`, `FieldDescription`, `FieldError`, `FieldControl`) for accessible label, helper, and error wiring\n- `Input` and `Textarea` forwardRef controls styled for GSL forms\n- `Form`, `FormField`, and `useFormField` adapters for optional `react-hook-form` integration (peer dependency)\n- Form documentation page at `/docs/form` for `Field`, `Input`, and `Textarea` primitives\n- FormField documentation page at `/docs/form-field` for `Form`, `FormField`, `useFormField`, and Zod validation\n- `zod` and `@hookform/resolvers` documented as optional peer dependencies for schema validation\n- `ToastOptions.icon` for optional leading icons with variant-tinted styling\n- `Draggable` compound primitives (`Draggable`, `DraggableHandle`) and `useDraggable` hook for repositioning panels within parent, window, or custom bounds\n- Draggable documentation page at `/docs/draggable` with props and exported types\n- `Sortable` compound primitives (`Sortable`, `SortableList`, `SortableItem`, `SortableHandle`) and `reorderItems` helper for list reordering via `@dnd-kit`\n- Sortable documentation page at `/docs/sortable` with props and exported types\n- `Toast` primitives (`ToastProvider`, `Toaster`) and `useToast` hook for imperative notifications\n- Toast documentation page at `/docs/toast` with props and exported types\n\n### Changed\n\n- Form docs split into `/docs/form` (Field, Input, Textarea) and `/docs/form-field` (Form, FormField, useFormField, Zod)\n- Dialog, Modal, BulkImportModal, and CommandDialog doc examples and README snippets now use `useDialogSearchParam` / `useModalSearchParam` for URL-driven open state\n- Toast rebuilt on [Sonner](https://sonner.emilkowal.ski/); `ToastProvider`, `Toaster`, and `useToast()` remain the public API; internal queue and styling now delegate to Sonner with GSL `unstyled` class names\n- `useToast().toasts` is deprecated and always returns `[]` (Sonner does not expose its queue)\n- Docs code examples use increased spacing between fenced blocks and inside highlighted code panels\n\n### Removed\n\n- Toast compound Radix parts: `Toast`, `ToastTitle`, `ToastDescription`, `ToastAction`, `ToastClose`, `ToastViewport`, `ToastIcon`\n- Toast reducer utilities: `toastReducer`, `createToastRecord`, `enforceToastLimit`, `TOAST_REMOVE_DELAY`, `createToastId`\n- `ToastProvider` props `swipeDirection` and `label` (not supported by Sonner)\n\n## [1.7.0] - 2026-06-15\n\n### Added\n\n- `Sidebar` compound primitives (`SidebarProvider`, `Sidebar`, `SidebarTrigger`, `SidebarOverlay`, nav parts) with mobile offcanvas drawer\n- `SidebarCollapse` desktop collapse toggle and `SidebarLink` `icon` prop for icon + label rows\n- `SidebarBadge` trailing pill for link counts and labels\n- `Command` compound primitives (`Command`, `CommandDialog`, input/list/item parts) built on cmdk\n- `CommandDialog` `shortcut` prop and `useCommandShortcut` hook for Cmd/Ctrl+K palette toggle\n- `CommandShortcut` trailing key hint chip for command items\n- `CommandInput` search icon and optional shortcut badge (inherits from `CommandDialog` when `shortcut` is set)\n- `CommandGroup` `loading` and `loadingLabel` props for per-group async result loading\n- `formatCommandShortcutLabels` helper and `useCommandDialog` hook\n- `Dialog` compound primitives (`Dialog`, `DialogTrigger`, `DialogPortal`, `DialogOverlay`, `DialogContent`, `DialogTitle`, `DialogDescription`, `DialogClose`) built on Radix Dialog\n- `Modal` compound primitives (`Modal`, `ModalTrigger`, `ModalPortal`, `ModalOverlay`, `ModalContent`, `ModalHeader`, `ModalTitle`, `ModalDescription`, `ModalBody`, `ModalFooter`, `ModalClose`) for near full-viewport shell layouts\n- `Sheet` compound primitives (`Sheet`, `SheetTrigger`, `SheetPortal`, `SheetOverlay`, `SheetContent`, `SheetHeader`, `SheetTitle`, `SheetDescription`, `SheetBody`, `SheetFooter`, `SheetClose`) for edge-sliding panels with `side` (`top`, `right`, `bottom`, `left`)\n- `Badge` component with semantic variants (`default`, `primary`, `success`, `warning`, `error`, `outline`) and `sm` / `md` sizes\n- `Breadcrumb` compound primitives (`Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis`) for hierarchical navigation trails\n- `ProgressBar` component with semantic variants (`default`, `success`, `warning`, `error`), `sm` / `md` sizes, and determinate / indeterminate modes\n- Hooks module: `useSearchParamOverlay`, `useDialogSearchParam`, and `useModalSearchParam` for URL search-param overlay state with flat prefixed data params (`dialog.userId`, etc.), `data`, `openWith`, and `SearchParamOverlayState`; includes `createSearchParamAdapter`, `createBrowserSearchParamAdapter`, and `readOverlayData` / `writeOverlayData` / `clearOverlayData` utilities\n- Dialog documentation page at `/docs/dialog` with props and exported types\n- Modal documentation page at `/docs/modal` with props and exported types\n- Sheet documentation page at `/docs/sheet` with props and exported types\n- Badge documentation page at `/docs/badge` with props and exported types\n- Breadcrumb documentation page at `/docs/breadcrumb` with props and exported types\n- ProgressBar documentation page at `/docs/progress-bar` with props and exported types\n- Hooks documentation page at `/docs/hooks` with URL overlay examples and adapter patterns\n- Sidebar documentation page at `/docs/sidebar` with props and exported types\n- Command documentation page at `/docs/command` with props and exported types\n\n### Changed\n\n- Sidebar desktop appearance: card panel with neutral active state and collapsible icon rail\n- Command inline results render in a floating popover so the search field height stays fixed\n- Command shortcut badges use a single unified chip (e.g. `⌘` `K`) instead of separate key caps per key\n- Command dialog input shows the keyboard shortcut badge automatically when `shortcut` is set\n- Component authoring rule requires full responsive behavior (320px to desktop, touch targets, reduced motion, docs preview checks)\n\n## [1.5.0] - 2026-06-15\n\n### Added\n\n- `RadioGroup` and `Radio` components with optional labels and part-level `classNames`\n- RadioGroup documentation page at `/docs/radio-group` with props and exported types\n- `Radio.description` for optional secondary text under each option label\n- `RadioGroup.variant=\"card\"` choice card layout with bordered selectable surfaces\n- `Tabs` compound primitives (`Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`) with `default` and `line` variants\n- Tabs documentation page at `/docs/tabs` with props and exported types\n\n### Changed\n\n- Tabs panels animate in with a fade and slide on tab switch; disabled when `prefers-reduced-motion: reduce` is set\n- Line variant tabs use a sliding underline indicator instead of per-trigger borders\n- Docs nav, README component sections, and `src/index.ts` exports list components alphabetically\n- Component authoring rule requires alphabetical ordering when adding components to nav, README, and exports\n\n## [1.4.0] - 2026-06-14\n\n### Added\n\n- `Button` component with primary, secondary, outline, and ghost variants, loading spinner, and disabled state\n- `ButtonClassNames` and `classNames` prop for part-level Tailwind/class overrides (`root`, `label`, `spinner`)\n- Button documentation page at `/docs/button` with props and exported types\n- `Checkbox` component with optional label and part-level `classNames`\n- Checkbox documentation page at `/docs/checkbox` with props and exported types\n- `Dropdown` select-style component with clearable support and part-level `classNames`\n- Dropdown documentation page at `/docs/dropdown` with props and exported types\n- `Popover` compound primitives (`Popover`, `PopoverTrigger`, `PopoverContent`, etc.) with styled content surface\n- Popover documentation page at `/docs/popover` with props and exported types\n- Cursor rule `.cursor/rules/gsl-component-authoring.mdc` for component conventions\n\n### Changed\n\n- Radix UI packages moved from peer dependencies to dependencies so a single `npm install @rfdtech/components` is sufficient\n- `FieldMappingSelect` in BulkImportModal now wraps the public `Dropdown` component\n- Dropdown trigger uses Lucide `ChevronDown` instead of a text glyph; adds `lucide-react` as a dependency\n- Popover docs example demonstrates an action menu pattern with `PopoverClose`; adds menu utility classes (`gsl-popover--menu`, `gsl-popover__menu`, `gsl-popover__menu-item`)\n- AppSwitcher app icons render image URLs as round cropped icons\n- `AppSwitcher` is data-only: pass `apps` directly and control loading with the `loading` prop; panel shows a Lucide spinner while loading\n\n### Removed\n\n- `AppSwitcher` remote fetch via `baseUrl` / `accessToken`\n- `useMeApps`, `fetchMeApps`, `MeAppsFetchError`, `buildMeAppsUrl`, `createMeAppsRequestInit`, `mapMeAppToAppItem`, `mapMeAppsToAppItems`\n- `MeApp`, `MeAppsResponse`, `UseMeAppsOptions`, `UseMeAppsReturn`, and `AppItem.metadata`\n\n## [1.3.0] - 2026-06-14\n\n### Added\n\n- `ThemeProvider` and `useTheme()` for light, dark, and system themes\n- Layered theme CSS (`base`, `light`, `dark`) with shared z-index tokens\n- Theme documentation page at `/docs/theme`\n\n### Changed\n\n- Theme CSS loads once from the library entry instead of per-component imports\n- `ThemeProvider` syncs `data-gsl-theme` to `document.documentElement` so portaled modals and popovers inherit the active palette\n\n## [1.2.0] - 2026-06-14\n\n### Added\n\n- Radix UI primitives as peer dependencies (`@radix-ui/react-popover`, `@radix-ui/react-dialog`, `@radix-ui/react-alert-dialog`, `@radix-ui/react-select`, `@radix-ui/react-radio-group`, `@radix-ui/react-slot`)\n\n### Changed\n\n- `AppSwitcher` now uses `@radix-ui/react-popover` for the app grid panel\n- `BulkImportModal` now uses `@radix-ui/react-dialog` and `@radix-ui/react-alert-dialog` for the modal shell and exit confirmation\n- Column mapping in `BulkImportModal` uses `@radix-ui/react-select`\n- Header row selection uses `@radix-ui/react-radio-group`\n\n### Removed\n\n- Custom popover, dialog, and field-mapping dropdown implementations replaced by Radix primitives\n\n## [1.0.3] - 2026-06-12\n\n### Added\n\n- Changelog page in docs (`/docs/changelog`) and root `CHANGELOG.md`\n\n## [1.0.2] - 2026-06-12\n\n### Added\n\n- `DataTable` component with column sorting, pagination, loading state, and empty state\n- `Pagination` component (standalone; composed by `DataTable`)\n- Interactive documentation site at `/docs` with MDX pages, syntax highlighting, and preview/code tabs\n- Demo app routing split into Demo and Docs pages with a sectioned docs sidebar\n\n### Changed\n\n- `DataTable` is static-only: parents pass `data` and control fetching with a `loading` prop (async `loadData` removed)\n- Demo app refactored into `DemoPage` and `DocsPage` with shared `DemoLayout`\n\n## [1.0.0] - 2026-06-11\n\n### Added\n\n- `Dropdown` single-value select component\n- `DropdownMenu` action menu component\n- `Combobox` async search component with debounced `loadOptions`\n- `prepublishOnly` script to run typecheck, tests, and build before publishing\n\n### Changed\n\n- `BulkImportModal` column mapping uses an internal field-mapping dropdown\n- README expanded with component usage and style import instructions\n\n### Removed\n\n- `Dropdown`, `Combobox`, `DropdownMenu`, `DataTable`, and `Pagination` components\n\n## [0.4.0] - 2026-06-11\n\n### Added\n\n- `AppSwitcher` component for switching between GSL systems\n- `BulkImportModal` multi-step import flow with editable rows and exit confirmation\n\n### Changed\n\n- Package renamed to `@rfdtech/components`\n- Bulk import stepper animations, accessibility, and responsive styling improvements",
549
+ "docs": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\nUpgrading from `1.22.0`? See the [migration guide](/docs/migration-v2) for a step-by-step walkthrough of every breaking change below.\n\n## [Unreleased]\n\n## [2.1.0] - 2026-07-25\n\n### Added\n\n- **PageSection**: new wrapper component that applies consistent vertical spacing between page sections via `--clet-app-layout-body-gap`. Replaces ad-hoc `margin-bottom` on section divs. `forwardRef`, `classNames.root`, `className`.\n- **QuickActions**: new dashboard component featuring a responsive tile grid (1/2/3 columns) and an optional \"Customize\" dialog for toggling action visibility. Each tile shows a primary-colored icon circle, label, and optional description. Fully controlled via `actions`, `customizable`, `hiddenIds`, and `onToggleVisibility` props.\n- **Demo**: enhanced `Dashboard2Page` with QuickActions, Tabs (pill variant), Timeline audit trail, and PageSection-wrapped sections. Added `UserCreatePage` (multi-step stepper form with zod validation) and `UserDetailPage` (profile card + timeline + quick actions). Updated `DemoLayout2` sidebar nav with Users links. New docs pages: `page-section.mdx`, `quick-actions.mdx`, `dashboard-patterns.mdx`.\n\n### Changed\n\n- **Card**: new `bordered` prop — when set, adds a 1px solid border using the `--clet-border` token. Header margin-bottom reduced from 24px to 16px for tighter spacing.\n- **Sidebar**: plain variant background now defaults to `--clet-surface-card` instead of `--clet-bg`, so the sidebar reads flush with the adjacent card surface. Overlay z-index now references `--clet-sidebar-z` instead of `--clet-z-overlay`, matching the mobile drawer's own stacking token.\n- **Sidebar**: new `mobileHeader` prop — when `variant=\"plain\"` and the consumer hasn't already added a `<SidebarHeader>` child, the library auto-wraps the provided content in a `SidebarHeader` with the built-in `clet-sidebar__header--mobile-only` class (hidden on desktop via the existing `@media (width >= 769px)` block in `src/components/sidebar/styles/sidebar.css`). The consumer is responsible for providing the brand content (typically a `SidebarBrand` with the same logo/title as the sibling `AppHeader`'s `AppHeaderBranding`) — `AppLayout` does not auto-extract it, since the header and sidebar are intentionally separate visual contexts with different styling. The previous migration recipe that required a hand-written `@media` rule to hide the consumer-supplied `SidebarHeader` no longer applies; the class is library-native.\n- **Sidebar**: the mobile backdrop is now rendered internally by `Sidebar` as a sibling of the `<aside>`, so explicit `<SidebarOverlay />` usage is no longer required (still exported for backward compatibility; harmless if duplicated — two stacked overlays both close the drawer on click, no visual artifact).\n- **Launchpad**: the top-right expand button (always rendered, `Maximize2` icon) is back, and a new muted \"See more\" line lives under the grid (between the grid and the role switcher, rendered only when `apps.length > 9`) — both controls open the same scaled-up expanded modal showing every app in `apps` uncapped. The under-grid \"See more\" is a real button (keyboard-focusable, click opens the modal) styled as plain muted text (full-width, centered, no border/background) so it reads as a hint rather than a primary action. Dropped the previous `MoreHorizontal` \"More\" tile that took the 9th grid slot — the cap is now a clean 9 apps, and the under-grid \"See more\" line is the overflow indicator.\n- **DateSelector**: the calendar header title is now clickable and opens a 3-level picker (day / month / year) inspired by iOS and Material patterns. Click the title in day view for a 3×4 month grid; click again for a 3×4 year grid. Month and year cells use the same selected/today/disabled styling as day cells. Navigation arrows adapt per level (month, year, or decade step). The popover re-opens at the selected date's period. Added `calendarMonthGrid`, `calendarMonth`, `calendarYearGrid`, `calendarYear` to `DateSelectorClassNames`.\n- **MetricCard**: label color tokens swapped between `default` and `outline` variants — the `default` variant label now reads `--clet-secondary` (gold) and the `outline` variant reads `--clet-text-secondary`, making visual hierarchy consistent across both variants.\n- **Demo**: `Dashboard2Page` table wrapped in `<Card bordered>` for a contained card-style layout. `DemoLayout2` sidebar now includes a mobile-only `SidebarHeader` with `SidebarBrand` for the app logo and title on small viewports.\n- **Demo**: migration guide updated with the new Card bordered and sidebar mobile-header patterns.\n\n### Fixed\n\n- **Sidebar**: removed the `document` click-outside listener that was racing the `AppHeader`'s mobile hamburger trigger — when the user clicked the hamburger to open the drawer, the document-level `click` handler fired on the same event and immediately re-closed it. The `SidebarOverlay` (now rendered internally on mobile) remains the close path. Clicking the hamburger in the `AppHeader` on a mobile viewport now reliably opens the drawer.\n- **Combobox**: mouse-wheel scrolling now works on the option list. An `onWheel` handler manually manages `scrollTop` to work around `cmdk`'s non-intercepted scroll behavior.\n\n## [2.0.2] - 2026-07-23\n\n### Changed\n\n- **Sidebar**: mobile drawer now uses a dedicated `--clet-sidebar-z` token defaulting to `10000` so it layers above headers, command popovers, and table bulk-action bars in responsive layouts.\n- **Table**: header bar stacks into a column below `1024px`, with actions wrapping beneath the search/filter row for smaller screens.\n\n### Fixed\n\n- **Lottie animations**: `BulkImportModal` and `ProgressModal` now load `lottie-react` only on the client, preventing `lottie-web` canvas initialization crashes during SSR/static builds and test imports.\n\n## [2.0.1] - 2026-07-16\n\n### Added\n\n- **New auto-installed `image-to-components` skill** (alongside `rfdtech-ui`, via `rfdui setup`): given a screenshot, image, mockup, or textual description of a screen to build, decompose it and map every element to an existing `@rfdtech/components` component/variant via the MCP (`search_components`, `get_component_types`, `get_component_examples`, `get_rules`) before writing any markup. Composes pages exclusively from matched components; if an element has no match, it stops and asks the user rather than hand-rolling custom UI — custom UI is only built when explicitly requested. Installed to `.claude/skills/image-to-components/`, `.cursor/rules/image-to-components.mdc`, and `.ai/image-to-components.md` for Claude Code, Cursor, and other tools respectively.\n- **`Switch`**: new binary on/off toggle built on `@radix-ui/react-switch` (new peer dependency), matching the `Checkbox` authoring pattern (plain function component, `label`/`labelPosition` for a \"Switch + Text\" variant, `classNames` slot map for `root`/`track`/`thumb`/`label`). 44×24 track, checked state uses `--gsl-primary`, all colors/sizes tokenized (`--gsl-switch-*`).\n- **`Notice`**: new boxed, persistent callout (`variant`: `default`/`info`/`success`/`warning`/`error`, `title`, `icon`, `leftBorder`, `dashed`, `color` escape hatch for a custom accent). No new brand colors — variants reuse `--gsl-primary`/`--gsl-success`/`--gsl-warning`/`--gsl-error`, tinted via `color-mix()` like `Badge`. `role` defaults to `\"alert\"` for the `error` variant, `\"status\"` otherwise.\n- Docs: `role-select.mdx` now has a real runnable preview (was a static, non-interactive code block only) and its `title` prop default is corrected to `\"View as\"` (was incorrectly documented as `\"Role\"`). `dropdown.mdx` gained a preview for the `disabled` state and turned its `formatOption` custom-rendering sample into a runnable example instead of a static snippet. `popup.mdx`'s variants example now matches the actual design spec — title/description/body combinations × row/stack footer layouts (title+description with row or stack buttons, an optional input body, description-only, title-only with a single button) — replacing an earlier example that didn't reflect the real variant set. `upload-field.mdx` gained a `multiple` mode preview.\n- **`UploadField`**: new optional `fileStatuses` prop overlays a per-file `\"uploading\"` (progress bar + cancel), `\"completed\"` (green check + existing trash icon), or `\"failed\"` (red-bordered card + \"Try Again\") state, index-aligned with `value` — omit it entirely for the unchanged default attach/remove look. New `onCancel`/`onRetry` callbacks. The file-type icon logic is now also exported standalone as `FileFormatIcon`. Fixed the inline PDF icon's hardcoded `#dc2626` to `var(--gsl-error)`.\n- **`PhoneNumberInput`**: flags are now real SVG icons from the new `country-flag-icons` dependency (each rendered with `role=\"img\"` + a `\"{country} flag\"` aria-label), replacing the previous emoji flags — consistent rendering across OSes/browsers instead of relying on platform emoji support. `CountrySelector`'s emoji flags are unchanged (see its deprecation note below).\n- **`DateRangeSelector`**: new optional `presets` prop — renders a left rail of quick-select presets (`{ label, getRange }`) and a \"Range: ...\" footer summary. Selecting a preset previews it on the calendar (Apply is still required to commit, same as manual day selection); the matching preset is highlighted, and clears when a day is clicked manually afterward. Both the rail and summary are opt-in — omitting `presets` keeps the existing layout unchanged.\n- **`Combobox`**: new searchable option list built on `cmdk` (following the `Command` component's inline-Popover pattern), opened from its own trigger button rather than Radix `Select`. Supports single selection (closes on pick), multi-selection with per-row checkboxes and a \"Selected: N options\" trigger summary (stays open while toggling), a per-option leading icon adornment, and `clearable`. Search matches each option's `label` via cmdk `keywords` while `value` stays the stable identifier.\n- A global text-sizing scale: `--gsl-text-size-xs/sm/base/md/lg/xl` in `src/styles/theme/base.css`, mapped to Tailwind `text-xs…xl` utilities via `--text-*` in the `@theme` block. (Named `text-size`, not `font-size`, to avoid a `classify()` ordering collision in `scripts/generate-theme-tokens.mjs` where `font` matches before `size`.)\n- Closed several gaps in the Tailwind `@theme` mapping (`src/styles/theme/tokens.css`): `--color-primary-light`, `--color-brand-gold` (the actual gold `--gsl-secondary`, kept separate from the existing `--color-secondary` → `surface-dark` mapping), `--color-text-muted`, `--color-border-strong`/`--color-border-subtle`, `--color-destructive-bg`, `--color-panel`, `--radius-xl`/`--radius-2xl`. A coverage-checklist comment at the bottom of `tokens.css` documents every global token that's intentionally not mapped (z-index tokens, overlay/shimmer) and why.\n- **Launchpad**: new sibling to `AppSwitcher` — a constrained, low-config 9-app launcher (fixed 3×3 grid) with a built-in bordered \"See all\" button and a role switcher rendered via `children` (only a `RoleSelect` element is accepted) below the grid, no manual `footer` composition needed. \"See all\" isn't a callback — it always opens a borderless `--gsl-radius-2xl` modal (50px padding, top-right close) showing every app in `apps` uncapped, in a 5-column 120px-icon grid — a scaled-up version of the same tile system. Both the popover and the modal title are a fixed \"Launchpad\" heading (`--gsl-font-header`, 600 weight); the trigger is a fixed round 9-dot icon with a hover tooltip reading \"Open Launchpad\" (also its accessible name) — none of these have a label/override prop. Loading state is spinner-only, no text. Both the popover panel and the modal's scrollable grid fade at the top/bottom edge (`mask-image`) instead of hard-clipping, and every app name reserves a fixed 2-line height so grid rows stay aligned regardless of name length. Icons render on a fixed palette of 9 brand gradients and 9 decorative overlay shapes (5% opacity) via `SystemLaunchpadIcon`/`LaunchpadIconTile` — `LaunchpadApp.icon` only type-checks as one of those two components, so a raw image URL, emoji, or arbitrary node can't opt an app out of the shared tile design. 50×50 icons, `--gsl-radius-base` corners. Fully independent from `AppSwitcher` (separate types, CSS classes, hook) — adopting it doesn't touch any existing `AppSwitcher` usage. Also exported: `LaunchpadItem`, `LaunchpadGridIcon`, `useLaunchpad`.\n- **ExportButton**: new button that exports tabular `data` to CSV, Excel (`.xlsx`), or a printable PDF report. Accepts all `Button` props (renders `<Button>` as its trigger) plus `columns` (`header` + `accessor`), `title`, optional `filename`/`filtersDescription` (both folded into the generated filename), and `formats` to limit which options appear in the dropdown. `Button` now forwards its ref so it can be used as a Radix Popover trigger. The underlying `exportToCsv`/`exportToXlsx`/`exportToPdf`/`formatFilenameTimestamp` functions (plus the `ExportMeta` type) are now exported from the package root too, for building a custom trigger instead of using `ExportButton`'s styling.\n- The PDF export's \"Generated\" timestamp now renders as `date.toLocaleString(\"en-US\", { dateStyle: \"medium\", timeStyle: \"short\" })` (e.g. `\"Jul 13, 2026, 2:05 PM\"`) instead of a hand-rolled `YYYY-MM-DD HH:MM` string. The filename timestamp (`formatFilenameTimestamp`) is unchanged — still `YYYY-MM-DD_HHmm`, kept filesystem-safe on purpose.\n- **Dropdown**: `name` (plus `required`/`form`) prop — Dropdown now participates in native `<form>`/`FormData` submission on its own, backed by Radix's hidden bubble `<select>`. Inside `TableFilter`, pass `name=\"role\"` directly instead of pairing a hand-written `<input type=\"hidden\" name=\"role\" value={roleValue} />` alongside it.\n- **`TableContent`**: `onRowClick` prop — called with `(row, event)` on row click, in addition to (not instead of) selection toggling when `selectable` is set. Use it to wire up row-click behavior (e.g. opening a detail view) without hand-rolling an `onClick` per cell.\n- **`TableContent`**: `bulkActions` prop (requires `selectable`) — surfaces bulk actions inside the row-actions menu (kebab click or right-click) as a \"Bulk actions\" section: always a Select all/Deselect all toggle, with the rest of the list appearing once one or more rows are selected. New `bulkActionsFooter` prop also renders the `TableBulkActions` bar at the bottom automatically, wired to the same `bulkActions`/`selectedIds`, instead of placing `<TableBulkActions>` yourself as a sibling. New `TableContentClassNames.actionsSectionLabel` key for the section label.\n- **`TableFooter`**: `noBorder` prop removes the footer's top border — replaces hand-writing `className=\"clet-table__footer--no-border\"` (which still works, but `noBorder` is now the documented way).\n\n### Breaking\n\n- **`AppItem.icon` (AppSwitcher) is now a closed set: `<SystemAppIcon>` or `<AppIconTile>` only.** Was `ReactNode | string` — a raw image URL, emoji, or arbitrary node is no longer accepted, even with `as any` bypassing the type it won't render (icons are inserted directly, no more string/URL handling in `AppSwitcherItem`). Every app icon renders on the shared 9-gradient / 9-overlay tile system by construction; there's no per-app opt-out. Replace `icon: \"https://...\"` with `icon: <SystemAppIcon name=\"App Name\" />` (or `<AppIconTile name=\"...\">` for a custom glyph).\n- **`AppHeaderProfile` is removed — merged into `ProfilePopover`.** They were the same menu with two names and two prop surfaces; now there's one component. `ProfilePopover` gained `user`/`variant` props that build the same compact avatar + name/role + chevron trigger `AppHeaderProfile` used to, alongside its existing `fullName`/`email`/`profilePhoto` mode. Replace `<AppHeaderProfile user={...} variant=\"avatar\">` with `<ProfilePopover user={...} variant=\"avatar\">` — same `items`, `loading`/`loadingLabel`, `onSignOut`, and `children` props carry over unchanged. `onProfileClick`/`onSettingsClick`/`onHelpClick` are gone; pass an `items` array instead (nothing hardcodes \"My Profile\" / \"Account Settings\" / \"Help & Support\" anymore — nothing renders in that section unless you pass `items`).\n- **`headerAction` is removed.** The popover header always shows a built-in light/dark theme toggle when a `ThemeProvider` is present in the tree, and nothing when it isn't — this is no longer a prop you can pass, override, or opt out of.\n- **`TableContent`: the kebab actions column no longer renders when `selectable` is true but no `rowActions` are passed.** Previously that combination still rendered a kebab column containing only a Select/Deselect toggle; selection is already handled by the checkbox column, so the empty kebab column was removed. If you relied on that Select/Deselect entry, pass an explicit `rowActions` entry for it instead.\n\n### Deprecated\n\n- **`AppSwitcher` is deprecated and no longer maintained.** Use [`Launchpad`](/docs/launchpad) for new work. `AppSwitcher` stays exported and won't be removed without its own breaking-change entry, but it receives no further features or design updates — `Launchpad` doesn't yet cover every `AppSwitcher` capability (flexible column count, an uncapped/differently-capped grid, a fully custom `footer`), so migrate only once it does, or if you don't need those.\n- **`CountrySelector` is deprecated in favor of `Combobox` (general country selection) or `PhoneNumberInput`'s own built-in country picker (phone numbers).** It was already unused anywhere else in the library — a fully standalone implementation duplicating what `PhoneNumberInput` builds inline. `CountrySelector` stays exported and won't be removed without its own breaking-change entry, but it receives no further features or design updates. See the [migration guide](/docs/migration-v2) for the replacement pattern.\n- **`NetworkOperator` is deprecated in favor of `Combobox` (build an operator list with `options`).** It was already unused anywhere else in the library — a standalone dropdown duplicating what `Combobox` covers generically. `NetworkOperator` stays exported and won't be removed without its own breaking-change entry, but it receives no further features or design updates. See the [migration guide](/docs/migration-v2) for the replacement pattern.\n\n### Changed\n\n- **MCP (`get_component`/`get_tokens`/`get_rules`/etc.) now serves `--clet-*` exclusively for every color token** — docs prose, type source, example source, and rule bodies are rewritten at index-build time (`mcp/src/indexer.ts`) so an agent reading through the MCP never encounters the legacy `--gsl-*` spelling for a color token; non-color tokens (radius/shadow/font/etc.) are unaffected. This also fixed a real data bug: `get_tokens` was previously showing the *literal* declared value of `--gsl-<name>` for color tokens — since that's now `var(--clet-<name>))`, agents were seeing a self-referential string instead of the real color; the indexer now resolves and serves the actual `--clet-<name>` value. The human docs site (rendered directly from the same `.mdx` source, not through this index) is unaffected and still explains both names. Also synced a duplicate `classify()`-style helper in `mcp/src/indexer.ts` that had drifted from `scripts/generate-theme-tokens.mjs` (missing the `secondary` fix from the surrounding token work).\n- **Every color token now has a `--clet-*` alias alongside its `--gsl-*` name**, both resolving to the identical value (`--gsl-<name>` is now defined as `var(--clet-<name>)`, not a literal value — existing `--gsl-*` overrides keep working unchanged). Covers all 46 color-classified tokens, global and component-scoped (e.g. `--clet-primary`, `--clet-notice-accent`, `--clet-sidebar-bg`). `--clet-*` is the new preferred name in docs/examples going forward. (Superseded below: the follow-up backward-compatibility sweep extended this `--clet-*` declaration to every token, non-color included — see the `.gsl-*`/`--gsl-*`/`gslTheme()` rename entry.) Also fixed a `classify()` gap in `scripts/generate-theme-tokens.mjs` where `--gsl-secondary` (the gold brand token) wasn't recognized as a color (the regex checked for `primary` but not `secondary`), so it was previously mistyped as a generic string in `gslTheme()` instead of a proper color value.\n- **Rebranded to \"Clet\"**: \"Ghana School of Law\" references in prose (package description, README, `getting-started.mdx`) now read \"Clet\". Docs site header, README title, AGENTS.md title, and the component-authoring cursor rule title also updated. The `@rfdtech/components` package name and the MCP server id (`gsl-components-docs`) are unchanged.\n- **Every `.gsl-*` CSS class, `--gsl-*` custom property, `data-gsl-theme` attribute, `.gsl-theme` root class, and the `gslTheme()` theming API is renamed to its `.clet-*`/`--clet-*`/`data-clet-theme`/`.clet-theme`/`cletTheme()` counterpart — but every old name keeps working, unchanged, with no migration required.** Confirmed by test (`npm test`), typecheck, `lint:css`, and `build` all passing after the rename, plus a direct render assertion that a component carries both classes and `ThemeProvider` sets both attributes on the same element. The compatibility mechanism, layer by layer:\n - **Classes**: every rendered element now carries both classes (e.g. `class=\"clet-sidebar gsl-sidebar\"`) — old `.gsl-*` selectors in your own CSS keep matching without any change.\n - **CSS custom properties**: every internal `var(--clet-<name>)` read is now `var(--gsl-<name>, var(--clet-<name>))` — overriding either `--gsl-<name>` or `--clet-<name>` (globally or per-component) works identically; `--gsl-<name>` wins if both are set.\n - **Theme attribute/root class**: `ThemeProvider` sets both `data-clet-theme` and `data-gsl-theme` on `<html>` and its wrapper `<div>`, and the wrapper carries both `clet-theme` and `gsl-theme` classes — `light.css`/`dark.css`/`base.css` selectors match either spelling.\n - **JS API**: `gslTheme()` is now an alias of `cletTheme()` (same function, same behavior). Legacy type names (`GslTheme`, `GslThemeConfig`, `GslComponentThemeConfig`, `GslComponentThemeOverrides`, `ResolvedGslTheme`, `GslColorValue`, `GslComponentTokenMap`, `GslGlobalTokens`, `GslLengthValue`, `GslOpacityValue`, `GslShadowValue`, `GslStringValue`, `GslZIndexValue`) are re-exported as aliases of their `Clet*` equivalents.\n - `clet-*` is the preferred spelling in new docs, examples, and internal source going forward; `gsl-*` is a permanent compatibility alias, not a deprecated one — it isn't scheduled for removal.\n- **Default brand color**: `--gsl-primary` (light theme) changed from `#051b2c` to `#083755`. `--gsl-focus` and every component that reads `--gsl-primary` pick up the new value automatically; dark-theme `--gsl-primary` (`#4a7fa8`) and the static Launchpad brand-gradient palette are unaffected.\n- **`--gsl-page-bg`** (light theme) changed from `#fff` to `#fafafa`. **`AppLayout`**'s `\"stacked\"` variant content area now explicitly reads `--gsl-page-bg` instead of `transparent` (so it reads flush with the page instead of showing whatever's behind it); **`MetricCard`**'s `\"outline\"` variant now uses `--gsl-page-bg` instead of `transparent` for the same reason — both previously relied on transparency inheriting an ambient background rather than an explicit token, which broke down whenever they weren't sitting directly on the page (e.g. an outline `MetricCard` inside `AppLayout`'s `\"default\"` panel would show through as the panel's gray, not the page's tone).\n- **`DateSelector` / `DateRangeSelector` / `Popup`**: hardcoded font sizes (`14px`/`13px`/`12px`) replaced with the new `--gsl-text-size-*` scale, and hardcoded trigger/calendar/popup dimensions (heights, padding, min-widths) promoted to component-scoped `--gsl-*` tokens — no visual change, but every size is now overridable via `gslTheme()`.\n- **AppSwitcher / AppIconTile**: tile icons are now 40×40 (was 32px) with no explicit grid gap (spacing comes from each item's own 8px padding), and generated tile skins (`SystemAppIcon`/`AppIconTile`) switched from a procedural HSL gradient + drawn pattern to a fixed palette of 9 brand gradients and 9 decorative overlay shapes, each independently hashed from `name` (same name always renders the same tile; gradient and overlay vary independently). The `footer` slot now lays out as a stretched flex column, and the panel scrolls (`max-height` + `overflow-y: auto`) — enables composing a bordered \"See all\" `<Button>` and a `<RoleSelect>` stacked below the grid.\n- **ProfilePopover**: removed the divider between `items` and `children` (e.g. a `RoleSelect`) — they now render flush together in one content block. Dividers still separate the header from that block, and that block from **Sign Out**.\n- `ProfilePopoverItem` and `AppUser` are now exported from the package root (previously `AppUser` lived under `AppHeaderProfileProps` and `ProfilePopoverItem` wasn't exported at all).\n- **`AppHeaderBranding`**: `title` now renders in the heading font at `20px` (new `--gsl-branding-title-font` token, `--gsl-branding-title-size` default raised from `14px`) instead of inheriting the block's font. Everything else in the branding block — the `subtitle` included — now defaults to the body font (`--gsl-branding-font` default changed from `--gsl-font-header` to `--gsl-font-body`); the old `\"plain\"`-variant-only body-font override was removed since it's now the default everywhere.\n\n### Fixed\n\n- **`Button`**: `variant=\"secondary\"` now actually has a transparent background (only a `--gsl-border` outline, filling with `--gsl-hover` on hover) — it had no dedicated base rule and was silently inheriting the root `.gsl-button` rule's opaque `var(--gsl-bg)` background, making it look filled instead of outlined like `ghost`/`destructive`.\n- **`Sidebar`**: the default (non-`\"plain\"`) desktop card panel now genuinely uses its own `--gsl-sidebar-bg` token — previously that token was referenced (with a fallback) in CSS but never actually declared anywhere, so it wasn't overridable via `gslTheme()` and didn't appear in `get_tokens(\"sidebar\")`, even though the docs already listed it. Visual default is unchanged (`var(--gsl-panel-bg, var(--gsl-surface-subtle))`), but it's now a real, themeable token.\n- **`UploadField`**: removing a file in `multiple` mode now removes only that file from the array; previously the remove button on any card cleared the entire selection back to `null`.\n- **TableContent**: right-clicking a row to open its `rowActions` menu now opens the menu at the exact cursor position instead of snapping it to the kebab column at the end of the row. Opening via the kebab button itself is unchanged — still anchored to, and right-aligned under, that button. Right-clicking the same row again while its menu is already open now closes it, instead of re-opening at the new cursor position.\n- **TableContent**: the empty state (no `data`, no custom `children` override) now keeps the column header row visible above the \"No results\" message, instead of replacing the whole table with just the empty-state div. Matches the loading skeleton, which already kept the header.\n- **TableFilter**: `variant=\"spread\"` fields (e.g. `Dropdown`) now actually lay out side by side in a row. Each field previously inherited `width: 100%` from its stacked/popover styling, which — as a flex-row child — made it claim the entire row and force every other field onto its own line. The fix's own field-width override no longer forces a size onto Radix's visually-hidden bubble `<select>` (rendered when a field uses `Dropdown`'s `name` prop).\n- **Table**: the header toolbar (`TableSearch`/`TableFilter`/`TableActions`), both `TableFilter` variants, `TablePagination`, and the bulk-actions bar now stack and wrap at `768px` and below instead of overflowing or clipping — matches the breakpoint `Sidebar` already uses for its mobile drawer.\n- **AppHeader**: the notification bell (`AppHeaderNotifications`) and any other button built on `.gsl-app-header__notif-btn` (e.g. a docs-link icon button in `AppHeaderActions`) now keep `flex-shrink: 0`, so they stay a fixed 36px circle instead of getting squeezed narrower than tall when the header runs out of room.\n\n## [2.0.0] - 2026-07-11\n\n### Breaking\n\n- **AppHeaderProfile**: `variant` no longer accepts `\"basic\"` — only `\"full\"` and `\"avatar\"` remain. `children` type narrowed from `ReactNode` to a single `RoleSelect` element, matching `ProfilePopover`'s `children` slot.\n- Default brand tokens changed: `--gsl-primary`/`--gsl-primary-light` moved from red (`#dc2626` / `#ef4444`) to navy (`#051b2c` / `#4a7fa8`), and a new `--gsl-secondary` (gold `#c8a24b`) token was added and is now the default `SidebarLink` active-link color. Apps that don't set their own `--gsl-*` overrides will see the new navy/gold branding instead of the old red.\n- Default typography changed: new `--gsl-font-header` (Roboto Serif) and `--gsl-font-body` (Lato) tokens, loaded via new `@fontsource/lato`/`@fontsource/roboto-serif` dependencies, now back nearly every component's heading/body text (each component defines its own `--gsl-<component>-font`/`-title-font`/`-value-font` variable defaulting to one of the two). Previously every component inherited the single system-font stack (`--gsl-font`). Apps that don't override these tokens will see the new typefaces everywhere; the CSS bundle also grows by the bundled font files.\n- **AppSwitcher**: new `maxItems` prop defaults to `6` — grids that previously rendered every app now silently truncate past the 6th unless `maxItems` is passed explicitly (e.g. `maxItems={apps.length}`).\n\n### Added\n\n- **Stepper**: horizontal step indicator for multi-step flows. `Stepper` holds the active step value and derives each `Step`'s `complete` / `active` / `upcoming` state. Each `Step` renders a numbered circle that flips to an animated check when done and a connector line that fills between completed steps; `StepLabel` supplies the step's text. Optional `clickable` mode turns steps into buttons firing `onValueChange`, with per-step `disabled`. All colors resolve from existing `--gsl-*` tokens through overridable internal variables (`--gsl-stepper-accent` defaults to `--gsl-primary`, plus `--gsl-stepper-marker-size`, `--gsl-stepper-track`, etc.), and animations respect `prefers-reduced-motion`. Also available as `Stepper.Step`, `Stepper.StepLabel` for compound-style imports.\n- Sidebar: `SidebarGroup` `collapsible` prop — the first `SidebarGroupLabel` child becomes a button toggle. The label keeps the same visual styling as the static `<p>` (same font, size, color, padding); clicking it fades the content open/closed while reserving the group's space in the layout. Supports uncontrolled (`defaultExpanded`) and controlled (`expanded` + `onExpandedChange`) state. Content has `inert` + `visibility: hidden` when folded, and the transition respects `prefers-reduced-motion`. New `SidebarGroupClassNames` keys: `groupToggle`, `groupContent`.\n- **ProfilePopover**: new user-menu popover — avatar/name/email trigger and header, `items` rows (`icon`, `label`, `onClick`, `danger`), `onSignOut`, `headerAction` slot (e.g. a theme toggle), a `loading` skeleton state with `loadingLabel`, and a `children` slot that only accepts a `RoleSelect`. `AppHeaderProfile` is now built on top of it.\n- **RoleSelect**: dropdown for switching between roles — confirms via a \"Confirm Role Switch\" dialog by default (skip with `noConfirm`), radio-style selected state, per-role `icon`/`disabled`. Composes into `ProfilePopover` via its `children` slot.\n- `gslTheme()` runtime theme API (`src/components/theme/gslTheme.ts`) — pass camelCase `--gsl-*` token overrides per light/dark mode, globally and/or per component; injects a `<style>` tag matched to the library's own selector specificity so overrides win by source order. Backed by a generated typed token surface (`src/generated/components.theme.ts`) built by the new `scripts/generate-theme-tokens.mjs` (`npm run generate:tokens`), which scans `--gsl-*` custom property definitions across `src/styles/theme` and every component's CSS.\n- MCP docs server + `rfdui` CLI (`mcp/`) — ships inside `@rfdtech/components`'s `dist/mcp` and exposes `list_components`, `search_components`, `get_component`, `get_component_examples`, `get_component_types`, `get_rules`, `search_rules`, `get_tokens`, and `search_docs` tools generated from the existing MDX docs, `src/types/*.ts`, and new `demo/docs/rules/*.md` design-rule files, so AI coding agents can look up real component usage instead of guessing.\n- MetricCard: `loading` prop — shimmering skeleton placeholders for icon/label/value/trend/description, with `loadingLabel` for the announced status text.\n- SidebarLink: `loading` prop — shimmering skeleton placeholder for icon/label, with `loadingLabel` for the announced status text.\n- TableFilter: `variant=\"spread\"` — removes the popover and lays filter fields out inline as a flex row with a consistent gap, with just a \"clear\" action (fields auto-apply as soon as their value changes — no Apply button). Same `onApply`/`onReset` and URL behavior as the default `\"popover\"` variant otherwise.\n- **AppHeaderNotificationItem**: new notification row component — unread dot (or a subtle read background), message `text`, and `time` string. Becomes keyboard-focusable (Enter/Space) only when `onClick` is passed. Replaces hand-rolled `.gsl-notif-popover__*` divs as the documented way to render `AppHeaderNotifications` children.\n- AppSwitcher: `maxItems` prop to cap the visible app grid, and a `children` slot rendered below the grid (e.g. a \"manage apps\" link).\n- Avatar: `background` and `backgroundVar` props to override the initials gradient with a solid color or CSS variable (falls back to the existing gradient).\n\n### Changed\n\n- AppHeaderProfile: rebuilt on top of `ProfilePopover` — adds `headerAction`, `loading`/`loadingLabel`, and `onProfileClick`/`onSettingsClick`/`onHelpClick`/`onSignOut` props with a default \"My Profile\" / \"Account Settings\" / \"Help & Support\" menu.\n\n## [1.22.0] - 2026-07-07\n\n### Added\n\n- **Timeline**: composable vertical timeline with `Timeline` + `TimelineItem` primitives. Four status variants (`complete`, `current`, `warning`, `error`), optional icon override in the dot, staggered connector draw and dot pop-in animations, and part-level `classNames`. Render any markup as the content column. Connector animations respect `prefers-reduced-motion`.\n- **RouterAdapter**: framework-agnostic router abstraction that replaces direct `react-router-dom` imports across 7 hooks and components. Library now ships two entry points:\n - `@rfdtech/components` — default, backward compatible (react-router-dom)\n - `@rfdtech/components/next` — Next.js App Router support via `<RouterAdapterProvider>`\n- New exports: `RouterAdapterProvider`, `setRouterAdapter`, `getRouterAdapter`, `useNextRouterAdapter`\n- RouterAdapter: SidebarLink, AppLayoutInner, table hooks (useTableFilter, useTablePagination, useTableState), TableSearch, TableFilter, and TablePagination no longer import react-router-dom directly — they consume the adapter from context, making them portable to other routers\n- TableContent: `emptyIcon` and `emptyText` props for a customizable empty state when `data` is empty; renders in both regular and virtualized paths\n- TableContent: default empty icon (Inbox) shown when no `emptyIcon` is provided\n- Table: `classNames` API added to TableContent, TableHeader, TableFooter, TableSearch, TableFilter, TableActions, and TablePagination for deep inner styling\n- Tabs: `pill` variant with connected pill-style triggers and underline indicator\n\n- Theme: Tailwind v4 `@theme` token integration — all `--gsl-*` design tokens are registered as Tailwind utility classes for consumers using Tailwind v4; dark mode selectors now support `.dark` and `.light` class-based toggling alongside `data-gsl-theme`\n- ProgressModal: non-dismissable processing overlay with Lottie animation and determinate progress bar\n- BulkImportModal: chunked processing for datasets over 1000 rows — file parsing, row mapping, and validation run in 1000-row batches with a progress overlay to prevent main-thread freezes\n- BulkImportModal: CSV files stream-parsed in chunks for smooth progress; XLSX files parse in one shot with chunked normalize afterward\n- BulkImportModal: incremental validation — editing a cell in step 4 only re-validates that row instead of the entire dataset\n- BulkImportModal: loading state replaces step content during chunked processing — step title, Lottie animation, progress bar, and row counter (e.g. \"5,000 / 50,000 rows parsed\")\n- BulkImportModal: tested against 1M row CSV files\n- New exports: `mapDataRows`, `normalizeRows`, `filterEmptyRows`, `isCsv`, `parseCsvText`, `validateBatch`\n\n### Changed\n\n- BulkImportModal: chunk sizes, max file size, and accepted extensions consolidated into shared `constants.ts` — all files now import from the single source of truth; each processing phase (parse, remap, initial validation, background validation) uses its own named constant\n- BulkImportModal: SelectHeaderRowStep now shows only the first 20 rows for header selection\n- BulkImportModal: ValidateDataStep overscan increased to 10 rows for smoother virtual scrolling\n- Deps: added `lottie-react` for Lottie animation rendering\n\n### Fixed\n\n- BulkImportModal: ValidateDataStep now correctly imports `BACKGROUND_VALIDATION_CHUNK_SIZE` — was `undefined` at runtime, silently falling back to `DEFAULT_CHUNK_SIZE`\n- Modal: confirm dialog overlay portaled to document.body — escapes transform containing block, covers full viewport\n- Modal: discard button uses DialogPrimitive.Close — no longer requires consumers to pass onOpenChange to ModalContent\n- Modal: consumer onInteractOutside and onEscapeKeyDown handlers no longer override internal preventClose confirm flow\n- Modal: added pointer-events: auto to confirm elements to prevent Radix body-level pointer-events: none from disabling them\n- Modal: removed stray debugger statement\n- Tooltip: rebuilt on Radix Popover primitive — content renders in portal, escaping overflow:hidden clipping from parent containers; Radix handles collision detection and viewport boundary awareness\n- BulkImportModal: loading state replaces step content inline instead of separate ProgressModal or portaled overlay\n\n### Removed\n\n- Modal: onOpenChange prop removed from ModalContentProps\n\n## [1.20.0] - 2026-06-28\n\n### Added\n\n- Table: `rowActions` prop — per-row kebab popover rendered internally by `TableContent`. Actions accept `variant` (`\"default\"` | `\"destructive\"`), `condition` filter per row, and `onClick` receives the row object. Popover opens on both kebab click and row right-click\n- Table: Select/Deselect as first item in the row actions popover when `selectable` is true\n- Table: `TableRowAction<T>` type for row-level popover actions\n\n### Changed\n\n- Table: selection is now fully controlled via `selectedIds` prop (renamed from `defaultSelectedIds`). Pass alongside `onSelectionChange` — both `TableContent` and `TableBulkActions` read from the same source\n- Table: `rowKey` now optional — falls back to row index when not provided\n- Table: checkbox column always rendered (collapsed to 0 width when empty, slides open on first selection)\n- Table: removed indeterminate visual from select-all checkbox; partial selection shows unchecked, clicking selects all\n\n### Fixed\n\n- Table: internal selection state never updated — `handleToggleRow`/`handleSelectAll` now properly call `setSelectedIds` via controlled `selectedIds` prop\n- Table: `handleSelectAll` checked type widened to accept Radix `boolean` (removed `\"indeterminate\"` case)\n- Table: `onClear` on `TableBulkActions` now properly resets internal selection via controlled `selectedIds` sync\n- Table: checkbox column animation — added `min-width` to transition for smooth entry\n\n### Removed\n\n- Table: `TableContentInnerProps` — merged into public `TableContentProps`\n- Table: internal selection `useState` — now fully controlled via `selectedIds` prop\n- Docs: removed hallucinated `rowActions`, `bulkActions`, `onRowClick`, `onRowContextMenu` props from TableContent\n- Docs: removed hallucinated right-click context menu section and `TableRowAction` type (previously listed but never existed)\n\n## [1.19.0] - 2026-06-26\n\n### Added\n\n- Table: right-click context menu on rows with built-in Select action and custom `rowActions` prop\n- Table: bulk action support in right-click popover via `bulkActions` prop (shown when multiple rows selected)\n- Table: hidden select column — checkbox column appears only after first row selection\n- Table: `onRowClick` for left-click, `onRowContextMenu` for custom right-click handling\n- Table: `TableRowAction` type for context menu actions\n- Dropdown: rebuilt on Radix Popover (fixes modal + dropdown interaction bug where clicking outside the dropdown inside a modal closed the modal)\n- Command: clear button (XCircle) in input bar when text is entered, matching TableSearch pattern\n\n### Changed\n\n- Table: hover/selected row states via CSS classes (`gsl-table__row--selected`, `gsl-table__row--context-open`)\n- Table: row height uses `var(--gsl-table-row-height, 44px)` token\n- Popover: border-radius uses `var(--gsl-radius-base)` instead of `var(--gsl-radius-xl)`\n\n### Fixed\n\n- Dropdown: clicking outside the dropdown popover inside a Modal no longer closes the Modal — switched from Radix Select to Radix Popover\n\n## [1.18.1] - 2026-06-22\n\n### Fixed\n\n- **AppLayout**: tests now wrap renders in `MemoryRouter` to fix `useSearchParams` is not defined error\n- **Command**: test queries use `document.querySelector` with cmdk `data-value` attribute instead of `value` attribute\n- **Lint**: removed unused imports across 15+ files; replaced `as any` casts with proper TypeScript types across all source files\n\n### Changed\n\n- **DateRangeSelector**: wrapped `range` computation in `useMemo` to prevent unstable reference\n- **Table**: wrapped `columns` and `data` in `useMemo` to avoid new‑array‑on‑every‑render\n- **Sortable**: wrapped `style` object in `useMemo`\n- **useTableState**: wrapped `sort` computation in `useMemo`\n- **useBulkImportFlow**: removed unnecessary dependencies from `canGoNext` dependency array\n\n## [1.18.0] - 2026-06-21\n\n### Breaking\n\n- **AppLayout**: Now wraps `SidebarProvider` and `BreadcrumbProvider` internally. Consumers no longer need to add these providers. Removed `AppBreadcrumb` slot component. Breadcrumbs are context driven, call `useBreadcrumbs()` from any page to set them.\n- **Card**: Removed `header` prop and `CardClassNames`. Use `CardHeader`, `CardTitle`, and `CardActions` sub-components instead.\n\n### Added\n\n- `BreadcrumbProvider`, `useBreadcrumbs`, `useBreadcrumbContext`, `BreadcrumbEntry` exports\n- `CardHeader`, `CardTitle`, `CardActions` sub-components\n- `useConfirmBeforeUnload` hook for browser `beforeunload` confirmation\n- BulkImportModal: browser confirmation prompt when a file has been uploaded but not confirmed\n- BulkImportModal: reset discarded rows button on validate data page\n\n### Changed\n\n- **ValidateDataStep**: replaced `<Table />` dependency with an internal virtualized table using `@tanstack/react-virtual`. Error counts correctly exclude discarded rows.\n- Minor CSS improvements across sidebar, button, modal, field, and table components\n\n## [1.17.0] - 2026-06-20\n\n### Added\n\n- ModalContent `onOpenChange` prop for direct close handler passing\n- BulkImportModal `defaultState` prop (`BulkImportFlowDefaultState`) for flow state preservation across open/close cycles\n- BulkImportModal flow state types exported\n- AppHeader: simplified wrapper with `as` prop for semantic flexibility\n- AppLayout: className and extra props passthrough on layout wrappers\n- SelectHeaderRowStep: responsive single-column header row selection with radio buttons on mobile\n- Table: `TableHeaderCell` with `sortable`, `sortDirection`, `onSort` and sort icon; sticky `TableHeader`\n\n### Changed\n\n- ModalContent: removed nested AlertDialog for close-confirmation (all alert() debug calls cleaned out)\n- UploadField: action button uses `<Button variant=\"primary\">` instead of raw `<button>`\n- BulkImportModal stepper: nav-based step indicator with clickable completed steps; footer/body use ModalFooter/ModalBody\n- UploadField: action button height aligned with Button component sizing (36px to 40px)\n\n### Fixed\n\n- ModalContent: no nested Radix Dialog (caused focus-trap / z-index conflicts)\n- BulkImportModal CSS lint: padding shorthand, rgba to rgb modern notation, ::last-child to :last-child\n- ValidateDataStep memoization\n- Modal test: close-button test updated\n\n## [1.16.1] - 2026-06-19\n\n### Added\n\n- SidebarLink `to` prop: renders as react-router `<Link>` for client-side navigation\n\n### Fixed\n\n- **Peer dependency externalization** — `react-router-dom` and `lucide-react` were bundled inside the library, causing `useLocation()` crash when consuming app ran its own `BrowserRouter`. Both are now externalized from the build and declared as peer dependencies, so the library and app share a single copy from the consumer's node_modules.\n- Tooltip arrow borders now face the correct direction per placement (top, bottom, left, right)\n\n### Changed\n\n- Moved `lucide-react` from `dependencies` to `peerDependencies` (consumer likely has it installed)\n\n## [1.16.0] - 2026-06-19\n\n### Added\n\n- Tooltip component with top/right/bottom/left positioning, arrow indicator, and pure-CSS hover reveal\n- SidebarLink tooltip on collapsed rail: shows label text on hover via Tooltip\n- Modal size variants (`sm`, `md`, `lg`, `xl`, `2xl`) with independently customizable `--gsl-modal-max-width-*` tokens\n- Modal `preventClose` prop: intercepts X, overlay click, and Escape; shows confirmation dialog via AlertDialog\n- Modal centered layout with popover-style border and shadow\n- AppBreadcrumb slot for breadcrumbs in AppLayout\n- Avatar component with initials/name display and configurable size\n- SidebarBadge now uses primary color tokens with overridable CSS variables\n\n### Changed\n\n- BreadcrumbLink: calls preventDefault on click for SPA safety, preserves consumer onClick\n- BulkImportModal exit confirm: uses Button component instead of raw elements (fixes Cancel dismissal)\n- Modal, Popover, App-switcher, Command dialog, Dropdown: unified border-radius to `--gsl-radius-xl` and box-shadow to `--gsl-shadow-md`\n- AppLayout: passes className and extra props through to all layout wrappers\n- AppHeader: simplified to passthrough container\n- AppSidebar/AppBody: simplified to passthrough, layout classes moved to AppLayout wrappers\n- Theme: adds --gsl-z-header token and body background\n\n### Fixed\n\n- Command inline popover: removed Portal wrapper so popover renders in-flow; adds pointer-events: none when closed to prevent blocking clicks\n\n## [1.15.2] - 2026-06-19\n\n### Added\n\n- ThemeProvider `storageKey` prop: persists theme to localStorage across sessions\n\n## [1.15.1] - 2026-06-19\n\n### Changed\n\n- Command: inline list now renders via Radix Popover portal, escaping all parent stacking contexts; z-index and positioning handled by Popover instead of absolute CSS\n\n### Fixed\n\n- Malformed CSS in table checkbox-cell and network-operator rules causing PostCSS \"Unknown word\" errors in consuming projects\n\n### Added\n\n- stylelint with `lint:css` / `lint:css:fix` scripts; wired into `prepublishOnly` to catch CSS syntax errors before publish\n\n## [1.14.0] - 2026-06-18\n\n### Changed\n\n- CommandDialog overlay now fades in/out with animation, matching Dialog and Modal component patterns\n\n## [1.13.2] - 2026-06-18\n\n### Changed\n\n- Toast: restyled with popover background, border, and shadow; action button uses Button secondary sm classes; close button is transparent ghost; title text matches semantic variant color; border-radius xl; glass background with backdrop blur\n\n## [1.13.1] - 2026-06-18\n\n### Added\n\n- `PhoneNumberInput` Zod validation form example with success/failure dialog\n- `UploadField` Zod validation form example with file type, min/max size, and name length checks\n\n### Changed\n\n- CommandDialog: border radius changed from xl to base; inline Command input uses xl\n- CommandDialog input wrapper: focus outline now follows border radius\n- UploadField Zod validation made stricter with minimum file size and filename length checks\n\n### Cleanups\n\n- Shortened all MDX page meta descriptions to 3--5 words\n\n### Fixes\n\n- UploadField Zod form example: dialog styling matches PhoneNumberInput form pattern\n\n## [1.13.0] - 2026-06-18\n\n### Added\n\n- `DateRangeSelector` year/month dropdown selectors for quick navigation\n- `DateRangeSelector` Apply/Cancel confirmation buttons (pending range committed on Apply)\n- `DateRangeSelector` side-by-side two-month calendar layout\n- `DateRangeSelectorClassNames` keys: `calendarFooter`, `applyButton`, `cancelButton`\n\n### Changed\n\n- `DateRangeSelector` uses project `Dropdown` and `Button` components\n- `--gsl-z-select` bumped from 1200 to 1400\n\n## [1.12.0] - 2026-06-18\n\n### Fixed\n\n- TypeScript strict errors in `CodeFigure`, `PopoverExample`, `SidebarExample`, `SortableExample`, and `DateRangeSelector.test`\n- Export `DateRangeValue` type from `DateRangeSelector` component module\n- `onReorder` callback type compatibility in `Sortable` example\n- Union type destructuring for optional `destructive` and `badge` props in docs examples\n\n### Changed\n\n- Replace native inputs with internal `Input` component across `Table`, `ValidateDataStep`, `ThemeToggle`\n- Rename `--gsl-rounded-base` token back to `--gsl-radius-base`\n- Date pickers use `--gsl-z-dropdown` token instead of dedicated `--gsl-z-datepicker`\n- Refactor `DateRangeSelector` and `UploadField` design\n- Add uncontrolled/RHF tests for all input components\n\n## [1.11.0] - 2026-06-18\n\n### Added\n\n- UploadField docs: Zod file validation section with `z.instanceof(File)`, `.refine()` for type/size, and multi-file array example\n\n## [1.10.0] - 2026-06-18\n\n## [1.9.0] - 2026-06-17\n\n### Added\n\n- `AppHeader` compound primitives (`AppHeader`, `AppHeaderActions`, `AppHeaderSearch`, `AppHeaderNotifications`, `AppHeaderProfile`)\n- `AppLayout`, `AppSidebar`, `AppBody` layout container with auto-positioning by `componentId`\n- `Table` compound primitives (`Table`, `TableHeader`, `TableSearch`, `TableFilter`, `TableContent`, `TableFooter`, `TablePagination`) with URL-driven state; `paramPrefix` required; pagination shows \"Showing {'{'}start{'}'} to {'{'}select{'}'} of {'{'}total{'}'}\"\n- `CountrySelector`, `DateSelector`, `DateRangeSelector`, `MetricCard`, `NetworkOperator`, `OtpInput`, `PhoneNumberInput`, `UploadField` input components\n- `Card` surface wrapper (`header`, `body`, `--gsl-surface-card`, `--gsl-card-padding`)\n- `SidebarBrand` sub-component for collapsed-aware brand area\n- `useTablePagination`, `useTableState`, `useTableFilter`, `useDebounce` hooks\n- `DocsLayout` for documentation pages\n- `countries` utility: country list with name, flag emoji, dial code, ISO code\n- CSS tokens `--gsl-surface-dark`, `--gsl-surface-card`, `--gsl-rounded-base`\n- Documentation pages for all new components with interactive examples\n\n### Changed\n\n- Sidebar uses `--gsl-radius-2xl` and `--gsl-surface-dark` for active states; scrollbar hidden; scroll hint internalized\n- CommandGroup loading replaced with skeleton rows\n- `--gsl-radius` renamed to `--gsl-rounded-base`\n- Added `invalid` prop and `aria-invalid` to `Dropdown`\n- `DemoLayout` and demo pages refactored for new components\n- `DateRangeSelector` refactored to single-trigger display with two-click range selection; start always kept before end (auto-swap); self-contained CSS; simplified `placeholder` prop from `{'{'} start, end {'}'}` object to `string`\n\n## [1.8.0] - 2026-06-15\n\n### Added\n\n- `Field` compound primitives (`Field`, `FieldLabel`, `FieldDescription`, `FieldError`, `FieldControl`) for accessible label, helper, and error wiring\n- `Input` and `Textarea` forwardRef controls styled for GSL forms\n- `Form`, `FormField`, and `useFormField` adapters for optional `react-hook-form` integration (peer dependency)\n- Form documentation page at `/docs/form` for `Field`, `Input`, and `Textarea` primitives\n- FormField documentation page at `/docs/form-field` for `Form`, `FormField`, `useFormField`, and Zod validation\n- `zod` and `@hookform/resolvers` documented as optional peer dependencies for schema validation\n- `ToastOptions.icon` for optional leading icons with variant-tinted styling\n- `Draggable` compound primitives (`Draggable`, `DraggableHandle`) and `useDraggable` hook for repositioning panels within parent, window, or custom bounds\n- Draggable documentation page at `/docs/draggable` with props and exported types\n- `Sortable` compound primitives (`Sortable`, `SortableList`, `SortableItem`, `SortableHandle`) and `reorderItems` helper for list reordering via `@dnd-kit`\n- Sortable documentation page at `/docs/sortable` with props and exported types\n- `Toast` primitives (`ToastProvider`, `Toaster`) and `useToast` hook for imperative notifications\n- Toast documentation page at `/docs/toast` with props and exported types\n\n### Changed\n\n- Form docs split into `/docs/form` (Field, Input, Textarea) and `/docs/form-field` (Form, FormField, useFormField, Zod)\n- Dialog, Modal, BulkImportModal, and CommandDialog doc examples and README snippets now use `useDialogSearchParam` / `useModalSearchParam` for URL-driven open state\n- Toast rebuilt on [Sonner](https://sonner.emilkowal.ski/); `ToastProvider`, `Toaster`, and `useToast()` remain the public API; internal queue and styling now delegate to Sonner with GSL `unstyled` class names\n- `useToast().toasts` is deprecated and always returns `[]` (Sonner does not expose its queue)\n- Docs code examples use increased spacing between fenced blocks and inside highlighted code panels\n\n### Removed\n\n- Toast compound Radix parts: `Toast`, `ToastTitle`, `ToastDescription`, `ToastAction`, `ToastClose`, `ToastViewport`, `ToastIcon`\n- Toast reducer utilities: `toastReducer`, `createToastRecord`, `enforceToastLimit`, `TOAST_REMOVE_DELAY`, `createToastId`\n- `ToastProvider` props `swipeDirection` and `label` (not supported by Sonner)\n\n## [1.7.0] - 2026-06-15\n\n### Added\n\n- `Sidebar` compound primitives (`SidebarProvider`, `Sidebar`, `SidebarTrigger`, `SidebarOverlay`, nav parts) with mobile offcanvas drawer\n- `SidebarCollapse` desktop collapse toggle and `SidebarLink` `icon` prop for icon + label rows\n- `SidebarBadge` trailing pill for link counts and labels\n- `Command` compound primitives (`Command`, `CommandDialog`, input/list/item parts) built on cmdk\n- `CommandDialog` `shortcut` prop and `useCommandShortcut` hook for Cmd/Ctrl+K palette toggle\n- `CommandShortcut` trailing key hint chip for command items\n- `CommandInput` search icon and optional shortcut badge (inherits from `CommandDialog` when `shortcut` is set)\n- `CommandGroup` `loading` and `loadingLabel` props for per-group async result loading\n- `formatCommandShortcutLabels` helper and `useCommandDialog` hook\n- `Dialog` compound primitives (`Dialog`, `DialogTrigger`, `DialogPortal`, `DialogOverlay`, `DialogContent`, `DialogTitle`, `DialogDescription`, `DialogClose`) built on Radix Dialog\n- `Modal` compound primitives (`Modal`, `ModalTrigger`, `ModalPortal`, `ModalOverlay`, `ModalContent`, `ModalHeader`, `ModalTitle`, `ModalDescription`, `ModalBody`, `ModalFooter`, `ModalClose`) for near full-viewport shell layouts\n- `Sheet` compound primitives (`Sheet`, `SheetTrigger`, `SheetPortal`, `SheetOverlay`, `SheetContent`, `SheetHeader`, `SheetTitle`, `SheetDescription`, `SheetBody`, `SheetFooter`, `SheetClose`) for edge-sliding panels with `side` (`top`, `right`, `bottom`, `left`)\n- `Badge` component with semantic variants (`default`, `primary`, `success`, `warning`, `error`, `outline`) and `sm` / `md` sizes\n- `Breadcrumb` compound primitives (`Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis`) for hierarchical navigation trails\n- `ProgressBar` component with semantic variants (`default`, `success`, `warning`, `error`), `sm` / `md` sizes, and determinate / indeterminate modes\n- Hooks module: `useSearchParamOverlay`, `useDialogSearchParam`, and `useModalSearchParam` for URL search-param overlay state with flat prefixed data params (`dialog.userId`, etc.), `data`, `openWith`, and `SearchParamOverlayState`; includes `createSearchParamAdapter`, `createBrowserSearchParamAdapter`, and `readOverlayData` / `writeOverlayData` / `clearOverlayData` utilities\n- Dialog documentation page at `/docs/dialog` with props and exported types\n- Modal documentation page at `/docs/modal` with props and exported types\n- Sheet documentation page at `/docs/sheet` with props and exported types\n- Badge documentation page at `/docs/badge` with props and exported types\n- Breadcrumb documentation page at `/docs/breadcrumb` with props and exported types\n- ProgressBar documentation page at `/docs/progress-bar` with props and exported types\n- Hooks documentation page at `/docs/hooks` with URL overlay examples and adapter patterns\n- Sidebar documentation page at `/docs/sidebar` with props and exported types\n- Command documentation page at `/docs/command` with props and exported types\n\n### Changed\n\n- Sidebar desktop appearance: card panel with neutral active state and collapsible icon rail\n- Command inline results render in a floating popover so the search field height stays fixed\n- Command shortcut badges use a single unified chip (e.g. `⌘` `K`) instead of separate key caps per key\n- Command dialog input shows the keyboard shortcut badge automatically when `shortcut` is set\n- Component authoring rule requires full responsive behavior (320px to desktop, touch targets, reduced motion, docs preview checks)\n\n## [1.5.0] - 2026-06-15\n\n### Added\n\n- `RadioGroup` and `Radio` components with optional labels and part-level `classNames`\n- RadioGroup documentation page at `/docs/radio-group` with props and exported types\n- `Radio.description` for optional secondary text under each option label\n- `RadioGroup.variant=\"card\"` choice card layout with bordered selectable surfaces\n- `Tabs` compound primitives (`Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`) with `default` and `line` variants\n- Tabs documentation page at `/docs/tabs` with props and exported types\n\n### Changed\n\n- Tabs panels animate in with a fade and slide on tab switch; disabled when `prefers-reduced-motion: reduce` is set\n- Line variant tabs use a sliding underline indicator instead of per-trigger borders\n- Docs nav, README component sections, and `src/index.ts` exports list components alphabetically\n- Component authoring rule requires alphabetical ordering when adding components to nav, README, and exports\n\n## [1.4.0] - 2026-06-14\n\n### Added\n\n- `Button` component with primary, secondary, outline, and ghost variants, loading spinner, and disabled state\n- `ButtonClassNames` and `classNames` prop for part-level Tailwind/class overrides (`root`, `label`, `spinner`)\n- Button documentation page at `/docs/button` with props and exported types\n- `Checkbox` component with optional label and part-level `classNames`\n- Checkbox documentation page at `/docs/checkbox` with props and exported types\n- `Dropdown` select-style component with clearable support and part-level `classNames`\n- Dropdown documentation page at `/docs/dropdown` with props and exported types\n- `Popover` compound primitives (`Popover`, `PopoverTrigger`, `PopoverContent`, etc.) with styled content surface\n- Popover documentation page at `/docs/popover` with props and exported types\n- Cursor rule `.cursor/rules/gsl-component-authoring.mdc` for component conventions\n\n### Changed\n\n- Radix UI packages moved from peer dependencies to dependencies so a single `npm install @rfdtech/components` is sufficient\n- `FieldMappingSelect` in BulkImportModal now wraps the public `Dropdown` component\n- Dropdown trigger uses Lucide `ChevronDown` instead of a text glyph; adds `lucide-react` as a dependency\n- Popover docs example demonstrates an action menu pattern with `PopoverClose`; adds menu utility classes (`gsl-popover--menu`, `gsl-popover__menu`, `gsl-popover__menu-item`)\n- AppSwitcher app icons render image URLs as round cropped icons\n- `AppSwitcher` is data-only: pass `apps` directly and control loading with the `loading` prop; panel shows a Lucide spinner while loading\n\n### Removed\n\n- `AppSwitcher` remote fetch via `baseUrl` / `accessToken`\n- `useMeApps`, `fetchMeApps`, `MeAppsFetchError`, `buildMeAppsUrl`, `createMeAppsRequestInit`, `mapMeAppToAppItem`, `mapMeAppsToAppItems`\n- `MeApp`, `MeAppsResponse`, `UseMeAppsOptions`, `UseMeAppsReturn`, and `AppItem.metadata`\n\n## [1.3.0] - 2026-06-14\n\n### Added\n\n- `ThemeProvider` and `useTheme()` for light, dark, and system themes\n- Layered theme CSS (`base`, `light`, `dark`) with shared z-index tokens\n- Theme documentation page at `/docs/theme`\n\n### Changed\n\n- Theme CSS loads once from the library entry instead of per-component imports\n- `ThemeProvider` syncs `data-gsl-theme` to `document.documentElement` so portaled modals and popovers inherit the active palette\n\n## [1.2.0] - 2026-06-14\n\n### Added\n\n- Radix UI primitives as peer dependencies (`@radix-ui/react-popover`, `@radix-ui/react-dialog`, `@radix-ui/react-alert-dialog`, `@radix-ui/react-select`, `@radix-ui/react-radio-group`, `@radix-ui/react-slot`)\n\n### Changed\n\n- `AppSwitcher` now uses `@radix-ui/react-popover` for the app grid panel\n- `BulkImportModal` now uses `@radix-ui/react-dialog` and `@radix-ui/react-alert-dialog` for the modal shell and exit confirmation\n- Column mapping in `BulkImportModal` uses `@radix-ui/react-select`\n- Header row selection uses `@radix-ui/react-radio-group`\n\n### Removed\n\n- Custom popover, dialog, and field-mapping dropdown implementations replaced by Radix primitives\n\n## [1.0.3] - 2026-06-12\n\n### Added\n\n- Changelog page in docs (`/docs/changelog`) and root `CHANGELOG.md`\n\n## [1.0.2] - 2026-06-12\n\n### Added\n\n- `DataTable` component with column sorting, pagination, loading state, and empty state\n- `Pagination` component (standalone; composed by `DataTable`)\n- Interactive documentation site at `/docs` with MDX pages, syntax highlighting, and preview/code tabs\n- Demo app routing split into Demo and Docs pages with a sectioned docs sidebar\n\n### Changed\n\n- `DataTable` is static-only: parents pass `data` and control fetching with a `loading` prop (async `loadData` removed)\n- Demo app refactored into `DemoPage` and `DocsPage` with shared `DemoLayout`\n\n## [1.0.0] - 2026-06-11\n\n### Added\n\n- `Dropdown` single-value select component\n- `DropdownMenu` action menu component\n- `Combobox` async search component with debounced `loadOptions`\n- `prepublishOnly` script to run typecheck, tests, and build before publishing\n\n### Changed\n\n- `BulkImportModal` column mapping uses an internal field-mapping dropdown\n- README expanded with component usage and style import instructions\n\n### Removed\n\n- `Dropdown`, `Combobox`, `DropdownMenu`, `DataTable`, and `Pagination` components\n\n## [0.4.0] - 2026-06-11\n\n### Added\n\n- `AppSwitcher` component for switching between GSL systems\n- `BulkImportModal` multi-step import flow with editable rows and exit confirmation\n\n### Changed\n\n- Package renamed to `@rfdtech/components`\n- Bulk import stepper animations, accessibility, and responsive styling improvements",
550
550
  "headings": [
551
551
  "Changelog",
552
552
  "[Unreleased]",
553
+ "[2.1.0] - 2026-07-25",
554
+ "Added",
555
+ "Changed",
556
+ "Fixed",
557
+ "[2.0.2] - 2026-07-23",
558
+ "Changed",
559
+ "Fixed",
560
+ "[2.0.1] - 2026-07-16",
553
561
  "Added",
554
562
  "Breaking",
555
563
  "Deprecated",
@@ -725,7 +733,7 @@
725
733
  ],
726
734
  "import": "@rfdtech/components"
727
735
  },
728
- "docs": "# Combobox\n\nSearchable option list built on `cmdk`, opened from a trigger button (unlike `Dropdown`, which is Radix `Select`-based and has no search). Supports single selection, multi-selection with checkboxes, and a per-option leading icon adornment.\n\n## Example\n\n<ExampleTabs\n title=\"Single select with search\"\n preview={<ComboboxPreview />}\n code={comboboxSingleSource}\n/>\n\n<ExampleTabs\n title=\"Multi-select with checkboxes\"\n preview={<ComboboxMultiPreview />}\n code={comboboxMultiSource}\n/>\n\n<ExampleTabs\n title=\"Per-option leading icon\"\n preview={<ComboboxIconsPreview />}\n code={comboboxIconsSource}\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `options` | `ComboboxOption[]` | — | The option list |\n| `multiple` | `boolean` | `false` | Enables multi-select mode; changes `value`/`onValueChange` to arrays |\n| `value` | `string \\| null` (single) or `string[]` (multi) | — | Controlled selection |\n| `onValueChange` | `(value) => void` | — | Fires on selection change |\n| `placeholder` | `string` | `\"Select\"` | Trigger text when nothing is selected |\n| `searchPlaceholder` | `string` | `\"Search...\"` | Search input placeholder |\n| `disabled` | `boolean` | `false` | Disables the trigger |\n| `invalid` | `boolean` | `false` | Error styling + `aria-invalid` |\n| `clearable` | `boolean` | `false` | Shows a clear control when a selection exists |\n| `emptyMessage` | `ReactNode` | `\"No results\"` | Shown when search matches nothing |\n| `aria-label` | `string` | — | Accessible name for the trigger button |\n| `classNames` | `ComboboxClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root wrapper |\n| `name` | `string` | — | Renders a hidden input for native form submission (comma-joined in multi mode) |\n\n## Types\n\n### `ComboboxProps`\n\nA discriminated union on `multiple` — when `multiple: true`, `value`/`onValueChange` are typed as `string[]`; otherwise `string | null`.\n\n### `ComboboxOption`\n\n| Key | Type | Description |\n|-----|------|--------------|\n| `value` | `string` | Unique option value |\n| `label` | `string` | Display label; also used for search matching |\n| `disabled` | `boolean?` | Excludes the option from selection |\n| `icon` | `ReactNode?` | Leading adornment (e.g. a star icon) |\n\n### `ComboboxClassNames`\n\nPart-level class names merged after base `clet-combobox*` styles:\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Outer wrapper `<span>` |\n| `trigger` | Trigger `<button>` |\n| `content` | Popover content |\n| `input` | Search `<input>` |\n| `list` | Option list |\n| `item` | Each option row |\n| `itemIcon` | Leading icon wrapper |\n| `itemCheck` | Trailing check/checkbox indicator |\n| `empty` | \"No results\" message |\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-combobox-trigger-height` | `40px` | Trigger button height |\n| `--clet-combobox-radius` | `var(--clet-radius-base)` | Trigger and popover corner radius |\n| `--clet-combobox-font` | `var(--clet-font-body)` | Font family |\n| `--clet-combobox-label-size` | `var(--clet-text-size-base)` | Trigger/search/item font size |\n| `--clet-combobox-menu-max-height` | `320px` | Max height of the scrollable option list |\n\n## Notes\n\n- Single-select closes the popover on selection; multi-select keeps it open so several options can be toggled in one pass, matching the checkbox-row pattern.\n- Search matches against each option's `label` via cmdk's `keywords`, while `value` stays the stable identifier passed to `onValueChange` — labels don't need to be unique for selection to work correctly.\n- The multi-select checkbox visual reuses the same checked-state colors as `Checkbox` (`--clet-primary` background, white check); single-select shows a plain check mark instead of a checkbox box.\n- The internal search input already carries the ARIA `combobox` role (from `cmdk`); the trigger button intentionally uses `aria-haspopup=\"listbox\"` instead, to avoid two conflicting `combobox` roles being present at once when open.",
736
+ "docs": "# Combobox\n\nSearchable option list built on `cmdk`, opened from a trigger button (unlike `Dropdown`, which is Radix `Select`-based and has no search). Supports single selection, multi-selection with checkboxes, and a per-option leading icon adornment.\n\n## Example\n\n<ExampleTabs\n title=\"Single select with search\"\n preview={<ComboboxPreview />}\n code={comboboxSingleSource}\n/>\n\n<ExampleTabs\n title=\"Multi-select with checkboxes\"\n preview={<ComboboxMultiPreview />}\n code={comboboxMultiSource}\n/>\n\n<ExampleTabs\n title=\"Per-option leading icon\"\n preview={<ComboboxIconsPreview />}\n code={comboboxIconsSource}\n/>\n\n<ExampleTabs\n title=\"Long list (scrolls past 320px)\"\n preview={<ComboboxLongPreview />}\n code={comboboxLongSource}\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `options` | `ComboboxOption[]` | — | The option list |\n| `multiple` | `boolean` | `false` | Enables multi-select mode; changes `value`/`onValueChange` to arrays |\n| `value` | `string \\| null` (single) or `string[]` (multi) | — | Controlled selection |\n| `onValueChange` | `(value) => void` | — | Fires on selection change |\n| `placeholder` | `string` | `\"Select\"` | Trigger text when nothing is selected |\n| `searchPlaceholder` | `string` | `\"Search...\"` | Search input placeholder |\n| `disabled` | `boolean` | `false` | Disables the trigger |\n| `invalid` | `boolean` | `false` | Error styling + `aria-invalid` |\n| `clearable` | `boolean` | `false` | Shows a clear control when a selection exists |\n| `emptyMessage` | `ReactNode` | `\"No results\"` | Shown when search matches nothing |\n| `aria-label` | `string` | — | Accessible name for the trigger button |\n| `classNames` | `ComboboxClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root wrapper |\n| `name` | `string` | — | Renders a hidden input for native form submission (comma-joined in multi mode) |\n\n## Types\n\n### `ComboboxProps`\n\nA discriminated union on `multiple` — when `multiple: true`, `value`/`onValueChange` are typed as `string[]`; otherwise `string | null`.\n\n### `ComboboxOption`\n\n| Key | Type | Description |\n|-----|------|--------------|\n| `value` | `string` | Unique option value |\n| `label` | `string` | Display label; also used for search matching |\n| `disabled` | `boolean?` | Excludes the option from selection |\n| `icon` | `ReactNode?` | Leading adornment (e.g. a star icon) |\n\n### `ComboboxClassNames`\n\nPart-level class names merged after base `clet-combobox*` styles:\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Outer wrapper `<span>` |\n| `trigger` | Trigger `<button>` |\n| `content` | Popover content |\n| `input` | Search `<input>` |\n| `list` | Option list |\n| `item` | Each option row |\n| `itemIcon` | Leading icon wrapper |\n| `itemCheck` | Trailing check/checkbox indicator |\n| `empty` | \"No results\" message |\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-combobox-trigger-height` | `40px` | Trigger button height |\n| `--clet-combobox-radius` | `var(--clet-radius-base)` | Trigger and popover corner radius |\n| `--clet-combobox-font` | `var(--clet-font-body)` | Font family |\n| `--clet-combobox-label-size` | `var(--clet-text-size-base)` | Trigger/search/item font size |\n| `--clet-combobox-menu-max-height` | `320px` | Max height of the scrollable option list |\n\n## Notes\n\n- Single-select closes the popover on selection; multi-select keeps it open so several options can be toggled in one pass, matching the checkbox-row pattern.\n- Search matches against each option's `label` via cmdk's `keywords`, while `value` stays the stable identifier passed to `onValueChange` — labels don't need to be unique for selection to work correctly.\n- The multi-select checkbox visual reuses the same checked-state colors as `Checkbox` (`--clet-primary` background, white check); single-select shows a plain check mark instead of a checkbox box.\n- The internal search input already carries the ARIA `combobox` role (from `cmdk`); the trigger button intentionally uses `aria-haspopup=\"listbox\"` instead, to avoid two conflicting `combobox` roles being present at once when open.",
729
737
  "headings": [
730
738
  "Combobox",
731
739
  "Example",
@@ -737,7 +745,7 @@
737
745
  "Design tokens",
738
746
  "Notes"
739
747
  ],
740
- "types": "import type { ReactNode } from \"react\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading icon/adornment rendered before the label (e.g. a star). */\n icon?: ReactNode;\n}\n\nexport interface ComboboxClassNames {\n root?: string;\n trigger?: string;\n content?: string;\n input?: string;\n list?: string;\n item?: string;\n itemIcon?: string;\n itemCheck?: string;\n empty?: string;\n}\n\ninterface ComboboxBaseProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n disabled?: boolean;\n invalid?: boolean;\n clearable?: boolean;\n emptyMessage?: ReactNode;\n \"aria-label\"?: string;\n classNames?: ComboboxClassNames;\n className?: string;\n name?: string;\n}\n\nexport type ComboboxProps =\n | (ComboboxBaseProps & {\n multiple?: false;\n value: string | null;\n onValueChange: (value: string | null) => void;\n })\n | (ComboboxBaseProps & {\n multiple: true;\n value: string[];\n onValueChange: (value: string[]) => void;\n });\n",
748
+ "types": "import type { ReactNode } from \"react\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading icon/adornment rendered before the label (e.g. a star). */\n icon?: ReactNode;\n}\n\nexport interface ComboboxClassNames {\n root?: string;\n trigger?: string;\n content?: string;\n input?: string;\n list?: string;\n item?: string;\n itemIcon?: string;\n itemCheck?: string;\n empty?: string;\n loading?: string;\n}\n\ninterface ComboboxBaseProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n disabled?: boolean;\n invalid?: boolean;\n clearable?: boolean;\n loading?: boolean;\n loadingLabel?: string;\n emptyMessage?: ReactNode;\n \"aria-label\"?: string;\n classNames?: ComboboxClassNames;\n className?: string;\n name?: string;\n}\n\nexport type ComboboxProps =\n | (ComboboxBaseProps & {\n multiple?: false;\n value: string | null;\n onValueChange: (value: string | null) => void;\n })\n | (ComboboxBaseProps & {\n multiple: true;\n value: string[];\n onValueChange: (value: string[]) => void;\n });\n",
741
749
  "examples": [
742
750
  {
743
751
  "file": "combobox-icons.example.tsx",
@@ -748,6 +756,15 @@
748
756
  ],
749
757
  "source": "import { Combobox } from \"@rfdtech/components\";\nimport { Star } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst options = [\n { value: \"1\", label: \"Option 1\", icon: <Star size={16} /> },\n { value: \"2\", label: \"Option 2\", icon: <Star size={16} /> },\n { value: \"3\", label: \"Option 3\", icon: <Star size={16} /> },\n];\n\nexport function ComboboxIconsExample() {\n const [value, setValue] = useState<string[]>([\"1\", \"2\"]);\n\n return (\n <Combobox\n aria-label=\"Select options\"\n placeholder=\"Select\"\n options={options}\n value={value}\n onValueChange={setValue}\n multiple\n />\n );\n}\n"
750
758
  },
759
+ {
760
+ "file": "combobox-long.example.tsx",
761
+ "importName": "comboboxLongSource",
762
+ "title": "Combobox Long",
763
+ "components": [
764
+ "Combobox"
765
+ ],
766
+ "source": "import { Combobox } from \"@rfdtech/components\";\nimport { useState } from \"react\";\n\nconst options = Array.from({ length: 20 }, (_, i) => ({\n value: String(i + 1),\n label: `Option ${i + 1}`,\n}));\n\nexport function ComboboxLongExample() {\n const [value, setValue] = useState<string | null>(null);\n\n return (\n <Combobox\n aria-label=\"Select\"\n placeholder=\"Select\"\n options={options}\n value={value}\n onValueChange={setValue}\n clearable\n />\n );\n}\n"
767
+ },
751
768
  {
752
769
  "file": "combobox-multi.example.tsx",
753
770
  "importName": "comboboxMultiSource",
@@ -904,6 +921,48 @@
904
921
  ],
905
922
  "ruleIds": []
906
923
  },
924
+ "dashboard-patterns": {
925
+ "meta": {
926
+ "slug": "dashboard-patterns",
927
+ "title": "Dashboard patterns",
928
+ "description": "Composing a dashboard with PageSection, QuickActions, Tabs, Timeline, and Stepper",
929
+ "category": "Guide",
930
+ "keywords": [
931
+ "dashboard",
932
+ "page composition",
933
+ "page section",
934
+ "quick actions",
935
+ "tabs",
936
+ "timeline",
937
+ "stepper",
938
+ "multi-step"
939
+ ],
940
+ "since": "2.1.0",
941
+ "status": "guide",
942
+ "related": [
943
+ "page-section",
944
+ "quick-actions",
945
+ "section-header",
946
+ "tabs",
947
+ "timeline",
948
+ "stepper",
949
+ "metric-card"
950
+ ]
951
+ },
952
+ "docs": "# Dashboard patterns\n\nHow to compose a modern dashboard page using `@rfdtech/components`.\n\n## Section layout with PageSection\n\nEvery distinct content area on a page is wrapped in `<PageSection>`, which applies a uniform\n`gap: var(--clet-app-layout-body-gap)` between sections. This replaces hand-rolled `<div>`s\nwith ad-hoc margins.\n\n```tsx\n<>\n <PageSection>\n <SectionHeader>...</SectionHeader>\n </PageSection>\n\n <PageSection>\n <QuickActions actions={[...]} customizable />\n </PageSection>\n\n <PageSection>\n <MetricCardsGrid />\n </PageSection>\n\n <PageSection>\n <Card bordered>\n <Tabs variant=\"pill\">...</Tabs>\n </Card>\n </PageSection>\n</>\n```\n\n## Quick actions\n\nPlace a `QuickActions` section between the page header and the main content. Each action tile\nshows an icon, label, and optional description. The \"Customize\" button lets users toggle\nvisibility — manage the hidden IDs externally and persist them however you like.\n\n```tsx\n<QuickActions\n title=\"Quick actions\"\n actions={allActions}\n customizable\n hiddenIds={hiddenIds}\n onToggleVisibility={handleToggle}\n onAction={handleAction}\n/>\n```\n\n## Tabbed content with pill tabs\n\nUse `Tabs variant=\"pill\"` (the new design system preferred variant) inside a bordered `Card`\nto organize related tables and views:\n\n```tsx\n<Card bordered>\n <Tabs variant=\"pill\" defaultValue=\"members\">\n <TabsList>\n <TabsTrigger value=\"members\">Members</TabsTrigger>\n <TabsTrigger value=\"audit\">Audit Trail</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"members\">\n <Table>\n <TableHeader>...</TableHeader>\n <TableContent variant=\"panel\" ... />\n <TableFooter noBorder>...</TableFooter>\n </Table>\n </TabsContent>\n\n <TabsContent value=\"audit\">\n <Timeline>\n <TimelineItem mode=\"success\">\n <TimelineTitle>Event title</TimelineTitle>\n <TimelineData>Date</TimelineData>\n <TimelineFooter>Description</TimelineFooter>\n </TimelineItem>\n </Timeline>\n </TabsContent>\n </Tabs>\n</Card>\n```\n\n## Audit trail with Timeline\n\nThe `Timeline` component renders a vertical event log with colored dots and animated connecting\nlines. Use it inside a TabsContent or standalone:\n\n| Mode | Use when |\n|------|----------|\n| `success` | Completed actions (user created, account reinstated) |\n| `primary` | Informational updates (profile changed, permissions modified) |\n| `warning` | Anomalies or pending items (login anomaly, access revoked) |\n| `error` | Critical events (account suspended) |\n| `muted` | Routine system events (scheduled maintenance) |\n\n## Multi-step form with Stepper\n\nFor flows like user creation, use `Stepper` with per-step zod validation. Each step validates\nits own fields before allowing navigation forward. Completed steps are clickable for navigation\nback.\n\n```tsx\n<Stepper value={step} clickable onValueChange={handleStepClick}>\n <Step value={1}><StepLabel>Personal Info</StepLabel></Step>\n <Step value={2}><StepLabel>Contact Details</StepLabel></Step>\n <Step value={3}><StepLabel>Role & Permissions</StepLabel></Step>\n <Step value={4}><StepLabel>Review</StepLabel></Step>\n</Stepper>\n```\n\nUse `form.trigger(fields)` per step to validate only the current step's fields before advancing.\n\n## Checklist\n\nWhen building a new dashboard page:\n\n- [ ] Wrap every section in `<PageSection>` — no bare `<div>`s with margin\n- [ ] Use `SectionHeader`/`SectionTitle`/`SectionDescription` for the page header\n- [ ] Add a `QuickActions` section after the header with relevant actions\n- [ ] Use `MetricCard variant=\"outline\"` for metric grids\n- [ ] Wrap tabbed content in `Card bordered` + `Tabs variant=\"pill\"`\n- [ ] Inside tabs: `TableContent variant=\"panel\"` + `TableFilter variant=\"spread\"` + `TableFooter noBorder`\n- [ ] Use `Timeline` for audit trail / activity logs\n- [ ] Use `Stepper` with zod per-step validation for multi-step forms",
953
+ "headings": [
954
+ "Dashboard patterns",
955
+ "Section layout with PageSection",
956
+ "Quick actions",
957
+ "Tabbed content with pill tabs",
958
+ "Audit trail with Timeline",
959
+ "Multi-step form with Stepper",
960
+ "Checklist"
961
+ ],
962
+ "types": null,
963
+ "examples": [],
964
+ "ruleIds": []
965
+ },
907
966
  "date-range-selector": {
908
967
  "meta": {
909
968
  "slug": "date-range-selector",
@@ -1010,7 +1069,7 @@
1010
1069
  ],
1011
1070
  "import": "@rfdtech/components"
1012
1071
  },
1013
- "docs": "# DateSelector\n\nDate picker for choosing a single date from a calendar grid. Built on `@radix-ui/react-popover` with CLET styling, month/year navigation arrows, weekday headers, and native `Date.toLocaleDateString` formatting.\n\n## Example\n\n<ExampleTabs title=\"Variants\" preview={<DateSelectorPreview />} code={dateSelectorSource} />\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `Date \\| null` | — | Controlled selected date |\n| `defaultValue` | `Date \\| null` | — | Default date (uncontrolled) |\n| `onChange` | `(date: Date \\| null) => void` | — | Selection handler |\n| `min` | `Date` | — | Minimum selectable date (inclusive) |\n| `max` | `Date` | — | Maximum selectable date (inclusive) |\n| `formatOptions` | `Intl.DateTimeFormatOptions` | — | Options for `Date.toLocaleDateString()` formatting |\n| `placeholder` | `string` | `\"Select date\"` | Trigger text when no date is selected |\n| `invalid` | `boolean` | `false` | Show error border styling |\n| `disabled` | `boolean` | `false` | Disable interaction |\n| `name` | `string` | — | Form field name (for react-hook-form) |\n| `onBlur` | `() => void` | — | Called when the trigger loses focus |\n| `classNames` | `DateSelectorClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root element |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## Usage with react-hook-form\n\n```tsx\nimport { useForm } from \"react-hook-form\";\nimport { Form, FormField, Field, FieldControl, FieldLabel, FieldError, DateSelector } from \"@rfdtech/components\";\n\nfunction DateForm() {\n const form = useForm<{ startDate: Date | null }>({ defaultValues: { startDate: null } });\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit((d) => console.log(d.startDate))}>\n <FormField\n control={form.control}\n name=\"startDate\"\n rules={{ required: \"Start date is required\" }}\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Start date</FieldLabel>\n <FieldControl>\n <DateSelector value={field.value} onChange={field.onChange} />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n </form>\n </Form>\n );\n}\n```\n\n## Types\n\n### `DateSelectorProps`\n\nSee the props table above.\n\n### `DateSelectorClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root wrapper (`.clet-date-selector`) |\n| `trigger` | Trigger button (`.clet-date-selector__trigger`) |\n| `calendar` | Calendar panel (`.clet-date-selector__calendar`) |\n| `calendarHeader` | Month/year header row (`.clet-date-selector__calendar-header`) |\n| `calendarTitle` | Month/year text (`.clet-date-selector__calendar-title`) |\n| `calendarNav` | Navigation buttons (`.clet-date-selector__calendar-nav`) |\n| `calendarWeekdays` | Weekday row (`.clet-date-selector__calendar-weekdays`) |\n| `calendarWeekday` | Individual weekday label (`.clet-date-selector__calendar-weekday`) |\n| `calendarGrid` | Day grid (`.clet-date-selector__calendar-grid`) |\n| `calendarDay` | Day cell (`.clet-date-selector__calendar-day`) |\n\n## Design tokens\n\n| Token | Used for |\n|-------|----------|\n| `--clet-radius-base` | Trigger and calendar border radius |\n| `--clet-date-selector-font` | Font family for the trigger |\n| `--clet-date-selector-calendar-font` | Font family for the calendar popover and day cells |\n| `--clet-date-selector-trigger-height` | Trigger button height (`40px`) |\n| `--clet-date-selector-calendar-min-width` | Calendar popover minimum width (`256px`) |\n| `--clet-date-selector-calendar-padding` | Calendar popover inner padding (`12px`) |\n| `--clet-date-selector-nav-size` | Prev/next month nav button size (`32px`) |\n| `--clet-date-selector-weekday-height` | Weekday label row height (`32px`) |\n| `--clet-text-size-base` | Trigger and month-title font size (`14px`) |\n| `--clet-text-size-xs` | Weekday label font size (`12px`) |\n| `--clet-text-size-sm` | Day cell font size (`13px`) |\n\n## Notes\n\n- Formatting uses `Date.toLocaleDateString()` — locale-aware and does not require a date library.\n- Navigation arrows move by month. Clicking the month/year text resets to today.\n- Days from adjacent months are shown muted and selectable.\n- The popover calendar uses `--clet-shadow-md` and matches the trigger width.\n\n## Don't\n\n- Override borders with `!border-0`/`!shadow-none` style class overrides — let it keep its default border, like Dropdown.",
1072
+ "docs": "# DateSelector\n\nDate picker for choosing a single date from a calendar grid. Built on `@radix-ui/react-popover` with CLET styling, 3-level picker (day / month / year) with clickable header navigation, weekday headers, and native `Date.toLocaleDateString` formatting.\n\n## Example\n\n<ExampleTabs title=\"Variants\" preview={<DateSelectorPreview />} code={dateSelectorSource} />\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `Date \\| null` | — | Controlled selected date |\n| `defaultValue` | `Date \\| null` | — | Default date (uncontrolled) |\n| `onChange` | `(date: Date \\| null) => void` | — | Selection handler |\n| `min` | `Date` | — | Minimum selectable date (inclusive) |\n| `max` | `Date` | — | Maximum selectable date (inclusive) |\n| `formatOptions` | `Intl.DateTimeFormatOptions` | — | Options for `Date.toLocaleDateString()` formatting |\n| `placeholder` | `string` | `\"Select date\"` | Trigger text when no date is selected |\n| `invalid` | `boolean` | `false` | Show error border styling |\n| `disabled` | `boolean` | `false` | Disable interaction |\n| `name` | `string` | — | Form field name (for react-hook-form) |\n| `onBlur` | `() => void` | — | Called when the trigger loses focus |\n| `classNames` | `DateSelectorClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root element |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## Usage with react-hook-form\n\n```tsx\nimport { useForm } from \"react-hook-form\";\nimport { Form, FormField, Field, FieldControl, FieldLabel, FieldError, DateSelector } from \"@rfdtech/components\";\n\nfunction DateForm() {\n const form = useForm<{ startDate: Date | null }>({ defaultValues: { startDate: null } });\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit((d) => console.log(d.startDate))}>\n <FormField\n control={form.control}\n name=\"startDate\"\n rules={{ required: \"Start date is required\" }}\n render={({ field, fieldState }) => (\n <Field invalid={!!fieldState.error}>\n <FieldLabel>Start date</FieldLabel>\n <FieldControl>\n <DateSelector value={field.value} onChange={field.onChange} />\n </FieldControl>\n <FieldError>{fieldState.error?.message}</FieldError>\n </Field>\n )}\n />\n </form>\n </Form>\n );\n}\n```\n\n## Types\n\n### `DateSelectorProps`\n\nSee the props table above.\n\n### `DateSelectorClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root wrapper (`.clet-date-selector`) |\n| `trigger` | Trigger button (`.clet-date-selector__trigger`) |\n| `calendar` | Calendar panel (`.clet-date-selector__calendar`) |\n| `calendarHeader` | Month/year header row (`.clet-date-selector__calendar-header`) |\n| `calendarTitle` | Month/year text (`.clet-date-selector__calendar-title`) |\n| `calendarNav` | Navigation buttons (`.clet-date-selector__calendar-nav`) |\n| `calendarWeekdays` | Weekday row (`.clet-date-selector__calendar-weekdays`) |\n| `calendarWeekday` | Individual weekday label (`.clet-date-selector__calendar-weekday`) |\n| `calendarGrid` | Day grid (`.clet-date-selector__calendar-grid`) |\n| `calendarDay` | Day cell (`.clet-date-selector__calendar-day`) |\n| `calendarMonthGrid` | Month grid (`.clet-date-selector__calendar-month-grid`) |\n| `calendarMonth` | Month cell (`.clet-date-selector__calendar-month`) |\n| `calendarYearGrid` | Year grid (`.clet-date-selector__calendar-year-grid`) |\n| `calendarYear` | Year cell (`.clet-date-selector__calendar-year`) |\n\n## Design tokens\n\n| Token | Used for |\n|-------|----------|\n| `--clet-radius-base` | Trigger and calendar border radius |\n| `--clet-date-selector-font` | Font family for the trigger |\n| `--clet-date-selector-calendar-font` | Font family for the calendar popover and day cells |\n| `--clet-date-selector-trigger-height` | Trigger button height (`40px`) |\n| `--clet-date-selector-calendar-min-width` | Calendar popover minimum width (`256px`) |\n| `--clet-date-selector-calendar-padding` | Calendar popover inner padding (`12px`) |\n| `--clet-date-selector-nav-size` | Prev/next month nav button size (`32px`) |\n| `--clet-date-selector-weekday-height` | Weekday label row height (`32px`) |\n| `--clet-text-size-base` | Trigger and month-title font size (`14px`) |\n| `--clet-text-size-xs` | Weekday label font size (`12px`) |\n| `--clet-text-size-sm` | Day cell font size (`13px`) |\n\n## Notes\n\n- Formatting uses `Date.toLocaleDateString()` — locale-aware and does not require a date library.\n- Title button cycles through a 3-level picker: click the header in day view to open the month grid, click again for the year grid. Click a month or year to return to the previous level. In year view the header shows a decade range (e.g. `2017 – 2028`) and arrows navigate by 12 years.\n- Navigation arrows adapt to the current view: month step in day view, year step in month view, decade step in year view.\n- Days from adjacent months are shown muted and selectable.\n- The popover calendar uses `--clet-shadow-md` and matches the trigger width.\n- The popover always re-opens in day view, reset to the selected date's period (or today).\n\n## Don't\n\n- Override borders with `!border-0`/`!shadow-none` style class overrides — let it keep its default border, like Dropdown.",
1014
1073
  "headings": [
1015
1074
  "DateSelector",
1016
1075
  "Example",
@@ -1023,7 +1082,7 @@
1023
1082
  "Notes",
1024
1083
  "Don't"
1025
1084
  ],
1026
- "types": "export interface DateSelectorClassNames {\n root?: string;\n trigger?: string;\n calendar?: string;\n calendarHeader?: string;\n calendarTitle?: string;\n calendarNav?: string;\n calendarWeekdays?: string;\n calendarWeekday?: string;\n calendarGrid?: string;\n calendarDay?: string;\n}\n\nexport interface DateSelectorProps {\n /** Controlled selected date */\n value?: Date | null;\n /** Uncontrolled default date */\n defaultValue?: Date;\n /** Called when a date is selected (null when cleared) */\n onChange?: (date: Date | null) => void;\n /** Placeholder text shown when no date is selected */\n placeholder?: string;\n invalid?: boolean;\n disabled?: boolean;\n /** Minimum selectable date (inclusive) */\n min?: Date;\n /** Maximum selectable date (inclusive) */\n max?: Date;\n /** Intl.DateTimeFormat options for displaying the selected date */\n formatOptions?: Intl.DateTimeFormatOptions;\n classNames?: DateSelectorClassNames;\n className?: string;\n /** Form field name (for RHF integration) */\n name?: string;\n /** Called when the trigger loses focus */\n onBlur?: () => void;\n}\n",
1085
+ "types": "export interface DateSelectorClassNames {\n root?: string;\n trigger?: string;\n calendar?: string;\n calendarHeader?: string;\n calendarTitle?: string;\n calendarNav?: string;\n calendarWeekdays?: string;\n calendarWeekday?: string;\n calendarGrid?: string;\n calendarDay?: string;\n calendarMonthGrid?: string;\n calendarMonth?: string;\n calendarYearGrid?: string;\n calendarYear?: string;\n}\n\nexport interface DateSelectorProps {\n /** Controlled selected date */\n value?: Date | null;\n /** Uncontrolled default date */\n defaultValue?: Date;\n /** Called when a date is selected (null when cleared) */\n onChange?: (date: Date | null) => void;\n /** Placeholder text shown when no date is selected */\n placeholder?: string;\n invalid?: boolean;\n disabled?: boolean;\n /** Minimum selectable date (inclusive) */\n min?: Date;\n /** Maximum selectable date (inclusive) */\n max?: Date;\n /** Intl.DateTimeFormat options for displaying the selected date */\n formatOptions?: Intl.DateTimeFormatOptions;\n classNames?: DateSelectorClassNames;\n className?: string;\n /** Form field name (for RHF integration) */\n name?: string;\n /** Called when the trigger loses focus */\n onBlur?: () => void;\n}\n",
1027
1086
  "examples": [
1028
1087
  {
1029
1088
  "file": "date-selector.example.tsx",
@@ -1587,7 +1646,7 @@
1587
1646
  "meta": {
1588
1647
  "slug": "launchpad",
1589
1648
  "title": "Launchpad",
1590
- "description": "Constrained 9-app launcher with a built-in expand button + required role switcher",
1649
+ "description": "Constrained 9-app launcher with a top-right expand button + a See more line under the grid + required role switcher",
1591
1650
  "category": "Components",
1592
1651
  "keywords": [
1593
1652
  "launchpad",
@@ -1606,7 +1665,7 @@
1606
1665
  ],
1607
1666
  "import": "@rfdtech/components"
1608
1667
  },
1609
- "docs": "# Launchpad\n\nA purpose-built \"app directory\" launcher for switching between CLET systems — a 3×3 grid of up to 9 apps, with a small ghost expand button at the top right of the panel and a required role switcher below the grid. The expand button's icon is always visible; its \"See more\" label only appears once `apps` overflows the 9-app cap. Clicking it opens a scaled-up expanded view of the same tile system a borderless, `--clet-radius-2xl` modal with a 5-column grid of every app in `apps` (uncapped). It grew out of the same underlying popover/grid idea as [`AppSwitcher`](/docs/app-switcher), with a much smaller, more opinionated API and every icon rendered on a fixed brand tile system with no escape hatch.\n\nPrefer `Launchpad` for all new work — `AppSwitcher` is deprecated and no longer maintained. `Launchpad` doesn't (yet) cover every `AppSwitcher` capability (flexible column count, an uncapped or differently-capped grid, a fully custom `footer`); if your case genuinely needs one of those, check with the team before reaching for the deprecated component.\n\n## Examples\n\n<ExampleTabs\n title=\"With apps, expand button, and a role switcher\"\n preview={<LaunchpadPreview />}\n code={launchpadSource}\n canvasClassName=\"demo-docs__preview-canvas--launchpad\"\n/>\n\n<ExampleTabs\n title=\"Loading state\"\n preview={<LaunchpadLoadingPreview />}\n code={launchpadLoadingSource}\n canvasClassName=\"demo-docs__preview-canvas--launchpad\"\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `apps` | `LaunchpadApp[]` | — | Apps to show. Popover grid is capped at 9 — not configurable; the built-in expand modal shows every app in `apps`, uncapped. |\n| `loading` | `boolean` | `false` | Shows a spinner (only — no text) in the panel instead of the grid |\n| `onAppSelect` | `(app: LaunchpadApp) => void` | — | Called when an app is selected, from either the popover grid or the expanded modal |\n| `children` | `ReactElement<RoleSelectProps>` | — (required) | A [`RoleSelect`](/docs/role-select) rendered below the grid, outside the scrollable/masked grid area and separated by a divider — only a `RoleSelect` element is accepted, and omitting it is a type error |\n| `open` | `boolean` | — | Controlled open state (the popover; the expand modal manages its own state internally) |\n| `onOpenChange` | `(open: boolean) => void` | — | Open state callback |\n| `trigger` | `ReactNode` | round 9-dot icon | Custom trigger element — still wrapped with the \"Open Launchpad\" tooltip/accessible name |\n| `className` | `string` | — | Root CSS class |\n| `style` | `CSSProperties` | — | Root inline styles |\n\nFixed, not configurable (no prop, no override): the popover title and the expanded modal's title both always read **\"Launchpad\"** (both `--clet-font-header`), the trigger's accessible name / hover tooltip always reads **\"Open Launchpad\"**, and the top-right expand button (always rendered, no prop to hide it) always opens the expanded modal — its icon always shows; its \"See more\" label only shows once `apps.length` exceeds the 9-app cap. Dropped relative to `AppSwitcher`: `columns` (fixed at 3, or 5 in the expanded modal), `maxItems` (fixed at 9 for the popover, uncapped in the modal), `placement` (fixed `bottom-end`), `closeOnSelect` (always closes on select), `title`/`triggerLabel`/`loadingLabel`/`onSeeAll`/`seeAllLabel` (fixed copy/behavior, no prop), and the generic `footer` slot (replaced by the built-in expand button + required `children`).\n\n## Types\n\n### `LaunchpadApp`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | `string` | Unique identifier |\n| `name` | `string` | Display name below the icon |\n| `icon` | `<SystemLaunchpadIcon>` \\| `<LaunchpadIconTile>` element | Must be one of these two components — a closed set enforced at the type level, so no raw image URL, emoji, or arbitrary icon can opt an app out of the shared tile design |\n| `href` | `string` | Optional navigation URL |\n| `onClick` | `(app: LaunchpadApp) => void` | Optional click handler |\n| `disabled` | `boolean` | Disables the app cell |\n| `badge` | `string` | Optional badge text |\n\n### `LaunchpadProps`\n\nProps for `Launchpad`. Requires `apps` and `children` (a `RoleSelect`, rendered below the grid outside the scrollable area); supports `loading` and panel/trigger customization.\n\nAlso exported: `UseLaunchpadOptions`, `UseLaunchpadReturn`.\n\n## Hooks\n\n**`useLaunchpad`** — headless open/close state for custom UI:\n\n```tsx title=\"App.tsx\"\nimport { useLaunchpad } from \"@rfdtech/components\";\n\nconst { open, toggle, close, triggerRef, panelRef } = useLaunchpad();\n```\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-shadow` | `--clet-shadow-sm` | Shadow theming hook on the root element |\n| `--clet-item-size` | `80px` (popover panel; root default is `50px`) | Width (and min-height) of each app grid item |\n| `--clet-icon-size` | `50px` | Width and height of the icon slot inside each item |\n| `--clet-launchpad-font` | `--clet-font-body` | Font family for the root element |\n\n## Notes\n\n- **`icon` is a closed set — every app renders on the shared tile system, no exceptions.** `LaunchpadApp.icon` only type-checks as a `SystemLaunchpadIcon` or `LaunchpadIconTile` element. This is deliberate: CLET is a component library for building a family of systems, and per-app custom icons (logos, image URLs, emoji) would let individual apps opt out of the shared visual language. Pass `icon={<SystemLaunchpadIcon name=\"System Name\" />}` — it renders a rounded-square tile (`--clet-radius-base`) with the system's initials on top of a fixed brand gradient (one of 9) and a decorative overlay shape (one of 9), each picked by an independent hash of `name` — same name always produces the same tile, and the gradient/overlay combination varies independently (up to 81 distinct looks). Need a custom glyph instead of initials? Use `<LaunchpadIconTile name=\"System Name\"><YourIcon /></LaunchpadIconTile>` — same tile, same gradient/overlay rules, just a different glyph on top.\n- Fetch apps in your application and pass `apps` + `loading`; Launchpad does not call APIs internally.\n- Also exported: `LaunchpadItem`, `LaunchpadGridIcon`, `SystemLaunchpadIcon`, `LaunchpadIconTile`.\n- **The expand button opens a scaled-up modal, not a callback.** There's no `onSeeAll` prop the small ghost `sm` button at the top right of the panel is always rendered (its expand icon always visible, its \"See more\" text only once `apps.length` exceeds 9) and always opens a borderless, `--clet-radius-2xl`, 50px-padded modal (built on `Modal`/`DialogPrimitive`) showing every app in `apps` in a 5-column, 120px-icon grid, with a top-right close button. It's the same tile system as the popover, just bigger selecting an app there calls the same `onAppSelect` and closes both the modal and the popover.\n- Only the app grid scrolls and fades at the top/bottom edge (a `mask-image` gradient on `.clet-launchpad__grid-scroll`) to hint there's more to scroll. The `children` role switcher and the divider above it sit outside that scrollable/masked area, so they stay fully visible and undimmed while the grid scrolls. The expanded modal's scrollable grid wrapper fades the same way.\n- Decorative tile overlays render at 5% opacity (`.clet-launchpad__tile-overlay`).\n- Each app's name label reserves a fixed 2-line height regardless of actual text length, so short and long names don't produce uneven grid row heights — true in both the popover and the expanded modal.\n- The panel scrolls (`max-height` + `overflow-y: auto`) once the grid + footer content grows tall.\n- `Launchpad` is a fully independent component tree from `AppSwitcher` — separate types, CSS classes (`.clet-launchpad*` vs `.clet-app-switcher*`), and hook (`useLaunchpad` vs `useAppSwitcher`). Adopting `Launchpad` doesn't affect any existing `AppSwitcher` usage.\n- Override shared tokens on `.clet-launchpad` for theming (see [Theme](/docs/theme)).",
1668
+ "docs": "# Launchpad\n\nA purpose-built \"app directory\" launcher for switching between CLET systems — a 3×3 grid of up to 9 apps, with a small ghost expand button at the top right of the panel (always rendered, opens a scaled-up \"See all\" modal showing every app uncapped), a muted \"See more\" line under the grid (rendered only when `apps.length` exceeds the 9-app cap, styled as plain text but clickable opens the same modal as the top-right button), and a required role switcher below. It grew out of the same underlying popover/grid idea as [`AppSwitcher`](/docs/app-switcher), with a much smaller, more opinionated API and every icon rendered on a fixed brand tile system with no escape hatch.\n\nPrefer `Launchpad` for all new work — `AppSwitcher` is deprecated and no longer maintained. `Launchpad` doesn't (yet) cover every `AppSwitcher` capability (flexible column count, an uncapped or differently-capped grid, a fully custom `footer`); if your case genuinely needs one of those, check with the team before reaching for the deprecated component.\n\n## Examples\n\n<ExampleTabs\n title=\"With apps, expand button, See more under the grid, and a role switcher\"\n preview={<LaunchpadPreview />}\n code={launchpadSource}\n canvasClassName=\"demo-docs__preview-canvas--launchpad\"\n/>\n\n<ExampleTabs\n title=\"Loading state\"\n preview={<LaunchpadLoadingPreview />}\n code={launchpadLoadingSource}\n canvasClassName=\"demo-docs__preview-canvas--launchpad\"\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `apps` | `LaunchpadApp[]` | — | Apps to show. Popover grid is capped at 9 — not configurable; the built-in expand modal shows every app in `apps`, uncapped. |\n| `loading` | `boolean` | `false` | Shows a spinner (only — no text) in the panel instead of the grid |\n| `onAppSelect` | `(app: LaunchpadApp) => void` | — | Called when an app is selected, from either the popover grid or the expanded modal |\n| `children` | `ReactElement<RoleSelectProps>` | — (required) | A [`RoleSelect`](/docs/role-select) rendered below the grid, outside the scrollable/masked grid area and separated by a divider — only a `RoleSelect` element is accepted, and omitting it is a type error |\n| `open` | `boolean` | — | Controlled open state (the popover; the expand modal manages its own state internally) |\n| `onOpenChange` | `(open: boolean) => void` | — | Open state callback |\n| `trigger` | `ReactNode` | round 9-dot icon | Custom trigger element — still wrapped with the \"Open Launchpad\" tooltip/accessible name |\n| `className` | `string` | — | Root CSS class |\n| `style` | `CSSProperties` | — | Root inline styles |\n| `showBrandStrip` | `boolean` | `true` | Shows the decorative brand strip along the bottom of the expanded \"See all\" modal |\n\nFixed, not configurable (no prop, no override): the popover title and the expanded modal's title both always read **\"Launchpad\"** (both `--clet-font-header`), the trigger's accessible name / hover tooltip always reads **\"Open Launchpad\"**, and the top-right expand button (always rendered, no prop to hide it) always opens the expanded modal — its expand icon always shows; its \"See more\" label is only available as the muted line under the grid (rendered only when `apps.length` exceeds the 9-app cap) or via the expand button's `aria-label`. Dropped relative to `AppSwitcher`: `columns` (fixed at 3, or 5 in the expanded modal), `maxItems` (fixed at 9 for the popover, uncapped in the modal), `placement` (fixed `bottom-end`), `closeOnSelect` (always closes on select), `title`/`triggerLabel`/`loadingLabel`/`onSeeAll`/`seeAllLabel` (fixed copy/behavior, no prop), and the generic `footer` slot (replaced by the built-in expand button + required `children`).\n\n## Types\n\n### `LaunchpadApp`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | `string` | Unique identifier |\n| `name` | `string` | Display name below the icon |\n| `icon` | `<SystemLaunchpadIcon>` \\| `<LaunchpadIconTile>` element | Must be one of these two components — a closed set enforced at the type level, so no raw image URL, emoji, or arbitrary icon can opt an app out of the shared tile design |\n| `href` | `string` | Optional navigation URL |\n| `onClick` | `(app: LaunchpadApp) => void` | Optional click handler |\n| `disabled` | `boolean` | Disables the app cell |\n| `badge` | `string` | Optional badge text |\n\n### `LaunchpadProps`\n\nProps for `Launchpad`. Requires `apps` and `children` (a `RoleSelect`, rendered below the grid outside the scrollable area); supports `loading` and panel/trigger customization.\n\nAlso exported: `UseLaunchpadOptions`, `UseLaunchpadReturn`.\n\n## Hooks\n\n**`useLaunchpad`** — headless open/close state for custom UI:\n\n```tsx title=\"App.tsx\"\nimport { useLaunchpad } from \"@rfdtech/components\";\n\nconst { open, toggle, close, triggerRef, panelRef } = useLaunchpad();\n```\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-shadow` | `--clet-shadow-sm` | Shadow theming hook on the root element |\n| `--clet-item-size` | `80px` (popover panel; root default is `50px`) | Width (and min-height) of each app grid item |\n| `--clet-icon-size` | `50px` | Width and height of the icon slot inside each item |\n| `--clet-launchpad-font` | `--clet-font-body` | Font family for the root element |\n\n## Notes\n\n- **`icon` is a closed set — every app renders on the shared tile system, no exceptions.** `LaunchpadApp.icon` only type-checks as a `SystemLaunchpadIcon` or `LaunchpadIconTile` element. This is deliberate: CLET is a component library for building a family of systems, and per-app custom icons (logos, image URLs, emoji) would let individual apps opt out of the shared visual language. Pass `icon={<SystemLaunchpadIcon name=\"System Name\" />}` — it renders a rounded-square tile (`--clet-radius-base`) with the system's initials on top of a fixed brand gradient (one of 9) and a decorative overlay shape (one of 9), each picked by an independent hash of `name` — same name always produces the same tile, and the gradient/overlay combination varies independently (up to 81 distinct looks). Need a custom glyph instead of initials? Use `<LaunchpadIconTile name=\"System Name\"><YourIcon /></LaunchpadIconTile>` — same tile, same gradient/overlay rules, just a different glyph on top.\n- Fetch apps in your application and pass `apps` + `loading`; Launchpad does not call APIs internally.\n- Also exported: `LaunchpadItem`, `LaunchpadGridIcon`, `SystemLaunchpadIcon`, `LaunchpadIconTile`.\n- **The \"See more\" line under the grid is a clickable button styled as muted text.** Rendered only when `apps.length > 9` (it lives outside the popover grid, between the grid-scroll area and the role-switcher footer), it opens the same expanded modal as the top-right expand button. Both controls share one `handleSeeAll` callback there's no separate \"static indicator\" and \"trigger\", just two visible affordances for the same expand action. If a system genuinely needs to expose more than 9 apps, the modal is the only path there's no `onSeeAll` prop or per-app \"show more here\" override.\n- Only the app grid scrolls and fades at the top/bottom edge (a `mask-image` gradient on `.clet-launchpad__grid-scroll`) to hint there's more to scroll. The \"See more\" line and the `children` role switcher and the divider above it sit outside that scrollable/masked area, so they stay fully visible and undimmed while the grid scrolls. The expanded modal's scrollable grid wrapper fades the same way.\n- Decorative tile overlays render at 5% opacity (`.clet-launchpad__tile-overlay`).\n- Each app's name label reserves a fixed 2-line height regardless of actual text length, so short and long names don't produce uneven grid row heights — true in both the popover and the expanded modal.\n- The panel scrolls (`max-height` + `overflow-y: auto`) once the grid + footer content grows tall.\n- `Launchpad` is a fully independent component tree from `AppSwitcher` — separate types, CSS classes (`.clet-launchpad*` vs `.clet-app-switcher*`), and hook (`useLaunchpad` vs `useAppSwitcher`). Adopting `Launchpad` doesn't affect any existing `AppSwitcher` usage.\n- Override shared tokens on `.clet-launchpad` for theming (see [Theme](/docs/theme)).",
1610
1669
  "headings": [
1611
1670
  "Launchpad",
1612
1671
  "Examples",
@@ -1618,7 +1677,7 @@
1618
1677
  "Design tokens",
1619
1678
  "Notes"
1620
1679
  ],
1621
- "types": "import type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { RoleSelect } from \"../components/role-select/RoleSelect\";\nimport type { RoleSelectProps } from \"./role-select\";\nimport type { LaunchpadIconTile, LaunchpadIconTileProps } from \"../components/launchpad/LaunchpadIconTile\";\nimport type { SystemLaunchpadIcon, SystemLaunchpadIconProps } from \"../components/launchpad/SystemLaunchpadIcon\";\n\nexport interface LaunchpadApp {\n /** Unique identifier for the app */\n id: string;\n /** Display name shown below the icon */\n name: string;\n /**\n * Icon tile — must be a `SystemLaunchpadIcon` (auto-generated initials\n * tile) or a `LaunchpadIconTile` (custom glyph on the same tile system).\n * This is a closed set on purpose: every app's icon renders on the shared\n * 9-gradient / 9-overlay tile system, so a raw image URL, emoji, or\n * arbitrary node can't be used to opt an app out of the design system's\n * visual language.\n */\n icon:\n | ReactElement<LaunchpadIconTileProps, typeof LaunchpadIconTile>\n | ReactElement<SystemLaunchpadIconProps, typeof SystemLaunchpadIcon>;\n /** URL to navigate to when the app is selected */\n href?: string;\n /** Custom click handler (called before navigation if href is set) */\n onClick?: (app: LaunchpadApp) => void;\n /** Whether the app is disabled */\n disabled?: boolean;\n /** Optional badge text (e.g. \"New\") */\n badge?: string;\n}\n\nexport interface LaunchpadProps {\n /**\n * Apps to display. The popover grid is capped at 9 (see design note on\n * the component); the built-in expand button opens a modal showing every\n * app in `apps`, uncapped.\n */\n apps: LaunchpadApp[];\n /** Shows a loading state (spinner only) in the panel instead of the grid */\n loading?: boolean;\n /** Controlled open state */\n open?: boolean;\n /** Callback when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Called when an app is selected (from the grid or the \"See all\" expanded view) */\n onAppSelect?: (app: LaunchpadApp) => void;\n /**\n * Role switcher rendered below the grid, outside the scrollable/masked\n * grid area and separated from it by a divider. Required — only a\n * `RoleSelect` element is accepted, and passing none is a type error.\n */\n children: ReactElement<RoleSelectProps, typeof RoleSelect>;\n /**\n * Custom trigger element — replaces the default square 9-dot icon.\n * Whatever you pass is still wrapped with the \"Open Launchpad\" tooltip\n * and accessible name.\n */\n trigger?: ReactNode;\n /** Additional CSS class for the root container */\n className?: string;\n /** Inline styles for the root container */\n style?: CSSProperties;\n /**\n * Shows the decorative brand strip along the bottom of the expanded\n * \"See all\" modal.\n * @default true\n */\n showBrandStrip?: boolean;\n}\n\nexport interface UseLaunchpadOptions {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\nexport interface UseLaunchpadReturn {\n open: boolean;\n setOpen: (open: boolean) => void;\n toggle: () => void;\n close: () => void;\n triggerRef: React.RefObject<HTMLButtonElement | null>;\n panelRef: React.RefObject<HTMLDivElement | null>;\n}\n",
1680
+ "types": "import type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport type { RoleSelect } from \"../components/role-select/RoleSelect\";\nimport type { RoleSelectProps } from \"./role-select\";\nimport type { LaunchpadIconTile, LaunchpadIconTileProps } from \"../components/launchpad/LaunchpadIconTile\";\nimport type { SystemLaunchpadIcon, SystemLaunchpadIconProps } from \"../components/launchpad/SystemLaunchpadIcon\";\n\nexport interface LaunchpadApp {\n /** Unique identifier for the app */\n id: string;\n /** Display name shown below the icon */\n name: string;\n /**\n * Icon tile — must be a `SystemLaunchpadIcon` (auto-generated initials\n * tile) or a `LaunchpadIconTile` (custom glyph on the same tile system).\n * This is a closed set on purpose: every app's icon renders on the shared\n * 9-gradient / 9-overlay tile system, so a raw image URL, emoji, or\n * arbitrary node can't be used to opt an app out of the design system's\n * visual language.\n */\n icon:\n | ReactElement<LaunchpadIconTileProps, typeof LaunchpadIconTile>\n | ReactElement<SystemLaunchpadIconProps, typeof SystemLaunchpadIcon>;\n /** URL to navigate to when the app is selected */\n href?: string;\n /** Custom click handler (called before navigation if href is set) */\n onClick?: (app: LaunchpadApp) => void;\n /** Whether the app is disabled */\n disabled?: boolean;\n /** Optional badge text (e.g. \"New\") */\n badge?: string;\n}\n\nexport interface LaunchpadProps {\n /**\n * Apps to display. The popover grid is capped at 9 (see design note on\n * the component); when `apps.length` exceeds the cap, the extras are not\n * rendered in the grid and a muted \"See more\" line appears under the\n * grid (between the grid and the role switcher). Clicking that line —\n * or the top-right expand button opens a scaled-up modal showing\n * every app in `apps`, uncapped.\n */\n apps: LaunchpadApp[];\n /** Shows a loading state (spinner only) in the panel instead of the grid */\n loading?: boolean;\n /** Controlled open state */\n open?: boolean;\n /** Callback when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Called when an app is selected (from the grid or the \"See all\" expanded view) */\n onAppSelect?: (app: LaunchpadApp) => void;\n /**\n * Role switcher rendered below the grid, outside the scrollable/masked\n * grid area and separated from it by a divider. Required — only a\n * `RoleSelect` element is accepted, and passing none is a type error.\n */\n children: ReactElement<RoleSelectProps, typeof RoleSelect>;\n /**\n * Custom trigger element — replaces the default square 9-dot icon.\n * Whatever you pass is still wrapped with the \"Open Launchpad\" tooltip\n * and accessible name.\n */\n trigger?: ReactNode;\n /** Additional CSS class for the root container */\n className?: string;\n /** Inline styles for the root container */\n style?: CSSProperties;\n /**\n * Shows the decorative brand strip along the bottom of the expanded\n * \"See all\" modal.\n * @default true\n */\n showBrandStrip?: boolean;\n}\n\nexport interface UseLaunchpadOptions {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\nexport interface UseLaunchpadReturn {\n open: boolean;\n setOpen: (open: boolean) => void;\n toggle: () => void;\n close: () => void;\n triggerRef: React.RefObject<HTMLButtonElement | null>;\n panelRef: React.RefObject<HTMLDivElement | null>;\n}\n",
1622
1681
  "examples": [
1623
1682
  {
1624
1683
  "file": "launchpad-loading.example.tsx",
@@ -1670,7 +1729,7 @@
1670
1729
  ],
1671
1730
  "import": "@rfdtech/components"
1672
1731
  },
1673
- "docs": "# MetricCard\n\nMetric display card for dashboards and overviews. Renders a compact card with a label, large value, optional trend arrow with percentage, icon, and description.\n\n## Example\n\n<ExampleTabs\n title=\"Metric cards\"\n preview={<MetricCardPreview />}\n code={metricCardSource}\n/>\n\n## Loading state\n\nSet `loading` to show shimmering skeleton placeholders instead of the label/value/trend/description content — useful while the metric value is still being fetched. The skeleton shape follows whichever of `icon`, `trend`, and `description` you pass, so the loaded and loading layouts line up.\n\n<ExampleTabs\n title=\"Loading state\"\n preview={<MetricCardLoadingPreview />}\n code={metricCardLoadingSource}\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `string` | — | Metric label shown above the value (uppercased via CSS) |\n| `value` | `string \\| number` | — | The primary metric value |\n| `icon` | `ReactNode` | — | Optional icon rendered next to the label (always muted tint) |\n| `description` | `string` | — | Subtitle shown below the value row |\n| `trend` | `\"up\" \\| \"down\" \\| \"neutral\"` | — | Trend direction; renders an arrow icon |\n| `trendValue` | `string` | — | Formatted trend text (e.g. `\"+12.5%\"`) |\n| `variant` | `\"default\" \\| \"outline\"` | `\"default\"` | Card style — `\"outline\"` is a bordered, no-fill card with chevron trend icons |\n| `color` | `string` | | Custom CSS color for the trend indicator; overrides variant |\n| `loading` | `boolean` | `false` | Show shimmering skeleton placeholders instead of the label/value/trend/description |\n| `loadingLabel` | `string` | `\"Loading metric\"` | Accessible label announced while loading |\n| `classNames` | `MetricCardClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root element |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## Variants\n\nVariants only affect the **trend indicator color**, not the background or text of the card:\n\n| Variant | Trend color |\n|---------|-------------|\n| `\"default\"` | `--clet-success` (up), `--clet-error` (down), `--clet-text-muted` (neutral) |\n| `\"primary\"` | `--clet-primary` |\n| `\"success\"` | `--clet-success` |\n| `\"warning\"` | `--clet-warning` |\n| `\"error\"` | `--clet-error` |\n\nOverride with the `color` prop (any CSS value) for custom accent colors.\n\n### Outline variant\n\nSet `variant=\"outline\"` for a bordered, no-fill card. It differs structurally from the default:\n\n- **Page background** — uses `--clet-main-bg` (not the default variant's `--clet-default-metric-card-bg`) with a `1px` border in `--clet-border`, radius `--clet-radius-base` — reads as a bordered cutout against a panel background rather than a filled card.\n- **Chevron trend icons** — `up` renders a chevron up, `down` a chevron down, `neutral` a dash.\n- **No `+`/`-` prefix** — a leading sign is stripped from `trendValue` (pass `\"12.5%\"`, not `\"+12.5%\"`).\n- **Larger value** — `32px` font size.\n- **Padding `24px`** with a unified `8px` gap between the label, value, and description.\n\n```tsx\n<MetricCard\n variant=\"outline\"\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"12.5%\"\n/>\n```\n\n## Types\n\n### `MetricCardProps`\n\nSee the props table above.\n\n### `MetricCardClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root element (`.clet-metric-card`) |\n| `icon` | Icon wrapper (`.clet-metric-card__icon`) |\n| `label` | Label text (`.clet-metric-card__label`) |\n| `value` | Value text (`.clet-metric-card__value`) |\n| `description` | Description text (`.clet-metric-card__description`) |\n| `trend` | Trend indicator (`.clet-metric-card__trend`) |\n\nAlso exported: `MetricCardVariant`, `MetricTrend`.\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-metric-card-font` | `--clet-font-body` | Card font family |\n| `--clet-metric-card-value-font` | `--clet-font-header` | Main value (header) font family — now defaults to the theme's header font token `--clet-font-header` |\n| `--clet-metric-card-value-size` | `28px` (`32px` outline) | Main value (header) font size |\n| `--clet-metric-card-value-color` | `--clet-text` | Main value (header) color |\n| `--clet-metric-card-description-size` | `13px` | Description font size |\n| `--clet-default-metric-card-bg` | `--clet-surface-card` | Card background — `\"default\"`/color variants only |\n| `--clet-metric-card-radius` | `--clet-radius-xl` | Card border-radius |\n| `--clet-metric-card-shadow` | `none` | Card box-shadow |\n\nOverride card background via `--clet-default-metric-card-bg`, radius via `--clet-metric-card-radius`, or shadow via `--clet-metric-card-shadow`. The `\"outline\"` variant reads the global `--clet-main-bg` directly instead of `--clet-default-metric-card-bg` — override `--clet-main-bg` (or `classNames.root`) to change it.\n\n## Notes\n\n- Background and text remain neutral across all variants — only the trend indicator changes color. Use the `color` prop for full control.\n- The icon always renders in `--clet-text-muted` regardless of variant.\n- Card width is determined by the parent container. Use a grid or flex wrapper for layout.\n- On mobile (`<= 768px`), padding reduces to 16px and value font size to 24px.\n\n## Do\n\n- Use `classNames={{root: \"shadow-primary\"}}` for a primary-tinted drop shadow instead of the default `none` shadow.",
1732
+ "docs": "# MetricCard\n\nMetric display card for dashboards and overviews. Renders a compact card with a label, large value, optional trend arrow with percentage, icon, and description.\n\n## Example\n\n<ExampleTabs\n title=\"Metric cards\"\n preview={<MetricCardPreview />}\n code={metricCardSource}\n/>\n\n## Loading state\n\nSet `loading` to show shimmering skeleton placeholders instead of the label/value/trend/description content — useful while the metric value is still being fetched. The skeleton shape follows whichever of `icon`, `trend`, and `description` you pass, so the loaded and loading layouts line up.\n\n<ExampleTabs\n title=\"Loading state\"\n preview={<MetricCardLoadingPreview />}\n code={metricCardLoadingSource}\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `string` | — | Metric label shown above the value (uppercased via CSS) |\n| `value` | `string \\| number` | — | The primary metric value |\n| `icon` | `ReactNode` | — | Optional icon rendered next to the label (always muted tint) |\n| `description` | `string` | — | Subtitle shown below the value row |\n| `trend` | `\"up\" \\| \"down\" \\| \"neutral\"` | — | Trend direction; renders an arrow icon |\n| `trendValue` | `string` | — | Formatted trend text (e.g. `\"+12.5%\"`) |\n| `variant` | `\"default\" \\| \"outline\" \\| \"bordered\"` | `\"default\"` | Card style — `\"outline\"` is a bordered, no-fill card with chevron trend icons; `\"bordered\"` is the default layout with a 1px border added |\n| `animate` | `boolean` | `false` | Enable count-up animation from 0 to the target value on mount and value change |\n| `animationDuration` | `number` | `1500` | Duration of the count-up animation in ms |\n| `loading` | `boolean` | `false` | Show shimmering skeleton placeholders instead of the label/value/trend/description |\n| `loadingLabel` | `string` | `\"Loading metric\"` | Accessible label announced while loading |\n| `classNames` | `MetricCardClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root element |\n\nAlso accepts remaining `HTMLAttributes<HTMLDivElement>`.\n\n## Variants\n\nVariants only affect the **trend indicator color**, not the background or text of the card:\n\n| Variant | Trend color |\n|---------|-------------|\n| `\"default\"` | `--clet-success` (up), `--clet-error` (down), `--clet-text-muted` (neutral) |\n| `\"outline\"` | Same as `\"default\"` uses chevron icons instead of arrows, no `+`/`-` prefix |\n| `\"bordered\"` | Same as `\"default\"` adds a 1px border to the card |\n\n### Outline variant\n\nSet `variant=\"outline\"` for a bordered, no-fill card. It differs structurally from the default:\n\n- **Page background** — uses `--clet-main-bg` (not the default variant's `--clet-default-metric-card-bg`) with a `1px` border in `--clet-border`, radius `--clet-radius-base` — reads as a bordered cutout against a panel background rather than a filled card.\n- **Chevron trend icons** — `up` renders a chevron up, `down` a chevron down, `neutral` a dash.\n- **No `+`/`-` prefix** — a leading sign is stripped from `trendValue` (pass `\"12.5%\"`, not `\"+12.5%\"`).\n- **Larger value** — `32px` font size.\n- **Padding `24px`** with a unified `8px` gap between the label, value, and description.\n\n```tsx\n<MetricCard\n variant=\"outline\"\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"12.5%\"\n/>\n```\n\n## Types\n\n### `MetricCardProps`\n\nSee the props table above.\n\n### `MetricCardClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root element (`.clet-metric-card`) |\n| `icon` | Icon wrapper (`.clet-metric-card__icon`) |\n| `label` | Label text (`.clet-metric-card__label`) |\n| `value` | Value text (`.clet-metric-card__value`) |\n| `description` | Description text (`.clet-metric-card__description`) |\n| `trend` | Trend indicator (`.clet-metric-card__trend`) |\n\nAlso exported: `MetricCardVariant`, `MetricTrend`.\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-metric-card-font` | `--clet-font-body` | Card font family |\n| `--clet-metric-card-value-font` | `--clet-font-header` | Main value (header) font family — now defaults to the theme's header font token `--clet-font-header` |\n| `--clet-metric-card-value-size` | `28px` (`32px` outline) | Main value (header) font size |\n| `--clet-metric-card-value-color` | `--clet-text` | Main value (header) color |\n| `--clet-metric-card-description-size` | `13px` | Description font size |\n| `--clet-default-metric-card-bg` | `--clet-surface-card` | Card background — `\"default\"`/color variants only |\n| `--clet-metric-card-radius` | `--clet-radius-xl` | Card border-radius |\n| `--clet-metric-card-shadow` | `none` | Card box-shadow |\n\nOverride card background via `--clet-default-metric-card-bg`, radius via `--clet-metric-card-radius`, or shadow via `--clet-metric-card-shadow`. The `\"outline\"` variant reads the global `--clet-main-bg` directly instead of `--clet-default-metric-card-bg` — override `--clet-main-bg` (or `classNames.root`) to change it.\n\n## Notes\n\n- Background and text remain neutral across all variants — only the trend indicator changes color. Use the `color` prop for full control.\n- The icon always renders in `--clet-text-muted` regardless of variant.\n- Card width is determined by the parent container. Use a grid or flex wrapper for layout.\n- On mobile (`<= 768px`), padding reduces to 16px and value font size to 24px.\n\n## Do\n\n- Use `classNames={{root: \"shadow-primary\"}}` for a primary-tinted drop shadow instead of the default `none` shadow.",
1674
1733
  "headings": [
1675
1734
  "MetricCard",
1676
1735
  "Example",
@@ -1685,7 +1744,7 @@
1685
1744
  "Notes",
1686
1745
  "Do"
1687
1746
  ],
1688
- "types": "import type { HTMLAttributes, ReactNode } from \"react\";\n\nexport type MetricTrend = \"up\" | \"down\" | \"neutral\";\n\nexport type MetricCardVariant = \"default\" | \"outline\";\n\nexport interface MetricCardClassNames {\n root?: string;\n icon?: string;\n label?: string;\n value?: string;\n description?: string;\n trend?: string;\n}\n\nexport interface MetricCardProps extends HTMLAttributes<HTMLDivElement> {\n /** Metric label shown above the value */\n label: string;\n /** The primary metric value */\n value: string | number;\n /** Optional icon displayed alongside the label */\n icon?: ReactNode;\n /** Subtitle or description below the value */\n description?: string;\n /** Visual variant. \"outline\" renders a no-fill, bordered card with chevron trend icons and no +/- prefix */\n variant?: MetricCardVariant;\n /** Direction of the trend indicator */\n trend?: MetricTrend;\n /** Formatted trend text (e.g. \"+12.5%\") */\n trendValue?: string;\n /** Enable count-up animation from 0 to the target value on mount and value change */\n animate?: boolean;\n /** Duration of the count-up animation in ms (default: 1500) */\n animationDuration?: number;\n /** Show shimmering skeleton placeholders instead of the label/value/description content */\n loading?: boolean;\n /** Accessible label announced while loading (default: \"Loading metric\") */\n loadingLabel?: string;\n classNames?: MetricCardClassNames;\n className?: string;\n}\n",
1747
+ "types": "import type { HTMLAttributes, ReactNode } from \"react\";\n\nexport type MetricTrend = \"up\" | \"down\" | \"neutral\";\n\nexport type MetricCardVariant = \"default\" | \"outline\" | \"bordered\";\n\nexport interface MetricCardClassNames {\n root?: string;\n icon?: string;\n label?: string;\n value?: string;\n description?: string;\n trend?: string;\n}\n\nexport interface MetricCardProps extends HTMLAttributes<HTMLDivElement> {\n /** Metric label shown above the value */\n label: string;\n /** The primary metric value */\n value: string | number;\n /** Optional icon displayed alongside the label */\n icon?: ReactNode;\n /** Subtitle or description below the value */\n description?: string;\n /**\n * Visual variant.\n * \"outline\" renders a no-fill, bordered card with chevron trend icons and no +/- prefix.\n * \"bordered\" renders the same layout as \"default\" with a 1px border added.\n */\n variant?: MetricCardVariant;\n /** Direction of the trend indicator */\n trend?: MetricTrend;\n /** Formatted trend text (e.g. \"+12.5%\") */\n trendValue?: string;\n /** Enable count-up animation from 0 to the target value on mount and value change */\n animate?: boolean;\n /** Duration of the count-up animation in ms (default: 1500) */\n animationDuration?: number;\n /** Show shimmering skeleton placeholders instead of the label/value/description content */\n loading?: boolean;\n /** Accessible label announced while loading (default: \"Loading metric\") */\n loadingLabel?: string;\n classNames?: MetricCardClassNames;\n className?: string;\n}\n",
1689
1748
  "examples": [
1690
1749
  {
1691
1750
  "file": "metric-card-loading.example.tsx",
@@ -1703,7 +1762,7 @@
1703
1762
  "components": [
1704
1763
  "MetricCard"
1705
1764
  ],
1706
- "source": "import { MetricCard } from \"@rfdtech/components\";\n\nexport function MetricCardExample() {\n return (\n <div\n style={{\n background: \"var(--clet-surface-subtle)\",\n padding: 20,\n borderRadius: \"var(--clet-radius-xl)\",\n width: \"100%\",\n }}\n >\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16 }}>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"+12.5%\"\n animate\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"-3.2%\"\n animate\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n animate\n />\n </div>\n </div>\n\n <div\n style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16, marginTop: 16 }}\n >\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"12.5%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"3.2%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n trend=\"neutral\"\n trendValue=\"0.0%\"\n />\n </div>\n </div>\n </div>\n );\n}\n"
1765
+ "source": "import { MetricCard } from \"@rfdtech/components\";\n\nexport function MetricCardExample() {\n return (\n <div\n style={{\n background: \"var(--clet-surface-subtle)\",\n padding: 20,\n borderRadius: \"var(--clet-radius-xl)\",\n width: \"100%\",\n }}\n >\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16 }}>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"+12.5%\"\n animate\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"-3.2%\"\n animate\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n animate\n />\n </div>\n </div>\n\n <div\n style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16, marginTop: 16 }}\n >\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n trend=\"up\"\n trendValue=\"12.5%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n trend=\"down\"\n trendValue=\"3.2%\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"outline\"\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n trend=\"neutral\"\n trendValue=\"0.0%\"\n />\n </div>\n </div>\n\n <div\n style={{ display: \"flex\", flexWrap: \"wrap\", gap: 16, marginTop: 16 }}\n >\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"bordered\"\n label=\"Revenue\"\n value=\"$128.4k\"\n description=\"Total revenue this quarter\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"bordered\"\n label=\"Active users\"\n value=\"2,847\"\n description=\"Current active users\"\n />\n </div>\n <div style={{ minWidth: 200, flex: 1 }}>\n <MetricCard\n variant=\"bordered\"\n label=\"Avg response\"\n value=\"1.2s\"\n description=\"Average API response time\"\n />\n </div>\n </div>\n </div>\n );\n}\n"
1707
1766
  }
1708
1767
  ],
1709
1768
  "ruleIds": [
@@ -1737,7 +1796,13 @@
1737
1796
  "stacked layout",
1738
1797
  "CountrySelector",
1739
1798
  "Combobox",
1740
- "NetworkOperator"
1799
+ "NetworkOperator",
1800
+ "gsl-to-clet",
1801
+ "cletTheme",
1802
+ "gslTheme",
1803
+ "rename",
1804
+ "TableFooter",
1805
+ "noBorder"
1741
1806
  ],
1742
1807
  "since": "2.0.0",
1743
1808
  "status": "guide",
@@ -1760,7 +1825,7 @@
1760
1825
  "network-operator"
1761
1826
  ]
1762
1827
  },
1763
- "docs": "# Migration guide: v1 → v2 — Agent Instructions\n\nAgent-only. All breaking changes and deprecations between `1.22.0` and current, organized as directives. See [Changelog](/docs/changelog) for dated list.\n\n## 1. `AppHeaderProfile` → `ProfilePopover`\n\n**Deprecation, not breaking.** `AppHeaderProfile` still works (thin wrapper around `ProfilePopover`). No forced migration, no removal date. `ProfilePopover` gets all new features.\n\n**When you encounter `AppHeaderProfile`:**\n\n- Replace with `ProfilePopover` using `user` and `variant` props for same trigger\n- `onProfileClick`/`onSettingsClick`/`onHelpClick` map directly to `ProfilePopover`'s `onMyProfile`/`onAccountSettings`/`onHelpAndSupport` — `items` now defaults to those three rows built in (\"My Profile\", \"Account Settings\", \"Help and Support\"), so you no longer hand-build them via `items`\n- `headerAction` prop silently ignored on both — delete it. Popover header now always shows built-in theme toggle when `ThemeProvider` is in tree\n- Sign Out now opens a \"Confirm Sign Out\" dialog before calling `onSignOut` — pass `noConfirmSignOut` to keep the old immediate-fire behavior\n\n```diff\n-import { AppHeaderProfile } from \"@rfdtech/components\";\n+import { ProfilePopover } from \"@rfdtech/components\";\n\n-<AppHeaderProfile user={user} variant=\"avatar\">\n- <RoleSelect roles={roles} selectedRole={role} onClickRole={setRole} />\n-</AppHeaderProfile>\n+<ProfilePopover user={user} variant=\"avatar\">\n+ <RoleSelect roles={roles} selectedRole={role} onClickRole={setRole} />\n+</ProfilePopover>\n```\n\n`items`, `loading`/`loadingLabel`, `onSignOut`, `children` carry over unchanged.\n\n**Wiring `onProfileClick`/`onSettingsClick`/`onHelpClick` to the built-in default rows:**\n\n```tsx\n<ProfilePopover\n user={user}\n variant=\"avatar\"\n onMyProfile={goToProfile}\n onAccountSettings={goToSettings}\n onHelpAndSupport={goToHelp}\n onSignOut={signOut}\n/>\n```\n\nOnly pass `items` if you need rows *other than* \"My Profile\"/\"Account Settings\"/\"Help and Support\" — a passed-in `items` array fully replaces the three defaults, it does not merge with them. Prefer the defaults (via `onMyProfile`/`onAccountSettings`/`onHelpAndSupport`) unless the system genuinely needs different rows.\n\nOnly `\"full\"` and `\"avatar\"` variants remain on both components (no `\"basic\"`).\n\n## 2. `AppItem.icon` → `SystemAppIcon` or `AppIconTile`\n\n`AppSwitcher`'s `AppItem.icon` now accepts `ReactNode` only — closed to `<SystemAppIcon name=\"...\" />` (auto-generated initials tile) or `<AppIconTile name=\"...\">custom glyph</AppIconTile>`. Every app icon renders on the shared 9-gradient/9-overlay tile system.\n\n```diff\n- { id: \"analytics\", name: \"Analytics\", icon: \"https://cdn.example.com/analytics.png\" },\n+ { id: \"analytics\", name: \"Analytics\", icon: <SystemAppIcon name=\"Analytics\" /> },\n- { id: \"custom\", name: \"Custom App\", icon: <MyCustomLogo /> },\n+ { id: \"custom\", name: \"Custom App\", icon: <AppIconTile name=\"Custom App\"><MyCustomLogo /></AppIconTile> },\n];\n```\n\n## 3. `AppSwitcher` maxItems default is 6\n\nDefault `maxItems` is 6. Migrate grids with >6 apps:\n\n```diff\n-<AppSwitcher apps={apps} />\n+<AppSwitcher apps={apps} maxItems={apps.length} />\n```\n\nBetter: switch to `Launchpad` (see §6).\n\n## 4. `Table`: selection-only loses kebab column\n\n`Table` with `selectable` but no `rowActions` no longer renders an empty kebab column. If you relied on the Select/Deselect entry there, add explicit row action:\n\n```diff\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n+ rowActions={[\n+ { id: \"select\", label: isSelected ? \"Deselect\" : \"Select\", onClick: toggleRow },\n+ ]}\n />\n```\n\n## 5. Rebrand: new default colors and typography\n\n**If no `--clet-*` overrides set:**\n\n- `--clet-primary`/`--clet-primary-light`: red (`#dc2626`/`#ef4444`) → navy (`#051b2c`/`#4a7fa8`)\n- New `--clet-secondary` (gold `#c8a24b`) — default `SidebarLink` active-link color\n- New typography: `--clet-font-header` (Roboto Serif), `--clet-font-body` (Lato) via `@fontsource/lato`, `@fontsource/roboto-serif`\n\n**To override:** put the call in `src/clet.theme.ts` (see [Theme](/docs/theme#recommended-file-srcclettheme-ts) for the recommended file convention) and side-effect-import it once in your entry point:\n\n```ts title=\"src/clet.theme.ts\"\nimport { cletTheme } from \"@rfdtech/components\";\ncletTheme({ all: { primary: \"#dc2626\", primaryLight: \"#fef2f2\" } });\n```\n\n```ts title=\"src/main.tsx\"\nimport \"@rfdtech/components/style.css\";\nimport \"./clet.theme\";\n```\n\n```css\n:root { --clet-font-header: var(--clet-font); --clet-font-body: var(--clet-font); }\n```\n\n**To keep new defaults: do nothing.** See `.legacy-theme` in `demo/demo.css` for the old red look reference.\n\n**Migrating a project that already has `--clet-*` token overrides?** Find every existing override of\na *known* `--clet-*` token — a `cletTheme()` call (`src/clet.theme.ts` or elsewhere) or hand-written\nCSS setting a real `--clet-*` custom property, cross-checked against `get_tokens()`. This is about\nrecognized library tokens only — not custom/arbitrary CSS variables or component-local one-off\nstyling. List every one found and **ask the user about each token individually** — keep the\nexisting override, or drop it for the new approved default. Don't decide silently, and don't batch\nit into one yes/no: some overrides are deliberate brand choices worth keeping, others only ever\nexisted to match the *old* pre-rebrand defaults and should be dropped now that those defaults\nthemselves changed.\n\n## 6. Prefer `Launchpad` over deprecated `AppSwitcher`\n\n`AppSwitcher` deprecated, no new features. `Launchpad` is the replacement — independent component tree.\n\n**Not a drop-in replacement.** Re-author the app list and trigger:\n\n```diff\n-import { AppSwitcher, SystemAppIcon } from \"@rfdtech/components\";\n+import { Launchpad, SystemLaunchpadIcon } from \"@rfdtech/components\";\n\n const apps = [\n- { id: \"analytics\", name: \"Analytics\", icon: <SystemAppIcon name=\"Analytics\" /> },\n+ { id: \"analytics\", name: \"Analytics\", icon: <SystemLaunchpadIcon name=\"Analytics\" /> },\n ];\n\n-<AppSwitcher apps={apps} maxItems={apps.length} footer={<CustomFooter />} />\n+<Launchpad apps={apps} onAppSelect={(app) => navigate(app.href)}>\n+ <RoleSelect roles={roles} selectedRole={role} onClickRole={setRole} />\n+</Launchpad>\n```\n\n**Launchpad differences:**\n\n- Icon type: `SystemLaunchpadIcon`/`LaunchpadIconTile` only (not `SystemAppIcon`/`AppIconTile`)\n- Grid: fixed 3×3 (9 apps), not configurable. The expand button opens a 5-column modal with every app\n- No `maxItems`/`columns`/generic `footer` slot. `children` is **required** and only accepts a single `RoleSelect` — omitting it is a type error, not just a lint warning\n- No `title`/`triggerLabel`/`loadingLabel`/`onSeeAll`/`seeAllLabel`/`placement`/`closeOnSelect`\n- Title always \"Launchpad\", trigger always \"Open Launchpad\", selecting app always closes popover\n- No bordered \"See all\" footer button anymore. A small ghost `sm` button sits at the top right of the panel next to the title — its expand icon is always visible, and its \"See more\" text label only appears once `apps.length` exceeds the 9-app cap. Never force the label always-on or reintroduce the old bordered button.\n\n**Stay on `AppSwitcher` if you need: flexible column count, uncapped grid without \"See all\" modal, or custom `footer`. Check with team before building new custom-footer `AppSwitcher` usage.**\n\n## 7. `CountrySelector` → `Combobox` or `PhoneNumberInput`'s country picker\n\n**Deprecation, not breaking.** `CountrySelector` still works unchanged. No forced migration, no removal date. It receives no new features or design updates.\n\n**When you encounter `CountrySelector`:**\n\n- For a standalone country picker (not tied to a phone number), replace with `Combobox` — build the option list from the `countries` shape (`{ value: code, label: name }`) and pass it via `options`\n- For phone-number entry, use `PhoneNumberInput` directly — its built-in country picker already covers this case; don't pair a separate `CountrySelector` with a plain text input\n- `CountrySelector`'s flags are emoji; `Combobox` has no built-in flag rendering — pass a flag as each option's `icon` (e.g. from `country-flag-icons/react/3x2`, the same package `PhoneNumberInput` now uses) if flags are needed\n\n```diff\n-import { CountrySelector } from \"@rfdtech/components\";\n+import { Combobox } from \"@rfdtech/components\";\n+import { countries } from \"some-country-data-source\";\n\n-<CountrySelector value={country} onChange={setCountry} />\n+<Combobox\n+ aria-label=\"Country\"\n+ options={countries.map((c) => ({ value: c.code, label: c.name }))}\n+ value={country}\n+ onValueChange={setCountry}\n+/>\n```\n\n## 8. `NetworkOperator` → `Combobox`\n\n**Deprecation, not breaking.** `NetworkOperator` still works unchanged. No forced migration, no removal date. It receives no new features or design updates.\n\n**When you encounter `NetworkOperator`:** replace with `Combobox` — build the option list from `NetworkOperator`'s `options` shape (`{ value, label, image? }`); `image` maps to `Combobox`'s per-option `icon` (pass an `<img>` element).\n\n```diff\n-import { NetworkOperator } from \"@rfdtech/components\";\n+import { Combobox } from \"@rfdtech/components\";\n\n-<NetworkOperator value={operator} onChange={setOperator} options={operators} />\n+<Combobox\n+ aria-label=\"Network operator\"\n+ options={operators.map((o) => ({\n+ value: o.value,\n+ label: o.label,\n+ icon: o.image ? <img src={o.image} alt=\"\" /> : undefined,\n+ }))}\n+ value={operator}\n+ onValueChange={setOperator}\n+/>\n```\n\n## Adopting the new design system\n\nThis is the actual \"v2\" migration. §1–§7 are about not breaking on version bump. This section is about looking like the new design system. None required for existing screens to compile. All required for new/touched screens.\n\n### Layout shell: `AppLayout variant=\"stacked\"` + `AppHeader variant=\"plain\"` + `Sidebar variant=\"plain\"`\n\nAdopt as a set, not individually:\n\n```diff\n-<AppLayout>\n+<AppLayout variant=\"stacked\">\n <AppHeader variant=\"plain\">...</AppHeader>\n <AppSidebar>\n <SidebarOverlay />\n- <Sidebar>\n+ <Sidebar variant=\"plain\">\n ...\n </Sidebar>\n </AppSidebar>\n <AppBody><Outlet /></AppBody>\n </AppLayout>\n```\n\n- **`AppLayout variant=\"stacked\"`**: header spans full width on top, sidebar + content side by side below. Default (`\"default\"`) runs sidebar full-height on left, header only spans content column.\n- **`AppHeader variant=\"plain\"`**: square corners, `--clet-primary` background, on-primary text. Continuous top bar with branding.\n- **`Sidebar variant=\"plain\"`**: transparent bg + right border instead of panel surface. Removes boxed-in panel-inside-panel look.\n\n**`SidebarGroup collapsible`:** All nav groups use accordion sections, not flat lists. Initialize with `Main` group expanded, others collapsed.\n\n**`SidebarFooter` must host a `ProfilePopover`:** Use `fullName`/`email` mode (not `user`/`variant`). Full name, email, role get stable real-estate-rich home here. Never omit sidebar footer — this is new relative to the old default layout shell (`DemoLayout`), which never required one.\n\n**No `SidebarTrigger`:** The old default layout shell renders `<SidebarTrigger>Menu</SidebarTrigger>` to manually toggle the sidebar. The new shell (`DemoLayout2`) drops it entirely — don't render `SidebarTrigger` in new/touched layouts. `SidebarProvider` is used with its defaults and no manual collapse control.\n\n**`SidebarBadge` — numeric counts only, not decorative text:** Compose it inside a `SidebarLink`'s children (`{link.badge && <SidebarBadge>{link.badge}</SidebarBadge>}`). Reserve it for a live count of actionable items on that nav destination (e.g. `8` unread alerts) — the number should reflect real pending/important items, not a static placeholder. The one exception is a short text badge like `\"New\"` for a feature callout (e.g. on a newly-added \"Documentation\" link) — don't invent other text badges beyond that pattern.\n\n**Branding lives in `AppHeader` via `AppHeaderBranding`, NOT in sidebar:**\n\n```tsx\n<AppHeader variant=\"plain\">\n <AppHeaderBranding\n logo={<img src=\"/clet-logo.png\" alt=\"\" width={28} height={28} />}\n title=\"YOUR PORTAL\"\n subtitle=\"Your Product Name\"\n />\n <AppHeaderActions>...</AppHeaderActions>\n</AppHeader>\n```\n\n`Sidebar` has NO `SidebarHeader`/`SidebarBrand` when `AppHeaderBranding` is used. Same logo must not appear twice on screen. **Use the current brand logo (`clet-logo.png`) for any new system** — `gsl-logo.png` is the older mark used only by the legacy default layout shell (`DemoLayout`); don't reuse it for new work.\n\n### Use built-in `loading` states — never hand-roll placeholders\n\nEvery component below supports a real `loading` prop with shimmering skeleton. Use it. Never render `\"...\"` placeholders, hand-rolled `<Spinner />`, or omit-until-ready.\n\n| Component | Loading prop | Replaces |\n| ----------- | ------------- | ---------- |\n| `Launchpad` / `AppSwitcher` | `loading` | Disabled/empty trigger or nothing until apps arrive |\n| `AppHeaderNotifications` | `loading` | Empty popover or hand-rolled spinner |\n| `ProfilePopover` | `loading` + `loadingLabel` | `\"...\"` or nothing for name/email/avatar |\n| `SidebarLink` | `loading` + `loadingLabel` | Not rendering nav items until data resolves, or full-page spinner |\n| `MetricCard` | `loading` + `loadingLabel` | `\"...\"` or `\"N/A\"` in `value`/`description` |\n| `TableContent` | `loading` + `loadingRows` | Empty table, spinner overlay, or hide-until-ready |\n\n**SidebarLink pattern:**\n\n```tsx\n{navLoading ? (\n <SidebarGroup>\n {Array.from({ length: 6 }).map((_, i) => (\n <SidebarLink key={i} loading icon={<span />}>Loading</SidebarLink>\n ))}\n </SidebarGroup>\n) : (\n navGroups.map((group) => /* real nav */)\n)}\n```\n\n### One role switcher, consistent across all three locations\n\n`RoleSelect` is required `children` on `Launchpad`, and accepted `children` on `ProfilePopover` (header) and `ProfilePopover` (sidebar footer). **Whenever a system allows a user to have more than one role — true for most systems — wire the same `RoleSelect` state into all three: `Launchpad`, the header `ProfilePopover`, and the sidebar footer `ProfilePopover`.** Don't add it to only one or two.\n\n```tsx\nconst [selectedRole, setSelectedRole] = useState(roles[0].id);\nconst roleSelectProps = { title: \"View as\", roles, selectedRole, onClickRole: (r) => setSelectedRole(r.id) };\n\n// Header\n<ProfilePopover variant=\"avatar\" user={user} onSignOut={onSignOut}>\n <RoleSelect {...roleSelectProps} />\n</ProfilePopover>\n\n// Sidebar footer\n<SidebarFooter>\n <ProfilePopover fullName={user.name} email={user.email} onSignOut={onSignOut}>\n <RoleSelect {...roleSelectProps} />\n </ProfilePopover>\n</SidebarFooter>\n\n// Launchpad — RoleSelect is required here, not optional\n<Launchpad apps={apps} onAppSelect={onAppSelect}>\n <RoleSelect {...roleSelectProps} />\n</Launchpad>\n```\n\nNeither `ProfilePopover` above passes `items` — both render the default \"My Profile\"/\"Account Settings\"/\"Help and Support\" rows. Only add `items` if the system needs different rows.\n\n**Header `ProfilePopover` uses `variant=\"avatar\"` when sidebar footer popover exists.** Reserve `variant=\"full\"` for headers with NO sidebar footer.\n\n### Table family — adopt together\n\n| Component | Prefer | Over |\n| ----------- | -------- | ------ |\n| `TableContent` | `variant=\"panel\"` | `\"default\"` |\n| `TableFilter` | `variant=\"spread\"` | `\"popover\"` (default) |\n| `TableFooter` | `className=\"clet-table__footer--no-border\"` | default top border |\n\n```diff\n <Table paramPrefix=\"members\">\n <TableHeader>\n <TableSearch placeholder=\"Search members...\" />\n- <TableFilter>\n+ <TableFilter variant=\"spread\">\n <Dropdown name=\"role\" ... />\n </TableFilter>\n </TableHeader>\n- <TableContent variant=\"panel\" columns={columns} data={data} rowKey={...} />\n+ <TableContent variant=\"panel\" columns={columns} data={data} rowKey={...} />\n- <TableFooter>\n+ <TableFooter className=\"clet-table__footer--no-border\">\n <TablePagination ... />\n </TableFooter>\n </Table>\n```\n\n`clet-table__footer--no-border` is a real library class (`.clet-table__footer--no-border { border-top: none; }`), not a demo hack.\n\n### Other preferred variants\n\n| Component | Prefer | Over |\n|-----------|--------|------|\n| `MetricCard` | `variant=\"outline\"` | `\"default\"` |\n| `Tabs` | `variant=\"pill\"` | `\"default\"` |\n\nUse `SectionHeader` (with `SectionTitle`/`SectionDescription`/`SectionActions`) for page headers — never hand-rolled `<div>`s.\n\nAll variants above are additive — no deadline to migrate old screens. But any screen built fresh or substantially touched must use these variants.\n\n## Checklist\n\n**Compatibility (keeps existing screens compiling — do when touching file):**\n\n- [ ] Migrate `AppHeaderProfile` → `ProfilePopover` where convenient (no removal date) (§1)\n- [ ] Add `RoleSelect` to `Launchpad` when migrating it — it's required now, not optional (§6)\n- [ ] Wire `onProfileClick`/`onSettingsClick`/`onHelpClick` handlers to `onMyProfile`/`onAccountSettings`/`onHelpAndSupport` — the three rows render by default now, don't hand-build them via `items` (§1)\n- [ ] Convert every `AppSwitcher` → `Launchpad` when possible (§6) — `AppSwitcher` is deprecated\n- [ ] Pass `maxItems={apps.length}` to any `AppSwitcher` with >6 apps, or switch to `Launchpad` (§3)\n- [ ] Add explicit `rowActions` Select/Deselect to `selectable`-only tables that need it (§4)\n- [ ] Decide to keep or override new brand colors/typography (§5)\n- [ ] If the project already has `--clet-*` token overrides, list every known-token override found and ask the user per-token whether to keep it or adopt the new default — never decide silently (§5)\n\n**Design system adoption (the actual \"looks like v2\" bar — for new/touched screens):**\n\n- [ ] New `AppSwitcher`-like work uses `Launchpad` first (§6) — `AppSwitcher` is deprecated, no new features\n- [ ] New/rebuilt layout shells use `AppLayout variant=\"stacked\"` + `AppHeader variant=\"plain\"` + `Sidebar variant=\"plain\"` + collapsible `SidebarGroup`s together (\"Layout shell\")\n- [ ] Every layout has `SidebarFooter` with `ProfilePopover` in `fullName`/`email` mode — never omit it (\"Layout shell\")\n- [ ] No `SidebarTrigger` in new layouts — that's an old default-layout-shell-only pattern (\"Layout shell\")\n- [ ] `SidebarBadge` shows a real numeric count of actionable items, not decorative text (`\"New\"` is the one exception) (\"Layout shell\")\n- [ ] Branding in `AppHeader` via `AppHeaderBranding` using the current logo (`clet-logo.png`), never duplicated in sidebar, never the legacy `gsl-logo.png` (\"Layout shell\")\n- [ ] Every loading-props table component uses its real `loading` prop — never hand-rolled `\"...\"` or spinner (\"Use built-in loading states\")\n- [ ] `RoleSelect` wired into all three locations (`Launchpad`, header `ProfilePopover`, sidebar footer `ProfilePopover`) from the same state, whenever the system allows more than one role — not just one or two of the three (\"One role switcher\")\n- [ ] Header `ProfilePopover` uses `variant=\"avatar\"` when sidebar footer exists (\"One role switcher\")\n- [ ] Neither `ProfilePopover` passes a custom `items` array unless the system needs rows other than the built-in \"My Profile\"/\"Account Settings\"/\"Help and Support\" defaults (\"One role switcher\")\n- [ ] New/touched `Table`s use all three: `TableContent variant=\"panel\"`, `TableFilter variant=\"spread\"`, `TableFooter className=\"clet-table__footer--no-border\"`\n- [ ] `MetricCard variant=\"outline\"`, `Tabs variant=\"pill\"` on new usage\n- [ ] New page headers use `SectionHeader`/`SectionTitle`/`SectionDescription`/`SectionActions`\n- [ ] `Launchpad`'s expand button shows its icon always, its \"See more\" label only once `apps.length` exceeds 9 — never the old bordered \"See all\" button",
1828
+ "docs": "# Migration guide: v1 → v2 — Agent Instructions\n\nAgent-only. All breaking changes and deprecations between `1.22.0` and current, organized as directives. See [Changelog](/docs/changelog) for dated list.\n\n## 1. `AppHeaderProfile` → `ProfilePopover`\n\n**Deprecation, not breaking.** `AppHeaderProfile` still works (thin wrapper around `ProfilePopover`). No forced migration, no removal date. `ProfilePopover` gets all new features.\n\n**When you encounter `AppHeaderProfile`:**\n\n- Replace with `ProfilePopover` using `user` and `variant` props for same trigger\n- `onProfileClick`/`onSettingsClick`/`onHelpClick` map directly to `ProfilePopover`'s `onMyProfile`/`onAccountSettings`/`onHelpAndSupport` — `items` now defaults to those three rows built in (\"My Profile\", \"Account Settings\", \"Help and Support\"), so you no longer hand-build them via `items`\n- `headerAction` prop silently ignored on both — delete it. Popover header now always shows built-in theme toggle when `ThemeProvider` is in tree\n- Sign Out now opens a \"Confirm Sign Out\" dialog before calling `onSignOut` — pass `noConfirmSignOut` to keep the old immediate-fire behavior\n\n```diff\n-import { AppHeaderProfile } from \"@rfdtech/components\";\n+import { ProfilePopover } from \"@rfdtech/components\";\n\n-<AppHeaderProfile user={user} variant=\"avatar\">\n- <RoleSelect roles={roles} selectedRole={role} onClickRole={setRole} />\n-</AppHeaderProfile>\n+<ProfilePopover user={user} variant=\"avatar\">\n+ <RoleSelect roles={roles} selectedRole={role} onClickRole={setRole} />\n+</ProfilePopover>\n```\n\n`items`, `loading`/`loadingLabel`, `onSignOut`, `children` carry over unchanged.\n\n**Wiring `onProfileClick`/`onSettingsClick`/`onHelpClick` to the built-in default rows:**\n\n```tsx\n<ProfilePopover\n user={user}\n variant=\"avatar\"\n onMyProfile={goToProfile}\n onAccountSettings={goToSettings}\n onHelpAndSupport={goToHelp}\n onSignOut={signOut}\n/>\n```\n\nOnly pass `items` if you need rows *other than* \"My Profile\"/\"Account Settings\"/\"Help and Support\" — a passed-in `items` array fully replaces the three defaults, it does not merge with them. Prefer the defaults (via `onMyProfile`/`onAccountSettings`/`onHelpAndSupport`) unless the system genuinely needs different rows.\n\nOnly `\"full\"` and `\"avatar\"` variants remain on both components (no `\"basic\"`).\n\n## 2. `AppItem.icon` → `SystemAppIcon` or `AppIconTile`\n\n`AppSwitcher`'s `AppItem.icon` now accepts `ReactNode` only — closed to `<SystemAppIcon name=\"...\" />` (auto-generated initials tile) or `<AppIconTile name=\"...\">custom glyph</AppIconTile>`. Every app icon renders on the shared 9-gradient/9-overlay tile system.\n\n```diff\n- { id: \"analytics\", name: \"Analytics\", icon: \"https://cdn.example.com/analytics.png\" },\n+ { id: \"analytics\", name: \"Analytics\", icon: <SystemAppIcon name=\"Analytics\" /> },\n- { id: \"custom\", name: \"Custom App\", icon: <MyCustomLogo /> },\n+ { id: \"custom\", name: \"Custom App\", icon: <AppIconTile name=\"Custom App\"><MyCustomLogo /></AppIconTile> },\n];\n```\n\n## 3. `AppSwitcher` maxItems default is 6\n\nDefault `maxItems` is 6. Migrate grids with >6 apps:\n\n```diff\n-<AppSwitcher apps={apps} />\n+<AppSwitcher apps={apps} maxItems={apps.length} />\n```\n\nBetter: switch to `Launchpad` (see §6).\n\n## 4. `Table`: selection-only loses kebab column\n\n`Table` with `selectable` but no `rowActions` no longer renders an empty kebab column. If you relied on the Select/Deselect entry there, add explicit row action:\n\n```diff\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n+ rowActions={[\n+ { id: \"select\", label: isSelected ? \"Deselect\" : \"Select\", onClick: toggleRow },\n+ ]}\n />\n```\n\n## 5. Rebrand: new default colors and typography\n\n**If no `--clet-*` overrides set:**\n\n- `--clet-primary`/`--clet-primary-light`: red (`#dc2626`/`#ef4444`) → navy (`#051b2c`/`#4a7fa8`)\n- New `--clet-secondary` (gold `#c8a24b`) — default `SidebarLink` active-link color\n- New typography: `--clet-font-header` (Roboto Serif), `--clet-font-body` (Lato) via `@fontsource/lato`, `@fontsource/roboto-serif`\n\n**To override:** put the call in `src/clet.theme.ts` (see [Theme](/docs/theme#recommended-file-srcclettheme-ts) for the recommended file convention) and side-effect-import it once in your entry point:\n\n```ts title=\"src/clet.theme.ts\"\nimport { cletTheme } from \"@rfdtech/components\";\ncletTheme({ all: { primary: \"#dc2626\", primaryLight: \"#fef2f2\" } });\n```\n\n```ts title=\"src/main.tsx\"\nimport \"@rfdtech/components/style.css\";\nimport \"./clet.theme\";\n```\n\n```css\n:root { --clet-font-header: var(--clet-font); --clet-font-body: var(--clet-font); }\n```\n\n**To keep new defaults: do nothing.** See `.legacy-theme` in `demo/demo.css` for the old red look reference.\n\n**Migrating a project that already has `--clet-*` token overrides?** Find every existing override of\na *known* `--clet-*` token — a `cletTheme()` call (`src/clet.theme.ts` or elsewhere) or hand-written\nCSS setting a real `--clet-*` custom property, cross-checked against `get_tokens()`. This is about\nrecognized library tokens only — not custom/arbitrary CSS variables or component-local one-off\nstyling. List every one found and **ask the user about each token individually** — keep the\nexisting override, or drop it for the new approved default. Don't decide silently, and don't batch\nit into one yes/no: some overrides are deliberate brand choices worth keeping, others only ever\nexisted to match the *old* pre-rebrand defaults and should be dropped now that those defaults\nthemselves changed.\n\n## 6. Prefer `Launchpad` over deprecated `AppSwitcher`\n\n`AppSwitcher` deprecated, no new features. `Launchpad` is the replacement — independent component tree.\n\n**Not a drop-in replacement.** Re-author the app list and trigger:\n\n```diff\n-import { AppSwitcher, SystemAppIcon } from \"@rfdtech/components\";\n+import { Launchpad, SystemLaunchpadIcon } from \"@rfdtech/components\";\n\n const apps = [\n- { id: \"analytics\", name: \"Analytics\", icon: <SystemAppIcon name=\"Analytics\" /> },\n+ { id: \"analytics\", name: \"Analytics\", icon: <SystemLaunchpadIcon name=\"Analytics\" /> },\n ];\n\n-<AppSwitcher apps={apps} maxItems={apps.length} footer={<CustomFooter />} />\n+<Launchpad apps={apps} onAppSelect={(app) => navigate(app.href)}>\n+ <RoleSelect roles={roles} selectedRole={role} onClickRole={setRole} />\n+</Launchpad>\n```\n\n**Launchpad differences:**\n\n- Icon type: `SystemLaunchpadIcon`/`LaunchpadIconTile` only (not `SystemAppIcon`/`AppIconTile`)\n- Grid: fixed 3×3 (9 apps), not configurable. The expand button opens a 5-column modal with every app\n- No `maxItems`/`columns`/generic `footer` slot. `children` is **required** and only accepts a single `RoleSelect` — omitting it is a type error, not just a lint warning\n- No `title`/`triggerLabel`/`loadingLabel`/`onSeeAll`/`seeAllLabel`/`placement`/`closeOnSelect`\n- Title always \"Launchpad\", trigger always \"Open Launchpad\", selecting app always closes popover\n- No bordered \"See all\" footer button anymore. A small ghost `sm` button sits at the top right of the panel next to the title — its expand icon is always visible, and its \"See more\" text label only appears once `apps.length` exceeds the 9-app cap. Never force the label always-on or reintroduce the old bordered button.\n\n**Stay on `AppSwitcher` if you need: flexible column count, uncapped grid without \"See all\" modal, or custom `footer`. Check with team before building new custom-footer `AppSwitcher` usage.**\n\n## 7. `CountrySelector` → `Combobox` or `PhoneNumberInput`'s country picker\n\n**Deprecation, not breaking.** `CountrySelector` still works unchanged. No forced migration, no removal date. It receives no new features or design updates.\n\n**When you encounter `CountrySelector`:**\n\n- For a standalone country picker (not tied to a phone number), replace with `Combobox` — build the option list from the `countries` shape (`{ value: code, label: name }`) and pass it via `options`\n- For phone-number entry, use `PhoneNumberInput` directly — its built-in country picker already covers this case; don't pair a separate `CountrySelector` with a plain text input\n- `CountrySelector`'s flags are emoji; `Combobox` has no built-in flag rendering — pass a flag as each option's `icon` (e.g. from `country-flag-icons/react/3x2`, the same package `PhoneNumberInput` now uses) if flags are needed\n\n```diff\n-import { CountrySelector } from \"@rfdtech/components\";\n+import { Combobox } from \"@rfdtech/components\";\n+import { countries } from \"some-country-data-source\";\n\n-<CountrySelector value={country} onChange={setCountry} />\n+<Combobox\n+ aria-label=\"Country\"\n+ options={countries.map((c) => ({ value: c.code, label: c.name }))}\n+ value={country}\n+ onValueChange={setCountry}\n+/>\n```\n\n## 8. `NetworkOperator` → `Combobox`\n\n**Deprecation, not breaking.** `NetworkOperator` still works unchanged. No forced migration, no removal date. It receives no new features or design updates.\n\n**When you encounter `NetworkOperator`:** replace with `Combobox` — build the option list from `NetworkOperator`'s `options` shape (`{ value, label, image? }`); `image` maps to `Combobox`'s per-option `icon` (pass an `<img>` element).\n\n```diff\n-import { NetworkOperator } from \"@rfdtech/components\";\n+import { Combobox } from \"@rfdtech/components\";\n\n-<NetworkOperator value={operator} onChange={setOperator} options={operators} />\n+<Combobox\n+ aria-label=\"Network operator\"\n+ options={operators.map((o) => ({\n+ value: o.value,\n+ label: o.label,\n+ icon: o.image ? <img src={o.image} alt=\"\" /> : undefined,\n+ }))}\n+ value={operator}\n+ onValueChange={setOperator}\n+/>\n```\n\n## 9. Naming rename: `.gsl-*` → `.clet-*`, `gslTheme()` → `cletTheme()`\n\n**Non-breaking.** Every `.gsl-*` CSS class, `--gsl-*` custom property, `data-gsl-theme` attribute, `.gsl-theme` root class, and the `gslTheme()` theming API function was renamed to its `.clet-*`/`--clet-*`/`data-clet-theme`/`.clet-theme`/`cletTheme()` counterpart. **Every old name keeps working, unchanged, with no forced migration and no removal date** — this is a permanent compatibility alias, not a deprecation:\n\n- **Classes**: every rendered element carries both classes (e.g. `class=\"clet-sidebar gsl-sidebar\"`) — existing `.gsl-*` selectors in your own CSS keep matching without any change.\n- **Custom properties**: every internal `var(--clet-<name>)` read resolves as `var(--gsl-<name>, var(--clet-<name>))` — overriding either `--gsl-<name>` or `--clet-<name>` (globally or per-component) works identically; `--gsl-<name>` wins if both are set.\n- **Theme attribute/root class**: `ThemeProvider` sets both `data-clet-theme` and `data-gsl-theme` on `<html>` and its wrapper `<div>`, carrying both `clet-theme` and `gsl-theme` classes.\n- **JS API**: `gslTheme()` is now an alias of `cletTheme()` (same function, same behavior). Legacy type names (`GslTheme`, `GslThemeConfig`, `GslComponentThemeConfig`, `GslComponentThemeOverrides`, `ResolvedGslTheme`, `GslColorValue`, `GslComponentTokenMap`, `GslGlobalTokens`, `GslLengthValue`, `GslOpacityValue`, `GslShadowValue`, `GslStringValue`, `GslZIndexValue`) are re-exported as aliases of their `Clet*` equivalents.\n\n**What to actually do:** nothing is required for existing code to keep working. For new or touched code, prefer the `clet-*` spelling — it's the preferred name going forward, even though `gsl-*` isn't scheduled for removal:\n\n```diff\n-gslTheme({ all: { primary: \"#083755\" } });\n-import type { GslThemeConfig } from \"@rfdtech/components\";\n+cletTheme({ all: { primary: \"#083755\" } });\n+import type { CletThemeConfig } from \"@rfdtech/components\";\n```\n\nSee [Theme](/docs/theme) and [Customize tokens](/README.md#7-customize-tokens) for the full token reference; every color token also has a `--clet-*` alias documented there.\n\n## Adopting the new design system\n\nThis is the actual \"v2\" migration. §1–§9 are about not breaking on version bump. This section is about looking like the new design system. None required for existing screens to compile. All required for new/touched screens.\n\n### Layout shell: `AppLayout variant=\"stacked\"` + `AppHeader variant=\"plain\"` + `Sidebar variant=\"plain\"`\n\nAdopt as a set, not individually:\n\n```diff\n-<AppLayout>\n+<AppLayout variant=\"stacked\">\n <AppHeader variant=\"plain\">...</AppHeader>\n <AppSidebar>\n- <SidebarOverlay />\n- <Sidebar>\n+ <Sidebar variant=\"plain\">\n ...\n </Sidebar>\n </AppSidebar>\n <AppBody><Outlet /></AppBody>\n </AppLayout>\n```\n\n- **`AppLayout variant=\"stacked\"`**: header spans full width on top, sidebar + content side by side below. Default (`\"default\"`) runs sidebar full-height on left, header only spans content column.\n- **`AppHeader variant=\"plain\"`**: square corners, `--clet-primary` background, on-primary text. Continuous top bar with branding.\n- **`Sidebar variant=\"plain\"`**: transparent bg + right border instead of panel surface. Removes boxed-in panel-inside-panel look.\n\n**`<SidebarOverlay />` is now optional and rendered internally by `Sidebar` on mobile.** The old default shell required you to remember to drop `<SidebarOverlay />` into `<AppSidebar>` — forgetting it left the drawer with no backdrop. `Sidebar` now renders its own mobile overlay as a sibling of the `<aside>` (still null on desktop), so a missing explicit `<SidebarOverlay />` is harmless. If you do render one, two overlays stack — both close the drawer on click, no visual artifact — so existing code keeps working unchanged.\n\n**`SidebarGroup collapsible`:** All nav groups use accordion sections, not flat lists. Initialize with `Main` group expanded, others collapsed.\n\n**`SidebarFooter` must host a `ProfilePopover`:** Use `fullName`/`email` mode (not `user`/`variant`). Full name, email, role get stable real-estate-rich home here. Never omit sidebar footer — this is new relative to the old default layout shell (`DemoLayout`), which never required one.\n\n**No `SidebarTrigger`:** The old default layout shell renders `<SidebarTrigger>Menu</SidebarTrigger>` to manually toggle the sidebar. The new shell (`DemoLayout2`) drops it entirely — don't render `SidebarTrigger` in new/touched layouts. `SidebarProvider` is used with its defaults and no manual collapse control.\n\n**`SidebarBadge` — numeric counts only, not decorative text:** Compose it inside a `SidebarLink`'s children (`{link.badge && <SidebarBadge>{link.badge}</SidebarBadge>}`). Reserve it for a live count of actionable items on that nav destination (e.g. `8` unread alerts) — the number should reflect real pending/important items, not a static placeholder. The one exception is a short text badge like `\"New\"` for a feature callout (e.g. on a newly-added \"Documentation\" link) — don't invent other text badges beyond that pattern.\n\n**Branding lives in `AppHeader` via `AppHeaderBranding`; the sidebar mirrors it to mobile via a `mobileHeader` prop:**\n\n```tsx\n<AppHeader variant=\"plain\">\n <AppHeaderBranding\n logo={<img src=\"/clet-logo.png\" alt=\"\" width={28} height={28} />}\n title=\"YOUR PORTAL\"\n subtitle=\"Your Product Name\"\n />\n <AppHeaderActions>...</AppHeaderActions>\n</AppHeader>\n```\n\nOn desktop, the plain `Sidebar` has NO `SidebarHeader`/`SidebarBrand` — same logo must not appear twice on screen.\n\nOn mobile the sidebar becomes a full-overlay drawer and the `AppHeader` collapses to just the hamburger. Pass the brand content to the plain `Sidebar` via `mobileHeader` and the library wraps it in a mobile-only `SidebarHeader` (hidden on desktop via the built-in `clet-sidebar__header--mobile-only` class):\n\n```tsx\n<Sidebar\n variant=\"plain\"\n mobileHeader={\n <SidebarBrand>\n <img src=\"/clet-logo.png\" alt=\"\" width={28} height={28} />\n <span>YOUR PORTAL</span>\n </SidebarBrand>\n }\n>\n <SidebarContent>...</SidebarContent>\n <SidebarFooter>...</SidebarFooter>\n</Sidebar>\n```\n\n- The `mobileHeader` prop only renders when `Sidebar variant=\"plain\"` and the consumer hasn't already supplied their own `<SidebarHeader>` child — add your own `<SidebarHeader>` to fully own the mobile header content.\n- The `clet-sidebar__header--mobile-only` class is now a built-in library class. No consumer CSS is required — the library hides it on desktop via the existing `@media (width >= 769px)` block in `src/components/sidebar/styles/sidebar.css`. The older migration recipe that required a hand-written `@media` rule no longer applies.\n- The `AppLayout` does **not** auto-extract the `AppHeaderBranding` and forward it to the sidebar. The two brand blocks (`AppHeaderBranding` for the header, `SidebarBrand` for the mobile drawer) are intentionally separate — they live in different visual contexts with different styling. Compose both, with the same logo/title content.\n\n**Use the current brand logo (`clet-logo.png`) for any new system** — `gsl-logo.png` is the older mark used only by the legacy default layout shell (`DemoLayout`); don't reuse it for new work.\n\n### Use built-in `loading` states — never hand-roll placeholders\n\nEvery component below supports a real `loading` prop with shimmering skeleton. Use it. Never render `\"...\"` placeholders, hand-rolled `<Spinner />`, or omit-until-ready.\n\n| Component | Loading prop | Replaces |\n| ----------- | ------------- | ---------- |\n| `Launchpad` / `AppSwitcher` | `loading` | Disabled/empty trigger or nothing until apps arrive |\n| `AppHeaderNotifications` | `loading` | Empty popover or hand-rolled spinner |\n| `ProfilePopover` | `loading` + `loadingLabel` | `\"...\"` or nothing for name/email/avatar |\n| `SidebarLink` | `loading` + `loadingLabel` | Not rendering nav items until data resolves, or full-page spinner |\n| `MetricCard` | `loading` + `loadingLabel` | `\"...\"` or `\"N/A\"` in `value`/`description` |\n| `TableContent` | `loading` + `loadingRows` | Empty table, spinner overlay, or hide-until-ready |\n\n**SidebarLink pattern:**\n\n```tsx\n{navLoading ? (\n <SidebarGroup>\n {Array.from({ length: 6 }).map((_, i) => (\n <SidebarLink key={i} loading icon={<span />}>Loading</SidebarLink>\n ))}\n </SidebarGroup>\n) : (\n navGroups.map((group) => /* real nav */)\n)}\n```\n\n### One role switcher, consistent across all three locations\n\n`RoleSelect` is required `children` on `Launchpad`, and accepted `children` on `ProfilePopover` (header) and `ProfilePopover` (sidebar footer). **Whenever a system allows a user to have more than one role — true for most systems — wire the same `RoleSelect` state into all three: `Launchpad`, the header `ProfilePopover`, and the sidebar footer `ProfilePopover`.** Don't add it to only one or two.\n\n```tsx\nconst [selectedRole, setSelectedRole] = useState(roles[0].id);\nconst roleSelectProps = { title: \"View as\", roles, selectedRole, onClickRole: (r) => setSelectedRole(r.id) };\n\n// Header\n<ProfilePopover variant=\"avatar\" user={user} onSignOut={onSignOut}>\n <RoleSelect {...roleSelectProps} />\n</ProfilePopover>\n\n// Sidebar footer\n<SidebarFooter>\n <ProfilePopover fullName={user.name} email={user.email} onSignOut={onSignOut}>\n <RoleSelect {...roleSelectProps} />\n </ProfilePopover>\n</SidebarFooter>\n\n// Launchpad — RoleSelect is required here, not optional\n<Launchpad apps={apps} onAppSelect={onAppSelect}>\n <RoleSelect {...roleSelectProps} />\n</Launchpad>\n```\n\nNeither `ProfilePopover` above passes `items` — both render the default \"My Profile\"/\"Account Settings\"/\"Help and Support\" rows. Only add `items` if the system needs different rows.\n\n**Header `ProfilePopover` uses `variant=\"avatar\"` when sidebar footer popover exists.** Reserve `variant=\"full\"` for headers with NO sidebar footer.\n\n### Table family — adopt together\n\n| Component | Prefer | Over |\n| ----------- | -------- | ------ |\n| `TableContent` | `variant=\"panel\"` | `\"default\"` |\n| `TableFilter` | `variant=\"spread\"` | `\"popover\"` (default) |\n| `TableFooter` | `noBorder` | default top border |\n\n```diff\n <Table paramPrefix=\"members\">\n <TableHeader>\n <TableSearch placeholder=\"Search members...\" />\n- <TableFilter>\n+ <TableFilter variant=\"spread\">\n <Dropdown name=\"role\" ... />\n </TableFilter>\n </TableHeader>\n- <TableContent variant=\"panel\" columns={columns} data={data} rowKey={...} />\n+ <TableContent variant=\"panel\" columns={columns} data={data} rowKey={...} />\n- <TableFooter>\n+ <TableFooter noBorder>\n <TablePagination ... />\n </TableFooter>\n </Table>\n```\n\n`noBorder` is a real `TableFooterProps` prop (applies `.clet-table__footer--no-border { border-top: none; }` internally), not a demo hack. Older code passing `className=\"clet-table__footer--no-border\"` by hand still works — prefer `noBorder` in new/touched code.\n\n### Other preferred variants\n\n| Component | Prefer | Over |\n|-----------|--------|------|\n| `MetricCard` | `variant=\"outline\"` | `\"default\"` |\n| `Tabs` | `variant=\"pill\"` | `\"default\"` |\n\nUse `SectionHeader` (with `SectionTitle`/`SectionDescription`/`SectionActions`) for page headers — never hand-rolled `<div>`s.\n\n## PageSection — consistent section spacing\n\nEvery distinct content section on a page must be wrapped in `<PageSection>` instead of a\nhand-rolled `<div>` with margin:\n\n```diff\n-<div style={{ marginBottom: 24 }}>\n+<PageSection>\n <MetricCard variant=\"outline\" ... />\n-</div>\n+</PageSection>\n```\n\n`PageSection` applies `gap: var(--clet-app-layout-body-gap)` — no new token, no hardcoded\nvalue. All sections get uniform spacing automatically.\n\n## QuickActions — dashboard action grids\n\nUse `<QuickActions>` for quick-action grids on dashboards. It replaces any hand-rolled\n\"action buttons\" row with a responsive tile grid (1/2/3 columns) and an optional \"Customize\"\ndialog for toggling visibility:\n\n```diff\n-<div className=\"flex gap-4\">\n- <Button>Add Member</Button>\n- <Button variant=\"secondary\">Export</Button>\n-</div>\n+<QuickActions\n+ actions={[\n+ { id: \"add\", label: \"Add Member\", icon: <UserPlus size={18} />, onClick: ... },\n+ { id: \"export\", label: \"Export Data\", icon: <Download size={18} />, onClick: ... },\n+ ]}\n+ customizable\n+/>\n```\n\nThe customize feature is fully controlled — pass `hiddenIds` and `onToggleVisibility` to\nmanage which actions are visible. Persist the state however you like (localStorage, zustand,\netc.).\n\nAll variants above are additive — no deadline to migrate old screens. But any screen built fresh or substantially touched must use these variants.\n\n## Checklist\n\n**Compatibility (keeps existing screens compiling — do when touching file):**\n\n- [ ] Migrate `AppHeaderProfile` → `ProfilePopover` where convenient (no removal date) (§1)\n- [ ] Add `RoleSelect` to `Launchpad` when migrating it — it's required now, not optional (§6)\n- [ ] Wire `onProfileClick`/`onSettingsClick`/`onHelpClick` handlers to `onMyProfile`/`onAccountSettings`/`onHelpAndSupport` — the three rows render by default now, don't hand-build them via `items` (§1)\n- [ ] Convert every `AppSwitcher` → `Launchpad` when possible (§6) — `AppSwitcher` is deprecated\n- [ ] Pass `maxItems={apps.length}` to any `AppSwitcher` with >6 apps, or switch to `Launchpad` (§3)\n- [ ] Add explicit `rowActions` Select/Deselect to `selectable`-only tables that need it (§4)\n- [ ] Decide to keep or override new brand colors/typography (§5)\n- [ ] If the project already has `--clet-*` token overrides, list every known-token override found and ask the user per-token whether to keep it or adopt the new default — never decide silently (§5)\n\n**Design system adoption (the actual \"looks like v2\" bar — for new/touched screens):**\n\n- [ ] New `AppSwitcher`-like work uses `Launchpad` first (§6) — `AppSwitcher` is deprecated, no new features\n- [ ] New/rebuilt layout shells use `AppLayout variant=\"stacked\"` + `AppHeader variant=\"plain\"` + `Sidebar variant=\"plain\"` + collapsible `SidebarGroup`s together (\"Layout shell\")\n- [ ] Every layout has `SidebarFooter` with `ProfilePopover` in `fullName`/`email` mode — never omit it (\"Layout shell\")\n- [ ] No `SidebarTrigger` in new layouts — that's an old default-layout-shell-only pattern (\"Layout shell\")\n- [ ] `<SidebarOverlay />` is no longer required — `Sidebar` renders its own mobile overlay internally (\"Layout shell\")\n- [ ] `SidebarBadge` shows a real numeric count of actionable items, not decorative text (`\"New\"` is the one exception) (\"Layout shell\")\n- [ ] Branding in `AppHeader` via `AppHeaderBranding` using the current logo (`clet-logo.png`), never duplicated in desktop sidebar — pass `mobileHeader` (a `SidebarBrand` with the same logo/title) to the plain `Sidebar` so the brand is visible in the mobile drawer; no consumer CSS needed for the hide-on-desktop rule (\"Layout shell\")\n- [ ] Every loading-props table component uses its real `loading` prop — never hand-rolled `\"...\"` or spinner (\"Use built-in loading states\")\n- [ ] `RoleSelect` wired into all three locations (`Launchpad`, header `ProfilePopover`, sidebar footer `ProfilePopover`) from the same state, whenever the system allows more than one role — not just one or two of the three (\"One role switcher\")\n- [ ] Header `ProfilePopover` uses `variant=\"avatar\"` when sidebar footer exists (\"One role switcher\")\n- [ ] Neither `ProfilePopover` passes a custom `items` array unless the system needs rows other than the built-in \"My Profile\"/\"Account Settings\"/\"Help and Support\" defaults (\"One role switcher\")\n- [ ] New/touched `Table`s use all three: `TableContent variant=\"panel\"`, `TableFilter variant=\"spread\"`, `TableFooter noBorder`\n- [ ] `MetricCard variant=\"outline\"`, `Tabs variant=\"pill\"` on new usage\n- [ ] New page headers use `SectionHeader`/`SectionTitle`/`SectionDescription`/`SectionActions`\n- [ ] Every page section is wrapped in `<PageSection>` — no bare `<div>`s with margin (\"PageSection\")\n- [ ] Quick-action rows use `<QuickActions>` with customizable visibility (\"QuickActions\")\n- [ ] `Launchpad`'s expand button shows its icon always, its \"See more\" label only once `apps.length` exceeds 9 — never the old bordered \"See all\" button",
1764
1829
  "headings": [
1765
1830
  "Migration guide: v1 → v2 — Agent Instructions",
1766
1831
  "1. `AppHeaderProfile` → `ProfilePopover`",
@@ -1771,12 +1836,15 @@
1771
1836
  "6. Prefer `Launchpad` over deprecated `AppSwitcher`",
1772
1837
  "7. `CountrySelector` → `Combobox` or `PhoneNumberInput`'s country picker",
1773
1838
  "8. `NetworkOperator` → `Combobox`",
1839
+ "9. Naming rename: `.gsl-*` → `.clet-*`, `gslTheme()` → `cletTheme()`",
1774
1840
  "Adopting the new design system",
1775
1841
  "Layout shell: `AppLayout variant=\"stacked\"` + `AppHeader variant=\"plain\"` + `Sidebar variant=\"plain\"`",
1776
1842
  "Use built-in `loading` states — never hand-roll placeholders",
1777
1843
  "One role switcher, consistent across all three locations",
1778
1844
  "Table family — adopt together",
1779
1845
  "Other preferred variants",
1846
+ "PageSection — consistent section spacing",
1847
+ "QuickActions — dashboard action grids",
1780
1848
  "Checklist"
1781
1849
  ],
1782
1850
  "types": null,
@@ -2040,6 +2108,55 @@
2040
2108
  ],
2041
2109
  "ruleIds": []
2042
2110
  },
2111
+ "page-section": {
2112
+ "meta": {
2113
+ "slug": "page-section",
2114
+ "title": "PageSection",
2115
+ "description": "Uniform vertical spacing wrapper for page sections",
2116
+ "category": "Components",
2117
+ "keywords": [
2118
+ "page section",
2119
+ "section",
2120
+ "spacing",
2121
+ "layout",
2122
+ "vertical gap"
2123
+ ],
2124
+ "since": "2.1.0",
2125
+ "status": "stable",
2126
+ "related": [
2127
+ "section-header",
2128
+ "quick-actions",
2129
+ "app-layout"
2130
+ ],
2131
+ "import": "@rfdtech/components"
2132
+ },
2133
+ "docs": "# PageSection\n\nWrapper that applies consistent vertical spacing between distinct sections on a page. Uses `--clet-app-layout-body-gap` as the default gap — no new token needed.\n\n## Example\n\n<ExampleTabs\n title=\"Section layout\"\n preview={<PageSectionPreview />}\n code={pageSectionSource}\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `PageSectionClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root |\n\nAlso accepts standard native `<div>` attributes (`id`, `aria-*`, `data-*`, etc.).\n\n## Types\n\n### `PageSectionClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root `<div>` element |\n\n## Notes\n\n- Every distinct content section on a page should be wrapped in its own `PageSection` — never use hand-rolled `<div>`s with margin.\n- The gap value comes from `--clet-app-layout-body-gap` (24px by default). Override it at any level via the same token.\n- Works inside an `<AppBody>`, a standalone page, or nested in any flex-column container.",
2134
+ "headings": [
2135
+ "PageSection",
2136
+ "Example",
2137
+ "Props",
2138
+ "Types",
2139
+ "`PageSectionClassNames`",
2140
+ "Notes"
2141
+ ],
2142
+ "types": "import type { HTMLAttributes } from \"react\";\n\nexport interface PageSectionClassNames {\n root?: string;\n}\n\nexport interface PageSectionProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: PageSectionClassNames;\n className?: string;\n}\n",
2143
+ "examples": [
2144
+ {
2145
+ "file": "page-section.example.tsx",
2146
+ "importName": "pageSectionSource",
2147
+ "title": "Page Section",
2148
+ "components": [
2149
+ "Card",
2150
+ "PageSection",
2151
+ "SectionDescription",
2152
+ "SectionHeader",
2153
+ "SectionTitle"
2154
+ ],
2155
+ "source": "import {\n PageSection,\n SectionHeader,\n SectionTitle,\n SectionDescription,\n Card,\n} from \"@rfdtech/components\";\n\nexport function PageSectionExample() {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 0, width: \"100%\" }}>\n <PageSection>\n <SectionHeader>\n <SectionTitle>Dashboard</SectionTitle>\n <SectionDescription>\n Sections are separated by consistent vertical gap.\n </SectionDescription>\n </SectionHeader>\n </PageSection>\n\n <PageSection>\n <div\n style={{\n height: 80,\n borderRadius: \"var(--clet-radius-xl)\",\n border: \"1px dashed var(--clet-border)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: 14,\n color: \"var(--clet-text-secondary)\",\n }}\n >\n Metric cards or stats grid\n </div>\n </PageSection>\n\n <PageSection>\n <Card bordered>\n <div\n style={{\n height: 120,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: 14,\n color: \"var(--clet-text-secondary)\",\n }}\n >\n Table or content area\n </div>\n </Card>\n </PageSection>\n </div>\n );\n}\n"
2156
+ }
2157
+ ],
2158
+ "ruleIds": []
2159
+ },
2043
2160
  "phone-number-input": {
2044
2161
  "meta": {
2045
2162
  "slug": "phone-number-input",
@@ -2388,6 +2505,54 @@
2388
2505
  ],
2389
2506
  "ruleIds": []
2390
2507
  },
2508
+ "quick-actions": {
2509
+ "meta": {
2510
+ "slug": "quick-actions",
2511
+ "title": "QuickActions",
2512
+ "description": "Quick action grid with optional customize dialog",
2513
+ "category": "Components",
2514
+ "keywords": [
2515
+ "quick actions",
2516
+ "action grid",
2517
+ "shortcuts",
2518
+ "customize",
2519
+ "dashboard actions"
2520
+ ],
2521
+ "since": "2.1.0",
2522
+ "status": "stable",
2523
+ "related": [
2524
+ "page-section",
2525
+ "dashboard-patterns",
2526
+ "button",
2527
+ "dialog",
2528
+ "checkbox"
2529
+ ],
2530
+ "import": "@rfdtech/components"
2531
+ },
2532
+ "docs": "# QuickActions\n\nA responsive grid of quick-action tiles with an optional \"Customize\" dialog for toggling visibility. Designed for dashboard sections where users need one-click access to common tasks.\n\n## Example\n\n<ExampleTabs\n title=\"Quick actions\"\n preview={<QuickActionsPreview />}\n code={quickActionsSource}\n/>\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `actions` | `QuickActionItem[]` | — | Array of action items to display (required) |\n| `title` | `string` | `\"Quick actions\"` | Section heading text |\n| `customizable` | `boolean` | `false` | Shows a \"Customize\" button and dialog |\n| `hiddenIds` | `Set<string>` | — | Currently hidden action IDs (for customize mode) |\n| `onToggleVisibility` | `(id: string) => void` | — | Called when user toggles an action in the customize dialog |\n| `onAction` | `(id: string) => void` | — | Called when a visible action tile is clicked |\n| `customizeLabel` | `string` | `\"Customize\"` | Text for the customize button |\n| `emptyMessage` | `string` | `\"No quick actions available.\"` | Text shown when no actions are visible |\n| `classNames` | `QuickActionsClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root |\n\nAlso accepts standard native `<div>` attributes.\n\n## Types\n\n### `QuickActionItem`\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `id` | `string` | Unique identifier |\n| `label` | `string` | Display text |\n| `icon` | `ReactNode` | Icon element (typically a lucide-react icon at `size={18}`) |\n| `description` | `string` (optional) | Secondary text |\n\n### `QuickActionsClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root wrapper |\n| `header` | Header row (title + customize button) |\n| `title` | Section title text |\n| `customizeButton` | Customize button |\n| `grid` | Action tile grid |\n| `item` | Individual action tile |\n| `itemIcon` | Icon circle inside each tile |\n| `itemContent` | Text content wrapper |\n| `itemLabel` | Action label text |\n| `itemDescription` | Action description text |\n| `dialog` | Customize dialog content |\n| `dialogCheckbox` | Checkbox list in the customize dialog |\n| `empty` | Empty state message |\n\n## Notes\n\n- The grid is responsive: 1 column on mobile, 2 on tablet (640px), 3 on desktop (1024px).\n- The \"Customize\" button opens a `Dialog` with `Checkbox` for each action — visibility state is fully controlled by the consumer via `hiddenIds` / `onToggleVisibility`.\n- Each tile has a primary-colored icon circle, action label, and optional description.\n- Tiles are `<button>` elements, so they participate in keyboard navigation by default.\n- Store the `hiddenIds` state in localStorage, zustand, or any state mechanism — the component is agnostic.",
2533
+ "headings": [
2534
+ "QuickActions",
2535
+ "Example",
2536
+ "Props",
2537
+ "Types",
2538
+ "`QuickActionItem`",
2539
+ "`QuickActionsClassNames`",
2540
+ "Notes"
2541
+ ],
2542
+ "types": "import type { HTMLAttributes, ReactNode } from \"react\";\n\nexport interface QuickActionItem {\n id: string;\n label: string;\n icon: ReactNode;\n description?: string;\n}\n\nexport interface QuickActionsClassNames {\n root?: string;\n header?: string;\n title?: string;\n customizeButton?: string;\n grid?: string;\n item?: string;\n itemIcon?: string;\n itemContent?: string;\n itemLabel?: string;\n itemDescription?: string;\n dialog?: string;\n dialogCheckbox?: string;\n empty?: string;\n}\n\nexport interface QuickActionsProps extends HTMLAttributes<HTMLDivElement> {\n actions: QuickActionItem[];\n title?: string;\n customizable?: boolean;\n hiddenIds?: Set<string>;\n onToggleVisibility?: (id: string) => void;\n onAction?: (id: string) => void;\n customizeLabel?: string;\n emptyMessage?: string;\n classNames?: QuickActionsClassNames;\n className?: string;\n}\n",
2543
+ "examples": [
2544
+ {
2545
+ "file": "quick-actions.example.tsx",
2546
+ "importName": "quickActionsSource",
2547
+ "title": "Quick Actions",
2548
+ "components": [
2549
+ "QuickActions"
2550
+ ],
2551
+ "source": "import { useMemo, useState } from \"react\";\nimport { QuickActions } from \"@rfdtech/components\";\nimport { UserPlus, Download, BarChart3, Shield, History, FileText } from \"lucide-react\";\n\nconst allActions = [\n { id: \"add\", label: \"Add Member\", icon: <UserPlus size={18} strokeWidth={1.5} />, description: \"Create a new member account\" },\n { id: \"export\", label: \"Export Data\", icon: <Download size={18} strokeWidth={1.5} />, description: \"Download member records\" },\n { id: \"reports\", label: \"View Reports\", icon: <BarChart3 size={18} strokeWidth={1.5} />, description: \"Access analytics and reports\" },\n { id: \"roles\", label: \"Manage Roles\", icon: <Shield size={18} strokeWidth={1.5} />, description: \"Configure role permissions\" },\n { id: \"audit\", label: \"Audit Log\", icon: <History size={18} strokeWidth={1.5} />, description: \"Review system audit trail\" },\n { id: \"docs\", label: \"Documents\", icon: <FileText size={18} strokeWidth={1.5} />, description: \"Browse and manage documents\" },\n];\n\nexport function QuickActionsExample() {\n const [hiddenIds, setHiddenIds] = useState<Set<string>>(new Set());\n const visibleActions = useMemo(\n () => allActions.filter((a) => !hiddenIds.has(a.id)),\n [hiddenIds],\n );\n\n return (\n <div style={{ width: \"100%\", maxWidth: 640 }}>\n <QuickActions\n actions={visibleActions}\n customizable\n hiddenIds={hiddenIds}\n onToggleVisibility={(id) => {\n setHiddenIds((prev) => {\n const next = new Set(prev);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n }}\n />\n </div>\n );\n}\n"
2552
+ }
2553
+ ],
2554
+ "ruleIds": []
2555
+ },
2391
2556
  "radio-group": {
2392
2557
  "meta": {
2393
2558
  "slug": "radio-group",
@@ -2704,7 +2869,7 @@
2704
2869
  ],
2705
2870
  "import": "@rfdtech/components"
2706
2871
  },
2707
- "docs": "# Sidebar\n\nCompound sidebar primitives for app shells and section navigation. Desktop shows a sticky card-style rail with optional collapse; mobile uses an offcanvas drawer with a toggle trigger and dismissible overlay.\n\n## Example\n\n<ExampleTabs\n title=\"App shell\"\n preview={<SidebarPreview />}\n code={sidebarSource}\n/>\n\n## Variants\n\n`Sidebar` accepts a `variant` prop that controls its surface style.\n\n- **`\"default\"`** (default) — card panel surface (`--clet-surface-subtle` background, rounded corners).\n- **`\"plain\"`** — transparent background with a right border instead of the card panel, and no border radius. Used for dashboard shells where the sidebar should sit flush against the layout edge.\n\n<ExampleTabs\n title='variant=\"plain\" — transparent background with a right border'\n preview={<SidebarPlainPreview />}\n code={sidebarPlainSource}\n/>\n\n## Loading state\n\nSet `loading` on `SidebarLink` to shimmer an icon+label placeholder row instead of the real link — useful while nav items are still being fetched. The placeholder is non-interactive and ignores `icon`/`children`/`to`/`onClick`/`active`.\n\n<ExampleTabs\n title=\"Loading state\"\n preview={<SidebarLoadingPreview />}\n code={sidebarLoadingSource}\n/>\n\n## React Router\n\nUse `asChild` on `SidebarLink` to merge styles onto `NavLink`:\n\n```tsx\n<SidebarLink asChild active={isActive}>\n <NavLink to=\"/dashboard\">Dashboard</NavLink>\n</SidebarLink>\n```\n\n## Components\n\n### `SidebarProvider`\n\nContext wrapper for open state, collapsed state, and mobile detection.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | — | Controlled open state (mobile drawer) |\n| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |\n| `onOpenChange` | `(open: boolean) => void` | — | Called when open state changes |\n| `collapsed` | `boolean` | — | Controlled collapsed state (desktop) |\n| `defaultCollapsed` | `boolean` | `false` | Initial collapsed state (uncontrolled) |\n| `onCollapsedChange` | `(collapsed: boolean) => void` | — | Called when collapsed state changes |\n| `breakpoint` | `number` | `768` | Max width in px treated as mobile |\n| `classNames` | `SidebarProviderClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the provider wrapper |\n| `children` | `ReactNode` | — | Trigger, overlay, sidebar, and page content |\n\nAlso exports `useSidebar()` for programmatic open/close and collapse.\n\n### `Sidebar`\n\nRoot `<aside>`. Sticky card panel on desktop; fixed offcanvas panel on mobile when open.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `\"default\" \\| \"plain\"` | `\"default\"` | Visual style — see [Variants](#variants) |\n| `classNames` | `SidebarClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the aside |\n| `children` | `ReactNode` | — | Header, content, footer |\n\n### `SidebarCollapse`\n\nDesktop collapse toggle. Hidden on mobile. Sets `aria-expanded` and `aria-controls`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarCollapseClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the button |\n\nAlso accepts standard `button` attributes.\n\n### `SidebarTrigger`\n\nMobile menu button. Hidden on desktop. Sets `aria-expanded` and `aria-controls`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarTriggerClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the button |\n| `children` | `ReactNode` | — | Button label |\n\nAlso accepts standard `button` attributes.\n\n### `SidebarOverlay`\n\nMobile backdrop. Click closes the drawer. Not rendered on desktop.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarOverlayClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the overlay button |\n\n### `SidebarHeader`\n\nTop branding or metadata slot. Use `clet-sidebar__header-brand` and `clet-sidebar__header-title` for logo + title layout alongside `SidebarCollapse`.\n\n### `SidebarContent`\n\nScrollable middle section for navigation.\n\n### `SidebarFooter`\n\nBottom slot separated by a top border. A plain wrapper — typically holds a [`ProfilePopover`](/profile-popover) for the signed-in user's trigger row and menu.\n\n```tsx\n<SidebarFooter>\n <ProfilePopover\n fullName=\"Kwame Asante\"\n email=\"kwame@clet.edu.gh\"\n items={[\n { icon: <User size={20} />, label: \"My Profile\", onClick: () => navigate(\"/profile\") },\n { icon: <Settings size={20} />, label: \"Account Settings\", onClick: () => navigate(\"/settings\") },\n ]}\n onSignOut={handleSignOut}\n >\n <RoleSelect\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n</SidebarFooter>\n```\n\n### `SidebarNav`\n\nNavigation landmark wrapper (`<nav>`).\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `aria-label` | `string` | `\"Sidebar\"` | Accessible name for the nav |\n| `classNames` | `SidebarNavClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the nav |\n\n### `SidebarGroup`\n\nSection wrapper for grouped links. Set `collapsible` to make the first `SidebarGroupLabel` child render as a button that toggles the group's content with an animated open/close.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `collapsible` | `boolean` | `false` | Render the label as a toggle button that animates content open/closed |\n| `defaultExpanded` | `boolean` | `true` | Initial expanded state (uncontrolled) |\n| `expanded` | `boolean` | — | Controlled expanded state |\n| `onExpandedChange` | `(expanded: boolean) => void` | — | Called when expanded state changes |\n| `classNames` | `SidebarGroupClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the group wrapper |\n| `children` | `ReactNode` | — | Optional `SidebarGroupLabel` + nav items |\n\n### `SidebarGroupLabel`\n\nUppercase section title. Renders as a `<p>` by default, or as a `<button>` toggle when the parent `SidebarGroup` has `collapsible`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarGroupLabelClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the label |\n| `children` | `ReactNode` | — | Label text |\n\n```tsx\n<SidebarGroup collapsible defaultExpanded={false}>\n <SidebarGroupLabel>Advanced</SidebarGroupLabel>\n <SidebarItem>\n <SidebarLink>API Keys</SidebarLink>\n </SidebarItem>\n</SidebarGroup>\n```\n\n### `SidebarItem`\n\nRow wrapper for a single link.\n\n### `SidebarBadge`\n\nTrailing count or label pill inside `SidebarLink`. Must be a direct child of `SidebarLink`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarBadgeClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the badge |\n| `children` | `ReactNode` | — | Badge text (e.g. `\"New\"`, `12`) |\n\n```tsx\n<SidebarLink href=\"/notifications\" icon={<Bell size={20} />}>\n Notification Templates\n <SidebarBadge>New</SidebarBadge>\n</SidebarLink>\n```\n\n### `SidebarLink`\n\nStyled navigation link.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `active` | `boolean` | `false` | Applies active link styling |\n| `asChild` | `boolean` | `false` | Merge props onto the child element |\n| `icon` | `ReactNode` | — | Optional leading icon; label wraps in `.clet-sidebar__link-label` for collapse |\n| `loading` | `boolean` | `false` | Render a shimmering skeleton placeholder instead of the icon/label — see [Loading state](#loading-state) |\n| `loadingLabel` | `string` | `\"Loading\"` | Accessible label announced while loading |\n| `classNames` | `SidebarLinkClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the link |\n| `href` | `string` | — | Link destination |\n\nAlso accepts standard anchor attributes when not using `asChild`. When using `asChild`, supply your own icon/label structure; collapse label hiding works best with the `icon` prop pattern. `SidebarBadge` children are not supported with `asChild`.\n\n## Types\n\n### `SidebarProviderProps`\n\nProps for `SidebarProvider`.\n\n### `SidebarProps`\n\nProps for `Sidebar`. Includes `variant?: \"default\" | \"plain\"` — see [Variants](#variants).\n\n### `SidebarTriggerProps`\n\nProps for `SidebarTrigger`.\n\n### `SidebarCollapseProps`\n\nProps for `SidebarCollapse`.\n\n### `SidebarOverlayProps`\n\nProps for `SidebarOverlay`.\n\n### `SidebarHeaderProps` / `SidebarContentProps` / `SidebarFooterProps`\n\nProps for layout slots.\n\n### `SidebarNavProps` / `SidebarGroupProps` / `SidebarGroupLabelProps` / `SidebarItemProps`\n\nProps for navigation structure parts.\n\n### `SidebarLinkProps`\n\nProps for `SidebarLink`.\n\n### `SidebarBadgeProps`\n\nProps for `SidebarBadge`.\n\n### `SidebarProviderClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Provider wrapper |\n\n### `SidebarClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Aside (`.clet-sidebar`) |\n\n### Other `*ClassNames`\n\n| Type | Key | Applied to |\n|------|-----|------------|\n| `SidebarOverlayClassNames` | `overlay` | Overlay button |\n| `SidebarTriggerClassNames` | `trigger` | Menu button |\n| `SidebarCollapseClassNames` | `collapse` | Collapse button |\n| `SidebarHeaderClassNames` | `header` | Header slot |\n| `SidebarContentClassNames` | `content` | Content slot |\n| `SidebarFooterClassNames` | `footer` | Footer slot |\n| `SidebarNavClassNames` | `nav` | Nav element |\n| `SidebarGroupClassNames` | `group` | Group wrapper |\n| `SidebarGroupClassNames` | `groupToggle` | Toggle button (when collapsible) |\n| `SidebarGroupClassNames` | `groupContent` | Content wrapper |\n| `SidebarGroupLabelClassNames` | `groupLabel` | Section label |\n| `SidebarItemClassNames` | `item` | Item wrapper |\n| `SidebarBadgeClassNames` | `badge` | Badge pill |\n| `SidebarLinkClassNames` | `link` | Link element |\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-sidebar-font` | `var(--clet-font-body)` | Font family override for the sidebar root, links, and trigger button |\n| `--clet-sidebar-text` | `var(--clet-text)` | Base text color for the sidebar root |\n| `--clet-sidebar-bg` | `var(--clet-bg)` | Sidebar surface background — the single override point referenced by every background declaration in the sidebar's CSS |\n| `--clet-sidebar-link-text` | `var(--clet-text-secondary)` | Default (inactive) link and collapse-button icon color |\n| `--clet-sidebar-link-hover-bg` | `var(--clet-surface-dark)` | Link background on hover |\n| `--clet-sidebar-link-hover-text` | `var(--clet-text)` | Link text color on hover |\n| `--clet-sidebar-link-active-bg` | `var(--clet-surface-dark)` | Background of the active link |\n| `--clet-sidebar-link-active-text` | `var(--clet-primary)` | Text color of the active link |\n| `--clet-sidebar-label-text` | `var(--clet-text)` | Group label / group-toggle text color |\n| `--clet-sidebar-badge-bg` | `var(--clet-primary)` | Badge background color (`var(--clet-hover)` on active links) |\n| `--clet-sidebar-badge-color` | `var(--clet-on-primary)` | Badge text color (`var(--clet-text-secondary)` on active links) |\n| `--clet-sidebar-mask-size` | `48px` | Height of the top/bottom scroll fade mask on `.clet-sidebar__content` |\n| `--clet-sidebar-mask-top` | `0px` | Top fade mask offset; animates to `--clet-sidebar-mask-size` once scrolled |\n| `--clet-sidebar-mask-bottom` | `0px` | Bottom fade mask offset; animates to `--clet-sidebar-mask-size` while more content is below |\n\n## Notes\n\n- Desktop renders a **card panel** (`--clet-bg` background, 24px radius, icon + label rows) by default. Active links use a neutral gray pill, not primary color.\n- `variant=\"plain\"` swaps the card panel for a transparent background with a right border (`--clet-border`) and no radius — use it alongside `AppHeader`'s `\"plain\"` variant for a flush dashboard shell.\n- Use `SidebarCollapse` in the header to collapse to a **72px icon rail** on desktop; group labels, link labels, badges, and footer content hide automatically.\n- `SidebarGroup` with `collapsible` renders its first `SidebarGroupLabel` as a button and animates the content open/close with an opacity transition. The label looks identical to the static label when not collapsible — same font, size, color, and padding. The content area stays in the layout when folded (items fade to invisible but the space is reserved) so toggling doesn't shift items below it. Set `defaultExpanded` for the initial state or use `expanded` + `onExpandedChange` for controlled state. The animation respects `prefers-reduced-motion`.\n- Mobile breakpoint defaults to **768px**; override with `breakpoint` on `SidebarProvider` if needed.\n- On mobile, the drawer slides in from the left; overlay click and resize back to desktop close the drawer.\n- Link touch targets are at least **40px** tall on mobile.\n- Slide transitions are disabled when `prefers-reduced-motion: reduce` is set.\n- `SidebarBadge` renders a trailing pill on the right of a link row; badges hide automatically when the sidebar is collapsed.\n- When collapsed, each `SidebarLink` automatically shows a `Tooltip` (positioned to the right) with the link's label text on hover. Links with no label text skip the tooltip.\n- Base `clet-*` classes are always applied; `classNames` and `className` append consumer classes for Tailwind utilities.",
2872
+ "docs": "# Sidebar\n\nCompound sidebar primitives for app shells and section navigation. Desktop shows a sticky card-style rail with optional collapse; mobile uses an offcanvas drawer with a toggle trigger and dismissible overlay.\n\n## Example\n\n<ExampleTabs\n title=\"App shell\"\n preview={<SidebarPreview />}\n code={sidebarSource}\n/>\n\n## Variants\n\n`Sidebar` accepts a `variant` prop that controls its surface style.\n\n- **`\"default\"`** (default) — card panel surface (`--clet-surface-subtle` background, rounded corners).\n- **`\"plain\"`** — transparent background with a right border instead of the card panel, and no border radius. Used for dashboard shells where the sidebar should sit flush against the layout edge.\n\n<ExampleTabs\n title='variant=\"plain\" — transparent background with a right border'\n preview={<SidebarPlainPreview />}\n code={sidebarPlainSource}\n/>\n\n## Loading state\n\nSet `loading` on `SidebarLink` to shimmer an icon+label placeholder row instead of the real link — useful while nav items are still being fetched. The placeholder is non-interactive and ignores `icon`/`children`/`to`/`onClick`/`active`.\n\n<ExampleTabs\n title=\"Loading state\"\n preview={<SidebarLoadingPreview />}\n code={sidebarLoadingSource}\n/>\n\n## React Router\n\nUse `asChild` on `SidebarLink` to merge styles onto `NavLink`:\n\n```tsx\n<SidebarLink asChild active={isActive}>\n <NavLink to=\"/dashboard\">Dashboard</NavLink>\n</SidebarLink>\n```\n\n## Components\n\n### `SidebarProvider`\n\nContext wrapper for open state, collapsed state, and mobile detection.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | — | Controlled open state (mobile drawer) |\n| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |\n| `onOpenChange` | `(open: boolean) => void` | — | Called when open state changes |\n| `collapsed` | `boolean` | — | Controlled collapsed state (desktop) |\n| `defaultCollapsed` | `boolean` | `false` | Initial collapsed state (uncontrolled) |\n| `onCollapsedChange` | `(collapsed: boolean) => void` | — | Called when collapsed state changes |\n| `breakpoint` | `number` | `768` | Max width in px treated as mobile |\n| `classNames` | `SidebarProviderClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the provider wrapper |\n| `children` | `ReactNode` | — | Trigger, overlay, sidebar, and page content |\n\nAlso exports `useSidebar()` for programmatic open/close and collapse.\n\n### `Sidebar`\n\nRoot `<aside>`. Sticky card panel on desktop; fixed offcanvas panel on mobile when open. Renders the mobile backdrop (`SidebarOverlay`) as an internal sibling on mobile viewports, so explicit `<SidebarOverlay />` usage is optional.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `\"default\" \\| \"plain\"` | `\"default\"` | Visual style — see [Variants](#variants) |\n| `mobileHeader` | `ReactNode` | — | Content rendered into a mobile-only `SidebarHeader` (hidden on desktop via the built-in `clet-sidebar__header--mobile-only` class) when `variant=\"plain\"`. Typically a `SidebarBrand` with the same logo/title as the sibling `AppHeader`'s `AppHeaderBranding`, so the brand is visible when the mobile drawer opens. Not auto-extracted by `AppLayout` — pass it explicitly. Skipped if you also supply your own `<SidebarHeader>` child. |\n| `classNames` | `SidebarClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the aside |\n| `children` | `ReactNode` | — | Header, content, footer |\n\n### `SidebarCollapse`\n\nDesktop collapse toggle. Hidden on mobile. Sets `aria-expanded` and `aria-controls`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarCollapseClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the button |\n\nAlso accepts standard `button` attributes.\n\n### `SidebarTrigger`\n\nMobile menu button. Hidden on desktop. Sets `aria-expanded` and `aria-controls`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarTriggerClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the button |\n| `children` | `ReactNode` | — | Button label |\n\nAlso accepts standard `button` attributes.\n\n### `SidebarOverlay`\n\nMobile backdrop. Click closes the drawer. Not rendered on desktop. **Rendered internally by `Sidebar` on mobile** — explicit usage is no longer required. Two stacked overlays both close the drawer on click with no visual artifact, so existing code keeps working unchanged.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarOverlayClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the overlay button |\n\n### `SidebarHeader`\n\nTop branding or metadata slot. Use `clet-sidebar__header-brand` and `clet-sidebar__header-title` for logo + title layout alongside `SidebarCollapse`.\n\n### `SidebarContent`\n\nScrollable middle section for navigation.\n\n### `SidebarFooter`\n\nBottom slot separated by a top border. A plain wrapper — typically holds a [`ProfilePopover`](/profile-popover) for the signed-in user's trigger row and menu.\n\n```tsx\n<SidebarFooter>\n <ProfilePopover\n fullName=\"Kwame Asante\"\n email=\"kwame@clet.edu.gh\"\n items={[\n { icon: <User size={20} />, label: \"My Profile\", onClick: () => navigate(\"/profile\") },\n { icon: <Settings size={20} />, label: \"Account Settings\", onClick: () => navigate(\"/settings\") },\n ]}\n onSignOut={handleSignOut}\n >\n <RoleSelect\n roles={roles}\n selectedRole={selectedRole}\n onClickRole={(role) => setSelectedRole(role.id)}\n />\n </ProfilePopover>\n</SidebarFooter>\n```\n\n### `SidebarNav`\n\nNavigation landmark wrapper (`<nav>`).\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `aria-label` | `string` | `\"Sidebar\"` | Accessible name for the nav |\n| `classNames` | `SidebarNavClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the nav |\n\n### `SidebarGroup`\n\nSection wrapper for grouped links. Set `collapsible` to make the first `SidebarGroupLabel` child render as a button that toggles the group's content with an animated open/close.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `collapsible` | `boolean` | `false` | Render the label as a toggle button that animates content open/closed |\n| `defaultExpanded` | `boolean` | `true` | Initial expanded state (uncontrolled) |\n| `expanded` | `boolean` | — | Controlled expanded state |\n| `onExpandedChange` | `(expanded: boolean) => void` | — | Called when expanded state changes |\n| `classNames` | `SidebarGroupClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the group wrapper |\n| `children` | `ReactNode` | — | Optional `SidebarGroupLabel` + nav items |\n\n### `SidebarGroupLabel`\n\nUppercase section title. Renders as a `<p>` by default, or as a `<button>` toggle when the parent `SidebarGroup` has `collapsible`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarGroupLabelClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the label |\n| `children` | `ReactNode` | — | Label text |\n\n```tsx\n<SidebarGroup collapsible defaultExpanded={false}>\n <SidebarGroupLabel>Advanced</SidebarGroupLabel>\n <SidebarItem>\n <SidebarLink>API Keys</SidebarLink>\n </SidebarItem>\n</SidebarGroup>\n```\n\n### `SidebarItem`\n\nRow wrapper for a single link.\n\n### `SidebarBadge`\n\nTrailing count or label pill inside `SidebarLink`. Must be a direct child of `SidebarLink`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `SidebarBadgeClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the badge |\n| `children` | `ReactNode` | — | Badge text (e.g. `\"New\"`, `12`) |\n\n```tsx\n<SidebarLink href=\"/notifications\" icon={<Bell size={20} />}>\n Notification Templates\n <SidebarBadge>New</SidebarBadge>\n</SidebarLink>\n```\n\n### `SidebarLink`\n\nStyled navigation link.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `active` | `boolean` | `false` | Applies active link styling |\n| `asChild` | `boolean` | `false` | Merge props onto the child element |\n| `icon` | `ReactNode` | — | Optional leading icon; label wraps in `.clet-sidebar__link-label` for collapse |\n| `loading` | `boolean` | `false` | Render a shimmering skeleton placeholder instead of the icon/label — see [Loading state](#loading-state) |\n| `loadingLabel` | `string` | `\"Loading\"` | Accessible label announced while loading |\n| `classNames` | `SidebarLinkClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the link |\n| `href` | `string` | — | Link destination |\n\nAlso accepts standard anchor attributes when not using `asChild`. When using `asChild`, supply your own icon/label structure; collapse label hiding works best with the `icon` prop pattern. `SidebarBadge` children are not supported with `asChild`.\n\n## Types\n\n### `SidebarProviderProps`\n\nProps for `SidebarProvider`.\n\n### `SidebarProps`\n\nProps for `Sidebar`. Includes `variant?: \"default\" | \"plain\"` and `mobileHeader?: ReactNode` — see [Variants](#variants) and the `Sidebar` prop table above.\n\n### `SidebarTriggerProps`\n\nProps for `SidebarTrigger`.\n\n### `SidebarCollapseProps`\n\nProps for `SidebarCollapse`.\n\n### `SidebarOverlayProps`\n\nProps for `SidebarOverlay`.\n\n### `SidebarHeaderProps` / `SidebarContentProps` / `SidebarFooterProps`\n\nProps for layout slots.\n\n### `SidebarNavProps` / `SidebarGroupProps` / `SidebarGroupLabelProps` / `SidebarItemProps`\n\nProps for navigation structure parts.\n\n### `SidebarLinkProps`\n\nProps for `SidebarLink`.\n\n### `SidebarBadgeProps`\n\nProps for `SidebarBadge`.\n\n### `SidebarProviderClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Provider wrapper |\n\n### `SidebarClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Aside (`.clet-sidebar`) |\n\n### Other `*ClassNames`\n\n| Type | Key | Applied to |\n|------|-----|------------|\n| `SidebarOverlayClassNames` | `overlay` | Overlay button |\n| `SidebarTriggerClassNames` | `trigger` | Menu button |\n| `SidebarCollapseClassNames` | `collapse` | Collapse button |\n| `SidebarHeaderClassNames` | `header` | Header slot |\n| `SidebarContentClassNames` | `content` | Content slot |\n| `SidebarFooterClassNames` | `footer` | Footer slot |\n| `SidebarNavClassNames` | `nav` | Nav element |\n| `SidebarGroupClassNames` | `group` | Group wrapper |\n| `SidebarGroupClassNames` | `groupToggle` | Toggle button (when collapsible) |\n| `SidebarGroupClassNames` | `groupContent` | Content wrapper |\n| `SidebarGroupLabelClassNames` | `groupLabel` | Section label |\n| `SidebarItemClassNames` | `item` | Item wrapper |\n| `SidebarBadgeClassNames` | `badge` | Badge pill |\n| `SidebarLinkClassNames` | `link` | Link element |\n\n## Design tokens\n\n| Token | Default | Used for |\n|-------|---------|----------|\n| `--clet-sidebar-font` | `var(--clet-font-body)` | Font family override for the sidebar root, links, and trigger button |\n| `--clet-sidebar-text` | `var(--clet-text)` | Base text color for the sidebar root |\n| `--clet-sidebar-bg` | `var(--clet-bg)` | Sidebar surface background — the single override point referenced by every background declaration in the sidebar's CSS |\n| `--clet-sidebar-link-text` | `var(--clet-text-secondary)` | Default (inactive) link and collapse-button icon color |\n| `--clet-sidebar-link-hover-bg` | `var(--clet-surface-dark)` | Link background on hover |\n| `--clet-sidebar-link-hover-text` | `var(--clet-text)` | Link text color on hover |\n| `--clet-sidebar-link-active-bg` | `var(--clet-surface-dark)` | Background of the active link |\n| `--clet-sidebar-link-active-text` | `var(--clet-primary)` | Text color of the active link |\n| `--clet-sidebar-label-text` | `var(--clet-text)` | Group label / group-toggle text color |\n| `--clet-sidebar-badge-bg` | `var(--clet-primary)` | Badge background color (`var(--clet-hover)` on active links) |\n| `--clet-sidebar-badge-color` | `var(--clet-on-primary)` | Badge text color (`var(--clet-text-secondary)` on active links) |\n| `--clet-sidebar-mask-size` | `48px` | Height of the top/bottom scroll fade mask on `.clet-sidebar__content` |\n| `--clet-sidebar-mask-top` | `0px` | Top fade mask offset; animates to `--clet-sidebar-mask-size` once scrolled |\n| `--clet-sidebar-mask-bottom` | `0px` | Bottom fade mask offset; animates to `--clet-sidebar-mask-size` while more content is below |\n\n## Notes\n\n- Desktop renders a **card panel** (`--clet-bg` background, 24px radius, icon + label rows) by default. Active links use a neutral gray pill, not primary color.\n- `variant=\"plain\"` swaps the card panel for a transparent background with a right border (`--clet-border`) and no radius — use it alongside `AppHeader`'s `\"plain\"` variant for a flush dashboard shell.\n- Use `SidebarCollapse` in the header to collapse to a **72px icon rail** on desktop; group labels, link labels, badges, and footer content hide automatically.\n- `SidebarGroup` with `collapsible` renders its first `SidebarGroupLabel` as a button and animates the content open/close with an opacity transition. The label looks identical to the static label when not collapsible — same font, size, color, and padding. The content area stays in the layout when folded (items fade to invisible but the space is reserved) so toggling doesn't shift items below it. Set `defaultExpanded` for the initial state or use `expanded` + `onExpandedChange` for controlled state. The animation respects `prefers-reduced-motion`.\n- Mobile breakpoint defaults to **768px**; override with `breakpoint` on `SidebarProvider` if needed.\n- On mobile, the drawer slides in from the left; overlay click and resize back to desktop close the drawer.\n- Link touch targets are at least **40px** tall on mobile.\n- Slide transitions are disabled when `prefers-reduced-motion: reduce` is set.\n- `SidebarBadge` renders a trailing pill on the right of a link row; badges hide automatically when the sidebar is collapsed.\n- When collapsed, each `SidebarLink` automatically shows a `Tooltip` (positioned to the right) with the link's label text on hover. Links with no label text skip the tooltip.\n- Base `clet-*` classes are always applied; `classNames` and `className` append consumer classes for Tailwind utilities.",
2708
2873
  "headings": [
2709
2874
  "Sidebar",
2710
2875
  "Example",
@@ -2742,7 +2907,7 @@
2742
2907
  "Design tokens",
2743
2908
  "Notes"
2744
2909
  ],
2745
- "types": "import type {\n ButtonHTMLAttributes,\n HTMLAttributes,\n ReactNode,\n} from \"react\";\n\nexport interface SidebarProviderClassNames {\n root?: string;\n}\n\nexport interface SidebarClassNames {\n root?: string;\n}\n\nexport interface SidebarOverlayClassNames {\n overlay?: string;\n}\n\nexport interface SidebarTriggerClassNames {\n trigger?: string;\n}\n\nexport interface SidebarCollapseClassNames {\n collapse?: string;\n}\n\nexport interface SidebarHeaderClassNames {\n header?: string;\n}\n\nexport interface SidebarContentClassNames {\n content?: string;\n}\n\nexport interface SidebarFooterClassNames {\n footer?: string;\n}\n\nexport interface SidebarNavClassNames {\n nav?: string;\n}\n\nexport interface SidebarGroupClassNames {\n group?: string;\n groupToggle?: string;\n groupContent?: string;\n}\n\nexport interface SidebarGroupLabelClassNames {\n groupLabel?: string;\n}\n\nexport interface SidebarItemClassNames {\n item?: string;\n}\n\nexport interface SidebarBadgeClassNames {\n badge?: string;\n}\n\nexport interface SidebarLinkClassNames {\n link?: string;\n}\n\nexport interface SidebarProviderProps {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n collapsed?: boolean;\n defaultCollapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n breakpoint?: number;\n classNames?: SidebarProviderClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarProps {\n classNames?: SidebarClassNames;\n className?: string;\n /**\n * Visual style. \"default\" keeps the panel surface.\n * \"plain\" makes the background transparent and adds a right border.\n */\n variant?: \"default\" | \"plain\";\n children: ReactNode;\n}\n\nexport interface SidebarOverlayProps {\n classNames?: SidebarOverlayClassNames;\n className?: string;\n}\n\nexport interface SidebarTriggerProps\n extends ButtonHTMLAttributes<HTMLButtonElement> {\n classNames?: SidebarTriggerClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarCollapseProps\n extends ButtonHTMLAttributes<HTMLButtonElement> {\n classNames?: SidebarCollapseClassNames;\n className?: string;\n}\n\nexport interface SidebarHeaderProps {\n classNames?: SidebarHeaderClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarBrandProps {\n classNames?: { root?: string };\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarContentProps {\n classNames?: SidebarContentClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarFooterProps {\n classNames?: SidebarFooterClassNames;\n className?: string;\n /** Typically a `ProfilePopover` with the trigger row and menu content */\n children: ReactNode;\n}\n\nexport interface SidebarNavProps extends HTMLAttributes<HTMLElement> {\n classNames?: SidebarNavClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarGroupProps {\n collapsible?: boolean;\n defaultExpanded?: boolean;\n expanded?: boolean;\n onExpandedChange?: (expanded: boolean) => void;\n classNames?: SidebarGroupClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarGroupLabelProps {\n classNames?: SidebarGroupLabelClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarItemProps {\n classNames?: SidebarItemClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarBadgeProps {\n classNames?: SidebarBadgeClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarLinkProps\n extends ButtonHTMLAttributes<HTMLButtonElement> {\n active?: boolean;\n asChild?: boolean;\n icon?: ReactNode;\n /** If provided, renders as a react-router `<Link>` with this path */\n to?: string;\n /** Render a shimmering skeleton placeholder instead of the icon/label, e.g. while nav items are still loading */\n loading?: boolean;\n /** Accessible label announced while loading (default: \"Loading\") */\n loadingLabel?: string;\n classNames?: SidebarLinkClassNames;\n className?: string;\n children: ReactNode;\n}\n",
2910
+ "types": "import type {\n ButtonHTMLAttributes,\n HTMLAttributes,\n ReactNode,\n} from \"react\";\n\nexport interface SidebarProviderClassNames {\n root?: string;\n}\n\nexport interface SidebarClassNames {\n root?: string;\n}\n\nexport interface SidebarOverlayClassNames {\n overlay?: string;\n}\n\nexport interface SidebarTriggerClassNames {\n trigger?: string;\n}\n\nexport interface SidebarCollapseClassNames {\n collapse?: string;\n}\n\nexport interface SidebarHeaderClassNames {\n header?: string;\n}\n\nexport interface SidebarContentClassNames {\n content?: string;\n}\n\nexport interface SidebarFooterClassNames {\n footer?: string;\n}\n\nexport interface SidebarNavClassNames {\n nav?: string;\n}\n\nexport interface SidebarGroupClassNames {\n group?: string;\n groupToggle?: string;\n groupContent?: string;\n}\n\nexport interface SidebarGroupLabelClassNames {\n groupLabel?: string;\n}\n\nexport interface SidebarItemClassNames {\n item?: string;\n}\n\nexport interface SidebarBadgeClassNames {\n badge?: string;\n}\n\nexport interface SidebarLinkClassNames {\n link?: string;\n}\n\nexport interface SidebarProviderProps {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n collapsed?: boolean;\n defaultCollapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n breakpoint?: number;\n classNames?: SidebarProviderClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarProps {\n classNames?: SidebarClassNames;\n className?: string;\n /**\n * Visual style. \"default\" keeps the panel surface.\n * \"plain\" makes the background transparent and adds a right border.\n */\n variant?: \"default\" | \"plain\";\n /**\n * Content rendered into a mobile-only `SidebarHeader` at the top of the\n * sidebar when `variant` is `\"plain\"`. Typically forwarded automatically\n * by `AppLayout` from the sibling `AppHeader`'s `AppHeaderBranding` so the\n * brand is visible when the mobile drawer opens. On desktop the auto-injected\n * `SidebarHeader` is hidden via the built-in `clet-sidebar__header--mobile-only`\n * class. Pass `null` to disable the auto-injection, or supply your own\n * `SidebarHeader` child to opt out.\n */\n mobileHeader?: ReactNode;\n children: ReactNode;\n}\n\nexport interface SidebarOverlayProps {\n classNames?: SidebarOverlayClassNames;\n className?: string;\n}\n\nexport interface SidebarTriggerProps\n extends ButtonHTMLAttributes<HTMLButtonElement> {\n classNames?: SidebarTriggerClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarCollapseProps\n extends ButtonHTMLAttributes<HTMLButtonElement> {\n classNames?: SidebarCollapseClassNames;\n className?: string;\n}\n\nexport interface SidebarHeaderProps {\n classNames?: SidebarHeaderClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarBrandProps {\n classNames?: { root?: string };\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarContentProps {\n classNames?: SidebarContentClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarFooterProps {\n classNames?: SidebarFooterClassNames;\n className?: string;\n /** Typically a `ProfilePopover` with the trigger row and menu content */\n children: ReactNode;\n}\n\nexport interface SidebarNavProps extends HTMLAttributes<HTMLElement> {\n classNames?: SidebarNavClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarGroupProps {\n collapsible?: boolean;\n defaultExpanded?: boolean;\n expanded?: boolean;\n onExpandedChange?: (expanded: boolean) => void;\n classNames?: SidebarGroupClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarGroupLabelProps {\n classNames?: SidebarGroupLabelClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarItemProps {\n classNames?: SidebarItemClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarBadgeProps {\n classNames?: SidebarBadgeClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface SidebarLinkProps\n extends ButtonHTMLAttributes<HTMLButtonElement> {\n active?: boolean;\n asChild?: boolean;\n icon?: ReactNode;\n /** If provided, renders as a react-router `<Link>` with this path */\n to?: string;\n /** Render a shimmering skeleton placeholder instead of the icon/label, e.g. while nav items are still loading */\n loading?: boolean;\n /** Accessible label announced while loading (default: \"Loading\") */\n loadingLabel?: string;\n classNames?: SidebarLinkClassNames;\n className?: string;\n children: ReactNode;\n}\n",
2746
2911
  "examples": [
2747
2912
  {
2748
2913
  "file": "sidebar-loading.example.tsx",
@@ -2805,6 +2970,7 @@
2805
2970
  "page-composition-sidebar-groups-collapsible",
2806
2971
  "page-composition-sidebar-footer-required",
2807
2972
  "page-composition-no-sidebar-trigger",
2973
+ "page-composition-sidebar-overlay-optional",
2808
2974
  "page-composition-sidebar-badge-numeric",
2809
2975
  "page-composition-branding-in-app-header",
2810
2976
  "page-composition-builtin-loading-states"
@@ -2986,7 +3152,7 @@
2986
3152
  ],
2987
3153
  "import": "@rfdtech/components"
2988
3154
  },
2989
- "docs": "# Table\n\nCompound table component with built-in URL-driven search, pagination, filters, column sorting, selection, bulk actions, and loading skeletons. All state lives in the URL — components read and write search params internally. Pass `paramPrefix` to `<Table>` to namespace state for multiple tables on one page.\n\n## Example\n\n<ExampleTabs\n title=\"Basic table with selection\"\n preview={<TablePreview />}\n code={tableSource}\n/>\n\n## Example Empty State\n\n<ExampleTabs\n title=\"Empty state\"\n preview={<TablePreviewEmpty />}\n code={tableEmptySource}\n/>\n\n## Panel variant\n\nPass `variant=\"panel\"` to `<TableContent>` for a faint recessed surface with\nsubtle row dividers and a border all around. It reads well when the table is\nembedded directly in a page — for example inside pill tabs — without a wrapping\ncard. The look is a pure CSS override driven by the\n`--clet-surface-panel` and `--clet-border-subtle` tokens (both have light and dark\nvalues).\n\n<ExampleTabs\n title=\"Panel tables in pill tabs\"\n preview={<TablePreviewPanel />}\n code={tablePanelSource}\n/>\n\n## CRUD with bulk delete and a row action menu\n\nSelectable rows drive `TableBulkActions`, which opens a confirm `Dialog` before the destructive\naction actually runs — nothing is deleted until the user confirms. Each row also has its own\n`Popover` action menu, built by hand for richer per-row content — prefer [`rowActions`](#row-actions)\n(kebab click + cursor-anchored right-click, no custom `Popover` wiring) unless you need content\n`rowActions` can't express.\n\n<ExampleTabs\n title=\"Selectable table with bulk delete and row actions\"\n preview={<TableCrudPreview />}\n code={tableCrudSource}\n/>\n\n## Toolbar filters with `TableActions`\n\n`TableActions` is a layout slot inside `TableHeader` for toolbar-level controls — like a filter\n`Dropdown` — that are distinct from per-row `rowActions` and bulk `TableBulkActions`. Wire it to\n`useTableState().filters` to drive client-side (or server) filtering.\n\n<ExampleTabs\n title=\"Role filter via TableActions\"\n preview={<TableActionsPreview />}\n code={tableActionsSource}\n/>\n\n## Spread filter variant\n\nPass `variant=\"spread\"` to `<TableFilter>` to drop the popover entirely — the\nfields render inline as a flex row (with a consistent gap) directly in the\ntoolbar, with just a \"clear\" action — no Apply button. Fields auto-apply as\nsoon as their value changes (no popover to dismiss, so there's nothing an\nexplicit Apply click would gate). Same `onApply`/`onReset` and URL behavior\nas the default popover variant otherwise, just no trigger button or panel.\n\n<ExampleTabs\n title=\"Spread variant — no popover, fields inline\"\n preview={<TableFilterSpreadPreview />}\n code={tableFilterSpreadSource}\n/>\n\n## Architecture\n\nEvery child component reads and writes its own URL keys through `useSearchParams`. The root `<Table paramPrefix>` provides a shared namespace via React Context. No prop drilling — pass `totalPages` and `totalItems` to `<TablePagination>`; everything else is fully automatic.\n\n```\n<Table paramPrefix=\"members\">\n <TableSearch /> → writes ?members.search=term\n <TableFilter> → writes ?members.f_role=admin\n <Dropdown name=\"role\" value={roleValue} onValueChange={setRoleValue} ... />\n </TableFilter>\n <TableContent /> → renders data + sort headers\n <TableBulkActions /> → shows selection bar with inline actions\n <TablePagination /> → reads/writes ?members.page=1&members.pageSize=10\n</Table>\n```\n\nFilter fields must be *named* form-associated elements for `FormData` to capture them — never a raw `<select>`/native `<button>`/native `<input>` as the visible control. `Dropdown` participates in native form submission on its own via its `name` prop (backed by Radix's hidden bubble `<select>`) — no separate hidden input needed. Other custom inputs without built-in form participation still need a hand-paired `<input type=\"hidden\" name=\"...\">`.\n\nMultiple tables on one page share the same URL without collision by using different prefixes:\n\n```tsx\n<Table paramPrefix=\"members\">{/* ... */}</Table>\n<Table paramPrefix=\"invoices\">{/* ... */}</Table>\n// URL: ?members.page=1&invoices.page=2&invoices.search=paid\n```\n\n## `<Table>`\n\nRoot wrapper. Accepts a `paramPrefix` that is provided via Context to all children.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `paramPrefix` | `string` | — | **Required.** URL key namespace shared by all child components |\n| `height` | `number \\| string` | — | Constrain the table height. Required when using `virtualRowHeight`. Number = px, string = CSS value. |\n| `classNames` | `TableClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root div |\n\n## `<TableSearch>`\n\nSearch input with debounce and a clear button. Writes the search term to `?search=` (or `?prefix.search=`) in the URL, resets page to 1 on change.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `placeholder` | `string` | `\"Search...\"` | Placeholder text |\n| `debounceMs` | `number` | `300` | Debounce delay in ms |\n| `onSearch` | `(value: string) => void` | — | Callback that fires on the **debounced** search value (not every keystroke) |\n| `onChange` | `(e: ChangeEvent) => void` | — | Raw input change event (fires on every keystroke) |\n| `classNames` | `TableSearchClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the search wrapper |\n\n## `<TableFilter>`\n\nPopover with filter fields and Apply / Reset buttons. Wraps children in a `<form>` element. On Apply, reads `FormData` from named fields and writes `f_{name}={value}` to the URL. On Reset, clears all `f_*` params.\n\nFilter fields must be *named* form-associated elements for `FormData` to read them — never a raw `<select>`/native `<button>`/native `<input>` as the visible control. Pass `name=\"status\"` straight to `Dropdown` and it participates in native form submission on its own (backed by Radix's hidden bubble `<select>`) — the `name` becomes the URL key. Other custom inputs without built-in form participation still need a hand-paired `<input type=\"hidden\" name=\"...\">` driving the same value.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Filter form content (inputs with `name` attributes) |\n| `onApply` | `() => void` | — | Optional callback when Apply is clicked (URL write happens first) |\n| `onReset` | `() => void` | — | Optional callback when Reset is clicked (URL clear happens first) |\n| `applyLabel` | `string` | `\"Apply Filter\"` | Apply button text |\n| `resetLabel` | `string` | `\"Reset\"` | Reset button aria-label |\n| `variant` | `\"popover\" \\| \"spread\"` | `\"popover\"` | `\"spread\"` removes the popover and lays fields out inline as a flex row — see [Spread filter variant](#spread-filter-variant) |\n| `classNames` | `TableFilterClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root div |\n\n## `<TableContent>`\n\nRenders a `<table>` with columns, data, optional sort, row selection, and loading skeletons.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `\"default\" \\| \"panel\"` | `\"default\"` | `\"panel\"` renders a faint recessed surface with subtle row dividers and a border all around (CSS-only override) |\n| `columns` | `TableColumn<T>[]` | — | Column definitions |\n| `data` | `T[]` | — | Row data |\n| `rowKey` | `(row: T) => string \\| number` | — | Unique key extractor for each row |\n| `loading` | `boolean` | `false` | Show skeleton rows instead of data |\n| `loadingRows` | `number` | `5` | Number of skeleton rows to display |\n| `selectable` | `boolean` | `false` | Show a checkbox selection column with select-all in the header |\n| `selectedIds` | `Set<string \\| number>` | `new Set()` | Controlled set of selected row keys. Pass with `onSelectionChange`. Required for `TableBulkActions` sync. |\n| `onSelectionChange` | `(selectedIds: Set<string \\| number>) => void` | — | Called with a new `Set` on any selection toggle |\n| `rowActions` | `TableRowAction<T>[]` | — | Row-level actions rendered as a kebab dropdown at the end of each row. Opens from the kebab button on click, or at the cursor position on right-click anywhere in the row — see [Row actions](#row-actions) below. |\n| `bulkActions` | `TableBulkAction[]` | — | Surfaces bulk actions inside the row-actions menu (kebab click or right-click) as a \"Bulk actions\" section — always includes a Select all/Deselect all toggle, with the rest of the list appearing once one or more rows are selected. Requires `selectable`. See [Row actions](#row-actions) below. |\n| `bulkActionsFooter` | `boolean` | `false` | Also render the `TableBulkActions` bar at the bottom of the table automatically, wired to the same `bulkActions`/`selectedIds` — skip placing `<TableBulkActions>` yourself as a sibling. When `false`, `bulkActions` only surface inside the row-actions menu. |\n| `onRowClick` | `(row: T, event: MouseEvent<HTMLTableRowElement>) => void` | — | Called when a row is clicked. Fires in addition to selection toggling when `selectable` is set — use it to e.g. open a detail view on row click. |\n| `className` | `string` | — | Merged onto the wrapper div |\n| `virtualRowHeight` | `number` | — | Row height in px. Set this to enable virtual scrolling. The parent `<Table>` must have a `height` set. |\n| `emptyIcon` | `ReactNode` | Inbox icon | JSX/ReactNode icon shown above the empty text when `data` is empty. Defaults to an Inbox icon from `lucide-react`. |\n| `emptyText` | `string` | `\"No results\"` | Message shown when `data` is empty |\n| `classNames` | `TableContentClassNames` | — | Part-level class overrides |\n\nWhen `selectable` is true, a checkbox column is always present, collapsed to 0 width when no rows are selected. The header checkbox selects or deselects all visible rows.\n\nRows show hover state and cursor pointer when `selectable` or `onRowClick` is set.\n\n### Row actions\n\n`rowActions` is the **preferred** way to add a per-row action menu — prefer it over hand-rolling a `Popover` per row (see the CRUD example below, kept only for cases needing fully custom menu content). It renders a single kebab-triggered dropdown that opens two ways:\n\n- **Kebab click** — opens anchored to the kebab button, right-aligned underneath it, same as any dropdown trigger.\n- **Right-click anywhere in the row** — opens at the exact cursor position (`clientX`/`clientY`), not snapped to the kebab column. This lets users right-click near the data they care about instead of hunting for the actions column. Right-clicking the same row again while its menu is already open closes it, instead of re-opening at the new cursor position.\n\nBoth open the same menu and share the same `TableRowAction<T>[]` items — no extra wiring needed.\n\n```tsx\n<TableContent\n columns={columns}\n data={rows}\n rowKey={(row) => row.id}\n rowActions={[\n { id: \"view\", label: \"View\", icon: <Eye size={14} />, onClick: (row) => openView(row) },\n { id: \"edit\", label: \"Edit\", icon: <Edit size={14} />, onClick: (row) => openEdit(row) },\n {\n id: \"delete\",\n label: \"Delete\",\n icon: <Trash2 size={14} />,\n variant: \"destructive\",\n onClick: (row) => deleteRow(row.id),\n },\n ]}\n/>\n```\n\n**Bulk actions in the row-actions menu.** Pass `bulkActions` (requires `selectable`) to surface bulk actions inside that same kebab/right-click menu, under a \"Bulk actions\" section label. A Select all/Deselect all toggle always shows there; the rest of `bulkActions` only appears once one or more rows are selected. Add `bulkActionsFooter` to also render the `TableBulkActions` bar at the bottom automatically — otherwise `bulkActions` only ever surface inside the menu, and you place `<TableBulkActions>` yourself if you want the bottom bar:\n\n```tsx\n<TableContent\n columns={columns}\n data={rows}\n rowKey={(row) => row.id}\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n rowActions={rowActions}\n bulkActions={[\n { id: \"export\", label: \"Export\", icon: <Download size={14} />, onClick: (ids) => exportRows(ids) },\n { id: \"delete\", label: \"Delete\", icon: <Trash2 size={14} />, destructive: true, onClick: (ids) => deleteRows(ids) },\n ]}\n bulkActionsFooter\n/>\n```\n\nNeed a plain row click instead (e.g. navigating to a detail page)? Use `onRowClick` — it fires independently of `rowActions` and of selection toggling:\n\n```tsx\n<TableContent\n columns={columns}\n data={rows}\n rowKey={(row) => row.id}\n onRowClick={(row) => navigate(`/members/${row.id}`)}\n/>\n```\n\n### `TableColumn<T>`\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| `id` | `string` | — | Unique column identifier |\n| `header` | `string` | — | Column header text |\n| `accessorKey` | `keyof T` | — | Property path on the row |\n| `accessorFn` | `(row: T) => ReactNode` | — | Custom value extractor (takes priority over `accessorKey`) |\n| `cell` | `(info: { row: T; value: ReactNode }) => ReactNode` | — | Custom cell renderer |\n| `width` | `number` | — | Fixed width in px |\n| `minWidth` | `number` | `120` | Minimum width (used when `width` is omitted) |\n| `maxWidth` | `number` | — | Maximum width in px |\n| `sortable` | `boolean` | `false` | Enable column sorting (client-side only) |\n| `align` | `\"left\" \\| \"center\" \\| \"right\"` | `\"left\"` | Text alignment |\n\n## `<TableBulkActions>`\n\nA bar that appears when rows are selected, showing \"Selected N\" on the left and action buttons on the right. Place it as a child of `<Table>`, between `<TableContent>` and `<TableFooter>`. Auto-hides when no rows are selected (`selectedIds.size === 0`).\n\nAction handlers receive `selectedIds` automatically — consumers never reference the selection state directly in their handlers.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `selectedIds` | `Set<string \\| number>` | — | **Required.** Currently selected row keys. Auto-hides when empty. |\n| `selectedCount` | `number` | `selectedIds.size` | Override displayed count |\n| `onClear` | `() => void` | — | Called when the X clear button is clicked |\n| `actions` | `TableBulkAction[]` | — | Predefined actions rendered as inline buttons |\n| `renderActions` | `({ selectedIds }) => ReactNode` | — | Render prop for custom action content. Replaces the default buttons. |\n| `classNames` | `TableBulkActionsClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root bar |\n\n### Basic usage\n\n```tsx\nconst [selected, setSelected] = useState<Set<string | number>>(new Set());\n\n<Table paramPrefix=\"users\">\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n columns={columns}\n data={users}\n rowKey={(u) => u.id}\n />\n <TableBulkActions\n selectedIds={selected}\n actions={[\n {\n id: \"delete\",\n label: \"Delete\",\n icon: <Trash2 size={14} />,\n onClick: (ids) => deleteUsers(ids),\n destructive: true,\n },\n ]}\n />\n</Table>\n```\n\n`selectedIds` is controlled — pass it alongside `onSelectionChange` so both `TableContent` and `TableBulkActions` read from the same source. `onClear` on `TableBulkActions` resets the shared state. Add `rowActions` to render a per-row kebab dropdown.\n\n### Custom render\n\nUse `renderActions` to replace the button group with your own content:\n\n```tsx\n<TableBulkActions\n selectedIds={selected}\n renderActions={({ selectedIds }) => (\n <>\n <Button size=\"sm\" variant=\"outline\" onClick={() => handleCustom(selectedIds)}>\n Custom\n </Button>\n </>\n )}\n/>\n```\n\n## `<TablePagination>`\n\nRenders \"Showing X-Y of Z\" with a page-size dropdown on the left, and page number buttons with Previous/Next on the right. Reads `page` and `pageSize` from the URL; writes changes back. Uses a sliding window with ellipsis when total pages exceed `visiblePages`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `totalPages` | `number` | — | Total number of pages |\n| `totalItems` | `number` | — | Total item count (shown as \"Showing X–Y of N\") |\n| `pageSizeOptions` | `number[]` | `[10,20,50,100]` | Options in the page-size dropdown |\n| `visiblePages` | `number` | `10` | Max page number buttons before ellipsis |\n| `classNames` | `TablePaginationClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root div |\n\n## `<TableHeader>`\n\nLayout wrapper for the header bar. Typically contains `<TableSearch>` and `<TableFilter>`.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `children` | `ReactNode` | Header content |\n| `classNames` | `TableHeaderClassNames` | — | Part-level class overrides |\n| `className` | `string` | Merged onto the header bar |\n\n## `<TableFooter>`\n\nLayout wrapper for the footer bar with `border-top`. Typically contains `<TablePagination>`.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `children` | `ReactNode` | Footer content |\n| `classNames` | `TableFooterClassNames` | — | Part-level class overrides |\n| `className` | `string` | Merged onto the footer bar |\n\n## `<TableActions>`\n\nActions area inside the header bar. Layout wrapper for action buttons.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `children` | `ReactNode` | Action content |\n| `classNames` | `TableActionsClassNames` | Part-level class overrides |\n| `className` | `string` | Merged onto the actions wrapper |\n\n## `useTableState`\n\n**Recommended** — the single hook for all table state. Reads page, pageSize, search, filters, and sort from URL params. Use it to derive filtered/paged data from your raw dataset. Prefer this over `useTableFilter` and `useTablePagination` individually.\n\n```tsx\nconst { page, pageSize, search, filters, pageSizeOptions } = useTableState({\n defaultPageSize: 10,\n paramPrefix: \"members\",\n});\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `defaultPage` | `number` | `1` | Initial page when URL has no value |\n| `defaultPageSize` | `number` | `10` | Initial page size |\n| `pageSizeOptions` | `number[]` | `[10,20,50,100]` | Options passed to pagination dropdown |\n| `defaultSearch` | `string` | `\"\"` | Initial search term |\n| `defaultFilters` | `Partial<TFilters>` | — | Initial filter defaults |\n| `defaultSort` | `{ column, direction } \\| null` | `null` | Initial sort state |\n| `paramPrefix` | `string` | — | Must match `<Table paramPrefix>` |\n\n**Returns:** `{ page, pageSize, pageSizeOptions, setPage, setPageSize, search, setSearch, filters, setFilters, setFilter, sort, setSort, resetAll }`\n\nFilters are stored as `Record<string, string>`. Values from URL are always strings — coerce in your filtering logic as needed. Use `filters.status ?? \"\"` to provide a default.\n\n## `useTableFilter`\n\nStandalone hook for URL-driven filter state without search, pagination, or sort.\n\n```tsx\nimport { useTableFilter } from \"@rfdtech/components\";\n\nconst { filters, setFilter, setFilters, resetFilters } = useTableFilter({\n defaults: { role: \"admin\" },\n paramPrefix: \"members\",\n});\n```\n\n| Option | Type | Default |\n|--------|------|---------|\n| `defaults` | `Record<string, string>` | — | Default filter values |\n| `paramPrefix` | `string` | — | URL key namespace |\n\n**Returns:** `{ filters, setFilter, setFilters, resetFilters }`\n\n## `useTablePagination`\n\nStandalone hook for URL-driven pagination without search or filters.\n\n```tsx\nconst { page, pageSize, setPage, setPageSize, reset } = useTablePagination({\n defaultPageSize: 10,\n});\n```\n\n| Option | Type | Default |\n|--------|------|---------|\n| `defaultPage` | `number` | `1` |\n| `defaultPageSize` | `number` | `10` |\n| `paramPrefix` | `string` | — |\n\n**Returns:** `{ page, pageSize, setPage, setPageSize, reset }`\n\n## Types\n\n### `TableProps`\n\nRoot props including `paramPrefix`, `classNames`, and `className`.\n\n### `TableClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root wrapper (`.clet-table`) |\n| `header` | Header bar (`.clet-table__header-bar`) |\n| `content` | Content wrapper (`.clet-table__content`) |\n| `footer` | Footer bar (`.clet-table__footer`) |\n| `selectionCell` | Selection checkbox cells (`.clet-table__checkbox-cell`) |\n\n### `TableBulkAction`\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `id` | `string` | Unique action identifier |\n| `label` | `string` | Display label in the menu |\n| `icon` | `ReactNode` | Optional leading icon |\n| `onClick` | `(selectedIds: Set<string \\| number>) => void` | Called with the current selection |\n| `destructive` | `boolean` | Renders the action in error color |\n\n### `TableRowAction`\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `id` | `string` | Unique action identifier |\n| `label` | `string` | Display label in the dropdown |\n| `icon` | `ReactNode` | Optional leading icon |\n| `variant` | `\"default\" \\| \"destructive\"` | Visual style. `\"destructive\"` renders in error color. |\n| `onClick` | `(row: T) => void` | Called with the row data when the action is selected |\n| `condition` | `(row: T) => boolean` | If provided, action only appears when this returns `true` for the row |\n\n### `TableBulkActionsClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Bar wrapper (`.clet-table__bulk-actions-bar`) |\n| `count` | \"Selected N\" label |\n\n### `TableContentClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Content wrapper (`.clet-table__content`) |\n| `empty` | Empty state container (`.clet-table__empty`) |\n| `emptyIcon` | Empty state icon (`.clet-table__empty-icon`) |\n| `emptyText` | Empty state text (`.clet-table__empty-text`) |\n| `th` | Column header cells |\n| `thLabel` | Header label text (`.clet-table__th-label`) |\n| `sortIcon` | Sort indicator (`.clet-table__sort-icon`) |\n| `checkboxCell` | Selection checkbox cells (`.clet-table__checkbox-cell`) |\n| `actionsCell` | Actions column cell (`.clet-table__actions-cell`) |\n| `actionsTrigger` | Kebab trigger button (`.clet-table__actions-trigger`) |\n| `actionsMenu` | Actions popover content (`.clet-table__actions-menu`) |\n| `actionsItem` | Individual action item (`.clet-table__actions-item`) |\n| `actionsSectionLabel` | \"Bulk actions\" section label inside the row-actions menu, shown when `bulkActions` is set (`.clet-table__actions-section-label`) |\n| `skeleton` | Loading skeleton spans (`.clet-table__skeleton--*`) |\n| `viewport` | Virtual scroll viewport (`.clet-table__viewport`) |\n\n### `TableSearchClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Search wrapper (`.clet-table__search`) |\n| `icon` | Search icon (`.clet-table__search-icon`) |\n| `input` | Search input (`.clet-table__search-input`) |\n| `clear` | Clear button (`.clet-table__search-clear`) |\n\n### `TableFilterClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Filter wrapper (`.clet-table__filter`) |\n| `trigger` | Filter trigger button (`.clet-table__filter-trigger`) |\n| `badge` | Active filter count badge (`.clet-table__filter-badge`) |\n| `content` | Popover content (`.clet-table__filter-content`) |\n| `header` | Popover header bar (`.clet-table__filter-header`) |\n| `resetButton` | Reset button (`.clet-table__filter-btn--reset`) |\n| `fields` | Filter form fields container (`.clet-table__filter-fields`) |\n| `actions` | Filter actions bar (`.clet-table__filter-actions`) |\n| `applyButton` | Apply button (`.clet-table__filter-btn--apply`) |\n\n### `TablePaginationClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Pagination wrapper (`.clet-table__pagination`) |\n| `results` | Results summary (`.clet-table__page-results`) |\n| `pageSize` | Page size dropdown (`.clet-table__page-size`) |\n| `pages` | Page number buttons container (`.clet-table__pagination-pages`) |\n| `ellipsis` | Ellipsis indicator (`.clet-table__page-ellipsis`) |\n\n### `TableHeaderClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Header bar (`.clet-table__header-bar`) |\n\n### `TableFooterClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Footer bar (`.clet-table__footer`) |\n\n### `TableActionsClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Actions wrapper (`.clet-table__actions`) |\n\n### `PaginationControlsProps`\n\nProps for `TablePagination` — see the props section above.\n\n## Design tokens\n\nThe table uses the standard `--clet-*` palette. The tokens with the most visual impact:\n\n| Token | Effect |\n|-------|--------|\n| `--clet-radius-base` | All border radii |\n| `--clet-surface-panel` | Background of the `panel` variant content surface |\n| `--clet-border-subtle` | Row dividers and outer border of the `panel` variant |\n| `--clet-table-font` | Font family for the table root, search input, and filter trigger — default `var(--clet-font-body)` |\n| `--clet-table-filter-font` | Font family for the filter popover's apply/reset buttons — default `var(--clet-font-body)` |\n| `--clet-table-actions-font` | Font family for row actions menu items — default `var(--clet-font-body)` |\n| `--clet-progress-modal-font` | Font family for the bulk-import progress modal's status text — default `var(--clet-font-body)` |\n\n## Notes\n\n- All URL writes are `replace: true` except page changes which use `pushState` for back-button support.\n- Changing search, filters, or page size automatically resets to page 1.\n- Sort is client-side only (`localeCompare`). No server integration.\n- Loading skeletons use a pulse animation; reduced-motion users get a static version.\n- Without a `paramPrefix`, keys are plain (`page`, `search`, `f_status`). With one, they are namespaced (`members.page`, `members.search`, `members.f_status`).\n- `paramPrefix` is required on `<Table>` — every table must have a unique namespace to avoid URL collisions.\n- The filter form captures all elements with a `name` attribute via `FormData`. For custom inputs (date pickers, multi-selects), ensure they set a `name` and participate in `FormData`.\n- Selection is page-local: \"select all\" only selects rows on the current page. Selection is fully controlled via `selectedIds` / `onSelectionChange` — pass the same `selectedIds` to `TableBulkActions` for sync.\n- At `768px` and below (matching the breakpoint `Sidebar` uses for its mobile drawer), `TableHeader`'s children stack full-width instead of squeezing into one row, both `TableFilter` variants stack their fields, `TablePagination` wraps and centers its two rows (results/page-size, then prev/pages/next), and the bulk-actions bar wraps instead of clipping.\n\n## Do\n\n- Use the `TableActions` slot inside `TableHeader` for toolbar-level filters or actions (e.g. a `Dropdown` filter) that are distinct from per-row `TableRowAction`s and bulk `TableBulkActions`.\n- Prefer `rowActions` over a hand-rolled per-row `Popover` for kebab menus — it gets cursor-anchored right-click for free, alongside the standard kebab-click trigger.\n- Prefer `bulkActions` + `bulkActionsFooter` on `TableContent` over manually pairing it with a standalone `<TableBulkActions>` sibling — one prop pair keeps the row-actions menu's \"Bulk actions\" section and the bottom bar in sync automatically. Reach for a standalone `<TableBulkActions>` only if you need it placed somewhere `TableContent` can't reach (e.g. outside `<Table>` context) or without the in-menu section.\n\n## Don't\n\n- Don't pull `TableSearch`, `TableFilter`, `TableActions`, or a view-mode toggle out of `<Table>` context into a plain `<div>` — these children require the ancestor `<Table paramPrefix>` to function.\n- When a page has a list/tile view toggle, don't rebuild `TableHeader` per mode — keep it (search, filters, toggle) identical for both, and only swap `TableContent` (list) for a grid (tiles) inside `<Table>`.\n- Don't hand-roll `onContextMenu` + a controlled `Popover` to get a cursor-anchored right-click menu — `rowActions` already does this.",
3155
+ "docs": "# Table\n\nCompound table component with built-in URL-driven search, pagination, filters, column sorting, selection, bulk actions, and loading skeletons. All state lives in the URL — components read and write search params internally. Pass `paramPrefix` to `<Table>` to namespace state for multiple tables on one page.\n\n## Example\n\n<ExampleTabs\n title=\"Basic table with selection\"\n preview={<TablePreview />}\n code={tableSource}\n/>\n\n## Example Empty State\n\n<ExampleTabs\n title=\"Empty state\"\n preview={<TablePreviewEmpty />}\n code={tableEmptySource}\n/>\n\n## Panel variant\n\nPass `variant=\"panel\"` to `<TableContent>` for a faint recessed surface with\nsubtle row dividers and a border all around. It reads well when the table is\nembedded directly in a page — for example inside pill tabs — without a wrapping\ncard. The look is a pure CSS override driven by the\n`--clet-surface-panel` and `--clet-border-subtle` tokens (both have light and dark\nvalues).\n\n<ExampleTabs\n title=\"Panel tables in pill tabs\"\n preview={<TablePreviewPanel />}\n code={tablePanelSource}\n/>\n\n## CRUD with bulk delete and a row action menu\n\nSelectable rows drive `TableBulkActions`, which opens a confirm `Dialog` before the destructive\naction actually runs — nothing is deleted until the user confirms. Each row also has its own\n`Popover` action menu, built by hand for richer per-row content — prefer [`rowActions`](#row-actions)\n(kebab click + cursor-anchored right-click, no custom `Popover` wiring) unless you need content\n`rowActions` can't express.\n\n<ExampleTabs\n title=\"Selectable table with bulk delete and row actions\"\n preview={<TableCrudPreview />}\n code={tableCrudSource}\n/>\n\n## Toolbar filters with `TableActions`\n\n`TableActions` is a layout slot inside `TableHeader` for toolbar-level controls — like a filter\n`Dropdown` — that are distinct from per-row `rowActions` and bulk `TableBulkActions`. Wire it to\n`useTableState().filters` to drive client-side (or server) filtering.\n\n<ExampleTabs\n title=\"Role filter via TableActions\"\n preview={<TableActionsPreview />}\n code={tableActionsSource}\n/>\n\n## Spread filter variant\n\nPass `variant=\"spread\"` to `<TableFilter>` to drop the popover entirely — the\nfields render inline as a flex row (with a consistent gap) directly in the\ntoolbar, with just a \"clear\" action — no Apply button. Fields auto-apply as\nsoon as their value changes (no popover to dismiss, so there's nothing an\nexplicit Apply click would gate). Same `onApply`/`onReset` and URL behavior\nas the default popover variant otherwise, just no trigger button or panel.\n\n<ExampleTabs\n title=\"Spread variant — no popover, fields inline\"\n preview={<TableFilterSpreadPreview />}\n code={tableFilterSpreadSource}\n/>\n\n## Architecture\n\nEvery child component reads and writes its own URL keys through `useSearchParams`. The root `<Table paramPrefix>` provides a shared namespace via React Context. No prop drilling — pass `totalPages` and `totalItems` to `<TablePagination>`; everything else is fully automatic.\n\n```\n<Table paramPrefix=\"members\">\n <TableSearch /> → writes ?members.search=term\n <TableFilter> → writes ?members.f_role=admin\n <Dropdown name=\"role\" value={roleValue} onValueChange={setRoleValue} ... />\n </TableFilter>\n <TableContent /> → renders data + sort headers\n <TableBulkActions /> → shows selection bar with inline actions\n <TablePagination /> → reads/writes ?members.page=1&members.pageSize=10\n</Table>\n```\n\nFilter fields must be *named* form-associated elements for `FormData` to capture them — never a raw `<select>`/native `<button>`/native `<input>` as the visible control. `Dropdown` participates in native form submission on its own via its `name` prop (backed by Radix's hidden bubble `<select>`) — no separate hidden input needed. Other custom inputs without built-in form participation still need a hand-paired `<input type=\"hidden\" name=\"...\">`.\n\nMultiple tables on one page share the same URL without collision by using different prefixes:\n\n```tsx\n<Table paramPrefix=\"members\">{/* ... */}</Table>\n<Table paramPrefix=\"invoices\">{/* ... */}</Table>\n// URL: ?members.page=1&invoices.page=2&invoices.search=paid\n```\n\n## `<Table>`\n\nRoot wrapper. Accepts a `paramPrefix` that is provided via Context to all children.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `paramPrefix` | `string` | — | **Required.** URL key namespace shared by all child components |\n| `height` | `number \\| string` | — | Constrain the table height. Required when using `virtualRowHeight`. Number = px, string = CSS value. |\n| `classNames` | `TableClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root div |\n\n## `<TableSearch>`\n\nSearch input with debounce and a clear button. Writes the search term to `?search=` (or `?prefix.search=`) in the URL, resets page to 1 on change.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `placeholder` | `string` | `\"Search...\"` | Placeholder text |\n| `debounceMs` | `number` | `300` | Debounce delay in ms |\n| `onSearch` | `(value: string) => void` | — | Callback that fires on the **debounced** search value (not every keystroke) |\n| `onChange` | `(e: ChangeEvent) => void` | — | Raw input change event (fires on every keystroke) |\n| `classNames` | `TableSearchClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the search wrapper |\n\n## `<TableFilter>`\n\nPopover with filter fields and Apply / Reset buttons. Wraps children in a `<form>` element. On Apply, reads `FormData` from named fields and writes `f_{name}={value}` to the URL. On Reset, clears all `f_*` params.\n\nFilter fields must be *named* form-associated elements for `FormData` to read them — never a raw `<select>`/native `<button>`/native `<input>` as the visible control. Pass `name=\"status\"` straight to `Dropdown` and it participates in native form submission on its own (backed by Radix's hidden bubble `<select>`) — the `name` becomes the URL key. Other custom inputs without built-in form participation still need a hand-paired `<input type=\"hidden\" name=\"...\">` driving the same value.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Filter form content (inputs with `name` attributes) |\n| `onApply` | `() => void` | — | Optional callback when Apply is clicked (URL write happens first) |\n| `onReset` | `() => void` | — | Optional callback when Reset is clicked (URL clear happens first) |\n| `applyLabel` | `string` | `\"Apply Filter\"` | Apply button text |\n| `resetLabel` | `string` | `\"Reset\"` | Reset button aria-label |\n| `variant` | `\"popover\" \\| \"spread\"` | `\"popover\"` | `\"spread\"` removes the popover and lays fields out inline as a flex row — see [Spread filter variant](#spread-filter-variant) |\n| `classNames` | `TableFilterClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root div |\n\n## `<TableContent>`\n\nRenders a `<table>` with columns, data, optional sort, row selection, and loading skeletons.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `\"default\" \\| \"panel\"` | `\"default\"` | `\"panel\"` renders a faint recessed surface with subtle row dividers and a border all around (CSS-only override) |\n| `columns` | `TableColumn<T>[]` | — | Column definitions |\n| `data` | `T[]` | — | Row data |\n| `rowKey` | `(row: T) => string \\| number` | — | Unique key extractor for each row |\n| `loading` | `boolean` | `false` | Show skeleton rows instead of data |\n| `loadingRows` | `number` | `5` | Number of skeleton rows to display |\n| `selectable` | `boolean` | `false` | Show a checkbox selection column with select-all in the header |\n| `selectedIds` | `Set<string \\| number>` | `new Set()` | Controlled set of selected row keys. Pass with `onSelectionChange`. Required for `TableBulkActions` sync. |\n| `onSelectionChange` | `(selectedIds: Set<string \\| number>) => void` | — | Called with a new `Set` on any selection toggle |\n| `rowActions` | `TableRowAction<T>[]` | — | Row-level actions rendered as a kebab dropdown at the end of each row. Opens from the kebab button on click, or at the cursor position on right-click anywhere in the row — see [Row actions](#row-actions) below. |\n| `bulkActions` | `TableBulkAction[]` | — | Surfaces bulk actions inside the row-actions menu (kebab click or right-click) as a \"Bulk actions\" section — always includes a Select all/Deselect all toggle, with the rest of the list appearing once one or more rows are selected. Requires `selectable`. See [Row actions](#row-actions) below. |\n| `bulkActionsFooter` | `boolean` | `false` | Also render the `TableBulkActions` bar at the bottom of the table automatically, wired to the same `bulkActions`/`selectedIds` — skip placing `<TableBulkActions>` yourself as a sibling. When `false`, `bulkActions` only surface inside the row-actions menu. |\n| `onRowClick` | `(row: T, event: MouseEvent<HTMLTableRowElement>) => void` | — | Called when a row is clicked. Fires in addition to selection toggling when `selectable` is set — use it to e.g. open a detail view on row click. |\n| `className` | `string` | — | Merged onto the wrapper div |\n| `virtualRowHeight` | `number` | — | Row height in px. Set this to enable virtual scrolling. The parent `<Table>` must have a `height` set. |\n| `emptyIcon` | `ReactNode` | Inbox icon | JSX/ReactNode icon shown above the empty text when `data` is empty. Defaults to an Inbox icon from `lucide-react`. |\n| `emptyText` | `string` | `\"No results\"` | Message shown when `data` is empty |\n| `classNames` | `TableContentClassNames` | — | Part-level class overrides |\n\nWhen `selectable` is true, a checkbox column is always present, collapsed to 0 width when no rows are selected. The header checkbox selects or deselects all visible rows.\n\nRows show hover state and cursor pointer when `selectable` or `onRowClick` is set.\n\n### Row actions\n\n`rowActions` is the **preferred** way to add a per-row action menu — prefer it over hand-rolling a `Popover` per row (see the CRUD example below, kept only for cases needing fully custom menu content). It renders a single kebab-triggered dropdown that opens two ways:\n\n- **Kebab click** — opens anchored to the kebab button, right-aligned underneath it, same as any dropdown trigger.\n- **Right-click anywhere in the row** — opens at the exact cursor position (`clientX`/`clientY`), not snapped to the kebab column. This lets users right-click near the data they care about instead of hunting for the actions column. Right-clicking the same row again while its menu is already open closes it, instead of re-opening at the new cursor position.\n\nBoth open the same menu and share the same `TableRowAction<T>[]` items — no extra wiring needed.\n\n```tsx\n<TableContent\n columns={columns}\n data={rows}\n rowKey={(row) => row.id}\n rowActions={[\n { id: \"view\", label: \"View\", icon: <Eye size={14} />, onClick: (row) => openView(row) },\n { id: \"edit\", label: \"Edit\", icon: <Edit size={14} />, onClick: (row) => openEdit(row) },\n {\n id: \"delete\",\n label: \"Delete\",\n icon: <Trash2 size={14} />,\n variant: \"destructive\",\n onClick: (row) => deleteRow(row.id),\n },\n ]}\n/>\n```\n\n**Bulk actions in the row-actions menu.** Pass `bulkActions` (requires `selectable`) to surface bulk actions inside that same kebab/right-click menu, under a \"Bulk actions\" section label. A Select all/Deselect all toggle always shows there; the rest of `bulkActions` only appears once one or more rows are selected. Add `bulkActionsFooter` to also render the `TableBulkActions` bar at the bottom automatically — otherwise `bulkActions` only ever surface inside the menu, and you place `<TableBulkActions>` yourself if you want the bottom bar:\n\n```tsx\n<TableContent\n columns={columns}\n data={rows}\n rowKey={(row) => row.id}\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n rowActions={rowActions}\n bulkActions={[\n { id: \"export\", label: \"Export\", icon: <Download size={14} />, onClick: (ids) => exportRows(ids) },\n { id: \"delete\", label: \"Delete\", icon: <Trash2 size={14} />, destructive: true, onClick: (ids) => deleteRows(ids) },\n ]}\n bulkActionsFooter\n/>\n```\n\nNeed a plain row click instead (e.g. navigating to a detail page)? Use `onRowClick` — it fires independently of `rowActions` and of selection toggling:\n\n```tsx\n<TableContent\n columns={columns}\n data={rows}\n rowKey={(row) => row.id}\n onRowClick={(row) => navigate(`/members/${row.id}`)}\n/>\n```\n\n### `TableColumn<T>`\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| `id` | `string` | — | Unique column identifier |\n| `header` | `string` | — | Column header text |\n| `accessorKey` | `keyof T` | — | Property path on the row |\n| `accessorFn` | `(row: T) => ReactNode` | — | Custom value extractor (takes priority over `accessorKey`) |\n| `cell` | `(info: { row: T; value: ReactNode }) => ReactNode` | — | Custom cell renderer |\n| `width` | `number` | — | Fixed width in px |\n| `minWidth` | `number` | `120` | Minimum width (used when `width` is omitted) |\n| `maxWidth` | `number` | — | Maximum width in px |\n| `sortable` | `boolean` | `false` | Enable column sorting (client-side only) |\n| `align` | `\"left\" \\| \"center\" \\| \"right\"` | `\"left\"` | Text alignment |\n\n## `<TableBulkActions>`\n\nA bar that appears when rows are selected, showing \"Selected N\" on the left and action buttons on the right. Place it as a child of `<Table>`, between `<TableContent>` and `<TableFooter>`. Auto-hides when no rows are selected (`selectedIds.size === 0`).\n\nAction handlers receive `selectedIds` automatically — consumers never reference the selection state directly in their handlers.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `selectedIds` | `Set<string \\| number>` | — | **Required.** Currently selected row keys. Auto-hides when empty. |\n| `selectedCount` | `number` | `selectedIds.size` | Override displayed count |\n| `onClear` | `() => void` | — | Called when the X clear button is clicked |\n| `actions` | `TableBulkAction[]` | — | Predefined actions rendered as inline buttons |\n| `renderActions` | `({ selectedIds }) => ReactNode` | — | Render prop for custom action content. Replaces the default buttons. |\n| `classNames` | `TableBulkActionsClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root bar |\n\n### Basic usage\n\n```tsx\nconst [selected, setSelected] = useState<Set<string | number>>(new Set());\n\n<Table paramPrefix=\"users\">\n <TableContent\n selectable\n selectedIds={selected}\n onSelectionChange={setSelected}\n columns={columns}\n data={users}\n rowKey={(u) => u.id}\n />\n <TableBulkActions\n selectedIds={selected}\n actions={[\n {\n id: \"delete\",\n label: \"Delete\",\n icon: <Trash2 size={14} />,\n onClick: (ids) => deleteUsers(ids),\n destructive: true,\n },\n ]}\n />\n</Table>\n```\n\n`selectedIds` is controlled — pass it alongside `onSelectionChange` so both `TableContent` and `TableBulkActions` read from the same source. `onClear` on `TableBulkActions` resets the shared state. Add `rowActions` to render a per-row kebab dropdown.\n\n### Custom render\n\nUse `renderActions` to replace the button group with your own content:\n\n```tsx\n<TableBulkActions\n selectedIds={selected}\n renderActions={({ selectedIds }) => (\n <>\n <Button size=\"sm\" variant=\"outline\" onClick={() => handleCustom(selectedIds)}>\n Custom\n </Button>\n </>\n )}\n/>\n```\n\n## `<TablePagination>`\n\nRenders \"Showing X-Y of Z\" with a page-size dropdown on the left, and page number buttons with Previous/Next on the right. Reads `page` and `pageSize` from the URL; writes changes back. Uses a sliding window with ellipsis when total pages exceed `visiblePages`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `totalPages` | `number` | — | Total number of pages |\n| `totalItems` | `number` | — | Total item count (shown as \"Showing X–Y of N\") |\n| `pageSizeOptions` | `number[]` | `[10,20,50,100]` | Options in the page-size dropdown |\n| `visiblePages` | `number` | `10` | Max page number buttons before ellipsis |\n| `classNames` | `TablePaginationClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root div |\n\n## `<TableHeader>`\n\nLayout wrapper for the header bar. Typically contains `<TableSearch>` and `<TableFilter>`.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `children` | `ReactNode` | Header content |\n| `classNames` | `TableHeaderClassNames` | — | Part-level class overrides |\n| `className` | `string` | Merged onto the header bar |\n\n## `<TableFooter>`\n\nLayout wrapper for the footer bar with `border-top`. Typically contains `<TablePagination>`. Pass `noBorder` to remove the top border — preferred inside `TableContent variant=\"panel\"` layouts, where the panel itself already provides a boundary.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `children` | `ReactNode` | Footer content |\n| `noBorder` | `boolean` | Removes the footer's top border |\n| `classNames` | `TableFooterClassNames` | — | Part-level class overrides |\n| `className` | `string` | Merged onto the footer bar |\n\n## `<TableActions>`\n\nActions area inside the header bar. Layout wrapper for action buttons.\n\n| Prop | Type | Description |\n|------|------|-------------|\n| `children` | `ReactNode` | Action content |\n| `classNames` | `TableActionsClassNames` | Part-level class overrides |\n| `className` | `string` | Merged onto the actions wrapper |\n\n## `useTableState`\n\n**Recommended** — the single hook for all table state. Reads page, pageSize, search, filters, and sort from URL params. Use it to derive filtered/paged data from your raw dataset. Prefer this over `useTableFilter` and `useTablePagination` individually.\n\n```tsx\nconst { page, pageSize, search, filters, pageSizeOptions } = useTableState({\n defaultPageSize: 10,\n paramPrefix: \"members\",\n});\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `defaultPage` | `number` | `1` | Initial page when URL has no value |\n| `defaultPageSize` | `number` | `10` | Initial page size |\n| `pageSizeOptions` | `number[]` | `[10,20,50,100]` | Options passed to pagination dropdown |\n| `defaultSearch` | `string` | `\"\"` | Initial search term |\n| `defaultFilters` | `Partial<TFilters>` | — | Initial filter defaults |\n| `defaultSort` | `{ column, direction } \\| null` | `null` | Initial sort state |\n| `paramPrefix` | `string` | — | Must match `<Table paramPrefix>` |\n\n**Returns:** `{ page, pageSize, pageSizeOptions, setPage, setPageSize, search, setSearch, filters, setFilters, setFilter, sort, setSort, resetAll }`\n\nFilters are stored as `Record<string, string>`. Values from URL are always strings — coerce in your filtering logic as needed. Use `filters.status ?? \"\"` to provide a default.\n\n## `useTableFilter`\n\nStandalone hook for URL-driven filter state without search, pagination, or sort.\n\n```tsx\nimport { useTableFilter } from \"@rfdtech/components\";\n\nconst { filters, setFilter, setFilters, resetFilters } = useTableFilter({\n defaults: { role: \"admin\" },\n paramPrefix: \"members\",\n});\n```\n\n| Option | Type | Default |\n|--------|------|---------|\n| `defaults` | `Record<string, string>` | — | Default filter values |\n| `paramPrefix` | `string` | — | URL key namespace |\n\n**Returns:** `{ filters, setFilter, setFilters, resetFilters }`\n\n## `useTablePagination`\n\nStandalone hook for URL-driven pagination without search or filters.\n\n```tsx\nconst { page, pageSize, setPage, setPageSize, reset } = useTablePagination({\n defaultPageSize: 10,\n});\n```\n\n| Option | Type | Default |\n|--------|------|---------|\n| `defaultPage` | `number` | `1` |\n| `defaultPageSize` | `number` | `10` |\n| `paramPrefix` | `string` | — |\n\n**Returns:** `{ page, pageSize, setPage, setPageSize, reset }`\n\n## Types\n\n### `TableProps`\n\nRoot props including `paramPrefix`, `classNames`, and `className`.\n\n### `TableClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Root wrapper (`.clet-table`) |\n| `header` | Header bar (`.clet-table__header-bar`) |\n| `content` | Content wrapper (`.clet-table__content`) |\n| `footer` | Footer bar (`.clet-table__footer`) |\n| `selectionCell` | Selection checkbox cells (`.clet-table__checkbox-cell`) |\n\n### `TableBulkAction`\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `id` | `string` | Unique action identifier |\n| `label` | `string` | Display label in the menu |\n| `icon` | `ReactNode` | Optional leading icon |\n| `onClick` | `(selectedIds: Set<string \\| number>) => void` | Called with the current selection |\n| `destructive` | `boolean` | Renders the action in error color |\n\n### `TableRowAction`\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `id` | `string` | Unique action identifier |\n| `label` | `string` | Display label in the dropdown |\n| `icon` | `ReactNode` | Optional leading icon |\n| `variant` | `\"default\" \\| \"destructive\"` | Visual style. `\"destructive\"` renders in error color. |\n| `onClick` | `(row: T) => void` | Called with the row data when the action is selected |\n| `condition` | `(row: T) => boolean` | If provided, action only appears when this returns `true` for the row |\n\n### `TableBulkActionsClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Bar wrapper (`.clet-table__bulk-actions-bar`) |\n| `count` | \"Selected N\" label |\n\n### `TableContentClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Content wrapper (`.clet-table__content`) |\n| `empty` | Empty state container (`.clet-table__empty`) |\n| `emptyIcon` | Empty state icon (`.clet-table__empty-icon`) |\n| `emptyText` | Empty state text (`.clet-table__empty-text`) |\n| `th` | Column header cells |\n| `thLabel` | Header label text (`.clet-table__th-label`) |\n| `sortIcon` | Sort indicator (`.clet-table__sort-icon`) |\n| `checkboxCell` | Selection checkbox cells (`.clet-table__checkbox-cell`) |\n| `actionsCell` | Actions column cell (`.clet-table__actions-cell`) |\n| `actionsTrigger` | Kebab trigger button (`.clet-table__actions-trigger`) |\n| `actionsMenu` | Actions popover content (`.clet-table__actions-menu`) |\n| `actionsItem` | Individual action item (`.clet-table__actions-item`) |\n| `actionsSectionLabel` | \"Bulk actions\" section label inside the row-actions menu, shown when `bulkActions` is set (`.clet-table__actions-section-label`) |\n| `skeleton` | Loading skeleton spans (`.clet-table__skeleton--*`) |\n| `viewport` | Virtual scroll viewport (`.clet-table__viewport`) |\n\n### `TableSearchClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Search wrapper (`.clet-table__search`) |\n| `icon` | Search icon (`.clet-table__search-icon`) |\n| `input` | Search input (`.clet-table__search-input`) |\n| `clear` | Clear button (`.clet-table__search-clear`) |\n\n### `TableFilterClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Filter wrapper (`.clet-table__filter`) |\n| `trigger` | Filter trigger button (`.clet-table__filter-trigger`) |\n| `badge` | Active filter count badge (`.clet-table__filter-badge`) |\n| `content` | Popover content (`.clet-table__filter-content`) |\n| `header` | Popover header bar (`.clet-table__filter-header`) |\n| `resetButton` | Reset button (`.clet-table__filter-btn--reset`) |\n| `fields` | Filter form fields container (`.clet-table__filter-fields`) |\n| `actions` | Filter actions bar (`.clet-table__filter-actions`) |\n| `applyButton` | Apply button (`.clet-table__filter-btn--apply`) |\n\n### `TablePaginationClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Pagination wrapper (`.clet-table__pagination`) |\n| `results` | Results summary (`.clet-table__page-results`) |\n| `pageSize` | Page size dropdown (`.clet-table__page-size`) |\n| `pages` | Page number buttons container (`.clet-table__pagination-pages`) |\n| `ellipsis` | Ellipsis indicator (`.clet-table__page-ellipsis`) |\n\n### `TableHeaderClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Header bar (`.clet-table__header-bar`) |\n\n### `TableFooterClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Footer bar (`.clet-table__footer`) |\n\n### `TableActionsClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | Actions wrapper (`.clet-table__actions`) |\n\n### `PaginationControlsProps`\n\nProps for `TablePagination` — see the props section above.\n\n## Design tokens\n\nThe table uses the standard `--clet-*` palette. The tokens with the most visual impact:\n\n| Token | Effect |\n|-------|--------|\n| `--clet-radius-base` | All border radii |\n| `--clet-surface-panel` | Background of the `panel` variant content surface |\n| `--clet-border-subtle` | Row dividers and outer border of the `panel` variant |\n| `--clet-table-font` | Font family for the table root, search input, and filter trigger — default `var(--clet-font-body)` |\n| `--clet-table-filter-font` | Font family for the filter popover's apply/reset buttons — default `var(--clet-font-body)` |\n| `--clet-table-actions-font` | Font family for row actions menu items — default `var(--clet-font-body)` |\n| `--clet-progress-modal-font` | Font family for the bulk-import progress modal's status text — default `var(--clet-font-body)` |\n\n## Notes\n\n- All URL writes are `replace: true` except page changes which use `pushState` for back-button support.\n- Changing search, filters, or page size automatically resets to page 1.\n- Sort is client-side only (`localeCompare`). No server integration.\n- Loading skeletons use a pulse animation; reduced-motion users get a static version.\n- Without a `paramPrefix`, keys are plain (`page`, `search`, `f_status`). With one, they are namespaced (`members.page`, `members.search`, `members.f_status`).\n- `paramPrefix` is required on `<Table>` — every table must have a unique namespace to avoid URL collisions.\n- The filter form captures all elements with a `name` attribute via `FormData`. For custom inputs (date pickers, multi-selects), ensure they set a `name` and participate in `FormData`.\n- Selection is page-local: \"select all\" only selects rows on the current page. Selection is fully controlled via `selectedIds` / `onSelectionChange` — pass the same `selectedIds` to `TableBulkActions` for sync.\n- At `768px` and below (matching the breakpoint `Sidebar` uses for its mobile drawer), `TableHeader`'s children stack full-width instead of squeezing into one row, both `TableFilter` variants stack their fields, `TablePagination` wraps and centers its two rows (results/page-size, then prev/pages/next), and the bulk-actions bar wraps instead of clipping.\n\n## Do\n\n- Use the `TableActions` slot inside `TableHeader` for toolbar-level filters or actions (e.g. a `Dropdown` filter) that are distinct from per-row `TableRowAction`s and bulk `TableBulkActions`.\n- Prefer `rowActions` over a hand-rolled per-row `Popover` for kebab menus — it gets cursor-anchored right-click for free, alongside the standard kebab-click trigger.\n- Prefer `bulkActions` + `bulkActionsFooter` on `TableContent` over manually pairing it with a standalone `<TableBulkActions>` sibling — one prop pair keeps the row-actions menu's \"Bulk actions\" section and the bottom bar in sync automatically. Reach for a standalone `<TableBulkActions>` only if you need it placed somewhere `TableContent` can't reach (e.g. outside `<Table>` context) or without the in-menu section.\n\n## Don't\n\n- Don't pull `TableSearch`, `TableFilter`, `TableActions`, or a view-mode toggle out of `<Table>` context into a plain `<div>` — these children require the ancestor `<Table paramPrefix>` to function.\n- When a page has a list/tile view toggle, don't rebuild `TableHeader` per mode — keep it (search, filters, toggle) identical for both, and only swap `TableContent` (list) for a grid (tiles) inside `<Table>`.\n- Don't hand-roll `onContextMenu` + a controlled `Popover` to get a cursor-anchored right-click menu — `rowActions` already does this.",
2990
3156
  "headings": [
2991
3157
  "Table",
2992
3158
  "Example",
@@ -3031,7 +3197,7 @@
3031
3197
  "Do",
3032
3198
  "Don't"
3033
3199
  ],
3034
- "types": "import type {\n HTMLAttributes,\n InputHTMLAttributes,\n MouseEvent,\n ReactNode,\n} from \"react\";\n\nexport interface TableColumn<T> {\n id: string;\n header: string;\n accessorKey?: keyof T;\n accessorFn?: (row: T) => ReactNode;\n cell?: (info: { row: T; value: ReactNode }) => ReactNode;\n /** Width in px. If omitted, column shares remaining space equally. */\n width?: number;\n minWidth?: number;\n maxWidth?: number;\n sortable?: boolean;\n align?: \"left\" | \"center\" | \"right\";\n}\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface TableSortState {\n column: string;\n direction: SortDirection;\n}\n\nexport interface TableRowAction<T> {\n id: string;\n label: string;\n icon?: ReactNode;\n onClick: (row: T) => void;\n variant?: \"default\" | \"destructive\";\n /** If provided, must return true for the action to appear for this row */\n condition?: (row: T) => boolean;\n}\n\nexport interface TableBulkAction {\n id: string;\n label: string;\n icon?: ReactNode;\n onClick: (selectedIds: Set<string | number>) => void;\n destructive?: boolean;\n}\n\nexport interface TableBulkActionsClassNames {\n root?: string;\n count?: string;\n}\n\nexport interface TableBulkActionsProps {\n /** Currently selected row keys */\n selectedIds: Set<string | number>;\n /** Override the displayed count (defaults to selectedIds.size) */\n selectedCount?: number;\n /** Callback to clear all selections */\n onClear?: () => void;\n /** Predefined actions rendered as inline buttons */\n actions?: TableBulkAction[];\n /**\n * Render prop for custom action content.\n * Receives selectedIds so consumers never need to manage selection state directly.\n */\n renderActions?: (params: { selectedIds: Set<string | number> }) => ReactNode;\n classNames?: TableBulkActionsClassNames;\n className?: string;\n}\n\nexport interface TableClassNames {\n root?: string;\n header?: string;\n content?: string;\n footer?: string;\n selectionCell?: string;\n}\n\nexport interface TableProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableClassNames;\n className?: string;\n /** URL param namespace shared by all child table components */\n paramPrefix: string;\n height?: number;\n}\n\nexport interface TableHeaderClassNames {\n root?: string;\n}\n\nexport interface TableHeaderProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableHeaderClassNames;\n className?: string;\n}\n\nexport interface TableActionsClassNames {\n root?: string;\n}\n\nexport interface TableActionsProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableActionsClassNames;\n className?: string;\n}\n\nexport interface TableContentClassNames {\n root?: string;\n empty?: string;\n emptyIcon?: string;\n emptyText?: string;\n th?: string;\n thLabel?: string;\n sortIcon?: string;\n checkboxCell?: string;\n actionsCell?: string;\n actionsTrigger?: string;\n actionsMenu?: string;\n actionsItem?: string;\n actionsSectionLabel?: string;\n skeleton?: string;\n viewport?: string;\n}\n\nexport interface TableContentProps<\n T = unknown,\n> extends HTMLAttributes<HTMLDivElement> {\n className?: string;\n classNames?: TableContentClassNames;\n /**\n * Visual style of the table content wrapper.\n * `\"panel\"` gives a faint recessed surface, subtle row dividers, and a\n * border all around — suited for embedding inside tabs or cards.\n */\n variant?: \"default\" | \"panel\";\n columns?: TableColumn<T>[];\n data?: T[];\n rowKey?: (row: T) => string | number;\n /** Icon (JSX/ReactNode) shown above the empty text when data is empty */\n emptyIcon?: ReactNode;\n /** Message shown when data is empty (defaults to \"No results\") */\n emptyText?: string;\n /** Show skeleton loading rows instead of data (default false) */\n loading?: boolean;\n /** Number of skeleton rows to show while loading (default 5) */\n loadingRows?: number;\n /** Show a checkbox selection column (default false) */\n selectable?: boolean;\n /** Current set of selected row keys. Pass with onSelectionChange for controlled mode. Required for TableBulkActions sync. */\n selectedIds?: Set<string | number>;\n /** Called when selection changes with the full set of selected row keys */\n onSelectionChange?: (selectedIds: Set<string | number>) => void;\n /** Row-level actions rendered as a kebab dropdown at the end of each row */\n rowActions?: TableRowAction<T>[];\n /**\n * Bulk actions surfaced inside the row-actions menu (kebab click or\n * right-click) as a \"Bulk actions\" section below the row's own actions —\n * always includes a \"Select all\" / \"Deselect all\" toggle, with the rest of\n * the list appearing once one or more rows are selected.\n */\n bulkActions?: TableBulkAction[];\n /**\n * When true, also renders the `TableBulkActions` bar at the bottom of the\n * table automatically, wired to the same `bulkActions`/`selectedIds` — no\n * need to place `<TableBulkActions>` yourself as a sibling. When false\n * (default), `bulkActions` only surface inside the row-actions menu.\n */\n bulkActionsFooter?: boolean;\n /** Called when a row is clicked (in addition to selection toggling, when `selectable` is set) */\n onRowClick?: (row: T, event: MouseEvent<HTMLTableRowElement>) => void;\n /** Row height in px. Set this to enable virtual scrolling. Parent <Table> must have a height set. */\n virtualRowHeight?: number;\n}\n\nexport interface TableFooterClassNames {\n root?: string;\n}\n\nexport interface TableFooterProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableFooterClassNames;\n className?: string;\n}\n\nexport interface TableSearchClassNames {\n root?: string;\n icon?: string;\n input?: string;\n clear?: string;\n}\n\nexport interface TableSearchProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"type\" | \"children\"\n> {\n placeholder?: string;\n debounceMs?: number;\n onSearch?: (value: string) => void;\n classNames?: TableSearchClassNames;\n className?: string;\n}\n\nexport interface TableFilterClassNames {\n root?: string;\n trigger?: string;\n badge?: string;\n content?: string;\n header?: string;\n resetButton?: string;\n fields?: string;\n actions?: string;\n applyButton?: string;\n}\n\nexport type TableFilterVariant = \"popover\" | \"spread\";\n\nexport interface TableFilterProps {\n children?: ReactNode;\n onApply?: () => void;\n onReset?: () => void;\n applyLabel?: string;\n resetLabel?: string;\n /** \"popover\" (default) opens the fields in a popover panel. \"spread\" removes the popover and lays the fields out inline as a flex row. */\n variant?: TableFilterVariant;\n classNames?: TableFilterClassNames;\n className?: string;\n}\n\nexport interface TablePaginationClassNames {\n root?: string;\n results?: string;\n pageSize?: string;\n pages?: string;\n ellipsis?: string;\n}\n\nexport interface PaginationControlsProps {\n totalPages: number;\n totalItems?: number;\n pageSizeOptions?: number[];\n defaultPageSize?: number;\n visiblePages?: number;\n classNames?: TablePaginationClassNames;\n className?: string;\n}\n",
3200
+ "types": "import type {\n HTMLAttributes,\n InputHTMLAttributes,\n MouseEvent,\n ReactNode,\n} from \"react\";\n\nexport interface TableColumn<T> {\n id: string;\n header: string;\n accessorKey?: keyof T;\n accessorFn?: (row: T) => ReactNode;\n cell?: (info: { row: T; value: ReactNode }) => ReactNode;\n /** Width in px. If omitted, column shares remaining space equally. */\n width?: number;\n minWidth?: number;\n maxWidth?: number;\n sortable?: boolean;\n align?: \"left\" | \"center\" | \"right\";\n}\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface TableSortState {\n column: string;\n direction: SortDirection;\n}\n\nexport interface TableRowAction<T> {\n id: string;\n label: string;\n icon?: ReactNode;\n onClick: (row: T) => void;\n variant?: \"default\" | \"destructive\";\n /** If provided, must return true for the action to appear for this row */\n condition?: (row: T) => boolean;\n}\n\nexport interface TableBulkAction {\n id: string;\n label: string;\n icon?: ReactNode;\n onClick: (selectedIds: Set<string | number>) => void;\n destructive?: boolean;\n}\n\nexport interface TableBulkActionsClassNames {\n root?: string;\n count?: string;\n}\n\nexport interface TableBulkActionsProps {\n /** Currently selected row keys */\n selectedIds: Set<string | number>;\n /** Override the displayed count (defaults to selectedIds.size) */\n selectedCount?: number;\n /** Callback to clear all selections */\n onClear?: () => void;\n /** Predefined actions rendered as inline buttons */\n actions?: TableBulkAction[];\n /**\n * Render prop for custom action content.\n * Receives selectedIds so consumers never need to manage selection state directly.\n */\n renderActions?: (params: { selectedIds: Set<string | number> }) => ReactNode;\n classNames?: TableBulkActionsClassNames;\n className?: string;\n}\n\nexport interface TableClassNames {\n root?: string;\n header?: string;\n content?: string;\n footer?: string;\n selectionCell?: string;\n}\n\nexport interface TableProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableClassNames;\n className?: string;\n /** URL param namespace shared by all child table components */\n paramPrefix: string;\n height?: number;\n}\n\nexport interface TableHeaderClassNames {\n root?: string;\n}\n\nexport interface TableHeaderProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableHeaderClassNames;\n className?: string;\n}\n\nexport interface TableActionsClassNames {\n root?: string;\n}\n\nexport interface TableActionsProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableActionsClassNames;\n className?: string;\n}\n\nexport interface TableContentClassNames {\n root?: string;\n empty?: string;\n emptyIcon?: string;\n emptyText?: string;\n th?: string;\n thLabel?: string;\n sortIcon?: string;\n checkboxCell?: string;\n actionsCell?: string;\n actionsTrigger?: string;\n actionsMenu?: string;\n actionsItem?: string;\n actionsSectionLabel?: string;\n skeleton?: string;\n viewport?: string;\n}\n\nexport interface TableContentProps<\n T = unknown,\n> extends HTMLAttributes<HTMLDivElement> {\n className?: string;\n classNames?: TableContentClassNames;\n /**\n * Visual style of the table content wrapper.\n * `\"panel\"` gives a faint recessed surface, subtle row dividers, and a\n * border all around — suited for embedding inside tabs or cards.\n */\n variant?: \"default\" | \"panel\";\n columns?: TableColumn<T>[];\n data?: T[];\n rowKey?: (row: T) => string | number;\n /** Icon (JSX/ReactNode) shown above the empty text when data is empty */\n emptyIcon?: ReactNode;\n /** Message shown when data is empty (defaults to \"No results\") */\n emptyText?: string;\n /** Show skeleton loading rows instead of data (default false) */\n loading?: boolean;\n /** Number of skeleton rows to show while loading (default 5) */\n loadingRows?: number;\n /** Show a checkbox selection column (default false) */\n selectable?: boolean;\n /** Current set of selected row keys. Pass with onSelectionChange for controlled mode. Required for TableBulkActions sync. */\n selectedIds?: Set<string | number>;\n /** Called when selection changes with the full set of selected row keys */\n onSelectionChange?: (selectedIds: Set<string | number>) => void;\n /** Row-level actions rendered as a kebab dropdown at the end of each row */\n rowActions?: TableRowAction<T>[];\n /**\n * Bulk actions surfaced inside the row-actions menu (kebab click or\n * right-click) as a \"Bulk actions\" section below the row's own actions —\n * always includes a \"Select all\" / \"Deselect all\" toggle, with the rest of\n * the list appearing once one or more rows are selected.\n */\n bulkActions?: TableBulkAction[];\n /**\n * When true, also renders the `TableBulkActions` bar at the bottom of the\n * table automatically, wired to the same `bulkActions`/`selectedIds` — no\n * need to place `<TableBulkActions>` yourself as a sibling. When false\n * (default), `bulkActions` only surface inside the row-actions menu.\n */\n bulkActionsFooter?: boolean;\n /** Called when a row is clicked (in addition to selection toggling, when `selectable` is set) */\n onRowClick?: (row: T, event: MouseEvent<HTMLTableRowElement>) => void;\n /** Row height in px. Set this to enable virtual scrolling. Parent <Table> must have a height set. */\n virtualRowHeight?: number;\n}\n\nexport interface TableFooterClassNames {\n root?: string;\n}\n\nexport interface TableFooterProps extends HTMLAttributes<HTMLDivElement> {\n classNames?: TableFooterClassNames;\n className?: string;\n /** Removes the footer's top border. Prefer this over hand-writing `className=\"clet-table__footer--no-border\"`. */\n noBorder?: boolean;\n}\n\nexport interface TableSearchClassNames {\n root?: string;\n icon?: string;\n input?: string;\n clear?: string;\n}\n\nexport interface TableSearchProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"type\" | \"children\"\n> {\n placeholder?: string;\n debounceMs?: number;\n onSearch?: (value: string) => void;\n classNames?: TableSearchClassNames;\n className?: string;\n}\n\nexport interface TableFilterClassNames {\n root?: string;\n trigger?: string;\n badge?: string;\n content?: string;\n header?: string;\n resetButton?: string;\n fields?: string;\n actions?: string;\n applyButton?: string;\n}\n\nexport type TableFilterVariant = \"popover\" | \"spread\";\n\nexport interface TableFilterProps {\n children?: ReactNode;\n onApply?: () => void;\n onReset?: () => void;\n applyLabel?: string;\n resetLabel?: string;\n /** \"popover\" (default) opens the fields in a popover panel. \"spread\" removes the popover and lays the fields out inline as a flex row. */\n variant?: TableFilterVariant;\n classNames?: TableFilterClassNames;\n className?: string;\n}\n\nexport interface TablePaginationClassNames {\n root?: string;\n results?: string;\n pageSize?: string;\n pages?: string;\n ellipsis?: string;\n}\n\nexport interface PaginationControlsProps {\n totalPages: number;\n totalItems?: number;\n pageSizeOptions?: number[];\n defaultPageSize?: number;\n visiblePages?: number;\n classNames?: TablePaginationClassNames;\n className?: string;\n}\n",
3035
3201
  "examples": [
3036
3202
  {
3037
3203
  "file": "table-actions.example.tsx",
@@ -3346,7 +3512,7 @@
3346
3512
  ],
3347
3513
  "import": "@rfdtech/components"
3348
3514
  },
3349
- "docs": "# Timeline\n\nComposable vertical timeline with `Timeline` and `TimelineItem` primitives. Renders an `<ol>` list with a left rail of dots and connectors, and a right column for custom content. Use `TimelineTitle`, `TimelineData`, and `TimelineFooter` sub-components for styled content slots.\n\n## Example\n\n<ExampleTabs\n title=\"Case history\"\n preview={<TimelinePreview />}\n code={timelineSource}\n/>\n\n## Components\n\n### `Timeline`\n\nRoot `<ol>` container. Accepts only `TimelineItem` children — non-items are filtered out. Injects `isLast` and index-based animation delay into each item.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `TimelineClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root |\n| `children` | `ReactNode` | — | Only `TimelineItem` children are rendered |\n\nAlso accepts standard native `<ol>` attributes.\n\n### `TimelineItem`\n\nSingle event entry. Renders the dot + connector rail on the left and `children` as content on the right.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `mode` | `\"primary\" \\| \"success\" \\| \"warning\" \\| \"error\" \\| \"muted\"` | — | Dot color variant from CLET primitives; no prop = neutral outlined dot |\n| `color` | `string` | — | Custom CSS color for the dot; overrides the mode's background and border |\n| `icon` | `ReactNode` | — | Replaces the default dot fill (e.g. `Check` / `AlertTriangle`) |\n| `classNames` | `TimelineItemClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root list item |\n| `children` | `ReactNode` | — | Right-side content (title, date, description, or any markup) |\n\nAlso accepts standard native `<li>` attributes.\n\n### `TimelineTitle`\n\nStyled heading for the item title. Renders as `<h3>` by default.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `as` | `\"h1\" \\| \"h2\" \\| \"h3\" \\| \"h4\" \\| \"h5\" \\| \"h6\"` | `\"h3\"` | Heading level |\n| `classNames` | `TimelineTitleClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the heading |\n| `children` | `ReactNode` | — | Title text |\n\nAlso accepts standard native heading attributes.\n\n### `TimelineData`\n\nStyled paragraph for date, description, or any metadata.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `TimelineDataClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the paragraph |\n| `children` | `ReactNode` | — | Data content |\n\nAlso accepts standard native `<p>` attributes.\n\n### `TimelineFooter`\n\nStyled wrapper for footer content below the title and data.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `TimelineFooterClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the footer |\n| `children` | `ReactNode` | — | Footer content |\n\nAlso accepts standard native `<div>` attributes.\n\n## Types\n\nExported from `@rfdtech/components`:\n\n- `TimelineProps`, `TimelineClassNames`\n- `TimelineItemProps`, `TimelineItemClassNames`\n- `TimelineTitleProps`, `TimelineTitleClassNames`\n- `TimelineDataProps`, `TimelineDataClassNames`\n- `TimelineFooterProps`, `TimelineFooterClassNames`\n- `TimelineItemMode`\n\n### `TimelineItemClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | `<li>` element |\n| `rail` | Left column with dot and connector |\n| `dot` | The dot itself |\n| `connector` | Vertical line between dots |\n| `content` | Right column wrapper |\n\n## Notes\n\n- The `primary` mode shows a pulsing ring around the dot and is intended for the \"active\" or \"you are here\" step.\n- Connectors animate in from top to bottom with a staggered delay per item. Disabled automatically when `prefers-reduced-motion: reduce` is active.\n- The timeline fits a 320px viewport — the content column wraps naturally, and the rail stays fixed at 20px.\n- Dot icons should be ~10×10px (lucide `size={10}`). Larger icons may overflow the 16px dot.\n- Wrap `Timeline` in your own `<section>` or card for layout and heading.\n\n### Tokens\n\nDot mode colors use existing CLET tokens. Customize the dot halo via `--clet-timeline-halo`:\n\n| Token | Default | Used by |\n|-------|---------|---------|\n| `--clet-primary` | Theme primary | `mode=\"primary\"` |\n| `--clet-success` | `#16a34a` | `mode=\"success\"` |\n| `--clet-warning` | `#eab308` | `mode=\"warning\"` |\n| `--clet-error` | `#dc2626` | `mode=\"error\"` |\n| `--clet-text-muted` | `#9ca3af` | `mode=\"muted\"` |\n| `--clet-timeline-halo` | `--clet-bg` | Ring behind the dot that covers the connector line |\n| `--clet-timeline-font` | `--clet-font-body` | Font family for the root `<ol>` |\n| `--clet-timeline-stagger-step` | `60ms` | Per-item animation delay increment (multiplied by item index) |\n| `--clet-timeline-draw-duration` | `300ms` | Duration of the connector line draw-in animation |\n| `--clet-timeline-pop-duration` | `200ms` | Duration of the dot pop-in animation |\n| `--clet-timeline-index` | `0` | Item index used to compute staggered animation delay; set automatically per item |",
3515
+ "docs": "# Timeline\n\nComposable vertical timeline with `Timeline` and `TimelineItem` primitives. Renders an `<ol>` list with a left rail of dots and connectors, and a right column for custom content. Use `TimelineTitle`, `TimelineData`, and `TimelineFooter` sub-components for styled content slots.\n\n## Example\n\n<ExampleTabs\n title=\"Case history\"\n preview={<TimelinePreview />}\n code={timelineSource}\n/>\n\n## Components\n\n### `Timeline`\n\nRoot `<ol>` container. Accepts only `TimelineItem` children — non-items are filtered out. Injects `isLast` and index-based animation delay into each item.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `TimelineClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root |\n| `children` | `ReactNode` | — | Only `TimelineItem` children are rendered |\n\nAlso accepts standard native `<ol>` attributes.\n\n### `TimelineItem`\n\nSingle event entry. Renders the dot + connector rail on the left and `children` as content on the right.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `mode` | `\"primary\" \\| \"success\" \\| \"warning\" \\| \"error\" \\| \"muted\"` | — | Dot color variant from CLET primitives; no prop = neutral outlined dot |\n| `color` | `string` | — | Custom CSS color for the dot; overrides the mode's background and border |\n| `icon` | `ReactNode` | — | Replaces the default dot fill (e.g. `Check` / `AlertTriangle`) |\n| `classNames` | `TimelineItemClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the root list item |\n| `children` | `ReactNode` | — | Right-side content (title, date, description, or any markup) |\n\nAlso accepts standard native `<li>` attributes.\n\n### `TimelineTitle`\n\nStyled heading for the item title. Renders as `<h3>` by default.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `as` | `\"h1\" \\| \"h2\" \\| \"h3\" \\| \"h4\" \\| \"h5\" \\| \"h6\"` | `\"h3\"` | Heading level |\n| `classNames` | `TimelineTitleClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the heading |\n| `children` | `ReactNode` | — | Title text |\n\nAlso accepts standard native heading attributes.\n\n### `TimelineData`\n\nStyled paragraph for date, description, or any metadata.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `TimelineDataClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the paragraph |\n| `children` | `ReactNode` | — | Data content |\n\nAlso accepts standard native `<p>` attributes.\n\n### `TimelineFooter`\n\nStyled wrapper for footer content below the title and data.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `classNames` | `TimelineFooterClassNames` | — | Part-level class overrides |\n| `className` | `string` | — | Merged onto the footer |\n| `children` | `ReactNode` | — | Footer content |\n\nAlso accepts standard native `<div>` attributes.\n\n## Types\n\nExported from `@rfdtech/components`:\n\n- `TimelineProps`, `TimelineClassNames`\n- `TimelineItemProps`, `TimelineItemClassNames`\n- `TimelineTitleProps`, `TimelineTitleClassNames`\n- `TimelineDataProps`, `TimelineDataClassNames`\n- `TimelineFooterProps`, `TimelineFooterClassNames`\n- `TimelineItemMode`\n\n### `TimelineItemClassNames`\n\n| Key | Applied to |\n|-----|------------|\n| `root` | `<li>` element |\n| `rail` | Left column with dot and connector |\n| `dot` | The dot itself |\n| `connector` | Vertical line between dots |\n| `content` | Right column wrapper |\n\n## Notes\n\n- The `primary` mode shows a pulsing ring around the dot and is intended for the \"active\" or \"you are here\" step.\n- Connectors animate in from top to bottom with a staggered delay per item. Disabled automatically when `prefers-reduced-motion: reduce` is active.\n- The timeline fits a 320px viewport — the content column wraps naturally, and the rail stays fixed at 20px.\n- Dot icons should be ~10×10px (lucide `size={10}`). Larger icons may overflow the 16px dot.\n- Wrap `Timeline` in your own `<section>` or card for layout and heading.\n\n## Don't\n\n- **Never put custom icons inside Timeline items.** The dot's mode colors (`primary`, `success`, `warning`, `error`, `muted`) already convey the event type visually. Adding an icon inside the dot overrides the mode's visual signal and clutters the rail. The `icon` prop exists for the rare case where a specific glyph is genuinely clearer than the mode color alone — but in practice, it should almost never be used. Use the mode prop and let the dot speak.\n\n### Tokens\n\nDot mode colors use existing CLET tokens. Customize the dot halo via `--clet-timeline-halo`:\n\n| Token | Default | Used by |\n|-------|---------|---------|\n| `--clet-primary` | Theme primary | `mode=\"primary\"` |\n| `--clet-success` | `#16a34a` | `mode=\"success\"` |\n| `--clet-warning` | `#eab308` | `mode=\"warning\"` |\n| `--clet-error` | `#dc2626` | `mode=\"error\"` |\n| `--clet-text-muted` | `#9ca3af` | `mode=\"muted\"` |\n| `--clet-timeline-halo` | `--clet-bg` | Ring behind the dot that covers the connector line |\n| `--clet-timeline-font` | `--clet-font-body` | Font family for the root `<ol>` |\n| `--clet-timeline-stagger-step` | `60ms` | Per-item animation delay increment (multiplied by item index) |\n| `--clet-timeline-draw-duration` | `300ms` | Duration of the connector line draw-in animation |\n| `--clet-timeline-pop-duration` | `200ms` | Duration of the dot pop-in animation |\n| `--clet-timeline-index` | `0` | Item index used to compute staggered animation delay; set automatically per item |",
3350
3516
  "headings": [
3351
3517
  "Timeline",
3352
3518
  "Example",
@@ -3359,6 +3525,7 @@
3359
3525
  "Types",
3360
3526
  "`TimelineItemClassNames`",
3361
3527
  "Notes",
3528
+ "Don't",
3362
3529
  "Tokens"
3363
3530
  ],
3364
3531
  "types": "import type {\n HTMLAttributes,\n LiHTMLAttributes,\n OlHTMLAttributes,\n ReactNode,\n} from \"react\";\n\nexport type TimelineItemMode = \"primary\" | \"success\" | \"warning\" | \"error\" | \"muted\";\n\nexport interface TimelineClassNames {\n root?: string;\n}\n\nexport interface TimelineProps extends Omit<\n OlHTMLAttributes<HTMLOListElement>,\n \"className\"\n> {\n classNames?: TimelineClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface TimelineItemClassNames {\n root?: string;\n rail?: string;\n dot?: string;\n connector?: string;\n content?: string;\n}\n\nexport interface TimelineItemProps extends Omit<\n LiHTMLAttributes<HTMLLIElement>,\n \"className\"\n> {\n mode?: TimelineItemMode;\n color?: string;\n icon?: ReactNode;\n classNames?: TimelineItemClassNames;\n className?: string;\n children?: ReactNode;\n isLast?: boolean;\n}\n\nexport interface TimelineTitleClassNames {\n title?: string;\n}\n\nexport interface TimelineTitleProps extends Omit<\n HTMLAttributes<HTMLHeadingElement>,\n \"className\"\n> {\n as?: \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n classNames?: TimelineTitleClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface TimelineDataClassNames {\n data?: string;\n}\n\nexport interface TimelineDataProps extends Omit<\n HTMLAttributes<HTMLParagraphElement>,\n \"className\"\n> {\n classNames?: TimelineDataClassNames;\n className?: string;\n children: ReactNode;\n}\n\nexport interface TimelineFooterClassNames {\n footer?: string;\n}\n\nexport interface TimelineFooterProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n \"className\"\n> {\n classNames?: TimelineFooterClassNames;\n className?: string;\n children: ReactNode;\n}\n",