@usefragments/ui 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +16 -16
  2. package/dist/assets/fragments-wordmark.cjs +30 -0
  3. package/dist/assets/fragments-wordmark.d.ts +13 -0
  4. package/dist/assets/fragments-wordmark.d.ts.map +1 -0
  5. package/dist/assets/fragments-wordmark.js +30 -0
  6. package/dist/assets/ui.css +3173 -3108
  7. package/dist/components/Accordion/Accordion.module.scss.cjs +8 -8
  8. package/dist/components/Accordion/Accordion.module.scss.js +8 -8
  9. package/dist/components/Accordion/index.cjs +22 -10
  10. package/dist/components/Accordion/index.d.ts.map +1 -1
  11. package/dist/components/Accordion/index.js +22 -10
  12. package/dist/components/Alert/Alert.module.scss.cjs +13 -13
  13. package/dist/components/Alert/Alert.module.scss.js +13 -13
  14. package/dist/components/AppShell/AppShell.module.scss.cjs +22 -10
  15. package/dist/components/AppShell/AppShell.module.scss.js +22 -10
  16. package/dist/components/AppShell/index.cjs +1 -1
  17. package/dist/components/AppShell/index.js +1 -1
  18. package/dist/components/Avatar/Avatar.module.scss.cjs +13 -13
  19. package/dist/components/Avatar/Avatar.module.scss.js +13 -13
  20. package/dist/components/Badge/Badge.module.scss.cjs +16 -16
  21. package/dist/components/Badge/Badge.module.scss.js +16 -16
  22. package/dist/components/BentoGrid/BentoGrid.module.scss.cjs +14 -14
  23. package/dist/components/BentoGrid/BentoGrid.module.scss.js +14 -14
  24. package/dist/components/Box/Box.module.scss.cjs +152 -152
  25. package/dist/components/Box/Box.module.scss.js +152 -152
  26. package/dist/components/Breadcrumbs/Breadcrumbs.module.scss.cjs +8 -8
  27. package/dist/components/Breadcrumbs/Breadcrumbs.module.scss.js +8 -8
  28. package/dist/components/Button/Button.module.scss.cjs +13 -13
  29. package/dist/components/Button/Button.module.scss.js +13 -13
  30. package/dist/components/Card/Card.module.scss.cjs +17 -17
  31. package/dist/components/Card/Card.module.scss.js +17 -17
  32. package/dist/components/Card/index.d.ts +3 -3
  33. package/dist/components/Card/index.d.ts.map +1 -1
  34. package/dist/components/Chart/Chart.module.scss.cjs +15 -15
  35. package/dist/components/Chart/Chart.module.scss.js +15 -15
  36. package/dist/components/Checkbox/Checkbox.module.scss.cjs +11 -11
  37. package/dist/components/Checkbox/Checkbox.module.scss.js +11 -11
  38. package/dist/components/Chip/Chip.module.scss.cjs +15 -15
  39. package/dist/components/Chip/Chip.module.scss.js +15 -15
  40. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
  41. package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
  42. package/dist/components/Collapsible/Collapsible.module.scss.cjs +10 -10
  43. package/dist/components/Collapsible/Collapsible.module.scss.js +10 -10
  44. package/dist/components/ColorPicker/ColorPicker.module.scss.cjs +14 -14
  45. package/dist/components/ColorPicker/ColorPicker.module.scss.js +14 -14
  46. package/dist/components/Combobox/Combobox.module.scss.cjs +23 -23
  47. package/dist/components/Combobox/Combobox.module.scss.js +23 -23
  48. package/dist/components/Command/Command.module.scss.cjs +11 -11
  49. package/dist/components/Command/Command.module.scss.js +11 -11
  50. package/dist/components/ConversationList/ConversationList.module.scss.cjs +10 -10
  51. package/dist/components/ConversationList/ConversationList.module.scss.js +10 -10
  52. package/dist/components/DataTable/DataTable.module.scss.cjs +34 -34
  53. package/dist/components/DataTable/DataTable.module.scss.js +34 -34
  54. package/dist/components/DataTable/index.d.ts +3 -1
  55. package/dist/components/DataTable/index.d.ts.map +1 -1
  56. package/dist/components/DatePicker/DatePicker.module.scss.cjs +33 -33
  57. package/dist/components/DatePicker/DatePicker.module.scss.js +33 -33
  58. package/dist/components/DatePicker/index.d.ts +1 -1
  59. package/dist/components/DatePicker/index.d.ts.map +1 -1
  60. package/dist/components/Dialog/Dialog.module.scss.cjs +14 -14
  61. package/dist/components/Dialog/Dialog.module.scss.js +14 -14
  62. package/dist/components/Drawer/Drawer.module.scss.cjs +28 -28
  63. package/dist/components/Drawer/Drawer.module.scss.js +28 -28
  64. package/dist/components/Drawer/index.cjs +20 -3
  65. package/dist/components/Drawer/index.d.ts.map +1 -1
  66. package/dist/components/Drawer/index.js +20 -3
  67. package/dist/components/Editor/Editor.module.scss.cjs +17 -17
  68. package/dist/components/Editor/Editor.module.scss.js +17 -17
  69. package/dist/components/EmptyState/EmptyState.module.scss.cjs +8 -8
  70. package/dist/components/EmptyState/EmptyState.module.scss.js +8 -8
  71. package/dist/components/Field/Field.module.scss.cjs +4 -4
  72. package/dist/components/Field/Field.module.scss.js +4 -4
  73. package/dist/components/Field/index.cjs +10 -9
  74. package/dist/components/Field/index.d.ts +7 -7
  75. package/dist/components/Field/index.d.ts.map +1 -1
  76. package/dist/components/Field/index.js +10 -9
  77. package/dist/components/Fieldset/Fieldset.module.scss.cjs +3 -3
  78. package/dist/components/Fieldset/Fieldset.module.scss.js +3 -3
  79. package/dist/components/Header/Header.module.scss.cjs +44 -29
  80. package/dist/components/Header/Header.module.scss.js +44 -29
  81. package/dist/components/Header/index.cjs +56 -3
  82. package/dist/components/Header/index.d.ts +25 -3
  83. package/dist/components/Header/index.d.ts.map +1 -1
  84. package/dist/components/Header/index.js +56 -3
  85. package/dist/components/Icon/Icon.module.scss.cjs +8 -8
  86. package/dist/components/Icon/Icon.module.scss.js +8 -8
  87. package/dist/components/IconButton/IconButton.module.scss.cjs +8 -8
  88. package/dist/components/IconButton/IconButton.module.scss.js +8 -8
  89. package/dist/components/Image/Image.module.scss.cjs +27 -27
  90. package/dist/components/Image/Image.module.scss.js +27 -27
  91. package/dist/components/Input/Input.module.scss.cjs +19 -19
  92. package/dist/components/Input/Input.module.scss.js +19 -19
  93. package/dist/components/Link/Link.module.scss.cjs +10 -10
  94. package/dist/components/Link/Link.module.scss.js +10 -10
  95. package/dist/components/List/List.module.scss.cjs +19 -19
  96. package/dist/components/List/List.module.scss.js +19 -19
  97. package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
  98. package/dist/components/Listbox/Listbox.module.scss.js +8 -8
  99. package/dist/components/Loading/Loading.module.scss.cjs +30 -30
  100. package/dist/components/Loading/Loading.module.scss.js +30 -30
  101. package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
  102. package/dist/components/Markdown/Markdown.module.scss.js +1 -1
  103. package/dist/components/Menu/Menu.module.scss.cjs +14 -14
  104. package/dist/components/Menu/Menu.module.scss.js +14 -14
  105. package/dist/components/Message/Message.module.scss.cjs +18 -18
  106. package/dist/components/Message/Message.module.scss.js +18 -18
  107. package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
  108. package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
  109. package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
  110. package/dist/components/Pagination/Pagination.module.scss.js +7 -7
  111. package/dist/components/Popover/Popover.module.scss.cjs +10 -10
  112. package/dist/components/Popover/Popover.module.scss.js +10 -10
  113. package/dist/components/Popover/index.cjs +27 -2
  114. package/dist/components/Popover/index.d.ts.map +1 -1
  115. package/dist/components/Popover/index.js +27 -2
  116. package/dist/components/Progress/Progress.module.scss.cjs +25 -25
  117. package/dist/components/Progress/Progress.module.scss.js +25 -25
  118. package/dist/components/Prompt/Prompt.module.scss.cjs +18 -18
  119. package/dist/components/Prompt/Prompt.module.scss.js +18 -18
  120. package/dist/components/RadioGroup/RadioGroup.module.scss.cjs +17 -17
  121. package/dist/components/RadioGroup/RadioGroup.module.scss.js +17 -17
  122. package/dist/components/RadioGroup/index.cjs +2 -0
  123. package/dist/components/RadioGroup/index.d.ts +2 -0
  124. package/dist/components/RadioGroup/index.d.ts.map +1 -1
  125. package/dist/components/RadioGroup/index.js +2 -0
  126. package/dist/components/ScrollArea/ScrollArea.module.scss.cjs +10 -10
  127. package/dist/components/ScrollArea/ScrollArea.module.scss.js +10 -10
  128. package/dist/components/Select/Select.module.scss.cjs +17 -17
  129. package/dist/components/Select/Select.module.scss.js +17 -17
  130. package/dist/components/Separator/Separator.module.scss.cjs +10 -10
  131. package/dist/components/Separator/Separator.module.scss.js +10 -10
  132. package/dist/components/Sidebar/Sidebar.module.scss.cjs +42 -42
  133. package/dist/components/Sidebar/Sidebar.module.scss.js +42 -42
  134. package/dist/components/Skeleton/Skeleton.module.scss.cjs +38 -38
  135. package/dist/components/Skeleton/Skeleton.module.scss.js +38 -38
  136. package/dist/components/Slider/Slider.module.scss.cjs +12 -12
  137. package/dist/components/Slider/Slider.module.scss.js +12 -12
  138. package/dist/components/Table/Table.module.scss.cjs +18 -18
  139. package/dist/components/Table/Table.module.scss.js +18 -18
  140. package/dist/components/TableOfContents/TableOfContents.module.scss.cjs +15 -15
  141. package/dist/components/TableOfContents/TableOfContents.module.scss.js +15 -15
  142. package/dist/components/Tabs/Tabs.module.scss.cjs +12 -12
  143. package/dist/components/Tabs/Tabs.module.scss.js +12 -12
  144. package/dist/components/Text/Text.module.scss.cjs +51 -51
  145. package/dist/components/Text/Text.module.scss.js +51 -51
  146. package/dist/components/Textarea/Textarea.module.scss.cjs +23 -23
  147. package/dist/components/Textarea/Textarea.module.scss.js +23 -23
  148. package/dist/components/Theme/ThemeToggle.module.scss.cjs +6 -6
  149. package/dist/components/Theme/ThemeToggle.module.scss.js +6 -6
  150. package/dist/components/ThinkingIndicator/ThinkingIndicator.module.scss.cjs +13 -13
  151. package/dist/components/ThinkingIndicator/ThinkingIndicator.module.scss.js +13 -13
  152. package/dist/components/Toast/Toast.module.scss.cjs +21 -21
  153. package/dist/components/Toast/Toast.module.scss.js +21 -21
  154. package/dist/components/Toggle/Toggle.module.scss.cjs +13 -13
  155. package/dist/components/Toggle/Toggle.module.scss.js +13 -13
  156. package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +17 -17
  157. package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +17 -17
  158. package/dist/components/Tooltip/Tooltip.module.scss.cjs +3 -3
  159. package/dist/components/Tooltip/Tooltip.module.scss.js +3 -3
  160. package/dist/data-table.cjs +58 -41
  161. package/dist/data-table.js +59 -41
  162. package/dist/datepicker.cjs +32 -72
  163. package/dist/datepicker.js +32 -72
  164. package/dist/index.cjs +2 -0
  165. package/dist/index.d.ts +1 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +2 -0
  168. package/fragments.json +1 -1
  169. package/package.json +4 -8
  170. package/src/assets/fragments-wordmark.tsx +47 -0
  171. package/src/components/Accordion/index.tsx +13 -3
  172. package/src/components/Alert/Alert.module.scss +15 -2
  173. package/src/components/AppShell/AppShell.module.scss +73 -15
  174. package/src/components/AppShell/index.tsx +1 -1
  175. package/src/components/Button/Button.module.scss +19 -20
  176. package/src/components/Card/Card.module.scss +3 -3
  177. package/src/components/Card/index.tsx +3 -3
  178. package/src/components/CodeBlock/CodeBlock.module.scss +2 -7
  179. package/src/components/Combobox/Combobox.module.scss +3 -2
  180. package/src/components/DataTable/DataTable.contract.json +16 -23
  181. package/src/components/DataTable/index.tsx +25 -4
  182. package/src/components/DatePicker/index.tsx +32 -84
  183. package/src/components/Drawer/index.tsx +38 -12
  184. package/src/components/Field/index.tsx +35 -35
  185. package/src/components/Header/Header.contract.json +41 -1
  186. package/src/components/Header/Header.module.scss +40 -0
  187. package/src/components/Header/index.tsx +88 -2
  188. package/src/components/IconButton/IconButton.contract.json +120 -0
  189. package/src/components/Listbox/Listbox.module.scss +5 -4
  190. package/src/components/Menu/Menu.module.scss +8 -10
  191. package/src/components/Popover/index.tsx +34 -2
  192. package/src/components/RadioGroup/RadioGroup.contract.json +13 -27
  193. package/src/components/RadioGroup/index.tsx +4 -0
  194. package/src/components/Select/Select.module.scss +13 -12
  195. package/src/components/TableOfContents/TableOfContents.module.scss +10 -1
  196. package/src/components/Toast/Toast.module.scss +15 -2
  197. package/src/components/ToggleGroup/ToggleGroup.module.scss +4 -1
  198. package/src/index.ts +1 -0
  199. package/src/styles/globals.scss +13 -10
  200. package/src/tokens/_component-properties.scss +25 -0
  201. package/src/tokens/_computed.scss +22 -4
  202. package/src/tokens/_derive.scss +55 -6
  203. package/src/tokens/_palettes.scss +24 -0
  204. package/src/tokens/_seeds.scss +20 -12
  205. package/src/tokens/_variables.scss +110 -63
@@ -25,12 +25,26 @@ export type HeaderIconSlot =
25
25
 
26
26
  export type HeaderIcons = Partial<Record<HeaderIconRenderState['slot'], HeaderIconSlot>>;
27
27
 
28
+ export type HeaderNavAlign = 'start' | 'center';
29
+ export type HeaderContainer = 'full' | 'page';
30
+
31
+ export interface HeaderElevatedOnScrollOptions {
32
+ /** Scroll offset before the elevated surface is applied */
33
+ threshold?: number;
34
+ }
35
+
28
36
  export interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
29
37
  children: React.ReactNode;
30
38
  /** Header height (default: '56px') */
31
39
  height?: string;
32
40
  /** Position behavior */
33
41
  position?: 'static' | 'fixed' | 'sticky';
42
+ /** Apply the elevated header surface after scroll */
43
+ elevatedOnScroll?: boolean | HeaderElevatedOnScrollOptions;
44
+ /** Navigation alignment inside the header */
45
+ navAlign?: HeaderNavAlign;
46
+ /** Header content width */
47
+ container?: HeaderContainer;
34
48
  /** Optional icon overrides for internal header controls (mobile trigger + nav menu chevron) */
35
49
  icons?: HeaderIcons;
36
50
  }
@@ -39,6 +53,8 @@ export interface HeaderBrandProps extends React.HTMLAttributes<HTMLElement> {
39
53
  children: React.ReactNode;
40
54
  /** Link destination */
41
55
  href?: string;
56
+ /** Render as child element (polymorphic) */
57
+ asChild?: boolean;
42
58
  }
43
59
 
44
60
  export interface HeaderNavProps extends React.HTMLAttributes<HTMLElement> {
@@ -101,6 +117,10 @@ export interface HeaderMobileNavProps {
101
117
  className?: string;
102
118
  }
103
119
 
120
+ export interface HeaderMobileNavActionsProps extends React.HTMLAttributes<HTMLDivElement> {
121
+ children: React.ReactNode;
122
+ }
123
+
104
124
  // ============================================
105
125
  // Internal Context
106
126
  // ============================================
@@ -175,17 +195,37 @@ function HeaderRoot({
175
195
  children,
176
196
  height = '56px',
177
197
  position = 'static',
198
+ elevatedOnScroll = false,
199
+ navAlign = 'start',
200
+ container = 'full',
178
201
  icons,
179
202
  className,
180
203
  style: styleProp,
181
204
  ...htmlProps
182
205
  }: HeaderProps) {
183
206
  const [mobileOpen, setMobileOpen] = React.useState(false);
207
+ const [scrolled, setScrolled] = React.useState(false);
208
+ const shouldElevateOnScroll = Boolean(elevatedOnScroll);
209
+ const scrollThreshold =
210
+ typeof elevatedOnScroll === 'object' ? (elevatedOnScroll.threshold ?? 16) : 16;
211
+
212
+ React.useEffect(() => {
213
+ if (!shouldElevateOnScroll) return;
214
+
215
+ const updateScrolled = () => setScrolled(window.scrollY > scrollThreshold);
216
+ updateScrolled();
217
+ window.addEventListener('scroll', updateScrolled, { passive: true });
218
+ return () => window.removeEventListener('scroll', updateScrolled);
219
+ }, [scrollThreshold, shouldElevateOnScroll]);
184
220
 
185
221
  const classes = [
186
222
  styles.header,
187
223
  position === 'fixed' && styles.fixed,
188
224
  position === 'sticky' && styles.sticky,
225
+ shouldElevateOnScroll && styles.elevatedOnScroll,
226
+ shouldElevateOnScroll && scrolled && styles.scrolled,
227
+ navAlign === 'center' && styles.navCentered,
228
+ container === 'page' && styles.containerPage,
189
229
  className,
190
230
  ].filter(Boolean).join(' ');
191
231
 
@@ -202,7 +242,13 @@ function HeaderRoot({
202
242
  return (
203
243
  <HeaderContext.Provider value={contextValue}>
204
244
  <HeaderIconContext.Provider value={icons}>
205
- <header {...htmlProps} className={classes} style={style} data-position={position}>
245
+ <header
246
+ {...htmlProps}
247
+ className={classes}
248
+ style={style}
249
+ data-position={position}
250
+ data-scrolled={shouldElevateOnScroll ? scrolled : undefined}
251
+ >
206
252
  <div className={styles.container}>
207
253
  {children}
208
254
  </div>
@@ -215,9 +261,30 @@ function HeaderRoot({
215
261
  /**
216
262
  * Header.Brand - Logo/brand slot
217
263
  */
218
- function HeaderBrand({ children, href, className, ...htmlProps }: HeaderBrandProps) {
264
+ function HeaderBrand({
265
+ children,
266
+ href,
267
+ asChild = false,
268
+ className,
269
+ ...htmlProps
270
+ }: HeaderBrandProps) {
219
271
  const classes = [styles.brand, className].filter(Boolean).join(' ');
220
272
 
273
+ if (asChild && React.isValidElement(children)) {
274
+ const childProps = children.props as {
275
+ className?: string;
276
+ onClick?: React.MouseEventHandler<HTMLElement>;
277
+ };
278
+ const { onClick, ...restHtmlProps } = htmlProps as React.HTMLAttributes<HTMLElement>;
279
+
280
+ return React.cloneElement(children, {
281
+ ...restHtmlProps,
282
+ ...(href ? { href } : {}),
283
+ className: [classes, childProps.className].filter(Boolean).join(' '),
284
+ onClick: onClick ? composeEventHandlers(childProps.onClick, onClick) : childProps.onClick,
285
+ } as React.HTMLAttributes<HTMLElement>);
286
+ }
287
+
221
288
  if (href) {
222
289
  return (
223
290
  <a {...htmlProps} href={href} className={classes}>
@@ -604,6 +671,23 @@ function HeaderMobileNavLink({
604
671
  );
605
672
  }
606
673
 
674
+ /**
675
+ * Header.MobileNavActions - Action row inside the mobile drawer
676
+ */
677
+ function HeaderMobileNavActions({
678
+ children,
679
+ className,
680
+ ...htmlProps
681
+ }: HeaderMobileNavActionsProps) {
682
+ const classes = [styles.mobileNavActions, className].filter(Boolean).join(' ');
683
+
684
+ return (
685
+ <div {...htmlProps} className={classes}>
686
+ {children}
687
+ </div>
688
+ );
689
+ }
690
+
607
691
  /**
608
692
  * Header.SkipLink - Skip to main content link (accessibility)
609
693
  */
@@ -641,6 +725,7 @@ export const Header = Object.assign(HeaderRoot, {
641
725
  SkipLink: HeaderSkipLink,
642
726
  MobileNav: HeaderMobileNav,
643
727
  MobileNavLink: HeaderMobileNavLink,
728
+ MobileNavActions: HeaderMobileNavActions,
644
729
  });
645
730
 
646
731
  export {
@@ -657,4 +742,5 @@ export {
657
742
  HeaderSkipLink,
658
743
  HeaderMobileNav,
659
744
  HeaderMobileNavLink,
745
+ HeaderMobileNavActions,
660
746
  };
@@ -0,0 +1,120 @@
1
+ {
2
+ "$schema": "https://usefragments.com/schemas/contract.v1.json",
3
+ "name": "IconButton",
4
+ "description": "Compact square button for icon-only actions in dense toolbars, top bars, and row controls.",
5
+ "category": "forms",
6
+ "tags": ["action", "button", "icon", "toolbar", "interactive"],
7
+ "status": "stable",
8
+ "sourcePath": "src/components/IconButton/index.tsx",
9
+ "exportName": "IconButton",
10
+ "propsSummary": [
11
+ "children: node (required)",
12
+ "variant: ghost|subtle|outlined (default: ghost)",
13
+ "size: sm|md|lg (default: md)",
14
+ "pressed: boolean",
15
+ "aria-label: string",
16
+ "aria-labelledby: string"
17
+ ],
18
+ "props": {
19
+ "children": {
20
+ "type": "node",
21
+ "description": "Icon element rendered inside the button",
22
+ "required": true
23
+ },
24
+ "variant": {
25
+ "type": "enum",
26
+ "description": "Visual treatment for the icon-only action",
27
+ "default": "ghost",
28
+ "required": false,
29
+ "values": ["ghost", "subtle", "outlined"]
30
+ },
31
+ "size": {
32
+ "type": "enum",
33
+ "description": "Control size aligned with adjacent fields and toolbar actions",
34
+ "default": "md",
35
+ "required": false,
36
+ "values": ["sm", "md", "lg"]
37
+ },
38
+ "pressed": {
39
+ "type": "boolean",
40
+ "description": "Marks a toggle-style action as pressed and sets aria-pressed",
41
+ "required": false
42
+ },
43
+ "aria-label": {
44
+ "type": "string",
45
+ "description": "Accessible action name when no labelled-by relationship is available",
46
+ "required": false
47
+ },
48
+ "aria-labelledby": {
49
+ "type": "string",
50
+ "description": "ID reference for an external accessible action label",
51
+ "required": false
52
+ }
53
+ },
54
+ "usage": {
55
+ "when": [
56
+ "Compact icon-only actions in toolbars or top bars",
57
+ "Row actions where a text button would be too wide",
58
+ "Toggle-style icon actions with a visible pressed state"
59
+ ],
60
+ "whenNot": [
61
+ "The action needs a visible text label (use Button)",
62
+ "The control changes a persistent boolean setting (use Switch)",
63
+ "The icon is decorative and not interactive (use Icon)"
64
+ ],
65
+ "guidelines": [
66
+ "Always provide aria-label or aria-labelledby",
67
+ "Use ghost for ordinary toolbar actions and outlined when more separation is needed",
68
+ "Use pressed only when the button represents a persistent toggle state"
69
+ ],
70
+ "accessibility": [
71
+ "Every icon-only button must have an accessible name",
72
+ "The accessible name should describe the action, not the icon",
73
+ "Pressed state must be exposed with aria-pressed"
74
+ ]
75
+ },
76
+ "examples": [
77
+ {
78
+ "name": "Toolbar action",
79
+ "description": "A named icon-only toolbar action",
80
+ "code": "<IconButton aria-label=\"Add item\">\n <PlusIcon aria-hidden />\n</IconButton>"
81
+ },
82
+ {
83
+ "name": "Pressed state",
84
+ "description": "A toggle-style action with its state exposed",
85
+ "code": "import { PushPin as PinIcon } from '@phosphor-icons/react';\n\n<IconButton aria-label=\"Pin item\" pressed>\n <PinIcon aria-hidden />\n</IconButton>"
86
+ }
87
+ ],
88
+ "relations": [
89
+ {
90
+ "component": "Button",
91
+ "relationship": "sibling",
92
+ "note": "Use Button when the action should have a visible text label or broader layout variants"
93
+ },
94
+ {
95
+ "component": "Icon",
96
+ "relationship": "complementary",
97
+ "note": "Icon supplies the visual glyph rendered inside IconButton"
98
+ }
99
+ ],
100
+ "contract": {
101
+ "propsSummary": [
102
+ "children: ReactNode - icon element (required)",
103
+ "variant: ghost|subtle|outlined - visual treatment",
104
+ "size: sm|md|lg - square control size",
105
+ "pressed: boolean - toggle state exposed with aria-pressed",
106
+ "aria-label or aria-labelledby - required accessible name"
107
+ ],
108
+ "a11yRules": ["A11Y_BTN_LABEL", "A11Y_BTN_FOCUS", "A11Y_TARGET_SIZE_MIN"]
109
+ },
110
+ "ai": {
111
+ "compositionPattern": "compound",
112
+ "subComponents": ["Root"]
113
+ },
114
+ "provenance": {
115
+ "source": "migrated",
116
+ "verified": false,
117
+ "frameworkSupport": "native",
118
+ "extractedAt": "2026-07-14T00:00:00.000Z"
119
+ }
120
+ }
@@ -1,5 +1,5 @@
1
- @use '../../tokens/variables' as *;
2
- @use '../../tokens/mixins' as *;
1
+ @use "../../tokens/variables" as *;
2
+ @use "../../tokens/mixins" as *;
3
3
 
4
4
  // ============================================
5
5
  // Listbox container
@@ -10,7 +10,7 @@
10
10
  @include popup-container;
11
11
 
12
12
  // Item height derived from text-base font + vertical padding (matches Select/Combobox)
13
- --_item-h: calc(
13
+ --_listbox-item-h: calc(
14
14
  var(--fui-font-size-sm, #{$fui-font-size-sm}) *
15
15
  var(--fui-line-height-normal, #{$fui-line-height-normal}) +
16
16
  var(--fui-space-2, #{$fui-space-2}) * 2
@@ -19,7 +19,8 @@
19
19
  min-width: 12rem;
20
20
  // Show N items + half-peek scroll hint (default 4.5 items)
21
21
  max-height: calc(
22
- var(--_item-h) * var(--fui-select-max-items, 4.5) + var(--fui-space-1, #{$fui-space-1}) * 2
22
+ var(--_listbox-item-h) * var(--fui-select-max-items, 4.5) +
23
+ var(--fui-space-1, #{$fui-space-1}) * 2
23
24
  );
24
25
  overflow-y: auto;
25
26
  border-color: var(--fui-form-group-border, $fui-border);
@@ -8,6 +8,7 @@ $fui-arrow-size: 10px !default;
8
8
  .positioner {
9
9
  z-index: 52;
10
10
  outline: none;
11
+ pointer-events: auto;
11
12
  }
12
13
 
13
14
  // Popup container
@@ -19,23 +20,21 @@ $fui-arrow-size: 10px !default;
19
20
  margin: 0;
20
21
  min-width: 12rem;
21
22
 
22
- // Animation
23
- opacity: 0;
24
- transform: scale(0.95);
23
+ // Menus must be fully hit-testable as soon as they open. A fade/scale
24
+ // entrance can let fast clicks pass through to page links under the portal.
25
+ opacity: 1;
26
+ transform: none;
25
27
  transform-origin: var(--transform-origin);
26
- transition:
27
- opacity var(--fui-transition-fast, $fui-transition-fast),
28
- transform var(--fui-transition-fast, $fui-transition-fast);
29
28
 
30
29
  &[data-open] {
31
30
  opacity: 1;
32
- transform: scale(1);
31
+ transform: none;
33
32
  }
34
33
 
35
34
  &[data-starting-style],
36
35
  &[data-ending-style] {
37
- opacity: 0;
38
- transform: scale(0.95);
36
+ opacity: 1;
37
+ transform: none;
39
38
  }
40
39
  }
41
40
 
@@ -204,7 +203,6 @@ $fui-arrow-size: 10px !default;
204
203
 
205
204
  @media (prefers-reduced-motion: reduce) {
206
205
  .popup {
207
- transition: none;
208
206
  transform: none;
209
207
 
210
208
  &[data-starting-style],
@@ -110,6 +110,28 @@ function CloseIcon() {
110
110
  );
111
111
  }
112
112
 
113
+ // ============================================
114
+ // Helpers
115
+ // ============================================
116
+
117
+ /**
118
+ * Base UI's button-like triggers default `nativeButton` to `true`. When an
119
+ * `asChild` trigger renders something other than a native `<button>` (an
120
+ * anchor, or a component rendered `as="a"`), that default is wrong and Base UI
121
+ * warns that native button semantics were removed. Resolve the correct value
122
+ * from the child so the trigger stays honest and the warning goes away.
123
+ */
124
+ function resolveNativeButton(child: React.ReactElement): boolean {
125
+ const type = child.type;
126
+ // Intrinsic elements: only `<button>` is a native button.
127
+ if (typeof type === 'string') return type === 'button';
128
+ // Custom components render a native `<button>` by default; treat anchors
129
+ // (via `as="a"` or an `href`) as non-native.
130
+ const props = (child.props ?? {}) as Record<string, unknown>;
131
+ if (props.as === 'a' || props.href != null) return false;
132
+ return true;
133
+ }
134
+
113
135
  // ============================================
114
136
  // Components
115
137
  // ============================================
@@ -139,7 +161,12 @@ function PopoverTrigger({ children, asChild, className, ...htmlProps }: PopoverT
139
161
  throw new Error('Popover.Trigger with asChild requires a single valid React element child.');
140
162
  }
141
163
  return (
142
- <BasePopover.Trigger {...htmlProps} className={className} render={children as React.ReactElement}>
164
+ <BasePopover.Trigger
165
+ {...htmlProps}
166
+ nativeButton={resolveNativeButton(children as React.ReactElement)}
167
+ className={className}
168
+ render={children as React.ReactElement}
169
+ >
143
170
  {null}
144
171
  </BasePopover.Trigger>
145
172
  );
@@ -240,7 +267,12 @@ function PopoverClose({ children, asChild, className, ...htmlProps }: PopoverClo
240
267
  throw new Error('Popover.Close with asChild requires a single valid React element child.');
241
268
  }
242
269
  return (
243
- <BasePopover.Close {...htmlProps} className={className} render={children as React.ReactElement}>
270
+ <BasePopover.Close
271
+ {...htmlProps}
272
+ nativeButton={resolveNativeButton(children as React.ReactElement)}
273
+ className={className}
274
+ render={children as React.ReactElement}
275
+ >
244
276
  {null}
245
277
  </BasePopover.Close>
246
278
  );
@@ -3,12 +3,7 @@
3
3
  "name": "RadioGroup",
4
4
  "description": "Single selection from a list of mutually exclusive options",
5
5
  "category": "forms",
6
- "tags": [
7
- "form",
8
- "radio",
9
- "selection",
10
- "options"
11
- ],
6
+ "tags": ["form", "radio", "selection", "options"],
12
7
  "status": "stable",
13
8
  "sourcePath": "src/components/RadioGroup/index.tsx",
14
9
  "exportName": "RadioGroup",
@@ -26,6 +21,7 @@
26
21
  "size: sm|md|lg (default: md)",
27
22
  "wrapperClassName: string",
28
23
  "groupClassName: string",
24
+ "groupId: string",
29
25
  "children: node (required)"
30
26
  ],
31
27
  "props": {
@@ -54,10 +50,7 @@
54
50
  "description": "Layout orientation",
55
51
  "default": "vertical",
56
52
  "required": false,
57
- "values": [
58
- "horizontal",
59
- "vertical"
60
- ]
53
+ "values": ["horizontal", "vertical"]
61
54
  },
62
55
  "disabled": {
63
56
  "type": "boolean",
@@ -90,21 +83,14 @@
90
83
  "description": "Size variant",
91
84
  "default": "md",
92
85
  "required": false,
93
- "values": [
94
- "sm",
95
- "md",
96
- "lg"
97
- ]
86
+ "values": ["sm", "md", "lg"]
98
87
  },
99
88
  "variant": {
100
89
  "type": "enum",
101
90
  "description": "Visual variant. 'default' is the inline radio, 'card' renders each item as a full-width clickable card with the radio tucked inside.",
102
91
  "default": "default",
103
92
  "required": false,
104
- "values": [
105
- "default",
106
- "card"
107
- ]
93
+ "values": ["default", "card"]
108
94
  },
109
95
  "wrapperClassName": {
110
96
  "type": "string",
@@ -116,6 +102,11 @@
116
102
  "description": "Class name for the inner radio group element",
117
103
  "required": false
118
104
  },
105
+ "groupId": {
106
+ "type": "string",
107
+ "description": "ID for the inner element with role=radiogroup; the inherited id remains on the outer field wrapper",
108
+ "required": false
109
+ },
119
110
  "children": {
120
111
  "type": "node",
121
112
  "description": "RadioGroup.Item elements",
@@ -213,19 +204,14 @@
213
204
  "error: boolean | string - error styling and message",
214
205
  "disabled: boolean - disable all options",
215
206
  "wrapperClassName/groupClassName - explicit styling targets for wrapper and group",
207
+ "groupId: string - ID forwarded to the inner radiogroup while id remains on the wrapper",
216
208
  "RadioGroup.Item supports helperText (preferred) and description (legacy alias) plus controlClassName/contentClassName for item-level styling"
217
209
  ],
218
- "a11yRules": [
219
- "A11Y_RADIO_GROUP",
220
- "A11Y_LABEL_REQUIRED",
221
- "A11Y_TARGET_SIZE_MIN"
222
- ]
210
+ "a11yRules": ["A11Y_RADIO_GROUP", "A11Y_LABEL_REQUIRED", "A11Y_TARGET_SIZE_MIN"]
223
211
  },
224
212
  "ai": {
225
213
  "compositionPattern": "compound",
226
- "subComponents": [
227
- "Item"
228
- ]
214
+ "subComponents": ["Item"]
229
215
  },
230
216
  "provenance": {
231
217
  "source": "migrated",
@@ -61,6 +61,8 @@ export interface RadioGroupProps extends Omit<
61
61
  wrapperClassName?: string;
62
62
  /** Class applied to the inner radio group container */
63
63
  groupClassName?: string;
64
+ /** ID applied to the inner element with `role="radiogroup"`. The inherited `id` remains on the outer field wrapper for compatibility. */
65
+ groupId?: string;
64
66
  /** Children (Radio.Item components) */
65
67
  children: React.ReactNode;
66
68
  }
@@ -216,6 +218,7 @@ const RadioGroupRoot = React.forwardRef<HTMLDivElement, RadioGroupProps>(functio
216
218
  variant = "default",
217
219
  wrapperClassName,
218
220
  groupClassName,
221
+ groupId,
219
222
  children,
220
223
  className,
221
224
  "aria-label": ariaLabel,
@@ -252,6 +255,7 @@ const RadioGroupRoot = React.forwardRef<HTMLDivElement, RadioGroupProps>(functio
252
255
  </span>
253
256
  )}
254
257
  <BaseRadioGroup
258
+ id={groupId}
255
259
  value={value}
256
260
  defaultValue={defaultValue}
257
261
  onValueChange={handleValueChange}
@@ -1,5 +1,5 @@
1
- @use '../../tokens/variables' as *;
2
- @use '../../tokens/mixins' as *;
1
+ @use "../../tokens/variables" as *;
2
+ @use "../../tokens/mixins" as *;
3
3
 
4
4
  // Form field wrapper (label + helper + error)
5
5
  @include form-field-wrapper;
@@ -10,7 +10,7 @@
10
10
  @include interactive-base;
11
11
  @include text-base;
12
12
  @include field-shell;
13
- @include control-size('md', 'input');
13
+ @include control-size("md", "input");
14
14
 
15
15
  display: inline-flex;
16
16
  align-items: center;
@@ -27,11 +27,11 @@
27
27
 
28
28
  // Trigger size variants
29
29
  .triggerSm {
30
- @include control-size('sm', 'input');
30
+ @include control-size("sm", "input");
31
31
  }
32
32
 
33
33
  .triggerLg {
34
- @include control-size('lg', 'input');
34
+ @include control-size("lg", "input");
35
35
  }
36
36
 
37
37
  // Value display
@@ -86,17 +86,18 @@
86
86
  border-color: var(--fui-form-group-border, $fui-border);
87
87
 
88
88
  // Item height derived from text-base font + vertical padding
89
- --_item-h: calc(
90
- var(--fui-font-size-sm, #{$fui-font-size-sm}) * var(--fui-line-height-normal, #{$fui-line-height-normal}) +
91
- var(--fui-space-2, #{$fui-space-2}) * 2
89
+ --_select-item-h: calc(
90
+ var(--fui-font-size-sm, #{$fui-font-size-sm}) *
91
+ var(--fui-line-height-normal, #{$fui-line-height-normal}) +
92
+ var(--fui-space-2, #{$fui-space-2}) * 2
92
93
  );
93
94
 
94
95
  min-width: var(--anchor-width);
95
96
  // Show N items + half-peek scroll hint (default 4.5 items)
96
97
  // !important needed to override Base UI's inline max-height: 100%
97
98
  max-height: calc(
98
- var(--_item-h) * var(--fui-select-max-items, 4.5) +
99
- var(--fui-space-1, #{$fui-space-1}) * 2
99
+ var(--_select-item-h) * var(--fui-select-max-items, 4.5) + var(--fui-space-1, #{$fui-space-1}) *
100
+ 2
100
101
  ) !important;
101
102
  overflow-y: auto !important;
102
103
 
@@ -214,12 +215,12 @@
214
215
  background-color: var(--fui-bg-elevated, $fui-bg-elevated);
215
216
  border: 1px solid var(--fui-border, $fui-border);
216
217
 
217
- &[data-side='top'] {
218
+ &[data-side="top"] {
218
219
  border-top: none;
219
220
  border-left: none;
220
221
  }
221
222
 
222
- &[data-side='bottom'] {
223
+ &[data-side="bottom"] {
223
224
  border-bottom: none;
224
225
  border-right: none;
225
226
  }
@@ -176,6 +176,15 @@ $rail-from-item-1: $rail-x-1 - $rail-pad; // 3px
176
176
  // Item link
177
177
  // ============================================
178
178
 
179
+ // ── Plain index (design-system default) ──────────────────────────────────────
180
+ // The "on this page" index reads as a quiet list — the active row is signalled
181
+ // by ink + medium weight (see .active), not a coloured rail. Drop the per-row
182
+ // rail and elbow pseudo-elements the snake geometry above would otherwise draw.
183
+ .root [data-depth]::before,
184
+ .root [data-depth]::after {
185
+ content: none;
186
+ }
187
+
179
188
  .link {
180
189
  display: flex;
181
190
  align-items: center;
@@ -231,7 +240,7 @@ $rail-from-item-1: $rail-x-1 - $rail-pad; // 3px
231
240
  }
232
241
 
233
242
  .active {
234
- color: var(--fui-color-accent, $fui-color-accent);
243
+ color: var(--fui-text-primary, $fui-text-primary);
235
244
  font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
236
245
  }
237
246
 
@@ -63,7 +63,9 @@
63
63
 
64
64
  .toast {
65
65
  display: flex;
66
- align-items: flex-start;
66
+ // Single-line toasts (title only) read best vertically centered; toasts
67
+ // with a description are multi-line and top-align — see `:has(.description)`.
68
+ align-items: center;
67
69
  gap: var(--fui-space-1, $fui-space-1);
68
70
  padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
69
71
  background-color: var(--fui-bg-elevated, $fui-bg-elevated);
@@ -76,6 +78,12 @@
76
78
  animation: toastEnter var(--fui-transition-normal, $fui-transition-normal);
77
79
  }
78
80
 
81
+ // With a description the toast is multi-line: top-align icon/close to the
82
+ // first line rather than the block centre.
83
+ .toast:has(.description) {
84
+ align-items: flex-start;
85
+ }
86
+
79
87
  @keyframes toastEnter {
80
88
  from {
81
89
  opacity: 0;
@@ -177,7 +185,6 @@
177
185
  flex-shrink: 0;
178
186
  width: 1.25rem;
179
187
  height: 1.25rem;
180
- margin-top: 1px;
181
188
 
182
189
  svg {
183
190
  width: 100%;
@@ -185,6 +192,12 @@
185
192
  }
186
193
  }
187
194
 
195
+ // Nudge the icon to meet the first line only in multi-line toasts; centered
196
+ // single-line toasts need no offset.
197
+ .toast:has(.description) .icon {
198
+ margin-top: 1px;
199
+ }
200
+
188
201
  .content {
189
202
  flex: 1;
190
203
  min-width: 0;
@@ -37,7 +37,10 @@
37
37
  .item {
38
38
  background-color: transparent;
39
39
  border: 1px solid transparent;
40
- border-radius: var(--fui-radius-sm, $fui-radius-sm);
40
+ // Match the container's radius (form-group-surface = --fui-radius-lg),
41
+ // less its 1px inset border, so the selected fill nests inside the
42
+ // container corners instead of poking past them.
43
+ border-radius: calc(var(--fui-radius-lg, #{$fui-radius-lg}) - 1px);
41
44
 
42
45
  &.selected {
43
46
  background-color: var(--fui-toggle-group-selected-bg, var(--fui-bg-hover, #{$fui-bg-hover}));