Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,876 @@
# @suaveplan/react-ui - React UI component library — opinionated providers, theme/pack plugin contracts, bundled translations, telemetry, shadcn-compatible registry generator.
## Summary
> Chassis for the Suaveplan React UI component library — opinionated
> providers, theme/pack plugin contracts, bundled translations,
> telemetry, and a shadcn-compatible registry generator.
This package ships the **chassis** for `@suaveplan/react-ui`. The 57+
components land per-batch under Wave 3 (a-h) of spec change
[`enterprise-react-ui-v2`](../../../docs/design/enterprise-react-ui-v2);
this README covers what Wave 2 ships.
<!-- begin:auto-docs -->
## Documentation tree
- [accordion](./src/components/accordion/accordion.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - accordion.variants — `accordionVariants`, `accordio…
- [accordion](./src/components/accordion/accordion.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./src/components/accordion/accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - alert.variants — `alertVariants` is the cva config……
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - alert.variants — `alertVariants` is the cva config……
- [alert.variants](./src/components/alert/alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./src/components/alert-dialog/alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./src/components/aspect-ratio/aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - avatar.variants — `avatarVariants` is the cva confi…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - avatar.variants — `avatarVariants` is the cva confi…
- [avatar.variants](./src/components/avatar/avatar.variants.md) — `avatarVariants` is the cva config that powers the `<Avatar>` chassis component (`<AvatarImage>` and `<AvatarFallback>`…
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - badge.variants — `badgeVariants` is the cva config……
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - badge.variants — `badgeVariants` is the cva config……
- [badge.variants](./src/components/badge/badge.variants.md) — `badgeVariants` is the cva config that powers the `<Badge>` chassis component. It exposes a `variant` axis with four ru…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb.variants](./src/components/breadcrumb/breadcrumb.variants.md) — `breadcrumbVariants`, `breadcrumbListVariants`, `breadcrumbItemVariants`, `breadcrumbLinkVariants`, `breadcrumbPageVari…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - button.variants — `buttonVariants` is the cva (clas…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./src/components/button/button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - calendar.test-fixtures — Wave-3h local test fixture…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - calendar.test-fixtures — Wave-3h local test fixture…
- [calendar.test-fixtures](./src/components/calendar/calendar.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.calendar.*` keyset. The chas…
- [calendar.variants](./src/components/calendar/calendar.variants.md) — `calendarVariants` is the cva config that powers the outer wrapper of `<Calendar>`. The recipe is intentionally minimal…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - card.variants — `cardVariants` (and its sub-compone…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - card.variants — `cardVariants` (and its sub-compone…
- [card.variants](./src/components/card/card.variants.md) — `cardVariants` (and its sub-component cva resolvers `cardHeaderVariants`, `cardTitleVariants`, `cardDescriptionVariants…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox.variants](./src/components/checkbox/checkbox.variants.md) — `checkboxVariants` is the cva (class-variance-authority) configuration that powers every `<Checkbox>` render. It is spl…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible.variants](./src/components/collapsible/collapsible.variants.md) — `collapsibleVariants`, `collapsibleTriggerVariants`, and `collapsibleContentVariants` are the cva (class-variance-autho…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog.test-fixtures](./src/components/confirm-delete-dialog/confirm-delete-dialog.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.confirmDelete.*` keyset. The…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - context-menu.variants — `contextMenuContentVariants…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - context-menu.variants — `contextMenuContentVariants…
- [context-menu.variants](./src/components/context-menu/context-menu.variants.md) — `contextMenuContentVariants`, `contextMenuItemVariants`, `contextMenuLabelVariants`, `contextMenuSeparatorVariants`, `c…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - data-table.variants — `dataTableVariants`, `dataTab…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./src/components/data-table/data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog.variants](./src/components/dialog/dialog.variants.md) — `dialogOverlayVariants`, `dialogContentVariants`, `dialogHeaderVariants`, `dialogFooterVariants`, `dialogTitleVariants`…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer.variants](./src/components/drawer/drawer.variants.md) — `drawerOverlayVariants`, `drawerContentVariants`, `drawerHandleVariants`, `drawerHeaderVariants`, `drawerFooterVariants…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu.variants](./src/components/dropdown-menu/dropdown-menu.variants.md) — `dropdownMenuContentVariants`, `dropdownMenuItemVariants`, `dropdownMenuLabelVariants`, `dropdownMenuSeparatorVariants`…
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - empty.variants — `emptyVariants` is the cva config……
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - empty.variants — `emptyVariants` is the cva config……
- [empty.variants](./src/components/empty/empty.variants.md) — `emptyVariants` is the cva config that powers the `<Empty>` chassis component (with `emptyHeaderVariants`, `emptyMediaV…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary.metrics](./src/components/error-boundary/error-boundary.metrics.md) — Co-located OTel counter for the chassis `<ErrorBoundary>` component. Lives next to the boundary itself rather than in t…
- [error-boundary.test-fixtures](./src/components/error-boundary/error-boundary.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.errorBoundary.*` keyset. The…
- [error-boundary.variants](./src/components/error-boundary/error-boundary.variants.md) — Cva slot definitions for the chassis `<ErrorBoundary>` default fallback. Splits the fallback into four named slots — ou…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - field.variants — `fieldVariants`, `fieldLabelVarian…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - field.variants — `fieldVariants`, `fieldLabelVarian…
- [field.variants](./src/components/field/field.variants.md) — `fieldVariants`, `fieldLabelVariants`, `fieldDescriptionVariants`, and `fieldErrorVariants` are the four cva (class-var…
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - field-group.variants — `fieldGroupVariants` is the……
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - field-group.variants — `fieldGroupVariants` is the……
- [field-group.variants](./src/components/field-group/field-group.variants.md) — `fieldGroupVariants` is the cva (class-variance-authority) configuration powering every `<FieldGroup>` render. It is sp…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set.variants](./src/components/field-set/field-set.variants.md) — `fieldSetVariants` and `fieldLegendVariants` are the cva (class-variance-authority) configurations powering `<FieldSet>…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - hover-card.variants — `hoverCardContentVariants` is…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - hover-card.variants — `hoverCardContentVariants` is…
- [hover-card.variants](./src/components/hover-card/hover-card.variants.md) — `hoverCardContentVariants` is the cva configuration powering the `<HoverCardContent>` floating panel. It is split from…
- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- input-number — <!-- begin:auto-docs --> _(no submodu…
- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- input-number — <!-- begin:auto-docs --> _(no submodu…
- [input.variants](./src/components/input/input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - input-group.variants — `inputGroupVariants`, `input…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - input-group.variants — `inputGroupVariants`, `input…
- [input-group.variants](./src/components/input-group/input-group.variants.md) — `inputGroupVariants`, `inputGroupAddonVariants`, `inputGroupInputVariants`, `inputGroupTextareaVariants`, and `inputGro…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - label.variants — `labelVariants` is the cva (class-…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - label.variants — `labelVariants` is the cva (class-…
- [label.variants](./src/components/label/label.variants.md) — `labelVariants` is the cva (class-variance-authority) configuration that powers every `<Label>` render. It is split fro…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar.variants](./src/components/menubar/menubar.variants.md) — `menubarVariants`, `menubarTriggerVariants`, `menubarContentVariants`, `menubarItemVariants`, `menubarLabelVariants`, `…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - pagination.variants — `paginationVariants`, `pagina…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./src/components/pagination/pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - popover.variants — `popoverContentVariants` is the……
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - popover.variants — `popoverContentVariants` is the……
- [popover.variants](./src/components/popover/popover.variants.md) — `popoverContentVariants` is the cva configuration powering the `<PopoverContent>` floating panel. It is split from `pop…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - progress.variants — `progressVariants` is the cva c…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - progress.variants — `progressVariants` is the cva c…
- [progress.variants](./src/components/progress/progress.variants.md) — `progressVariants` is the cva config that powers the `<Progress>` chassis component (paired with `progressIndicatorVari…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group.variants](./src/components/radio-group/radio-group.variants.md) — `radioGroupVariants` and `radioGroupItemVariants` are the cva (class-variance-authority) configurations powering `<Radi…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - resizable.variants — `resizablePanelGroupVariants`,…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - resizable.variants — `resizablePanelGroupVariants`,…
- [resizable.variants](./src/components/resizable/resizable.variants.md) — `resizablePanelGroupVariants`, `resizablePanelVariants`, `resizableHandleVariants`, and `resizableHandleGripVariants` a…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area.variants](./src/components/scroll-area/scroll-area.variants.md) — `scrollAreaVariants`, `scrollAreaViewportVariants`, `scrollAreaScrollbarVariants`, `scrollAreaThumbVariants`, and `scro…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - select.test-fixtures — Wave-3h local test fixture t…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - select.test-fixtures — Wave-3h local test fixture t…
- [select.test-fixtures](./src/components/select/select.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.select.placeholder` key. The…
- [select.variants](./src/components/select/select.variants.md) — The `<Select>` component is composed of six visual slots — trigger, content panel, item row, group label, separator, an…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - separator.variants — `separatorVariants` is the cva…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - separator.variants — `separatorVariants` is the cva…
- [separator.variants](./src/components/separator/separator.variants.md) — `separatorVariants` is the cva (class-variance-authority) configuration that powers every `<Separator>` render. It is s…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet.variants](./src/components/sheet/sheet.variants.md) — `sheetOverlayVariants`, `sheetContentVariants`, `sheetHeaderVariants`, `sheetFooterVariants`, `sheetTitleVariants`, `sh…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton.variants](./src/components/skeleton/skeleton.variants.md) — `skeletonVariants` is the cva (class-variance-authority) configuration that powers every `<Skeleton>` render. It is spl…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - sonner.variants — Defines the `sonnerToasterVariant…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - sonner.variants — Defines the `sonnerToasterVariant…
- [sonner.variants](./src/components/sonner/sonner.variants.md) — Defines the `sonnerToasterVariants` cva slot for the chassis `<Sonner>` toaster host. Sonner manages its own stacking,…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - spinner.variants — `spinnerVariants` is the cva con…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - spinner.variants — `spinnerVariants` is the cva con…
- [spinner.variants](./src/components/spinner/spinner.variants.md) — `spinnerVariants` is the cva config that powers the `<Spinner>` chassis component. It exposes a `size` axis (`sm` / `de…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - status-badge.variants — `statusBadgeVariants` is th…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - status-badge.variants — `statusBadgeVariants` is th…
- [status-badge.variants](./src/components/status-badge/status-badge.variants.md) — `statusBadgeVariants` is the cva config that powers the `<StatusBadge>` chassis component. It exposes a `status` axis w…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - switch.variants — `switchVariants` and `switchThumb…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - switch.variants — `switchVariants` and `switchThumb…
- [switch.variants](./src/components/switch/switch.variants.md) — `switchVariants` and `switchThumbVariants` are the two cva (class-variance-authority) configurations that together powe…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - table.variants — `tableVariants` and its eight sub-…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - table.variants — `tableVariants` and its eight sub-…
- [table.variants](./src/components/table/table.variants.md) — `tableVariants` and its eight sub-component cva resolvers (`tableHeaderVariants`, `tableBodyVariants`, `tableFooterVari…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs.variants](./src/components/tabs/tabs.variants.md) — `tabsVariants`, `tabsListVariants`, `tabsTriggerVariants`, and `tabsContentVariants` are the cva (class-variance-author…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - textarea.variants — `textareaVariants` is the cva (…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - textarea.variants — `textareaVariants` is the cva (…
- [textarea.variants](./src/components/textarea/textarea.variants.md) — `textareaVariants` is the cva (class-variance-authority) configuration that powers every `<Textarea>` render. It is spl…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle.variants](./src/components/theme-toggle/theme-toggle.variants.md) — Cva slot for the chassis `<ThemeToggle>` icon-button. Mirrors the shape of `<Button variant="outline" size="icon">` so…
- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - toggle.variants — `toggleVariants` is the cva (clas…
- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./src/components/toggle/toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group.variants](./src/components/toggle-group/toggle-group.variants.md) — `toggleGroupVariants` and `toggleGroupItemVariants` are the cva (class-variance-authority) configurations powering `<To…
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - tooltip.variants — `tooltipContentVariants` is the……
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - tooltip.variants — `tooltipContentVariants` is the……
- [tooltip.variants](./src/components/tooltip/tooltip.variants.md) — `tooltipContentVariants` is the cva configuration powering the `<TooltipContent>` floating bubble. It is split from `to…
- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/errors/error-codes.md) — Defines every stable error code emitted by the `@suaveplan/react-ui` package. Codes are grouped under the `REACT_UI` na…
- [react-ui-error](./src/errors/react-ui-error.md) — Owns every error thrown by the `@suaveplan/react-ui` package. Extends the project-wide `SuaveplanError` so each instan…
- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [augmentation](./src/locales/augmentation.md) — Side-effect-only TypeScript module that augments `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface with…
- [schema](./src/locales/schema.md) — Zod schema asserting that every locale bundle declares exactly the keys present in the canonical `en-GB.json` source-of…
- [translation-keys](./src/locales/translation-keys.md) — `translation-keys.ts` is the codegen-emitted string-literal union of every translation key contributed by `@suaveplan/r…
- [observability](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [react-ui-metrics](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pack](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- pack-registry-provider — <!-- begin:auto-docs --> _(…
- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pack-context](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- pack-registry-provider — <!-- begin:auto-docs --> _(…
- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [theme](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- theme-provider — <!-- begin:auto-docs --> _(no submo…
- [layer-stack](./src/theme/layer-stack/layer-stack.md) — The `layer-stack` module is the runtime engine that backs `<ThemeProvider>`, `useThemeMutator()`, and `useResolvedTheme…
- [layer-stack](./src/theme/layer-stack/layer-stack.md) — The `layer-stack` module is the runtime engine that backs `<ThemeProvider>`, `useThemeMutator()`, and `useResolvedTheme…
- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dark](./src/theme/themes/dark.md) — The dark counterpart of `lightTheme`. Same variant surface, same token names, lightness anchors inverted so foreground…
- [high-contrast](./src/theme/themes/high-contrast.md) — Accessibility-first theme that meets WCAG AAA contrast for normal and large text alike. Pure-white foreground (`#FFFFFF…
- [light](./src/theme/themes/light.md) — The default `ThemePlugin` instance shipped with `@suaveplan/react-ui`. `lightTheme` matches the shadcn `nova` token pre…
- [use-resolved-theme-tokens](./src/theme/use-resolved-theme-tokens/use-resolved-theme-tokens.md) — `useResolvedThemeTokens()` is the read-side counterpart to `useThemeMutator()`. It returns the live merged token state…
- [use-resolved-theme-tokens](./src/theme/use-resolved-theme-tokens/use-resolved-theme-tokens.md) — `useResolvedThemeTokens()` is the read-side counterpart to `useThemeMutator()`. It returns the live merged token state…
- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-theme-mutator](./src/theme/use-theme-mutator/use-theme-mutator.md) — `useThemeMutator()` is the public surface a consumer reaches for when it wants to push CSS custom properties onto the a…
- [use-theme-mutator](./src/theme/use-theme-mutator/use-theme-mutator.md) — `useThemeMutator()` is the public surface a consumer reaches for when it wants to push CSS custom properties onto the a…
- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [theme-context](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- theme-provider — <!-- begin:auto-docs --> _(no submo…
- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [version](./src/version.md) — Declares the running version of `@suaveplan/react-ui` as the constant `REACT_UI_VERSION`. The version is hand-maintaine…
<!-- end:auto-docs --> - accordion.variants — `accordionVariants`, `accordio…
- [accordion](./src/components/accordion/accordion.md) — <!-- begin:auto-docs --> _(no submodules)_- accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./src/components/accordion/accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_- alert.variants — `alertVariants` is the cva config……
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_- alert.variants — `alertVariants` is the cva config……
- [alert.variants](./src/components/alert/alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./src/components/alert-dialog/alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./src/components/aspect-ratio/aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_- avatar.variants — `avatarVariants` is the cva confi…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_- avatar.variants — `avatarVariants` is the cva confi…
- [avatar.variants](./src/components/avatar/avatar.variants.md) — `avatarVariants` is the cva config that powers the `<Avatar>` chassis component (`<AvatarImage>` and `<AvatarFallback>`…
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_- badge.variants — `badgeVariants` is the cva config……
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_- badge.variants — `badgeVariants` is the cva config……
- [badge.variants](./src/components/badge/badge.variants.md) — `badgeVariants` is the cva config that powers the `<Badge>` chassis component. It exposes a `variant` axis with four ru…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_- breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_- breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb.variants](./src/components/breadcrumb/breadcrumb.variants.md) — `breadcrumbVariants`, `breadcrumbListVariants`, `breadcrumbItemVariants`, `breadcrumbLinkVariants`, `breadcrumbPageVari…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_- button.variants — `buttonVariants` is the cva (clas…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_- button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./src/components/button/button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- calendar.test-fixtures — Wave-3h local test fixture…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- calendar.test-fixtures — Wave-3h local test fixture…
- [calendar.test-fixtures](./src/components/calendar/calendar.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.calendar.*` keyset. The chas…
- [calendar.variants](./src/components/calendar/calendar.variants.md) — `calendarVariants` is the cva config that powers the outer wrapper of `<Calendar>`. The recipe is intentionally minimal…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_- card.variants — `cardVariants` (and its sub-compone…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_- card.variants — `cardVariants` (and its sub-compone…
- [card.variants](./src/components/card/card.variants.md) — `cardVariants` (and its sub-component cva resolvers `cardHeaderVariants`, `cardTitleVariants`, `cardDescriptionVariants…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_- checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_- checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox.variants](./src/components/checkbox/checkbox.variants.md) — `checkboxVariants` is the cva (class-variance-authority) configuration that powers every `<Checkbox>` render. It is spl…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_- collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_- collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible.variants](./src/components/collapsible/collapsible.variants.md) — `collapsibleVariants`, `collapsibleTriggerVariants`, and `collapsibleContentVariants` are the cva (class-variance-autho…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog.test-fixtures](./src/components/confirm-delete-dialog/confirm-delete-dialog.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.confirmDelete.*` keyset. The…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- context-menu.variants — `contextMenuContentVariants…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- context-menu.variants — `contextMenuContentVariants…
- [context-menu.variants](./src/components/context-menu/context-menu.variants.md) — `contextMenuContentVariants`, `contextMenuItemVariants`, `contextMenuLabelVariants`, `contextMenuSeparatorVariants`, `c…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./src/components/data-table/data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog.variants](./src/components/dialog/dialog.variants.md) — `dialogOverlayVariants`, `dialogContentVariants`, `dialogHeaderVariants`, `dialogFooterVariants`, `dialogTitleVariants`…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_- drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_- drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer.variants](./src/components/drawer/drawer.variants.md) — `drawerOverlayVariants`, `drawerContentVariants`, `drawerHandleVariants`, `drawerHeaderVariants`, `drawerFooterVariants…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu.variants](./src/components/dropdown-menu/dropdown-menu.variants.md) — `dropdownMenuContentVariants`, `dropdownMenuItemVariants`, `dropdownMenuLabelVariants`, `dropdownMenuSeparatorVariants`…
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_- empty.variants — `emptyVariants` is the cva config……
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_- empty.variants — `emptyVariants` is the cva config……
- [empty.variants](./src/components/empty/empty.variants.md) — `emptyVariants` is the cva config that powers the `<Empty>` chassis component (with `emptyHeaderVariants`, `emptyMediaV…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_- error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_- error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary.metrics](./src/components/error-boundary/error-boundary.metrics.md) — Co-located OTel counter for the chassis `<ErrorBoundary>` component. Lives next to the boundary itself rather than in t…
- [error-boundary.test-fixtures](./src/components/error-boundary/error-boundary.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.errorBoundary.*` keyset. The…
- [error-boundary.variants](./src/components/error-boundary/error-boundary.variants.md) — Cva slot definitions for the chassis `<ErrorBoundary>` default fallback. Splits the fallback into four named slots — ou…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_- field.variants — `fieldVariants`, `fieldLabelVarian…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_- field.variants — `fieldVariants`, `fieldLabelVarian…
- [field.variants](./src/components/field/field.variants.md) — `fieldVariants`, `fieldLabelVariants`, `fieldDescriptionVariants`, and `fieldErrorVariants` are the four cva (class-var…
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_- field-group.variants — `fieldGroupVariants` is the……
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_- field-group.variants — `fieldGroupVariants` is the……
- [field-group.variants](./src/components/field-group/field-group.variants.md) — `fieldGroupVariants` is the cva (class-variance-authority) configuration powering every `<FieldGroup>` render. It is sp…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_- field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_- field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set.variants](./src/components/field-set/field-set.variants.md) — `fieldSetVariants` and `fieldLegendVariants` are the cva (class-variance-authority) configurations powering `<FieldSet>…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_- hover-card.variants — `hoverCardContentVariants` is…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_- hover-card.variants — `hoverCardContentVariants` is…
- [hover-card.variants](./src/components/hover-card/hover-card.variants.md) — `hoverCardContentVariants` is the cva configuration powering the `<HoverCardContent>` floating panel. It is split from…
- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_- [input.variants](./src/components/input/input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_- input-group.variants — `inputGroupVariants`, `input…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_- input-group.variants — `inputGroupVariants`, `input…
- [input-group.variants](./src/components/input-group/input-group.variants.md) — `inputGroupVariants`, `inputGroupAddonVariants`, `inputGroupInputVariants`, `inputGroupTextareaVariants`, and `inputGro…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_- label.variants — `labelVariants` is the cva (class-…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_- label.variants — `labelVariants` is the cva (class-…
- [label.variants](./src/components/label/label.variants.md) — `labelVariants` is the cva (class-variance-authority) configuration that powers every `<Label>` render. It is split fro…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_- menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_- menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar.variants](./src/components/menubar/menubar.variants.md) — `menubarVariants`, `menubarTriggerVariants`, `menubarContentVariants`, `menubarItemVariants`, `menubarLabelVariants`, `…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_- pagination.variants — `paginationVariants`, `pagina…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_- pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./src/components/pagination/pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_- popover.variants — `popoverContentVariants` is the……
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_- popover.variants — `popoverContentVariants` is the……
- [popover.variants](./src/components/popover/popover.variants.md) — `popoverContentVariants` is the cva configuration powering the `<PopoverContent>` floating panel. It is split from `pop…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_- progress.variants — `progressVariants` is the cva c…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_- progress.variants — `progressVariants` is the cva c…
- [progress.variants](./src/components/progress/progress.variants.md) — `progressVariants` is the cva config that powers the `<Progress>` chassis component (paired with `progressIndicatorVari…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_- radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_- radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group.variants](./src/components/radio-group/radio-group.variants.md) — `radioGroupVariants` and `radioGroupItemVariants` are the cva (class-variance-authority) configurations powering `<Radi…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_- resizable.variants — `resizablePanelGroupVariants`,…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_- resizable.variants — `resizablePanelGroupVariants`,…
- [resizable.variants](./src/components/resizable/resizable.variants.md) — `resizablePanelGroupVariants`, `resizablePanelVariants`, `resizableHandleVariants`, and `resizableHandleGripVariants` a…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_- scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_- scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area.variants](./src/components/scroll-area/scroll-area.variants.md) — `scrollAreaVariants`, `scrollAreaViewportVariants`, `scrollAreaScrollbarVariants`, `scrollAreaThumbVariants`, and `scro…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_- select.test-fixtures — Wave-3h local test fixture t…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_- select.test-fixtures — Wave-3h local test fixture t…
- [select.test-fixtures](./src/components/select/select.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.select.placeholder` key. The…
- [select.variants](./src/components/select/select.variants.md) — The `<Select>` component is composed of six visual slots — trigger, content panel, item row, group label, separator, an…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_- separator.variants — `separatorVariants` is the cva…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_- separator.variants — `separatorVariants` is the cva…
- [separator.variants](./src/components/separator/separator.variants.md) — `separatorVariants` is the cva (class-variance-authority) configuration that powers every `<Separator>` render. It is s…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_- sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_- sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet.variants](./src/components/sheet/sheet.variants.md) — `sheetOverlayVariants`, `sheetContentVariants`, `sheetHeaderVariants`, `sheetFooterVariants`, `sheetTitleVariants`, `sh…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_- skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_- skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton.variants](./src/components/skeleton/skeleton.variants.md) — `skeletonVariants` is the cva (class-variance-authority) configuration that powers every `<Skeleton>` render. It is spl…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_- sonner.variants — Defines the `sonnerToasterVariant…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_- sonner.variants — Defines the `sonnerToasterVariant…
- [sonner.variants](./src/components/sonner/sonner.variants.md) — Defines the `sonnerToasterVariants` cva slot for the chassis `<Sonner>` toaster host. Sonner manages its own stacking,…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_- spinner.variants — `spinnerVariants` is the cva con…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_- spinner.variants — `spinnerVariants` is the cva con…
- [spinner.variants](./src/components/spinner/spinner.variants.md) — `spinnerVariants` is the cva config that powers the `<Spinner>` chassis component. It exposes a `size` axis (`sm` / `de…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- status-badge.variants — `statusBadgeVariants` is th…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- status-badge.variants — `statusBadgeVariants` is th…
- [status-badge.variants](./src/components/status-badge/status-badge.variants.md) — `statusBadgeVariants` is the cva config that powers the `<StatusBadge>` chassis component. It exposes a `status` axis w…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_- switch.variants — `switchVariants` and `switchThumb…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_- switch.variants — `switchVariants` and `switchThumb…
- [switch.variants](./src/components/switch/switch.variants.md) — `switchVariants` and `switchThumbVariants` are the two cva (class-variance-authority) configurations that together powe…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_- table.variants — `tableVariants` and its eight sub-…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_- table.variants — `tableVariants` and its eight sub-…
- [table.variants](./src/components/table/table.variants.md) — `tableVariants` and its eight sub-component cva resolvers (`tableHeaderVariants`, `tableBodyVariants`, `tableFooterVari…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_- tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_- tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs.variants](./src/components/tabs/tabs.variants.md) — `tabsVariants`, `tabsListVariants`, `tabsTriggerVariants`, and `tabsContentVariants` are the cva (class-variance-author…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_- textarea.variants — `textareaVariants` is the cva (…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_- textarea.variants — `textareaVariants` is the cva (…
- [textarea.variants](./src/components/textarea/textarea.variants.md) — `textareaVariants` is the cva (class-variance-authority) configuration that powers every `<Textarea>` render. It is spl…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle.variants](./src/components/theme-toggle/theme-toggle.variants.md) — Cva slot for the chassis `<ThemeToggle>` icon-button. Mirrors the shape of `<Button variant="outline" size="icon">` so…
- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./src/components/toggle/toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group.variants](./src/components/toggle-group/toggle-group.variants.md) — `toggleGroupVariants` and `toggleGroupItemVariants` are the cva (class-variance-authority) configurations powering `<To…
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_- tooltip.variants — `tooltipContentVariants` is the……
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_- tooltip.variants — `tooltipContentVariants` is the……
- [tooltip.variants](./src/components/tooltip/tooltip.variants.md) — `tooltipContentVariants` is the cva configuration powering the `<TooltipContent>` floating bubble. It is split from `to…
- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — Defines every stable error code emitted by the `@suaveplan/react-ui` package. Codes are grouped under the `REACT_UI` na…
- [react-ui-error](./src/errors/react-ui-error.md) — Owns every error thrown by the `@suaveplan/react-ui` package. Extends the project-wide `SuaveplanError` so each instan…
- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [augmentation](./src/locales/augmentation.md) — Side-effect-only TypeScript module that augments `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface with…
- [schema](./src/locales/schema.md) — Zod schema asserting that every locale bundle declares exactly the keys present in the canonical `en-GB.json` source-of…
- [translation-keys](./src/locales/translation-keys.md) — `translation-keys.ts` is the codegen-emitted string-literal union of every translation key contributed by `@suaveplan/r…
- [observability](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-metrics](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [dark](./src/theme/themes/dark.md) — The dark counterpart of `lightTheme`. Same variant surface, same token names, lightness anchors inverted so foreground…
- [high-contrast](./src/theme/themes/high-contrast.md) — Accessibility-first theme that meets WCAG AAA contrast for normal and large text alike. Pure-white foreground (`#FFFFFF…
- [light](./src/theme/themes/light.md) — The default `ThemePlugin` instance shipped with `@suaveplan/react-ui`. `lightTheme` matches the shadcn `nova` token pre…
- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-context](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/version.md) — Declares the running version of `@suaveplan/react-ui` as the constant `REACT_UI_VERSION`. The version is hand-maintaine…
- accordion.variants — `accordionVariants`, `accordio…
- [accordion](./src/components/accordion/accordion.md) — <!-- begin:auto-docs --> _(no submodules)_- accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./src/components/accordion/accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_- alert.variants — `alertVariants` is the cva config……
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_- alert.variants — `alertVariants` is the cva config……
- [alert.variants](./src/components/alert/alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./src/components/alert-dialog/alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./src/components/aspect-ratio/aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_- avatar.variants — `avatarVariants` is the cva confi…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_- avatar.variants — `avatarVariants` is the cva confi…
- [avatar.variants](./src/components/avatar/avatar.variants.md) — `avatarVariants` is the cva config that powers the `<Avatar>` chassis component (`<AvatarImage>` and `<AvatarFallback>`…
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_- badge.variants — `badgeVariants` is the cva config……
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_- badge.variants — `badgeVariants` is the cva config……
- [badge.variants](./src/components/badge/badge.variants.md) — `badgeVariants` is the cva config that powers the `<Badge>` chassis component. It exposes a `variant` axis with four ru…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_- breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_- breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb.variants](./src/components/breadcrumb/breadcrumb.variants.md) — `breadcrumbVariants`, `breadcrumbListVariants`, `breadcrumbItemVariants`, `breadcrumbLinkVariants`, `breadcrumbPageVari…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_- button.variants — `buttonVariants` is the cva (clas…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_- button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./src/components/button/button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- calendar.test-fixtures — Wave-3h local test fixture…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- calendar.test-fixtures — Wave-3h local test fixture…
- [calendar.test-fixtures](./src/components/calendar/calendar.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.calendar.*` keyset. The chas…
- [calendar.variants](./src/components/calendar/calendar.variants.md) — `calendarVariants` is the cva config that powers the outer wrapper of `<Calendar>`. The recipe is intentionally minimal…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_- card.variants — `cardVariants` (and its sub-compone…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_- card.variants — `cardVariants` (and its sub-compone…
- [card.variants](./src/components/card/card.variants.md) — `cardVariants` (and its sub-component cva resolvers `cardHeaderVariants`, `cardTitleVariants`, `cardDescriptionVariants…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_- checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_- checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox.variants](./src/components/checkbox/checkbox.variants.md) — `checkboxVariants` is the cva (class-variance-authority) configuration that powers every `<Checkbox>` render. It is spl…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_- collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_- collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible.variants](./src/components/collapsible/collapsible.variants.md) — `collapsibleVariants`, `collapsibleTriggerVariants`, and `collapsibleContentVariants` are the cva (class-variance-autho…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog.test-fixtures](./src/components/confirm-delete-dialog/confirm-delete-dialog.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.confirmDelete.*` keyset. The…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- context-menu.variants — `contextMenuContentVariants…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- context-menu.variants — `contextMenuContentVariants…
- [context-menu.variants](./src/components/context-menu/context-menu.variants.md) — `contextMenuContentVariants`, `contextMenuItemVariants`, `contextMenuLabelVariants`, `contextMenuSeparatorVariants`, `c…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./src/components/data-table/data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog.variants](./src/components/dialog/dialog.variants.md) — `dialogOverlayVariants`, `dialogContentVariants`, `dialogHeaderVariants`, `dialogFooterVariants`, `dialogTitleVariants`…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_- drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_- drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer.variants](./src/components/drawer/drawer.variants.md) — `drawerOverlayVariants`, `drawerContentVariants`, `drawerHandleVariants`, `drawerHeaderVariants`, `drawerFooterVariants…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu.variants](./src/components/dropdown-menu/dropdown-menu.variants.md) — `dropdownMenuContentVariants`, `dropdownMenuItemVariants`, `dropdownMenuLabelVariants`, `dropdownMenuSeparatorVariants`…
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_- empty.variants — `emptyVariants` is the cva config……
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_- empty.variants — `emptyVariants` is the cva config……
- [empty.variants](./src/components/empty/empty.variants.md) — `emptyVariants` is the cva config that powers the `<Empty>` chassis component (with `emptyHeaderVariants`, `emptyMediaV…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_- error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_- error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary.metrics](./src/components/error-boundary/error-boundary.metrics.md) — Co-located OTel counter for the chassis `<ErrorBoundary>` component. Lives next to the boundary itself rather than in t…
- [error-boundary.test-fixtures](./src/components/error-boundary/error-boundary.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.errorBoundary.*` keyset. The…
- [error-boundary.variants](./src/components/error-boundary/error-boundary.variants.md) — Cva slot definitions for the chassis `<ErrorBoundary>` default fallback. Splits the fallback into four named slots — ou…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_- field.variants — `fieldVariants`, `fieldLabelVarian…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_- field.variants — `fieldVariants`, `fieldLabelVarian…
- [field.variants](./src/components/field/field.variants.md) — `fieldVariants`, `fieldLabelVariants`, `fieldDescriptionVariants`, and `fieldErrorVariants` are the four cva (class-var…
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_- field-group.variants — `fieldGroupVariants` is the……
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_- field-group.variants — `fieldGroupVariants` is the……
- [field-group.variants](./src/components/field-group/field-group.variants.md) — `fieldGroupVariants` is the cva (class-variance-authority) configuration powering every `<FieldGroup>` render. It is sp…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_- field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_- field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set.variants](./src/components/field-set/field-set.variants.md) — `fieldSetVariants` and `fieldLegendVariants` are the cva (class-variance-authority) configurations powering `<FieldSet>…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_- hover-card.variants — `hoverCardContentVariants` is…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_- hover-card.variants — `hoverCardContentVariants` is…
- [hover-card.variants](./src/components/hover-card/hover-card.variants.md) — `hoverCardContentVariants` is the cva configuration powering the `<HoverCardContent>` floating panel. It is split from…
- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:au…
- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:au…
- [input.variants](./src/components/input/input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_- input-group.variants — `inputGroupVariants`, `input…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_- input-group.variants — `inputGroupVariants`, `input…
- [input-group.variants](./src/components/input-group/input-group.variants.md) — `inputGroupVariants`, `inputGroupAddonVariants`, `inputGroupInputVariants`, `inputGroupTextareaVariants`, and `inputGro…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_- label.variants — `labelVariants` is the cva (class-…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_- label.variants — `labelVariants` is the cva (class-…
- [label.variants](./src/components/label/label.variants.md) — `labelVariants` is the cva (class-variance-authority) configuration that powers every `<Label>` render. It is split fro…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_- menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_- menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar.variants](./src/components/menubar/menubar.variants.md) — `menubarVariants`, `menubarTriggerVariants`, `menubarContentVariants`, `menubarItemVariants`, `menubarLabelVariants`, `…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_- pagination.variants — `paginationVariants`, `pagina…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_- pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./src/components/pagination/pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_- popover.variants — `popoverContentVariants` is the……
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_- popover.variants — `popoverContentVariants` is the……
- [popover.variants](./src/components/popover/popover.variants.md) — `popoverContentVariants` is the cva configuration powering the `<PopoverContent>` floating panel. It is split from `pop…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_- progress.variants — `progressVariants` is the cva c…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_- progress.variants — `progressVariants` is the cva c…
- [progress.variants](./src/components/progress/progress.variants.md) — `progressVariants` is the cva config that powers the `<Progress>` chassis component (paired with `progressIndicatorVari…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_- radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_- radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group.variants](./src/components/radio-group/radio-group.variants.md) — `radioGroupVariants` and `radioGroupItemVariants` are the cva (class-variance-authority) configurations powering `<Radi…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_- resizable.variants — `resizablePanelGroupVariants`,…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_- resizable.variants — `resizablePanelGroupVariants`,…
- [resizable.variants](./src/components/resizable/resizable.variants.md) — `resizablePanelGroupVariants`, `resizablePanelVariants`, `resizableHandleVariants`, and `resizableHandleGripVariants` a…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_- scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_- scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area.variants](./src/components/scroll-area/scroll-area.variants.md) — `scrollAreaVariants`, `scrollAreaViewportVariants`, `scrollAreaScrollbarVariants`, `scrollAreaThumbVariants`, and `scro…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_- select.test-fixtures — Wave-3h local test fixture t…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_- select.test-fixtures — Wave-3h local test fixture t…
- [select.test-fixtures](./src/components/select/select.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.select.placeholder` key. The…
- [select.variants](./src/components/select/select.variants.md) — The `<Select>` component is composed of six visual slots — trigger, content panel, item row, group label, separator, an…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_- separator.variants — `separatorVariants` is the cva…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_- separator.variants — `separatorVariants` is the cva…
- [separator.variants](./src/components/separator/separator.variants.md) — `separatorVariants` is the cva (class-variance-authority) configuration that powers every `<Separator>` render. It is s…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_- sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_- sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet.variants](./src/components/sheet/sheet.variants.md) — `sheetOverlayVariants`, `sheetContentVariants`, `sheetHeaderVariants`, `sheetFooterVariants`, `sheetTitleVariants`, `sh…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_- skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_- skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton.variants](./src/components/skeleton/skeleton.variants.md) — `skeletonVariants` is the cva (class-variance-authority) configuration that powers every `<Skeleton>` render. It is spl…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_- sonner.variants — Defines the `sonnerToasterVariant…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_- sonner.variants — Defines the `sonnerToasterVariant…
- [sonner.variants](./src/components/sonner/sonner.variants.md) — Defines the `sonnerToasterVariants` cva slot for the chassis `<Sonner>` toaster host. Sonner manages its own stacking,…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_- spinner.variants — `spinnerVariants` is the cva con…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_- spinner.variants — `spinnerVariants` is the cva con…
- [spinner.variants](./src/components/spinner/spinner.variants.md) — `spinnerVariants` is the cva config that powers the `<Spinner>` chassis component. It exposes a `size` axis (`sm` / `de…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- status-badge.variants — `statusBadgeVariants` is th…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- status-badge.variants — `statusBadgeVariants` is th…
- [status-badge.variants](./src/components/status-badge/status-badge.variants.md) — `statusBadgeVariants` is the cva config that powers the `<StatusBadge>` chassis component. It exposes a `status` axis w…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_- switch.variants — `switchVariants` and `switchThumb…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_- switch.variants — `switchVariants` and `switchThumb…
- [switch.variants](./src/components/switch/switch.variants.md) — `switchVariants` and `switchThumbVariants` are the two cva (class-variance-authority) configurations that together powe…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_- table.variants — `tableVariants` and its eight sub-…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_- table.variants — `tableVariants` and its eight sub-…
- [table.variants](./src/components/table/table.variants.md) — `tableVariants` and its eight sub-component cva resolvers (`tableHeaderVariants`, `tableBodyVariants`, `tableFooterVari…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_- tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_- tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs.variants](./src/components/tabs/tabs.variants.md) — `tabsVariants`, `tabsListVariants`, `tabsTriggerVariants`, and `tabsContentVariants` are the cva (class-variance-author…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_- textarea.variants — `textareaVariants` is the cva (…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_- textarea.variants — `textareaVariants` is the cva (…
- [textarea.variants](./src/components/textarea/textarea.variants.md) — `textareaVariants` is the cva (class-variance-authority) configuration that powers every `<Textarea>` render. It is spl…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle.variants](./src/components/theme-toggle/theme-toggle.variants.md) — Cva slot for the chassis `<ThemeToggle>` icon-button. Mirrors the shape of `<Button variant="outline" size="icon">` so…
- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./src/components/toggle/toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group.variants](./src/components/toggle-group/toggle-group.variants.md) — `toggleGroupVariants` and `toggleGroupItemVariants` are the cva (class-variance-authority) configurations powering `<To…
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_- tooltip.variants — `tooltipContentVariants` is the……
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_- tooltip.variants — `tooltipContentVariants` is the……
- [tooltip.variants](./src/components/tooltip/tooltip.variants.md) — `tooltipContentVariants` is the cva configuration powering the `<TooltipContent>` floating bubble. It is split from `to…
- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — Defines every stable error code emitted by the `@suaveplan/react-ui` package. Codes are grouped under the `REACT_UI` na…
- [react-ui-error](./src/errors/react-ui-error.md) — Owns every error thrown by the `@suaveplan/react-ui` package. Extends the project-wide `SuaveplanError` so each instan…
- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [augmentation](./src/locales/augmentation.md) — Side-effect-only TypeScript module that augments `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface with…
- [schema](./src/locales/schema.md) — Zod schema asserting that every locale bundle declares exactly the keys present in the canonical `en-GB.json` source-of…
- [translation-keys](./src/locales/translation-keys.md) — `translation-keys.ts` is the codegen-emitted string-literal union of every translation key contributed by `@suaveplan/r…
- [observability](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-metrics](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-…
- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-…
- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-provider — <!-- begin:…
- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [dark](./src/theme/themes/dark.md) — The dark counterpart of `lightTheme`. Same variant surface, same token names, lightness anchors inverted so foreground…
- [high-contrast](./src/theme/themes/high-contrast.md) — Accessibility-first theme that meets WCAG AAA contrast for normal and large text alike. Pure-white foreground (`#FFFFFF…
- [light](./src/theme/themes/light.md) — The default `ThemePlugin` instance shipped with `@suaveplan/react-ui`. `lightTheme` matches the shadcn `nova` token pre…
- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-context](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-provider — <!-- begin:…
- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/version.md) — Declares the running version of `@suaveplan/react-ui` as the constant `REACT_UI_VERSION`. The version is hand-maintaine…
- accordion.variants — `accordionVariants`, `accordio…
- [accordion](./src/components/accordion/accordion.md) — <!-- begin:auto-docs --> _(no submodules)_- accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./src/components/accordion/accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_- alert.variants — `alertVariants` is the cva config…
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_- alert.variants — `alertVariants` is the cva config…
- [alert.variants](./src/components/alert/alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./src/components/alert-dialog/alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./src/components/aspect-ratio/aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_- avatar.variants — `avatarVariants` is the cva confi…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_- avatar.variants — `avatarVariants` is the cva confi…
- [avatar.variants](./src/components/avatar/avatar.variants.md) — `avatarVariants` is the cva config that powers the `<Avatar>` chassis component (`<AvatarImage>` and `<AvatarFallback>`…
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_- badge.variants — `badgeVariants` is the cva config…
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_- badge.variants — `badgeVariants` is the cva config…
- [badge.variants](./src/components/badge/badge.variants.md) — `badgeVariants` is the cva config that powers the `<Badge>` chassis component. It exposes a `variant` axis with four ru…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_- breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_- breadcrumb.variants — `breadcrumbVariants`, `breadc…
- [breadcrumb.variants](./src/components/breadcrumb/breadcrumb.variants.md) — `breadcrumbVariants`, `breadcrumbListVariants`, `breadcrumbItemVariants`, `breadcrumbLinkVariants`, `breadcrumbPageVari…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_- button.variants — `buttonVariants` is the cva (clas…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_- button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./src/components/button/button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- calendar.test-fixtures — Wave-3h local test fixture…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- calendar.test-fixtures — Wave-3h local test fixture…
- [calendar.test-fixtures](./src/components/calendar/calendar.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.calendar.*` keyset. The chas…
- [calendar.variants](./src/components/calendar/calendar.variants.md) — `calendarVariants` is the cva config that powers the outer wrapper of `<Calendar>`. The recipe is intentionally minimal…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_- card.variants — `cardVariants` (and its sub-compone…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_- card.variants — `cardVariants` (and its sub-compone…
- [card.variants](./src/components/card/card.variants.md) — `cardVariants` (and its sub-component cva resolvers `cardHeaderVariants`, `cardTitleVariants`, `cardDescriptionVariants…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_- checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_- checkbox.variants — `checkboxVariants` is the cva (…
- [checkbox.variants](./src/components/checkbox/checkbox.variants.md) — `checkboxVariants` is the cva (class-variance-authority) configuration that powers every `<Checkbox>` render. It is spl…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_- collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_- collapsible.variants — `collapsibleVariants`, `coll…
- [collapsible.variants](./src/components/collapsible/collapsible.variants.md) — `collapsibleVariants`, `collapsibleTriggerVariants`, and `collapsibleContentVariants` are the cva (class-variance-autho…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- confirm-delete-dialog.test-fixtures — Wave-3g local…
- [confirm-delete-dialog.test-fixtures](./src/components/confirm-delete-dialog/confirm-delete-dialog.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.confirmDelete.*` keyset. The…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- context-menu.variants — `contextMenuContentVariants…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- context-menu.variants — `contextMenuContentVariants…
- [context-menu.variants](./src/components/context-menu/context-menu.variants.md) — `contextMenuContentVariants`, `contextMenuItemVariants`, `contextMenuLabelVariants`, `contextMenuSeparatorVariants`, `c…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./src/components/data-table/data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- dialog.variants — `dialogOverlayVariants`, `dialogC…
- [dialog.variants](./src/components/dialog/dialog.variants.md) — `dialogOverlayVariants`, `dialogContentVariants`, `dialogHeaderVariants`, `dialogFooterVariants`, `dialogTitleVariants`…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_- drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_- drawer.variants — `drawerOverlayVariants`, `drawerC…
- [drawer.variants](./src/components/drawer/drawer.variants.md) — `drawerOverlayVariants`, `drawerContentVariants`, `drawerHandleVariants`, `drawerHeaderVariants`, `drawerFooterVariants…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_- dropdown-menu.variants — `dropdownMenuContentVarian…
- [dropdown-menu.variants](./src/components/dropdown-menu/dropdown-menu.variants.md) — `dropdownMenuContentVariants`, `dropdownMenuItemVariants`, `dropdownMenuLabelVariants`, `dropdownMenuSeparatorVariants`…
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_- empty.variants — `emptyVariants` is the cva config…
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_- empty.variants — `emptyVariants` is the cva config…
- [empty.variants](./src/components/empty/empty.variants.md) — `emptyVariants` is the cva config that powers the `<Empty>` chassis component (with `emptyHeaderVariants`, `emptyMediaV…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_- error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_- error-boundary.metrics — Co-located OTel counter fo…
- [error-boundary.metrics](./src/components/error-boundary/error-boundary.metrics.md) — Co-located OTel counter for the chassis `<ErrorBoundary>` component. Lives next to the boundary itself rather than in t…
- [error-boundary.test-fixtures](./src/components/error-boundary/error-boundary.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.errorBoundary.*` keyset. The…
- [error-boundary.variants](./src/components/error-boundary/error-boundary.variants.md) — Cva slot definitions for the chassis `<ErrorBoundary>` default fallback. Splits the fallback into four named slots — ou…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_- field.variants — `fieldVariants`, `fieldLabelVarian…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_- field.variants — `fieldVariants`, `fieldLabelVarian…
- [field.variants](./src/components/field/field.variants.md) — `fieldVariants`, `fieldLabelVariants`, `fieldDescriptionVariants`, and `fieldErrorVariants` are the four cva (class-var…
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_- field-group.variants — `fieldGroupVariants` is the…
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_- field-group.variants — `fieldGroupVariants` is the…
- [field-group.variants](./src/components/field-group/field-group.variants.md) — `fieldGroupVariants` is the cva (class-variance-authority) configuration powering every `<FieldGroup>` render. It is sp…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_- field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_- field-set.variants — `fieldSetVariants` and `fieldL…
- [field-set.variants](./src/components/field-set/field-set.variants.md) — `fieldSetVariants` and `fieldLegendVariants` are the cva (class-variance-authority) configurations powering `<FieldSet>…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_- hover-card.variants — `hoverCardContentVariants` is…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_- hover-card.variants — `hoverCardContentVariants` is…
- [hover-card.variants](./src/components/hover-card/hover-card.variants.md) — `hoverCardContentVariants` is the cva configuration powering the `<HoverCardContent>` floating panel. It is split from…
- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:auto-docs --> _(no submod…
- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_- input-number — <!-- begin:auto-docs --> _(no submod…
- [input.variants](./src/components/input/input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_- input-group.variants — `inputGroupVariants`, `input…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_- input-group.variants — `inputGroupVariants`, `input…
- [input-group.variants](./src/components/input-group/input-group.variants.md) — `inputGroupVariants`, `inputGroupAddonVariants`, `inputGroupInputVariants`, `inputGroupTextareaVariants`, and `inputGro…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_- label.variants — `labelVariants` is the cva (class-…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_- label.variants — `labelVariants` is the cva (class-…
- [label.variants](./src/components/label/label.variants.md) — `labelVariants` is the cva (class-variance-authority) configuration that powers every `<Label>` render. It is split fro…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_- menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_- menubar.variants — `menubarVariants`, `menubarTrigg…
- [menubar.variants](./src/components/menubar/menubar.variants.md) — `menubarVariants`, `menubarTriggerVariants`, `menubarContentVariants`, `menubarItemVariants`, `menubarLabelVariants`, `…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_- pagination.variants — `paginationVariants`, `pagina…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_- pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./src/components/pagination/pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_- popover.variants — `popoverContentVariants` is the…
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_- popover.variants — `popoverContentVariants` is the…
- [popover.variants](./src/components/popover/popover.variants.md) — `popoverContentVariants` is the cva configuration powering the `<PopoverContent>` floating panel. It is split from `pop…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_- progress.variants — `progressVariants` is the cva c…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_- progress.variants — `progressVariants` is the cva c…
- [progress.variants](./src/components/progress/progress.variants.md) — `progressVariants` is the cva config that powers the `<Progress>` chassis component (paired with `progressIndicatorVari…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_- radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_- radio-group.variants — `radioGroupVariants` and `ra…
- [radio-group.variants](./src/components/radio-group/radio-group.variants.md) — `radioGroupVariants` and `radioGroupItemVariants` are the cva (class-variance-authority) configurations powering `<Radi…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_- resizable.variants — `resizablePanelGroupVariants`,…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_- resizable.variants — `resizablePanelGroupVariants`,…
- [resizable.variants](./src/components/resizable/resizable.variants.md) — `resizablePanelGroupVariants`, `resizablePanelVariants`, `resizableHandleVariants`, and `resizableHandleGripVariants` a…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_- scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_- scroll-area.variants — `scrollAreaVariants`, `scrol…
- [scroll-area.variants](./src/components/scroll-area/scroll-area.variants.md) — `scrollAreaVariants`, `scrollAreaViewportVariants`, `scrollAreaScrollbarVariants`, `scrollAreaThumbVariants`, and `scro…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_- select.test-fixtures — Wave-3h local test fixture t…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_- select.test-fixtures — Wave-3h local test fixture t…
- [select.test-fixtures](./src/components/select/select.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.select.placeholder` key. The…
- [select.variants](./src/components/select/select.variants.md) — The `<Select>` component is composed of six visual slots — trigger, content panel, item row, group label, separator, an…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_- separator.variants — `separatorVariants` is the cva…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_- separator.variants — `separatorVariants` is the cva…
- [separator.variants](./src/components/separator/separator.variants.md) — `separatorVariants` is the cva (class-variance-authority) configuration that powers every `<Separator>` render. It is s…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_- sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_- sheet.variants — `sheetOverlayVariants`, `sheetCont…
- [sheet.variants](./src/components/sheet/sheet.variants.md) — `sheetOverlayVariants`, `sheetContentVariants`, `sheetHeaderVariants`, `sheetFooterVariants`, `sheetTitleVariants`, `sh…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_- skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_- skeleton.variants — `skeletonVariants` is the cva (…
- [skeleton.variants](./src/components/skeleton/skeleton.variants.md) — `skeletonVariants` is the cva (class-variance-authority) configuration that powers every `<Skeleton>` render. It is spl…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_- sonner.variants — Defines the `sonnerToasterVariant…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_- sonner.variants — Defines the `sonnerToasterVariant…
- [sonner.variants](./src/components/sonner/sonner.variants.md) — Defines the `sonnerToasterVariants` cva slot for the chassis `<Sonner>` toaster host. Sonner manages its own stacking,…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_- spinner.variants — `spinnerVariants` is the cva con…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_- spinner.variants — `spinnerVariants` is the cva con…
- [spinner.variants](./src/components/spinner/spinner.variants.md) — `spinnerVariants` is the cva config that powers the `<Spinner>` chassis component. It exposes a `size` axis (`sm` / `de…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- status-badge.variants — `statusBadgeVariants` is th…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- status-badge.variants — `statusBadgeVariants` is th…
- [status-badge.variants](./src/components/status-badge/status-badge.variants.md) — `statusBadgeVariants` is the cva config that powers the `<StatusBadge>` chassis component. It exposes a `status` axis w…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_- switch.variants — `switchVariants` and `switchThumb…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_- switch.variants — `switchVariants` and `switchThumb…
- [switch.variants](./src/components/switch/switch.variants.md) — `switchVariants` and `switchThumbVariants` are the two cva (class-variance-authority) configurations that together powe…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_- table.variants — `tableVariants` and its eight sub-…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_- table.variants — `tableVariants` and its eight sub-…
- [table.variants](./src/components/table/table.variants.md) — `tableVariants` and its eight sub-component cva resolvers (`tableHeaderVariants`, `tableBodyVariants`, `tableFooterVari…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_- tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_- tabs.variants — `tabsVariants`, `tabsListVariants`,…
- [tabs.variants](./src/components/tabs/tabs.variants.md) — `tabsVariants`, `tabsListVariants`, `tabsTriggerVariants`, and `tabsContentVariants` are the cva (class-variance-author…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_- textarea.variants — `textareaVariants` is the cva (…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_- textarea.variants — `textareaVariants` is the cva (…
- [textarea.variants](./src/components/textarea/textarea.variants.md) — `textareaVariants` is the cva (class-variance-authority) configuration that powers every `<Textarea>` render. It is spl…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-toggle.variants — Cva slot for the chassis `<…
- [theme-toggle.variants](./src/components/theme-toggle/theme-toggle.variants.md) — Cva slot for the chassis `<ThemeToggle>` icon-button. Mirrors the shape of `<Button variant="outline" size="icon">` so…
- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./src/components/toggle/toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_- toggle-group.variants — `toggleGroupVariants` and `…
- [toggle-group.variants](./src/components/toggle-group/toggle-group.variants.md) — `toggleGroupVariants` and `toggleGroupItemVariants` are the cva (class-variance-authority) configurations powering `<To…
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_- tooltip.variants — `tooltipContentVariants` is the…
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_- tooltip.variants — `tooltipContentVariants` is the…
- [tooltip.variants](./src/components/tooltip/tooltip.variants.md) — `tooltipContentVariants` is the cva configuration powering the `<TooltipContent>` floating bubble. It is split from `to…
- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — Defines every stable error code emitted by the `@suaveplan/react-ui` package. Codes are grouped under the `REACT_UI` na…
- [react-ui-error](./src/errors/react-ui-error.md) — Owns every error thrown by the `@suaveplan/react-ui` package. Extends the project-wide `SuaveplanError` so each instan…
- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [augmentation](./src/locales/augmentation.md) — Side-effect-only TypeScript module that augments `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface with…
- [schema](./src/locales/schema.md) — Zod schema asserting that every locale bundle declares exactly the keys present in the canonical `en-GB.json` source-of…
- [translation-keys](./src/locales/translation-keys.md) — `translation-keys.ts` is the codegen-emitted string-literal union of every translation key contributed by `@suaveplan/r…
- [observability](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-metrics](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-- begin:auto-docs --> _…
- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack-context](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- pack-registry-provider — <!-- begin:auto-docs --> _…
- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-provider — <!-- begin:auto-docs --> _(no subm…
- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [dark](./src/theme/themes/dark.md) — The dark counterpart of `lightTheme`. Same variant surface, same token names, lightness anchors inverted so foreground…
- [high-contrast](./src/theme/themes/high-contrast.md) — Accessibility-first theme that meets WCAG AAA contrast for normal and large text alike. Pure-white foreground (`#FFFFFF…
- [light](./src/theme/themes/light.md) — The default `ThemePlugin` instance shipped with `@suaveplan/react-ui`. `lightTheme` matches the shadcn `nova` token pre…
- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme-context](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- theme-provider — <!-- begin:auto-docs --> _(no subm…
- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/version.md) — Declares the running version of `@suaveplan/react-ui` as the constant `REACT_UI_VERSION`. The version is hand-maintaine…
- [accordion](./src/components/accordion/accordion.md) — <!-- begin:auto-docs --> _(no submodules)_- [accordion.variants](./src/components/accordion/accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_ - [alert](./src/components/alert/alert.md) — <!-- begin:auto-docs --> _(no submodules)_ - [alert.variants](./src/components/alert/alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [alert-dialog](./src/components/alert-dialog/alert-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [alert-dialog.variants](./src/components/alert-dialog/alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_ - [aspect-ratio](./src/components/aspect-ratio/aspect-ratio.md) — <!-- begin:auto-docs --> _(no submodules)_ - [aspect-ratio.variants](./src/components/aspect-ratio/aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [avatar](./src/components/avatar/avatar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [avatar.variants](./src/components/avatar/avatar.variants.md) — `avatarVariants` is the cva config that powers the `<Avatar>` chassis component (`<AvatarImage>` and `<AvatarFallback>`…
- [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [badge](./src/components/badge/badge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [badge.variants](./src/components/badge/badge.variants.md) — `badgeVariants` is the cva config that powers the `<Badge>` chassis component. It exposes a `variant` axis with four ru…
- [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_ - [breadcrumb](./src/components/breadcrumb/breadcrumb.md) — <!-- begin:auto-docs --> _(no submodules)_ - [breadcrumb.variants](./src/components/breadcrumb/breadcrumb.variants.md) — `breadcrumbVariants`, `breadcrumbListVariants`, `breadcrumbItemVariants`, `breadcrumbLinkVariants`, `breadcrumbPageVari…
- [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_ - [button](./src/components/button/button.md) — <!-- begin:auto-docs --> _(no submodules)_ - [button.variants](./src/components/button/button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [calendar](./src/components/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [calendar.test-fixtures](./src/components/calendar/calendar.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.calendar.*` keyset. The chas…
- [calendar.variants](./src/components/calendar/calendar.variants.md) — `calendarVariants` is the cva config that powers the outer wrapper of `<Calendar>`. The recipe is intentionally minimal…
- [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_ - [card](./src/components/card/card.md) — <!-- begin:auto-docs --> _(no submodules)_ - [card.variants](./src/components/card/card.variants.md) — `cardVariants` (and its sub-component cva resolvers `cardHeaderVariants`, `cardTitleVariants`, `cardDescriptionVariants…
- [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_ - [checkbox](./src/components/checkbox/checkbox.md) — <!-- begin:auto-docs --> _(no submodules)_ - [checkbox.variants](./src/components/checkbox/checkbox.variants.md) — `checkboxVariants` is the cva (class-variance-authority) configuration that powers every `<Checkbox>` render. It is spl…
- [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_ - [collapsible](./src/components/collapsible/collapsible.md) — <!-- begin:auto-docs --> _(no submodules)_ - [collapsible.variants](./src/components/collapsible/collapsible.variants.md) — `collapsibleVariants`, `collapsibleTriggerVariants`, and `collapsibleContentVariants` are the cva (class-variance-autho…
- [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [confirm-delete-dialog](./src/components/confirm-delete-dialog/confirm-delete-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [confirm-delete-dialog.test-fixtures](./src/components/confirm-delete-dialog/confirm-delete-dialog.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.confirmDelete.*` keyset. The…
- [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ - [context-menu](./src/components/context-menu/context-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ - [context-menu.variants](./src/components/context-menu/context-menu.variants.md) — `contextMenuContentVariants`, `contextMenuItemVariants`, `contextMenuLabelVariants`, `contextMenuSeparatorVariants`, `c…
- [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_ - [data-table](./src/components/data-table/data-table.md) — <!-- begin:auto-docs --> _(no submodules)_ - [data-table.variants](./src/components/data-table/data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dialog](./src/components/dialog/dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dialog.variants](./src/components/dialog/dialog.variants.md) — `dialogOverlayVariants`, `dialogContentVariants`, `dialogHeaderVariants`, `dialogFooterVariants`, `dialogTitleVariants`…
- [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [drawer](./src/components/drawer/drawer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [drawer.variants](./src/components/drawer/drawer.variants.md) — `drawerOverlayVariants`, `drawerContentVariants`, `drawerHandleVariants`, `drawerHeaderVariants`, `drawerFooterVariants…
- [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dropdown-menu](./src/components/dropdown-menu/dropdown-menu.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dropdown-menu.variants](./src/components/dropdown-menu/dropdown-menu.variants.md) — `dropdownMenuContentVariants`, `dropdownMenuItemVariants`, `dropdownMenuLabelVariants`, `dropdownMenuSeparatorVariants`…
- [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [empty](./src/components/empty/empty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [empty.variants](./src/components/empty/empty.variants.md) — `emptyVariants` is the cva config that powers the `<Empty>` chassis component (with `emptyHeaderVariants`, `emptyMediaV…
- [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-boundary](./src/components/error-boundary/error-boundary.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-boundary.metrics](./src/components/error-boundary/error-boundary.metrics.md) — Co-located OTel counter for the chassis `<ErrorBoundary>` component. Lives next to the boundary itself rather than in t…
- [error-boundary.test-fixtures](./src/components/error-boundary/error-boundary.test-fixtures.md) — Wave-3g local test fixture that augments the chassis test i18n instance with the `react-ui.errorBoundary.*` keyset. The…
- [error-boundary.variants](./src/components/error-boundary/error-boundary.variants.md) — Cva slot definitions for the chassis `<ErrorBoundary>` default fallback. Splits the fallback into four named slots — ou…
- [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_ - [field](./src/components/field/field.md) — <!-- begin:auto-docs --> _(no submodules)_ - [field.variants](./src/components/field/field.variants.md) — `fieldVariants`, `fieldLabelVariants`, `fieldDescriptionVariants`, and `fieldErrorVariants` are the four cva (class-var…
- [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [field-group](./src/components/field-group/field-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [field-group.variants](./src/components/field-group/field-group.variants.md) — `fieldGroupVariants` is the cva (class-variance-authority) configuration powering every `<FieldGroup>` render. It is sp…
- [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_ - [field-set](./src/components/field-set/field-set.md) — <!-- begin:auto-docs --> _(no submodules)_ - [field-set.variants](./src/components/field-set/field-set.variants.md) — `fieldSetVariants` and `fieldLegendVariants` are the cva (class-variance-authority) configurations powering `<FieldSet>…
- [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hover-card](./src/components/hover-card/hover-card.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hover-card.variants](./src/components/hover-card/hover-card.variants.md) — `hoverCardContentVariants` is the cva configuration powering the `<HoverCardContent>` floating panel. It is split from…
- [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_ - [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_ - [input-number](./src/components/input/input-number/input-number.md) — <!-- begin:auto-docs --> _(no submodules)_ - [input](./src/components/input/input.md) — <!-- begin:auto-docs --> _(no submodules)_ - [input.variants](./src/components/input/input.variants.md) — `inputVariants` is the cva (class-variance-authority) configuration that powers every `<Input>` render. It is split fro…
- [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [input-group](./src/components/input-group/input-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [input-group.variants](./src/components/input-group/input-group.variants.md) — `inputGroupVariants`, `inputGroupAddonVariants`, `inputGroupInputVariants`, `inputGroupTextareaVariants`, and `inputGro…
- [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_ - [label](./src/components/label/label.md) — <!-- begin:auto-docs --> _(no submodules)_ - [label.variants](./src/components/label/label.variants.md) — `labelVariants` is the cva (class-variance-authority) configuration that powers every `<Label>` render. It is split fro…
- [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [menubar](./src/components/menubar/menubar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [menubar.variants](./src/components/menubar/menubar.variants.md) — `menubarVariants`, `menubarTriggerVariants`, `menubarContentVariants`, `menubarItemVariants`, `menubarLabelVariants`, `…
- [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pagination](./src/components/pagination/pagination.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pagination.variants](./src/components/pagination/pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_ - [popover](./src/components/popover/popover.md) — <!-- begin:auto-docs --> _(no submodules)_ - [popover.variants](./src/components/popover/popover.variants.md) — `popoverContentVariants` is the cva configuration powering the `<PopoverContent>` floating panel. It is split from `pop…
- [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_ - [progress](./src/components/progress/progress.md) — <!-- begin:auto-docs --> _(no submodules)_ - [progress.variants](./src/components/progress/progress.variants.md) — `progressVariants` is the cva config that powers the `<Progress>` chassis component (paired with `progressIndicatorVari…
- [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [radio-group](./src/components/radio-group/radio-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [radio-group.variants](./src/components/radio-group/radio-group.variants.md) — `radioGroupVariants` and `radioGroupItemVariants` are the cva (class-variance-authority) configurations powering `<Radi…
- [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_ - [resizable](./src/components/resizable/resizable.md) — <!-- begin:auto-docs --> _(no submodules)_ - [resizable.variants](./src/components/resizable/resizable.variants.md) — `resizablePanelGroupVariants`, `resizablePanelVariants`, `resizableHandleVariants`, and `resizableHandleGripVariants` a…
- [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_ - [scroll-area](./src/components/scroll-area/scroll-area.md) — <!-- begin:auto-docs --> _(no submodules)_ - [scroll-area.variants](./src/components/scroll-area/scroll-area.variants.md) — `scrollAreaVariants`, `scrollAreaViewportVariants`, `scrollAreaScrollbarVariants`, `scrollAreaThumbVariants`, and `scro…
- [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [select](./src/components/select/select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [select.test-fixtures](./src/components/select/select.test-fixtures.md) — Wave-3h local test fixture that augments the chassis test i18n instance with the `react-ui.select.placeholder` key. The…
- [select.variants](./src/components/select/select.variants.md) — The `<Select>` component is composed of six visual slots — trigger, content panel, item row, group label, separator, an…
- [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [separator](./src/components/separator/separator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [separator.variants](./src/components/separator/separator.variants.md) — `separatorVariants` is the cva (class-variance-authority) configuration that powers every `<Separator>` render. It is s…
- [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sheet](./src/components/sheet/sheet.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sheet.variants](./src/components/sheet/sheet.variants.md) — `sheetOverlayVariants`, `sheetContentVariants`, `sheetHeaderVariants`, `sheetFooterVariants`, `sheetTitleVariants`, `sh…
- [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_ - [skeleton](./src/components/skeleton/skeleton.md) — <!-- begin:auto-docs --> _(no submodules)_ - [skeleton.variants](./src/components/skeleton/skeleton.variants.md) — `skeletonVariants` is the cva (class-variance-authority) configuration that powers every `<Skeleton>` render. It is spl…
- [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sonner](./src/components/sonner/sonner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sonner.variants](./src/components/sonner/sonner.variants.md) — Defines the `sonnerToasterVariants` cva slot for the chassis `<Sonner>` toaster host. Sonner manages its own stacking,…
- [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [spinner](./src/components/spinner/spinner.md) — <!-- begin:auto-docs --> _(no submodules)_ - [spinner.variants](./src/components/spinner/spinner.variants.md) — `spinnerVariants` is the cva config that powers the `<Spinner>` chassis component. It exposes a `size` axis (`sm` / `de…
- [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [status-badge](./src/components/status-badge/status-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [status-badge.variants](./src/components/status-badge/status-badge.variants.md) — `statusBadgeVariants` is the cva config that powers the `<StatusBadge>` chassis component. It exposes a `status` axis w…
- [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_ - [switch](./src/components/switch/switch.md) — <!-- begin:auto-docs --> _(no submodules)_ - [switch.variants](./src/components/switch/switch.variants.md) — `switchVariants` and `switchThumbVariants` are the two cva (class-variance-authority) configurations that together powe…
- [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_ - [table](./src/components/table/table.md) — <!-- begin:auto-docs --> _(no submodules)_ - [table.variants](./src/components/table/table.variants.md) — `tableVariants` and its eight sub-component cva resolvers (`tableHeaderVariants`, `tableBodyVariants`, `tableFooterVari…
- [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tabs](./src/components/tabs/tabs.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tabs.variants](./src/components/tabs/tabs.variants.md) — `tabsVariants`, `tabsListVariants`, `tabsTriggerVariants`, and `tabsContentVariants` are the cva (class-variance-author…
- [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_ - [textarea](./src/components/textarea/textarea.md) — <!-- begin:auto-docs --> _(no submodules)_ - [textarea.variants](./src/components/textarea/textarea.variants.md) — `textareaVariants` is the cva (class-variance-authority) configuration that powers every `<Textarea>` render. It is spl…
- [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-toggle](./src/components/theme-toggle/theme-toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-toggle.variants](./src/components/theme-toggle/theme-toggle.variants.md) — Cva slot for the chassis `<ThemeToggle>` icon-button. Mirrors the shape of `<Button variant="outline" size="icon">` so…
- [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_ - [toast](./src/components/toast/toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [toggle](./src/components/toggle/toggle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [toggle.variants](./src/components/toggle/toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [toggle-group](./src/components/toggle-group/toggle-group.md) — <!-- begin:auto-docs --> _(no submodules)_ - [toggle-group.variants](./src/components/toggle-group/toggle-group.variants.md) — `toggleGroupVariants` and `toggleGroupItemVariants` are the cva (class-variance-authority) configurations powering `<To…
- [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tooltip](./src/components/tooltip/tooltip.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tooltip.variants](./src/components/tooltip/tooltip.variants.md) — `tooltipContentVariants` is the cva configuration powering the `<TooltipContent>` floating bubble. It is split from `to…
- [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-toast-sonner-bridge](./src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — Defines every stable error code emitted by the `@suaveplan/react-ui` package. Codes are grouped under the `REACT_UI` na…
- [react-ui-error](./src/errors/react-ui-error.md) — Owns every error thrown by the `@suaveplan/react-ui` package. Extends the project-wide `SuaveplanError` so each instan…
- [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-mobile](./src/hooks/use-mobile/use-mobile.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-toast](./src/hooks/use-toast/use-toast.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-cell](./src/i18n/formatted-cell/formatted-cell.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-currency](./src/i18n/formatted-currency/formatted-currency.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-date](./src/i18n/formatted-date/formatted-date.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-list](./src/i18n/formatted-list/formatted-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-message](./src/i18n/formatted-message/formatted-message.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-number](./src/i18n/formatted-number/formatted-number.md) — <!-- begin:auto-docs --> _(no submodules)_- [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_ - [formatted-relative-time](./src/i18n/formatted-relative-time/formatted-relative-time.md) — <!-- begin:auto-docs --> _(no submodules)_- [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_ - [plural](./src/i18n/plural/plural.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [react-ui-i18n-bridge](./src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_ - [stories-shell](./src/i18n/stories-shell/stories-shell.md) — <!-- begin:auto-docs --> _(no submodules)_- [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tanstack-bridge](./src/i18n/tanstack-bridge/tanstack-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validation-message-bridge](./src/i18n/validation-message-bridge/validation-message-bridge.md) — <!-- begin:auto-docs --> _(no submodules)_- [augmentation](./src/locales/augmentation.md) — Side-effect-only TypeScript module that augments `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface with…
- [schema](./src/locales/schema.md) — Zod schema asserting that every locale bundle declares exactly the keys present in the canonical `en-GB.json` source-of…
- [translation-keys](./src/locales/translation-keys.md) — `translation-keys.ts` is the codegen-emitted string-literal union of every translation key contributed by `@suaveplan/r…
- [observability](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [react-ui-metrics](./src/observability/react-ui-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pack](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pack-registry-provider](./src/pack/pack-registry-provider/pack-registry-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_ - [runtime-override](./src/pack/runtime-override/runtime-override.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-pack-component](./src/pack/use-pack-component/use-pack-component.md) — <!-- begin:auto-docs --> _(no submodules)_ - [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_ - [version-check](./src/pack/version-check/version-check.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pack-context](./src/pack/pack-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [react-ui-provider](./src/provider/react-ui-provider/react-ui-provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [theme](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-provider](./src/theme/theme-provider/theme-provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-switcher](./src/theme/theme-switcher/theme-switcher.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dark](./src/theme/themes/dark.md) — The dark counterpart of `lightTheme`. Same variant surface, same token names, lightness anchors inverted so foreground…
- [high-contrast](./src/theme/themes/high-contrast.md) — Accessibility-first theme that meets WCAG AAA contrast for normal and large text alike. Pure-white foreground (`#FFFFFF…
- [light](./src/theme/themes/light.md) — The default `ThemePlugin` instance shipped with `@suaveplan/react-ui`. `lightTheme` matches the shadcn `nova` token pre…
- [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-theme](./src/theme/use-theme/use-theme.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-theme-tokens](./src/theme/use-theme-tokens/use-theme-tokens.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-themed-variants](./src/theme/use-themed-variants/use-themed-variants.md) — <!-- begin:auto-docs --> _(no submodules)_ - [theme-context](./src/theme/theme-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cn](./src/utils/cn/cn.md) — <!-- begin:auto-docs --> _(no submodules)_- [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_ - [motion](./src/utils/motion/motion.md) — <!-- begin:auto-docs --> _(no submodules)_- [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_ - [variants](./src/variants/variants.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/version.md) — Declares the running version of `@suaveplan/react-ui` as the constant `REACT_UI_VERSION`. The version is hand-maintaine…
## What is here
- `<ReactUiProvider>` — opinionated wrapper composing
`<TranslationProvider>` → `<ThemeProvider>` → `<PackRegistryProvider>`
- `<ThemeProvider>` — theme registry with FOUC-prevention inline script,
`prefers-color-scheme` + `prefers-reduced-motion` listeners, and a
schema-validated `ThemePlugin` contract
- `<PackRegistryProvider>` — plugin registry with collision detection,
lifecycle hooks, peer-version checks (warn-only), and per-pack
translation auto-registration
- `useTheme`, `useThemeTokens`, `useThemedVariants`, `<ThemeSwitcher>` —
read-side hooks
- `usePackComponent` — typed pack-component lookup
- `useMobile`, `useToast`, `useDirection` — chassis hooks
- `cn`, `motionToken`, `defineVariants`, `cva` — utilities + variant
authoring helpers
- Three built-in themes: `lightTheme` (shadcn `nova`), `darkTheme` (pure
inversion), `highContrastTheme` (WCAG AAA)
- Five bundled locale catalogs (`en-GB`, `zh-CN`, `ar-AE`, `cy-GB`,
`ru-RU`); auto-registered into the active i18n instance
- OpenTelemetry counters / histograms / spans for every chassis
surface (theme switches, pack lifecycle, locale-bundle loads, themed
variants resolve, toast notifications, provider mount)
- shadcn-CLI-compatible registry generator at
`scripts/build-registry.ts`
## Installation
```bash
bun add @suaveplan/react-ui @suaveplan/react-i18n @suaveplan/i18n
```
## Quickstart
```tsx
import { ReactUiProvider } from "@suaveplan/react-ui/provider";
import { darkTheme, lightTheme } from "@suaveplan/react-ui/theme";
import { createI18n } from "@suaveplan/i18n/plugin";
const i18n = createI18n({ defaultLocale: "en-GB" });
export function App() {
return (
<ReactUiProvider
i18n={i18n}
themes={[lightTheme, darkTheme]}
defaultTheme="light"
>
<YourApp />
</ReactUiProvider>
);
}
```
## Tier and tree-shaking
- **Tier**: React. Peer-deps `react ^18.0.0 || ^19.0.0` /
`react-dom ^18.0.0 || ^19.0.0`.
- **Tree-shaken**: each feature module is exported under its own
subpath in `package.json#exports` so a consumer importing only `cn`
(`import { cn } from '@suaveplan/react-ui/utils/cn'`) does NOT pull
the rest of the chassis.
- **CSS side-effects**: `package.json#sideEffects` whitelists
`styles/global.css` and the bundled locale `*.json` so bundlers
preserve CSS layer registration AND lazy-import chunk emission.
## OpenSpec
Owning change: [`enterprise-react-ui-v2`](../../../docs/design/enterprise-react-ui-v2).
Cross-cutting design: `docs/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md`.
## License
MIT — © Suaveplan Team.

View File

@@ -0,0 +1,588 @@
{
"name": "@suaveplan/react-ui",
"version": "0.1.0",
"type": "module",
"sideEffects": [
"./dist/styles/global.css",
"./dist/locales/en-GB.js",
"./dist/locales/zh-CN.js",
"./dist/locales/ar-AE.js",
"./dist/locales/cy-GB.js",
"./dist/locales/ru-RU.js",
"./src/styles/global.css"
],
"description": "React UI component library — opinionated providers, theme/pack plugin contracts, bundled translations, telemetry, shadcn-compatible registry generator.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./provider": {
"import": {
"types": "./dist/provider/react-ui-provider/react-ui-provider.d.ts",
"default": "./dist/provider/react-ui-provider/react-ui-provider.js"
}
},
"./theme": {
"import": {
"types": "./dist/theme/index.d.ts",
"default": "./dist/theme/index.js"
}
},
"./theme/theme-provider": {
"import": {
"types": "./dist/theme/theme-provider/theme-provider.d.ts",
"default": "./dist/theme/theme-provider/theme-provider.js"
}
},
"./theme/use-theme": {
"import": {
"types": "./dist/theme/use-theme/use-theme.d.ts",
"default": "./dist/theme/use-theme/use-theme.js"
}
},
"./theme/use-theme-tokens": {
"import": {
"types": "./dist/theme/use-theme-tokens/use-theme-tokens.d.ts",
"default": "./dist/theme/use-theme-tokens/use-theme-tokens.js"
}
},
"./theme/use-themed-variants": {
"import": {
"types": "./dist/theme/use-themed-variants/use-themed-variants.d.ts",
"default": "./dist/theme/use-themed-variants/use-themed-variants.js"
}
},
"./theme/use-theme-mutator": {
"import": {
"types": "./dist/theme/use-theme-mutator/use-theme-mutator.d.ts",
"default": "./dist/theme/use-theme-mutator/use-theme-mutator.js"
}
},
"./theme/use-resolved-theme-tokens": {
"import": {
"types": "./dist/theme/use-resolved-theme-tokens/use-resolved-theme-tokens.d.ts",
"default": "./dist/theme/use-resolved-theme-tokens/use-resolved-theme-tokens.js"
}
},
"./theme/layer-stack": {
"import": {
"types": "./dist/theme/layer-stack/index.d.ts",
"default": "./dist/theme/layer-stack/index.js"
}
},
"./theme/theme-switcher": {
"import": {
"types": "./dist/theme/theme-switcher/theme-switcher.d.ts",
"default": "./dist/theme/theme-switcher/theme-switcher.js"
}
},
"./theme/themes/light": {
"import": {
"types": "./dist/theme/themes/light.d.ts",
"default": "./dist/theme/themes/light.js"
}
},
"./theme/themes/dark": {
"import": {
"types": "./dist/theme/themes/dark.d.ts",
"default": "./dist/theme/themes/dark.js"
}
},
"./theme/themes/high-contrast": {
"import": {
"types": "./dist/theme/themes/high-contrast.d.ts",
"default": "./dist/theme/themes/high-contrast.js"
}
},
"./pack": {
"import": {
"types": "./dist/pack/index.d.ts",
"default": "./dist/pack/index.js"
}
},
"./pack/pack-registry-provider": {
"import": {
"types": "./dist/pack/pack-registry-provider/pack-registry-provider.d.ts",
"default": "./dist/pack/pack-registry-provider/pack-registry-provider.js"
}
},
"./pack/use-pack-component": {
"import": {
"types": "./dist/pack/use-pack-component/use-pack-component.d.ts",
"default": "./dist/pack/use-pack-component/use-pack-component.js"
}
},
"./hooks/use-mobile": {
"import": {
"types": "./dist/hooks/use-mobile/use-mobile.d.ts",
"default": "./dist/hooks/use-mobile/use-mobile.js"
}
},
"./hooks/use-toast": {
"import": {
"types": "./dist/hooks/use-toast/use-toast.d.ts",
"default": "./dist/hooks/use-toast/use-toast.js"
}
},
"./hooks/use-direction": {
"import": {
"types": "./dist/hooks/use-direction/index.d.ts",
"default": "./dist/hooks/use-direction/index.js"
}
},
"./utils/cn": {
"import": {
"types": "./dist/utils/cn/cn.d.ts",
"default": "./dist/utils/cn/cn.js"
}
},
"./utils/motion": {
"import": {
"types": "./dist/utils/motion/motion.d.ts",
"default": "./dist/utils/motion/motion.js"
}
},
"./variants": {
"import": {
"types": "./dist/variants/variants.d.ts",
"default": "./dist/variants/variants.js"
}
},
"./locales": {
"import": {
"types": "./dist/locales/index.d.ts",
"default": "./dist/locales/index.js"
}
},
"./errors": {
"import": {
"types": "./dist/errors/index.d.ts",
"default": "./dist/errors/index.js"
}
},
"./observability": {
"import": {
"types": "./dist/observability/react-ui-metrics.d.ts",
"default": "./dist/observability/react-ui-metrics.js"
}
},
"./styles/global.css": "./src/styles/global.css",
"./components/button/button": {
"import": {
"types": "./dist/components/button/button.d.ts",
"default": "./dist/components/button/button.js"
}
},
"./components/input/input": {
"import": {
"types": "./dist/components/input/input.d.ts",
"default": "./dist/components/input/input.js"
}
},
"./components/label/label": {
"import": {
"types": "./dist/components/label/label.d.ts",
"default": "./dist/components/label/label.js"
}
},
"./components/separator/separator": {
"import": {
"types": "./dist/components/separator/separator.d.ts",
"default": "./dist/components/separator/separator.js"
}
},
"./components/skeleton/skeleton": {
"import": {
"types": "./dist/components/skeleton/skeleton.d.ts",
"default": "./dist/components/skeleton/skeleton.js"
}
},
"./components/checkbox/checkbox": {
"import": {
"types": "./dist/components/checkbox/checkbox.d.ts",
"default": "./dist/components/checkbox/checkbox.js"
}
},
"./components/field/field": {
"import": {
"types": "./dist/components/field/field.d.ts",
"default": "./dist/components/field/field.js"
}
},
"./components/field-group/field-group": {
"import": {
"types": "./dist/components/field-group/field-group.d.ts",
"default": "./dist/components/field-group/field-group.js"
}
},
"./components/field-set/field-set": {
"import": {
"types": "./dist/components/field-set/field-set.d.ts",
"default": "./dist/components/field-set/field-set.js"
}
},
"./components/input-group/input-group": {
"import": {
"types": "./dist/components/input-group/input-group.d.ts",
"default": "./dist/components/input-group/input-group.js"
}
},
"./components/radio-group/radio-group": {
"import": {
"types": "./dist/components/radio-group/radio-group.d.ts",
"default": "./dist/components/radio-group/radio-group.js"
}
},
"./components/switch/switch": {
"import": {
"types": "./dist/components/switch/switch.d.ts",
"default": "./dist/components/switch/switch.js"
}
},
"./components/textarea/textarea": {
"import": {
"types": "./dist/components/textarea/textarea.d.ts",
"default": "./dist/components/textarea/textarea.js"
}
},
"./components/toggle/toggle": {
"import": {
"types": "./dist/components/toggle/toggle.d.ts",
"default": "./dist/components/toggle/toggle.js"
}
},
"./components/toggle-group/toggle-group": {
"import": {
"types": "./dist/components/toggle-group/toggle-group.d.ts",
"default": "./dist/components/toggle-group/toggle-group.js"
}
},
"./components/dialog/dialog": {
"import": {
"types": "./dist/components/dialog/dialog.d.ts",
"default": "./dist/components/dialog/dialog.js"
}
},
"./components/sheet/sheet": {
"import": {
"types": "./dist/components/sheet/sheet.d.ts",
"default": "./dist/components/sheet/sheet.js"
}
},
"./components/drawer/drawer": {
"import": {
"types": "./dist/components/drawer/drawer.d.ts",
"default": "./dist/components/drawer/drawer.js"
}
},
"./components/alert-dialog/alert-dialog": {
"import": {
"types": "./dist/components/alert-dialog/alert-dialog.d.ts",
"default": "./dist/components/alert-dialog/alert-dialog.js"
}
},
"./components/popover/popover": {
"import": {
"types": "./dist/components/popover/popover.d.ts",
"default": "./dist/components/popover/popover.js"
}
},
"./components/hover-card/hover-card": {
"import": {
"types": "./dist/components/hover-card/hover-card.d.ts",
"default": "./dist/components/hover-card/hover-card.js"
}
},
"./components/tooltip/tooltip": {
"import": {
"types": "./dist/components/tooltip/tooltip.d.ts",
"default": "./dist/components/tooltip/tooltip.js"
}
},
"./components/context-menu/context-menu": {
"import": {
"types": "./dist/components/context-menu/context-menu.d.ts",
"default": "./dist/components/context-menu/context-menu.js"
}
},
"./components/table/table": {
"import": {
"types": "./dist/components/table/table.d.ts",
"default": "./dist/components/table/table.js"
}
},
"./components/data-table/data-table": {
"import": {
"types": "./dist/components/data-table/data-table.d.ts",
"default": "./dist/components/data-table/data-table.js"
}
},
"./components/card/card": {
"import": {
"types": "./dist/components/card/card.d.ts",
"default": "./dist/components/card/card.js"
}
},
"./components/badge/badge": {
"import": {
"types": "./dist/components/badge/badge.d.ts",
"default": "./dist/components/badge/badge.js"
}
},
"./components/status-badge/status-badge": {
"import": {
"types": "./dist/components/status-badge/status-badge.d.ts",
"default": "./dist/components/status-badge/status-badge.js"
}
},
"./components/avatar/avatar": {
"import": {
"types": "./dist/components/avatar/avatar.d.ts",
"default": "./dist/components/avatar/avatar.js"
}
},
"./components/alert/alert": {
"import": {
"types": "./dist/components/alert/alert.d.ts",
"default": "./dist/components/alert/alert.js"
}
},
"./components/empty/empty": {
"import": {
"types": "./dist/components/empty/empty.d.ts",
"default": "./dist/components/empty/empty.js"
}
},
"./components/progress/progress": {
"import": {
"types": "./dist/components/progress/progress.d.ts",
"default": "./dist/components/progress/progress.js"
}
},
"./components/spinner/spinner": {
"import": {
"types": "./dist/components/spinner/spinner.d.ts",
"default": "./dist/components/spinner/spinner.js"
}
},
"./components/accordion/accordion": {
"import": {
"types": "./dist/components/accordion/accordion.d.ts",
"default": "./dist/components/accordion/accordion.js"
}
},
"./components/aspect-ratio/aspect-ratio": {
"import": {
"types": "./dist/components/aspect-ratio/aspect-ratio.d.ts",
"default": "./dist/components/aspect-ratio/aspect-ratio.js"
}
},
"./components/breadcrumb/breadcrumb": {
"import": {
"types": "./dist/components/breadcrumb/breadcrumb.d.ts",
"default": "./dist/components/breadcrumb/breadcrumb.js"
}
},
"./components/calendar/calendar": {
"import": {
"types": "./dist/components/calendar/calendar.d.ts",
"default": "./dist/components/calendar/calendar.js"
}
},
"./components/collapsible/collapsible": {
"import": {
"types": "./dist/components/collapsible/collapsible.d.ts",
"default": "./dist/components/collapsible/collapsible.js"
}
},
"./components/confirm-delete-dialog/confirm-delete-dialog": {
"import": {
"types": "./dist/components/confirm-delete-dialog/confirm-delete-dialog.d.ts",
"default": "./dist/components/confirm-delete-dialog/confirm-delete-dialog.js"
}
},
"./components/dropdown-menu/dropdown-menu": {
"import": {
"types": "./dist/components/dropdown-menu/dropdown-menu.d.ts",
"default": "./dist/components/dropdown-menu/dropdown-menu.js"
}
},
"./components/error-boundary/error-boundary": {
"import": {
"types": "./dist/components/error-boundary/error-boundary.d.ts",
"default": "./dist/components/error-boundary/error-boundary.js"
}
},
"./components/menubar/menubar": {
"import": {
"types": "./dist/components/menubar/menubar.d.ts",
"default": "./dist/components/menubar/menubar.js"
}
},
"./components/pagination/pagination": {
"import": {
"types": "./dist/components/pagination/pagination.d.ts",
"default": "./dist/components/pagination/pagination.js"
}
},
"./components/resizable/resizable": {
"import": {
"types": "./dist/components/resizable/resizable.d.ts",
"default": "./dist/components/resizable/resizable.js"
}
},
"./components/scroll-area/scroll-area": {
"import": {
"types": "./dist/components/scroll-area/scroll-area.d.ts",
"default": "./dist/components/scroll-area/scroll-area.js"
}
},
"./components/select/select": {
"import": {
"types": "./dist/components/select/select.d.ts",
"default": "./dist/components/select/select.js"
}
},
"./components/sonner/sonner": {
"import": {
"types": "./dist/components/sonner/sonner.d.ts",
"default": "./dist/components/sonner/sonner.js"
}
},
"./components/tabs/tabs": {
"import": {
"types": "./dist/components/tabs/tabs.d.ts",
"default": "./dist/components/tabs/tabs.js"
}
},
"./components/theme-toggle/theme-toggle": {
"import": {
"types": "./dist/components/theme-toggle/theme-toggle.d.ts",
"default": "./dist/components/theme-toggle/theme-toggle.js"
}
},
"./components/toast/toast": {
"import": {
"types": "./dist/components/toast/toast.d.ts",
"default": "./dist/components/toast/toast.js"
}
},
"./components/use-toast-sonner-bridge/use-toast-sonner-bridge": {
"import": {
"types": "./dist/components/use-toast-sonner-bridge/use-toast-sonner-bridge.d.ts",
"default": "./dist/components/use-toast-sonner-bridge/use-toast-sonner-bridge.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/ scripts/ && bun run ../../../../scripts/lint/check-no-backwards-deps.ts --package-json ./package.json && bun run ../../../../scripts/lint/check-no-hardcoded-strings.ts src && bun scripts/check-i18n-keys.ts && bun scripts/check-registry-eject.ts",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"check:i18n-keys": "bun scripts/check-i18n-keys.ts",
"check:i18n-keys:strict": "bun scripts/check-i18n-keys.ts --strict",
"check:registry-eject": "bun scripts/check-registry-eject.ts",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts .",
"registry:build": "bun scripts/build-registry.ts",
"locales:check": "bun scripts/check-locale-completeness.ts ."
},
"dependencies": {
"@suaveplan/error": "workspace:^",
"@suaveplan/events": "workspace:^",
"@suaveplan/i18n": "workspace:^",
"@suaveplan/i18n-icu-parser": "workspace:^",
"@suaveplan/locale-data": "workspace:^",
"@suaveplan/logger": "workspace:^",
"@suaveplan/react-i18n": "workspace:^",
"@suaveplan/types": "workspace:^",
"@radix-ui/react-accordion": "catalog:ui",
"@radix-ui/react-alert-dialog": "catalog:ui",
"@radix-ui/react-aspect-ratio": "catalog:ui",
"@radix-ui/react-avatar": "catalog:ui",
"@radix-ui/react-checkbox": "catalog:ui",
"@radix-ui/react-collapsible": "catalog:ui",
"@radix-ui/react-context-menu": "catalog:ui",
"@radix-ui/react-dialog": "catalog:ui",
"@radix-ui/react-direction": "catalog:ui",
"@radix-ui/react-dropdown-menu": "catalog:ui",
"@radix-ui/react-hover-card": "catalog:ui",
"@radix-ui/react-label": "catalog:ui",
"@radix-ui/react-menubar": "catalog:ui",
"@radix-ui/react-popover": "catalog:ui",
"@radix-ui/react-progress": "catalog:ui",
"@radix-ui/react-radio-group": "catalog:ui",
"@radix-ui/react-scroll-area": "catalog:ui",
"@radix-ui/react-select": "catalog:ui",
"@radix-ui/react-separator": "catalog:ui",
"@radix-ui/react-slot": "catalog:ui",
"@radix-ui/react-switch": "catalog:ui",
"@radix-ui/react-tabs": "catalog:ui",
"@radix-ui/react-toggle": "catalog:ui",
"@radix-ui/react-toggle-group": "catalog:ui",
"@radix-ui/react-tooltip": "catalog:ui",
"@tanstack/react-table": "catalog:ui",
"react-day-picker": "catalog:ui",
"react-resizable-panels": "catalog:ui",
"vaul": "catalog:ui",
"class-variance-authority": "catalog:ui",
"clsx": "catalog:ui",
"lucide-react": "catalog:ui",
"semver": "catalog:ui",
"sonner": "catalog:ui",
"tailwind-merge": "catalog:ui",
"@opentelemetry/api": "catalog:observability",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@suaveplan/testing-dom": "workspace:^",
"@testing-library/react": "catalog:testing",
"@types/node": "catalog:",
"@types/react": "catalog:testing",
"@types/react-dom": "catalog:testing",
"@types/semver": "catalog:ui",
"@vitejs/plugin-react": "catalog:testing",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"happy-dom": "catalog:testing",
"react": "catalog:testing",
"react-dom": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"registry",
"README.md"
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/accordion",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "accordion.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/alert-dialog",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "alert-dialog.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/alert",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "alert.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/aspect-ratio",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "aspect-ratio.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/avatar",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "avatar.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/badge",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "badge.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/breadcrumb",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "breadcrumb.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/button",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "button.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/calendar",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "calendar.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/card",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "card.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/checkbox",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "checkbox.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/collapsible",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "collapsible.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/confirm-delete-dialog",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "confirm-delete-dialog.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/context-menu",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "context-menu.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/data-table",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "data-table.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/dialog",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "dialog.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/drawer",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "drawer.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/dropdown-menu",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "dropdown-menu.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/empty",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "empty.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/error-boundary",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "error-boundary.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/field-group",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "field-group.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/field-set",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "field-set.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/field",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "field.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/hover-card",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "hover-card.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,210 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan",
"items": [
{
"name": "@suaveplan/accordion",
"type": "registry:ui"
},
{
"name": "@suaveplan/alert",
"type": "registry:ui"
},
{
"name": "@suaveplan/alert-dialog",
"type": "registry:ui"
},
{
"name": "@suaveplan/aspect-ratio",
"type": "registry:ui"
},
{
"name": "@suaveplan/avatar",
"type": "registry:ui"
},
{
"name": "@suaveplan/badge",
"type": "registry:ui"
},
{
"name": "@suaveplan/breadcrumb",
"type": "registry:ui"
},
{
"name": "@suaveplan/button",
"type": "registry:ui"
},
{
"name": "@suaveplan/calendar",
"type": "registry:ui"
},
{
"name": "@suaveplan/card",
"type": "registry:ui"
},
{
"name": "@suaveplan/checkbox",
"type": "registry:ui"
},
{
"name": "@suaveplan/collapsible",
"type": "registry:ui"
},
{
"name": "@suaveplan/confirm-delete-dialog",
"type": "registry:ui"
},
{
"name": "@suaveplan/context-menu",
"type": "registry:ui"
},
{
"name": "@suaveplan/data-table",
"type": "registry:ui"
},
{
"name": "@suaveplan/dialog",
"type": "registry:ui"
},
{
"name": "@suaveplan/drawer",
"type": "registry:ui"
},
{
"name": "@suaveplan/dropdown-menu",
"type": "registry:ui"
},
{
"name": "@suaveplan/empty",
"type": "registry:ui"
},
{
"name": "@suaveplan/error-boundary",
"type": "registry:ui"
},
{
"name": "@suaveplan/field",
"type": "registry:ui"
},
{
"name": "@suaveplan/field-group",
"type": "registry:ui"
},
{
"name": "@suaveplan/field-set",
"type": "registry:ui"
},
{
"name": "@suaveplan/hover-card",
"type": "registry:ui"
},
{
"name": "@suaveplan/input",
"type": "registry:ui"
},
{
"name": "@suaveplan/input-group",
"type": "registry:ui"
},
{
"name": "@suaveplan/label",
"type": "registry:ui"
},
{
"name": "@suaveplan/menubar",
"type": "registry:ui"
},
{
"name": "@suaveplan/pagination",
"type": "registry:ui"
},
{
"name": "@suaveplan/popover",
"type": "registry:ui"
},
{
"name": "@suaveplan/progress",
"type": "registry:ui"
},
{
"name": "@suaveplan/radio-group",
"type": "registry:ui"
},
{
"name": "@suaveplan/resizable",
"type": "registry:ui"
},
{
"name": "@suaveplan/scroll-area",
"type": "registry:ui"
},
{
"name": "@suaveplan/select",
"type": "registry:ui"
},
{
"name": "@suaveplan/separator",
"type": "registry:ui"
},
{
"name": "@suaveplan/sheet",
"type": "registry:ui"
},
{
"name": "@suaveplan/skeleton",
"type": "registry:ui"
},
{
"name": "@suaveplan/sonner",
"type": "registry:ui"
},
{
"name": "@suaveplan/spinner",
"type": "registry:ui"
},
{
"name": "@suaveplan/status-badge",
"type": "registry:ui"
},
{
"name": "@suaveplan/switch",
"type": "registry:ui"
},
{
"name": "@suaveplan/table",
"type": "registry:ui"
},
{
"name": "@suaveplan/tabs",
"type": "registry:ui"
},
{
"name": "@suaveplan/textarea",
"type": "registry:ui"
},
{
"name": "@suaveplan/theme-toggle",
"type": "registry:ui"
},
{
"name": "@suaveplan/toast",
"type": "registry:ui"
},
{
"name": "@suaveplan/toggle",
"type": "registry:ui"
},
{
"name": "@suaveplan/toggle-group",
"type": "registry:ui"
},
{
"name": "@suaveplan/tooltip",
"type": "registry:ui"
},
{
"name": "@suaveplan/use-toast-sonner-bridge",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/input-group",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "input-group.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/input",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "input.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/label",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "label.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/menubar",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "menubar.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/pagination",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "pagination.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/popover",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "popover.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/progress",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "progress.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/radio-group",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "radio-group.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/resizable",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "resizable.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/scroll-area",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "scroll-area.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/select",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "select.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/separator",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "separator.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/sheet",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "sheet.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/skeleton",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "skeleton.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/sonner",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "sonner.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/spinner",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "spinner.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/status-badge",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "status-badge.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/switch",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "switch.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/table",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "table.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/tabs",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "tabs.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/textarea",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "textarea.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/theme-toggle",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "theme-toggle.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/toast",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "toast.ts",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/toggle-group",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "toggle-group.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/toggle",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "toggle.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/tooltip",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "tooltip.tsx",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "@suaveplan/use-toast-sonner-bridge",
"type": "registry:ui",
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "use-toast-sonner-bridge.ts",
"type": "registry:ui"
}
]
}

View File

@@ -0,0 +1,137 @@
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
build,
buildRegistryIndex,
buildRegistryItem,
parseArgs,
serialize,
validateIndex,
validateItem,
} from "./build-registry.js";
describe("build-registry", () => {
it(
"buildRegistryItem produces a shadcn-compatible item",
{ timeout: 5000 },
() => {
const item = buildRegistryItem("button");
expect(item.name).toBe("@suaveplan/button");
expect(item.type).toBe("registry:ui");
expect(item.files).toEqual([
{ path: "button.tsx", type: "registry:ui" },
]);
expect(validateItem(item)).toBe(true);
}
);
it(
"buildRegistryIndex includes every supplied name",
{ timeout: 5000 },
() => {
const idx = buildRegistryIndex(["button", "card"]);
expect(idx.items).toHaveLength(2);
expect(idx.items[0]?.name).toBe("@suaveplan/button");
expect(validateIndex(idx)).toBe(true);
}
);
it(
"parseArgs picks up --out and --components flags",
{ timeout: 5000 },
() => {
const parsed = parseArgs([
"--out",
"/tmp/out",
"--components",
"/tmp/src/components",
]);
expect(parsed.outDir).toBe("/tmp/out");
expect(parsed.componentsDir).toBe("/tmp/src/components");
}
);
it(
"serialize emits canonical JSON with trailing newline",
{ timeout: 5000 },
() => {
const out = serialize({ a: 1 });
expect(out).toBe('{\n "a": 1\n}\n');
}
);
it(
"build emits an empty index when components dir does not exist",
{ timeout: 10000 },
async () => {
const dir = await mkdtemp(join(tmpdir(), "registry-empty-"));
const componentsDir = join(dir, "src", "components");
const outDir = join(dir, "registry", "r");
const result = await build({ componentsDir, outDir });
expect(result.items).toEqual([]);
const idx = JSON.parse(await readFile(result.indexPath, "utf8"));
expect(idx.items).toEqual([]);
}
);
it(
"build emits one manifest per component when present",
{ timeout: 10000 },
async () => {
const dir = await mkdtemp(join(tmpdir(), "registry-some-"));
const componentsDir = join(dir, "src", "components");
const outDir = join(dir, "registry", "r");
await mkdir(join(componentsDir, "button"), { recursive: true });
await writeFile(
join(componentsDir, "button", "button.tsx"),
"export const Button = () => null;"
);
await mkdir(join(componentsDir, "card"), { recursive: true });
await writeFile(
join(componentsDir, "card", "index.ts"),
"export {};"
);
const result = await build({ componentsDir, outDir });
expect(result.items.sort()).toEqual(["button", "card"]);
const buttonItem = JSON.parse(
await readFile(join(outDir, "button.json"), "utf8")
);
expect(buttonItem.name).toBe("@suaveplan/button");
const idx = JSON.parse(await readFile(result.indexPath, "utf8"));
expect(idx.items).toHaveLength(2);
}
);
it(
"validateItem rejects malformed items",
{ timeout: 5000 },
() => {
expect(
validateItem({
$schema: "wrong",
name: "x",
type: "registry:ui",
dependencies: [],
registryDependencies: [],
files: [],
} as never)
).toBe(false);
}
);
it(
"validateIndex rejects malformed indexes",
{ timeout: 5000 },
() => {
expect(
validateIndex({
$schema: "wrong",
name: "x",
items: [],
} as never)
).toBe(false);
}
);
});

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env bun
/**
* `build-registry.ts` — emits the shadcn-CLI-compatible registry tree
* under `packages/react/react-ui/registry/r/` for the chassis.
*
* Real implementation that supersedes the placeholder shipped under
* `ops/registry/scripts/build-registry.ts` (Wave 4 ops scaffolding).
*
* Behaviour:
*
* * Walks `<package-root>/src/components/` (when it exists). For every
* direct subdirectory that contains a `<name>.tsx` file or an
* `index.ts` barrel, emits a `registry/r/<name>.json` manifest.
* * Always emits `registry/r/index.json` with the manifest list (even
* when the components dir is empty, which is the Wave 2 chassis
* state — no components ship yet; they land per-batch in Wave 3a-h).
* * Validates every emitted manifest against the upstream
* shadcn registry-item JSON schema URL declared in the `$schema`
* field. The schema itself is resolved at runtime by the consumer
* when they invoke `shadcn add`.
*
* Future Wave 3 enhancements:
*
* * Strip internal-only OTel hooks (replaced with no-op shims in
* ejected source)
* * Rewrite `@suaveplan/react-i18n` imports to a configurable
* consumer alias (default `@/lib/i18n`)
* * Capture Tailwind/cva dependencies into `registryDependencies`
*
* Usage:
* bun packages/react/react-ui/scripts/build-registry.ts
* bun packages/react/react-ui/scripts/build-registry.ts --out ./tmp/r
*/
import { existsSync } from "node:fs";
import { mkdir, readdir, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const REGISTRY_SCHEMA_URL = "https://ui.shadcn.com/schema/registry-item.json";
const REGISTRY_NAMESPACE = "@suaveplan";
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const PACKAGE_ROOT = resolve(SCRIPT_DIR, "..");
const DEFAULT_COMPONENTS_DIR = join(PACKAGE_ROOT, "src", "components");
const DEFAULT_OUT_DIR = join(PACKAGE_ROOT, "registry", "r");
export interface RegistryItem {
readonly $schema: string;
readonly name: string;
readonly type: "registry:ui";
readonly dependencies: readonly string[];
readonly registryDependencies: readonly string[];
readonly files: readonly RegistryFile[];
}
export interface RegistryFile {
readonly path: string;
readonly type: "registry:ui";
}
export interface RegistryIndex {
readonly $schema: string;
readonly name: string;
readonly items: readonly RegistryIndexEntry[];
}
export interface RegistryIndexEntry {
readonly name: string;
readonly type: "registry:ui";
}
export interface BuildOptions {
readonly componentsDir: string;
readonly outDir: string;
}
export function parseArgs(argv: readonly string[]): Partial<BuildOptions> {
const opts: { componentsDir?: string; outDir?: string } = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--out" && i + 1 < argv.length) {
i += 1;
opts.outDir = resolve(argv[i] as string);
continue;
}
if (arg === "--components" && i + 1 < argv.length) {
i += 1;
opts.componentsDir = resolve(argv[i] as string);
}
}
return opts;
}
export async function listComponentDirs(
componentsDir: string
): Promise<string[]> {
if (!existsSync(componentsDir)) return [];
const entries = await readdir(componentsDir, { withFileTypes: true });
const dirs: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const sub = join(componentsDir, entry.name);
const hasIndex =
existsSync(join(sub, "index.ts")) ||
existsSync(join(sub, "index.tsx")) ||
existsSync(join(sub, `${entry.name}.tsx`));
if (hasIndex) dirs.push(entry.name);
}
return dirs.sort();
}
export function buildRegistryItem(
name: string,
componentsDir?: string
): RegistryItem {
// Most components ship as `<name>.tsx`. A handful (toast,
// use-toast-sonner-bridge) are JSX-free and ship as `<name>.ts`.
// Prefer the `.tsx` form when present so consumers get the JSX
// template; fall back to `.ts` when the component is hooks-only.
const extension =
componentsDir !== undefined &&
existsSync(join(componentsDir, name, `${name}.tsx`))
? "tsx"
: componentsDir !== undefined &&
existsSync(join(componentsDir, name, `${name}.ts`))
? "ts"
: "tsx";
return {
$schema: REGISTRY_SCHEMA_URL,
name: `${REGISTRY_NAMESPACE}/${name}`,
type: "registry:ui",
dependencies: [],
registryDependencies: [],
files: [{ path: `${name}.${extension}`, type: "registry:ui" }],
};
}
export function buildRegistryIndex(names: readonly string[]): RegistryIndex {
return {
$schema: REGISTRY_SCHEMA_URL,
name: REGISTRY_NAMESPACE,
items: names.map((n) => ({
name: `${REGISTRY_NAMESPACE}/${n}`,
type: "registry:ui" as const,
})),
};
}
export function serialize(value: unknown): string {
return `${JSON.stringify(value, null, 2)}\n`;
}
/** Validate the emitted item against the bare shadcn schema shape. */
export function validateItem(value: RegistryItem): boolean {
return (
typeof value.$schema === "string" &&
value.$schema === REGISTRY_SCHEMA_URL &&
typeof value.name === "string" &&
value.name.length > 0 &&
value.type === "registry:ui" &&
Array.isArray(value.dependencies) &&
Array.isArray(value.registryDependencies) &&
Array.isArray(value.files) &&
value.files.every(
(f) => typeof f.path === "string" && f.type === "registry:ui"
)
);
}
export function validateIndex(value: RegistryIndex): boolean {
return (
typeof value.$schema === "string" &&
value.$schema === REGISTRY_SCHEMA_URL &&
typeof value.name === "string" &&
Array.isArray(value.items) &&
value.items.every(
(i) => typeof i.name === "string" && i.type === "registry:ui"
)
);
}
async function ensureDir(path: string): Promise<void> {
await mkdir(path, { recursive: true });
}
export async function writeFileAtomic(
path: string,
content: string
): Promise<void> {
await ensureDir(dirname(path));
await writeFile(path, content, { encoding: "utf8" });
}
export async function build(options: BuildOptions): Promise<{
readonly items: readonly string[];
readonly indexPath: string;
}> {
const componentNames = await listComponentDirs(options.componentsDir);
if (componentNames.length === 0) {
process.stdout.write(
`build-registry: no components found under ${options.componentsDir}; emitting empty registry index\n`
);
}
for (const name of componentNames) {
const item = buildRegistryItem(name, options.componentsDir);
if (!validateItem(item)) {
throw new Error(`build-registry: emitted item failed validation for ${name}`);
}
const out = join(options.outDir, `${name}.json`);
await writeFileAtomic(out, serialize(item));
process.stdout.write(`wrote ${out}\n`);
}
const index = buildRegistryIndex(componentNames);
if (!validateIndex(index)) {
throw new Error("build-registry: emitted index failed validation");
}
const indexPath = join(options.outDir, "index.json");
await writeFileAtomic(indexPath, serialize(index));
process.stdout.write(`wrote ${indexPath}\n`);
return { items: componentNames, indexPath };
}
async function main(): Promise<void> {
const parsed = parseArgs(process.argv.slice(2));
const options: BuildOptions = {
componentsDir: parsed.componentsDir ?? DEFAULT_COMPONENTS_DIR,
outDir: parsed.outDir ?? DEFAULT_OUT_DIR,
};
await ensureDir(options.outDir);
await build(options);
}
if (import.meta.main) {
await main().catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
process.stderr.write(`build-registry FAILED: ${message}\n`);
process.exit(1);
});
}

View File

@@ -0,0 +1,77 @@
# `check-i18n-keys`
## Purpose
Translation-key drift gate for `@suaveplan/react-ui`. Walks every `*.{ts,tsx}` under the package's source roots, extracts every `react-ui.*` translation-key reference from the documented call shapes, and diffs the discovered set against the canonical `src/locales/en-GB.json`. Reports `MISSING <key>` for keys referenced in code but absent from the bundle (consumers will hit a missing-key warning) and `ORPHAN <key>` for bundle keys nothing in source references (dead translation keys). Catches drift the moment a developer renames a key in code but forgets the bundle, or vice versa.
## Features
- Pattern-based extraction over six chassis-recognised call shapes: `t("react-ui.…")`, `tDynamic(...)`, `i18nKey="…"` JSX attribute, `i18nKey: "…"` object literal, `<name>Key={"…"}` prop default, `?? "react-ui.…"` fallback shorthand, `const DEFAULT_*_KEY = "…"` module-scope literal, `labelKey: "…"` plugin shape.
- Source location (`<file>:<line>`) attached to every MISSING diagnostic for quick navigation.
- `--strict` flag promotes ORPHAN diagnostics to errors. Default exits 0 on ORPHAN-only (warn) so the gate ships green even when the bundle has pre-existing dead keys; CI can opt into strict mode after a cleanup pass.
- Wired into `bunx turbo lint` — every commit in CI runs the default mode.
## Usage: Basic Example
```bash
bun run check:i18n-keys
# check-i18n-keys: clean — 23 keys in code, 23 keys in bundle.
```
When a key is missing:
```bash
bun run check:i18n-keys
# MISSING react-ui.foo.absent at src/components/foo/foo.tsx:42
# check-i18n-keys: 1 missing, 0 orphan (orphan = warn)
# (exit 1)
```
## Usage: Advanced Example
Strict mode for a periodic bundle-cleanup pass:
```bash
bun run check:i18n-keys:strict
# ORPHAN react-ui.popover.close
# ORPHAN react-ui.alertDialog.cancel
# check-i18n-keys: 0 missing, 2 orphan (strict)
# (exit 1)
```
The script's exported helpers can be reused by other tooling (e.g. a CI report aggregator):
```ts
import {
diff,
extractKeysFromSource,
loadBundleKeys,
} from "./check-i18n-keys.js";
const sourceKeys = (await extractKeysFromSource(files, root)).keys;
const bundleKeys = await loadBundleKeys(bundlePath);
const diagnostics = diff(sourceKeys, bundleKeys);
```
## API Reference
### CLI
`bun scripts/check-i18n-keys.ts [--strict]`
- Default mode: exits non-zero only when at least one MISSING diagnostic appears. ORPHANs are printed as warnings.
- `--strict`: exits non-zero on any diagnostic (MISSING or ORPHAN).
### Exported helpers
- `extractKeysFromSource(files, rootForRelative): Promise<{ keys: Map<key, location> }>` — walks the supplied source files, returns a map keyed by translation key, value `<relative-path>:<line>`.
- `loadBundleKeys(bundlePath): Promise<Set<string>>` — reads the bundle JSON and returns the subset of keys in the `react-ui.*` namespace.
- `diff(sourceKeys, bundleKeys): Diagnostic[]` — pure function returning sorted diagnostics (MISSING before ORPHAN, alphabetic within each kind).
## Implementation Notes
- Pattern-based instead of AST-based — no `@babel/parser` dep. The supported call shapes are a closed set documented at the top of the script. New idioms (e.g. a future `const KEYS = { close: "react-ui.foo" }` const-record style) need a new pattern; the script falls into the catch-all-of-last-resort `?? "react-ui.…"` only when the value lives in a discriminating context.
- The extraction explicitly EXCLUDES `*.test.tsx`, `*.stories.tsx`, `*.stories.test.tsx`, and `*.bench.ts` — test fixtures and stories often reference keys that exist in the bundle for component contracts, not for the test's own runtime use; treating them as references would mask real orphans.
- Scan roots: `src/components`, `src/i18n`, `src/theme`, `src/pack`, `src/provider`, `src/hooks`, `src/errors`. Any future surface that ships `react-ui.*` keys must be added here.
- The default-vs-strict split is deliberate. Pre-existing orphans in the bundle (`react-ui.empty.defaultDescription`, `react-ui.popover.close`, …) are public-API keys consumers may pass via `*Key` prop overrides; removing them would be a breaking change. Default mode warns; strict mode lets the team enforce zero-orphan after a bundle cleanup change.
- The script is a `Bun.import.meta.main` entry point; `process.exit(await main())` is the only top-level side effect. Imports are pure helpers reusable from tests.

View File

@@ -0,0 +1,171 @@
/**
* Tests for `check-i18n-keys.ts`.
*
* Build a fixtures filesystem on disk under `mkdtemp` and call the
* exported helpers (`extractKeysFromSource`, `loadBundleKeys`,
* `diff`) directly. The script's `main()` integrates with
* `process.argv` / `process.exit` so it is exercised end-to-end via
* a `Bun.spawn` round-trip in the last test.
*/
import { afterEach, beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
diff,
extractKeysFromSource,
loadBundleKeys,
} from "./check-i18n-keys.js";
let workDir: string;
beforeEach(async () => {
workDir = await mkdtemp(join(tmpdir(), "i18n-keys-"));
});
afterEach(async () => {
await rm(workDir, { recursive: true, force: true });
});
async function writeFixture(rel: string, content: string): Promise<string> {
const full = join(workDir, rel);
await mkdir(join(full, ".."), { recursive: true });
await writeFile(full, content, "utf8");
return full;
}
describe("extractKeysFromSource", () => {
it("captures t() calls with double quotes", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const out = t("react-ui.foo.bar");`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.foo.bar")).toBe(true);
});
it("captures t() calls with single quotes", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const out = t('react-ui.foo.bar');`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.foo.bar")).toBe(true);
});
it("captures i18nKey JSX attribute", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`<FormattedMessage i18nKey="react-ui.greeting" />`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.greeting")).toBe(true);
});
it("captures the DEFAULT_*_KEY const idiom", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const DEFAULT_LABEL_KEY = "react-ui.thing.label";`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.thing.label")).toBe(true);
});
it("captures `?? \"react-ui.…\"` fallback shorthand", {
timeout: 5000,
}, async () => {
const f = await writeFixture(
"a.tsx",
`const k = labelKey ?? "react-ui.fallback.label";`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.has("react-ui.fallback.label")).toBe(true);
});
it("records the source location for each key", {
timeout: 5000,
}, async () => {
const f = await writeFixture(
"deep/nested.tsx",
`// header\nconst k = t("react-ui.line.two");`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.get("react-ui.line.two")).toMatch(/deep\/nested\.tsx:2$/);
});
it("ignores non-namespace strings", { timeout: 5000 }, async () => {
const f = await writeFixture(
"a.tsx",
`const k = t("other.namespace.key");`
);
const { keys } = await extractKeysFromSource([f], workDir);
expect(keys.size).toBe(0);
});
});
describe("loadBundleKeys", () => {
it("loads only keys in the react-ui namespace", {
timeout: 5000,
}, async () => {
const path = join(workDir, "bundle.json");
await writeFile(
path,
JSON.stringify({
"react-ui.foo": "Foo",
"react-ui.bar": "Bar",
"other.namespace.key": "other",
}),
"utf8"
);
const keys = await loadBundleKeys(path);
expect(keys.has("react-ui.foo")).toBe(true);
expect(keys.has("react-ui.bar")).toBe(true);
expect(keys.has("other.namespace.key")).toBe(false);
});
});
describe("diff", () => {
it("reports MISSING for code-only keys", { timeout: 5000 }, () => {
const codeKeys = new Map([["react-ui.code.only", "src/x.tsx:1"]]);
const bundleKeys = new Set(["react-ui.bundled"]);
const diags = diff(codeKeys, bundleKeys);
expect(
diags.find((d) => d.kind === "MISSING")?.key
).toBe("react-ui.code.only");
});
it("reports ORPHAN for bundle-only keys", { timeout: 5000 }, () => {
const codeKeys = new Map<string, string>();
const bundleKeys = new Set(["react-ui.bundle.only"]);
const diags = diff(codeKeys, bundleKeys);
expect(
diags.find((d) => d.kind === "ORPHAN")?.key
).toBe("react-ui.bundle.only");
});
it("returns empty diagnostics on a clean source/bundle pair", {
timeout: 5000,
}, () => {
const codeKeys = new Map([["react-ui.k", "src/x.tsx:1"]]);
const bundleKeys = new Set(["react-ui.k"]);
expect(diff(codeKeys, bundleKeys)).toEqual([]);
});
it("orders MISSING before ORPHAN, then alphabetically", {
timeout: 5000,
}, () => {
const codeKeys = new Map([
["react-ui.b", "x:1"],
["react-ui.a", "x:2"],
]);
const bundleKeys = new Set(["react-ui.zzz", "react-ui.aaa"]);
const diags = diff(codeKeys, bundleKeys);
expect(diags.map((d) => `${d.kind}:${d.key}`)).toEqual([
"MISSING:react-ui.a",
"MISSING:react-ui.b",
"ORPHAN:react-ui.aaa",
"ORPHAN:react-ui.zzz",
]);
});
});

View File

@@ -0,0 +1,239 @@
#!/usr/bin/env bun
/**
* `check-i18n-keys.ts` — translation-key drift gate for
* `@suaveplan/react-ui`.
*
* Walks every `*.{ts,tsx}` under `src/components/**` and `src/i18n/**`
* and extracts translation-key references from these call shapes:
*
* - `t("react-ui.…")` / `t('react-ui.…')`
* - `tDynamic("react-ui.…")` / `tDynamic('react-ui.…')`
* - `<FormattedMessage i18nKey="react-ui.…" />` / `i18nKey='…'`
* - `<Trans i18nKey="…">` (existing components)
* - `i18nKey: "react-ui.…"` (object-literal column-config style)
* - `(.+)Key={?["']react-ui\.…["']` for prop-drilled keys
* (Pagination, Calendar, DataTable name multiple `*Key` props)
* - `?? "react-ui.…"` (default-key fallback pattern)
*
* The script then diffs the discovered set against the canonical
* `src/locales/en-GB.json`, reporting:
*
* - `MISSING <key> at <file>:<line>` — referenced in code but absent
* from the bundle (consumers will hit a missing-key warning).
* - `ORPHAN <key>` — present in the bundle but never referenced
* (dead translation key).
*
* Exits non-zero on any diagnostic. Wired into `bunx turbo lint`.
*
* Pattern-based extraction is intentional — the package has no
* `@babel/parser` dep and the supported call shapes are a closed set.
* The script accepts both single and double-quoted attribute values
* and ignores keys outside the `react-ui.…` namespace.
*/
import { readdir, readFile } from "node:fs/promises";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const PACKAGE_ROOT = resolve(
dirname(fileURLToPath(import.meta.url)),
".."
);
const SCAN_ROOTS = [
join(PACKAGE_ROOT, "src", "components"),
join(PACKAGE_ROOT, "src", "i18n"),
join(PACKAGE_ROOT, "src", "theme"),
join(PACKAGE_ROOT, "src", "pack"),
join(PACKAGE_ROOT, "src", "provider"),
join(PACKAGE_ROOT, "src", "hooks"),
join(PACKAGE_ROOT, "src", "errors"),
];
const BUNDLE_PATH = join(PACKAGE_ROOT, "src", "locales", "en-GB.json");
const NAMESPACE = "react-ui.";
export interface Diagnostic {
readonly kind: "MISSING" | "ORPHAN";
readonly key: string;
/** Source path + line for MISSING diagnostics; absent for ORPHAN. */
readonly location?: string;
}
export interface KeyExtraction {
readonly keys: ReadonlyMap<string, string>;
}
const KEY_PATTERNS: ReadonlyArray<RegExp> = [
/\bt\s*\(\s*["'](react-ui\.[^"']+)["']/g,
/\btDynamic\s*\(\s*["'](react-ui\.[^"']+)["']/g,
/\bi18nKey\s*=\s*["'](react-ui\.[^"']+)["']/g,
/\bi18nKey\s*:\s*["'](react-ui\.[^"']+)["']/g,
/\b\w+Key\s*=\s*\{?\s*["'](react-ui\.[^"']+)["']/g,
/\?\?\s*["'](react-ui\.[^"']+)["']/g,
// `const DEFAULT_*_KEY = "react-ui.…"` — the chassis convention
// for module-scoped default keys consumed via the *Key prop
// pattern above.
/(?:const|let|var)\s+\w+\s*(?::[^=]+)?=\s*["'](react-ui\.[^"']+)["']/g,
// `labelKey: "react-ui.…"` — theme-plugin / pack-plugin literal
// (chassis convention).
/\blabelKey\s*:\s*["'](react-ui\.[^"']+)["']/g,
// `as ThemePlugin["labelKey"]` cast pattern — keys forwarded
// through `as const` annotations into a typed slot.
/["'](react-ui\.[a-zA-Z][a-zA-Z0-9.]*)["']\s*as\s+const\b/g,
];
async function walk(dir: string): Promise<string[]> {
const out: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === "node_modules" || entry.name === "dist") continue;
const full = join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await walk(full)));
} else if (
entry.isFile() &&
(entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
!entry.name.endsWith(".test.ts") &&
!entry.name.endsWith(".test.tsx") &&
!entry.name.endsWith(".stories.tsx") &&
!entry.name.endsWith(".stories.test.tsx") &&
!entry.name.endsWith(".bench.ts")
) {
out.push(full);
}
}
return out;
}
function lineNumber(source: string, offset: number): number {
let line = 1;
for (let i = 0; i < offset && i < source.length; i += 1) {
if (source.charCodeAt(i) === 10) line += 1;
}
return line;
}
export async function extractKeysFromSource(
sourceFiles: ReadonlyArray<string>,
rootForRelative: string
): Promise<KeyExtraction> {
const keys = new Map<string, string>();
for (const file of sourceFiles) {
const text = await readFile(file, "utf8");
for (const pattern of KEY_PATTERNS) {
for (const match of text.matchAll(pattern)) {
const key = match[1];
if (typeof key !== "string" || keys.has(key)) continue;
const idx = match.index ?? 0;
const ln = lineNumber(text, idx);
keys.set(key, `${relative(rootForRelative, file)}:${ln}`);
}
}
}
return { keys };
}
export function diff(
sourceKeys: ReadonlyMap<string, string>,
bundleKeys: ReadonlySet<string>
): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
for (const [key, location] of sourceKeys) {
if (!bundleKeys.has(key)) {
diagnostics.push({ kind: "MISSING", key, location });
}
}
for (const key of bundleKeys) {
if (!sourceKeys.has(key)) {
diagnostics.push({ kind: "ORPHAN", key });
}
}
diagnostics.sort((a, b) => {
if (a.kind !== b.kind) return a.kind === "MISSING" ? -1 : 1;
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
});
return diagnostics;
}
export async function loadBundleKeys(
bundlePath: string
): Promise<Set<string>> {
const raw = await readFile(bundlePath, "utf8");
const parsed: Record<string, unknown> = JSON.parse(raw);
const out = new Set<string>();
for (const key of Object.keys(parsed)) {
if (key.startsWith(NAMESPACE)) out.add(key);
}
return out;
}
export interface CheckOptions {
readonly strict: boolean;
}
function parseArgs(argv: ReadonlyArray<string>): CheckOptions {
let strict = false;
for (const arg of argv) {
if (arg === "--strict") strict = true;
}
return { strict };
}
async function main(): Promise<number> {
const opts = parseArgs(process.argv.slice(2));
const sourceFiles: string[] = [];
for (const root of SCAN_ROOTS) {
try {
sourceFiles.push(...(await walk(root)));
} catch (err) {
if (
(err as NodeJS.ErrnoException).code !== "ENOENT" &&
(err as NodeJS.ErrnoException).code !== "ENOTDIR"
) {
throw err;
}
}
}
if (sourceFiles.length === 0) {
console.error(
`check-i18n-keys: no source files found under any of ${SCAN_ROOTS.join(", ")}`
);
return 2;
}
const { keys: sourceKeys } = await extractKeysFromSource(
sourceFiles,
PACKAGE_ROOT
);
const bundleKeys = await loadBundleKeys(BUNDLE_PATH);
const diagnostics = diff(sourceKeys, bundleKeys);
if (diagnostics.length === 0) {
console.log(
`check-i18n-keys: clean — ${sourceKeys.size} keys in code, ${bundleKeys.size} keys in bundle.`
);
return 0;
}
const missingCount = diagnostics.filter((d) => d.kind === "MISSING").length;
const orphanCount = diagnostics.length - missingCount;
for (const d of diagnostics) {
if (d.kind === "MISSING") {
console.error(`MISSING ${d.key} at ${d.location ?? "?"}`);
} else if (opts.strict) {
console.error(`ORPHAN ${d.key}`);
} else {
console.warn(`ORPHAN ${d.key} (warn — use --strict to fail)`);
}
}
console.error(
`check-i18n-keys: ${missingCount} missing, ${orphanCount} orphan` +
(opts.strict ? " (strict)" : " (orphan = warn)")
);
if (missingCount > 0) return 1;
if (opts.strict && orphanCount > 0) return 1;
return 0;
}
if (import.meta.main) {
process.exit(await main());
}

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env bun
/**
* `check-locale-completeness.ts` — assert every key in
* `<package>/src/locales/en-GB.json` exists in every other locale
* bundle shipped by the package. Pure file walk; no transitive deps on
* the package source.
*
* Usage:
* bun packages/react/react-ui/scripts/check-locale-completeness.ts <package-root>
*
* Exit codes:
* 0 — every key parity-complete
* 1 — at least one key missing
* 2 — internal failure
*/
import { readdir, readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
interface BundleDiff {
readonly locale: string;
readonly missing: ReadonlyArray<string>;
readonly extra: ReadonlyArray<string>;
}
export async function checkLocaleCompleteness(
packageRoot: string
): Promise<BundleDiff[]> {
const localesDir = join(resolve(packageRoot), "src", "locales");
const enPath = join(localesDir, "en-GB.json");
const enKeys = new Set(
Object.keys(JSON.parse(await readFile(enPath, "utf8")))
);
const entries = await readdir(localesDir);
const diffs: BundleDiff[] = [];
for (const entry of entries) {
if (entry === "en-GB.json") continue;
if (!entry.endsWith(".json")) continue;
const otherPath = join(localesDir, entry);
const other = JSON.parse(await readFile(otherPath, "utf8")) as Record<
string,
string
>;
const otherKeys = new Set(Object.keys(other));
const missing: string[] = [];
for (const k of enKeys) {
if (!otherKeys.has(k)) missing.push(k);
}
const extra: string[] = [];
for (const k of otherKeys) {
if (!enKeys.has(k)) extra.push(k);
}
if (missing.length > 0 || extra.length > 0) {
diffs.push({
locale: entry.replace(/\.json$/, ""),
missing,
extra,
});
}
}
return diffs;
}
function format(diffs: ReadonlyArray<BundleDiff>): string {
if (diffs.length === 0) {
return "check-locale-completeness: every locale parity-complete.";
}
const lines: string[] = [
`check-locale-completeness: ${diffs.length} locale(s) failed parity.`,
];
for (const d of diffs) {
lines.push(` ${d.locale}:`);
if (d.missing.length > 0) {
lines.push(" missing keys:");
for (const k of d.missing) lines.push(` - ${k}`);
}
if (d.extra.length > 0) {
lines.push(" extra keys (not in en-GB):");
for (const k of d.extra) lines.push(` - ${k}`);
}
}
return lines.join("\n");
}
async function main(): Promise<void> {
const argv = process.argv.slice(2);
if (argv.length === 0) {
process.stderr.write(
"Usage: check-locale-completeness.ts <package-root>\n"
);
process.exit(2);
}
const root = argv[0] as string;
const diffs = await checkLocaleCompleteness(root);
const message = format(diffs);
if (diffs.length === 0) {
process.stdout.write(`${message}\n`);
process.exit(0);
}
process.stderr.write(`${message}\n`);
process.exit(1);
}
if (import.meta.main) {
await main().catch((err: unknown) => {
process.stderr.write(`internal error: ${(err as Error).message}\n`);
process.exit(2);
});
}

View File

@@ -0,0 +1,151 @@
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { checkRoots } from "../../../../../scripts/lint/check-no-hardcoded-strings.js";
async function makeTempFile(name: string, body: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "no-hardcoded-strings-"));
const file = join(dir, name);
await writeFile(file, body, "utf8");
return dir;
}
describe("check-no-hardcoded-strings", () => {
it(
"passes a t-wrapped JSX text node",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"ok.tsx",
`import { useTranslation } from '@suaveplan/react-i18n';
export function X() {
const { t } = useTranslation();
return <button type="button">{t('react-ui.themeToggle.label')}</button>;
}
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics).toEqual([]);
}
);
it(
"fails a bare JSX text node",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"bad.tsx",
`export function X() {
return <button type="button">Close</button>;
}
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
expect(diagnostics[0]?.rule).toBe("jsx-text");
expect(diagnostics[0]?.snippet).toBe("Close");
}
);
it(
"fails a string literal aria-label",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"aria.tsx",
`export function X() {
return <button type="button" aria-label="Close" />;
}
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
expect(diagnostics[0]?.rule).toBe("jsx-attr-aria-label");
}
);
it(
"fails an object-literal label string",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"obj.ts",
`export const fields = { email: { label: 'Email' } };
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
expect(diagnostics[0]?.rule).toBe("object-label");
}
);
it(
"fails a JSX expression string-literal child",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"literal.tsx",
`export function X() {
return <span>{'hardcoded'}</span>;
}
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
}
);
it(
"fails a string-literal placeholder attribute",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"placeholder.tsx",
`export function X() {
return <input type="text" placeholder="Email address" />;
}
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
expect(diagnostics[0]?.rule).toBe("jsx-attr-placeholder");
}
);
it(
"skips test/spec/stories files via the skip suffix list",
{ timeout: 10000 },
async () => {
const root = await makeTempFile(
"thing.test.tsx",
`export function X() {
return <button type="button">hardcoded but exempt</button>;
}
`
);
const diagnostics = await checkRoots([{ root }]);
expect(diagnostics).toEqual([]);
}
);
it(
"skips spike directories",
{ timeout: 10000 },
async () => {
const dir = await mkdtemp(join(tmpdir(), "spike-skip-"));
const spikeDir = join(dir, "spike");
const fs = await import("node:fs/promises");
await fs.mkdir(spikeDir, { recursive: true });
await fs.writeFile(
join(spikeDir, "x.tsx"),
`export function X() {
return <button type="button">spike literal</button>;
}
`
);
const diagnostics = await checkRoots([{ root: dir }]);
expect(diagnostics).toEqual([]);
}
);
});

View File

@@ -0,0 +1,63 @@
# `check-registry-eject`
## Purpose
Shadcn-eject smoke gate for `@suaveplan/react-ui`. Verifies the `registry/r/*.json` tree against the package source so a downstream consumer running `shadcn add @suaveplan/<name>` always finds the files the registry promised. Catches the realistic drift modes today: a registry entry whose listed file vanished (rename / restructure), a component directory shipped without a registry entry (forgot to regenerate), an orphaned registry entry whose component was deleted (forgot to clean up), and entries that fail JSON or shape validation. Runs as part of `bunx turbo lint` on every commit.
## Features
- Per-entry validation: shape (`name`, `type`, `files`), JSON parse, non-empty `files[]`.
- Per-file existence: every `files[].path` resolves to a real file under `src/components/<name>/`.
- Bidirectional drift detection: registry entries without matching component directories AND component directories without registry entries both fail.
- Sorted, stable diagnostic output for diff-friendly CI logs.
- Pure helper exports (`audit`, `loadEntry`) reusable from tests.
## Usage: Basic Example
```bash
bun run check:registry-eject
# check-registry-eject: clean — 51 registry entries match 51 component directories.
```
When a registry entry lists a missing file:
```bash
bun run check:registry-eject
# MISSING_SOURCE_FILE registry/r/toast.json: file 'toast.tsx' does not exist at src/components/toast/toast.tsx
# check-registry-eject: 1 diagnostic(s) — see above.
# (exit 1)
```
## Usage: Advanced Example
The exported helpers can be reused — for example, a CI report generator that walks both registries (`react-ui` + a future `react-ui-social`) and emits a unified report:
```ts
import { audit } from "@suaveplan/react-ui/scripts/check-registry-eject";
const diagnostics = await audit({
registryEntries: await listEntries(packageA),
componentDirs: await listDirs(packageA),
componentsDir: join(packageA, "src/components"),
});
```
## API Reference
### CLI
`bun scripts/check-registry-eject.ts`
Exits 0 on a clean tree; exits 1 on any diagnostic; exits 2 when the registry directory or components directory is missing entirely (configuration error).
### Exported helpers
- `loadEntry(entryPath): Promise<RegistryEntry | { error }>` — reads + validates a single `registry/r/<name>.json` entry. Returns `{ error: string }` on any structural failure (missing field, wrong type, malformed JSON).
- `audit({ registryEntries, componentDirs, componentsDir }): Promise<Diagnostic[]>` — pure function returning sorted diagnostics. The four diagnostic kinds are: `MISSING_REGISTRY_ENTRY`, `MISSING_SOURCE_FILE`, `EMPTY_FILES_LIST`, `MALFORMED_ENTRY`, `ORPHAN_REGISTRY_ENTRY`.
## Implementation Notes
- The gate is intentionally narrower than the proposal's full vision — it verifies file presence + entry shape, NOT that the ejected source compiles against a synthesized consumer `tsconfig.json`. The synthesized-tsconfig roundtrip depends on the registry transform itself (rewriting `@suaveplan/react-ui` imports to consumer-local paths) which today is a passthrough — `build-registry.ts` ships source verbatim, so a consumer's `shadcn add` already gets compilable source. When the transform lands as a callable helper (per the comment block in `build-registry.ts`), this gate's `audit()` will be extended to invoke it and then run `tsc --noEmit` against the transformed tree.
- `index.json` is excluded from the per-entry walk — it is the manifest of manifests, not an individual component.
- Component directories whose only files are barrels (`index.ts` only, no `<name>.tsx`/`<name>.ts`) are NOT detected as component directories. The list-detection mirrors `build-registry.ts`'s heuristic.
- Both `.tsx` and `.ts` extensions are valid for the file listed in `files[]`. Components like `<Toast>` that ship as JSX-free TypeScript ship as `.ts`; the registry entry for them lists `<name>.ts` and the gate accepts it.

View File

@@ -0,0 +1,200 @@
/**
* Tests for `check-registry-eject.ts`.
*
* Builds fixtures filesystems on disk under `mkdtemp` and exercises
* the exported helpers (`audit`, `loadEntry`) directly. The script's
* `main()` is exercised indirectly through these helpers — its
* top-level behaviour (process.exit) is covered by the lint gate
* itself running on real source.
*/
import { afterEach, beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { audit, loadEntry } from "./check-registry-eject.js";
let workDir: string;
beforeEach(async () => {
workDir = await mkdtemp(join(tmpdir(), "registry-eject-"));
});
afterEach(async () => {
await rm(workDir, { recursive: true, force: true });
});
async function writeJson(rel: string, body: unknown): Promise<string> {
const full = join(workDir, rel);
await mkdir(join(full, ".."), { recursive: true });
await writeFile(full, JSON.stringify(body, null, 2), "utf8");
return full;
}
async function writeFixture(rel: string, content = ""): Promise<string> {
const full = join(workDir, rel);
await mkdir(join(full, ".."), { recursive: true });
await writeFile(full, content, "utf8");
return full;
}
describe("loadEntry", () => {
it("loads a valid registry entry", { timeout: 5000 }, async () => {
const entry = await writeJson("button.json", {
name: "@suaveplan/button",
type: "registry:ui",
files: [{ path: "button.tsx", type: "registry:ui" }],
});
const result = await loadEntry(entry);
expect("error" in result).toBe(false);
});
it("rejects an entry without a name", { timeout: 5000 }, async () => {
const entry = await writeJson("bad.json", {
type: "registry:ui",
files: [],
});
const result = await loadEntry(entry);
expect("error" in result).toBe(true);
});
it("rejects an entry with non-array files", {
timeout: 5000,
}, async () => {
const entry = await writeJson("bad.json", {
name: "x",
type: "registry:ui",
files: "oops",
});
const result = await loadEntry(entry);
expect("error" in result).toBe(true);
});
it("rejects malformed JSON", { timeout: 5000 }, async () => {
const entry = await writeFixture("bad.json", "{ not json");
const result = await loadEntry(entry);
expect("error" in result).toBe(true);
});
});
describe("audit", () => {
it("returns no diagnostics on a clean fixtures tree", {
timeout: 5000,
}, async () => {
const componentsDir = join(workDir, "src/components");
await writeFixture("src/components/button/button.tsx", "export {}");
const entry = await writeJson("registry/r/button.json", {
name: "@x/button",
type: "registry:ui",
files: [{ path: "button.tsx", type: "registry:ui" }],
});
const diagnostics = await audit({
registryEntries: [entry],
componentDirs: ["button"],
componentsDir,
});
expect(diagnostics).toEqual([]);
});
it("flags a component directory with no registry entry", {
timeout: 5000,
}, async () => {
const componentsDir = join(workDir, "src/components");
await writeFixture("src/components/button/button.tsx", "export {}");
const diagnostics = await audit({
registryEntries: [],
componentDirs: ["button"],
componentsDir,
});
expect(
diagnostics.find(
(d) => d.kind === "MISSING_REGISTRY_ENTRY"
)?.message
).toMatch(/button/);
});
it("flags a registry entry whose source file is missing", {
timeout: 5000,
}, async () => {
const componentsDir = join(workDir, "src/components");
await mkdir(join(componentsDir, "ghost"), { recursive: true });
const entry = await writeJson("registry/r/ghost.json", {
name: "@x/ghost",
type: "registry:ui",
files: [{ path: "ghost.tsx", type: "registry:ui" }],
});
const diagnostics = await audit({
registryEntries: [entry],
componentDirs: ["ghost"],
componentsDir,
});
expect(
diagnostics.find((d) => d.kind === "MISSING_SOURCE_FILE")?.message
).toMatch(/ghost\.tsx/);
});
it("flags an orphan registry entry (no matching component dir)", {
timeout: 5000,
}, async () => {
const componentsDir = join(workDir, "src/components");
await writeFixture(
"src/components/button/button.tsx",
"export {}"
);
const entryButton = await writeJson("registry/r/button.json", {
name: "@x/button",
type: "registry:ui",
files: [{ path: "button.tsx", type: "registry:ui" }],
});
const entryStale = await writeJson("registry/r/removed.json", {
name: "@x/removed",
type: "registry:ui",
files: [{ path: "removed.tsx", type: "registry:ui" }],
});
const diagnostics = await audit({
registryEntries: [entryButton, entryStale],
componentDirs: ["button"],
componentsDir,
});
expect(
diagnostics.find(
(d) => d.kind === "ORPHAN_REGISTRY_ENTRY"
)?.message
).toMatch(/removed/);
});
it("flags an entry with empty files[]", { timeout: 5000 }, async () => {
const componentsDir = join(workDir, "src/components");
await writeFixture("src/components/foo/foo.tsx", "export {}");
const entry = await writeJson("registry/r/foo.json", {
name: "@x/foo",
type: "registry:ui",
files: [],
});
const diagnostics = await audit({
registryEntries: [entry],
componentDirs: ["foo"],
componentsDir,
});
expect(
diagnostics.find((d) => d.kind === "EMPTY_FILES_LIST")?.message
).toMatch(/foo\.json/);
});
it("flags malformed JSON entries", { timeout: 5000 }, async () => {
const componentsDir = join(workDir, "src/components");
await writeFixture("src/components/foo/foo.tsx", "");
const entry = await writeFixture(
"registry/r/foo.json",
"{ not json"
);
const diagnostics = await audit({
registryEntries: [entry],
componentDirs: ["foo"],
componentsDir,
});
expect(
diagnostics.find((d) => d.kind === "MALFORMED_ENTRY")?.message
).toMatch(/foo\.json/);
});
});

View File

@@ -0,0 +1,243 @@
#!/usr/bin/env bun
/**
* `check-registry-eject.ts` — shadcn-eject smoke gate for
* `@suaveplan/react-ui`.
*
* Verifies the shadcn `registry/r/*.json` tree against the package
* source so a downstream consumer running `shadcn add` always finds
* the files the registry promised. Specifically:
*
* 1. Every `registry/r/<name>.json` entry SHALL list at least one
* `files[]` path; every listed path SHALL resolve to an
* existing file under `src/components/<name>/` (or the
* configured `componentsDir`).
* 2. Every public component directory under `src/components/`
* that ships a `<name>.tsx` or `index.{ts,tsx}` SHALL have a
* corresponding `registry/r/<name>.json`. Drift in either
* direction (registry entry without source, or source without
* registry entry) fails the gate.
* 3. Every entry SHALL declare a stable `name` and `type` per the
* shadcn schema and SHALL parse as JSON.
*
* NOT in scope today (deferred until the registry transform itself
* lands as a callable helper — see `build-registry.ts` § "Future
* Wave 3 enhancements"):
*
* - Applying the transform that rewrites `@suaveplan/react-ui`
* imports to consumer-local relative paths.
* - Running `tsc --noEmit` against the transformed tree under a
* synthesized consumer `tsconfig.json` to verify the
* `@suaveplan/react-i18n` import contract survives ejection.
*
* Those gates will land alongside the transform itself; until then,
* `bun run typecheck` over the in-source files is the strongest
* signal we have that an ejected component would compile (the
* registry ships source verbatim today; the transform is a no-op).
*
* Wired into `bunx turbo lint`. Exits non-zero on any diagnostic.
*/
import { existsSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const PACKAGE_ROOT = resolve(
dirname(fileURLToPath(import.meta.url)),
".."
);
const REGISTRY_DIR = join(PACKAGE_ROOT, "registry", "r");
const COMPONENTS_DIR = join(PACKAGE_ROOT, "src", "components");
export interface RegistryFile {
readonly path: string;
readonly type: string;
}
export interface RegistryEntry {
readonly name: string;
readonly type: string;
readonly files: ReadonlyArray<RegistryFile>;
readonly registryDependencies?: ReadonlyArray<string>;
readonly dependencies?: ReadonlyArray<string>;
}
export type DiagnosticKind =
| "MISSING_REGISTRY_ENTRY"
| "MISSING_SOURCE_FILE"
| "MALFORMED_ENTRY"
| "EMPTY_FILES_LIST"
| "ORPHAN_REGISTRY_ENTRY";
export interface Diagnostic {
readonly kind: DiagnosticKind;
readonly message: string;
}
async function listRegistryEntryFiles(): Promise<string[]> {
if (!existsSync(REGISTRY_DIR)) return [];
const entries = await readdir(REGISTRY_DIR, { withFileTypes: true });
return entries
.filter(
(e) =>
e.isFile() &&
e.name.endsWith(".json") &&
e.name !== "index.json"
)
.map((e) => join(REGISTRY_DIR, e.name))
.sort();
}
async function listComponentDirectories(): Promise<string[]> {
if (!existsSync(COMPONENTS_DIR)) return [];
const entries = await readdir(COMPONENTS_DIR, { withFileTypes: true });
const out: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const sub = join(COMPONENTS_DIR, entry.name);
const hasIndex =
existsSync(join(sub, "index.ts")) ||
existsSync(join(sub, "index.tsx")) ||
existsSync(join(sub, `${entry.name}.tsx`));
if (hasIndex) out.push(entry.name);
}
return out.sort();
}
export async function loadEntry(
entryPath: string
): Promise<RegistryEntry | { readonly error: string }> {
try {
const raw = await readFile(entryPath, "utf8");
const parsed = JSON.parse(raw) as Partial<RegistryEntry>;
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
return { error: "missing or empty 'name'" };
}
if (typeof parsed.type !== "string") {
return { error: "missing 'type'" };
}
if (!Array.isArray(parsed.files)) {
return { error: "missing 'files' array" };
}
for (const f of parsed.files) {
if (
typeof f !== "object" ||
f === null ||
typeof (f as RegistryFile).path !== "string"
) {
return { error: "files[] entry missing 'path'" };
}
}
return parsed as RegistryEntry;
} catch (err) {
return { error: (err as Error).message };
}
}
export interface AuditOptions {
readonly registryEntries: ReadonlyArray<string>;
readonly componentDirs: ReadonlyArray<string>;
readonly componentsDir: string;
}
export async function audit(opts: AuditOptions): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = [];
const entryNames = new Set<string>();
for (const entryPath of opts.registryEntries) {
const entryRel = relative(PACKAGE_ROOT, entryPath);
const loaded = await loadEntry(entryPath);
if ("error" in loaded) {
diagnostics.push({
kind: "MALFORMED_ENTRY",
message: `${entryRel}: ${loaded.error}`,
});
continue;
}
const entryBase = entryPath.replace(/\.json$/, "").split("/").pop()!;
entryNames.add(entryBase);
if (loaded.files.length === 0) {
diagnostics.push({
kind: "EMPTY_FILES_LIST",
message: `${entryRel}: files[] is empty`,
});
continue;
}
for (const file of loaded.files) {
const sourcePath = join(opts.componentsDir, entryBase, file.path);
if (!existsSync(sourcePath)) {
diagnostics.push({
kind: "MISSING_SOURCE_FILE",
message: `${entryRel}: file '${file.path}' does not exist at ${relative(PACKAGE_ROOT, sourcePath)}`,
});
}
}
}
for (const dir of opts.componentDirs) {
if (!entryNames.has(dir)) {
diagnostics.push({
kind: "MISSING_REGISTRY_ENTRY",
message: `src/components/${dir}/ has no registry entry at registry/r/${dir}.json`,
});
}
}
for (const entryName of entryNames) {
if (!opts.componentDirs.includes(entryName)) {
diagnostics.push({
kind: "ORPHAN_REGISTRY_ENTRY",
message: `registry/r/${entryName}.json has no matching src/components/${entryName}/`,
});
}
}
diagnostics.sort((a, b) => {
if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
return a.message < b.message ? -1 : a.message > b.message ? 1 : 0;
});
return diagnostics;
}
async function main(): Promise<number> {
const registryEntries = await listRegistryEntryFiles();
const componentDirs = await listComponentDirectories();
if (registryEntries.length === 0) {
console.error(
`check-registry-eject: no registry entries found under ${relative(PACKAGE_ROOT, REGISTRY_DIR)} — run 'bun scripts/build-registry.ts' first.`
);
return 2;
}
if (componentDirs.length === 0) {
console.error(
`check-registry-eject: no component directories found under ${relative(PACKAGE_ROOT, COMPONENTS_DIR)}`
);
return 2;
}
const diagnostics = await audit({
registryEntries,
componentDirs,
componentsDir: COMPONENTS_DIR,
});
if (diagnostics.length === 0) {
console.log(
`check-registry-eject: clean — ${registryEntries.length} registry entries match ${componentDirs.length} component directories.`
);
return 0;
}
for (const d of diagnostics) {
console.error(`${d.kind} ${d.message}`);
}
console.error(
`check-registry-eject: ${diagnostics.length} diagnostic(s) — see above.`
);
return 1;
}
if (import.meta.main) {
process.exit(await main());
}

View File

@@ -0,0 +1,166 @@
# Accordion
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- accordion.variants — `accordionVariants`, `accordio…
- [accordion.variants](./accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
- [accordion.variants](./accordion.variants.md) — `accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionCont…
## Purpose
`<Accordion>`, `<AccordionItem>`, `<AccordionHeader>`, `<AccordionTrigger>`, and `<AccordionContent>` are the chassis multi-section disclosure primitives built on `@radix-ui/react-accordion`. Radix supplies the WAI-ARIA accordion semantics: each trigger renders a `<button>` inside an `<h3>` header with `aria-expanded` reflecting the open state and `aria-controls` referencing its content panel; sibling triggers participate in a roving tabindex so ArrowUp/ArrowDown move focus between them and Home/End jump to first/last. Space and Enter activate the focused trigger. The chassis layers cva slots for every part and rotates a `<ChevronDownIcon>` inside the trigger when the active item opens.
Use `type="single"` for radio-style accordions where one item opens at a time (`collapsible` lets the active item be re-clicked closed); use `type="multiple"` for checkbox-style accordions where any number of items may be open simultaneously. The two modes are a TypeScript discriminated union — `value` is `string` under `single` and `string[]` under `multiple` — and the chassis preserves the discrimination so consumer call sites get accurate autocomplete.
## Features
- WAI-ARIA accordion semantics auto-managed by Radix (trigger `aria-expanded` / `aria-controls`, header `<h3>` wrapper, roving tabindex across triggers).
- Keyboard navigation: ArrowUp / ArrowDown move between triggers, Home / End jump to first / last, Space / Enter activate the focused trigger.
- `type="single"` / `type="multiple"` discriminated union with mode-appropriate `value` / `defaultValue` types.
- Chevron icon (`<ChevronDownIcon>` from lucide) rendered inside the trigger; rotates 180deg on the open transition via the cva's `data-[state=open]>[data-icon='chevron']:rotate-180` selector.
- Smooth open/close animation via `--radix-accordion-content-height`; collapses to none under `prefers-reduced-motion: reduce`.
- Theme-driven cva overrides for `Accordion`, `AccordionItem`, `AccordionHeader`, `AccordionTrigger`, and `AccordionContent`.
- All sub-components forward refs to their underlying Radix elements.
- `disabled` at the Root level short-circuits all triggers; per-item `disabled` (Radix-native on `<AccordionItem>`) lets a single item be locked.
## Usage: Basic Example
```tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@suaveplan/react-ui";
export function Faq() {
return (
<Accordion type="single" collapsible>
<AccordionItem value="shipping">
<AccordionTrigger>How long does shipping take?</AccordionTrigger>
<AccordionContent>
Standard delivery is 3–5 business days.
</AccordionContent>
</AccordionItem>
<AccordionItem value="returns">
<AccordionTrigger>What is the returns policy?</AccordionTrigger>
<AccordionContent>
Items can be returned within 30 days for a full refund.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
```
## Usage: Advanced Example
```tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@suaveplan/react-ui";
import { useState } from "react";
interface Section {
readonly id: string;
readonly title: string;
readonly body: React.ReactNode;
}
interface MultiAccordionProps {
readonly sections: ReadonlyArray<Section>;
readonly defaultOpenIds?: ReadonlyArray<string>;
readonly onOpenChange?(openIds: ReadonlyArray<string>): void;
}
export function MultiAccordion({
sections,
defaultOpenIds,
onOpenChange,
}: MultiAccordionProps) {
const [openIds, setOpenIds] = useState<string[]>([
...(defaultOpenIds ?? []),
]);
function handleChange(next: string[]): void {
setOpenIds(next);
onOpenChange?.(next);
}
return (
<Accordion
type="multiple"
value={openIds}
onValueChange={handleChange}
className="rounded-md border"
>
{sections.map((section) => (
<AccordionItem key={section.id} value={section.id}>
<AccordionTrigger>{section.title}</AccordionTrigger>
<AccordionContent>{section.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}
```
## API Reference
### `<Accordion>`
Renders the Radix Root that owns the open-set state machine.
| Prop | Type | Default | Description |
|---|---|---|---|
| `type` | `"single" \| "multiple"` | _required_ | Discriminator; controls the shape of `value` / `defaultValue` and whether siblings can be open at once. |
| `value` | `string` (single) \| `string[]` (multiple) | _uncontrolled_ | Controlled open set. |
| `defaultValue` | `string` (single) \| `string[]` (multiple) | _none_ | Initial open set for uncontrolled use. |
| `onValueChange` | `(value: string) => void` (single) \| `(value: string[]) => void` (multiple) | _none_ | Fires on every open / close transition. |
| `collapsible` | `boolean` | `false` | When `type="single"`, lets the active item be re-clicked closed. |
| `disabled` | `boolean` | `false` | Disables every trigger. |
| `dir` | `"ltr" \| "rtl"` | inherited | Reading direction; affects ArrowLeft / ArrowRight handling on horizontal accordions. |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` | Layout orientation; vertical (default) responds to ArrowUp / ArrowDown, horizontal to ArrowLeft / ArrowRight. |
### `<AccordionItem>`
Wraps the Radix Item; carries a unique `value` string used by the Root to discriminate the open set.
| Prop | Type | Default | Description |
|---|---|---|---|
| `value` | `string` | _required_ | Unique identifier within this Accordion. |
| `disabled` | `boolean` | `false` | Disables the trigger inside this item. |
### `<AccordionTrigger>`
Wraps the Radix Trigger and renders the chevron icon. Always sits inside an `<AccordionItem>`.
### `<AccordionHeader>`
The `<h3>` wrapper around the trigger. `<AccordionTrigger>` already wraps itself in this header — render `<AccordionHeader>` directly only when you need a custom heading element.
### `<AccordionContent>`
Wraps the Radix Content. Children unmount when closed (Radix default). The chassis pads the content with `pb-3 pt-0` so the body sits below the trigger without an awkward gap above.
## Implementation Notes
- The Radix Root is a discriminated union on `type`; the chassis surface preserves the union via `AccordionProps = ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>`. Internally we cast through `unknown` because TypeScript cannot prove the spread preserves the discrimination — the runtime accepts both shapes equivalently.
- The chevron icon is rendered as a sibling of the trigger label so the `data-[state=open]>[data-icon='chevron']:rotate-180` cva selector can flip it without escaping the cva surface.
- The chassis NEVER assigns a numeric `z-index` (per the shadcn skill rule). The accordion sits in normal flow.
- The animation classes (`animate-collapsible-down/up`) are reused from the Collapsible cva so a single set of keyframes covers both primitives.
- Per WAI-ARIA, the trigger is wrapped in an `<h3>` header so screen-readers announce the item as a heading. Consumers may render a custom `<AccordionHeader>` to change the heading level when nested inside a heading hierarchy.

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import * as Stories from "./accordion.stories.js";
const STORY_KEYS = [
"Default",
"Playground",
"Variants",
"States",
"WithI18n",
"WithRTL",
"WithTheme",
"A11y",
"Composition",
] as const;
type StoryKey = (typeof STORY_KEYS)[number];
interface StoryRecord {
readonly args?: Record<string, unknown>;
readonly render?: (args: Record<string, unknown>) => React.ReactNode;
}
describe("accordion.stories", () => {
it("exports every Layout story", { timeout: 5000 }, () => {
for (const key of STORY_KEYS) {
expect(
(Stories as unknown as Record<string, unknown>)[key],
`missing story export: ${key}`
).toBeDefined();
}
});
for (const key of STORY_KEYS) {
it(`renders the ${key} story without throwing`, {
timeout: 10000,
}, () => {
const story = (Stories as unknown as Record<StoryKey, StoryRecord>)[
key
];
expect(story).toBeDefined();
const render = story.render;
expect(typeof render).toBe("function");
if (render === undefined) return;
const tree = render(story.args ?? {});
const ui = (
<div data-testid="story-host">{tree as React.ReactNode}</div>
);
const { container } = renderComponent(ui);
expect(
container.querySelector("[data-testid='story-host']")
).not.toBeNull();
});
}
});

View File

@@ -0,0 +1,388 @@
/**
* Storybook stories for `<Accordion>`.
*
* `@storybook/react` is NOT a dep of `@suaveplan/react-ui`; story-meta
* shapes are inlined.
*/
import {
createI18n as createI18nNarrow,
type I18nInstance,
} from "@suaveplan/i18n/plugin";
import { TranslationProvider } from "@suaveplan/react-i18n";
import type { ReactElement, ReactNode } from "react";
import type { SupportedLocale } from "../../locales/locales.types.js";
import type { ThemePlugin } from "../../theme/theme.types.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { darkTheme } from "../../theme/themes/dark.js";
import { highContrastTheme } from "../../theme/themes/high-contrast.js";
import { lightTheme } from "../../theme/themes/light.js";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./accordion.js";
import type { AccordionProps } from "./accordion.types.js";
function createI18n(
options: Parameters<typeof createI18nNarrow>[0]
): I18nInstance {
return createI18nNarrow(options) as unknown as I18nInstance;
}
interface Meta<T> {
readonly component: T;
readonly title: string;
readonly tags?: readonly string[];
readonly parameters?: Record<string, unknown>;
readonly argTypes?: Record<string, unknown>;
}
interface StoryObj<TArgs> {
readonly args?: Partial<TArgs>;
readonly render?: (args: Partial<TArgs>) => ReactNode;
readonly name?: string;
readonly parameters?: Record<string, unknown>;
}
const meta: Meta<unknown> = {
component: undefined as unknown as never,
title: "react-ui/Layout/Accordion",
tags: ["autodocs"],
argTypes: {
type: {
control: { type: "select" },
options: ["single", "multiple"],
},
collapsible: { control: { type: "boolean" } },
disabled: { control: { type: "boolean" } },
},
};
export default meta;
type Story = StoryObj<AccordionProps>;
interface ProviderShellProps {
readonly locale: SupportedLocale;
readonly themes: ReadonlyArray<ThemePlugin>;
readonly defaultTheme: string;
readonly children: ReactNode;
}
function ProviderShellSync(props: ProviderShellProps): ReactElement {
const i18n = createI18n({
defaultLocale: props.locale,
fallbackLocale: "en-GB",
});
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider
themes={props.themes}
defaultTheme={props.defaultTheme}
>
{props.children}
</ThemeProvider>
</TranslationProvider>
);
}
const FAQ_ITEMS = [
{
id: "shipping",
title: "How long does shipping take?",
body: "Standard delivery is 3–5 business days.",
},
{
id: "returns",
title: "What is the returns policy?",
body: "Items can be returned within 30 days for a full refund.",
},
{
id: "support",
title: "How do I contact support?",
body: "Email support@example.com or use the in-app chat.",
},
] as const;
export const Default: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 480 }}>
<Accordion type="single" collapsible>
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</ProviderShellSync>
),
};
export const Playground: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 480 }}>
{/* The cast through `unknown` is necessary because TypeScript
cannot prove that the playground args satisfy the
discriminated union shape. */}
<Accordion {...(args as unknown as AccordionProps)}>
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</ProviderShellSync>
),
args: { type: "single", collapsible: true, disabled: false },
};
export const Variants: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="grid grid-cols-2 gap-4" style={{ width: 720 }}>
<div>
<p className="mb-2 text-sm font-medium">
single + collapsible
</p>
<Accordion
type="single"
collapsible
defaultValue="shipping"
>
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>
{item.title}
</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
<div>
<p className="mb-2 text-sm font-medium">multiple</p>
<Accordion
type="multiple"
defaultValue={["shipping", "returns"]}
>
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>
{item.title}
</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
</ProviderShellSync>
),
};
export const States: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-4" style={{ width: 480 }}>
<Accordion type="single" collapsible defaultValue="shipping">
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
<Accordion type="single" collapsible disabled>
<AccordionItem value="locked">
<AccordionTrigger>Disabled accordion</AccordionTrigger>
<AccordionContent>Cannot toggle.</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</ProviderShellSync>
),
};
const LOCALES: ReadonlyArray<SupportedLocale> = [
"en-GB",
"zh-CN",
"ar-AE",
"cy-GB",
"ru-RU",
];
export const WithI18n: Story = {
render: () => (
<div className="flex flex-col gap-3" style={{ width: 480 }}>
{LOCALES.map((locale) => (
<ProviderShellSync
key={locale}
locale={locale}
themes={[lightTheme]}
defaultTheme="light"
>
<div dir={locale === "ar-AE" ? "rtl" : "ltr"}>
<p className="text-xs">{locale}</p>
<Accordion type="single" collapsible>
<AccordionItem value="one">
<AccordionTrigger>
{locale} — 1
</AccordionTrigger>
<AccordionContent>Body</AccordionContent>
</AccordionItem>
<AccordionItem value="two">
<AccordionTrigger>
{locale} — 2
</AccordionTrigger>
<AccordionContent>Body</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</ProviderShellSync>
))}
</div>
),
};
export const WithRTL: Story = {
render: () => (
<div dir="rtl" style={{ width: 480 }}>
<ProviderShellSync
locale="ar-AE"
themes={[lightTheme]}
defaultTheme="light"
>
<Accordion type="single" collapsible defaultValue="one">
<AccordionItem value="one">
<AccordionTrigger>
ما هي سياسة الإرجاع؟
</AccordionTrigger>
<AccordionContent>
يمكنك إرجاع المنتجات خلال 30 يومًا.
</AccordionContent>
</AccordionItem>
<AccordionItem value="two">
<AccordionTrigger>
كيف أتواصل مع الدعم؟
</AccordionTrigger>
<AccordionContent>
راسلنا عبر البريد الإلكتروني.
</AccordionContent>
</AccordionItem>
</Accordion>
</ProviderShellSync>
</div>
),
};
const THEMES: ReadonlyArray<{
readonly label: string;
readonly theme: ThemePlugin;
}> = [
{ label: "light", theme: lightTheme },
{ label: "dark", theme: darkTheme },
{ label: "highContrast", theme: highContrastTheme },
];
export const WithTheme: Story = {
render: () => (
<div className="grid grid-cols-3 gap-3" style={{ width: 720 }}>
{THEMES.map(({ label, theme }) => (
<ProviderShellSync
key={label}
locale="en-GB"
themes={[theme]}
defaultTheme={theme.name}
>
<div>
<p className="text-xs">{label}</p>
<Accordion type="single" collapsible defaultValue="one">
<AccordionItem value="one">
<AccordionTrigger>Item 1</AccordionTrigger>
<AccordionContent>Body</AccordionContent>
</AccordionItem>
<AccordionItem value="two">
<AccordionTrigger>Item 2</AccordionTrigger>
<AccordionContent>Body</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</ProviderShellSync>
))}
</div>
),
};
export const A11y: Story = {
parameters: {
a11y: {
element: "#storybook-root",
config: {
rules: [{ id: "color-contrast", enabled: true }],
},
},
},
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 480 }}>
<Accordion type="single" collapsible>
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</ProviderShellSync>
),
};
export const Composition: Story = {
name: "Composition (multi-mode FAQ)",
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 480 }}>
<Accordion type="multiple" defaultValue={["shipping"]}>
{FAQ_ITEMS.map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionContent>{item.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</ProviderShellSync>
),
};

View File

@@ -0,0 +1,300 @@
import { TranslationProvider } from "@suaveplan/react-i18n";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import { createRef } from "react";
import { createTestI18n } from "../../../tests/setup/test-i18n.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { lightTheme } from "../../theme/themes/light.js";
import {
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from "./accordion.js";
import {
accordionContentVariants,
accordionHeaderVariants,
accordionItemVariants,
accordionTriggerVariants,
accordionVariants,
} from "./accordion.variants.js";
async function withProviders(
node: React.ReactElement
): Promise<React.ReactElement> {
const i18n = await createTestI18n({ locale: "en-GB" });
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
{node}
</ThemeProvider>
</TranslationProvider>
);
}
function ThreeItem({ open }: { open?: string } = {}): React.ReactElement {
return (
<Accordion type="single" collapsible defaultValue={open}>
<AccordionItem value="one">
<AccordionTrigger>Trigger one</AccordionTrigger>
<AccordionContent>Content one</AccordionContent>
</AccordionItem>
<AccordionItem value="two">
<AccordionTrigger>Trigger two</AccordionTrigger>
<AccordionContent>Content two</AccordionContent>
</AccordionItem>
<AccordionItem value="three">
<AccordionTrigger>Trigger three</AccordionTrigger>
<AccordionContent>Content three</AccordionContent>
</AccordionItem>
</Accordion>
);
}
describe("<Accordion>", () => {
it("renders all triggers and no content by default", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, queryByText } = renderComponent(tree);
expect(getByText("Trigger one")).not.toBeNull();
expect(getByText("Trigger two")).not.toBeNull();
expect(queryByText("Content one")).toBeNull();
});
it("opens the first item when its trigger is clicked", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, findByText, user } = renderComponent(tree);
await user.click(getByText("Trigger one"));
expect(await findByText("Content one")).not.toBeNull();
});
it("closes the open item when re-clicked (single+collapsible)", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem open="one" />);
const { getByText, queryByText, user } = renderComponent(tree);
expect(queryByText("Content one")).not.toBeNull();
await user.click(getByText("Trigger one"));
expect(queryByText("Content one")).toBeNull();
});
it("closes the previous item when a new item opens (single mode)", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem open="one" />);
const { getByText, queryByText, findByText, user } =
renderComponent(tree);
expect(queryByText("Content one")).not.toBeNull();
await user.click(getByText("Trigger two"));
expect(await findByText("Content two")).not.toBeNull();
expect(queryByText("Content one")).toBeNull();
});
it("supports type=multiple keeping siblings open simultaneously", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Accordion type="multiple" defaultValue={["one"]}>
<AccordionItem value="one">
<AccordionTrigger>Trigger one</AccordionTrigger>
<AccordionContent>Content one</AccordionContent>
</AccordionItem>
<AccordionItem value="two">
<AccordionTrigger>Trigger two</AccordionTrigger>
<AccordionContent>Content two</AccordionContent>
</AccordionItem>
</Accordion>
);
const { getByText, findByText, queryByText, user } =
renderComponent(tree);
await user.click(getByText("Trigger two"));
expect(await findByText("Content two")).not.toBeNull();
expect(queryByText("Content one")).not.toBeNull();
});
it("toggles via Enter key on the trigger", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, findByText, user } = renderComponent(tree);
const trigger = getByText("Trigger one") as HTMLButtonElement;
trigger.focus();
await user.keyboard("{Enter}");
expect(await findByText("Content one")).not.toBeNull();
});
it("toggles via Space key on the trigger", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, findByText, user } = renderComponent(tree);
const trigger = getByText("Trigger two") as HTMLButtonElement;
trigger.focus();
await user.keyboard(" ");
expect(await findByText("Content two")).not.toBeNull();
});
it("ArrowDown moves focus to the next trigger", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, user } = renderComponent(tree);
const first = getByText("Trigger one") as HTMLButtonElement;
first.focus();
await user.keyboard("{ArrowDown}");
expect(document.activeElement?.textContent).toContain("Trigger two");
});
it("ArrowUp moves focus to the previous trigger", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, user } = renderComponent(tree);
const second = getByText("Trigger two") as HTMLButtonElement;
second.focus();
await user.keyboard("{ArrowUp}");
expect(document.activeElement?.textContent).toContain("Trigger one");
});
it("Home jumps to the first trigger", { timeout: 5000 }, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, user } = renderComponent(tree);
const third = getByText("Trigger three") as HTMLButtonElement;
third.focus();
await user.keyboard("{Home}");
expect(document.activeElement?.textContent).toContain("Trigger one");
});
it("End jumps to the last trigger", { timeout: 5000 }, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, user } = renderComponent(tree);
const first = getByText("Trigger one") as HTMLButtonElement;
first.focus();
await user.keyboard("{End}");
expect(document.activeElement?.textContent).toContain("Trigger three");
});
it("renders the chevron icon inside the trigger", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { container } = renderComponent(tree);
const icons = container.querySelectorAll("[data-icon='chevron']");
expect(icons.length).toBeGreaterThan(0);
});
it("aria-expanded reflects the open state per item", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { getByText, user } = renderComponent(tree);
const trigger = getByText("Trigger one") as HTMLButtonElement;
expect(trigger.getAttribute("aria-expanded")).toBe("false");
await user.click(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("true");
});
it("respects disabled at the Root level", { timeout: 5000 }, async () => {
const tree = await withProviders(
<Accordion type="single" collapsible disabled>
<AccordionItem value="one">
<AccordionTrigger>Trigger one</AccordionTrigger>
<AccordionContent>Content one</AccordionContent>
</AccordionItem>
</Accordion>
);
const { getByText, queryByText, user } = renderComponent(tree);
await user.click(getByText("Trigger one"));
expect(queryByText("Content one")).toBeNull();
});
it("renders the AccordionHeader as an h3 by default", {
timeout: 5000,
}, async () => {
const tree = await withProviders(<ThreeItem />);
const { container } = renderComponent(tree);
const headers = container.querySelectorAll("h3");
expect(headers.length).toBeGreaterThan(0);
});
it("forwards refs on the Root and Item slots", {
timeout: 5000,
}, async () => {
const rootRef = createRef<HTMLDivElement>();
const itemRef = createRef<HTMLDivElement>();
const triggerRef = createRef<HTMLButtonElement>();
const contentRef = createRef<HTMLDivElement>();
const headerRef = createRef<HTMLHeadingElement>();
const tree = await withProviders(
<Accordion
type="single"
collapsible
defaultValue="one"
ref={rootRef}
>
<AccordionItem value="one" ref={itemRef}>
<AccordionHeader ref={headerRef}>
<span>x</span>
</AccordionHeader>
<AccordionTrigger ref={triggerRef}>
Trigger
</AccordionTrigger>
<AccordionContent ref={contentRef}>Body</AccordionContent>
</AccordionItem>
</Accordion>
);
renderComponent(tree);
expect(rootRef.current).not.toBeNull();
expect(itemRef.current).not.toBeNull();
expect(triggerRef.current).not.toBeNull();
expect(contentRef.current).not.toBeNull();
expect(headerRef.current).not.toBeNull();
});
it("merges custom className on every slot", { timeout: 5000 }, async () => {
const tree = await withProviders(
<Accordion
type="single"
collapsible
defaultValue="one"
className="root-extra"
data-testid="root"
>
<AccordionItem
value="one"
className="item-extra"
data-testid="item"
>
<AccordionTrigger className="trigger-extra">
Trigger
</AccordionTrigger>
<AccordionContent
className="content-extra"
data-testid="content"
>
Body
</AccordionContent>
</AccordionItem>
</Accordion>
);
const { getByText, getByTestId } = renderComponent(tree);
expect(getByTestId("root").className).toContain("root-extra");
expect(getByTestId("item").className).toContain("item-extra");
expect((getByText("Trigger") as HTMLButtonElement).className).toContain(
"trigger-extra"
);
expect(getByTestId("content").className).toContain("content-extra");
});
it("exposes every accordion cva resolver", { timeout: 5000 }, () => {
expect(accordionVariants().length).toBeGreaterThan(0);
expect(accordionItemVariants().length).toBeGreaterThan(0);
expect(accordionTriggerVariants().length).toBeGreaterThan(0);
expect(accordionHeaderVariants().length).toBeGreaterThan(0);
expect(accordionContentVariants().length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,208 @@
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import {
type ComponentProps,
type ComponentRef,
type ForwardRefExoticComponent,
forwardRef,
type ReactElement,
type RefAttributes,
} from "react";
import { useThemedVariants } from "../../theme/use-themed-variants/use-themed-variants.js";
import { cn } from "../../utils/cn/cn.js";
import type {
AccordionContentProps,
AccordionHeaderProps,
AccordionItemProps,
AccordionProps,
AccordionTriggerProps,
} from "./accordion.types.js";
import {
accordionContentVariants,
accordionHeaderVariants,
accordionItemVariants,
accordionTriggerVariants,
accordionVariants,
} from "./accordion.variants.js";
/**
* `<Accordion>` — chassis multi-section disclosure built on
* `@radix-ui/react-accordion`.
*
* Radix supplies the WAI-ARIA accordion semantics: each trigger renders
* a `<button>` inside an `<h3>` header with `aria-expanded` reflecting
* the open state and `aria-controls` referencing its content panel.
* Keyboard navigation: ArrowUp/ArrowDown move between triggers, Home
* jumps to first, End jumps to last; Space and Enter activate the
* focused trigger.
*
* The chassis layers cva slots for every part and rotates a chevron
* icon when the active item opens (via the
* `data-[state=open]>[data-icon='chevron']:rotate-180` selector inside
* `accordionTriggerVariants`).
*
* Use `type="single"` for radio-style accordions where one panel opens
* at a time (`collapsible` lets the active panel be re-clicked closed);
* use `type="multiple"` for checkbox-style accordions where any number
* of panels can be open simultaneously.
*/
export const Accordion = forwardRef<
ComponentRef<typeof AccordionPrimitive.Root>,
AccordionProps
>(function Accordion(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants("Accordion", accordionVariants);
// Radix Root is a discriminated union on `type` (`single` requires a
// `value: string`, `multiple` requires `value: string[]`). Our
// surface accepts the union directly via `AccordionProps`, but
// spreading it back into the Radix component erases the narrowing.
// Cast via `unknown` to a relaxed shape since the runtime accepts
// both shapes equivalently — the consumer-facing typing remains the
// discriminated union.
const Root =
AccordionPrimitive.Root as unknown as ForwardRefExoticComponent<
ComponentProps<"div"> & {
readonly type?: "single" | "multiple";
readonly value?: string | string[];
readonly defaultValue?: string | string[];
readonly onValueChange?: (value: never) => void;
readonly collapsible?: boolean;
readonly disabled?: boolean;
} & RefAttributes<HTMLDivElement>
>;
return <Root ref={ref} className={cn(variants(), className)} {...rest} />;
});
Accordion.displayName = "Accordion";
/**
* `<AccordionItem>` — single item inside an `<Accordion>`. Each item
* carries a unique `value` string used by the Root's `value` /
* `defaultValue` to identify which item is open. Radix unmounts the
* content when closed by default.
*/
export const AccordionItem = forwardRef<
ComponentRef<typeof AccordionPrimitive.Item>,
AccordionItemProps
>(function AccordionItem(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants("AccordionItem", accordionItemVariants);
return (
<AccordionPrimitive.Item
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AccordionItem.displayName = "AccordionItem";
/**
* `<AccordionHeader>` — the `<h3>` wrapper around the trigger. Most
* consumers will not render this directly — `<AccordionTrigger>` wraps
* itself in this header automatically. Exposed for parity with
* upstream so consumers who want a custom header element can compose
* around it.
*/
export const AccordionHeader = forwardRef<
ComponentRef<typeof AccordionPrimitive.Header>,
AccordionHeaderProps
>(function AccordionHeader(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AccordionHeader",
accordionHeaderVariants
);
return (
<AccordionPrimitive.Header
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AccordionHeader.displayName = "AccordionHeader";
/**
* `<AccordionTrigger>` — the `<button>` that toggles its parent item.
* The chassis renders a `<ChevronDownIcon>` from lucide as a sibling
* of the trigger label; the icon rotates 180deg on the open transition
* via the cva's `data-[state=open]` selector.
*
* Always render the trigger inside an `<AccordionItem>`. Radix wraps
* the trigger in an `<h3>` header automatically, so consumers do not
* need to supply one.
*/
export const AccordionTrigger = forwardRef<
ComponentRef<typeof AccordionPrimitive.Trigger>,
AccordionTriggerProps
>(function AccordionTrigger(props, ref): ReactElement {
const { className, children, ...rest } = props;
const variants = useThemedVariants(
"AccordionTrigger",
accordionTriggerVariants
);
const headerVariants = useThemedVariants(
"AccordionHeader",
accordionHeaderVariants
);
return (
<AccordionPrimitive.Header className={headerVariants()}>
<AccordionPrimitive.Trigger
ref={ref}
className={cn(variants(), className)}
{...rest}
>
{children}
<ChevronDownIcon
className="size-4 shrink-0"
data-icon="chevron"
aria-hidden
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
});
AccordionTrigger.displayName = "AccordionTrigger";
/**
* `<AccordionContent>` — the panel that mounts when its item is open.
* Animates open/close via `--radix-accordion-content-height`; collapses
* to none under `prefers-reduced-motion: reduce`.
*
* Forwards refs to the rendered `<div>`.
*/
export const AccordionContent = forwardRef<
ComponentRef<typeof AccordionPrimitive.Content>,
AccordionContentProps
>(function AccordionContent(props, ref): ReactElement {
const { className, children, ...rest } = props;
const variants = useThemedVariants(
"AccordionContent",
accordionContentVariants
);
return (
<AccordionPrimitive.Content
ref={ref}
className={cn(variants(), className)}
{...rest}
>
<div className="pb-3 pt-0">{children}</div>
</AccordionPrimitive.Content>
);
});
AccordionContent.displayName = "AccordionContent";
export {
accordionContentVariants,
accordionHeaderVariants,
accordionItemVariants,
accordionTriggerVariants,
accordionVariants,
};

View File

@@ -0,0 +1,74 @@
import type * as AccordionPrimitive from "@radix-ui/react-accordion";
import type { ComponentPropsWithoutRef } from "react";
import type {
accordionContentVariants,
accordionHeaderVariants,
accordionItemVariants,
accordionTriggerVariants,
accordionVariants,
} from "./accordion.variants.js";
/**
* Module augmentation registering every Accordion cva slot into
* `ThemableVariants` so `useThemedVariants` is type-narrowed and a
* `ThemePlugin` may declare overrides.
*/
declare module "../../theme/theme.types.js" {
interface ThemableVariants {
readonly Accordion: typeof accordionVariants;
readonly AccordionItem: typeof accordionItemVariants;
readonly AccordionTrigger: typeof accordionTriggerVariants;
readonly AccordionHeader: typeof accordionHeaderVariants;
readonly AccordionContent: typeof accordionContentVariants;
}
}
/**
* Public props for `<Accordion>`.
*
* Wraps the Radix Root which accepts `type: "single" | "multiple"` —
* `single` exposes a string `value` plus an optional `collapsible` flag
* that lets the active item be re-clicked to close; `multiple` exposes
* a `string[]` value (sibling items may be open simultaneously).
*
* `RadixRootProps` is the discriminated-union shape Radix exports — we
* preserve the discrimination so consumer call sites get autocomplete
* on `value: string` (single) vs `value: string[]` (multiple) without
* losing the `type` narrowing.
*/
export type AccordionProps = ComponentPropsWithoutRef<
typeof AccordionPrimitive.Root
>;
/**
* Public props for `<AccordionItem>` — wraps the Radix Item. Each item
* carries a unique `value` string used as the discriminator inside the
* Root's `value` / `defaultValue` state.
*/
export interface AccordionItemProps
extends ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> {}
/**
* Public props for `<AccordionHeader>` — wraps the Radix Header which
* renders an `<h3>` by default for correct heading semantics.
*/
export interface AccordionHeaderProps
extends ComponentPropsWithoutRef<typeof AccordionPrimitive.Header> {}
/**
* Public props for `<AccordionTrigger>` — wraps the Radix Trigger
* which renders a `<button>` by default. The chassis renders the
* trigger with a chevron sibling that rotates on the open transition
* (driven by the `data-[state=open]>[data-icon='chevron']:rotate-180`
* cva selector).
*/
export interface AccordionTriggerProps
extends ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> {}
/**
* Public props for `<AccordionContent>` — wraps the Radix Content
* which mounts/unmounts children on the open/close cycle and animates
* via `--radix-accordion-content-height`.
*/
export interface AccordionContentProps
extends ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> {}

View File

@@ -0,0 +1,83 @@
# Accordion Variants (cva config)
## Purpose
`accordionVariants`, `accordionItemVariants`, `accordionHeaderVariants`, `accordionTriggerVariants`, and `accordionContentVariants` are the cva (class-variance-authority) configurations powering every `<Accordion>` slot. They are split from `accordion.tsx` into their own module so an ejected source copy under `npx shadcn@latest add @suaveplan/accordion` keeps the cva configurations in a dedicated file. Theme overrides flow through `useThemedVariants("Accordion<Slot>")`.
The root cva stacks items vertically with no gap so per-item bottom borders form a continuous separator stroke. The trigger cva carries focus / hover styling and the chevron-rotation hook. The content cva matches the Collapsible animation recipe so a single set of keyframes covers both primitives.
## Features
- Root: `flex flex-col` so items stack vertically.
- Item: `border-b last:border-b-0` so adjacent items appear connected; the final item drops the bottom stroke.
- Header: `flex` with default heading margins stripped via the Radix render path.
- Trigger: full-width flex row with end-aligned chevron, `hover:underline`, `focus-visible:ring-2`, `disabled:opacity-50`. The `[&[data-state=open]>[data-icon='chevron']]:rotate-180` selector flips the chevron on the open transition; `[&>[data-icon='chevron']]:transition-transform` smooths the rotation.
- Content: `overflow-hidden text-sm` plus `data-[state=open]:animate-collapsible-down` / `data-[state=closed]:animate-collapsible-up` so Radix's `--radix-accordion-content-height` CSS variable animates cleanly. `motion-reduce:animate-none` collapses the transition under `prefers-reduced-motion: reduce`.
- NEVER carries a numeric `z-index` (the accordion sits in normal flow).
## Usage: Basic Example
```ts
import {
accordionContentVariants,
accordionTriggerVariants,
accordionVariants,
} from "@suaveplan/react-ui";
const rootClassName = accordionVariants();
const triggerClassName = accordionTriggerVariants();
const contentClassName = accordionContentVariants();
```
## Usage: Advanced Example
```ts
import { accordionTriggerVariants, cn } from "@suaveplan/react-ui";
export function CustomTrigger() {
return (
<button
type="button"
aria-expanded
data-state="closed"
className={cn(
accordionTriggerVariants(),
"rounded bg-muted px-3"
)}
>
Custom row
<span data-icon="chevron">⌄</span>
</button>
);
}
```
## API Reference
### `accordionVariants(): string`
Returns the root grouping classes; takes no arguments.
### `accordionItemVariants(): string`
Returns the per-item border classes; takes no arguments.
### `accordionHeaderVariants(): string`
Returns the `<h3>` wrapper classes; takes no arguments.
### `accordionTriggerVariants(): string`
Returns the trigger button classes (including the chevron rotation selector); takes no arguments.
### `accordionContentVariants(): string`
Returns the content panel classes; takes no arguments.
## Implementation Notes
- The chevron rotation is implemented via a Tailwind arbitrary-variant selector (`[&[data-state=open]>[data-icon='chevron']]:rotate-180`) so the trigger does not need to render its own state-aware element. Any sibling carrying `data-icon="chevron"` will rotate.
- The trigger's `motion-reduce` rule covers BOTH the trigger transition AND the chevron rotation (`motion-reduce:[&>[data-icon='chevron']]:transition-none`) so reduced-motion users see no animation at all.
- The content cva is intentionally identical to `collapsibleContentVariants` so an editor switching between Accordion and Collapsible sees the same animation behaviour.
- Per the shadcn skill rule, the cva NEVER assigns a numeric `z-index`. The accordion renders inline in the document flow.
- The `last:border-b-0` rule prevents a double-border problem when the accordion sits inside a bordered container.

View File

@@ -0,0 +1,91 @@
import { defineVariants } from "../../variants/variants.js";
/**
* `accordionVariants` — base cva for the `<Accordion>` root.
*
* Stacks `<AccordionItem>` children vertically with no gap so the
* borders between items appear as a continuous separator stroke.
*/
export const accordionVariants = defineVariants("Accordion", "flex flex-col", {
variants: {},
defaultVariants: {},
});
/**
* `accordionItemVariants` — base cva for `<AccordionItem>`.
*
* Each item carries a bottom border so adjacent items appear connected.
* The final item drops the border via `last:border-b-0` so the bottom
* stroke doesn't double up against the surrounding container.
*/
export const accordionItemVariants = defineVariants(
"AccordionItem",
"border-b last:border-b-0",
{
variants: {},
defaultVariants: {},
}
);
/**
* `accordionTriggerVariants` — base cva for `<AccordionTrigger>`.
*
* Renders as a flex row with the label on the start side and a chevron
* on the end side. `data-[state=open]:rotate-180` rotates a sibling
* `[data-icon='chevron']` element on the open transition. Focus-visible
* ring matches the chassis Button family.
*/
export const accordionTriggerVariants = defineVariants(
"AccordionTrigger",
[
"flex w-full items-center justify-between gap-2",
"py-3 text-sm font-medium",
"transition-colors hover:underline",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-50",
"[&[data-state=open]>[data-icon='chevron']]:rotate-180",
"[&>[data-icon='chevron']]:transition-transform",
"motion-reduce:transition-none",
"motion-reduce:[&>[data-icon='chevron']]:transition-none",
].join(" "),
{
variants: {},
defaultVariants: {},
}
);
/**
* `accordionHeaderVariants` — base cva for `<AccordionHeader>`.
*
* Wraps the trigger in an `<h3>` per the Radix default. The cva strips
* the heading's default margins so the row sits flush.
*/
export const accordionHeaderVariants = defineVariants(
"AccordionHeader",
"flex",
{
variants: {},
defaultVariants: {},
}
);
/**
* `accordionContentVariants` — base cva for `<AccordionContent>`.
*
* Carries the open/close animation hooks driven by Radix's
* `data-state="open"` / `"closed"` attribute. Animates via
* `--radix-accordion-content-height` (mirrors the Collapsible recipe).
*/
export const accordionContentVariants = defineVariants(
"AccordionContent",
[
"overflow-hidden text-sm",
"data-[state=closed]:animate-collapsible-up",
"data-[state=open]:animate-collapsible-down",
"motion-reduce:animate-none",
].join(" "),
{
variants: {},
defaultVariants: {},
}
);

View File

@@ -0,0 +1,19 @@
export {
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
accordionContentVariants,
accordionHeaderVariants,
accordionItemVariants,
accordionTriggerVariants,
accordionVariants,
} from "./accordion.js";
export type {
AccordionContentProps,
AccordionHeaderProps,
AccordionItemProps,
AccordionProps,
AccordionTriggerProps,
} from "./accordion.types.js";

View File

@@ -0,0 +1,29 @@
{
"componentDirName": "accordion",
"exports": {
"values": [
"Accordion",
"AccordionContent",
"AccordionHeader",
"AccordionItem",
"AccordionTrigger",
"accordionContentVariants",
"accordionHeaderVariants",
"accordionItemVariants",
"accordionTriggerVariants",
"accordionVariants"
],
"types": [
"AccordionContentProps",
"AccordionHeaderProps",
"AccordionItemProps",
"AccordionProps",
"AccordionTriggerProps"
]
},
"subpathExports": ["./components/accordion/accordion"],
"catalogDeps": {
"@radix-ui/react-accordion": "^1.2.12"
},
"translationKeys": {}
}

View File

@@ -0,0 +1,163 @@
# AlertDialog
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- alert-dialog.variants — `alertDialogOverlayVariants…
- [alert-dialog.variants](./alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
- [alert-dialog.variants](./alert-dialog.variants.md) — `alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `…
## Purpose
`<AlertDialog>` and its sub-components (`<AlertDialogTrigger>`, `<AlertDialogPortal>`, `<AlertDialogOverlay>`, `<AlertDialogContent>`, `<AlertDialogHeader>`, `<AlertDialogFooter>`, `<AlertDialogTitle>`, `<AlertDialogDescription>`, `<AlertDialogAction>`, `<AlertDialogCancel>`) are the chassis modal CONFIRMATION dialog primitives built on `@radix-ui/react-alert-dialog`.
The structural difference between `<AlertDialog>` and `<Dialog>` is the click-outside-suppression: an alert dialog CANNOT be dismissed by clicking the overlay backdrop — the user MUST press the `<AlertDialogAction>` or `<AlertDialogCancel>` button. Use `<AlertDialog>` for destructive operations (delete / discard / log out) where an accidental backdrop click could lose work; use `<Dialog>` for everything else.
Per the WAI-ARIA alertdialog pattern, every alert dialog MUST contain an `<AlertDialogTitle>` so the accessible name is announced. Use `className="sr-only"` if the visible UI does not need a heading. Radix renders `role="alertdialog"` (NOT `role="dialog"`) so screen readers know to announce the prompt as a high-priority interrupt.
## Features
- `role="alertdialog"` (Radix-managed) so screen readers announce a high-priority prompt.
- Click-outside dismissal SUPPRESSED — only Action / Cancel resolve the dialog.
- Escape closes (treated as cancel) — Radix-managed.
- Focus-trap on open; restore-focus to the trigger on close.
- Four content sizes (`sm`, `default`, `lg`, `xl`) scale the panel max-width.
- `<AlertDialogAction>` defaults to Button `variant="default"` styling (primary).
- `<AlertDialogCancel>` defaults to Button `variant="outline"` styling (secondary).
- Theme-driven cva overrides for every slot via `useThemedVariants("AlertDialog<Slot>")`.
- All sub-components forward refs.
- `motion-reduce:animate-none` collapses animations under `prefers-reduced-motion: reduce`.
## Usage: Basic Example
```tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@suaveplan/react-ui";
export function ConfirmDelete() {
return (
<AlertDialog>
<AlertDialogTrigger>Delete account</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
```
## Usage: Advanced Example
```tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@suaveplan/react-ui";
import { useState } from "react";
export function ConfirmDestructive({
title,
description,
onConfirm,
}: {
readonly title: string;
readonly description: string;
readonly onConfirm: () => Promise<void>;
}) {
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
async function handleConfirm() {
setBusy(true);
try {
await onConfirm();
setOpen(false);
} finally {
setBusy(false);
}
}
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={busy}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm} disabled={busy}>
{busy ? "Working…" : "Confirm"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
```
## API Reference
### `<AlertDialog>`
Wraps the Radix AlertDialog Root. Same lifecycle props as `<Dialog>` (`open`, `defaultOpen`, `onOpenChange`).
### `<AlertDialogContent>`
| Prop | Type | Default | Description |
|---|---|---|---|
| `size` | `"sm" \| "default" \| "lg" \| "xl"` | `"default"` | Visual variant scaling the panel max-width. |
### `<AlertDialogTitle>`
REQUIRED for accessibility. Render `className="sr-only"` if no visible heading is needed.
### `<AlertDialogAction>`, `<AlertDialogCancel>`
Renders as buttons with the default Button `variant="default"` and `variant="outline"` styling respectively. Activating either resolves the dialog (closes it).
### `<AlertDialogTrigger>`, `<AlertDialogPortal>`, `<AlertDialogOverlay>`, `<AlertDialogHeader>`, `<AlertDialogFooter>`, `<AlertDialogDescription>`
Direct re-exports of the Radix sub-components with the chassis cva applied.
## Implementation Notes
- The chassis NEVER assigns a numeric `z-index` — Radix portals handle stacking.
- The Action / Cancel button cva is INLINED into AlertDialog rather than reusing `<Button>` so an ejected source copy stays self-contained (matches the shadcn upstream `alert-dialog` recipe).
- `<AlertDialogContent>` self-portals via `<AlertDialogPortal>` so consumers do not need to compose the portal manually.
- The Cancel button uses `mt-2 sm:mt-0` so it stacks beneath the Action on small screens (Action becomes the visually emphasised primary-end position) and aligns horizontally above `sm`.
- Radix's a11y warning fires when no `<AlertDialogDescription>` is provided either — the chassis does not enforce this since some single-line confirmations only need a title; consumers can pass `aria-describedby={undefined}` on `<AlertDialogContent>` if they want to suppress the warning explicitly.

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import * as Stories from "./alert-dialog.stories.js";
const STORY_KEYS = [
"Default",
"Playground",
"Variants",
"States",
"WithI18n",
"WithRTL",
"WithTheme",
"A11y",
"Composition",
] as const;
type StoryKey = (typeof STORY_KEYS)[number];
interface StoryRecord {
readonly args?: Record<string, unknown>;
readonly render?: (args: Record<string, unknown>) => React.ReactNode;
}
describe("alert-dialog.stories", () => {
it("exports every Overlays story", { timeout: 5000 }, () => {
for (const key of STORY_KEYS) {
expect(
(Stories as unknown as Record<string, unknown>)[key],
`missing story export: ${key}`
).toBeDefined();
}
});
for (const key of STORY_KEYS) {
it(`renders the ${key} story without throwing`, {
timeout: 10000,
}, () => {
const story = (Stories as unknown as Record<StoryKey, StoryRecord>)[
key
];
expect(story).toBeDefined();
const render = story.render;
expect(typeof render).toBe("function");
if (render === undefined) return;
const tree = render(story.args ?? {});
const ui = (
<div data-testid="story-host">{tree as React.ReactNode}</div>
);
const { container } = renderComponent(ui);
expect(
container.querySelector("[data-testid='story-host']")
).not.toBeNull();
});
}
});

View File

@@ -0,0 +1,366 @@
/**
* Storybook stories for `<AlertDialog>`.
*/
import {
createI18n as createI18nNarrow,
type I18nInstance,
} from "@suaveplan/i18n/plugin";
import { TranslationProvider } from "@suaveplan/react-i18n";
import type { ReactElement, ReactNode } from "react";
import type { SupportedLocale } from "../../locales/locales.types.js";
import type { ThemePlugin } from "../../theme/theme.types.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { darkTheme } from "../../theme/themes/dark.js";
import { highContrastTheme } from "../../theme/themes/high-contrast.js";
import { lightTheme } from "../../theme/themes/light.js";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "./alert-dialog.js";
import type { AlertDialogContentProps } from "./alert-dialog.types.js";
function createI18n(
options: Parameters<typeof createI18nNarrow>[0]
): I18nInstance {
return createI18nNarrow(options) as unknown as I18nInstance;
}
interface Meta<T> {
readonly component: T;
readonly title: string;
readonly tags?: readonly string[];
readonly parameters?: Record<string, unknown>;
readonly argTypes?: Record<string, unknown>;
}
interface StoryObj<TArgs> {
readonly args?: Partial<TArgs>;
readonly render?: (args: Partial<TArgs>) => ReactNode;
readonly name?: string;
readonly parameters?: Record<string, unknown>;
}
const meta: Meta<unknown> = {
component: undefined as unknown as never,
title: "react-ui/Overlays/AlertDialog",
tags: ["autodocs"],
argTypes: {
size: {
control: { type: "select" },
options: ["sm", "default", "lg", "xl"],
},
},
};
export default meta;
type Story = StoryObj<AlertDialogContentProps>;
interface ProviderShellProps {
readonly locale: SupportedLocale;
readonly themes: ReadonlyArray<ThemePlugin>;
readonly defaultTheme: string;
readonly children: ReactNode;
}
function ProviderShellSync(props: ProviderShellProps): ReactElement {
const i18n = createI18n({
defaultLocale: props.locale,
fallbackLocale: "en-GB",
});
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider
themes={props.themes}
defaultTheme={props.defaultTheme}
>
{props.children}
</ThemeProvider>
</TranslationProvider>
);
}
export const Default: Story = {
render: (_args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog>
<AlertDialogTrigger>Delete account</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
),
};
export const Playground: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog defaultOpen>
<AlertDialogContent {...args}>
<AlertDialogHeader>
<AlertDialogTitle>Heading</AlertDialogTitle>
<AlertDialogDescription>
Body text describing the action.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
),
args: { size: "default" },
};
export const Variants: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-3">
{(["sm", "default", "lg", "xl"] as const).map((size) => (
<AlertDialog key={size}>
<AlertDialogTrigger>{size}</AlertDialogTrigger>
<AlertDialogContent size={size}>
<AlertDialogTitle>{size}</AlertDialogTitle>
<AlertDialogDescription>
Confirmation at size {size}.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
))}
</div>
</ProviderShellSync>
),
};
export const States: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Open by default</AlertDialogTitle>
<AlertDialogDescription>
The user MUST press Cancel or OK to dismiss.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
),
};
const LOCALES: ReadonlyArray<SupportedLocale> = [
"en-GB",
"zh-CN",
"ar-AE",
"cy-GB",
"ru-RU",
];
export const WithI18n: Story = {
render: () => (
<div className="flex flex-col gap-3">
{LOCALES.map((locale) => (
<div
key={locale}
dir={locale === "ar-AE" ? "rtl" : "ltr"}
className="flex items-center gap-2"
>
<span style={{ width: 80 }}>{locale}</span>
<ProviderShellSync
locale={locale}
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>{locale}</AlertDialogTitle>
<AlertDialogDescription>
{locale}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const WithRTL: Story = {
render: () => (
<div dir="rtl">
<ProviderShellSync
locale="ar-AE"
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogTitle>عنوان</AlertDialogTitle>
<AlertDialogDescription>وصف</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>إلغاء</AlertDialogCancel>
<AlertDialogAction>تأكيد</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
</div>
),
};
const THEMES: ReadonlyArray<{
readonly label: string;
readonly theme: ThemePlugin;
}> = [
{ label: "light", theme: lightTheme },
{ label: "dark", theme: darkTheme },
{ label: "highContrast", theme: highContrastTheme },
];
export const WithTheme: Story = {
render: () => (
<div className="flex flex-col gap-3">
{THEMES.map(({ label, theme }) => (
<div key={label} className="flex items-center gap-2 p-3 border">
<span style={{ width: 120 }}>{label}</span>
<ProviderShellSync
locale="en-GB"
themes={[theme]}
defaultTheme={theme.name}
>
<AlertDialog>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>{label}</AlertDialogTitle>
<AlertDialogDescription>
Theme {label}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const A11y: Story = {
parameters: {
a11y: {
element: "#storybook-root",
config: {
rules: [
{ id: "color-contrast", enabled: true },
{ id: "aria-dialog-name", enabled: true },
],
},
},
},
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
<AlertDialogDescription>
Unsaved changes will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep editing</AlertDialogCancel>
<AlertDialogAction>Discard</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
),
};
export const Composition: Story = {
name: "Composition (delete confirmation)",
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<AlertDialog>
<AlertDialogTrigger>Delete project</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Delete project</AlertDialogTitle>
<AlertDialogDescription>
All files will be permanently removed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ProviderShellSync>
),
};

View File

@@ -0,0 +1,258 @@
import { TranslationProvider } from "@suaveplan/react-i18n";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import { createRef } from "react";
import { createTestI18n } from "../../../tests/setup/test-i18n.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { lightTheme } from "../../theme/themes/light.js";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "./alert-dialog.js";
import {
alertDialogActionVariants,
alertDialogCancelVariants,
alertDialogContentVariants,
alertDialogDescriptionVariants,
alertDialogFooterVariants,
alertDialogHeaderVariants,
alertDialogOverlayVariants,
alertDialogTitleVariants,
} from "./alert-dialog.variants.js";
async function withProviders(
node: React.ReactElement
): Promise<React.ReactElement> {
const i18n = await createTestI18n({ locale: "en-GB" });
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
{node}
</ThemeProvider>
</TranslationProvider>
);
}
describe("<AlertDialog>", () => {
it("opens via the trigger and renders alertdialog role", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AlertDialog>
<AlertDialogTrigger>Delete</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Confirm</AlertDialogTitle>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
);
const { getByText, findByRole, user } = renderComponent(tree);
await user.click(getByText("Delete"));
const alertDialog = await findByRole("alertdialog");
expect(alertDialog).not.toBeNull();
});
it("Escape closes the dialog (cancel)", { timeout: 5000 }, async () => {
const tree = await withProviders(
<AlertDialog>
<AlertDialogTrigger>Delete</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Confirm</AlertDialogTitle>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
);
const { getByText, findByRole, queryByRole, user } =
renderComponent(tree);
const trigger = getByText("Delete") as HTMLElement;
await user.click(trigger);
await findByRole("alertdialog");
await user.keyboard("{Escape}");
expect(queryByRole("alertdialog")).toBeNull();
expect(document.activeElement).toBe(trigger);
});
it("AlertDialogAction closes the dialog when activated", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogTitle>Heading</AlertDialogTitle>
<AlertDialogDescription>Body</AlertDialogDescription>
<AlertDialogAction>Confirm</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
);
const { findByRole, queryByRole, user } = renderComponent(tree);
const action = await findByRole("button", { name: "Confirm" });
await user.click(action);
expect(queryByRole("alertdialog")).toBeNull();
});
it("AlertDialogCancel closes the dialog when activated", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogTitle>Confirm</AlertDialogTitle>
<AlertDialogCancel>Cancel</AlertDialogCancel>
</AlertDialogContent>
</AlertDialog>
);
const { findByText, queryByRole, user } = renderComponent(tree);
await user.click(await findByText("Cancel"));
expect(queryByRole("alertdialog")).toBeNull();
});
it("does NOT close on overlay backdrop click (modal confirmation)", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogTitle>Modal</AlertDialogTitle>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
);
const { findByRole, queryByRole, user } = renderComponent(tree);
const dialog = await findByRole("alertdialog");
// Find the overlay (sibling of the content) and click it.
const overlay = dialog.parentElement?.querySelector(
"[data-state]:not([role])"
) as HTMLElement | null;
if (overlay !== null) {
await user.click(overlay);
}
expect(queryByRole("alertdialog")).not.toBeNull();
});
it("forwards refs on every sub-component", { timeout: 5000 }, async () => {
const triggerRef = createRef<HTMLButtonElement>();
const contentRef = createRef<HTMLDivElement>();
const titleRef = createRef<HTMLHeadingElement>();
const descRef = createRef<HTMLParagraphElement>();
const headerRef = createRef<HTMLDivElement>();
const footerRef = createRef<HTMLDivElement>();
const actionRef = createRef<HTMLButtonElement>();
const cancelRef = createRef<HTMLButtonElement>();
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogTrigger ref={triggerRef}>Open</AlertDialogTrigger>
<AlertDialogContent ref={contentRef}>
<AlertDialogHeader ref={headerRef}>
<AlertDialogTitle ref={titleRef}>T</AlertDialogTitle>
<AlertDialogDescription ref={descRef}>
D
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter ref={footerRef}>
<AlertDialogCancel ref={cancelRef}>C</AlertDialogCancel>
<AlertDialogAction ref={actionRef}>A</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
renderComponent(tree);
expect(triggerRef.current).not.toBeNull();
expect(contentRef.current).not.toBeNull();
expect(titleRef.current).not.toBeNull();
expect(descRef.current).not.toBeNull();
expect(headerRef.current).not.toBeNull();
expect(footerRef.current).not.toBeNull();
expect(actionRef.current).not.toBeNull();
expect(cancelRef.current).not.toBeNull();
});
it("merges custom className", { timeout: 5000 }, async () => {
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogContent className="c-extra">
<AlertDialogHeader className="h-extra">
<AlertDialogTitle className="t-extra">
T
</AlertDialogTitle>
<AlertDialogDescription className="d-extra">
D
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="f-extra">
<AlertDialogAction className="a-extra">
OK
</AlertDialogAction>
<AlertDialogCancel className="x-extra">
Cancel
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
const { findByRole } = renderComponent(tree);
const dialog = await findByRole("alertdialog");
expect(dialog.className).toContain("c-extra");
expect(dialog.querySelector(".h-extra")).not.toBeNull();
expect(dialog.querySelector(".t-extra")).not.toBeNull();
expect(dialog.querySelector(".d-extra")).not.toBeNull();
expect(dialog.querySelector(".f-extra")).not.toBeNull();
expect(dialog.querySelector(".a-extra")).not.toBeNull();
expect(dialog.querySelector(".x-extra")).not.toBeNull();
});
it("applies the size variant to AlertDialogContent", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogContent size="xl">
<AlertDialogTitle>T</AlertDialogTitle>
</AlertDialogContent>
</AlertDialog>
);
const { findByRole } = renderComponent(tree);
const dialog = await findByRole("alertdialog");
expect(dialog.className).toContain("max-w-4xl");
});
it("renders Cancel + Action buttons with their canonical styling", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AlertDialog defaultOpen>
<AlertDialogContent>
<AlertDialogTitle>T</AlertDialogTitle>
<AlertDialogAction>OK</AlertDialogAction>
<AlertDialogCancel>Cancel</AlertDialogCancel>
</AlertDialogContent>
</AlertDialog>
);
const { findByText } = renderComponent(tree);
const action = await findByText("OK");
const cancel = await findByText("Cancel");
expect(action.className).toContain("bg-primary");
expect(cancel.className).toContain("border");
});
it("exposes every alert-dialog cva resolver", { timeout: 5000 }, () => {
expect(alertDialogOverlayVariants().length).toBeGreaterThan(0);
for (const size of ["sm", "default", "lg", "xl"] as const) {
expect(alertDialogContentVariants({ size }).length).toBeGreaterThan(
0
);
}
expect(alertDialogHeaderVariants().length).toBeGreaterThan(0);
expect(alertDialogFooterVariants().length).toBeGreaterThan(0);
expect(alertDialogTitleVariants().length).toBeGreaterThan(0);
expect(alertDialogDescriptionVariants().length).toBeGreaterThan(0);
expect(alertDialogActionVariants().length).toBeGreaterThan(0);
expect(alertDialogCancelVariants().length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,259 @@
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { type ComponentRef, forwardRef, type ReactElement } from "react";
import { useThemedVariants } from "../../theme/use-themed-variants/use-themed-variants.js";
import { cn } from "../../utils/cn/cn.js";
import type {
AlertDialogActionProps,
AlertDialogCancelProps,
AlertDialogContentProps,
AlertDialogDescriptionProps,
AlertDialogFooterProps,
AlertDialogHeaderProps,
AlertDialogOverlayProps,
AlertDialogPortalProps,
AlertDialogProps,
AlertDialogTitleProps,
AlertDialogTriggerProps,
} from "./alert-dialog.types.js";
import {
alertDialogActionVariants,
alertDialogCancelVariants,
alertDialogContentVariants,
alertDialogDescriptionVariants,
alertDialogFooterVariants,
alertDialogHeaderVariants,
alertDialogOverlayVariants,
alertDialogTitleVariants,
} from "./alert-dialog.variants.js";
/**
* `<AlertDialog>` — chassis modal confirmation dialog built on
* `@radix-ui/react-alert-dialog`.
*
* Distinct from `<Dialog>`: AlertDialog suppresses the click-outside
* dismissal so the user MUST press the Action or Cancel button to
* resolve the prompt. Useful for destructive operations (delete /
* discard) where an accidental backdrop click could lose work.
*
* Radix supplies the WAI-ARIA semantics — focus-trap, restore-focus,
* Escape closes (treated as cancel), and the role="alertdialog"
* announcement.
*/
export const AlertDialog = (props: AlertDialogProps): ReactElement => (
<AlertDialogPrimitive.Root {...props} />
);
AlertDialog.displayName = "AlertDialog";
/**
* `<AlertDialogTrigger>` — opens the alert dialog when activated.
*/
export const AlertDialogTrigger = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Trigger>,
AlertDialogTriggerProps
>(function AlertDialogTrigger(props, ref): ReactElement {
return <AlertDialogPrimitive.Trigger ref={ref} {...props} />;
});
AlertDialogTrigger.displayName = "AlertDialogTrigger";
/**
* `<AlertDialogPortal>` — renders the alert dialog into the body.
*/
export const AlertDialogPortal = (
props: AlertDialogPortalProps
): ReactElement => <AlertDialogPrimitive.Portal {...props} />;
AlertDialogPortal.displayName = "AlertDialogPortal";
/**
* `<AlertDialogOverlay>` — translucent backdrop. Click-outside is
* suppressed by Radix.
*/
export const AlertDialogOverlay = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Overlay>,
AlertDialogOverlayProps
>(function AlertDialogOverlay(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogOverlay",
alertDialogOverlayVariants
);
return (
<AlertDialogPrimitive.Overlay
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AlertDialogOverlay.displayName = "AlertDialogOverlay";
/**
* `<AlertDialogContent>` — the panel. Self-portals via
* `<AlertDialogPortal>`. REQUIRED to contain `<AlertDialogTitle>`.
*/
export const AlertDialogContent = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Content>,
AlertDialogContentProps
>(function AlertDialogContent(props, ref): ReactElement {
const { className, size, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogContent",
alertDialogContentVariants
);
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(variants({ size }), className)}
{...rest}
/>
</AlertDialogPortal>
);
});
AlertDialogContent.displayName = "AlertDialogContent";
/**
* `<AlertDialogHeader>` — flex column wrapping title and description.
*/
export const AlertDialogHeader = forwardRef<
HTMLDivElement,
AlertDialogHeaderProps
>(function AlertDialogHeader(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogHeader",
alertDialogHeaderVariants
);
return <div ref={ref} className={cn(variants(), className)} {...rest} />;
});
AlertDialogHeader.displayName = "AlertDialogHeader";
/**
* `<AlertDialogFooter>` — flex container for the Action / Cancel
* pair.
*/
export const AlertDialogFooter = forwardRef<
HTMLDivElement,
AlertDialogFooterProps
>(function AlertDialogFooter(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogFooter",
alertDialogFooterVariants
);
return <div ref={ref} className={cn(variants(), className)} {...rest} />;
});
AlertDialogFooter.displayName = "AlertDialogFooter";
/**
* `<AlertDialogTitle>` — REQUIRED for accessibility. Render
* `className="sr-only"` if no visible heading is needed.
*/
export const AlertDialogTitle = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Title>,
AlertDialogTitleProps
>(function AlertDialogTitle(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogTitle",
alertDialogTitleVariants
);
return (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AlertDialogTitle.displayName = "AlertDialogTitle";
/**
* `<AlertDialogDescription>` — supplementary description text.
*/
export const AlertDialogDescription = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Description>,
AlertDialogDescriptionProps
>(function AlertDialogDescription(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogDescription",
alertDialogDescriptionVariants
);
return (
<AlertDialogPrimitive.Description
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AlertDialogDescription.displayName = "AlertDialogDescription";
/**
* `<AlertDialogAction>` — the primary confirm button. Defaults to
* Button `variant="default"` styling.
*/
export const AlertDialogAction = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Action>,
AlertDialogActionProps
>(function AlertDialogAction(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogAction",
alertDialogActionVariants
);
return (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AlertDialogAction.displayName = "AlertDialogAction";
/**
* `<AlertDialogCancel>` — the cancel button. Defaults to Button
* `variant="outline"` styling.
*/
export const AlertDialogCancel = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Cancel>,
AlertDialogCancelProps
>(function AlertDialogCancel(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDialogCancel",
alertDialogCancelVariants
);
return (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AlertDialogCancel.displayName = "AlertDialogCancel";
export {
alertDialogActionVariants,
alertDialogCancelVariants,
alertDialogContentVariants,
alertDialogDescriptionVariants,
alertDialogFooterVariants,
alertDialogHeaderVariants,
alertDialogOverlayVariants,
alertDialogTitleVariants,
};

View File

@@ -0,0 +1,118 @@
import type * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import type { ComponentPropsWithoutRef, HTMLAttributes } from "react";
import type { VariantProps } from "../../variants/variants.js";
import type {
alertDialogActionVariants,
alertDialogCancelVariants,
alertDialogContentVariants,
alertDialogDescriptionVariants,
alertDialogFooterVariants,
alertDialogHeaderVariants,
alertDialogOverlayVariants,
alertDialogTitleVariants,
} from "./alert-dialog.variants.js";
/**
* Module augmentation registering every AlertDialog cva slot into
* `ThemableVariants`.
*/
declare module "../../theme/theme.types.js" {
interface ThemableVariants {
readonly AlertDialogOverlay: typeof alertDialogOverlayVariants;
readonly AlertDialogContent: typeof alertDialogContentVariants;
readonly AlertDialogHeader: typeof alertDialogHeaderVariants;
readonly AlertDialogFooter: typeof alertDialogFooterVariants;
readonly AlertDialogTitle: typeof alertDialogTitleVariants;
readonly AlertDialogDescription: typeof alertDialogDescriptionVariants;
readonly AlertDialogAction: typeof alertDialogActionVariants;
readonly AlertDialogCancel: typeof alertDialogCancelVariants;
}
}
export type AlertDialogContentVariantProps = VariantProps<
typeof alertDialogContentVariants
>;
/**
* Public props for `<AlertDialog>` — wraps the Radix AlertDialog Root.
*
* Distinct from Dialog: AlertDialog is a modal CONFIRMATION dialog
* that CANNOT be dismissed by clicking the overlay backdrop. The user
* MUST press the Action or Cancel button.
*/
export interface AlertDialogProps
extends ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Root> {}
/**
* Public props for `<AlertDialogTrigger>` — opens the alert dialog.
*/
export interface AlertDialogTriggerProps
extends ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger> {}
/**
* Public props for `<AlertDialogPortal>` — renders the alert dialog
* into the body.
*/
export interface AlertDialogPortalProps
extends ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Portal> {}
/**
* Public props for `<AlertDialogOverlay>` — translucent backdrop.
* Click-outside is suppressed by Radix.
*/
export interface AlertDialogOverlayProps
extends Omit<
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>,
"asChild"
> {}
/**
* Public props for `<AlertDialogContent>` — the dialog panel. REQUIRED
* to contain `<AlertDialogTitle>`.
*/
export interface AlertDialogContentProps
extends Omit<
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>,
"asChild"
>,
AlertDialogContentVariantProps {}
/**
* Public props for `<AlertDialogHeader>` / `<AlertDialogFooter>`.
*/
export interface AlertDialogHeaderProps
extends HTMLAttributes<HTMLDivElement> {}
export interface AlertDialogFooterProps
extends HTMLAttributes<HTMLDivElement> {}
/**
* Public props for `<AlertDialogTitle>` — REQUIRED for accessibility.
*/
export interface AlertDialogTitleProps
extends Omit<
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>,
"asChild"
> {}
/**
* Public props for `<AlertDialogDescription>`.
*/
export interface AlertDialogDescriptionProps
extends Omit<
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>,
"asChild"
> {}
/**
* Public props for `<AlertDialogAction>` — the primary confirm button.
* Defaults to Button `variant="default"` styling.
*/
export interface AlertDialogActionProps
extends ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> {}
/**
* Public props for `<AlertDialogCancel>` — the cancel button. Defaults
* to Button `variant="outline"` styling.
*/
export interface AlertDialogCancelProps
extends ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> {}

View File

@@ -0,0 +1,69 @@
# AlertDialog Variants (cva config)
## Purpose
`alertDialogOverlayVariants`, `alertDialogContentVariants`, `alertDialogHeaderVariants`, `alertDialogFooterVariants`, `alertDialogTitleVariants`, `alertDialogDescriptionVariants`, `alertDialogActionVariants`, and `alertDialogCancelVariants` are the cva configurations powering every `<AlertDialog>` slot. They are split from `alert-dialog.tsx` into their own module so an ejected source copy under `npx shadcn@latest add @suaveplan/alert-dialog` keeps the cva configurations in a dedicated file. Theme overrides flow through `useThemedVariants("AlertDialog<Slot>")`.
The Action and Cancel button cva configurations duplicate the Button `variant="default"` / `variant="outline"` recipes so the alert dialog stays self-contained — an ejected copy does not require pulling in `<Button>` separately.
## Features
- Overlay: `fixed inset-0 bg-black/80` matching Dialog/Sheet visual language.
- Content: same centred panel + four sizes (`sm` / `default` / `lg` / `xl`) as Dialog, allowing consumers to scale the confirmation surface for short vs verbose copy.
- Header / footer: matched to Dialog so visual cadence stays identical.
- Title: `text-lg font-semibold` (no `tracking-tight` so confirmation prompts sit slightly looser than narrative dialogs).
- Description: muted-foreground text-sm.
- Action button: `bg-primary text-primary-foreground hover:bg-primary/90` (Button `variant="default"` recipe).
- Cancel button: `border border-input bg-background hover:bg-accent` (Button `variant="outline"` recipe), with `mt-2 sm:mt-0` for small-screen stacking.
## Usage: Basic Example
```ts
import {
alertDialogContentVariants,
alertDialogActionVariants,
alertDialogCancelVariants,
} from "@suaveplan/react-ui";
const contentClassName = alertDialogContentVariants({ size: "sm" });
const actionClassName = alertDialogActionVariants();
const cancelClassName = alertDialogCancelVariants();
```
## Usage: Advanced Example
```ts
import { alertDialogContentVariants, cn } from "@suaveplan/react-ui";
export function CustomAlert() {
return (
<div
role="alertdialog"
aria-modal="true"
className={cn(
alertDialogContentVariants({ size: "sm" }),
"border-2 border-destructive"
)}
>
…
</div>
);
}
```
## API Reference
- `alertDialogOverlayVariants(): string`
- `alertDialogContentVariants(props?: { size?: "sm" | "default" | "lg" | "xl" }): string`
- `alertDialogHeaderVariants(): string`
- `alertDialogFooterVariants(): string`
- `alertDialogTitleVariants(): string`
- `alertDialogDescriptionVariants(): string`
- `alertDialogActionVariants(): string`
- `alertDialogCancelVariants(): string`
## Implementation Notes
- The Action / Cancel cva duplicate the Button cva recipes intentionally — keeping the alert dialog self-contained means ejecting the alert dialog source via `npx shadcn add` does not implicitly require ejecting `<Button>` as well. Consumers who want a unified visual language can override the Action cva via theme `variantOverrides.AlertDialogAction`.
- Per the shadcn skill rule, the cva NEVER assigns a numeric `z-index`. Radix's `<Portal>` mounts the content above all page content via DOM placement.
- The Cancel `mt-2 sm:mt-0` ensures Cancel stacks beneath the Action on mobile (Action becomes visually prominent, aligning with the accept-as-default action) and horizontally aligns above `sm`.

View File

@@ -0,0 +1,131 @@
import { defineVariants } from "../../variants/variants.js";
/**
* `alertDialogOverlayVariants` — translucent backdrop drawn behind the
* alert dialog. Identical to Dialog's overlay; AlertDialog's distinct
* behaviour is the click-outside-suppression (Radix-managed) rather
* than a visual difference.
*/
export const alertDialogOverlayVariants = defineVariants(
"AlertDialogOverlay",
[
"fixed inset-0 bg-black/80",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"motion-reduce:animate-none",
].join(" "),
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDialogContentVariants` — centred panel for confirmation
* dialogs. Same `size` axis as Dialog so consumers can scale the
* confirmation surface for short vs verbose copy.
*/
export const alertDialogContentVariants = defineVariants(
"AlertDialogContent",
[
"fixed left-[50%] top-[50%] grid w-full",
"translate-x-[-50%] translate-y-[-50%] gap-4",
"border bg-background p-6 shadow-lg",
"duration-200 sm:rounded-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"motion-reduce:animate-none",
].join(" "),
{
variants: {
size: {
sm: "max-w-sm",
default: "max-w-lg",
lg: "max-w-2xl",
xl: "max-w-4xl",
},
},
defaultVariants: {
size: "default",
},
}
);
/**
* `alertDialogHeaderVariants` — flex column wrapping title and
* description.
*/
export const alertDialogHeaderVariants = defineVariants(
"AlertDialogHeader",
"flex flex-col gap-2 text-center sm:text-start",
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDialogFooterVariants` — flex container for the
* Action/Cancel pair.
*/
export const alertDialogFooterVariants = defineVariants(
"AlertDialogFooter",
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDialogTitleVariants` — the alert heading. REQUIRED for a11y.
*/
export const alertDialogTitleVariants = defineVariants(
"AlertDialogTitle",
"text-lg font-semibold",
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDialogDescriptionVariants` — supplementary description text
* below the title.
*/
export const alertDialogDescriptionVariants = defineVariants(
"AlertDialogDescription",
"text-sm text-muted-foreground",
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDialogActionVariants` — primary button shape (matches Button
* `variant="default"`).
*/
export const alertDialogActionVariants = defineVariants(
"AlertDialogAction",
[
"inline-flex items-center justify-center whitespace-nowrap",
"rounded-md text-sm font-medium",
"bg-primary text-primary-foreground shadow",
"transition-colors duration-150 ease-out",
"hover:bg-primary/90",
"focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50",
"h-9 px-4 py-2",
"motion-reduce:transition-none",
].join(" "),
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDialogCancelVariants` — outlined secondary button shape
* (matches Button `variant="outline"`). RTL-safe via `mt-2 sm:mt-0`.
*/
export const alertDialogCancelVariants = defineVariants(
"AlertDialogCancel",
[
"inline-flex items-center justify-center whitespace-nowrap",
"rounded-md text-sm font-medium",
"border border-input bg-background shadow-sm",
"transition-colors duration-150 ease-out",
"hover:bg-accent hover:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50",
"h-9 px-4 py-2 mt-2 sm:mt-0",
"motion-reduce:transition-none",
].join(" "),
{ variants: {}, defaultVariants: {} }
);

View File

@@ -0,0 +1,35 @@
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
alertDialogActionVariants,
alertDialogCancelVariants,
alertDialogContentVariants,
alertDialogDescriptionVariants,
alertDialogFooterVariants,
alertDialogHeaderVariants,
alertDialogOverlayVariants,
alertDialogTitleVariants,
} from "./alert-dialog.js";
export type {
AlertDialogActionProps,
AlertDialogCancelProps,
AlertDialogContentProps,
AlertDialogContentVariantProps,
AlertDialogDescriptionProps,
AlertDialogFooterProps,
AlertDialogHeaderProps,
AlertDialogOverlayProps,
AlertDialogPortalProps,
AlertDialogProps,
AlertDialogTitleProps,
AlertDialogTriggerProps,
} from "./alert-dialog.types.js";

View File

@@ -0,0 +1,125 @@
# Alert
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - alert.variants — `alertVariants` is the cva config……
- [alert.variants](./alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- alert.variants — `alertVariants` is the cva config……
- [alert.variants](./alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- alert.variants — `alertVariants` is the cva config……
- [alert.variants](./alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- alert.variants — `alertVariants` is the cva config…
- [alert.variants](./alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
- [alert.variants](./alert.variants.md) — `alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four ru…
## Purpose
`<Alert>` is the chassis callout for inline messages, errors, and notices. It ships as a family of four components — `<Alert>` (root), `<AlertTitle>`, `<AlertDescription>`, `<AlertIcon>` — that compose into the conventional shadcn alert layout. Per the shadcn skill rule, ALWAYS use `<Alert>` for callouts rather than building custom styled divs (`<div className="border-l-4 border-yellow-500 ...">`) — hand-rolled callouts diverge across teams in colour scale, iconography, accessibility hooks, and dark-mode treatment.
The chassis renders `role="alert"` on the root so screen-readers announce the message. The `variant` axis swaps semantic colour pairs for four rungs (`default` / `destructive` / `success` / `warning`), and `<AlertIcon>` selects the matching lucide-react icon (`InfoIcon` / `AlertCircleIcon` / `CheckCircle2Icon` / `AlertTriangleIcon`) for the variant.
## Features
- Four colour variants via the `variant` axis (`default` / `destructive` / `success` / `warning`).
- Four composition primitives — `Alert`, `AlertTitle`, `AlertDescription`, `AlertIcon`.
- `role="alert"` automatically applied so screen-readers announce the message.
- `data-variant` attribute on the rendered root so tests / consumer CSS can target alerts by variant.
- `[&>svg]:` Tailwind arbitrary modifiers position any direct svg child as an inline icon — no consumer flex setup required.
- `<AlertTitle>` renders as `<h5>` so it nests under page-level headings without disrupting the document outline.
- `<AlertDescription>` renders as `<div>` so consumers can compose paragraphs / lists / inline elements without nesting block elements inside a `<p>`.
- All sub-components forward refs to their underlying DOM nodes.
## Usage: Basic Example
```tsx
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
} from "@suaveplan/react-ui";
export function PolicyChange() {
return (
<Alert variant="warning">
<AlertIcon variant="warning" />
<AlertTitle>Policy update</AlertTitle>
<AlertDescription>
Your team's retention policy has been updated.
</AlertDescription>
</Alert>
);
}
```
## Usage: Advanced Example
```tsx
import {
Alert,
AlertDescription,
AlertTitle,
Button,
} from "@suaveplan/react-ui";
import { ZapIcon } from "lucide-react";
export function CustomIconAlert() {
return (
<Alert variant="success">
<ZapIcon className="size-4" aria-hidden="true" />
<AlertTitle>Sync complete</AlertTitle>
<AlertDescription>
<p>523 records synchronised in 1.4 seconds.</p>
<Button variant="outline" size="sm" className="mt-2">
View report
</Button>
</AlertDescription>
</Alert>
);
}
```
## API Reference
### `<Alert>`
`forwardRef<HTMLDivElement, AlertProps>` — root container. Renders `role="alert"` and `data-variant`.
| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | `"default" \| "destructive" \| "success" \| "warning"` | `"default"` | Colour palette. |
| _all_ `HTMLAttributes<HTMLDivElement>` | _native_ | _native_ | Forwarded as-is. |
### `<AlertTitle>`
`forwardRef<HTMLHeadingElement, AlertTitleProps>` — heading rendered as `<h5>`.
### `<AlertDescription>`
`forwardRef<HTMLDivElement, AlertDescriptionProps>` — body text. Rendered as `<div>` to allow nested block elements.
### `<AlertIcon>`
`function (props: AlertIconProps): ReactElement` — convenience wrapper that renders the lucide-react icon for the supplied `variant`. Marked `aria-hidden="true"` so the visible title / description carries the accessible name. Not a `forwardRef` (icons are stateless and the parent `<Alert>` reaches them via the cva `[&>svg]:` modifiers, not via ref-based DOM access).
| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | same as `<Alert>` | `"default"` | Variant whose default icon to render. |
| `className` | `string` | _undefined_ | Optional className passed to the rendered icon. |
#### Errors
The Alert components do NOT call `useTranslation()` so they do not interact with the i18n provider. Children carrying user-perceivable text must be resolved at the call site with `t('react-ui.alert.policy.title')` or similar.
## Implementation Notes
- `role="alert"` is unconditionally applied. This may produce verbose announcements when many alerts mount at once (e.g. a list of validation errors); consumers in that scenario should wrap the whole list in `<div role="alert" aria-live="polite">` and pass `role="presentation"` to each `<Alert>` to suppress duplicate announcements (or use `<Alert>` purely for visual styling and let the parent live region carry the accessibility semantics).
- The `[&>svg]:absolute [&>svg]:start-4 [&>svg]:top-4` rules in the cva position any direct SVG child as the inline icon. This means consumers can drop any icon — `<MailIcon />`, `<SparklesIcon />`, `<AlertIcon variant="warning" />` — directly inside the alert root and it lands correctly without a wrapper element.
- `<AlertIcon>` is a convenience wrapper, not a required slot. Alerts without any icon (just title + description) render correctly — the magic-modifier rules only fire when a direct SVG child exists.
- `<AlertTitle>` defaults to `<h5>` because alerts typically appear inside a section that already has its own `<h2>` / `<h3>` heading. Consumers needing a different heading level should compose their own heading element with `alertTitleVariants()`.
- `<AlertDescription>` renders as `<div>` (not `<p>`) so consumers can compose nested block elements (paragraphs, lists, buttons) without invalid HTML — paragraphs cannot contain block elements.

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import * as Stories from "./alert.stories.js";
const STORY_KEYS = [
"Default",
"Playground",
"Variants",
"States",
"WithI18n",
"WithTheme",
"A11y",
"Composition",
] as const;
interface StoryRecord {
readonly args?: Record<string, unknown>;
readonly render?: (args: Record<string, unknown>) => React.ReactNode;
}
describe("alert.stories", () => {
it("exports every Data story", { timeout: 5000 }, () => {
for (const key of STORY_KEYS) {
expect(
(Stories as unknown as Record<string, unknown>)[key],
`missing story export: ${key}`
).toBeDefined();
}
});
for (const key of STORY_KEYS) {
it(`renders the ${key} story without throwing`, {
timeout: 10000,
}, () => {
const story = (Stories as unknown as Record<string, StoryRecord>)[
key
];
expect(story).toBeDefined();
expect(typeof story.render).toBe("function");
if (story.render === undefined) return;
const tree = story.render(story.args ?? {});
const ui = (
<div data-testid="story-host">{tree as React.ReactNode}</div>
);
const { container } = renderComponent(ui);
expect(
container.querySelector("[data-testid='story-host']")
).not.toBeNull();
});
}
});

View File

@@ -0,0 +1,279 @@
/**
* Storybook stories for `<Alert>`.
*
* `@storybook/react` is NOT a dep of `@suaveplan/react-ui`; story-meta
* shapes are inlined.
*/
import {
createI18n as createI18nNarrow,
type I18nInstance,
} from "@suaveplan/i18n/plugin";
import { TranslationProvider } from "@suaveplan/react-i18n";
function createI18n(
options: Parameters<typeof createI18nNarrow>[0]
): I18nInstance {
return createI18nNarrow(options) as unknown as I18nInstance;
}
import type { ReactElement, ReactNode } from "react";
import type { SupportedLocale } from "../../locales/locales.types.js";
import type { ThemePlugin } from "../../theme/theme.types.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { darkTheme } from "../../theme/themes/dark.js";
import { highContrastTheme } from "../../theme/themes/high-contrast.js";
import { lightTheme } from "../../theme/themes/light.js";
import { Alert, AlertDescription, AlertIcon, AlertTitle } from "./alert.js";
import type { AlertProps } from "./alert.types.js";
interface Meta<T> {
readonly component: T;
readonly title: string;
readonly tags?: readonly string[];
readonly parameters?: Record<string, unknown>;
readonly argTypes?: Record<string, unknown>;
}
interface StoryObj<TArgs> {
readonly args?: Partial<TArgs>;
readonly render?: (args: Partial<TArgs>) => ReactNode;
readonly name?: string;
readonly parameters?: Record<string, unknown>;
}
const meta: Meta<typeof Alert> = {
component: Alert,
title: "react-ui/Data/Alert",
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<AlertProps>;
interface ProviderShellProps {
readonly locale: SupportedLocale;
readonly themes: ReadonlyArray<ThemePlugin>;
readonly defaultTheme: string;
readonly children: ReactNode;
}
function ProviderShellSync(props: ProviderShellProps): ReactElement {
const i18n = createI18n({
defaultLocale: props.locale,
fallbackLocale: "en-GB",
});
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider
themes={props.themes}
defaultTheme={props.defaultTheme}
>
{props.children}
</ThemeProvider>
</TranslationProvider>
);
}
export const Default: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Alert {...args}>
<AlertIcon variant={args.variant} />
<AlertTitle>Notice</AlertTitle>
<AlertDescription>Default variant alert.</AlertDescription>
</Alert>
</ProviderShellSync>
),
};
export const Playground: Story = {
args: { variant: "default" },
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Alert {...args}>
<AlertIcon variant={args.variant} />
<AlertTitle>Playground</AlertTitle>
<AlertDescription>Try different variants.</AlertDescription>
</Alert>
</ProviderShellSync>
),
};
export const Variants: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-3 max-w-md">
<Alert variant="default">
<AlertIcon variant="default" />
<AlertTitle>Heads up</AlertTitle>
<AlertDescription>
Default informational tone.
</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertIcon variant="destructive" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong.</AlertDescription>
</Alert>
<Alert variant="success">
<AlertIcon variant="success" />
<AlertTitle>Saved</AlertTitle>
<AlertDescription>Your changes are saved.</AlertDescription>
</Alert>
<Alert variant="warning">
<AlertIcon variant="warning" />
<AlertTitle>Heads up</AlertTitle>
<AlertDescription>This may take a while.</AlertDescription>
</Alert>
</div>
</ProviderShellSync>
),
};
export const States: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-3 max-w-md">
<Alert>
<AlertTitle>Title only</AlertTitle>
</Alert>
<Alert variant="destructive">
<AlertDescription>Description only.</AlertDescription>
</Alert>
</div>
</ProviderShellSync>
),
};
const LOCALES: ReadonlyArray<SupportedLocale> = [
"en-GB",
"zh-CN",
"ar-AE",
"cy-GB",
"ru-RU",
];
export const WithI18n: Story = {
render: () => (
<div className="flex flex-col gap-2">
{LOCALES.map((locale) => (
<div
key={locale}
dir={locale === "ar-AE" ? "rtl" : "ltr"}
className="flex items-start gap-2"
>
<span style={{ width: 80 }}>{locale}</span>
<ProviderShellSync
locale={locale}
themes={[lightTheme]}
defaultTheme="light"
>
<Alert variant="success" className="max-w-xs">
<AlertIcon variant="success" />
<AlertTitle>{locale}</AlertTitle>
<AlertDescription>{locale}</AlertDescription>
</Alert>
</ProviderShellSync>
</div>
))}
</div>
),
};
const THEMES: ReadonlyArray<{
readonly label: string;
readonly theme: ThemePlugin;
}> = [
{ label: "light", theme: lightTheme },
{ label: "dark", theme: darkTheme },
{ label: "highContrast", theme: highContrastTheme },
];
export const WithTheme: Story = {
render: () => (
<div className="flex flex-col gap-2">
{THEMES.map(({ label, theme }) => (
<div key={label} className="flex items-start gap-2 p-3 border">
<span style={{ width: 120 }}>{label}</span>
<ProviderShellSync
locale="en-GB"
themes={[theme]}
defaultTheme={theme.name}
>
<Alert variant="warning" className="max-w-xs">
<AlertIcon variant="warning" />
<AlertTitle>Warning</AlertTitle>
<AlertDescription>Heads up.</AlertDescription>
</Alert>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const A11y: Story = {
parameters: {
a11y: {
element: "#storybook-root",
config: {
rules: [{ id: "color-contrast", enabled: true }],
},
},
},
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Alert variant="success">
<AlertIcon variant="success" />
<AlertTitle>Saved</AlertTitle>
<AlertDescription>Your changes are saved.</AlertDescription>
</Alert>
</ProviderShellSync>
),
};
export const Composition: Story = {
name: "Composition (with action)",
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Alert variant="warning" className="max-w-md">
<AlertIcon variant="warning" />
<AlertTitle>Storage almost full</AlertTitle>
<AlertDescription>
<p>You are using 90% of your storage quota.</p>
<button
type="button"
className="mt-2 inline-flex items-center gap-1 rounded-md border px-3 py-1 text-sm"
>
Upgrade plan
</button>
</AlertDescription>
</Alert>
</ProviderShellSync>
),
};

View File

@@ -0,0 +1,230 @@
import { TranslationProvider } from "@suaveplan/react-i18n";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import { createRef } from "react";
import { createTestI18n } from "../../../tests/setup/test-i18n.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { lightTheme } from "../../theme/themes/light.js";
import { Alert, AlertDescription, AlertIcon, AlertTitle } from "./alert.js";
import {
alertDescriptionVariants,
alertTitleVariants,
alertVariants,
} from "./alert.variants.js";
async function withProviders(
node: React.ReactElement
): Promise<React.ReactElement> {
const i18n = await createTestI18n({ locale: "en-GB" });
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
{node}
</ThemeProvider>
</TranslationProvider>
);
}
describe("<Alert>", () => {
it("renders root with role=alert", { timeout: 5000 }, async () => {
const tree = await withProviders(
<Alert>
<AlertTitle>T</AlertTitle>
<AlertDescription>D</AlertDescription>
</Alert>
);
const { getByRole } = renderComponent(tree);
expect(getByRole("alert")).not.toBeNull();
});
it("renders title as h5 and description as div", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert>
<AlertTitle>Heading</AlertTitle>
<AlertDescription>Body</AlertDescription>
</Alert>
);
const { getByText } = renderComponent(tree);
expect(getByText("Heading").tagName).toBe("H5");
expect(getByText("Body").tagName).toBe("DIV");
});
it("applies the data-variant attribute matching the variant", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert variant="warning" data-testid="al">
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("al").getAttribute("data-variant")).toBe("warning");
});
it("falls back to default data-variant when no prop is supplied", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert data-testid="al">
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("al").getAttribute("data-variant")).toBe("default");
});
it("applies the destructive variant border colour", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert variant="destructive" data-testid="al">
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("al").className).toContain("border-destructive");
});
it("applies the success variant colour palette", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert variant="success" data-testid="al">
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("al").className).toContain("text-emerald-700");
});
it("AlertIcon renders the default Info icon by default", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert>
<AlertIcon />
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByRole } = renderComponent(tree);
const icon = getByRole("alert").querySelector("[data-icon='default']");
expect(icon).not.toBeNull();
});
it("AlertIcon renders the destructive icon for the destructive variant", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert variant="destructive">
<AlertIcon variant="destructive" />
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByRole } = renderComponent(tree);
expect(
getByRole("alert").querySelector("[data-icon='destructive']")
).not.toBeNull();
});
it("AlertIcon renders the success icon for the success variant", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert variant="success">
<AlertIcon variant="success" />
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByRole } = renderComponent(tree);
expect(
getByRole("alert").querySelector("[data-icon='success']")
).not.toBeNull();
});
it("AlertIcon renders the warning icon for the warning variant", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert variant="warning">
<AlertIcon variant="warning" />
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByRole } = renderComponent(tree);
expect(
getByRole("alert").querySelector("[data-icon='warning']")
).not.toBeNull();
});
it("AlertIcon marks the icon aria-hidden=true", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert>
<AlertIcon />
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByRole } = renderComponent(tree);
const icon = getByRole("alert").querySelector("svg");
expect(icon?.getAttribute("aria-hidden")).toBe("true");
});
it("AlertIcon merges consumer className", { timeout: 5000 }, async () => {
const tree = await withProviders(
<Alert>
<AlertIcon className="custom-spin" />
<AlertTitle>T</AlertTitle>
</Alert>
);
const { getByRole } = renderComponent(tree);
const icon = getByRole("alert").querySelector("svg");
expect(icon?.getAttribute("class")).toContain("custom-spin");
});
it("merges custom className on every sub-component", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Alert className="a-extra">
<AlertTitle className="t-extra">T</AlertTitle>
<AlertDescription className="d-extra">D</AlertDescription>
</Alert>
);
const { container } = renderComponent(tree);
expect(container.querySelector(".a-extra")).not.toBeNull();
expect(container.querySelector(".t-extra")).not.toBeNull();
expect(container.querySelector(".d-extra")).not.toBeNull();
});
it("forwards refs on every sub-component", { timeout: 5000 }, async () => {
const rootRef = createRef<HTMLDivElement>();
const titleRef = createRef<HTMLHeadingElement>();
const descRef = createRef<HTMLDivElement>();
const tree = await withProviders(
<Alert ref={rootRef}>
<AlertTitle ref={titleRef}>T</AlertTitle>
<AlertDescription ref={descRef}>D</AlertDescription>
</Alert>
);
renderComponent(tree);
expect(rootRef.current).not.toBeNull();
expect(titleRef.current?.tagName).toBe("H5");
expect(descRef.current?.tagName).toBe("DIV");
});
it("exposes every alert cva resolver", { timeout: 5000 }, () => {
for (const v of [
"default",
"destructive",
"success",
"warning",
] as const) {
expect(alertVariants({ variant: v }).length).toBeGreaterThan(0);
}
expect(alertTitleVariants().length).toBeGreaterThan(0);
expect(alertDescriptionVariants().length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,131 @@
import {
AlertCircleIcon,
AlertTriangleIcon,
CheckCircle2Icon,
InfoIcon,
} from "lucide-react";
import { type ComponentType, forwardRef, type ReactElement } from "react";
import { useThemedVariants } from "../../theme/use-themed-variants/use-themed-variants.js";
import { cn } from "../../utils/cn/cn.js";
import type {
AlertDescriptionProps,
AlertIconProps,
AlertProps,
AlertTitleProps,
AlertVariantProps,
} from "./alert.types.js";
import {
alertDescriptionVariants,
alertTitleVariants,
alertVariants,
} from "./alert.variants.js";
interface IconProps {
readonly className?: string;
readonly "aria-hidden"?: boolean;
readonly "data-icon"?: string;
}
const VARIANT_ICON: Record<
NonNullable<AlertVariantProps["variant"]>,
ComponentType<IconProps>
> = {
default: InfoIcon,
destructive: AlertCircleIcon,
success: CheckCircle2Icon,
warning: AlertTriangleIcon,
};
/**
* `<Alert>` — chassis callout for inline messages, errors, and notices.
*
* Per shadcn skill rule: use `Alert` for callouts — don't build custom
* styled divs (`<div className="border-l-4 border-yellow-500 ...">`)
* because hand-rolled callouts diverge across teams in colour scale,
* iconography, and accessibility hooks.
*
* The chassis renders `role="alert"` on the root so screen-readers
* announce the message. The `variant` axis swaps semantic colour pairs
* for four rungs (`default` / `destructive` / `success` / `warning`)
* and the `<AlertIcon>` companion picks the matching lucide-react icon
* for the variant.
*
* Refs are forwarded to the underlying `<div>` for every sub-component.
*/
export const Alert = forwardRef<HTMLDivElement, AlertProps>(
function Alert(props, ref): ReactElement {
const { className, variant, ...rest } = props;
const variants = useThemedVariants("Alert", alertVariants);
return (
<div
ref={ref}
role="alert"
className={cn(variants({ variant }), className)}
data-variant={variant ?? "default"}
{...rest}
/>
);
}
);
Alert.displayName = "Alert";
/**
* `<AlertTitle>` — heading rendered as `<h5>` so it nests under page-
* level `<h1>`-`<h4>` headings without disrupting the document outline.
*/
export const AlertTitle = forwardRef<HTMLHeadingElement, AlertTitleProps>(
function AlertTitle(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants("AlertTitle", alertTitleVariants);
return <h5 ref={ref} className={cn(variants(), className)} {...rest} />;
}
);
AlertTitle.displayName = "AlertTitle";
/**
* `<AlertDescription>` — body text rendered as a `<div>` so consumers
* can compose paragraphs / lists / inline elements without nesting
* block elements inside a `<p>`.
*/
export const AlertDescription = forwardRef<
HTMLDivElement,
AlertDescriptionProps
>(function AlertDescription(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants(
"AlertDescription",
alertDescriptionVariants
);
return <div ref={ref} className={cn(variants(), className)} {...rest} />;
});
AlertDescription.displayName = "AlertDescription";
/**
* `<AlertIcon>` — convenience wrapper that renders the lucide-react
* icon matching the supplied `variant`. Marked `aria-hidden="true"` so
* the visible title / description is the accessible name.
*
* Pair the `variant` prop with the parent `<Alert>`'s variant for
* consistent iconography. Consumers needing a custom icon should drop
* any svg directly inside the `<Alert>` root — the cva positions any
* direct svg child via `[&>svg]:absolute [&>svg]:start-4`.
*/
export function AlertIcon(props: AlertIconProps): ReactElement {
const { variant, className } = props;
const resolvedVariant = variant ?? "default";
const Icon = VARIANT_ICON[resolvedVariant];
return (
<Icon
className={cn("size-4", className)}
aria-hidden={true}
data-icon={resolvedVariant}
/>
);
}
AlertIcon.displayName = "AlertIcon";
export { alertDescriptionVariants, alertTitleVariants, alertVariants };

View File

@@ -0,0 +1,64 @@
import type { HTMLAttributes } from "react";
import type { VariantProps } from "../../variants/variants.js";
import type {
alertDescriptionVariants,
alertTitleVariants,
alertVariants,
} from "./alert.variants.js";
/**
* Module augmentation registering every Alert cva slot into
* `ThemableVariants` so `useThemedVariants` is type-narrowed and a
* `ThemePlugin` may declare overrides.
*/
declare module "../../theme/theme.types.js" {
interface ThemableVariants {
readonly Alert: typeof alertVariants;
readonly AlertTitle: typeof alertTitleVariants;
readonly AlertDescription: typeof alertDescriptionVariants;
}
}
export type AlertVariantProps = VariantProps<typeof alertVariants>;
/**
* Public props for `<Alert>` — root container. Renders `role="alert"`
* by default so screen-readers announce the alert. The `variant` axis
* swaps semantic colour pairs.
*/
export interface AlertProps
extends HTMLAttributes<HTMLDivElement>,
AlertVariantProps {}
/**
* Public props for `<AlertTitle>` — heading rendered as `<h5>` so it
* nests under page-level headings without disrupting the document
* outline.
*/
export interface AlertTitleProps extends HTMLAttributes<HTMLHeadingElement> {}
/**
* Public props for `<AlertDescription>` — body text rendered as a `<div>`
* so consumers can compose paragraphs / lists / inline elements without
* nesting block elements inside a `<p>`.
*/
export interface AlertDescriptionProps extends HTMLAttributes<HTMLDivElement> {}
/**
* Public props for `<AlertIcon>` — convenience wrapper that renders the
* lucide-react icon matching the alert variant. Consumers can override
* the icon by passing children; without children, `<AlertIcon>` selects
* the default for the parent alert's variant.
*/
export interface AlertIconProps {
/**
* The variant whose default icon to render. When unset, defaults to
* `default` (the InfoIcon). Pair with the parent `<Alert variant>` for
* consistent iconography.
*/
readonly variant?: AlertVariantProps["variant"];
/**
* Optional className passed to the rendered icon.
*/
readonly className?: string;
}

View File

@@ -0,0 +1,57 @@
# Alert Variants
## Purpose
`alertVariants` is the cva config that powers the `<Alert>` chassis component. It exposes a `variant` axis with four rungs (`default` / `destructive` / `success` / `warning`) that each pair a semantic border colour with matching foreground and icon tints. The base class string includes the magic `[&>svg]:` modifiers that position any direct SVG child as an inline icon in the top-start corner without requiring consumer flex setup — this is the same trick the upstream shadcn `alert` recipe uses.
`alertTitleVariants` and `alertDescriptionVariants` are presentation-only with no exposed axes — they encode the conventional spacing and typography for the title / body rows.
## Features
- Four colour variants via the `variant` axis (`default` / `destructive` / `success` / `warning`).
- `[&>svg]:absolute [&>svg]:start-4 [&>svg]:top-4` magic modifiers so any direct svg child positions correctly without a wrapper.
- `[&>svg~*]:ps-7` ensures siblings of the svg get logical-property left padding so RTL layouts work without bespoke markup.
- `[&>svg+div]:translate-y-[-3px]` nudges the title row up by 3px so its baseline aligns with the icon centre.
- Title is `font-medium leading-none tracking-tight`; description is `text-sm` with `leading-relaxed` on nested `<p>` elements.
## Usage: Basic Example
```tsx
import { alertVariants } from "@suaveplan/react-ui";
const default_ = alertVariants();
const danger = alertVariants({ variant: "destructive" });
const success = alertVariants({ variant: "success" });
const warning = alertVariants({ variant: "warning" });
```
## Usage: Advanced Example
```tsx
import { cva } from "class-variance-authority";
import { alertVariants } from "@suaveplan/react-ui";
const stickyAlert = cva(
[alertVariants({ variant: "warning" }), "sticky top-0 z-30"].join(" ")
);
```
## API Reference
### `alertVariants(opts?)`
| Option | Type | Default | Description |
|---|---|---|---|
| `variant` | `"default" \| "destructive" \| "success" \| "warning"` | `"default"` | Colour palette for border, foreground, and inline icon. |
### `alertTitleVariants()`, `alertDescriptionVariants()`
No options — single canonical class strings.
## Implementation Notes
- The `[&>svg]:` Tailwind arbitrary descendant modifiers are how shadcn positions the icon — any direct SVG child of the alert is auto-placed in the top-start corner (`absolute start-4 top-4`). This avoids the need for a separate `<AlertIcon>` slot at the JSX level — consumers can drop any icon component directly inside the alert root.
- The chassis ships an `<AlertIcon>` convenience wrapper that picks the lucide-react default for the alert variant, but it is optional — consumers can supply any icon (`<MailIcon />`, `<SparklesIcon />`, etc.) inline.
- `[&>svg~*]:ps-7` uses logical-property `ps-7` (padding-inline-start) so siblings of the icon space correctly in both LTR and RTL layouts.
- The destructive variant keeps the same `border-destructive` colour in dark mode (no `dark:` override) because the destructive token is already calibrated for both colour schemes via the theme `cssVariables`.
- Success / warning variants use Tailwind's `emerald-*` and `amber-*` palettes rather than relying on theme tokens because they are semantic-fixed colours; consumers wanting to swap them can register a `variantOverrides.Alert` on their theme plugin.

View File

@@ -0,0 +1,62 @@
import { defineVariants } from "../../variants/variants.js";
/**
* `alertVariants` — base cva config for `<Alert>`.
*
* Mirrors the upstream shadcn `alert` recipe: a bordered surface that
* holds an optional icon, a title, and a description. The `variant`
* axis swaps semantic colour pairs for four rungs (`default` /
* `destructive` / `success` / `warning`).
*
* Per shadcn skill rule: callouts use `Alert` — don't build custom
* styled divs. The chassis enforces this by exposing the `variant`
* scale that consumers would otherwise hand-roll inconsistently.
*
* The `[&>svg]:` modifiers position any svg child as an inline icon
* in the top-start corner without requiring consumer flex setup.
*/
export const alertVariants = defineVariants(
"Alert",
[
"relative w-full rounded-lg border p-4",
"[&>svg~*]:ps-7 [&>svg+div]:translate-y-[-3px]",
"[&>svg]:absolute [&>svg]:start-4 [&>svg]:top-4",
"[&>svg]:text-foreground",
].join(" "),
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
success:
"border-emerald-500/50 text-emerald-700 dark:text-emerald-300 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-300",
warning:
"border-amber-500/50 text-amber-700 dark:text-amber-300 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-300",
},
},
defaultVariants: {
variant: "default",
},
}
);
/**
* `alertTitleVariants` — heading row inside the alert. Bold and
* medium-tracked so it stands above the description.
*/
export const alertTitleVariants = defineVariants(
"AlertTitle",
"mb-1 font-medium leading-none tracking-tight",
{ variants: {}, defaultVariants: {} }
);
/**
* `alertDescriptionVariants` — body text below the title. Smaller font,
* relaxed leading.
*/
export const alertDescriptionVariants = defineVariants(
"AlertDescription",
"text-sm [&_p]:leading-relaxed",
{ variants: {}, defaultVariants: {} }
);

View File

@@ -0,0 +1,16 @@
export {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
alertDescriptionVariants,
alertTitleVariants,
alertVariants,
} from "./alert.js";
export type {
AlertDescriptionProps,
AlertIconProps,
AlertProps,
AlertTitleProps,
AlertVariantProps,
} from "./alert.types.js";

View File

@@ -0,0 +1,124 @@
# AspectRatio
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- aspect-ratio.variants — `aspectRatioVariants` is th…
- [aspect-ratio.variants](./aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
- [aspect-ratio.variants](./aspect-ratio.variants.md) — `aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. I…
## Purpose
`<AspectRatio>` is the chassis fixed-aspect-ratio container built on `@radix-ui/react-aspect-ratio`. Radix sizes the inner box via inline `padding-bottom` so the rendered element preserves the requested numeric ratio at every viewport width — the wrapping `<div>` then serves as a positioning context for any embedded `<img>`, `<video>`, `<iframe>`, or other layout-sensitive child. The chassis layers a small cva recipe (`relative w-full overflow-hidden`) so embedded media slot in cleanly without consumers having to remember the boilerplate every time.
The component is purely structural — it takes the requested `ratio` prop, sizes the box, and renders children. No translations, no theming axes, no event handlers. The Radix primitive does not assign a numeric `z-index` and neither does the chassis, so stacking remains implicit.
## Features
- Wraps `@radix-ui/react-aspect-ratio` with a chassis-friendly cva slot.
- `ratio` prop accepts any positive number (`16 / 9`, `4 / 3`, `1`, `21 / 9`, …); Radix interprets it as `width / height`.
- `relative w-full overflow-hidden` baked in so embedded `<img className="h-full w-full object-cover">` or `<iframe className="absolute inset-0 size-full">` children sit correctly.
- Forwards refs to the rendered `<div>`.
- Theme-driven cva override via `useThemedVariants("AspectRatio")`.
- No translations and no a11y semantics required (the box is layout-only).
## Usage: Basic Example
```tsx
import { AspectRatio } from "@suaveplan/react-ui";
export function CoverImage({ src, alt }: { src: string; alt: string }) {
return (
<AspectRatio ratio={16 / 9}>
<img
src={src}
alt={alt}
className="h-full w-full object-cover"
/>
</AspectRatio>
);
}
```
## Usage: Advanced Example
```tsx
import { AspectRatio, cn } from "@suaveplan/react-ui";
interface MediaTileProps {
readonly kind: "image" | "video" | "embed";
readonly src: string;
readonly alt?: string;
readonly title?: string;
}
export function MediaTile({ kind, src, alt, title }: MediaTileProps) {
return (
<AspectRatio
ratio={kind === "video" ? 16 / 9 : kind === "embed" ? 4 / 3 : 1}
className={cn(
"rounded-lg border bg-muted",
kind === "embed" && "shadow-md"
)}
>
{kind === "image" ? (
<img
src={src}
alt={alt ?? ""}
className="h-full w-full object-cover"
/>
) : kind === "video" ? (
<video
src={src}
controls
className="h-full w-full object-cover"
aria-label={title}
/>
) : (
<iframe
src={src}
title={title ?? "Embed"}
className="absolute inset-0 size-full border-0"
/>
)}
</AspectRatio>
);
}
```
## API Reference
### `<AspectRatio>`
Renders the Radix Root which constrains the box to the supplied numeric ratio.
| Prop | Type | Default | Description |
|---|---|---|---|
| `ratio` | `number` | `1` | Desired `width / height` ratio. Radix uses it to compute the `padding-bottom` percentage. |
| `className` | `string` | _none_ | Additional classes merged via `cn()` after the chassis cva recipe. |
| `children` | `ReactNode` | _none_ | The content to be sized to the ratio. Position children with `className="h-full w-full object-cover"` or `className="absolute inset-0 size-full"` for full-bleed behaviour. |
Forwards refs to the rendered `<div>` element.
### `aspectRatioVariants(): string`
Returns the chassis cva class string. Takes no arguments. Useful when assembling a custom slot that wants the same recipe without rendering an `<AspectRatio>`.
## Implementation Notes
- The cva carries `relative` so embedded children using `absolute inset-0` (e.g. iframes) anchor correctly. Without it, the absolutely-positioned child escapes to the nearest positioned ancestor.
- `overflow-hidden` is included so rounded corners on `className="rounded-lg"` clip the embedded media — without it, `<img>` overflow would leak out the corners.
- `w-full` ensures the wrapper fills the parent's width; the height is then computed by Radix from the ratio. Consumers can override with `className="w-1/2"` or similar when a fixed-width tile is desired.
- The chassis NEVER assigns a numeric `z-index`; stacking is the consumer's responsibility (typically managed by a surrounding overlay primitive such as `<Dialog>`).
- Radix's primitive accepts `ratio` as a number; passing `0` or a negative value results in invalid CSS. The chassis does not validate at runtime — supply a positive ratio.
- Because the component is purely structural, there are no translation keys to override and no a11y semantics injected. Any media inside should carry its own `alt` / `title` / `aria-label` per WAI-ARIA practice.

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import * as Stories from "./aspect-ratio.stories.js";
const STORY_KEYS = [
"Default",
"Playground",
"Variants",
"States",
"WithI18n",
"WithRTL",
"WithTheme",
"A11y",
"Composition",
] as const;
type StoryKey = (typeof STORY_KEYS)[number];
interface StoryRecord {
readonly args?: Record<string, unknown>;
readonly render?: (args: Record<string, unknown>) => React.ReactNode;
}
describe("aspect-ratio.stories", () => {
it("exports every Layout story", { timeout: 5000 }, () => {
for (const key of STORY_KEYS) {
expect(
(Stories as unknown as Record<string, unknown>)[key],
`missing story export: ${key}`
).toBeDefined();
}
});
for (const key of STORY_KEYS) {
it(`renders the ${key} story without throwing`, {
timeout: 10000,
}, () => {
const story = (Stories as unknown as Record<StoryKey, StoryRecord>)[
key
];
expect(story).toBeDefined();
const render = story.render;
expect(typeof render).toBe("function");
if (render === undefined) return;
const tree = render(story.args ?? {});
const ui = (
<div data-testid="story-host">{tree as React.ReactNode}</div>
);
const { container } = renderComponent(ui);
expect(
container.querySelector("[data-testid='story-host']")
).not.toBeNull();
});
}
});

View File

@@ -0,0 +1,327 @@
/**
* Storybook stories for `<AspectRatio>`.
*
* `@storybook/react` is NOT a dep of `@suaveplan/react-ui`; story-meta
* shapes are inlined.
*/
import {
createI18n as createI18nNarrow,
type I18nInstance,
} from "@suaveplan/i18n/plugin";
import { TranslationProvider } from "@suaveplan/react-i18n";
import type { ReactElement, ReactNode } from "react";
import type { SupportedLocale } from "../../locales/locales.types.js";
import type { ThemePlugin } from "../../theme/theme.types.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { darkTheme } from "../../theme/themes/dark.js";
import { highContrastTheme } from "../../theme/themes/high-contrast.js";
import { lightTheme } from "../../theme/themes/light.js";
import { AspectRatio } from "./aspect-ratio.js";
import type { AspectRatioProps } from "./aspect-ratio.types.js";
function createI18n(
options: Parameters<typeof createI18nNarrow>[0]
): I18nInstance {
return createI18nNarrow(options) as unknown as I18nInstance;
}
interface Meta<T> {
readonly component: T;
readonly title: string;
readonly tags?: readonly string[];
readonly parameters?: Record<string, unknown>;
readonly argTypes?: Record<string, unknown>;
}
interface StoryObj<TArgs> {
readonly args?: Partial<TArgs>;
readonly render?: (args: Partial<TArgs>) => ReactNode;
readonly name?: string;
readonly parameters?: Record<string, unknown>;
}
const meta: Meta<unknown> = {
component: undefined as unknown as never,
title: "react-ui/Layout/AspectRatio",
tags: ["autodocs"],
argTypes: {
ratio: {
control: { type: "number" },
},
},
};
export default meta;
type Story = StoryObj<AspectRatioProps>;
interface ProviderShellProps {
readonly locale: SupportedLocale;
readonly themes: ReadonlyArray<ThemePlugin>;
readonly defaultTheme: string;
readonly children: ReactNode;
}
function ProviderShellSync(props: ProviderShellProps): ReactElement {
const i18n = createI18n({
defaultLocale: props.locale,
fallbackLocale: "en-GB",
});
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider
themes={props.themes}
defaultTheme={props.defaultTheme}
>
{props.children}
</ThemeProvider>
</TranslationProvider>
);
}
const PLACEHOLDER_PNG =
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 9'><rect width='16' height='9' fill='%2399a'/></svg>";
export const Default: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 320 }}>
<AspectRatio ratio={16 / 9}>
<img
src={PLACEHOLDER_PNG}
alt="placeholder"
className="h-full w-full object-cover"
/>
</AspectRatio>
</div>
</ProviderShellSync>
),
};
export const Playground: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 320 }}>
<AspectRatio {...args}>
<div className="flex h-full w-full items-center justify-center bg-muted">
Tile
</div>
</AspectRatio>
</div>
</ProviderShellSync>
),
args: { ratio: 16 / 9 },
};
const RATIOS: ReadonlyArray<{
readonly label: string;
readonly ratio: number;
}> = [
{ label: "1:1 (square)", ratio: 1 },
{ label: "4:3 (legacy)", ratio: 4 / 3 },
{ label: "16:9 (widescreen)", ratio: 16 / 9 },
{ label: "21:9 (cinema)", ratio: 21 / 9 },
];
export const Variants: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="grid grid-cols-2 gap-3" style={{ width: 480 }}>
{RATIOS.map(({ label, ratio }) => (
<div key={label}>
<p className="text-sm font-medium">{label}</p>
<AspectRatio ratio={ratio} className="rounded border">
<div className="flex h-full w-full items-center justify-center bg-muted">
{label}
</div>
</AspectRatio>
</div>
))}
</div>
</ProviderShellSync>
),
};
export const States: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 320 }}>
<AspectRatio ratio={1} className="rounded border bg-muted">
<div className="flex h-full w-full items-center justify-center">
Empty placeholder
</div>
</AspectRatio>
</div>
</ProviderShellSync>
),
};
const LOCALES: ReadonlyArray<SupportedLocale> = [
"en-GB",
"zh-CN",
"ar-AE",
"cy-GB",
"ru-RU",
];
export const WithI18n: Story = {
render: () => (
<div className="grid grid-cols-3 gap-3" style={{ width: 480 }}>
{LOCALES.map((locale) => (
<ProviderShellSync
key={locale}
locale={locale}
themes={[lightTheme]}
defaultTheme="light"
>
<div>
<p className="text-xs">{locale}</p>
<AspectRatio ratio={1} className="rounded border">
<div className="flex h-full w-full items-center justify-center bg-muted">
{locale}
</div>
</AspectRatio>
</div>
</ProviderShellSync>
))}
</div>
),
};
export const WithRTL: Story = {
render: () => (
<div dir="rtl" style={{ width: 320 }}>
<ProviderShellSync
locale="ar-AE"
themes={[lightTheme]}
defaultTheme="light"
>
<AspectRatio ratio={16 / 9} className="rounded border">
<div className="flex h-full w-full items-center justify-center bg-muted">
rtl
</div>
</AspectRatio>
</ProviderShellSync>
</div>
),
};
const THEMES: ReadonlyArray<{
readonly label: string;
readonly theme: ThemePlugin;
}> = [
{ label: "light", theme: lightTheme },
{ label: "dark", theme: darkTheme },
{ label: "highContrast", theme: highContrastTheme },
];
export const WithTheme: Story = {
render: () => (
<div className="grid grid-cols-3 gap-3" style={{ width: 480 }}>
{THEMES.map(({ label, theme }) => (
<ProviderShellSync
key={label}
locale="en-GB"
themes={[theme]}
defaultTheme={theme.name}
>
<div>
<p className="text-xs">{label}</p>
<AspectRatio ratio={4 / 3} className="rounded border">
<div className="flex h-full w-full items-center justify-center bg-muted">
{label}
</div>
</AspectRatio>
</div>
</ProviderShellSync>
))}
</div>
),
};
export const A11y: Story = {
parameters: {
a11y: {
element: "#storybook-root",
config: {
rules: [{ id: "color-contrast", enabled: true }],
},
},
},
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div style={{ width: 320 }}>
<AspectRatio ratio={16 / 9} className="rounded border">
<img
src={PLACEHOLDER_PNG}
alt="A landscape with rolling hills"
className="h-full w-full object-cover"
/>
</AspectRatio>
</div>
</ProviderShellSync>
),
};
export const Composition: Story = {
name: "Composition (image, video, iframe)",
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="grid grid-cols-3 gap-3" style={{ width: 720 }}>
<div>
<p className="text-xs">image</p>
<AspectRatio ratio={16 / 9} className="rounded border">
<img
src={PLACEHOLDER_PNG}
alt="placeholder"
className="h-full w-full object-cover"
/>
</AspectRatio>
</div>
<div>
<p className="text-xs">video</p>
<AspectRatio
ratio={16 / 9}
className="rounded border bg-black"
>
<div className="flex h-full w-full items-center justify-center text-white">
video placeholder
</div>
</AspectRatio>
</div>
<div>
<p className="text-xs">iframe (4:3)</p>
<AspectRatio ratio={4 / 3} className="rounded border">
<div className="absolute inset-0 flex size-full items-center justify-center bg-muted">
embed placeholder
</div>
</AspectRatio>
</div>
</div>
</ProviderShellSync>
),
};

View File

@@ -0,0 +1,136 @@
import { TranslationProvider } from "@suaveplan/react-i18n";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import { createRef } from "react";
import { createTestI18n } from "../../../tests/setup/test-i18n.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { lightTheme } from "../../theme/themes/light.js";
import { AspectRatio } from "./aspect-ratio.js";
import { aspectRatioVariants } from "./aspect-ratio.variants.js";
async function withProviders(
node: React.ReactElement
): Promise<React.ReactElement> {
const i18n = await createTestI18n({ locale: "en-GB" });
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
{node}
</ThemeProvider>
</TranslationProvider>
);
}
describe("<AspectRatio>", () => {
it("renders a div with the supplied children", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AspectRatio ratio={16 / 9}>
<span data-testid="content">child</span>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("content")).not.toBeNull();
});
it("applies the chassis cva recipe to the rendered element", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AspectRatio ratio={1} data-testid="ar">
<span>square</span>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
const root = getByTestId("ar");
expect(root.className).toContain("relative");
expect(root.className).toContain("w-full");
expect(root.className).toContain("overflow-hidden");
});
it("merges consumer className alongside the cva classes", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AspectRatio
ratio={4 / 3}
className="rounded-lg border"
data-testid="ar"
>
<span>media</span>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
const root = getByTestId("ar");
expect(root.className).toContain("rounded-lg");
expect(root.className).toContain("border");
expect(root.className).toContain("relative");
});
it("forwards a ref to the underlying div", { timeout: 5000 }, async () => {
const ref = createRef<HTMLDivElement>();
const tree = await withProviders(
<AspectRatio ratio={16 / 9} ref={ref}>
<span>x</span>
</AspectRatio>
);
renderComponent(tree);
expect(ref.current).not.toBeNull();
expect(ref.current?.tagName).toBe("DIV");
});
it("renders the cva resolver to a non-empty string", {
timeout: 5000,
}, () => {
expect(aspectRatioVariants().length).toBeGreaterThan(0);
});
it("supports a square ratio (1)", { timeout: 5000 }, async () => {
const tree = await withProviders(
<AspectRatio ratio={1} data-testid="ar">
<span>square</span>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("ar")).not.toBeNull();
});
it("supports a widescreen ratio (16:9)", { timeout: 5000 }, async () => {
const tree = await withProviders(
<AspectRatio ratio={16 / 9} data-testid="ar">
<span>wide</span>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("ar")).not.toBeNull();
});
it("supports a legacy ratio (4:3)", { timeout: 5000 }, async () => {
const tree = await withProviders(
<AspectRatio ratio={4 / 3} data-testid="ar">
<span>legacy</span>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
expect(getByTestId("ar")).not.toBeNull();
});
it("renders nested media-shaped children unchanged", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<AspectRatio ratio={16 / 9}>
<img
src="data:image/png;base64,iVBORw0KGgo="
alt="test"
className="h-full w-full object-cover"
data-testid="img"
/>
</AspectRatio>
);
const { getByTestId } = renderComponent(tree);
const img = getByTestId("img") as HTMLImageElement;
expect(img.className).toContain("object-cover");
});
});

View File

@@ -0,0 +1,39 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
import { type ComponentRef, forwardRef, type ReactElement } from "react";
import { useThemedVariants } from "../../theme/use-themed-variants/use-themed-variants.js";
import { cn } from "../../utils/cn/cn.js";
import type { AspectRatioProps } from "./aspect-ratio.types.js";
import { aspectRatioVariants } from "./aspect-ratio.variants.js";
/**
* `<AspectRatio>` — chassis fixed-aspect-ratio container built on
* `@radix-ui/react-aspect-ratio`.
*
* Radix sizes the inner box via inline `padding-bottom` so the
* containing element preserves the requested ratio at every viewport
* width. The chassis layers the `relative w-full overflow-hidden` cva
* recipe so embedded `<img>`, `<video>`, or `<iframe>` children fit
* cleanly when sized with `className="h-full w-full object-cover"`.
*
* Pass `ratio={16 / 9}` for widescreen video, `ratio={4 / 3}` for
* legacy media, or `ratio={1}` for square thumbnails. Refs forward to
* the rendered `<div>` element.
*/
export const AspectRatio = forwardRef<
ComponentRef<typeof AspectRatioPrimitive.Root>,
AspectRatioProps
>(function AspectRatio(props, ref): ReactElement {
const { className, ...rest } = props;
const variants = useThemedVariants("AspectRatio", aspectRatioVariants);
return (
<AspectRatioPrimitive.Root
ref={ref}
className={cn(variants(), className)}
{...rest}
/>
);
});
AspectRatio.displayName = "AspectRatio";
export { aspectRatioVariants };

View File

@@ -0,0 +1,28 @@
import type * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
import type { ComponentPropsWithoutRef } from "react";
import type { aspectRatioVariants } from "./aspect-ratio.variants.js";
/**
* Module augmentation registering the `<AspectRatio>` cva slot into
* `ThemableVariants` so `useThemedVariants` is type-narrowed and a
* `ThemePlugin` may declare overrides.
*/
declare module "../../theme/theme.types.js" {
interface ThemableVariants {
readonly AspectRatio: typeof aspectRatioVariants;
}
}
/**
* Public props for `<AspectRatio>`.
*
* Wraps `@radix-ui/react-aspect-ratio` and pre-applies the chassis cva
* recipe. The `ratio` prop is REQUIRED by the underlying primitive when
* a constrained aspect is desired (e.g. `16 / 9`, `4 / 3`, `1`). Radix
* defaults to `1` when omitted; the chassis preserves that default.
*
* The element renders as a `<div>` and forwards refs to the underlying
* Radix root.
*/
export interface AspectRatioProps
extends ComponentPropsWithoutRef<typeof AspectRatioPrimitive.Root> {}

View File

@@ -0,0 +1,55 @@
# AspectRatio Variants (cva config)
## Purpose
`aspectRatioVariants` is the cva (class-variance-authority) configuration powering the `<AspectRatio>` outer wrapper. It is split from `aspect-ratio.tsx` into its own module so an ejected source copy under `npx shadcn@latest add @suaveplan/aspect-ratio` keeps the cva configuration in a dedicated file. Theme overrides flow through `useThemedVariants("AspectRatio")`.
The cva is intentionally minimal — no axes, no parameters. Radix already constrains the inner box via inline `padding-bottom`, so the only chassis decision is the structural wrapper recipe (`relative w-full overflow-hidden`).
## Features
- Single class string: `relative w-full overflow-hidden`.
- `relative` so embedded `absolute inset-0` children (iframes) anchor correctly.
- `w-full` so the wrapper fills the parent width by default; consumers can override.
- `overflow-hidden` so rounded corners on the wrapper clip embedded media cleanly.
- NEVER carries a numeric `z-index` (stacking is managed by the surrounding overlay primitive).
## Usage: Basic Example
```ts
import { aspectRatioVariants } from "@suaveplan/react-ui";
const className = aspectRatioVariants();
```
## Usage: Advanced Example
```ts
import { aspectRatioVariants, cn } from "@suaveplan/react-ui";
export function CustomMediaTile() {
return (
<div
className={cn(
aspectRatioVariants(),
"rounded-2xl border-2 border-primary"
)}
style={{ aspectRatio: "16 / 9" }}
>
…
</div>
);
}
```
## API Reference
### `aspectRatioVariants(): string`
Returns the chassis wrapper class string. No arguments — the variant config has no axes.
## Implementation Notes
- The cva exists primarily so a `ThemePlugin` may declare an override via `variantOverrides.AspectRatio` to swap the recipe (e.g. add `rounded-md` by default in a "soft" theme variant). When no theme override is registered, the base recipe is returned verbatim.
- Per the shadcn skill rule, the cva NEVER assigns a numeric `z-index`. Radix's primitive does not portal the box, so stacking remains under the consumer's surrounding layout.
- The cva does not include `block` because Radix's primitive renders an inline-styled `<div>` that already lays out as a block element.

View File

@@ -0,0 +1,19 @@
import { defineVariants } from "../../variants/variants.js";
/**
* `aspectRatioVariants` — base cva for the `<AspectRatio>` outer wrapper.
*
* The Radix `<AspectRatio.Root>` already constrains the box to the
* supplied numeric ratio via inline `padding-bottom` percentage; the
* cva is therefore intentionally minimal — it carries the
* `relative w-full overflow-hidden` recipe consumers expect for embedded
* media (image / video / iframe). NEVER carries a numeric `z-index`.
*/
export const aspectRatioVariants = defineVariants(
"AspectRatio",
"relative w-full overflow-hidden",
{
variants: {},
defaultVariants: {},
}
);

Some files were not shown because too many files have changed in this diff Show More