@fragments-sdk/ui 0.3.0 → 0.5.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 (175) hide show
  1. package/README.md +98 -2
  2. package/fragments.json +1 -1
  3. package/package.json +11 -5
  4. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  5. package/src/components/Accordion/Accordion.module.scss +111 -0
  6. package/src/components/Accordion/index.tsx +271 -0
  7. package/src/components/Alert/Alert.fragment.tsx +67 -42
  8. package/src/components/Alert/Alert.module.scss +31 -21
  9. package/src/components/Alert/index.tsx +202 -73
  10. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  11. package/src/components/AppShell/AppShell.module.scss +213 -0
  12. package/src/components/AppShell/index.tsx +398 -0
  13. package/src/components/Avatar/Avatar.fragment.tsx +2 -2
  14. package/src/components/Avatar/index.tsx +8 -9
  15. package/src/components/Badge/Badge.fragment.tsx +2 -2
  16. package/src/components/Badge/Badge.module.scss +16 -10
  17. package/src/components/Badge/index.tsx +20 -6
  18. package/src/components/Box/Box.fragment.tsx +168 -0
  19. package/src/components/Box/Box.module.scss +84 -0
  20. package/src/components/Box/index.tsx +78 -0
  21. package/src/components/Button/Button.fragment.tsx +2 -2
  22. package/src/components/Button/Button.module.scss +42 -0
  23. package/src/components/Button/index.tsx +67 -33
  24. package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
  25. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  26. package/src/components/ButtonGroup/index.tsx +40 -0
  27. package/src/components/Card/Card.fragment.tsx +52 -26
  28. package/src/components/Card/Card.module.scss +52 -5
  29. package/src/components/Card/index.tsx +154 -53
  30. package/src/components/Chart/Chart.fragment.tsx +213 -0
  31. package/src/components/Chart/Chart.module.scss +123 -0
  32. package/src/components/Chart/index.tsx +267 -0
  33. package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
  34. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  35. package/src/components/Checkbox/index.tsx +3 -4
  36. package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
  37. package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
  38. package/src/components/CodeBlock/index.tsx +599 -0
  39. package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
  40. package/src/components/Collapsible/Collapsible.module.scss +117 -0
  41. package/src/components/Collapsible/index.tsx +219 -0
  42. package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
  43. package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
  44. package/src/components/ColorPicker/index.tsx +129 -0
  45. package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
  46. package/src/components/ConversationList/ConversationList.module.scss +160 -0
  47. package/src/components/ConversationList/index.tsx +254 -0
  48. package/src/components/Dialog/Dialog.fragment.tsx +12 -3
  49. package/src/components/Dialog/Dialog.module.scss +26 -7
  50. package/src/components/Dialog/index.tsx +12 -15
  51. package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
  52. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  53. package/src/components/EmptyState/index.tsx +104 -69
  54. package/src/components/Field/Field.fragment.tsx +165 -0
  55. package/src/components/Field/Field.module.scss +31 -0
  56. package/src/components/Field/index.tsx +143 -0
  57. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  58. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  59. package/src/components/Fieldset/index.tsx +47 -0
  60. package/src/components/Form/Form.fragment.tsx +286 -0
  61. package/src/components/Form/Form.module.scss +8 -0
  62. package/src/components/Form/index.tsx +53 -0
  63. package/src/components/Grid/Grid.fragment.tsx +18 -18
  64. package/src/components/Grid/index.tsx +6 -1
  65. package/src/components/Header/Header.fragment.tsx +192 -0
  66. package/src/components/Header/Header.module.scss +208 -0
  67. package/src/components/Header/index.tsx +363 -0
  68. package/src/components/Icon/Icon.fragment.tsx +138 -0
  69. package/src/components/Icon/Icon.module.scss +38 -0
  70. package/src/components/Icon/index.tsx +58 -0
  71. package/src/components/Image/Image.fragment.tsx +195 -0
  72. package/src/components/Image/Image.module.scss +77 -0
  73. package/src/components/Image/index.tsx +95 -0
  74. package/src/components/Input/Input.fragment.tsx +1 -1
  75. package/src/components/Input/Input.module.scss +75 -2
  76. package/src/components/Input/index.tsx +60 -21
  77. package/src/components/Link/Link.fragment.tsx +132 -0
  78. package/src/components/Link/Link.module.scss +67 -0
  79. package/src/components/Link/index.tsx +57 -0
  80. package/src/components/List/List.fragment.tsx +152 -0
  81. package/src/components/List/List.module.scss +71 -0
  82. package/src/components/List/index.tsx +106 -0
  83. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  84. package/src/components/Listbox/Listbox.module.scss +97 -0
  85. package/src/components/Listbox/index.tsx +121 -0
  86. package/src/components/Loading/Loading.fragment.tsx +153 -0
  87. package/src/components/Loading/Loading.module.scss +256 -0
  88. package/src/components/Loading/index.tsx +236 -0
  89. package/src/components/Menu/Menu.fragment.tsx +12 -3
  90. package/src/components/Menu/Menu.module.scss +17 -1
  91. package/src/components/Menu/index.tsx +3 -3
  92. package/src/components/Message/Message.fragment.tsx +200 -0
  93. package/src/components/Message/Message.module.scss +224 -0
  94. package/src/components/Message/index.tsx +278 -0
  95. package/src/components/Popover/Popover.fragment.tsx +13 -4
  96. package/src/components/Popover/Popover.module.scss +33 -10
  97. package/src/components/Popover/index.tsx +9 -11
  98. package/src/components/Progress/Progress.fragment.tsx +1 -1
  99. package/src/components/Progress/Progress.module.scss +11 -11
  100. package/src/components/Progress/index.tsx +34 -7
  101. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  102. package/src/components/Prompt/Prompt.module.scss +243 -0
  103. package/src/components/Prompt/index.tsx +439 -0
  104. package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
  105. package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
  106. package/src/components/RadioGroup/index.tsx +3 -4
  107. package/src/components/Select/Select.fragment.tsx +10 -1
  108. package/src/components/Select/Select.module.scss +8 -0
  109. package/src/components/Select/index.tsx +91 -12
  110. package/src/components/Separator/Separator.fragment.tsx +1 -1
  111. package/src/components/Separator/index.tsx +7 -3
  112. package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
  113. package/src/components/Sidebar/Sidebar.module.scss +91 -47
  114. package/src/components/Sidebar/index.tsx +57 -14
  115. package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
  116. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  117. package/src/components/Slider/Slider.fragment.tsx +201 -0
  118. package/src/components/Slider/Slider.module.scss +87 -0
  119. package/src/components/Slider/index.tsx +88 -0
  120. package/src/components/Stack/Stack.fragment.tsx +194 -0
  121. package/src/components/Stack/Stack.module.scss +120 -0
  122. package/src/components/Stack/index.tsx +148 -0
  123. package/src/components/Table/Table.fragment.tsx +10 -3
  124. package/src/components/Table/Table.module.scss +57 -0
  125. package/src/components/Table/index.tsx +44 -6
  126. package/src/components/Tabs/Tabs.fragment.tsx +10 -1
  127. package/src/components/Tabs/Tabs.module.scss +25 -10
  128. package/src/components/Tabs/index.tsx +11 -8
  129. package/src/components/Text/Text.fragment.tsx +188 -0
  130. package/src/components/Text/Text.module.scss +82 -0
  131. package/src/components/Text/index.tsx +58 -0
  132. package/src/components/Textarea/Textarea.fragment.tsx +1 -1
  133. package/src/components/Textarea/index.tsx +3 -7
  134. package/src/components/Theme/Theme.fragment.tsx +128 -0
  135. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  136. package/src/components/Theme/index.tsx +343 -0
  137. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
  138. package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
  139. package/src/components/ThinkingIndicator/index.tsx +258 -0
  140. package/src/components/Toast/Toast.fragment.tsx +6 -6
  141. package/src/components/Toast/Toast.module.scss +16 -1
  142. package/src/components/Toast/index.tsx +27 -11
  143. package/src/components/Toggle/Toggle.fragment.tsx +1 -1
  144. package/src/components/Toggle/Toggle.module.scss +25 -10
  145. package/src/components/Toggle/index.tsx +12 -0
  146. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
  147. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  148. package/src/components/ToggleGroup/index.tsx +144 -0
  149. package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
  150. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  151. package/src/components/Tooltip/index.tsx +4 -2
  152. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  153. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  154. package/src/components/VisuallyHidden/index.tsx +29 -0
  155. package/src/index.ts +278 -3
  156. package/src/recipes/AIChat.recipe.ts +266 -0
  157. package/src/recipes/AppShell.recipe.ts +175 -0
  158. package/src/recipes/CardGrid.recipe.ts +6 -2
  159. package/src/recipes/ChatInterface.recipe.ts +87 -0
  160. package/src/recipes/CodeExamples.recipe.ts +66 -0
  161. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  162. package/src/recipes/DashboardNav.recipe.ts +183 -0
  163. package/src/recipes/LoginForm.recipe.ts +8 -1
  164. package/src/recipes/SettingsPage.recipe.ts +37 -20
  165. package/src/styles/globals.scss +31 -0
  166. package/src/tokens/_computed.scss +212 -0
  167. package/src/tokens/_density.scss +171 -0
  168. package/src/tokens/_derive.scss +287 -0
  169. package/src/tokens/_index.scss +41 -0
  170. package/src/tokens/_mixins.scss +95 -1
  171. package/src/tokens/_palettes.scss +185 -0
  172. package/src/tokens/_radius.scss +107 -0
  173. package/src/tokens/_seeds.scss +59 -0
  174. package/src/tokens/_variables.scss +507 -101
  175. package/src/utils/a11y.tsx +439 -0
@@ -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
+ });
@@ -0,0 +1,119 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ .wrapper {
5
+ display: flex;
6
+ flex-direction: column;
7
+ }
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
+
34
+ .label {
35
+ @include label-text;
36
+ margin-bottom: var(--fui-space-1, $fui-space-1);
37
+ }
38
+
39
+ .inputWrapper {
40
+ display: flex;
41
+ gap: var(--fui-space-2, $fui-space-2);
42
+ align-items: center;
43
+ }
44
+
45
+ .swatch {
46
+ @include button-reset;
47
+ @include interactive-base;
48
+ width: 40px;
49
+ height: 36px;
50
+ border: 1px solid var(--fui-border, $fui-border);
51
+ border-radius: var(--fui-radius-sm, $fui-radius-sm);
52
+ flex-shrink: 0;
53
+
54
+ &:hover:not(:disabled) {
55
+ border-color: var(--fui-border-strong, $fui-border-strong);
56
+ }
57
+
58
+ &:focus-visible {
59
+ @include focus-ring;
60
+ }
61
+
62
+ &:disabled {
63
+ opacity: 0.5;
64
+ cursor: not-allowed;
65
+ }
66
+ }
67
+
68
+ // Hex input wrapper (using Input component)
69
+ .hexInput {
70
+ flex: 1;
71
+ min-width: 0;
72
+ }
73
+
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;
78
+ }
79
+
80
+ .positioner {
81
+ z-index: 100;
82
+ outline: none;
83
+ }
84
+
85
+ .popup {
86
+ background-color: var(--fui-bg-elevated, $fui-bg-elevated);
87
+ border: 1px solid var(--fui-border, $fui-border);
88
+ border-radius: var(--fui-radius-lg, $fui-radius-lg);
89
+ box-shadow: var(--fui-shadow-md, $fui-shadow-md);
90
+ padding: var(--fui-space-2, $fui-space-2);
91
+
92
+ // react-colorful styling overrides
93
+ :global(.react-colorful) {
94
+ width: 200px;
95
+ height: 200px;
96
+ }
97
+
98
+ :global(.react-colorful__saturation) {
99
+ border-radius: var(--fui-radius-md, $fui-radius-md);
100
+ margin-bottom: var(--fui-space-2, $fui-space-2);
101
+ }
102
+
103
+ :global(.react-colorful__hue) {
104
+ height: 12px;
105
+ border-radius: var(--fui-radius-sm, $fui-radius-sm);
106
+ }
107
+
108
+ :global(.react-colorful__pointer) {
109
+ width: 16px;
110
+ height: 16px;
111
+ border: 2px solid white;
112
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
113
+ }
114
+ }
115
+
116
+ .description {
117
+ @include helper-text;
118
+ margin-top: var(--fui-space-1, $fui-space-1);
119
+ }
@@ -0,0 +1,129 @@
1
+ import * as React from 'react';
2
+ import { HexColorPicker } from 'react-colorful';
3
+ import { Popover as BasePopover } from '@base-ui/react/popover';
4
+ import { Field } from '@base-ui/react/field';
5
+ import { Input } from '../Input';
6
+ import styles from './ColorPicker.module.scss';
7
+ import '../../styles/globals.scss';
8
+
9
+ export interface ColorPickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
10
+ /** Label text above the picker */
11
+ label?: string;
12
+ /** Controlled color value in hex format (#RRGGBB) */
13
+ value?: string;
14
+ /** Default color for uncontrolled usage */
15
+ defaultValue?: string;
16
+ /** Called with new color value when changed */
17
+ onChange?: (color: string) => void;
18
+ /** Helper text below the picker */
19
+ description?: string;
20
+ /** Disable the color picker */
21
+ disabled?: boolean;
22
+ /** Size variant */
23
+ size?: 'sm' | 'md';
24
+ /** Show the hex input field */
25
+ showInput?: boolean;
26
+ }
27
+
28
+ export const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(
29
+ function ColorPicker(
30
+ {
31
+ label,
32
+ value,
33
+ defaultValue = '#000000',
34
+ onChange,
35
+ description,
36
+ disabled = false,
37
+ size = 'md',
38
+ showInput = true,
39
+ className,
40
+ ...htmlProps
41
+ },
42
+ ref
43
+ ) {
44
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
45
+ const [inputValue, setInputValue] = React.useState(value ?? defaultValue);
46
+ const displayValue = value !== undefined ? value : internalValue;
47
+
48
+ // Sync input value when controlled value changes
49
+ React.useEffect(() => {
50
+ if (value !== undefined) {
51
+ setInputValue(value);
52
+ }
53
+ }, [value]);
54
+
55
+ const handleChange = (color: string) => {
56
+ setInternalValue(color);
57
+ setInputValue(color);
58
+ onChange?.(color);
59
+ };
60
+
61
+ const handleInputChange = (newValue: string) => {
62
+ setInputValue(newValue);
63
+
64
+ // Only update if it's a valid hex color
65
+ if (/^#[0-9A-Fa-f]{6}$/.test(newValue)) {
66
+ setInternalValue(newValue);
67
+ onChange?.(newValue);
68
+ }
69
+ };
70
+
71
+ const handleInputBlur = () => {
72
+ // Reset to valid value on blur if invalid
73
+ if (!/^#[0-9A-Fa-f]{6}$/.test(inputValue)) {
74
+ setInputValue(displayValue);
75
+ }
76
+ };
77
+
78
+ const wrapperClasses = [
79
+ styles.wrapper,
80
+ styles[`size${size.charAt(0).toUpperCase() + size.slice(1)}`],
81
+ className,
82
+ ].filter(Boolean).join(' ');
83
+
84
+ return (
85
+ <Field.Root
86
+ ref={ref}
87
+ {...htmlProps}
88
+ disabled={disabled}
89
+ className={wrapperClasses}
90
+ >
91
+ {label && <Field.Label className={styles.label}>{label}</Field.Label>}
92
+ <div className={styles.inputWrapper}>
93
+ <BasePopover.Root>
94
+ <BasePopover.Trigger
95
+ className={styles.swatch}
96
+ style={{ backgroundColor: displayValue }}
97
+ disabled={disabled}
98
+ aria-label={label ? `Edit ${label} color` : 'Edit color'}
99
+ />
100
+ <BasePopover.Portal>
101
+ <BasePopover.Positioner side="bottom" align="start" sideOffset={4} className={styles.positioner}>
102
+ <BasePopover.Popup className={styles.popup}>
103
+ <HexColorPicker color={displayValue} onChange={handleChange} />
104
+ </BasePopover.Popup>
105
+ </BasePopover.Positioner>
106
+ </BasePopover.Portal>
107
+ </BasePopover.Root>
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
+ )}
120
+ </div>
121
+ {description && (
122
+ <Field.Description className={styles.description}>
123
+ {description}
124
+ </Field.Description>
125
+ )}
126
+ </Field.Root>
127
+ );
128
+ }
129
+ );
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { ConversationList } from '.';
4
+ import { Message } from '../Message';
5
+
6
+ export default defineSegment({
7
+ component: ConversationList,
8
+
9
+ meta: {
10
+ name: 'ConversationList',
11
+ description: 'Scrollable message container with auto-scroll and history loading',
12
+ category: 'ai',
13
+ status: 'stable',
14
+ tags: ['conversation', 'chat', 'messages', 'scroll', 'ai', 'list'],
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Building a chat interface with multiple messages',
20
+ 'Need auto-scroll behavior when new messages arrive',
21
+ 'Require infinite scroll for loading message history',
22
+ 'Want date separators between message groups',
23
+ ],
24
+ whenNot: [
25
+ 'Simple list of items without chat context (use List)',
26
+ 'Single message display (use Message directly)',
27
+ 'Non-scrolling message layout',
28
+ ],
29
+ guidelines: [
30
+ 'Use autoScroll="smart" for best UX (only auto-scrolls when near bottom)',
31
+ 'Implement onScrollTop for loading older messages',
32
+ 'Provide an emptyState for new conversations',
33
+ 'Use DateSeparator between messages from different days',
34
+ ],
35
+ accessibility: [
36
+ 'Uses proper ARIA roles for separators',
37
+ 'Typing indicator has aria-label',
38
+ 'Smooth scroll respects reduced motion preferences',
39
+ 'Keyboard navigation works within scrollable container',
40
+ ],
41
+ },
42
+
43
+ props: {
44
+ children: {
45
+ type: 'ReactNode',
46
+ description: 'Message components',
47
+ required: true,
48
+ },
49
+ autoScroll: {
50
+ type: 'boolean | "smart"',
51
+ default: '"smart"',
52
+ description: 'Auto-scroll behavior: true (always), false (never), or "smart" (only when near bottom)',
53
+ },
54
+ onScrollTop: {
55
+ type: 'function',
56
+ description: 'Callback when user scrolls to top (for loading history)',
57
+ },
58
+ loadingHistory: {
59
+ type: 'boolean',
60
+ default: 'false',
61
+ description: 'Show loading spinner at top when loading history',
62
+ },
63
+ emptyState: {
64
+ type: 'ReactNode',
65
+ description: 'Content to show when conversation is empty',
66
+ },
67
+ scrollTopThreshold: {
68
+ type: 'number',
69
+ default: '50',
70
+ description: 'Pixels from top to trigger onScrollTop',
71
+ },
72
+ scrollBottomThreshold: {
73
+ type: 'number',
74
+ default: '100',
75
+ description: 'Pixels from bottom for smart auto-scroll',
76
+ },
77
+ },
78
+
79
+ relations: [
80
+ {
81
+ component: 'Message',
82
+ relationship: 'child',
83
+ note: 'ConversationList contains Message components',
84
+ },
85
+ {
86
+ component: 'ThinkingIndicator',
87
+ relationship: 'child',
88
+ note: 'Show ThinkingIndicator at bottom while awaiting response',
89
+ },
90
+ {
91
+ component: 'Prompt',
92
+ relationship: 'sibling',
93
+ note: 'Typically paired with Prompt for input',
94
+ },
95
+ ],
96
+
97
+ contract: {
98
+ propsSummary: [
99
+ 'children: ReactNode - Message components',
100
+ 'autoScroll: boolean | "smart" - scroll behavior (default: "smart")',
101
+ 'onScrollTop: () => void - callback for loading history',
102
+ 'loadingHistory: boolean - show history loading spinner',
103
+ 'emptyState: ReactNode - empty conversation content',
104
+ ],
105
+ scenarioTags: [
106
+ 'ui.chat',
107
+ 'ui.conversation',
108
+ 'ai.assistant',
109
+ 'layout.scroll',
110
+ ],
111
+ a11yRules: [
112
+ 'A11Y_ARIA_ROLES',
113
+ 'A11Y_MOTION_PREFERENCE',
114
+ ],
115
+ bans: [],
116
+ },
117
+
118
+ variants: [
119
+ {
120
+ name: 'Basic',
121
+ description: 'Simple conversation with messages',
122
+ render: () => (
123
+ <div style={{ height: '300px', border: '1px solid #e4e4e7', borderRadius: '8px' }}>
124
+ <ConversationList>
125
+ <Message role="user">
126
+ <Message.Content>Hello!</Message.Content>
127
+ </Message>
128
+ <Message role="assistant">
129
+ <Message.Content>Hi there! How can I help you today?</Message.Content>
130
+ </Message>
131
+ <Message role="user">
132
+ <Message.Content>Can you explain React hooks?</Message.Content>
133
+ </Message>
134
+ </ConversationList>
135
+ </div>
136
+ ),
137
+ },
138
+ {
139
+ name: 'With Date Separators',
140
+ description: 'Messages grouped by date',
141
+ render: () => (
142
+ <div style={{ height: '300px', border: '1px solid #e4e4e7', borderRadius: '8px' }}>
143
+ <ConversationList>
144
+ <ConversationList.DateSeparator date={new Date(Date.now() - 86400000)} />
145
+ <Message role="user">
146
+ <Message.Content>A message from yesterday</Message.Content>
147
+ </Message>
148
+ <ConversationList.DateSeparator date={new Date()} />
149
+ <Message role="assistant">
150
+ <Message.Content>And a message from today!</Message.Content>
151
+ </Message>
152
+ </ConversationList>
153
+ </div>
154
+ ),
155
+ },
156
+ {
157
+ name: 'With Typing Indicator',
158
+ description: 'Shows assistant is typing',
159
+ render: () => (
160
+ <div style={{ height: '300px', border: '1px solid #e4e4e7', borderRadius: '8px' }}>
161
+ <ConversationList>
162
+ <Message role="user">
163
+ <Message.Content>What is TypeScript?</Message.Content>
164
+ </Message>
165
+ <ConversationList.TypingIndicator name="Assistant" />
166
+ </ConversationList>
167
+ </div>
168
+ ),
169
+ },
170
+ {
171
+ name: 'Loading History',
172
+ description: 'Loading older messages',
173
+ render: () => (
174
+ <div style={{ height: '300px', border: '1px solid #e4e4e7', borderRadius: '8px' }}>
175
+ <ConversationList loadingHistory>
176
+ <Message role="user">
177
+ <Message.Content>This is the latest message</Message.Content>
178
+ </Message>
179
+ </ConversationList>
180
+ </div>
181
+ ),
182
+ },
183
+ {
184
+ name: 'Empty State',
185
+ description: 'No messages yet',
186
+ render: () => (
187
+ <div style={{ height: '300px', border: '1px solid #e4e4e7', borderRadius: '8px' }}>
188
+ <ConversationList
189
+ emptyState={
190
+ <div style={{ textAlign: 'center', color: '#71717a', padding: '2rem' }}>
191
+ <p>No messages yet</p>
192
+ <p style={{ fontSize: '12px' }}>Start a conversation!</p>
193
+ </div>
194
+ }
195
+ >
196
+ {/* No messages */}
197
+ </ConversationList>
198
+ </div>
199
+ ),
200
+ },
201
+ ],
202
+ });