@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,196 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { ColorPicker } from '.';
4
+
5
+ export default defineSegment({
6
+ component: ColorPicker,
7
+
8
+ meta: {
9
+ name: 'ColorPicker',
10
+ description: 'Color selection control with hex input and visual picker. Displays a swatch that opens a full color picker on click.',
11
+ category: 'forms',
12
+ status: 'stable',
13
+ tags: ['color', 'picker', 'input', 'hex', 'swatch', 'theme'],
14
+ since: '0.2.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Theme customization interfaces',
20
+ 'Brand color selection',
21
+ 'Design tool color inputs',
22
+ 'User preference settings for colors',
23
+ ],
24
+ whenNot: [
25
+ 'Predefined color options only (use RadioGroup with swatches)',
26
+ 'Simple color display without editing (use a colored Badge)',
27
+ 'Color indication only (use semantic color tokens)',
28
+ ],
29
+ guidelines: [
30
+ 'Always provide a label to describe what the color is for',
31
+ 'Use description to explain color usage or constraints',
32
+ 'Consider providing color presets alongside the picker for common choices',
33
+ 'Validate hex format (#RRGGBB) on input',
34
+ ],
35
+ accessibility: [
36
+ 'Label is associated with the color input',
37
+ 'Swatch button has appropriate aria-label',
38
+ 'Color picker popup is keyboard accessible',
39
+ 'Hex input allows direct text entry',
40
+ ],
41
+ },
42
+
43
+ props: {
44
+ value: {
45
+ type: 'string',
46
+ description: 'Controlled color value in hex format (#RRGGBB)',
47
+ },
48
+ defaultValue: {
49
+ type: 'string',
50
+ description: 'Default color for uncontrolled usage',
51
+ default: '#000000',
52
+ },
53
+ onChange: {
54
+ type: 'function',
55
+ description: 'Called with new color value when changed',
56
+ },
57
+ label: {
58
+ type: 'string',
59
+ description: 'Label text above the picker',
60
+ },
61
+ description: {
62
+ type: 'string',
63
+ description: 'Helper text below the picker',
64
+ },
65
+ disabled: {
66
+ type: 'boolean',
67
+ description: 'Disable the color picker',
68
+ default: 'false',
69
+ },
70
+ size: {
71
+ type: 'enum',
72
+ description: 'Size variant',
73
+ values: ['sm', 'md'],
74
+ default: 'md',
75
+ },
76
+ showInput: {
77
+ type: 'boolean',
78
+ description: 'Show the hex input field',
79
+ default: 'true',
80
+ },
81
+ },
82
+
83
+ relations: [
84
+ { component: 'Input', relationship: 'sibling', note: 'ColorPicker is a specialized input for colors' },
85
+ { component: 'RadioGroup', relationship: 'alternative', note: 'Use RadioGroup for predefined color choices' },
86
+ { component: 'Field', relationship: 'parent', note: 'ColorPicker uses Field internally for structure' },
87
+ ],
88
+
89
+ contract: {
90
+ propsSummary: [
91
+ 'value: string - controlled hex color (#RRGGBB)',
92
+ 'defaultValue: string - initial color for uncontrolled usage',
93
+ 'onChange: (color: string) => void - change handler',
94
+ 'label: string - field label',
95
+ 'description: string - helper text',
96
+ 'disabled: boolean - disable interaction',
97
+ 'size: sm|md - size variant',
98
+ 'showInput: boolean - show hex input field',
99
+ ],
100
+ scenarioTags: [
101
+ 'forms.color',
102
+ 'input.specialized',
103
+ 'theme.customization',
104
+ ],
105
+ a11yRules: ['A11Y_LABEL_REQUIRED', 'A11Y_FOCUS_VISIBLE'],
106
+ },
107
+
108
+ variants: [
109
+ {
110
+ name: 'Default',
111
+ description: 'Basic color picker with label',
112
+ render: () => (
113
+ <ColorPicker
114
+ label="Brand Color"
115
+ defaultValue="#3b82f6"
116
+ />
117
+ ),
118
+ },
119
+ {
120
+ name: 'With Description',
121
+ description: 'Color picker with helper text',
122
+ render: () => (
123
+ <ColorPicker
124
+ label="Primary Color"
125
+ defaultValue="#10b981"
126
+ description="This color will be used for buttons and links"
127
+ />
128
+ ),
129
+ },
130
+ {
131
+ name: 'Controlled',
132
+ description: 'Controlled color picker that logs changes',
133
+ render: () => {
134
+ const [color, setColor] = React.useState('#ef4444');
135
+ return (
136
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
137
+ <ColorPicker
138
+ label="Accent Color"
139
+ value={color}
140
+ onChange={setColor}
141
+ />
142
+ <div style={{ fontSize: '14px', color: 'var(--fui-text-secondary)' }}>
143
+ Selected: {color}
144
+ </div>
145
+ </div>
146
+ );
147
+ },
148
+ },
149
+ {
150
+ name: 'Multiple Pickers',
151
+ description: 'Multiple color pickers for theme configuration',
152
+ render: () => (
153
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '240px' }}>
154
+ <ColorPicker label="Primary" defaultValue="#3b82f6" />
155
+ <ColorPicker label="Success" defaultValue="#22c55e" />
156
+ <ColorPicker label="Warning" defaultValue="#f59e0b" />
157
+ <ColorPicker label="Danger" defaultValue="#ef4444" />
158
+ </div>
159
+ ),
160
+ },
161
+ {
162
+ name: 'Compact',
163
+ description: 'Small size with swatch only (no input)',
164
+ render: () => (
165
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
166
+ <ColorPicker defaultValue="#ef4444" size="sm" showInput={false} />
167
+ <ColorPicker defaultValue="#f59e0b" size="sm" showInput={false} />
168
+ <ColorPicker defaultValue="#22c55e" size="sm" showInput={false} />
169
+ <ColorPicker defaultValue="#3b82f6" size="sm" showInput={false} />
170
+ </div>
171
+ ),
172
+ },
173
+ {
174
+ name: 'Sizes',
175
+ description: 'Different size variants',
176
+ render: () => (
177
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '240px' }}>
178
+ <ColorPicker label="Small" defaultValue="#3b82f6" size="sm" />
179
+ <ColorPicker label="Medium (default)" defaultValue="#3b82f6" size="md" />
180
+ </div>
181
+ ),
182
+ },
183
+ {
184
+ name: 'Disabled',
185
+ description: 'Disabled color picker',
186
+ render: () => (
187
+ <ColorPicker
188
+ label="Locked Color"
189
+ defaultValue="#64748b"
190
+ description="This color cannot be changed"
191
+ disabled
192
+ />
193
+ ),
194
+ },
195
+ ],
196
+ });
@@ -6,6 +6,31 @@
6
6
  flex-direction: column;
7
7
  }
8
8
 
9
+ // Size variants
10
+ .sizeSm {
11
+ .inputWrapper {
12
+ gap: var(--fui-space-1, $fui-space-1);
13
+ }
14
+
15
+ .swatch {
16
+ width: $fui-touch-sm;
17
+ height: $fui-touch-sm;
18
+ }
19
+
20
+ .hexInput {
21
+ min-width: 80px;
22
+ flex: 0 0 auto;
23
+ }
24
+
25
+ .label {
26
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
27
+ }
28
+ }
29
+
30
+ .sizeMd {
31
+ // Default size - no overrides needed
32
+ }
33
+
9
34
  .label {
10
35
  @include label-text;
11
36
  margin-bottom: var(--fui-space-1, $fui-space-1);
@@ -40,31 +65,16 @@
40
65
  }
41
66
  }
42
67
 
43
- .input {
44
- @include text-base;
45
- @include interactive-base;
68
+ // Hex input wrapper (using Input component)
69
+ .hexInput {
46
70
  flex: 1;
47
- height: 36px;
48
- padding: 0 var(--fui-space-2, $fui-space-2);
49
- background-color: var(--fui-bg-elevated, $fui-bg-elevated);
50
- border: 1px solid var(--fui-border-strong, $fui-border-strong);
51
- border-radius: var(--fui-radius-md, $fui-radius-md);
52
- font-family: var(--fui-font-mono, $fui-font-mono);
53
- font-size: var(--fui-font-size-sm, $fui-font-size-sm);
54
-
55
- &:hover:not(:disabled):not(:focus) {
56
- border-color: var(--fui-text-tertiary, $fui-text-tertiary);
57
- }
58
-
59
- &:focus {
60
- @include focus-ring;
61
- border-color: var(--fui-color-accent, $fui-color-accent);
62
- }
71
+ min-width: 0;
72
+ }
63
73
 
64
- &:disabled {
65
- background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
66
- color: var(--fui-text-tertiary, $fui-text-tertiary);
67
- }
74
+ // Style the actual input field inside Input component
75
+ .hexInputField {
76
+ font-family: var(--fui-font-mono, $fui-font-mono) !important;
77
+ text-transform: lowercase;
68
78
  }
69
79
 
70
80
  .positioner {
@@ -2,16 +2,27 @@ import * as React from 'react';
2
2
  import { HexColorPicker } from 'react-colorful';
3
3
  import { Popover as BasePopover } from '@base-ui/react/popover';
4
4
  import { Field } from '@base-ui/react/field';
5
+ import { Input } from '../Input';
5
6
  import styles from './ColorPicker.module.scss';
6
7
  import '../../styles/globals.scss';
7
8
 
8
9
  export interface ColorPickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
10
+ /** Label text above the picker */
9
11
  label?: string;
12
+ /** Controlled color value in hex format (#RRGGBB) */
10
13
  value?: string;
14
+ /** Default color for uncontrolled usage */
11
15
  defaultValue?: string;
16
+ /** Called with new color value when changed */
12
17
  onChange?: (color: string) => void;
18
+ /** Helper text below the picker */
13
19
  description?: string;
20
+ /** Disable the color picker */
14
21
  disabled?: boolean;
22
+ /** Size variant */
23
+ size?: 'sm' | 'md';
24
+ /** Show the hex input field */
25
+ showInput?: boolean;
15
26
  }
16
27
 
17
28
  export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
@@ -23,6 +34,8 @@ export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
23
34
  onChange,
24
35
  description,
25
36
  disabled = false,
37
+ size = 'md',
38
+ showInput = true,
26
39
  className,
27
40
  ...htmlProps
28
41
  },
@@ -45,8 +58,7 @@ export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
45
58
  onChange?.(color);
46
59
  };
47
60
 
48
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
49
- const newValue = e.target.value;
61
+ const handleInputChange = (newValue: string) => {
50
62
  setInputValue(newValue);
51
63
 
52
64
  // Only update if it's a valid hex color
@@ -63,12 +75,18 @@ export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
63
75
  }
64
76
  };
65
77
 
78
+ const wrapperClasses = [
79
+ styles.wrapper,
80
+ styles[`size${size.charAt(0).toUpperCase() + size.slice(1)}`],
81
+ className,
82
+ ].filter(Boolean).join(' ');
83
+
66
84
  return (
67
85
  <Field.Root
68
86
  ref={ref}
69
87
  {...htmlProps}
70
88
  disabled={disabled}
71
- className={[styles.wrapper, className].filter(Boolean).join(' ')}
89
+ className={wrapperClasses}
72
90
  >
73
91
  {label && <Field.Label className={styles.label}>{label}</Field.Label>}
74
92
  <div className={styles.inputWrapper}>
@@ -77,6 +95,7 @@ export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
77
95
  className={styles.swatch}
78
96
  style={{ backgroundColor: displayValue }}
79
97
  disabled={disabled}
98
+ aria-label={label ? `Edit ${label} color` : 'Edit color'}
80
99
  />
81
100
  <BasePopover.Portal>
82
101
  <BasePopover.Positioner side="bottom" align="start" sideOffset={4} className={styles.positioner}>
@@ -86,15 +105,18 @@ export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
86
105
  </BasePopover.Positioner>
87
106
  </BasePopover.Portal>
88
107
  </BasePopover.Root>
89
- <input
90
- type="text"
91
- value={inputValue}
92
- onChange={handleInputChange}
93
- onBlur={handleInputBlur}
94
- disabled={disabled}
95
- className={styles.input}
96
- spellCheck={false}
97
- />
108
+ {showInput && (
109
+ <Input
110
+ value={inputValue}
111
+ onChange={handleInputChange}
112
+ onBlur={handleInputBlur}
113
+ disabled={disabled}
114
+ size={size}
115
+ className={styles.hexInput}
116
+ inputClassName={styles.hexInputField}
117
+ aria-label={label ? `${label} hex value` : 'Hex value'}
118
+ />
119
+ )}
98
120
  </div>
99
121
  {description && (
100
122
  <Field.Description className={styles.description}>
@@ -0,0 +1,220 @@
1
+ import React, { useState } from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Combobox } from '.';
4
+
5
+ // Stateful wrapper for interactive demos
6
+ function StatefulCombobox(props: React.ComponentProps<typeof Combobox> & {
7
+ children: React.ReactNode;
8
+ initialValue?: string | string[];
9
+ }) {
10
+ const { initialValue, children, ...rest } = props;
11
+ const [value, setValue] = useState<string | string[] | null>(initialValue ?? (props.multiple ? [] : null));
12
+ return (
13
+ <Combobox {...rest} value={value} onValueChange={setValue}>
14
+ {children}
15
+ </Combobox>
16
+ );
17
+ }
18
+
19
+ export default defineSegment({
20
+ component: Combobox,
21
+
22
+ meta: {
23
+ name: 'Combobox',
24
+ description: 'Searchable select input that filters a dropdown list of options as you type. Supports single and multiple selection with chips.',
25
+ category: 'forms',
26
+ status: 'stable',
27
+ tags: ['combobox', 'autocomplete', 'search', 'select', 'typeahead', 'form', 'multiselect'],
28
+ since: '0.1.0',
29
+ },
30
+
31
+ usage: {
32
+ when: [
33
+ 'Users need to search/filter through many options',
34
+ 'Large option lists where scrolling is impractical',
35
+ 'When users might know what they are looking for',
36
+ 'Autocomplete or typeahead functionality',
37
+ 'Multiple selections from a searchable list',
38
+ ],
39
+ whenNot: [
40
+ 'Few options (under 5) - use Select or RadioGroup',
41
+ 'Free-form text with no predefined options - use Input',
42
+ 'Non-searchable single selection - use Select',
43
+ 'Actions, not selection - use Menu',
44
+ ],
45
+ guidelines: [
46
+ 'Include a placeholder that explains what to search for',
47
+ 'Provide an empty state message when no results match',
48
+ 'Group related options with Combobox.Group for large lists',
49
+ 'Keep option text concise and searchable',
50
+ 'Use multiple prop for multi-select with chip display',
51
+ ],
52
+ accessibility: [
53
+ 'Full keyboard navigation support (arrow keys, enter, escape)',
54
+ 'Type-ahead filtering within options',
55
+ 'Proper ARIA combobox roles and attributes',
56
+ 'Screen reader announcements for filtered results',
57
+ 'Chip removal via keyboard in multi-select mode',
58
+ ],
59
+ },
60
+
61
+ props: {
62
+ children: {
63
+ type: 'node',
64
+ description: 'Combobox input and content',
65
+ required: true,
66
+ },
67
+ value: {
68
+ type: 'string',
69
+ description: 'Controlled selected value (string for single, string[] for multiple)',
70
+ },
71
+ defaultValue: {
72
+ type: 'string',
73
+ description: 'Default selected value (uncontrolled)',
74
+ },
75
+ onValueChange: {
76
+ type: 'function',
77
+ description: 'Called when selection changes',
78
+ },
79
+ multiple: {
80
+ type: 'boolean',
81
+ description: 'Allow multiple selections with chips',
82
+ default: 'false',
83
+ },
84
+ placeholder: {
85
+ type: 'string',
86
+ description: 'Placeholder text for the input',
87
+ },
88
+ disabled: {
89
+ type: 'boolean',
90
+ description: 'Disable the combobox',
91
+ default: 'false',
92
+ },
93
+ autoHighlight: {
94
+ type: 'boolean',
95
+ description: 'Auto-highlight first matching item while filtering',
96
+ default: 'true',
97
+ },
98
+ },
99
+
100
+ relations: [
101
+ { component: 'Select', relationship: 'alternative', note: 'Use Select when search/filtering is not needed' },
102
+ { component: 'Input', relationship: 'sibling', note: 'Use Input for free-form text entry' },
103
+ { component: 'Listbox', relationship: 'sibling', note: 'Use Listbox for inline option lists' },
104
+ ],
105
+
106
+ contract: {
107
+ propsSummary: [
108
+ 'value: string | string[] - controlled selected value',
109
+ 'onValueChange: (value) => void - selection handler',
110
+ 'multiple: boolean - enable multi-select with chips',
111
+ 'placeholder: string - input placeholder text',
112
+ 'disabled: boolean - disable combobox',
113
+ 'autoHighlight: boolean - auto-highlight first match',
114
+ ],
115
+ scenarioTags: [
116
+ 'form.combobox',
117
+ 'form.autocomplete',
118
+ 'form.multiselect',
119
+ 'input.search',
120
+ ],
121
+ a11yRules: ['A11Y_COMBOBOX_KEYBOARD', 'A11Y_COMBOBOX_LABEL'],
122
+ },
123
+
124
+ ai: {
125
+ compositionPattern: 'compound',
126
+ subComponents: ['Input', 'Trigger', 'Content', 'Item', 'ItemIndicator', 'Empty', 'Group', 'GroupLabel'],
127
+ requiredChildren: ['Input', 'Content'],
128
+ commonPatterns: [
129
+ '<Combobox placeholder="Search..."><Combobox.Input /><Combobox.Content><Combobox.Item value="opt1">{label1}</Combobox.Item><Combobox.Item value="opt2">{label2}</Combobox.Item></Combobox.Content></Combobox>',
130
+ '<Combobox multiple placeholder="Select items..."><Combobox.Input /><Combobox.Content><Combobox.Item value="opt1">{label1}</Combobox.Item><Combobox.Item value="opt2">{label2}</Combobox.Item></Combobox.Content></Combobox>',
131
+ ],
132
+ },
133
+
134
+ variants: [
135
+ {
136
+ name: 'Default',
137
+ description: 'Basic searchable select',
138
+ render: () => (
139
+ <StatefulCombobox placeholder="Select a fruit">
140
+ <Combobox.Input />
141
+ <Combobox.Content>
142
+ <Combobox.Item value="apple">Apple</Combobox.Item>
143
+ <Combobox.Item value="banana">Banana</Combobox.Item>
144
+ <Combobox.Item value="orange">Orange</Combobox.Item>
145
+ <Combobox.Item value="grape">Grape</Combobox.Item>
146
+ </Combobox.Content>
147
+ </StatefulCombobox>
148
+ ),
149
+ },
150
+ {
151
+ name: 'Multiple Selection',
152
+ description: 'Multi-select with chips',
153
+ render: () => (
154
+ <StatefulCombobox multiple placeholder="Select fruits...">
155
+ <Combobox.Input />
156
+ <Combobox.Content>
157
+ <Combobox.Item value="apple">Apple</Combobox.Item>
158
+ <Combobox.Item value="banana">Banana</Combobox.Item>
159
+ <Combobox.Item value="orange">Orange</Combobox.Item>
160
+ <Combobox.Item value="grape">Grape</Combobox.Item>
161
+ <Combobox.Item value="mango">Mango</Combobox.Item>
162
+ <Combobox.Item value="kiwi">Kiwi</Combobox.Item>
163
+ </Combobox.Content>
164
+ </StatefulCombobox>
165
+ ),
166
+ },
167
+ {
168
+ name: 'With Groups',
169
+ description: 'Options organized into groups',
170
+ render: () => (
171
+ <StatefulCombobox placeholder="Search countries...">
172
+ <Combobox.Input />
173
+ <Combobox.Content>
174
+ <Combobox.Group>
175
+ <Combobox.GroupLabel>North America</Combobox.GroupLabel>
176
+ <Combobox.Item value="us">United States</Combobox.Item>
177
+ <Combobox.Item value="ca">Canada</Combobox.Item>
178
+ <Combobox.Item value="mx">Mexico</Combobox.Item>
179
+ </Combobox.Group>
180
+ <Combobox.Group>
181
+ <Combobox.GroupLabel>Europe</Combobox.GroupLabel>
182
+ <Combobox.Item value="uk">United Kingdom</Combobox.Item>
183
+ <Combobox.Item value="de">Germany</Combobox.Item>
184
+ <Combobox.Item value="fr">France</Combobox.Item>
185
+ </Combobox.Group>
186
+ </Combobox.Content>
187
+ </StatefulCombobox>
188
+ ),
189
+ },
190
+ {
191
+ name: 'With Empty State',
192
+ description: 'Shows message when no results match',
193
+ render: () => (
194
+ <StatefulCombobox placeholder="Search programming languages...">
195
+ <Combobox.Input />
196
+ <Combobox.Content>
197
+ <Combobox.Empty>No results found</Combobox.Empty>
198
+ <Combobox.Item value="js">JavaScript</Combobox.Item>
199
+ <Combobox.Item value="ts">TypeScript</Combobox.Item>
200
+ <Combobox.Item value="py">Python</Combobox.Item>
201
+ <Combobox.Item value="rs">Rust</Combobox.Item>
202
+ <Combobox.Item value="go">Go</Combobox.Item>
203
+ </Combobox.Content>
204
+ </StatefulCombobox>
205
+ ),
206
+ },
207
+ {
208
+ name: 'Disabled',
209
+ description: 'Disabled combobox',
210
+ render: () => (
211
+ <Combobox disabled placeholder="Search...">
212
+ <Combobox.Input />
213
+ <Combobox.Content>
214
+ <Combobox.Item value="1">Option 1</Combobox.Item>
215
+ </Combobox.Content>
216
+ </Combobox>
217
+ ),
218
+ },
219
+ ],
220
+ });