@fragments-sdk/ui 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +9 -4
  3. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  4. package/src/components/Accordion/Accordion.module.scss +111 -0
  5. package/src/components/Accordion/index.tsx +271 -0
  6. package/src/components/Alert/Alert.fragment.tsx +66 -41
  7. package/src/components/Alert/Alert.module.scss +31 -21
  8. package/src/components/Alert/index.tsx +202 -73
  9. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  10. package/src/components/AppShell/AppShell.module.scss +213 -0
  11. package/src/components/AppShell/index.tsx +398 -0
  12. package/src/components/Avatar/index.tsx +8 -9
  13. package/src/components/Badge/Badge.module.scss +16 -10
  14. package/src/components/Badge/index.tsx +20 -6
  15. package/src/components/Box/Box.fragment.tsx +168 -0
  16. package/src/components/Box/Box.module.scss +84 -0
  17. package/src/components/Box/index.tsx +78 -0
  18. package/src/components/Button/Button.module.scss +42 -0
  19. package/src/components/Button/index.tsx +67 -33
  20. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  21. package/src/components/ButtonGroup/index.tsx +40 -0
  22. package/src/components/Card/Card.fragment.tsx +51 -25
  23. package/src/components/Card/Card.module.scss +52 -5
  24. package/src/components/Card/index.tsx +154 -53
  25. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  26. package/src/components/Checkbox/index.tsx +3 -4
  27. package/src/components/CodeBlock/CodeBlock.fragment.tsx +201 -0
  28. package/src/components/CodeBlock/CodeBlock.module.scss +224 -0
  29. package/src/components/CodeBlock/index.tsx +385 -0
  30. package/src/components/ColorChip/ColorChip.module.scss +165 -0
  31. package/src/components/ColorChip/index.tsx +157 -0
  32. package/src/components/ColorPicker/ColorPicker.module.scss +109 -0
  33. package/src/components/ColorPicker/index.tsx +107 -0
  34. package/src/components/Dialog/Dialog.fragment.tsx +9 -0
  35. package/src/components/Dialog/Dialog.module.scss +26 -7
  36. package/src/components/Dialog/index.tsx +12 -15
  37. package/src/components/EmptyState/EmptyState.fragment.tsx +54 -71
  38. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  39. package/src/components/EmptyState/index.tsx +104 -69
  40. package/src/components/Field/Field.fragment.tsx +165 -0
  41. package/src/components/Field/Field.module.scss +31 -0
  42. package/src/components/Field/index.tsx +143 -0
  43. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  44. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  45. package/src/components/Fieldset/index.tsx +47 -0
  46. package/src/components/Form/Form.fragment.tsx +286 -0
  47. package/src/components/Form/Form.module.scss +8 -0
  48. package/src/components/Form/index.tsx +53 -0
  49. package/src/components/Grid/Grid.fragment.tsx +17 -17
  50. package/src/components/Grid/index.tsx +6 -1
  51. package/src/components/Header/Header.fragment.tsx +192 -0
  52. package/src/components/Header/Header.module.scss +209 -0
  53. package/src/components/Header/index.tsx +363 -0
  54. package/src/components/Icon/Icon.fragment.tsx +138 -0
  55. package/src/components/Icon/Icon.module.scss +38 -0
  56. package/src/components/Icon/index.tsx +58 -0
  57. package/src/components/Image/Image.fragment.tsx +195 -0
  58. package/src/components/Image/Image.module.scss +77 -0
  59. package/src/components/Image/index.tsx +95 -0
  60. package/src/components/Input/Input.module.scss +75 -2
  61. package/src/components/Input/index.tsx +60 -21
  62. package/src/components/Link/Link.fragment.tsx +132 -0
  63. package/src/components/Link/Link.module.scss +67 -0
  64. package/src/components/Link/index.tsx +57 -0
  65. package/src/components/List/List.fragment.tsx +152 -0
  66. package/src/components/List/List.module.scss +71 -0
  67. package/src/components/List/index.tsx +106 -0
  68. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  69. package/src/components/Listbox/Listbox.module.scss +97 -0
  70. package/src/components/Listbox/index.tsx +121 -0
  71. package/src/components/Menu/Menu.fragment.tsx +9 -0
  72. package/src/components/Menu/Menu.module.scss +17 -1
  73. package/src/components/Menu/index.tsx +3 -3
  74. package/src/components/Popover/Popover.fragment.tsx +9 -0
  75. package/src/components/Popover/Popover.module.scss +33 -10
  76. package/src/components/Popover/index.tsx +9 -11
  77. package/src/components/Progress/Progress.module.scss +11 -11
  78. package/src/components/Progress/index.tsx +34 -7
  79. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  80. package/src/components/Prompt/Prompt.module.scss +243 -0
  81. package/src/components/Prompt/index.tsx +439 -0
  82. package/src/components/RadioGroup/RadioGroup.module.scss +3 -3
  83. package/src/components/RadioGroup/index.tsx +3 -4
  84. package/src/components/Select/Select.fragment.tsx +9 -0
  85. package/src/components/Select/index.tsx +6 -7
  86. package/src/components/Separator/index.tsx +7 -3
  87. package/src/components/Sidebar/Sidebar.fragment.tsx +783 -0
  88. package/src/components/Sidebar/Sidebar.module.scss +586 -0
  89. package/src/components/Sidebar/index.tsx +1013 -0
  90. package/src/components/Skeleton/Skeleton.fragment.tsx +5 -5
  91. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  92. package/src/components/Slider/Slider.module.scss +87 -0
  93. package/src/components/Slider/index.tsx +88 -0
  94. package/src/components/Stack/Stack.module.scss +120 -0
  95. package/src/components/Stack/index.tsx +148 -0
  96. package/src/components/Table/Table.fragment.tsx +7 -0
  97. package/src/components/Table/Table.module.scss +57 -0
  98. package/src/components/Table/index.tsx +44 -6
  99. package/src/components/Tabs/Tabs.fragment.tsx +9 -0
  100. package/src/components/Tabs/Tabs.module.scss +25 -10
  101. package/src/components/Tabs/index.tsx +11 -8
  102. package/src/components/Text/Text.module.scss +82 -0
  103. package/src/components/Text/index.tsx +58 -0
  104. package/src/components/Textarea/index.tsx +3 -7
  105. package/src/components/Theme/Theme.fragment.tsx +128 -0
  106. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  107. package/src/components/Theme/index.tsx +343 -0
  108. package/src/components/Toast/Toast.fragment.tsx +5 -5
  109. package/src/components/Toast/Toast.module.scss +16 -1
  110. package/src/components/Toast/index.tsx +27 -11
  111. package/src/components/Toggle/Toggle.module.scss +25 -10
  112. package/src/components/Toggle/index.tsx +12 -0
  113. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  114. package/src/components/ToggleGroup/index.tsx +144 -0
  115. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  116. package/src/components/Tooltip/index.tsx +4 -2
  117. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  118. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  119. package/src/components/VisuallyHidden/index.tsx +29 -0
  120. package/src/index.ts +241 -3
  121. package/src/recipes/AppShell.recipe.ts +175 -0
  122. package/src/recipes/CardGrid.recipe.ts +6 -2
  123. package/src/recipes/ChatInterface.recipe.ts +87 -0
  124. package/src/recipes/CodeExamples.recipe.ts +66 -0
  125. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  126. package/src/recipes/DashboardNav.recipe.ts +183 -0
  127. package/src/recipes/LoginForm.recipe.ts +8 -1
  128. package/src/recipes/SettingsPage.recipe.ts +37 -20
  129. package/src/styles/globals.scss +31 -0
  130. package/src/tokens/_index.scss +3 -0
  131. package/src/tokens/_mixins.scss +54 -1
  132. package/src/tokens/_variables.scss +429 -64
  133. package/src/utils/a11y.tsx +439 -0
@@ -0,0 +1,191 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Listbox } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Listbox,
7
+
8
+ meta: {
9
+ name: 'Listbox',
10
+ description: 'Controlled listbox for search results, autocomplete dropdowns, and command menus. Provides Menu-like styling without requiring a trigger.',
11
+ category: 'forms',
12
+ status: 'stable',
13
+ tags: ['listbox', 'search', 'autocomplete', 'combobox', 'command', 'dropdown'],
14
+ since: '0.3.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Search result dropdowns',
20
+ 'Autocomplete suggestions',
21
+ 'Command palette results',
22
+ 'Keyboard-navigable option lists',
23
+ ],
24
+ whenNot: [
25
+ 'Static lists without selection (use List)',
26
+ 'Action menus with trigger button (use Menu)',
27
+ 'Form field selection (use Select)',
28
+ 'Navigation menus (use Sidebar or Tabs)',
29
+ ],
30
+ guidelines: [
31
+ 'Control open/close state externally based on input focus or query',
32
+ 'Implement keyboard navigation (arrow keys, enter, escape) in parent',
33
+ 'Use Listbox.Empty for no results state',
34
+ 'Group related items with Listbox.Group when appropriate',
35
+ ],
36
+ accessibility: [
37
+ 'Uses listbox and option ARIA roles',
38
+ 'aria-selected indicates current selection',
39
+ 'aria-disabled for non-interactive items',
40
+ 'Connect to input with aria-controls for full combobox pattern',
41
+ ],
42
+ },
43
+
44
+ props: {
45
+ children: {
46
+ type: 'node',
47
+ description: 'Listbox.Item, Listbox.Group, or Listbox.Empty components',
48
+ required: true,
49
+ },
50
+ 'aria-label': {
51
+ type: 'string',
52
+ description: 'Accessible label for the listbox',
53
+ },
54
+ className: {
55
+ type: 'string',
56
+ description: 'Additional CSS class',
57
+ },
58
+ style: {
59
+ type: 'object',
60
+ description: 'Inline styles',
61
+ },
62
+ },
63
+
64
+ relations: [
65
+ { component: 'Input', relationship: 'sibling', note: 'Pair with Input for search/autocomplete patterns' },
66
+ { component: 'Menu', relationship: 'alternative', note: 'Use Menu when you need a trigger button' },
67
+ { component: 'Select', relationship: 'alternative', note: 'Use Select for form field selection' },
68
+ { component: 'List', relationship: 'alternative', note: 'Use List for static, non-interactive lists' },
69
+ ],
70
+
71
+ contract: {
72
+ propsSummary: [
73
+ 'children: ReactNode - Listbox.Item components (required)',
74
+ 'aria-label: string - accessible label',
75
+ 'Listbox.Item selected: boolean - highlight state',
76
+ 'Listbox.Item disabled: boolean - non-interactive',
77
+ ],
78
+ subComponents: [
79
+ {
80
+ name: 'Listbox.Item',
81
+ props: [
82
+ 'children: ReactNode - item content',
83
+ 'selected: boolean - highlight/selected state',
84
+ 'disabled: boolean - non-interactive',
85
+ 'onClick: () => void - click handler',
86
+ 'onMouseEnter: () => void - hover handler',
87
+ ],
88
+ },
89
+ {
90
+ name: 'Listbox.Group',
91
+ props: [
92
+ 'children: ReactNode - grouped items',
93
+ 'label: string - group heading',
94
+ ],
95
+ },
96
+ {
97
+ name: 'Listbox.Empty',
98
+ props: [
99
+ 'children: ReactNode - empty state message',
100
+ ],
101
+ },
102
+ ],
103
+ scenarioTags: [
104
+ 'form.search',
105
+ 'form.autocomplete',
106
+ 'action.command',
107
+ 'display.results',
108
+ ],
109
+ a11yRules: ['A11Y_LISTBOX_ROLE', 'A11Y_OPTION_ROLE'],
110
+ },
111
+
112
+ ai: {
113
+ compositionPattern: 'compound',
114
+ subComponents: ['Item', 'Group', 'Empty'],
115
+ requiredChildren: ['Item'],
116
+ commonPatterns: [
117
+ '<Listbox aria-label="Search results">{results.map(item => <Listbox.Item key={item.id} selected={item.id === selectedId} onClick={() => onSelect(item)}>{item.label}</Listbox.Item>)}</Listbox>',
118
+ ],
119
+ },
120
+
121
+ variants: [
122
+ {
123
+ name: 'Default',
124
+ description: 'Basic listbox with selectable items',
125
+ render: () => (
126
+ <Listbox aria-label="Options">
127
+ <Listbox.Item selected>First option</Listbox.Item>
128
+ <Listbox.Item>Second option</Listbox.Item>
129
+ <Listbox.Item>Third option</Listbox.Item>
130
+ </Listbox>
131
+ ),
132
+ },
133
+ {
134
+ name: 'Search Results',
135
+ description: 'Typical search results pattern with label and metadata',
136
+ render: () => (
137
+ <Listbox aria-label="Search results">
138
+ <Listbox.Item selected>
139
+ <span style={{ fontWeight: 500 }}>Button</span>
140
+ <span style={{ marginLeft: 'auto', fontSize: '0.75rem', color: 'var(--fui-text-tertiary)' }}>Components</span>
141
+ </Listbox.Item>
142
+ <Listbox.Item>
143
+ <span style={{ fontWeight: 500 }}>Badge</span>
144
+ <span style={{ marginLeft: 'auto', fontSize: '0.75rem', color: 'var(--fui-text-tertiary)' }}>Components</span>
145
+ </Listbox.Item>
146
+ <Listbox.Item>
147
+ <span style={{ fontWeight: 500 }}>Box</span>
148
+ <span style={{ marginLeft: 'auto', fontSize: '0.75rem', color: 'var(--fui-text-tertiary)' }}>Layout</span>
149
+ </Listbox.Item>
150
+ </Listbox>
151
+ ),
152
+ },
153
+ {
154
+ name: 'With Groups',
155
+ description: 'Grouped items with labels',
156
+ render: () => (
157
+ <Listbox aria-label="Commands">
158
+ <Listbox.Group label="Recent">
159
+ <Listbox.Item selected>Open file...</Listbox.Item>
160
+ <Listbox.Item>Save as...</Listbox.Item>
161
+ </Listbox.Group>
162
+ <Listbox.Group label="Actions">
163
+ <Listbox.Item>Copy</Listbox.Item>
164
+ <Listbox.Item>Paste</Listbox.Item>
165
+ <Listbox.Item disabled>Cut</Listbox.Item>
166
+ </Listbox.Group>
167
+ </Listbox>
168
+ ),
169
+ },
170
+ {
171
+ name: 'Empty State',
172
+ description: 'No results found message',
173
+ render: () => (
174
+ <Listbox aria-label="Search results">
175
+ <Listbox.Empty>No results found</Listbox.Empty>
176
+ </Listbox>
177
+ ),
178
+ },
179
+ {
180
+ name: 'With Disabled Items',
181
+ description: 'Mix of enabled and disabled items',
182
+ render: () => (
183
+ <Listbox aria-label="Options">
184
+ <Listbox.Item>Available option</Listbox.Item>
185
+ <Listbox.Item disabled>Disabled option</Listbox.Item>
186
+ <Listbox.Item>Another option</Listbox.Item>
187
+ </Listbox>
188
+ ),
189
+ },
190
+ ],
191
+ });
@@ -0,0 +1,97 @@
1
+ @use '../../tokens/mixins' as *;
2
+
3
+ // ============================================
4
+ // Listbox container
5
+ // ============================================
6
+
7
+ .listbox {
8
+ @include surface-elevated;
9
+
10
+ min-width: 12rem;
11
+ padding: var(--fui-space-1, 0.25rem);
12
+ box-shadow: var(--fui-shadow-lg);
13
+ max-height: 320px;
14
+ overflow-y: auto;
15
+ }
16
+
17
+ // ============================================
18
+ // Listbox item
19
+ // ============================================
20
+
21
+ .item {
22
+ @include button-reset;
23
+ @include text-base;
24
+
25
+ display: flex;
26
+ align-items: center;
27
+ gap: var(--fui-space-2, 0.5rem);
28
+ width: 100%;
29
+ padding: var(--fui-space-2, 0.5rem) var(--fui-space-3, 0.75rem);
30
+ border-radius: var(--fui-radius-sm, 0.25rem);
31
+ cursor: pointer;
32
+ outline: none;
33
+ text-align: left;
34
+ color: var(--fui-text-primary);
35
+ background: transparent;
36
+ border: none;
37
+ transition: background-color 0.1s ease;
38
+
39
+ &:hover,
40
+ &:focus {
41
+ outline: none;
42
+ }
43
+ }
44
+
45
+ .itemSelected {
46
+ background-color: var(--fui-bg-hover);
47
+ }
48
+
49
+ .itemDisabled {
50
+ color: var(--fui-text-tertiary);
51
+ cursor: not-allowed;
52
+ }
53
+
54
+ // ============================================
55
+ // Group
56
+ // ============================================
57
+
58
+ .group {
59
+ &:not(:first-child) {
60
+ margin-top: var(--fui-space-1, 0.25rem);
61
+ padding-top: var(--fui-space-1, 0.25rem);
62
+ border-top: 1px solid var(--fui-border);
63
+ }
64
+ }
65
+
66
+ .groupLabel {
67
+ padding: var(--fui-space-1, 0.25rem) var(--fui-space-3, 0.75rem);
68
+ font-size: var(--fui-font-size-xs, 0.75rem);
69
+ font-weight: var(--fui-font-weight-medium, 500);
70
+ color: var(--fui-text-tertiary);
71
+ }
72
+
73
+ // ============================================
74
+ // Empty state
75
+ // ============================================
76
+
77
+ .empty {
78
+ padding: var(--fui-space-3, 0.75rem);
79
+ font-size: var(--fui-font-size-sm, 0.875rem);
80
+ color: var(--fui-text-secondary);
81
+ text-align: center;
82
+ }
83
+
84
+ // ============================================
85
+ // Accessibility: High Contrast Mode
86
+ // ============================================
87
+
88
+ @media (prefers-contrast: more) {
89
+ .listbox {
90
+ border-width: 2px;
91
+ }
92
+
93
+ .itemSelected {
94
+ outline: 2px solid var(--fui-color-accent);
95
+ outline-offset: -2px;
96
+ }
97
+ }
@@ -0,0 +1,121 @@
1
+ import * as React from 'react';
2
+ import styles from './Listbox.module.scss';
3
+ import '../../styles/globals.scss';
4
+
5
+ // ============================================
6
+ // Types
7
+ // ============================================
8
+
9
+ export interface ListboxProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export interface ListboxItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {
14
+ children: React.ReactNode;
15
+ /** Whether this item is currently selected/highlighted */
16
+ selected?: boolean;
17
+ /** Whether this item is disabled */
18
+ disabled?: boolean;
19
+ /** Click handler */
20
+ onClick?: () => void;
21
+ }
22
+
23
+ export interface ListboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
24
+ children: React.ReactNode;
25
+ /** Group label */
26
+ label?: string;
27
+ }
28
+
29
+ export interface ListboxEmptyProps extends React.HTMLAttributes<HTMLDivElement> {
30
+ children: React.ReactNode;
31
+ }
32
+
33
+ // ============================================
34
+ // Components
35
+ // ============================================
36
+
37
+ function ListboxRoot({
38
+ children,
39
+ className,
40
+ style,
41
+ 'aria-label': ariaLabel,
42
+ ...htmlProps
43
+ }: ListboxProps) {
44
+ const classes = [styles.listbox, className].filter(Boolean).join(' ');
45
+
46
+ return (
47
+ <div
48
+ {...htmlProps}
49
+ role="listbox"
50
+ aria-label={ariaLabel}
51
+ className={classes}
52
+ style={style}
53
+ >
54
+ {children}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function ListboxItem({
60
+ children,
61
+ selected = false,
62
+ disabled = false,
63
+ onClick,
64
+ onMouseEnter,
65
+ className,
66
+ style,
67
+ ...htmlProps
68
+ }: ListboxItemProps) {
69
+ const classes = [
70
+ styles.item,
71
+ selected && styles.itemSelected,
72
+ disabled && styles.itemDisabled,
73
+ className,
74
+ ]
75
+ .filter(Boolean)
76
+ .join(' ');
77
+
78
+ return (
79
+ <div
80
+ {...htmlProps}
81
+ role="option"
82
+ aria-selected={selected}
83
+ aria-disabled={disabled}
84
+ onClick={disabled ? undefined : onClick}
85
+ onMouseEnter={onMouseEnter}
86
+ className={classes}
87
+ style={style}
88
+ >
89
+ {children}
90
+ </div>
91
+ );
92
+ }
93
+
94
+ function ListboxGroup({ children, label, className, ...htmlProps }: ListboxGroupProps) {
95
+ const classes = [styles.group, className].filter(Boolean).join(' ');
96
+
97
+ return (
98
+ <div {...htmlProps} role="group" aria-label={label} className={classes}>
99
+ {label && <div className={styles.groupLabel}>{label}</div>}
100
+ {children}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function ListboxEmpty({ children, className, ...htmlProps }: ListboxEmptyProps) {
106
+ const classes = [styles.empty, className].filter(Boolean).join(' ');
107
+ return <div {...htmlProps} className={classes}>{children}</div>;
108
+ }
109
+
110
+ // ============================================
111
+ // Export compound component
112
+ // ============================================
113
+
114
+ export const Listbox = Object.assign(ListboxRoot, {
115
+ Item: ListboxItem,
116
+ Group: ListboxGroup,
117
+ Empty: ListboxEmpty,
118
+ });
119
+
120
+ // Re-export individual components
121
+ export { ListboxRoot, ListboxItem, ListboxGroup, ListboxEmpty };
@@ -88,6 +88,15 @@ export default defineSegment({
88
88
  a11yRules: ['A11Y_MENU_KEYBOARD', 'A11Y_MENU_ROLE'],
89
89
  },
90
90
 
91
+ ai: {
92
+ compositionPattern: 'compound',
93
+ subComponents: ['Trigger', 'Content', 'Item', 'CheckboxItem', 'RadioGroup', 'RadioItem', 'Group', 'GroupLabel', 'Separator'],
94
+ requiredChildren: ['Trigger', 'Content'],
95
+ commonPatterns: [
96
+ '<Menu><Menu.Trigger asChild><Button>Actions</Button></Menu.Trigger><Menu.Content><Menu.Item>{action1}</Menu.Item><Menu.Separator /><Menu.Item danger>{delete}</Menu.Item></Menu.Content></Menu>',
97
+ ],
98
+ },
99
+
91
100
  variants: [
92
101
  {
93
102
  name: 'Default',
@@ -44,7 +44,7 @@
44
44
  align-items: center;
45
45
  gap: var(--fui-space-2, $fui-space-2);
46
46
  width: 100%;
47
- padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
47
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-padding-item-md, $fui-padding-item-md);
48
48
  border-radius: var(--fui-radius-sm, $fui-radius-sm);
49
49
  cursor: pointer;
50
50
  outline: none;
@@ -188,3 +188,19 @@
188
188
  border-color: transparent transparent transparent var(--fui-text-secondary, $fui-text-secondary);
189
189
  }
190
190
  }
191
+
192
+ // ============================================
193
+ // Accessibility: Reduced Motion
194
+ // ============================================
195
+
196
+ @media (prefers-reduced-motion: reduce) {
197
+ .popup {
198
+ transition: none;
199
+ transform: none;
200
+
201
+ &[data-starting-style],
202
+ &[data-ending-style] {
203
+ transform: none;
204
+ }
205
+ }
206
+ }
@@ -22,9 +22,8 @@ export interface MenuTriggerProps {
22
22
  className?: string;
23
23
  }
24
24
 
25
- export interface MenuContentProps {
25
+ export interface MenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
26
26
  children: React.ReactNode;
27
- className?: string;
28
27
  side?: 'top' | 'bottom' | 'left' | 'right';
29
28
  align?: 'start' | 'center' | 'end';
30
29
  sideOffset?: number;
@@ -160,6 +159,7 @@ function MenuContent({
160
159
  side = 'bottom',
161
160
  align = 'start',
162
161
  sideOffset = 4,
162
+ ...htmlProps
163
163
  }: MenuContentProps) {
164
164
  const popupClasses = [styles.popup, className].filter(Boolean).join(' ');
165
165
 
@@ -171,7 +171,7 @@ function MenuContent({
171
171
  sideOffset={sideOffset}
172
172
  className={styles.positioner}
173
173
  >
174
- <BaseMenu.Popup className={popupClasses}>
174
+ <BaseMenu.Popup {...htmlProps} className={popupClasses}>
175
175
  {children}
176
176
  </BaseMenu.Popup>
177
177
  </BaseMenu.Positioner>
@@ -90,6 +90,15 @@ export default defineSegment({
90
90
  a11yRules: ['A11Y_POPOVER_FOCUS', 'A11Y_POPOVER_ESCAPE'],
91
91
  },
92
92
 
93
+ ai: {
94
+ compositionPattern: 'compound',
95
+ subComponents: ['Trigger', 'Content', 'Close', 'Title', 'Description', 'Body', 'Footer'],
96
+ requiredChildren: ['Trigger', 'Content'],
97
+ commonPatterns: [
98
+ '<Popover><Popover.Trigger asChild><Button>Open</Button></Popover.Trigger><Popover.Content><Popover.Close /><Popover.Title>{title}</Popover.Title><Popover.Description>{description}</Popover.Description></Popover.Content></Popover>',
99
+ ],
100
+ },
101
+
93
102
  variants: [
94
103
  {
95
104
  name: 'Default',
@@ -14,12 +14,12 @@
14
14
 
15
15
  min-width: 12rem;
16
16
  max-width: 24rem;
17
- padding: var(--fui-space-4, $fui-space-4);
17
+ padding: var(--fui-padding-container-md, $fui-padding-container-md);
18
18
  box-shadow: var(--fui-shadow-md, $fui-shadow-md);
19
19
 
20
20
  // Animation
21
21
  opacity: 0;
22
- transform: scale(0.95) translateY(-4px);
22
+ transform: scale(0.95) translateY(-$fui-anim-offset-sm);
23
23
  transform-origin: var(--transform-origin);
24
24
  transition:
25
25
  opacity var(--fui-transition-fast, $fui-transition-fast),
@@ -32,22 +32,22 @@
32
32
 
33
33
  &[data-starting-style] {
34
34
  opacity: 0;
35
- transform: scale(0.95) translateY(-4px);
35
+ transform: scale(0.95) translateY(-$fui-anim-offset-sm);
36
36
  }
37
37
 
38
38
  &[data-ending-style] {
39
39
  opacity: 0;
40
- transform: scale(0.95) translateY(4px);
40
+ transform: scale(0.95) translateY($fui-anim-offset-sm);
41
41
  }
42
42
 
43
43
  // Adjust animation direction based on side
44
44
  &[data-side='top'] {
45
45
  &[data-starting-style] {
46
- transform: scale(0.95) translateY(4px);
46
+ transform: scale(0.95) translateY($fui-anim-offset-sm);
47
47
  }
48
48
 
49
49
  &[data-ending-style] {
50
- transform: scale(0.95) translateY(-4px);
50
+ transform: scale(0.95) translateY(-$fui-anim-offset-sm);
51
51
  }
52
52
  }
53
53
 
@@ -62,7 +62,7 @@
62
62
 
63
63
  // Size variants
64
64
  .sm {
65
- padding: var(--fui-space-3, $fui-space-3);
65
+ padding: var(--fui-padding-container-sm, $fui-padding-container-sm);
66
66
  max-width: 16rem;
67
67
  }
68
68
 
@@ -71,7 +71,7 @@
71
71
  }
72
72
 
73
73
  .lg {
74
- padding: var(--fui-space-5, $fui-space-5);
74
+ padding: var(--fui-padding-container-lg, $fui-padding-container-lg);
75
75
  max-width: 32rem;
76
76
  }
77
77
 
@@ -121,8 +121,8 @@
121
121
 
122
122
  // Arrow
123
123
  .arrow {
124
- width: 10px;
125
- height: 10px;
124
+ width: $fui-arrow-size;
125
+ height: $fui-arrow-size;
126
126
  transform: rotate(45deg);
127
127
  background-color: var(--fui-bg-elevated, $fui-bg-elevated);
128
128
  border: 1px solid var(--fui-border, $fui-border);
@@ -163,3 +163,26 @@
163
163
  padding-top: var(--fui-space-3, $fui-space-3);
164
164
  border-top: 1px solid var(--fui-border, $fui-border);
165
165
  }
166
+
167
+ // ============================================
168
+ // Accessibility: Reduced Motion
169
+ // ============================================
170
+
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .popup {
173
+ transition: none;
174
+ transform: none;
175
+
176
+ &[data-starting-style],
177
+ &[data-ending-style] {
178
+ transform: none;
179
+ }
180
+
181
+ &[data-side='top'] {
182
+ &[data-starting-style],
183
+ &[data-ending-style] {
184
+ transform: none;
185
+ }
186
+ }
187
+ }
188
+ }
@@ -22,14 +22,13 @@ export interface PopoverTriggerProps {
22
22
  className?: string;
23
23
  }
24
24
 
25
- export interface PopoverContentProps {
25
+ export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
26
26
  children: React.ReactNode;
27
27
  size?: 'sm' | 'md' | 'lg';
28
28
  side?: 'top' | 'bottom' | 'left' | 'right';
29
29
  align?: 'start' | 'center' | 'end';
30
30
  sideOffset?: number;
31
31
  arrow?: boolean;
32
- className?: string;
33
32
  }
34
33
 
35
34
  export interface PopoverTitleProps {
@@ -42,14 +41,12 @@ export interface PopoverDescriptionProps {
42
41
  className?: string;
43
42
  }
44
43
 
45
- export interface PopoverBodyProps {
44
+ export interface PopoverBodyProps extends React.HTMLAttributes<HTMLDivElement> {
46
45
  children: React.ReactNode;
47
- className?: string;
48
46
  }
49
47
 
50
- export interface PopoverFooterProps {
48
+ export interface PopoverFooterProps extends React.HTMLAttributes<HTMLDivElement> {
51
49
  children: React.ReactNode;
52
- className?: string;
53
50
  }
54
51
 
55
52
  export interface PopoverCloseProps {
@@ -129,6 +126,7 @@ function PopoverContent({
129
126
  sideOffset = 8,
130
127
  arrow = false,
131
128
  className,
129
+ ...htmlProps
132
130
  }: PopoverContentProps) {
133
131
  const popupClasses = [
134
132
  styles.popup,
@@ -144,7 +142,7 @@ function PopoverContent({
144
142
  sideOffset={sideOffset}
145
143
  className={styles.positioner}
146
144
  >
147
- <BasePopover.Popup className={popupClasses}>
145
+ <BasePopover.Popup {...htmlProps} className={popupClasses}>
148
146
  {children}
149
147
  {arrow && <BasePopover.Arrow className={styles.arrow} />}
150
148
  </BasePopover.Popup>
@@ -167,14 +165,14 @@ function PopoverDescription({ children, className }: PopoverDescriptionProps) {
167
165
  );
168
166
  }
169
167
 
170
- function PopoverBody({ children, className }: PopoverBodyProps) {
168
+ function PopoverBody({ children, className, ...htmlProps }: PopoverBodyProps) {
171
169
  const classes = [styles.body, className].filter(Boolean).join(' ');
172
- return <div className={classes}>{children}</div>;
170
+ return <div {...htmlProps} className={classes}>{children}</div>;
173
171
  }
174
172
 
175
- function PopoverFooter({ children, className }: PopoverFooterProps) {
173
+ function PopoverFooter({ children, className, ...htmlProps }: PopoverFooterProps) {
176
174
  const classes = [styles.footer, className].filter(Boolean).join(' ');
177
- return <div className={classes}>{children}</div>;
175
+ return <div {...htmlProps} className={classes}>{children}</div>;
178
176
  }
179
177
 
180
178
  function PopoverClose({ children, asChild, className }: PopoverCloseProps) {