@rfdtech/components 2.0.0 → 2.0.1
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.
- package/README.md +108 -50
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +10 -5
- package/dist/mcp/generated/components.json +13 -5
- package/dist/mcp/generated/index.meta.json +196 -196
- package/dist/mcp/generated/rules.json +1 -1
- package/dist/mcp/generated/search.json +9 -1
- package/dist/mcp/generated/tokens.json +1 -1
- package/dist/next.cjs +1 -1
- package/dist/next.css +1 -1
- package/dist/next.js +10 -5
- package/dist/types/table.d.ts +2 -0
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -546,10 +546,11 @@
|
|
|
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.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.0.1] - 2026-07-16",
|
|
553
554
|
"Added",
|
|
554
555
|
"Breaking",
|
|
555
556
|
"Deprecated",
|
|
@@ -1737,7 +1738,13 @@
|
|
|
1737
1738
|
"stacked layout",
|
|
1738
1739
|
"CountrySelector",
|
|
1739
1740
|
"Combobox",
|
|
1740
|
-
"NetworkOperator"
|
|
1741
|
+
"NetworkOperator",
|
|
1742
|
+
"gsl-to-clet",
|
|
1743
|
+
"cletTheme",
|
|
1744
|
+
"gslTheme",
|
|
1745
|
+
"rename",
|
|
1746
|
+
"TableFooter",
|
|
1747
|
+
"noBorder"
|
|
1741
1748
|
],
|
|
1742
1749
|
"since": "2.0.0",
|
|
1743
1750
|
"status": "guide",
|
|
@@ -1760,7 +1767,7 @@
|
|
|
1760
1767
|
"network-operator"
|
|
1761
1768
|
]
|
|
1762
1769
|
},
|
|
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",
|
|
1770
|
+
"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**`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` | `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\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 noBorder`\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",
|
|
1764
1771
|
"headings": [
|
|
1765
1772
|
"Migration guide: v1 → v2 — Agent Instructions",
|
|
1766
1773
|
"1. `AppHeaderProfile` → `ProfilePopover`",
|
|
@@ -1771,6 +1778,7 @@
|
|
|
1771
1778
|
"6. Prefer `Launchpad` over deprecated `AppSwitcher`",
|
|
1772
1779
|
"7. `CountrySelector` → `Combobox` or `PhoneNumberInput`'s country picker",
|
|
1773
1780
|
"8. `NetworkOperator` → `Combobox`",
|
|
1781
|
+
"9. Naming rename: `.gsl-*` → `.clet-*`, `gslTheme()` → `cletTheme()`",
|
|
1774
1782
|
"Adopting the new design system",
|
|
1775
1783
|
"Layout shell: `AppLayout variant=\"stacked\"` + `AppHeader variant=\"plain\"` + `Sidebar variant=\"plain\"`",
|
|
1776
1784
|
"Use built-in `loading` states — never hand-roll placeholders",
|
|
@@ -2986,7 +2994,7 @@
|
|
|
2986
2994
|
],
|
|
2987
2995
|
"import": "@rfdtech/components"
|
|
2988
2996
|
},
|
|
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.",
|
|
2997
|
+
"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
2998
|
"headings": [
|
|
2991
2999
|
"Table",
|
|
2992
3000
|
"Example",
|
|
@@ -3031,7 +3039,7 @@
|
|
|
3031
3039
|
"Do",
|
|
3032
3040
|
"Don't"
|
|
3033
3041
|
],
|
|
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",
|
|
3042
|
+
"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
3043
|
"examples": [
|
|
3036
3044
|
{
|
|
3037
3045
|
"file": "table-actions.example.tsx",
|