@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.
- package/fragments.json +1 -1
- package/package.json +9 -4
- package/src/components/Accordion/Accordion.fragment.tsx +186 -0
- package/src/components/Accordion/Accordion.module.scss +111 -0
- package/src/components/Accordion/index.tsx +271 -0
- package/src/components/Alert/Alert.fragment.tsx +66 -41
- package/src/components/Alert/Alert.module.scss +31 -21
- package/src/components/Alert/index.tsx +202 -73
- package/src/components/AppShell/AppShell.fragment.tsx +315 -0
- package/src/components/AppShell/AppShell.module.scss +213 -0
- package/src/components/AppShell/index.tsx +398 -0
- package/src/components/Avatar/index.tsx +8 -9
- package/src/components/Badge/Badge.module.scss +16 -10
- package/src/components/Badge/index.tsx +20 -6
- package/src/components/Box/Box.fragment.tsx +168 -0
- package/src/components/Box/Box.module.scss +84 -0
- package/src/components/Box/index.tsx +78 -0
- package/src/components/Button/Button.module.scss +42 -0
- package/src/components/Button/index.tsx +67 -33
- package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
- package/src/components/ButtonGroup/index.tsx +40 -0
- package/src/components/Card/Card.fragment.tsx +51 -25
- package/src/components/Card/Card.module.scss +52 -5
- package/src/components/Card/index.tsx +154 -53
- package/src/components/Checkbox/Checkbox.module.scss +4 -4
- package/src/components/Checkbox/index.tsx +3 -4
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +201 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +224 -0
- package/src/components/CodeBlock/index.tsx +385 -0
- package/src/components/ColorChip/ColorChip.module.scss +165 -0
- package/src/components/ColorChip/index.tsx +157 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +109 -0
- package/src/components/ColorPicker/index.tsx +107 -0
- package/src/components/Dialog/Dialog.fragment.tsx +9 -0
- package/src/components/Dialog/Dialog.module.scss +26 -7
- package/src/components/Dialog/index.tsx +12 -15
- package/src/components/EmptyState/EmptyState.fragment.tsx +54 -71
- package/src/components/EmptyState/EmptyState.module.scss +9 -9
- package/src/components/EmptyState/index.tsx +104 -69
- package/src/components/Field/Field.fragment.tsx +165 -0
- package/src/components/Field/Field.module.scss +31 -0
- package/src/components/Field/index.tsx +143 -0
- package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
- package/src/components/Fieldset/Fieldset.module.scss +22 -0
- package/src/components/Fieldset/index.tsx +47 -0
- package/src/components/Form/Form.fragment.tsx +286 -0
- package/src/components/Form/Form.module.scss +8 -0
- package/src/components/Form/index.tsx +53 -0
- package/src/components/Grid/Grid.fragment.tsx +17 -17
- package/src/components/Grid/index.tsx +6 -1
- package/src/components/Header/Header.fragment.tsx +192 -0
- package/src/components/Header/Header.module.scss +209 -0
- package/src/components/Header/index.tsx +363 -0
- package/src/components/Icon/Icon.fragment.tsx +138 -0
- package/src/components/Icon/Icon.module.scss +38 -0
- package/src/components/Icon/index.tsx +58 -0
- package/src/components/Image/Image.fragment.tsx +195 -0
- package/src/components/Image/Image.module.scss +77 -0
- package/src/components/Image/index.tsx +95 -0
- package/src/components/Input/Input.module.scss +75 -2
- package/src/components/Input/index.tsx +60 -21
- package/src/components/Link/Link.fragment.tsx +132 -0
- package/src/components/Link/Link.module.scss +67 -0
- package/src/components/Link/index.tsx +57 -0
- package/src/components/List/List.fragment.tsx +152 -0
- package/src/components/List/List.module.scss +71 -0
- package/src/components/List/index.tsx +106 -0
- package/src/components/Listbox/Listbox.fragment.tsx +191 -0
- package/src/components/Listbox/Listbox.module.scss +97 -0
- package/src/components/Listbox/index.tsx +121 -0
- package/src/components/Menu/Menu.fragment.tsx +9 -0
- package/src/components/Menu/Menu.module.scss +17 -1
- package/src/components/Menu/index.tsx +3 -3
- package/src/components/Popover/Popover.fragment.tsx +9 -0
- package/src/components/Popover/Popover.module.scss +33 -10
- package/src/components/Popover/index.tsx +9 -11
- package/src/components/Progress/Progress.module.scss +11 -11
- package/src/components/Progress/index.tsx +34 -7
- package/src/components/Prompt/Prompt.fragment.tsx +231 -0
- package/src/components/Prompt/Prompt.module.scss +243 -0
- package/src/components/Prompt/index.tsx +439 -0
- package/src/components/RadioGroup/RadioGroup.module.scss +3 -3
- package/src/components/RadioGroup/index.tsx +3 -4
- package/src/components/Select/Select.fragment.tsx +9 -0
- package/src/components/Select/index.tsx +6 -7
- package/src/components/Separator/index.tsx +7 -3
- package/src/components/Sidebar/Sidebar.fragment.tsx +783 -0
- package/src/components/Sidebar/Sidebar.module.scss +586 -0
- package/src/components/Sidebar/index.tsx +1013 -0
- package/src/components/Skeleton/Skeleton.fragment.tsx +5 -5
- package/src/components/Skeleton/Skeleton.module.scss +11 -0
- package/src/components/Slider/Slider.module.scss +87 -0
- package/src/components/Slider/index.tsx +88 -0
- package/src/components/Stack/Stack.module.scss +120 -0
- package/src/components/Stack/index.tsx +148 -0
- package/src/components/Table/Table.fragment.tsx +7 -0
- package/src/components/Table/Table.module.scss +57 -0
- package/src/components/Table/index.tsx +44 -6
- package/src/components/Tabs/Tabs.fragment.tsx +9 -0
- package/src/components/Tabs/Tabs.module.scss +25 -10
- package/src/components/Tabs/index.tsx +11 -8
- package/src/components/Text/Text.module.scss +82 -0
- package/src/components/Text/index.tsx +58 -0
- package/src/components/Textarea/index.tsx +3 -7
- package/src/components/Theme/Theme.fragment.tsx +128 -0
- package/src/components/Theme/ThemeToggle.module.scss +82 -0
- package/src/components/Theme/index.tsx +343 -0
- package/src/components/Toast/Toast.fragment.tsx +5 -5
- package/src/components/Toast/Toast.module.scss +16 -1
- package/src/components/Toast/index.tsx +27 -11
- package/src/components/Toggle/Toggle.module.scss +25 -10
- package/src/components/Toggle/index.tsx +12 -0
- package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
- package/src/components/ToggleGroup/index.tsx +144 -0
- package/src/components/Tooltip/Tooltip.module.scss +4 -4
- package/src/components/Tooltip/index.tsx +4 -2
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
- package/src/components/VisuallyHidden/index.tsx +29 -0
- package/src/index.ts +241 -3
- package/src/recipes/AppShell.recipe.ts +175 -0
- package/src/recipes/CardGrid.recipe.ts +6 -2
- package/src/recipes/ChatInterface.recipe.ts +87 -0
- package/src/recipes/CodeExamples.recipe.ts +66 -0
- package/src/recipes/DashboardLayout.recipe.ts +46 -12
- package/src/recipes/DashboardNav.recipe.ts +183 -0
- package/src/recipes/LoginForm.recipe.ts +8 -1
- package/src/recipes/SettingsPage.recipe.ts +37 -20
- package/src/styles/globals.scss +31 -0
- package/src/tokens/_index.scss +3 -0
- package/src/tokens/_mixins.scss +54 -1
- package/src/tokens/_variables.scss +429 -64
- 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-
|
|
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-
|
|
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(-
|
|
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(-
|
|
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(
|
|
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(
|
|
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(-
|
|
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-
|
|
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-
|
|
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:
|
|
125
|
-
height:
|
|
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) {
|