@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.
Files changed (97) hide show
  1. package/README.md +98 -2
  2. package/fragments.json +1 -1
  3. package/package.json +4 -3
  4. package/src/components/Accordion/Accordion.fragment.tsx +1 -1
  5. package/src/components/Alert/Alert.fragment.tsx +1 -1
  6. package/src/components/AppShell/AppShell.fragment.tsx +4 -4
  7. package/src/components/Avatar/Avatar.fragment.tsx +2 -2
  8. package/src/components/Badge/Badge.fragment.tsx +2 -2
  9. package/src/components/Badge/Badge.module.scss +1 -1
  10. package/src/components/Box/Box.fragment.tsx +1 -1
  11. package/src/components/Button/Button.fragment.tsx +2 -2
  12. package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
  13. package/src/components/Card/Card.fragment.tsx +1 -1
  14. package/src/components/Chart/Chart.fragment.tsx +213 -0
  15. package/src/components/Chart/Chart.module.scss +123 -0
  16. package/src/components/Chart/index.tsx +267 -0
  17. package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
  18. package/src/components/CodeBlock/CodeBlock.fragment.tsx +265 -6
  19. package/src/components/CodeBlock/CodeBlock.module.scss +141 -3
  20. package/src/components/CodeBlock/index.tsx +250 -36
  21. package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
  22. package/src/components/Collapsible/Collapsible.module.scss +117 -0
  23. package/src/components/Collapsible/index.tsx +219 -0
  24. package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
  25. package/src/components/ColorPicker/ColorPicker.module.scss +33 -23
  26. package/src/components/ColorPicker/index.tsx +34 -12
  27. package/src/components/Combobox/Combobox.fragment.tsx +220 -0
  28. package/src/components/Combobox/Combobox.module.scss +268 -0
  29. package/src/components/Combobox/index.tsx +398 -0
  30. package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
  31. package/src/components/ConversationList/ConversationList.module.scss +160 -0
  32. package/src/components/ConversationList/index.tsx +254 -0
  33. package/src/components/Dialog/Dialog.fragment.tsx +3 -3
  34. package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
  35. package/src/components/Field/Field.fragment.tsx +3 -3
  36. package/src/components/Fieldset/Fieldset.fragment.tsx +7 -7
  37. package/src/components/Form/Form.fragment.tsx +11 -11
  38. package/src/components/Grid/Grid.fragment.tsx +1 -1
  39. package/src/components/Header/Header.fragment.tsx +4 -4
  40. package/src/components/Header/Header.module.scss +9 -10
  41. package/src/components/Icon/Icon.fragment.tsx +2 -2
  42. package/src/components/Image/Image.fragment.tsx +2 -2
  43. package/src/components/Input/Input.fragment.tsx +1 -1
  44. package/src/components/Input/Input.module.scss +2 -2
  45. package/src/components/Link/Link.fragment.tsx +1 -1
  46. package/src/components/List/List.fragment.tsx +2 -2
  47. package/src/components/Listbox/Listbox.fragment.tsx +1 -1
  48. package/src/components/Loading/Loading.fragment.tsx +153 -0
  49. package/src/components/Loading/Loading.module.scss +256 -0
  50. package/src/components/Loading/index.tsx +236 -0
  51. package/src/components/Menu/Menu.fragment.tsx +3 -3
  52. package/src/components/Message/Message.fragment.tsx +200 -0
  53. package/src/components/Message/Message.module.scss +224 -0
  54. package/src/components/Message/index.tsx +278 -0
  55. package/src/components/Popover/Popover.fragment.tsx +4 -4
  56. package/src/components/Progress/Progress.fragment.tsx +1 -1
  57. package/src/components/Prompt/Prompt.fragment.tsx +2 -2
  58. package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
  59. package/src/components/RadioGroup/RadioGroup.module.scss +7 -4
  60. package/src/components/Select/Select.fragment.tsx +1 -1
  61. package/src/components/Select/Select.module.scss +8 -0
  62. package/src/components/Select/index.tsx +85 -5
  63. package/src/components/Separator/Separator.fragment.tsx +1 -1
  64. package/src/components/Sidebar/Sidebar.fragment.tsx +2 -2
  65. package/src/components/Sidebar/Sidebar.module.scss +19 -0
  66. package/src/components/Sidebar/index.tsx +52 -11
  67. package/src/components/Skeleton/Skeleton.fragment.tsx +1 -1
  68. package/src/components/Slider/Slider.fragment.tsx +201 -0
  69. package/src/components/Stack/Stack.fragment.tsx +194 -0
  70. package/src/components/Table/Table.fragment.tsx +3 -3
  71. package/src/components/Tabs/Tabs.fragment.tsx +1 -1
  72. package/src/components/Tabs/Tabs.module.scss +2 -2
  73. package/src/components/Text/Text.fragment.tsx +188 -0
  74. package/src/components/Textarea/Textarea.fragment.tsx +1 -1
  75. package/src/components/Theme/Theme.fragment.tsx +2 -2
  76. package/src/components/Theme/ThemeToggle.module.scss +13 -13
  77. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
  78. package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
  79. package/src/components/ThinkingIndicator/index.tsx +258 -0
  80. package/src/components/Toast/Toast.fragment.tsx +1 -1
  81. package/src/components/Toggle/Toggle.fragment.tsx +1 -1
  82. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
  83. package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
  84. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
  85. package/src/index.ts +99 -3
  86. package/src/recipes/AIChat.recipe.ts +266 -0
  87. package/src/tokens/_computed.scss +212 -0
  88. package/src/tokens/_density.scss +171 -0
  89. package/src/tokens/_derive.scss +287 -0
  90. package/src/tokens/_index.scss +39 -1
  91. package/src/tokens/_mixins.scss +41 -0
  92. package/src/tokens/_palettes.scss +185 -0
  93. package/src/tokens/_radius.scss +107 -0
  94. package/src/tokens/_seeds.scss +59 -0
  95. package/src/tokens/_variables.scss +171 -130
  96. package/src/components/ColorChip/ColorChip.module.scss +0 -165
  97. 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
+ };