@fragments-sdk/ui 0.4.0 → 0.6.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/README.md +98 -2
- package/fragments.json +1 -1
- package/package.json +4 -3
- package/src/components/Accordion/Accordion.fragment.tsx +1 -1
- package/src/components/Alert/Alert.fragment.tsx +1 -1
- package/src/components/AppShell/AppShell.fragment.tsx +4 -4
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Badge/Badge.module.scss +1 -1
- package/src/components/Box/Box.fragment.tsx +1 -1
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
- package/src/components/Card/Card.fragment.tsx +1 -1
- package/src/components/Chart/Chart.fragment.tsx +213 -0
- package/src/components/Chart/Chart.module.scss +123 -0
- package/src/components/Chart/index.tsx +267 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +265 -6
- package/src/components/CodeBlock/CodeBlock.module.scss +141 -3
- package/src/components/CodeBlock/index.tsx +250 -36
- package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
- package/src/components/Collapsible/Collapsible.module.scss +117 -0
- package/src/components/Collapsible/index.tsx +219 -0
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +33 -23
- package/src/components/ColorPicker/index.tsx +34 -12
- package/src/components/Combobox/Combobox.fragment.tsx +220 -0
- package/src/components/Combobox/Combobox.module.scss +268 -0
- package/src/components/Combobox/index.tsx +398 -0
- package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
- package/src/components/ConversationList/ConversationList.module.scss +160 -0
- package/src/components/ConversationList/index.tsx +254 -0
- package/src/components/Dialog/Dialog.fragment.tsx +3 -3
- package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
- package/src/components/Field/Field.fragment.tsx +3 -3
- package/src/components/Fieldset/Fieldset.fragment.tsx +7 -7
- package/src/components/Form/Form.fragment.tsx +11 -11
- package/src/components/Grid/Grid.fragment.tsx +1 -1
- package/src/components/Header/Header.fragment.tsx +4 -4
- package/src/components/Header/Header.module.scss +9 -10
- package/src/components/Icon/Icon.fragment.tsx +2 -2
- package/src/components/Image/Image.fragment.tsx +2 -2
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.module.scss +2 -2
- package/src/components/Link/Link.fragment.tsx +1 -1
- package/src/components/List/List.fragment.tsx +2 -2
- package/src/components/Listbox/Listbox.fragment.tsx +1 -1
- package/src/components/Loading/Loading.fragment.tsx +153 -0
- package/src/components/Loading/Loading.module.scss +256 -0
- package/src/components/Loading/index.tsx +236 -0
- package/src/components/Menu/Menu.fragment.tsx +3 -3
- package/src/components/Message/Message.fragment.tsx +200 -0
- package/src/components/Message/Message.module.scss +224 -0
- package/src/components/Message/index.tsx +278 -0
- package/src/components/Popover/Popover.fragment.tsx +4 -4
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Prompt/Prompt.fragment.tsx +2 -2
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.module.scss +7 -4
- package/src/components/Select/Select.fragment.tsx +1 -1
- package/src/components/Select/Select.module.scss +8 -0
- package/src/components/Select/index.tsx +85 -5
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +2 -2
- package/src/components/Sidebar/Sidebar.module.scss +19 -0
- package/src/components/Sidebar/index.tsx +52 -11
- package/src/components/Skeleton/Skeleton.fragment.tsx +1 -1
- package/src/components/Slider/Slider.fragment.tsx +201 -0
- package/src/components/Stack/Stack.fragment.tsx +194 -0
- package/src/components/Table/Table.fragment.tsx +3 -3
- package/src/components/Tabs/Tabs.fragment.tsx +1 -1
- package/src/components/Tabs/Tabs.module.scss +2 -2
- package/src/components/Text/Text.fragment.tsx +188 -0
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Theme/Theme.fragment.tsx +2 -2
- package/src/components/Theme/ThemeToggle.module.scss +13 -13
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
- package/src/components/ThinkingIndicator/index.tsx +258 -0
- package/src/components/Toast/Toast.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
- package/src/index.ts +99 -3
- package/src/recipes/AIChat.recipe.ts +266 -0
- package/src/tokens/_computed.scss +212 -0
- package/src/tokens/_density.scss +171 -0
- package/src/tokens/_derive.scss +287 -0
- package/src/tokens/_index.scss +39 -1
- package/src/tokens/_mixins.scss +41 -0
- package/src/tokens/_palettes.scss +185 -0
- package/src/tokens/_radius.scss +107 -0
- package/src/tokens/_seeds.scss +59 -0
- package/src/tokens/_variables.scss +171 -130
- package/src/components/ColorChip/ColorChip.module.scss +0 -165
- package/src/components/ColorChip/index.tsx +0 -157
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// Multi-select container — stacks input and chips vertically
|
|
5
|
+
.multiContainer {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Input + trigger wrapper
|
|
12
|
+
.inputWrapper {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
17
|
+
width: 100%;
|
|
18
|
+
min-width: 10rem;
|
|
19
|
+
min-height: var(--fui-input-height, $fui-input-height);
|
|
20
|
+
padding: var(--fui-space-1, $fui-space-1);
|
|
21
|
+
padding-right: 0;
|
|
22
|
+
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
23
|
+
border: 1px solid var(--fui-border-strong, $fui-border-strong);
|
|
24
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
25
|
+
transition:
|
|
26
|
+
border-color var(--fui-transition-fast, $fui-transition-fast),
|
|
27
|
+
box-shadow var(--fui-transition-fast, $fui-transition-fast);
|
|
28
|
+
|
|
29
|
+
&:hover:not(:has([data-disabled])) {
|
|
30
|
+
border-color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:focus-within {
|
|
34
|
+
@include focus-ring;
|
|
35
|
+
border-color: var(--fui-color-accent, $fui-color-accent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:has([data-disabled]) {
|
|
39
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
40
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Text input
|
|
45
|
+
.input {
|
|
46
|
+
@include text-base;
|
|
47
|
+
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 4rem;
|
|
50
|
+
height: calc(var(--fui-input-height, $fui-input-height) - var(--fui-space-2, $fui-space-2));
|
|
51
|
+
padding: 0 var(--fui-space-2, $fui-space-2);
|
|
52
|
+
background: transparent;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
55
|
+
outline: none;
|
|
56
|
+
|
|
57
|
+
&::placeholder {
|
|
58
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&:disabled,
|
|
62
|
+
&[data-disabled] {
|
|
63
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
64
|
+
cursor: not-allowed;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Chips container (multi-select) — rendered below the input wrapper
|
|
69
|
+
.chips {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
73
|
+
width: 100%;
|
|
74
|
+
padding-top: var(--fui-space-1, $fui-space-1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Individual chip (multi-select)
|
|
78
|
+
.chip {
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
82
|
+
padding: 2px var(--fui-space-2, $fui-space-2);
|
|
83
|
+
background-color: var(--fui-bg-subtle, $fui-bg-subtle);
|
|
84
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
85
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
86
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
87
|
+
line-height: 1.4;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
max-width: 12rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.chipLabel {
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
text-overflow: ellipsis;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Chip remove button
|
|
99
|
+
.chipRemove {
|
|
100
|
+
@include button-reset;
|
|
101
|
+
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
width: 1rem;
|
|
107
|
+
height: 1rem;
|
|
108
|
+
border-radius: var(--fui-radius-xs, 2px);
|
|
109
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast);
|
|
112
|
+
|
|
113
|
+
&:hover {
|
|
114
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
115
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Chevron trigger button
|
|
120
|
+
.trigger {
|
|
121
|
+
@include button-reset;
|
|
122
|
+
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
align-self: stretch;
|
|
128
|
+
width: 2rem;
|
|
129
|
+
margin-left: auto;
|
|
130
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
border-radius: 0 var(--fui-radius-md, $fui-radius-md) var(--fui-radius-md, $fui-radius-md) 0;
|
|
133
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast);
|
|
134
|
+
|
|
135
|
+
&:hover {
|
|
136
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&[data-disabled] {
|
|
140
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
141
|
+
cursor: not-allowed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
svg {
|
|
145
|
+
width: 1rem;
|
|
146
|
+
height: 1rem;
|
|
147
|
+
transition: transform var(--fui-transition-fast, $fui-transition-fast);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
&[data-popup-open] svg {
|
|
151
|
+
transform: rotate(180deg);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Positioner
|
|
156
|
+
.positioner {
|
|
157
|
+
z-index: 50;
|
|
158
|
+
outline: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Popup container
|
|
162
|
+
.popup {
|
|
163
|
+
@include surface-elevated;
|
|
164
|
+
|
|
165
|
+
min-width: var(--anchor-width);
|
|
166
|
+
max-height: 20rem;
|
|
167
|
+
overflow-y: auto;
|
|
168
|
+
padding: var(--fui-space-1, $fui-space-1);
|
|
169
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
170
|
+
|
|
171
|
+
// Animation
|
|
172
|
+
opacity: 0;
|
|
173
|
+
transform: scale(0.95);
|
|
174
|
+
transform-origin: var(--transform-origin);
|
|
175
|
+
transition:
|
|
176
|
+
opacity var(--fui-transition-fast, $fui-transition-fast),
|
|
177
|
+
transform var(--fui-transition-fast, $fui-transition-fast);
|
|
178
|
+
|
|
179
|
+
&[data-open] {
|
|
180
|
+
opacity: 1;
|
|
181
|
+
transform: scale(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
&[data-starting-style],
|
|
185
|
+
&[data-ending-style] {
|
|
186
|
+
opacity: 0;
|
|
187
|
+
transform: scale(0.95);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Individual option
|
|
192
|
+
.item {
|
|
193
|
+
@include button-reset;
|
|
194
|
+
@include text-base;
|
|
195
|
+
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
199
|
+
width: 100%;
|
|
200
|
+
padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
|
|
201
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
outline: none;
|
|
204
|
+
|
|
205
|
+
&[data-highlighted] {
|
|
206
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
&[data-selected] {
|
|
210
|
+
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
211
|
+
color: var(--fui-text-inverse, $fui-text-inverse);
|
|
212
|
+
|
|
213
|
+
&[data-highlighted] {
|
|
214
|
+
background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
&[data-disabled] {
|
|
219
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
220
|
+
cursor: not-allowed;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Selection indicator (checkmark)
|
|
225
|
+
.itemIndicator {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
width: 1rem;
|
|
230
|
+
height: 1rem;
|
|
231
|
+
margin-left: auto;
|
|
232
|
+
|
|
233
|
+
svg {
|
|
234
|
+
width: 0.875rem;
|
|
235
|
+
height: 0.875rem;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Empty state
|
|
240
|
+
.empty {
|
|
241
|
+
@include text-base;
|
|
242
|
+
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
padding: var(--fui-space-4, $fui-space-4) var(--fui-space-3, $fui-space-3);
|
|
247
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
248
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Group container
|
|
252
|
+
.group {
|
|
253
|
+
&:not(:first-child) {
|
|
254
|
+
margin-top: var(--fui-space-1, $fui-space-1);
|
|
255
|
+
padding-top: var(--fui-space-1, $fui-space-1);
|
|
256
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Group label
|
|
261
|
+
.groupLabel {
|
|
262
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
263
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
264
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
265
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
266
|
+
text-transform: uppercase;
|
|
267
|
+
letter-spacing: 0.05em;
|
|
268
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Combobox as BaseCombobox } from '@base-ui/react/combobox';
|
|
3
|
+
import styles from './Combobox.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface ComboboxProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/** Controlled selected value (string for single, string[] for multiple) */
|
|
14
|
+
value?: string | string[] | null;
|
|
15
|
+
/** Default selected value (uncontrolled) */
|
|
16
|
+
defaultValue?: string | string[];
|
|
17
|
+
/** Called when selection changes */
|
|
18
|
+
onValueChange?: (value: string | string[] | null) => void;
|
|
19
|
+
/** Whether multiple items can be selected */
|
|
20
|
+
multiple?: boolean;
|
|
21
|
+
open?: boolean;
|
|
22
|
+
defaultOpen?: boolean;
|
|
23
|
+
onOpenChange?: (open: boolean) => void;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
name?: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
/** Auto-highlight first matching item while filtering */
|
|
29
|
+
autoHighlight?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ComboboxInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ComboboxTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ComboboxContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
sideOffset?: number;
|
|
44
|
+
align?: 'start' | 'center' | 'end';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ComboboxItemProps {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
value: string;
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ComboboxEmptyProps {
|
|
55
|
+
children: React.ReactNode;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ComboboxGroupProps {
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ComboboxGroupLabelProps {
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
className?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// Icons
|
|
71
|
+
// ============================================
|
|
72
|
+
|
|
73
|
+
function ChevronDownIcon() {
|
|
74
|
+
return (
|
|
75
|
+
<svg
|
|
76
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
77
|
+
width="16"
|
|
78
|
+
height="16"
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
strokeWidth="2"
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
strokeLinejoin="round"
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
>
|
|
87
|
+
<polyline points="6 9 12 15 18 9" />
|
|
88
|
+
</svg>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function CheckIcon() {
|
|
93
|
+
return (
|
|
94
|
+
<svg
|
|
95
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
+
width="14"
|
|
97
|
+
height="14"
|
|
98
|
+
viewBox="0 0 24 24"
|
|
99
|
+
fill="none"
|
|
100
|
+
stroke="currentColor"
|
|
101
|
+
strokeWidth="2.5"
|
|
102
|
+
strokeLinecap="round"
|
|
103
|
+
strokeLinejoin="round"
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
>
|
|
106
|
+
<polyline points="20 6 9 17 4 12" />
|
|
107
|
+
</svg>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function XIcon() {
|
|
112
|
+
return (
|
|
113
|
+
<svg
|
|
114
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
115
|
+
width="12"
|
|
116
|
+
height="12"
|
|
117
|
+
viewBox="0 0 24 24"
|
|
118
|
+
fill="none"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
strokeWidth="2.5"
|
|
121
|
+
strokeLinecap="round"
|
|
122
|
+
strokeLinejoin="round"
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
>
|
|
125
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
126
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
127
|
+
</svg>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================
|
|
132
|
+
// Context for Combobox state
|
|
133
|
+
// ============================================
|
|
134
|
+
|
|
135
|
+
interface ComboboxContextValue {
|
|
136
|
+
placeholder?: string;
|
|
137
|
+
multiple?: boolean;
|
|
138
|
+
selectedValues: string[];
|
|
139
|
+
itemsRef: React.MutableRefObject<Map<string, string>>;
|
|
140
|
+
itemsVersion: number;
|
|
141
|
+
incrementItemsVersion: () => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ComboboxContext = React.createContext<ComboboxContextValue>({
|
|
145
|
+
selectedValues: [],
|
|
146
|
+
itemsRef: { current: new Map() },
|
|
147
|
+
itemsVersion: 0,
|
|
148
|
+
incrementItemsVersion: () => {},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ============================================
|
|
152
|
+
// Components
|
|
153
|
+
// ============================================
|
|
154
|
+
|
|
155
|
+
function ComboboxRoot({
|
|
156
|
+
children,
|
|
157
|
+
value,
|
|
158
|
+
defaultValue,
|
|
159
|
+
onValueChange,
|
|
160
|
+
multiple = false,
|
|
161
|
+
open,
|
|
162
|
+
defaultOpen,
|
|
163
|
+
onOpenChange,
|
|
164
|
+
disabled,
|
|
165
|
+
required,
|
|
166
|
+
name,
|
|
167
|
+
placeholder,
|
|
168
|
+
autoHighlight = true,
|
|
169
|
+
}: ComboboxProps) {
|
|
170
|
+
// Track selected values for chip rendering
|
|
171
|
+
const [internalValue, setInternalValue] = React.useState<string | string[] | null>(
|
|
172
|
+
value ?? defaultValue ?? (multiple ? [] : null)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Sync with controlled value
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
if (value !== undefined) {
|
|
178
|
+
setInternalValue(value);
|
|
179
|
+
}
|
|
180
|
+
}, [value]);
|
|
181
|
+
|
|
182
|
+
// Registry for item value → label mapping
|
|
183
|
+
const itemsRef = React.useRef<Map<string, string>>(new Map());
|
|
184
|
+
const [itemsVersion, setItemsVersion] = React.useState(0);
|
|
185
|
+
const incrementItemsVersion = React.useCallback(() => {
|
|
186
|
+
setItemsVersion((v) => v + 1);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
const handleValueChange = React.useCallback(
|
|
190
|
+
(newValue: string | string[] | null) => {
|
|
191
|
+
if (value === undefined) {
|
|
192
|
+
setInternalValue(newValue);
|
|
193
|
+
}
|
|
194
|
+
onValueChange?.(newValue);
|
|
195
|
+
},
|
|
196
|
+
[value, onValueChange]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const handleOpenChange = React.useCallback(
|
|
200
|
+
(nextOpen: boolean) => {
|
|
201
|
+
onOpenChange?.(nextOpen);
|
|
202
|
+
},
|
|
203
|
+
[onOpenChange]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Convert value → label for input display
|
|
207
|
+
const itemToStringLabel = React.useCallback(
|
|
208
|
+
(itemValue: string) => {
|
|
209
|
+
return itemsRef.current.get(itemValue) ?? itemValue;
|
|
210
|
+
},
|
|
211
|
+
[]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Derive selected values array for chip rendering
|
|
215
|
+
const currentValue = value !== undefined ? value : internalValue;
|
|
216
|
+
const selectedValues = React.useMemo(() => {
|
|
217
|
+
if (currentValue == null) return [];
|
|
218
|
+
if (Array.isArray(currentValue)) return currentValue;
|
|
219
|
+
return [currentValue];
|
|
220
|
+
}, [currentValue]);
|
|
221
|
+
|
|
222
|
+
const contextValue = React.useMemo(
|
|
223
|
+
() => ({ placeholder, multiple, selectedValues, itemsRef, itemsVersion, incrementItemsVersion }),
|
|
224
|
+
[placeholder, multiple, selectedValues, itemsVersion, incrementItemsVersion]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<ComboboxContext.Provider value={contextValue}>
|
|
229
|
+
<BaseCombobox.Root
|
|
230
|
+
value={value as any}
|
|
231
|
+
defaultValue={defaultValue as any}
|
|
232
|
+
onValueChange={handleValueChange as any}
|
|
233
|
+
open={open}
|
|
234
|
+
defaultOpen={defaultOpen}
|
|
235
|
+
onOpenChange={handleOpenChange as any}
|
|
236
|
+
disabled={disabled}
|
|
237
|
+
required={required}
|
|
238
|
+
name={name}
|
|
239
|
+
multiple={multiple as any}
|
|
240
|
+
autoHighlight={autoHighlight}
|
|
241
|
+
itemToStringLabel={itemToStringLabel}
|
|
242
|
+
>
|
|
243
|
+
{children}
|
|
244
|
+
</BaseCombobox.Root>
|
|
245
|
+
</ComboboxContext.Provider>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ComboboxInput({ className, ...htmlProps }: ComboboxInputProps) {
|
|
250
|
+
const context = React.useContext(ComboboxContext);
|
|
251
|
+
const classes = [styles.input, className].filter(Boolean).join(' ');
|
|
252
|
+
|
|
253
|
+
if (context.multiple) {
|
|
254
|
+
return (
|
|
255
|
+
<div className={styles.multiContainer}>
|
|
256
|
+
<div className={styles.inputWrapper}>
|
|
257
|
+
<BaseCombobox.Input
|
|
258
|
+
placeholder={context.selectedValues.length === 0 ? context.placeholder : undefined}
|
|
259
|
+
{...htmlProps}
|
|
260
|
+
className={classes}
|
|
261
|
+
/>
|
|
262
|
+
<BaseCombobox.Trigger className={styles.trigger}>
|
|
263
|
+
<ChevronDownIcon />
|
|
264
|
+
</BaseCombobox.Trigger>
|
|
265
|
+
</div>
|
|
266
|
+
{context.selectedValues.length > 0 && (
|
|
267
|
+
<BaseCombobox.Chips className={styles.chips}>
|
|
268
|
+
{context.selectedValues.map((chipValue) => (
|
|
269
|
+
<BaseCombobox.Chip key={chipValue} className={styles.chip}>
|
|
270
|
+
<span className={styles.chipLabel}>
|
|
271
|
+
{context.itemsRef.current.get(chipValue) ?? chipValue}
|
|
272
|
+
</span>
|
|
273
|
+
<BaseCombobox.ChipRemove className={styles.chipRemove}>
|
|
274
|
+
<XIcon />
|
|
275
|
+
</BaseCombobox.ChipRemove>
|
|
276
|
+
</BaseCombobox.Chip>
|
|
277
|
+
))}
|
|
278
|
+
</BaseCombobox.Chips>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div className={styles.inputWrapper}>
|
|
286
|
+
<BaseCombobox.Input
|
|
287
|
+
placeholder={context.placeholder}
|
|
288
|
+
{...htmlProps}
|
|
289
|
+
className={classes}
|
|
290
|
+
/>
|
|
291
|
+
<BaseCombobox.Trigger className={styles.trigger}>
|
|
292
|
+
<ChevronDownIcon />
|
|
293
|
+
</BaseCombobox.Trigger>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function ComboboxTrigger({ children, className, ...htmlProps }: ComboboxTriggerProps) {
|
|
299
|
+
const classes = [styles.trigger, className].filter(Boolean).join(' ');
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<BaseCombobox.Trigger {...htmlProps} className={classes}>
|
|
303
|
+
{children ?? <ChevronDownIcon />}
|
|
304
|
+
</BaseCombobox.Trigger>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function ComboboxContent({
|
|
309
|
+
children,
|
|
310
|
+
className,
|
|
311
|
+
sideOffset = 4,
|
|
312
|
+
align = 'start',
|
|
313
|
+
...htmlProps
|
|
314
|
+
}: ComboboxContentProps) {
|
|
315
|
+
const popupClasses = [styles.popup, className].filter(Boolean).join(' ');
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<BaseCombobox.Portal>
|
|
319
|
+
<BaseCombobox.Positioner
|
|
320
|
+
side="bottom"
|
|
321
|
+
sideOffset={sideOffset}
|
|
322
|
+
align={align}
|
|
323
|
+
className={styles.positioner}
|
|
324
|
+
>
|
|
325
|
+
<BaseCombobox.Popup {...htmlProps} className={popupClasses}>
|
|
326
|
+
{children}
|
|
327
|
+
</BaseCombobox.Popup>
|
|
328
|
+
</BaseCombobox.Positioner>
|
|
329
|
+
</BaseCombobox.Portal>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function ComboboxItem({ children, value, disabled, className }: ComboboxItemProps) {
|
|
334
|
+
const { itemsRef, incrementItemsVersion } = React.useContext(ComboboxContext);
|
|
335
|
+
const classes = [styles.item, className].filter(Boolean).join(' ');
|
|
336
|
+
|
|
337
|
+
// Register this item's label in the registry so the input can display it
|
|
338
|
+
const label = typeof children === 'string' ? children : String(children);
|
|
339
|
+
React.useEffect(() => {
|
|
340
|
+
itemsRef.current.set(value, label);
|
|
341
|
+
incrementItemsVersion();
|
|
342
|
+
return () => {
|
|
343
|
+
itemsRef.current.delete(value);
|
|
344
|
+
};
|
|
345
|
+
// itemsRef is a stable ref, incrementItemsVersion is a stable callback
|
|
346
|
+
}, [itemsRef, incrementItemsVersion, value, label]);
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<BaseCombobox.Item value={value} disabled={disabled} className={classes}>
|
|
350
|
+
{children}
|
|
351
|
+
<BaseCombobox.ItemIndicator className={styles.itemIndicator}>
|
|
352
|
+
<CheckIcon />
|
|
353
|
+
</BaseCombobox.ItemIndicator>
|
|
354
|
+
</BaseCombobox.Item>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function ComboboxEmpty({ children, className }: ComboboxEmptyProps) {
|
|
359
|
+
const classes = [styles.empty, className].filter(Boolean).join(' ');
|
|
360
|
+
return <BaseCombobox.Empty className={classes}>{children}</BaseCombobox.Empty>;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function ComboboxGroup({ children, className }: ComboboxGroupProps) {
|
|
364
|
+
const classes = [styles.group, className].filter(Boolean).join(' ');
|
|
365
|
+
return <BaseCombobox.Group className={classes}>{children}</BaseCombobox.Group>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function ComboboxGroupLabel({ children, className }: ComboboxGroupLabelProps) {
|
|
369
|
+
const classes = [styles.groupLabel, className].filter(Boolean).join(' ');
|
|
370
|
+
return <BaseCombobox.GroupLabel className={classes}>{children}</BaseCombobox.GroupLabel>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================
|
|
374
|
+
// Export compound component
|
|
375
|
+
// ============================================
|
|
376
|
+
|
|
377
|
+
export const Combobox = Object.assign(ComboboxRoot, {
|
|
378
|
+
Input: ComboboxInput,
|
|
379
|
+
Trigger: ComboboxTrigger,
|
|
380
|
+
Content: ComboboxContent,
|
|
381
|
+
Item: ComboboxItem,
|
|
382
|
+
ItemIndicator: BaseCombobox.ItemIndicator,
|
|
383
|
+
Empty: ComboboxEmpty,
|
|
384
|
+
Group: ComboboxGroup,
|
|
385
|
+
GroupLabel: ComboboxGroupLabel,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Re-export individual components
|
|
389
|
+
export {
|
|
390
|
+
ComboboxRoot,
|
|
391
|
+
ComboboxInput,
|
|
392
|
+
ComboboxTrigger,
|
|
393
|
+
ComboboxContent,
|
|
394
|
+
ComboboxItem,
|
|
395
|
+
ComboboxEmpty,
|
|
396
|
+
ComboboxGroup,
|
|
397
|
+
ComboboxGroupLabel,
|
|
398
|
+
};
|