@uxelle/skills 0.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +58 -0
  3. package/dist/index.d.ts +26 -0
  4. package/dist/index.js +586 -0
  5. package/index.json +573 -0
  6. package/package.json +57 -0
  7. package/skills/uxelle-components/Accordion.md +51 -0
  8. package/skills/uxelle-components/AccordionGroup.md +39 -0
  9. package/skills/uxelle-components/Banner.md +73 -0
  10. package/skills/uxelle-components/BannerAnnouncement.md +63 -0
  11. package/skills/uxelle-components/Breadcrumb.md +38 -0
  12. package/skills/uxelle-components/Breadcrumbs.md +32 -0
  13. package/skills/uxelle-components/Bubble.md +34 -0
  14. package/skills/uxelle-components/Button.md +54 -0
  15. package/skills/uxelle-components/ButtonGroup.md +43 -0
  16. package/skills/uxelle-components/Card.md +35 -0
  17. package/skills/uxelle-components/Checkbox.md +57 -0
  18. package/skills/uxelle-components/CheckboxGroup.md +47 -0
  19. package/skills/uxelle-components/ChoiceChip.md +44 -0
  20. package/skills/uxelle-components/ChoiceChipGroup.md +50 -0
  21. package/skills/uxelle-components/DatePicker.md +59 -0
  22. package/skills/uxelle-components/DatePickerRange.md +73 -0
  23. package/skills/uxelle-components/Dialog.md +83 -0
  24. package/skills/uxelle-components/Divider.md +37 -0
  25. package/skills/uxelle-components/DynamicAngleLine.md +36 -0
  26. package/skills/uxelle-components/DynamicAngleLineCombo.md +30 -0
  27. package/skills/uxelle-components/DynamicAngleShape.md +32 -0
  28. package/skills/uxelle-components/DynamicAngleShapeSurface.md +35 -0
  29. package/skills/uxelle-components/DynamicAngleTexture.md +29 -0
  30. package/skills/uxelle-components/DynamicAngleTextureText.md +35 -0
  31. package/skills/uxelle-components/FieldMessage.md +31 -0
  32. package/skills/uxelle-components/FileUpload.md +53 -0
  33. package/skills/uxelle-components/FilterChip.md +35 -0
  34. package/skills/uxelle-components/FilterChipGroup.md +42 -0
  35. package/skills/uxelle-components/Footer.md +34 -0
  36. package/skills/uxelle-components/Hero.md +61 -0
  37. package/skills/uxelle-components/Icon.md +43 -0
  38. package/skills/uxelle-components/IconButton.md +44 -0
  39. package/skills/uxelle-components/Image.md +40 -0
  40. package/skills/uxelle-components/Label.md +35 -0
  41. package/skills/uxelle-components/LabelBadge.md +58 -0
  42. package/skills/uxelle-components/LanguageSelector.md +38 -0
  43. package/skills/uxelle-components/LanguageSelectorButton.md +34 -0
  44. package/skills/uxelle-components/Layout.md +81 -0
  45. package/skills/uxelle-components/LinearProgress.md +53 -0
  46. package/skills/uxelle-components/Link.md +52 -0
  47. package/skills/uxelle-components/List.md +35 -0
  48. package/skills/uxelle-components/ListControls.md +100 -0
  49. package/skills/uxelle-components/ListItem.md +69 -0
  50. package/skills/uxelle-components/Lockup.md +50 -0
  51. package/skills/uxelle-components/Logo.md +39 -0
  52. package/skills/uxelle-components/Menu.md +48 -0
  53. package/skills/uxelle-components/MultiSelect.md +54 -0
  54. package/skills/uxelle-components/NavLink.md +44 -0
  55. package/skills/uxelle-components/Navigation.md +61 -0
  56. package/skills/uxelle-components/NavigationSide.md +51 -0
  57. package/skills/uxelle-components/NavigationSideGroup.md +36 -0
  58. package/skills/uxelle-components/NavigationSideItem.md +56 -0
  59. package/skills/uxelle-components/NavigationSideSubItem.md +39 -0
  60. package/skills/uxelle-components/NotificationBadge.md +62 -0
  61. package/skills/uxelle-components/Pagination.md +49 -0
  62. package/skills/uxelle-components/PaginationComplex.md +51 -0
  63. package/skills/uxelle-components/PaginationSimple.md +44 -0
  64. package/skills/uxelle-components/ProductCard.md +46 -0
  65. package/skills/uxelle-components/Quantity.md +45 -0
  66. package/skills/uxelle-components/Radio.md +55 -0
  67. package/skills/uxelle-components/RadioGroup.md +49 -0
  68. package/skills/uxelle-components/SKILL.md +242 -0
  69. package/skills/uxelle-components/Scrim.md +28 -0
  70. package/skills/uxelle-components/SegmentedControl.md +118 -0
  71. package/skills/uxelle-components/Select.md +55 -0
  72. package/skills/uxelle-components/Sheet.md +31 -0
  73. package/skills/uxelle-components/Skeleton.md +35 -0
  74. package/skills/uxelle-components/Spinner.md +33 -0
  75. package/skills/uxelle-components/StatTile.md +150 -0
  76. package/skills/uxelle-components/Stepper.md +73 -0
  77. package/skills/uxelle-components/Switch.md +41 -0
  78. package/skills/uxelle-components/Tab.md +41 -0
  79. package/skills/uxelle-components/Table.md +55 -0
  80. package/skills/uxelle-components/TabsCompact.md +37 -0
  81. package/skills/uxelle-components/TabsStretch.md +35 -0
  82. package/skills/uxelle-components/Text.md +56 -0
  83. package/skills/uxelle-components/Textarea.md +77 -0
  84. package/skills/uxelle-components/Textfield.md +62 -0
  85. package/skills/uxelle-components/Tooltip.md +33 -0
  86. package/skills/uxelle-design-harness/SKILL.md +149 -0
  87. package/skills/uxelle-design-harness/a2ui.md +73 -0
  88. package/skills/uxelle-design-harness/how-to-accessibility.md +31 -0
  89. package/skills/uxelle-design-harness/how-to-color.md +82 -0
  90. package/skills/uxelle-design-harness/how-to-host.md +108 -0
  91. package/skills/uxelle-design-harness/how-to-page-layout.md +203 -0
  92. package/skills/uxelle-design-harness/principles.md +72 -0
  93. package/skills/uxelle-design-harness/recipe-app-chrome.md +168 -0
  94. package/skills/uxelle-design-harness/recipe-card-grid.md +104 -0
  95. package/skills/uxelle-design-harness/recipe-cta-band.md +91 -0
  96. package/skills/uxelle-design-harness/recipe-dashboard-overview.md +118 -0
  97. package/skills/uxelle-design-harness/recipe-data-table-page.md +323 -0
  98. package/skills/uxelle-design-harness/recipe-feature-section.md +96 -0
  99. package/skills/uxelle-design-harness/recipe-footer.md +156 -0
  100. package/skills/uxelle-design-harness/recipe-form-section.md +163 -0
  101. package/skills/uxelle-design-harness/recipe-hero.md +89 -0
  102. package/skills/uxelle-design-harness/recipe-landing-page.md +94 -0
  103. package/skills/uxelle-design-harness/recipe-logo-wall.md +80 -0
  104. package/skills/uxelle-design-harness/recipe-multi-step-flow.md +108 -0
  105. package/skills/uxelle-design-harness/recipe-page-header.md +102 -0
  106. package/skills/uxelle-design-harness/recipe-page-shell.md +82 -0
  107. package/skills/uxelle-design-harness/recipe-pricing.md +83 -0
  108. package/skills/uxelle-design-harness/recipe-query-bar.md +108 -0
  109. package/skills/uxelle-design-harness/recipe-record-detail.md +96 -0
  110. package/skills/uxelle-design-harness/recipe-settings-page.md +170 -0
  111. package/skills/uxelle-design-harness/recipe-stat-callouts.md +111 -0
  112. package/skills/uxelle-design-harness/recipe-states.md +119 -0
  113. package/skills/uxelle-design-harness/recipe-summary-list.md +88 -0
  114. package/skills/uxelle-design-harness/recipe-template.md +62 -0
  115. package/skills/uxelle-design-harness/recipe-testimonial.md +73 -0
  116. package/skills/uxelle-design-harness/spacing-steps.md +57 -0
  117. package/skills/uxelle-design-harness/tokens.md +101 -0
@@ -0,0 +1,36 @@
1
+ # NavigationSideGroup
2
+
3
+ Import: `import { NavigationSideGroup } from "@uxelle/components";`
4
+
5
+ Landmark list of destination rows in a vertical side rail, with an optional end divider.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | children | `ReactNode` | `—` | `NavigationSideItem` rows, rendered as list items inside this group's `<ul>`. Accordion parents keep their own `NavigationSideSubItem` rows as children, so sub-items are never direct children of the group. |
14
+ | endDivider | `boolean` | `true` | Adds a low-emphasis `Divider` below the last row to separate this group from the next. Set `false` on the last group in the rail. |
15
+ | collapsed | `boolean` | `false` | Icon-only layout for every row in the group: labels hide and each row falls back to a tooltip. A row can opt out with its own `navCollapsed`, and a parent `NavigationSide` with `expanded={false}` turns this on for its groups. |
16
+
17
+ <!-- prettier-ignore-end -->
18
+
19
+ Extends `HTMLAttributes` — supports standard HTML attributes.
20
+
21
+ ## Usage
22
+
23
+ ```tsx
24
+ <NavigationSideGroup aria-label="Product">
25
+ <NavigationSideItem label="Overview" href="/overview" leadingIconName="dashboard" />
26
+ <NavigationSideItem label="Timing" href="/timing" leadingIconName="schedule" />
27
+ </NavigationSideGroup>
28
+ ```
29
+
30
+
31
+ ## Notes
32
+
33
+ - Renders a named `<nav>` containing a `<ul>` of `NavigationSideItem` children (including accordion parents with `NavigationSideSubItem` rows). This is the side navigation for the experience — not a nested landmark inside another `nav`. Compose groups in `NavigationSide` `centerSlotContent`; that chrome is an `<aside>` (or a `<header>` when `mobile`), not a `nav`.
34
+ - Pass `aria-label` (or `aria-labelledby`) when more than one group is on the page so each landmark has a unique name. The default name is “Side navigation”.
35
+ - Set `collapsed` for the icon-only rail layout, or place the group in `NavigationSide` with `expanded={false}` so child rows inherit collapsed spacing unless they set `navCollapsed` themselves.
36
+ - Set `endDivider` to show a low-emphasis horizontal rule after the last row.
@@ -0,0 +1,56 @@
1
+ # NavigationSideItem
2
+
3
+ Import: `import { NavigationSideItem } from "@uxelle/components";`
4
+
5
+ Destination row for a vertical side navigation rail. Shows a leading icon, label, and optional trailing slot.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | label | `string` | `"Label"` | Row text shown beside the leading icon. Also the accessible name: on the collapsed rail the text is hidden, applied as `aria-label`, and surfaced in a tooltip on hover and focus. |
14
+ | leadingIconName | `string` | `"outbound"` | Material Symbol name for the leading icon. Shown at both rail widths — it is the only visible content once the rail collapses. |
15
+ | iconVariant | `"sharpUnfilled" \| "sharpFilled"` | `"sharpUnfilled"` | Fill style for the leading icon: `sharpUnfilled` for the outline glyph, `sharpFilled` for the solid one. |
16
+ | trailingSlot | `boolean` | `false` | Reveals `trailingSlotContent` at the end of the row. Ignored when `accordion` is true (the chevron owns that spot) or when the rail is collapsed. |
17
+ | trailingSlotContent | `ReactNode` | `undefined` | Content for the trailing region, typically a `LabelBadge`, short text, or a decorative icon. Rendered only when `trailingSlot` is true, and it must stay non-interactive because it sits inside the row's own link or button. |
18
+ | subNavigationItemsSlot | `ReactNode` | `undefined` | Nested `NavigationSideSubItem` rows revealed when `accordion` is true; takes precedence over `children`. On the collapsed rail they are re-rendered as a `List` of `ListItem`s inside the flyout. |
19
+ | children | `ReactNode` | `undefined` | Nested `NavigationSideSubItem` rows revealed when `accordion` is true, used when `subNavigationItemsSlot` is omitted. Not the row label — that is `label`. On the collapsed rail they become a `List` of `ListItem`s in the flyout. |
20
+ | activated | `boolean` | `false` | Marks this row as the current page: filled surface plus a leading indicator bar. Destination rows also get `aria-current="page"`; accordion parents do not, so set it on the matching child instead. |
21
+ | navCollapsed | `boolean` | `false` | Icon-only rail layout: hides the label, the trailing slot, and the inline accordion panel, and shows the label in a tooltip on hover and focus. Inherited from the enclosing `NavigationSideGroup` when omitted. |
22
+ | accordion | `boolean` | `false` | Turns the row into an expandable parent with a trailing chevron that reveals its nested rows. The control is always a button, so `href` is ignored. |
23
+ | open | `boolean` | `undefined` | Controlled expanded state of the accordion panel (or of the collapsed-rail flyout). Pass with `onOpenChange`; omit to use `defaultOpen` instead. |
24
+ | defaultOpen | `boolean` | `false` | Whether the accordion panel starts expanded when `open` is omitted. |
25
+ | onOpenChange | `(open: boolean) => void` | `undefined` | Called with the next expanded state each time the accordion parent toggles. |
26
+ | notificationBadge | `boolean` | `false` | Overlays an unread dot on the leading icon. The dot itself is decorative; the state is folded into the control name as `"{label}, notifications"`. |
27
+ | href | `string` | `undefined` | Destination URL, which renders the row as an anchor instead of a button. Ignored when `accordion` is true — put a section landing page in a `NavigationSideSubItem` child instead. |
28
+ | className | `string` | `—` | |
29
+
30
+ <!-- prettier-ignore-end -->
31
+
32
+ ## Usage
33
+
34
+ ```tsx
35
+ <NavigationSideItem label="Dashboard" href="/dashboard" leadingIconName="dashboard" />
36
+ ```
37
+
38
+ ```tsx
39
+ <NavigationSideItem accordion label="Products">
40
+ <NavigationSideSubItem leadingIcon={false} label="Overview" href="/products" />
41
+ <NavigationSideSubItem leadingIcon={false} label="Catalog" href="/products/catalog" />
42
+ </NavigationSideItem>
43
+ ```
44
+
45
+
46
+ ## Notes
47
+
48
+ - Do not use as a top or header nav link (`NavLink`). Nested accordion destinations use `NavigationSideSubItem`.
49
+ - Compose inside `NavigationSideGroup` (a `<nav>` list). This row is a list item; other HTML attributes (`aria-*`, `data-*`, `id`) go to the interactive control, and `className` is on the row wrapper.
50
+ - Renders as an anchor when `href` is set; otherwise a button. Accordion parents are **not** destinations: `href` is ignored and the control is always a button. Put a section landing page in a `NavigationSideSubItem` child instead.
51
+ - Set `activated` for the current page — fill and leading indicator. Destinations also set `aria-current="page"`; accordion parents do not. When a child is the current page, set `activated` on that child, not only on the parent.
52
+ - Set `accordion` for a parent that reveals `subNavigationItemsSlot` (or `children`): `NavigationSideSubItem` rows (`href` for destinations). Omit `open` for uncontrolled (`defaultOpen` + `onOpenChange`). The trigger is a button with `aria-expanded` / `aria-controls`; the nested list animates open and closed (inert and `aria-hidden` when collapsed). When `navCollapsed` is also true, those rows open in a disclosure flyout (`Menu` `pattern="disclosure"`, `direction="Right Top"`) as a `List` of interactive `ListItem`s. Keyboard: Enter/Space toggles; Tab moves through destinations; Tab-out and Escape close and return focus to the trigger.
53
+ - The leading icon is always shown. Group collapsed layout with `NavigationSideGroup` (`collapsed`) or set `navCollapsed`.
54
+ - When `navCollapsed`, the visible label is hidden; pass `label` (or `aria-label`) for the accessible name. A tooltip shows the label on hover and focus.
55
+ - When `notificationBadge` is true, the dot stays decorative; unread state is included in the control name (`"{label}, notifications"`). Override with `aria-label` when you need a different name.
56
+ - Trailing slot content must be non-interactive (no nested buttons or links). Use a badge, text, or decorative icon.
@@ -0,0 +1,39 @@
1
+ # NavigationSideSubItem
2
+
3
+ Import: `import { NavigationSideSubItem } from "@uxelle/components";`
4
+
5
+ Nested destination row under an accordion side-nav parent.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | label | `string` | `"Label"` | Text for this nested destination, indented under its accordion `NavigationSideItem` parent. It is the row's accessible name, and becomes the `ListItem` center text in the collapsed-rail flyout. |
14
+ | leadingIcon | `boolean` | `true` | Renders the leading icon before the label. Set `false` for text-only nested rows, which is the usual look under an accordion parent. |
15
+ | leadingIconName | `string` | `"outbound"` | Material Symbol name for the leading icon. Ignored when `leadingIcon` is false. |
16
+ | iconVariant | `"sharpUnfilled" \| "sharpFilled"` | `"sharpUnfilled"` | Fill style for the leading icon: `sharpUnfilled` for the outline glyph, `sharpFilled` for the solid one. |
17
+ | activated | `boolean` | `false` | Marks this row as the current page: filled surface, leading indicator bar, and `aria-current="page"`. Set it here rather than on the accordion parent. |
18
+ | href | `string` | `undefined` | Destination URL, which renders the row as an anchor instead of a button. Omit for a button that only reports clicks through `onClick`. |
19
+ | className | `string` | `—` | |
20
+
21
+ <!-- prettier-ignore-end -->
22
+
23
+ ## Usage
24
+
25
+ ```tsx
26
+ <NavigationSideItem accordion label="Products">
27
+ <NavigationSideSubItem leadingIcon={false} label="Overview" href="/products" />
28
+ <NavigationSideSubItem leadingIcon={false} label="Catalog" href="/products/catalog" />
29
+ </NavigationSideItem>
30
+ ```
31
+
32
+
33
+ ## Notes
34
+
35
+ - Use as a child of an accordion `NavigationSideItem`, not as a top-level rail row (`NavigationSideItem`). Compose inside `NavigationSideGroup` so this row is a list item in the side `nav`.
36
+ - Renders as an anchor when `href` is set; otherwise a button. `className` is on the row wrapper; other HTML attributes (`aria-*`, `data-*`, `id`) go to the interactive control.
37
+ - Set `activated` for the current page — fill, leading indicator, and `aria-current="page"`.
38
+ - Set `leadingIcon={false}` to hide the icon.
39
+ - When the parent accordion is collapsed (`navCollapsed`), this row is presented as a `ListItem` in the disclosure flyout (a list of links, not a menu).
@@ -0,0 +1,62 @@
1
+ # NotificationBadge
2
+
3
+ Import: `import { NotificationBadge } from "@uxelle/components";`
4
+
5
+ Notification indicator on a corner of its child (typically `Icon` or `IconButton`). Toggle `counter` and `count` for a numeric pill.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | position | `"top-left" \| "top-right"` | `"top-right"` | Corner of the wrapped child the indicator sits on. Use `top-left` when the child's own content or a neighbouring control would collide with the usual top-right placement. |
14
+ | overlap | `"rectangular" \| "circular"` | `—` | Shape of the wrapped child, which sets how far the indicator is pulled toward its center: `circular` tightens the offset for round hosts such as an avatar, `rectangular` suits square icons and buttons. Omit to follow the theme's overlay prop-control (the Open Source theme resolves to circular, the others to rectangular). |
15
+ | counter | `boolean` | `false` | When true the indicator becomes a numeric pill showing `count`; when false it stays a small dot with no text. |
16
+ | count | `number` | `—` | Number shown in the counter pill. Values above 99 render as `99+`, an omitted count is treated as `0`, and the value is ignored while `counter` is false. |
17
+ | dataColorSwitcher | `"neutral" \| "neutral-alt" \| "primary" \| "secondary" \| "tertiary" \| "ancillary" \| "success" \| "warning" \| "danger" \| "info"` | `"danger"` | Palette for the dot or counter fill and its text. Applied as `data-color-switcher` on the indicator only, so the ring separating it from the child keeps the page palette even when the app or a toolbar sets a different one on `:root`. |
18
+ | "data-color-switcher" | `string` | `—` | Attribute spelling of `dataColorSwitcher`, for call sites that spread DOM props. Wins over `dataColorSwitcher` when both are set. |
19
+ | indicatorAriaHidden | `boolean` | `true` | Keeps the dot or counter out of the accessibility tree so it is not announced separately from the control it decorates; instead carry unread state on that control. Set false only when the indicator itself should be read, and pair it with `indicatorLabel` or `indicatorId`. |
20
+ | indicatorId | `string` | `""` | Id placed on the indicator element so the focusable child can point at it with `aria-describedby` and have the count read as part of that control's description. |
21
+ | indicatorAriaLive | `"off" \| "polite" \| "assertive"` | `""` | How count changes are announced once the indicator is exposed with `indicatorAriaHidden={false}`: `polite` waits for a pause, `assertive` interrupts. Ignored while the indicator is hidden from assistive technology. |
22
+ | indicatorLabel | `string` | `""` | Accessible name for a dot-only indicator exposed with `indicatorAriaHidden={false}`, such as "Unread". Ignored when `counter` is true because the visible number names it, and a missing value warns in development. |
23
+ | children (required) | `ReactNode` | `—` | Element the badge decorates, typically an **Icon** or **IconButton**. The wrapper sizes itself to this node and overlays the indicator on its corner, so set icon or control dimensions on the child rather than on the badge. |
24
+ | "aria-label" | `string` | `""` | Accessible name for the wrapper span. Rarely the right place — name the focusable child such as an `IconButton` instead, so unread state is announced with the control. |
25
+ | "aria-labelledby" | `string` | `""` | Space-separated ids of elements that name the wrapper span. As with `aria-label`, prefer naming the focusable child. |
26
+ | "aria-describedby" | `string` | `""` | Space-separated ids of elements that describe the wrapper span. To tie the count to the control instead, set `indicatorId` and reference it from the focusable child. |
27
+
28
+ <!-- prettier-ignore-end -->
29
+
30
+ ## Usage
31
+
32
+ ```tsx
33
+ <NotificationBadge position="top-right">
34
+ <Icon iconName="mail" variant="sharpUnfilled" />
35
+ </NotificationBadge>
36
+ ```
37
+
38
+ ```tsx
39
+ <NotificationBadge position="top-right" counter count={3}>
40
+ <Icon iconName="mail" variant="sharpUnfilled" />
41
+ </NotificationBadge>
42
+ ```
43
+
44
+ ```tsx
45
+ <NotificationBadge dataColorSwitcher="success" position="top-right">
46
+ <Icon iconName="mail" variant="sharpUnfilled" />
47
+ </NotificationBadge>
48
+ ```
49
+
50
+ ```tsx
51
+ <NotificationBadge overlap="circular">
52
+ <Avatar … />
53
+ </NotificationBadge>
54
+ ```
55
+
56
+
57
+ ## Notes
58
+
59
+ - The wrapper sizes to `children`; set icon / control dimensions on the child you pass in.
60
+ - **Accessibility (WCAG-aligned):** By default the indicator is `aria-hidden` so assistive tech does not hear the dot/counter separately from the control it decorates. Reflect unread state on the **focusable** child (`aria-label`, or `aria-describedby` pointing at `indicatorId` when `indicatorAriaHidden={false}`). For dot-only badges with `indicatorAriaHidden={false}`, set `indicatorLabel`. Use `indicatorAriaLive` when counts update and the indicator stays visible to AT. Contrast uses mapped tokens (AA text / non-text pairs from the theme).
61
+ - **Overlap:** Omit `overlap` to follow the theme overlay prop-control (`Rectangular` or `Circular`; unread tokens fall back to rectangular). Pass `rectangular` or `circular` to override that theme default.
62
+ - **Color:** The outer ring uses the page (inherited) `data-color-switcher` surface (`--uxl-color-switcher-background`). Fill and counter text follow `dataColorSwitcher` on the dot only. Storybook’s toolbar palette affects the ring; the prop still scopes danger/success/etc. for the pill.
@@ -0,0 +1,49 @@
1
+ # Pagination
2
+
3
+ Import: `import { Pagination } from "@uxelle/components";`
4
+
5
+ Numbered pagination footer for tables, lists, and card grids.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | currentPage | `number` | `PAGINATION_DEFAULTS.defaultCurrentPage` | Active page when controlled, 1-based and clamped to the available range. Pass with `onChange`; omit it to let the component track its own page from `defaultCurrentPage`. |
14
+ | defaultCurrentPage | `number` | `1` | Page shown on first render when `currentPage` is omitted, 1-based. Read only on mount — after that the component owns the value and reports moves through `onChange`. |
15
+ | onChange | `(page: number) => void` | `—` | Fires with the new 1-based page number, already clamped to the available range. Also fires when a `totalItems` or `itemsPerPage` change forces the current page back into range. |
16
+ | disabled | `boolean` | `false` | Blocks interaction on every control — previous/next, the page numbers, and the items-per-page menu. Labels and the item range stay visible. |
17
+ | summary | `boolean` | `true` | When `true`, renders the leading region with the items-per-page menu and live item-range text. When `false`, only the page controls render. |
18
+ | totalItems | `number` | `100` | Size of the full data set. Drives the item-range label and the page count (`ceil(totalItems / itemsPerPage)`, minimum one page). |
19
+ | itemsPerPage | `number` | `PAGINATION_DEFAULTS.defaultItemsPerPage` | Page size when controlled. Pass with `onItemsPerPageChange`; a value larger than `totalItems` falls back to the largest entry in `itemsPerPageOptions` that fits. |
20
+ | defaultItemsPerPage | `number` | `10` | Page size on first render when `itemsPerPage` is omitted. After mount the component owns the value and reports changes through `onItemsPerPageChange`. |
21
+ | onItemsPerPageChange | `(itemsPerPage: number) => void` | `—` | Fires with the newly selected page size, already clamped. When the new size pushes the current page past the last page, `onChange` also fires with the corrected page. |
22
+ | itemsPerPageOptions | `readonly number[]` | `PAGINATION_DEFAULT_ITEMS_PER_PAGE_OPTIONS` | Page sizes offered in the items-per-page menu, in display order. Also the candidate set when the active size has to shrink to fit `totalItems`. |
23
+ | itemsPerPageLabel | `(itemsPerPage: number) => string` | `—` | Builds the items-per-page trigger text from the active page size; the result is also that button's accessible name. Defaults to `Items per page: 10`. |
24
+ | rangeLabel | `(start: number, end: number, total: number) => string` | `—` | Builds the item-range text from the 1-based `start` and `end` of the current page plus `totalItems`. Rendered in a polite live region; defaults to `11-20 of 100`. |
25
+ | previousLabel | `string` | `"Previous page"` | Accessible name for the icon-only previous-page button — its only label. Translate for non-English locales. |
26
+ | nextLabel | `string` | `"Next page"` | Accessible name for the icon-only next-page button — its only label. Translate for non-English locales. |
27
+ | pagesLabel | `string` | `"Pages"` | Accessible name for the group wrapping previous, the page numbers, and next. Translate for non-English locales. |
28
+ | "aria-label" | `string` | `"Pagination"` | Accessible name for the root `nav` landmark; give each pagination region on a page a distinct value. Translate for non-English locales. |
29
+
30
+ <!-- prettier-ignore-end -->
31
+
32
+ ## Usage
33
+
34
+ ```tsx
35
+ <Pagination
36
+ currentPage={page}
37
+ onChange={setPage}
38
+ totalItems={100}
39
+ itemsPerPage={10}
40
+ onItemsPerPageChange={setItemsPerPage}
41
+ />
42
+ ```
43
+
44
+
45
+ ## Notes
46
+
47
+ - **Summary** — Optional leading region with an items-per-page menu and live range text.
48
+ - **Pages** — Circular page controls with ellipsis for long ranges; previous/next disable on first and last page.
49
+ - **State** — Supports controlled and uncontrolled `currentPage` and `itemsPerPage`.
@@ -0,0 +1,51 @@
1
+ # PaginationComplex
2
+
3
+ Import: `import { PaginationComplex } from "@uxelle/components";`
4
+
5
+ Full data-view pagination with items-per-page and page-jump menus.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | currentPage | `number` | `PAGINATION_COMPLEX_DEFAULTS.defaultCurrentPage` | Active page when controlled, 1-based and clamped to the available range. Pass with `onChange`; omit it to let the component track its own page from `defaultCurrentPage`. |
14
+ | defaultCurrentPage | `number` | `1` | Page shown on first render when `currentPage` is omitted, 1-based. Read only on mount — after that the component owns the value and reports moves through `onChange`. |
15
+ | onChange | `(page: number) => void` | `—` | Fires with the new 1-based page number, already clamped to the available range. Also fires when a `totalItems` or `itemsPerPage` change forces the current page back into range. |
16
+ | disabled | `boolean` | `false` | Blocks interaction on every control — previous/next, the page-jump menu, and the items-per-page menu. Labels and the item range stay visible. |
17
+ | totalItems | `number` | `100` | Size of the full data set. Drives the item-range label and the page count (`ceil(totalItems / itemsPerPage)`, minimum one page). |
18
+ | itemsPerPage | `number` | `PAGINATION_COMPLEX_DEFAULTS.defaultItemsPerPage` | Page size when controlled. Pass with `onItemsPerPageChange`; a value larger than `totalItems` falls back to the largest entry in `itemsPerPageOptions` that fits. |
19
+ | defaultItemsPerPage | `number` | `10` | Page size on first render when `itemsPerPage` is omitted. After mount the component owns the value and reports changes through `onItemsPerPageChange`. |
20
+ | onItemsPerPageChange | `(itemsPerPage: number) => void` | `—` | Fires with the newly selected page size, already clamped. When the new size pushes the current page past the last page, `onChange` also fires with the corrected page. |
21
+ | itemsPerPageOptions | `readonly number[]` | `PAGINATION_DEFAULT_ITEMS_PER_PAGE_OPTIONS` | Page sizes offered in the items-per-page menu, in display order. Also the candidate set when the active size has to shrink to fit `totalItems`. |
22
+ | itemsPerPageLabel | `(itemsPerPage: number) => string` | `—` | Builds the items-per-page trigger text from the active page size; the result is also that button's accessible name. Defaults to `Items per page: 10`. |
23
+ | rangeLabel | `(start: number, end: number, total: number) => string` | `—` | Builds the item-range text from the 1-based `start` and `end` of the current page plus `totalItems`. Rendered in a polite live region; defaults to `11-20 of 100`. |
24
+ | currentPageLabel | `(page: number) => string` | `—` | Builds the page-jump trigger text from the active 1-based page; the result is also that button's accessible name. Defaults to `Page 3`. |
25
+ | pageCountLabel | `(totalPages: number) => string` | `—` | Builds the total-page-count text beside the page-jump trigger, which also describes that button through `aria-describedby`. Defaults to `of 120`. |
26
+ | previousLabel | `string` | `"Previous page"` | Accessible name for the icon-only previous-page button — its only label. Translate for non-English locales. |
27
+ | nextLabel | `string` | `"Next page"` | Accessible name for the icon-only next-page button — its only label. Translate for non-English locales. |
28
+ | pagesLabel | `string` | `"Pages"` | Accessible name for the group wrapping the page-jump menu, the page count, and previous/next. Translate for non-English locales. |
29
+ | "aria-label" | `string` | `"Pagination"` | Accessible name for the root `nav` landmark; give each pagination region on a page a distinct value. Translate for non-English locales. |
30
+
31
+ <!-- prettier-ignore-end -->
32
+
33
+ ## Usage
34
+
35
+ ```tsx
36
+ <PaginationComplex
37
+ currentPage={page}
38
+ onChange={setPage}
39
+ totalItems={1200}
40
+ itemsPerPage={10}
41
+ onItemsPerPageChange={setItemsPerPage}
42
+ />
43
+ ```
44
+
45
+
46
+ ## Notes
47
+
48
+ - **Left** — Items-per-page menu and live item-range text.
49
+ - **Right** — Page-jump menu, total page count, and previous/next controls.
50
+ - **Page jump** — Lists every page up to 100 entries; larger totals window around the current page and always include first and last.
51
+ - **State** — Supports controlled and uncontrolled `currentPage` and `itemsPerPage`.
@@ -0,0 +1,44 @@
1
+ # PaginationSimple
2
+
3
+ Import: `import { PaginationSimple } from "@uxelle/components";`
4
+
5
+ Compact previous/next pagination with an optional item-range label.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | currentPage | `number` | `PAGINATION_SIMPLE_DEFAULTS.defaultCurrentPage` | Active page when controlled, 1-based and clamped to the available range. Pass with `onChange`; omit it to let the component track its own page from `defaultCurrentPage`. |
14
+ | defaultCurrentPage | `number` | `1` | Page shown on first render when `currentPage` is omitted, 1-based. Read only on mount — after that the component owns the value and reports moves through `onChange`. |
15
+ | onChange | `(page: number) => void` | `—` | Fires with the new 1-based page number, already clamped to the available range. Also fires when a `totalItems` or `itemsPerPage` change forces the current page back into range. |
16
+ | disabled | `boolean` | `false` | Blocks interaction on the previous and next buttons. The item-range label stays visible. |
17
+ | label | `boolean` | `true` | When `true`, the item-range text sits between the previous and next buttons. When `false`, it stays in the DOM as a visually hidden polite live region. |
18
+ | totalItems | `number` | `2000` | Size of the full data set. Drives the item-range label and the page count (`ceil(totalItems / itemsPerPage)`, minimum one page). |
19
+ | itemsPerPage | `number` | `25` | Fixed page size used to derive the item range and the page count. There is no items-per-page menu here, so it only changes when you change the prop. |
20
+ | rangeLabel | `(start: number, end: number, total: number) => string` | `—` | Builds the item-range text from the 1-based `start` and `end` of the current page plus `totalItems`. Defaults to `1-25 of 2,000 Items`, and is still exposed to assistive tech when `label` is `false`. |
21
+ | previousLabel | `string` | `"Previous page"` | Accessible name for the icon-only previous-page button — its only label. Translate for non-English locales. |
22
+ | nextLabel | `string` | `"Next page"` | Accessible name for the icon-only next-page button — its only label. Translate for non-English locales. |
23
+ | pagesLabel | `string` | `"Pages"` | Accessible name for the group wrapping previous, the item range, and next. Translate for non-English locales. |
24
+ | "aria-label" | `string` | `"Pagination"` | Accessible name for the root `nav` landmark; give each pagination region on a page a distinct value. Translate for non-English locales. |
25
+
26
+ <!-- prettier-ignore-end -->
27
+
28
+ ## Usage
29
+
30
+ ```tsx
31
+ <PaginationSimple
32
+ currentPage={page}
33
+ onChange={setPage}
34
+ totalItems={2000}
35
+ itemsPerPage={25}
36
+ />
37
+ ```
38
+
39
+
40
+ ## Notes
41
+
42
+ - **Label** — When `label` is true, shows the current item range between chevrons. When false, the range stays in a polite live region for assistive technologies.
43
+ - **Boundaries** — Previous disables on the first page; next disables on the last page.
44
+ - **State** — Supports controlled and uncontrolled `currentPage`.
@@ -0,0 +1,46 @@
1
+ # ProductCard
2
+
3
+ Import: `import { ProductCard } from "@uxelle/components";`
4
+
5
+ Product-oriented card with stacked top, center, and bottom content regions.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | topSlot | `boolean` | `true` | Gate for the first stacked region. Set `false` to suppress it even when `topSlotContent` is set; the region is skipped anyway when that content is empty, so no gap is left behind. |
14
+ | topSlotContent | `ReactNode` | `null` | Content for the first stacked region, typically a `LabelBadge`, a title, and supporting copy. Rendered only when `topSlot` is true and this is non-empty. `ProductCard` takes no `children` — use these slots. |
15
+ | centerSlot | `boolean` | `true` | Gate for the middle stacked region. Set `false` to suppress it even when `centerSlotContent` is set; the region is skipped anyway when that content is empty, so no gap is left behind. |
16
+ | centerSlotContent | `ReactNode` | `null` | Content for the middle stacked region, typically an `Image`. Rendered only when `centerSlot` is true and this is non-empty. |
17
+ | bottomSlot | `boolean` | `true` | Gate for the last stacked region. Set `false` to suppress it even when `bottomSlotContent` is set; the region is skipped anyway when that content is empty, so no gap is left behind. |
18
+ | bottomSlotContent | `ReactNode` | `null` | Content for the last stacked region, typically a `Button` or action group. Rendered only when `bottomSlot` is true and this is non-empty. |
19
+ | padding | `"Default" \| "None"` | `"Default"` | Inner spacing and stroke around the card. `Default` applies theme spacing and a 1px stroke; `None` removes both so media can bleed to the edges. Elevation stays either way. |
20
+
21
+ <!-- prettier-ignore-end -->
22
+
23
+ ## Usage
24
+
25
+ ```tsx
26
+ <ProductCard
27
+ topSlotContent={
28
+ <>
29
+ <LabelBadge label="Category" emphasis="low" />
30
+ <Text type="Display Extra Small">Your product name looks great here</Text>
31
+ <Text type="Body Medium">Describe something here</Text>
32
+ </>
33
+ }
34
+ centerSlotContent={<Image src="/product.jpg" alt="" aspectRatio="1:1" />}
35
+ bottomSlotContent={
36
+ <Button fullWidth emphasis="medium">
37
+ Learn more
38
+ </Button>
39
+ }
40
+ />
41
+ ```
42
+
43
+
44
+ ## Notes
45
+
46
+ - Compose category metadata and titles in `topSlotContent`, media in `centerSlotContent`, and actions in `bottomSlotContent`. Toggle regions with `topSlot`, `centerSlot`, and `bottomSlot`; empty content is not rendered, so unused regions do not add gap. Set `padding="None"` when content should bleed to the edges without stroke. Remaining HTML attributes—including `aria-*`, `id`, and `className`—are forwarded to the root element. Prefer `Card` for a single unconstrained content surface.
@@ -0,0 +1,45 @@
1
+ # Quantity
2
+
3
+ Import: `import { Quantity } from "@uxelle/components";`
4
+
5
+ Numeric quantity control with decrement and increment actions and an editable value field.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | label | `boolean` | `true` | Renders the label row above the stepper. The row also needs non-empty `labelText`; once it is hidden, `aria-label` or `aria-labelledby` are forwarded to the input so the field still has a name. |
14
+ | labelText | `string` | `"Quantity"` | Visible label text tied to the numeric input through `htmlFor`. An empty string hides the whole label row even when `label` is true, so supply `aria-label` in that case. |
15
+ | fieldMessage | `boolean` | `false` | Renders a helper FieldMessage under the stepper and links it to the input with `aria-describedby`. Falls back to a min/max hint when `fieldMessageContent` is empty. |
16
+ | fieldMessageContent | `string` | `""` | Helper copy shown while `fieldMessage` is on. Leave it empty to use the generated hint that reads out the current `min` and `max`. |
17
+ | value | `number` | `QUANTITY_DEFAULT_VALUE` | Current quantity, which makes the field controlled. Pair with `onChange` or the stepper cannot update, and note the value is clamped into `min`–`max` before display. |
18
+ | defaultValue | `number` | `QUANTITY_DEFAULT_VALUE` | Starting quantity when `value` is omitted. Read once on mount and clamped into `min`–`max`; later changes to it are ignored. |
19
+ | min | `number` | `QUANTITY_VALUE_MIN` | Lowest quantity the field accepts. Decrement is disabled here, typed input clamps up to it, and Home jumps straight to it. |
20
+ | max | `number` | `QUANTITY_VALUE_MAX` | Highest quantity the field accepts. Increment is disabled here, typed input clamps down to it, and End jumps straight to it. |
21
+ | onChange | `(value: number) => void` | `—` | Receives the committed, clamped quantity — from the stepper buttons, arrow keys, Home / End, or when typed input commits on Enter or blur. Typing on its own does not fire it. |
22
+ | decrementDisabled | `boolean` | `false` | Forces the decrement button off even away from `min`, where it is already disabled automatically. While off, typed input cannot be clamped below the current quantity. |
23
+ | incrementDisabled | `boolean` | `false` | Forces the increment button off even away from `max`, where it is already disabled automatically. While off, typed input cannot be clamped above the current quantity. |
24
+ | required | `boolean` | `false` | Marks the field mandatory: required indicator on the label plus `required` and `aria-required` on the input. Enforcement is left to the surrounding form. |
25
+ | decrementAriaLabel | `string` | `"Decrease quantity"` | Accessible name announced for the minus button, which has no visible text. Localize it alongside `labelText`. |
26
+ | incrementAriaLabel | `string` | `"Increase quantity"` | Accessible name announced for the plus button, which has no visible text. Localize it alongside `labelText`. |
27
+ | name | `string` | `""` | Form field name on the numeric input, so the quantity is submitted with the surrounding form. Omit it for a standalone stepper. |
28
+ | id | `string` | `"qty-field"` | Id for the numeric input and the `htmlFor` target of the built-in label. Omit it for a generated id; set it when external `aria-describedby` copy or tests need a stable hook. |
29
+
30
+ <!-- prettier-ignore-end -->
31
+
32
+ ## Usage
33
+
34
+ ```tsx
35
+ <Quantity labelText="Quantity" min={0} max={99} defaultValue={1} fieldMessage />
36
+ ```
37
+
38
+
39
+ ## Notes
40
+
41
+ - Enforces `min` and `max` in code and disables the bound stepper at limits.
42
+ - **Form libraries**: `onChange` emits the committed, clamped number (not a DOM event), which is the shape controlled wrappers expect. With react-hook-form, use `Controller` and pass `field.value` to `value` and `field.onChange` to `onChange` — it receives the number directly. Forward `field.ref`, `field.onBlur`, and `field.name` so focus-on-error, touched state, and submission work. Avoid `register`, which expects a native input event.
43
+ - **Accessibility**: `role="spinbutton"` with `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`. Stepper buttons have accessible names; associate helper copy via `aria-describedby`.
44
+ - **Ref**: Forwarded to the `<input>` for focus and form-library integration.
45
+ - **Unlabeled usage**: When `label={true}` (default) but `labelText` is empty, pass `aria-label` or `aria-labelledby`.
@@ -0,0 +1,55 @@
1
+ # Radio
2
+
3
+ Import: `import { Radio } from "@uxelle/components";`
4
+
5
+ Native radio built as `<label>` + `<input type="radio">`.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | checked | `boolean` | `false` | Selected state for a standalone radio — controlled when paired with `onChange`, otherwise only the initial value. Ignored inside RadioGroup, where the group's `value` decides which option is selected. |
14
+ | value | `string` | `""` | Value submitted with the form, and the identity this option is matched against in RadioGroup `value`. Standalone radios fall back to an empty string, and grouped options without one get a generated value that will never match your data. |
15
+ | disabled | `boolean` | `false` | Blocks interaction and applies disabled styling. Grouped options report `aria-disabled` so they stay discoverable, and standalone radios also set the native `disabled` attribute; a disabled RadioGroup does the same to every option. |
16
+ | embedded | `boolean` | `false` | Indicator-only mode for a radio nested in a larger target such as a ListItem row: no hover, active, or focus ring, and `tabIndex={-1}`. The parent must own focus, the accessible name, and keyboard activation. |
17
+ | name | `string` | `—` | Native `name` that groups standalone radios so the browser enforces a single selection. Ignored inside **RadioGroup**, which leaves options unnamed on purpose so Tab reaches each one and submits through a hidden input instead. |
18
+ | onChange | `ChangeEventHandler<HTMLInputElement>` | `—` | Native change handler for a standalone radio; read `event.target.value`. Inside **RadioGroup** the group's own `onChange` runs first and is the one to wire. |
19
+ | label | `boolean` | `true` | Renders `children` beside the dot and points `aria-labelledby` at that text. With `false` there is no visible name, so set `aria-label` or `aria-labelledby` on `inputProps`; a RadioGroup name covers the group, never the option. |
20
+ | children | `ReactNode` | `—` | Label content beside the dot, usually a `Text` node; it becomes the accessible name through `aria-labelledby`. Not rendered when `label` is `false` or `embedded` is set. |
21
+ | id | `string` | `—` | Id applied to the `<input>` and matched by the wrapping label's `htmlFor`. Omit it for a generated id; set it when other elements or tests need a stable hook. |
22
+ | className | `string` | `—` | Class names merged onto the root `<label>`, applied before `labelProps.className` so both survive. The `<input>` keeps its own classes. |
23
+ | labelProps | `Omit<LabelHTMLAttributes<HTMLLabelElement>, "htmlFor" \| "children">` | `—` | Extra attributes spread onto the root `<label>` wrapper — use it for layout hooks, `data-*`, and pointer handlers, not form semantics. `htmlFor` and `children` are managed for you, so they cannot be passed. |
24
+ | inputProps | `RadioInputProps` | `—` | Attributes spread onto the `<input>` before the component's own; put form semantics here (`required`, `form`, `autoComplete`, ARIA). `type`, `name`, `value`, `checked`, `disabled`, `defaultChecked`, `onChange`, and `ref` are managed for you. |
25
+ | "inputProps.aria-describedby" | `string` | `""` | Space-separated ids of elements describing this one option, such as a price or caveat under the label. Group-level helper copy belongs on RadioGroup instead. |
26
+ | "inputProps.aria-label" | `string` | `""` | Accessible name for the option when `label` is false and no visible text exists. Skip it whenever visible label `children` can name the control instead. |
27
+ | "inputProps.aria-labelledby" | `string` | `""` | Space-separated ids of elements naming this option. Merged after the id generated for visible label `children`, so both are announced. |
28
+
29
+ <!-- prettier-ignore-end -->
30
+
31
+ ## Usage
32
+
33
+ ```tsx
34
+ <Radio>
35
+ {/* Content */}
36
+ </Radio>
37
+ ```
38
+
39
+ ## Notes
40
+
41
+ - **Accessibility (WCAG 2.x AA baseline)**
42
+ - **Keyboard** — Native `<input type="radio">` (implicit **`role="radio"`**; **`checked`** exposes state—do not set **`aria-checked`**). **Inside **RadioGroup****, each enabled option uses **`tabIndex={0}`** so **Tab** visits every radio; **arrow keys** move focus and selection. **Enter** / **Space** activate while focused. **`embedded`** uses **`tabIndex={-1}`**.
43
+ - **Option naming** — Visible label text is referenced with **`aria-labelledby`**. Per-option helper copy via **`inputProps`** **`aria-describedby`**.
44
+ - **Disabled** — In a group, **`aria-disabled`** is used (options stay discoverable); standalone radios also set the HTML **`disabled`** attribute for form semantics.
45
+ - **Focus visible** — Keyboard focus is shown with a **`:focus-visible`** outline on the **root `<label>`** (the full hit target), using the design-system focus ring token. **`embedded`** omits the ring; the embedding control must expose a single clear focus indicator.
46
+ - **Name and state** — Checked state is exposed via the native control (not only icon color). The icon is **decorative / redundant** with that state and is marked **`aria-hidden`**. Provide an **accessible name** with visible **`children`**, **`inputProps`** (**`aria-label`** / **`aria-labelledby`**), or **`labelProps`** as appropriate. When **`label={false}`** (no visible label text in this component), set a name on **`inputProps`** so the control is not unnamed.
47
+ - **Form / validation** — Pass **`required`**, **`aria-invalid`**, **`aria-describedby`**, **`aria-errormessage`**, and **`autoComplete`** on **`inputProps`**. Inside **`RadioGroup`**, helper or error text is often associated with the **`fieldset`**; if you describe a specific option, point **`aria-describedby`** at stable **`id`s** on the describing elements.
48
+ - **`labelProps` vs `inputProps`**
49
+ - Spread **`labelProps`** on the root `<label>`. Spread **`inputProps`** on the `<input>` **first**, then the component applies authoritative props (`type`, `name`, `checked`, `disabled`, `value`, `onChange`, `id`), so those always win.
50
+ - Put **form and control** semantics on **`inputProps`**, not on **`labelProps`**: `aria-describedby`, `autoComplete`, `form`. Group-level **`aria-required`**, **`aria-invalid`**, and **`aria-errormessage`** belong on **RadioGroup**. When **not** **`embedded`** and **not** inside **RadioGroup**, you may pass **`tabIndex`** on **`inputProps`**; **inside **RadioGroup****, **`tabIndex`** is **`0`** per enabled option (**`inputProps.tabIndex`** is ignored).
51
+ - **Grouped vs standalone**
52
+ - **Inside **RadioGroup****: selection is owned by the group’s **`value`** / **`defaultValue`**. The **`checked`** prop does **not** control **`checked`** state on the input.
53
+ - **Standalone**: use **`checked`** with **`onChange`** for controlled mode; without **`onChange`**, internal state is seeded from **`checked`** when it is defined; otherwise the option toggles on interaction.
54
+ - **Embedded mode**
55
+ - The radio is not in the tab order (`tabIndex={-1}`). The **embedding parent** must be focusable, have an accessible name, handle keyboard activation, and must not duplicate this control in the tab order ([APG radiobutton pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radiobutton/)).
@@ -0,0 +1,49 @@
1
+ # RadioGroup
2
+
3
+ Import: `import { RadioGroup } from "@uxelle/components";`
4
+
5
+ Fieldset-backed radio group; semantics and a11y rules are documented on **RadioGroupProps**.
6
+
7
+ ## Props
8
+
9
+ <!-- prettier-ignore-start -->
10
+
11
+ | Prop | Type | Default | Description |
12
+ |------|------|---------|-------------|
13
+ | children | `ReactNode` | `—` | The `Radio` options for this group, in DOM order. The group owns selection through `value` / `defaultValue`, so each child only needs its own `value` and its label content; nesting them in fragments or wrapper elements is fine. |
14
+ | name (required) | `string` | `—` | Form field `name` submitted with the selected value through a hidden input. Nested **Radio** controls deliberately omit `name` so Tab reaches every option while the form still sees a single field. |
15
+ | value | `string` | `—` | `value` of the selected option, which makes the group controlled. Pair with `onChange`; omit it to let the group track selection itself from `defaultValue`. |
16
+ | defaultValue | `string` | `—` | `value` selected on first render when uncontrolled. Changing it later resets the uncontrolled selection, so derive it from state that does not churn. |
17
+ | onChange | `(value: string, event: ChangeEvent<HTMLInputElement>) => void` | `—` | Receives the next selected value plus the originating change event. Clicking the already-selected option clears the group, so the handler can be called with an empty string. |
18
+ | label | `boolean` | `false` | Shows the group's visible name above the options and wires it as the accessible name via `aria-labelledby`. Needs non-empty `labelText`; with `false`, name the group with `aria-label` or `aria-labelledby` instead or it is left unnamed. |
19
+ | labelText | `string` | `""` | Text of the visible group name. Blank or whitespace-only text hides the row entirely, leaving the group unnamed unless you pass `aria-label` or `aria-labelledby`. |
20
+ | fieldMessage | `ReactNode` | `—` | Helper or validation content rendered below the options. Strings and numbers are wrapped in a **FieldMessage** styled by `fieldMessageType`; any other node renders as-is, and its container id is linked to the group automatically. |
21
+ | fieldMessageType | `"Info" \| "Danger" \| "Warning" \| "Success"` | `RADIO_GROUP_DEFAULTS.fieldMessageType` | Tone and icon for a string or number `fieldMessage`. `Danger` also marks the group invalid, links the message with `aria-errormessage`, and announces it politely; other types link it with `aria-describedby`. |
22
+ | required | `boolean` | `false` | Sets `aria-required` on the radiogroup so a selection is announced as mandatory. Nothing is enforced natively — validate the group value yourself and report the failure through `fieldMessage`. |
23
+ | invalid | `boolean` | `false` | Marks the group invalid with `aria-invalid` unless `aria-invalid` is passed explicitly. Redundant when `fieldMessageType` is `Danger`, which already implies it. |
24
+ | disabled | `boolean` | `false` | Locks the group: every option reports `aria-disabled` and stops responding, and the hidden submit input is disabled so nothing is sent. The fieldset itself is never natively disabled, so assistive tech can still read the options. |
25
+ | role | `string` | `"radiogroup"` | Role placed on the fieldset, `radiogroup` unless overridden. Change it only for exceptional composition patterns, since arrow-key navigation looks for that role. |
26
+ | "aria-label" | `string` | `""` | Accessible name used when no visible group name is rendered. Ignored while `label` and `labelText` show one, which takes precedence. |
27
+ | "aria-labelledby" | `string` | `""` | Space-separated ids of elements that name the group. Merged with the id generated for the visible group name when `label` and `labelText` are set. |
28
+ | "aria-describedby" | `string` | `""` | Space-separated ids of helper or instruction copy. Merged with the field message region id for non-error messages (Info, Warning, Success). |
29
+ | "aria-errormessage" | `string` | `""` | Id of the element holding the error text. Defaults to the field message region id when `fieldMessageType` is `Danger`, and is only announced while the group is also marked invalid. |
30
+ | "aria-invalid" | `boolean` | `false` | Explicit `aria-invalid` on the radiogroup, overriding whatever the component would infer. Omit it to let `invalid` or a `Danger` field message drive the state. |
31
+ | "aria-required" | `boolean` | `false` | Explicit `aria-required` on the radiogroup, overriding what `required` would set. Use it when the requirement is conditional and `required` cannot express it. |
32
+
33
+ <!-- prettier-ignore-end -->
34
+
35
+ ## Usage
36
+
37
+ ```tsx
38
+ <RadioGroup
39
+ name="plan"
40
+ label
41
+ labelText="Plan"
42
+ defaultValue="a"
43
+ onChange={(next) => setPlan(next)}
44
+ >
45
+ <Radio value="a"><Text type="Body Medium">Plan A</Text></Radio>
46
+ <Radio value="b"><Text type="Body Medium">Plan B</Text></Radio>
47
+ </RadioGroup>
48
+ ```
49
+