@proyecto-viviana/solidaria-components 0.2.5 → 0.3.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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -272
  3. package/dist/ActionBar.d.ts +79 -0
  4. package/dist/ActionBar.d.ts.map +1 -0
  5. package/dist/ActionGroup.d.ts +74 -0
  6. package/dist/ActionGroup.d.ts.map +1 -0
  7. package/dist/Alert.d.ts +70 -0
  8. package/dist/Alert.d.ts.map +1 -0
  9. package/dist/Autocomplete.d.ts +5 -5
  10. package/dist/Autocomplete.d.ts.map +1 -1
  11. package/dist/Breadcrumbs.d.ts +27 -8
  12. package/dist/Breadcrumbs.d.ts.map +1 -1
  13. package/dist/Button.d.ts +28 -5
  14. package/dist/Button.d.ts.map +1 -1
  15. package/dist/Calendar.d.ts +51 -7
  16. package/dist/Calendar.d.ts.map +1 -1
  17. package/dist/Checkbox.d.ts +33 -8
  18. package/dist/Checkbox.d.ts.map +1 -1
  19. package/dist/Collection.d.ts +130 -0
  20. package/dist/Collection.d.ts.map +1 -0
  21. package/dist/Color.d.ts +210 -9
  22. package/dist/Color.d.ts.map +1 -1
  23. package/dist/ColorEditor.d.ts +42 -0
  24. package/dist/ColorEditor.d.ts.map +1 -0
  25. package/dist/ComboBox.d.ts +146 -16
  26. package/dist/ComboBox.d.ts.map +1 -1
  27. package/dist/ContextualHelpTrigger.d.ts +40 -0
  28. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  29. package/dist/DateField.d.ts +35 -8
  30. package/dist/DateField.d.ts.map +1 -1
  31. package/dist/DatePicker.d.ts +101 -5
  32. package/dist/DatePicker.d.ts.map +1 -1
  33. package/dist/DateRangePickerContext.d.ts +30 -0
  34. package/dist/DateRangePickerContext.d.ts.map +1 -0
  35. package/dist/Dialog.d.ts +5 -5
  36. package/dist/Dialog.d.ts.map +1 -1
  37. package/dist/Disclosure.d.ts +25 -5
  38. package/dist/Disclosure.d.ts.map +1 -1
  39. package/dist/DragAndDrop.d.ts +80 -0
  40. package/dist/DragAndDrop.d.ts.map +1 -0
  41. package/dist/DragPreview.d.ts +14 -0
  42. package/dist/DragPreview.d.ts.map +1 -0
  43. package/dist/DropZone.d.ts +27 -0
  44. package/dist/DropZone.d.ts.map +1 -0
  45. package/dist/FieldError.d.ts +27 -0
  46. package/dist/FieldError.d.ts.map +1 -0
  47. package/dist/FileTrigger.d.ts +26 -0
  48. package/dist/FileTrigger.d.ts.map +1 -0
  49. package/dist/Focusable.d.ts +27 -0
  50. package/dist/Focusable.d.ts.map +1 -0
  51. package/dist/Form.d.ts +41 -0
  52. package/dist/Form.d.ts.map +1 -0
  53. package/dist/GridList.d.ts +69 -10
  54. package/dist/GridList.d.ts.map +1 -1
  55. package/dist/HiddenDateInput.d.ts +26 -0
  56. package/dist/HiddenDateInput.d.ts.map +1 -0
  57. package/dist/HiddenTimeInput.d.ts +25 -0
  58. package/dist/HiddenTimeInput.d.ts.map +1 -0
  59. package/dist/Icon.d.ts +57 -0
  60. package/dist/Icon.d.ts.map +1 -0
  61. package/dist/Keyboard.d.ts +13 -0
  62. package/dist/Keyboard.d.ts.map +1 -0
  63. package/dist/Landmark.d.ts +3 -3
  64. package/dist/Landmark.d.ts.map +1 -1
  65. package/dist/Link.d.ts +10 -4
  66. package/dist/Link.d.ts.map +1 -1
  67. package/dist/ListBox.d.ts +73 -11
  68. package/dist/ListBox.d.ts.map +1 -1
  69. package/dist/ListDropTargetDelegate.d.ts +38 -0
  70. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  71. package/dist/Menu.d.ts +79 -10
  72. package/dist/Menu.d.ts.map +1 -1
  73. package/dist/Meter.d.ts +4 -4
  74. package/dist/Meter.d.ts.map +1 -1
  75. package/dist/Modal.d.ts +6 -4
  76. package/dist/Modal.d.ts.map +1 -1
  77. package/dist/NumberField.d.ts +10 -12
  78. package/dist/NumberField.d.ts.map +1 -1
  79. package/dist/Popover.d.ts +32 -7
  80. package/dist/Popover.d.ts.map +1 -1
  81. package/dist/Pressable.d.ts +27 -0
  82. package/dist/Pressable.d.ts.map +1 -0
  83. package/dist/ProgressBar.d.ts +6 -4
  84. package/dist/ProgressBar.d.ts.map +1 -1
  85. package/dist/RadioGroup.d.ts +43 -9
  86. package/dist/RadioGroup.d.ts.map +1 -1
  87. package/dist/RangeCalendar.d.ts +39 -7
  88. package/dist/RangeCalendar.d.ts.map +1 -1
  89. package/dist/RouterProvider.d.ts +75 -0
  90. package/dist/RouterProvider.d.ts.map +1 -0
  91. package/dist/SearchField.d.ts +23 -21
  92. package/dist/SearchField.d.ts.map +1 -1
  93. package/dist/Select.d.ts +48 -7
  94. package/dist/Select.d.ts.map +1 -1
  95. package/dist/SelectionIndicator.d.ts +30 -0
  96. package/dist/SelectionIndicator.d.ts.map +1 -0
  97. package/dist/Separator.d.ts +9 -3
  98. package/dist/Separator.d.ts.map +1 -1
  99. package/dist/SharedElementTransition.d.ts +41 -0
  100. package/dist/SharedElementTransition.d.ts.map +1 -0
  101. package/dist/Slider.d.ts +15 -8
  102. package/dist/Slider.d.ts.map +1 -1
  103. package/dist/StepList.d.ts +90 -0
  104. package/dist/StepList.d.ts.map +1 -0
  105. package/dist/Switch.d.ts +11 -5
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Table.d.ts +222 -19
  108. package/dist/Table.d.ts.map +1 -1
  109. package/dist/Tabs.d.ts +47 -10
  110. package/dist/Tabs.d.ts.map +1 -1
  111. package/dist/TagGroup.d.ts +22 -10
  112. package/dist/TagGroup.d.ts.map +1 -1
  113. package/dist/Text.d.ts +10 -0
  114. package/dist/Text.d.ts.map +1 -0
  115. package/dist/TextField.d.ts +19 -11
  116. package/dist/TextField.d.ts.map +1 -1
  117. package/dist/TimeField.d.ts +32 -7
  118. package/dist/TimeField.d.ts.map +1 -1
  119. package/dist/Toast.d.ts +29 -14
  120. package/dist/Toast.d.ts.map +1 -1
  121. package/dist/ToggleButton.d.ts +36 -0
  122. package/dist/ToggleButton.d.ts.map +1 -0
  123. package/dist/ToggleButtonGroup.d.ts +33 -0
  124. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  125. package/dist/Toolbar.d.ts +7 -3
  126. package/dist/Toolbar.d.ts.map +1 -1
  127. package/dist/Tooltip.d.ts +58 -7
  128. package/dist/Tooltip.d.ts.map +1 -1
  129. package/dist/Tree.d.ts +102 -11
  130. package/dist/Tree.d.ts.map +1 -1
  131. package/dist/Virtualizer.d.ts +61 -0
  132. package/dist/Virtualizer.d.ts.map +1 -0
  133. package/dist/VirtualizerLayouts.d.ts +82 -0
  134. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  135. package/dist/VisuallyHidden.d.ts +4 -2
  136. package/dist/VisuallyHidden.d.ts.map +1 -1
  137. package/dist/contexts.d.ts +6 -1
  138. package/dist/contexts.d.ts.map +1 -1
  139. package/dist/index.d.ts +73 -39
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +23342 -10644
  142. package/dist/index.js.map +1 -7
  143. package/dist/index.jsx +18110 -0
  144. package/dist/index.jsx.map +1 -0
  145. package/dist/useDragAndDrop.d.ts +93 -0
  146. package/dist/useDragAndDrop.d.ts.map +1 -0
  147. package/dist/utils.d.ts +8 -2
  148. package/dist/utils.d.ts.map +1 -1
  149. package/dist/virtualizer/Layout.d.ts +79 -0
  150. package/dist/virtualizer/Layout.d.ts.map +1 -0
  151. package/package.json +33 -32
  152. package/src/ActionBar.tsx +251 -0
  153. package/src/ActionGroup.tsx +277 -0
  154. package/src/Alert.tsx +152 -0
  155. package/src/Autocomplete.tsx +39 -44
  156. package/src/Breadcrumbs.tsx +227 -72
  157. package/src/Button.tsx +315 -74
  158. package/src/Calendar.tsx +347 -141
  159. package/src/Checkbox.tsx +414 -123
  160. package/src/Collection.tsx +350 -0
  161. package/src/Color.tsx +1325 -284
  162. package/src/ColorEditor.tsx +213 -0
  163. package/src/ComboBox.tsx +644 -245
  164. package/src/ContextualHelpTrigger.tsx +195 -0
  165. package/src/DateField.tsx +274 -106
  166. package/src/DatePicker.tsx +892 -111
  167. package/src/DateRangePickerContext.tsx +44 -0
  168. package/src/Dialog.tsx +173 -104
  169. package/src/Disclosure.tsx +158 -105
  170. package/src/DragAndDrop.tsx +340 -0
  171. package/src/DragPreview.tsx +47 -0
  172. package/src/DropZone.tsx +233 -0
  173. package/src/FieldError.tsx +89 -0
  174. package/src/FileTrigger.tsx +83 -0
  175. package/src/Focusable.tsx +103 -0
  176. package/src/Form.tsx +140 -0
  177. package/src/GridList.tsx +542 -128
  178. package/src/HiddenDateInput.tsx +153 -0
  179. package/src/HiddenTimeInput.tsx +133 -0
  180. package/src/Icon.tsx +133 -0
  181. package/src/Keyboard.tsx +26 -0
  182. package/src/Landmark.tsx +37 -63
  183. package/src/Link.tsx +132 -69
  184. package/src/ListBox.tsx +656 -106
  185. package/src/ListDropTargetDelegate.ts +283 -0
  186. package/src/Menu.tsx +1234 -132
  187. package/src/Meter.tsx +44 -58
  188. package/src/Modal.tsx +262 -166
  189. package/src/NumberField.tsx +267 -151
  190. package/src/Popover.tsx +452 -343
  191. package/src/Pressable.tsx +108 -0
  192. package/src/ProgressBar.tsx +54 -59
  193. package/src/RadioGroup.tsx +533 -121
  194. package/src/RangeCalendar.tsx +249 -150
  195. package/src/RouterProvider.tsx +223 -0
  196. package/src/SearchField.tsx +460 -133
  197. package/src/Select.tsx +804 -233
  198. package/src/SelectionIndicator.tsx +108 -0
  199. package/src/Separator.tsx +47 -49
  200. package/src/SharedElementTransition.tsx +264 -0
  201. package/src/Slider.tsx +148 -98
  202. package/src/StepList.tsx +272 -0
  203. package/src/Switch.tsx +93 -46
  204. package/src/Table.tsx +1551 -225
  205. package/src/Tabs.tsx +377 -123
  206. package/src/TagGroup.tsx +233 -135
  207. package/src/Text.tsx +18 -0
  208. package/src/TextField.tsx +413 -86
  209. package/src/TimeField.tsx +232 -222
  210. package/src/Toast.tsx +306 -160
  211. package/src/ToggleButton.tsx +169 -0
  212. package/src/ToggleButtonGroup.tsx +141 -0
  213. package/src/Toolbar.tsx +61 -70
  214. package/src/Tooltip.tsx +473 -116
  215. package/src/Tree.tsx +1514 -175
  216. package/src/Virtualizer.tsx +730 -0
  217. package/src/VirtualizerLayouts.ts +280 -0
  218. package/src/VisuallyHidden.tsx +32 -38
  219. package/src/contexts.ts +29 -36
  220. package/src/index.ts +972 -620
  221. package/src/useDragAndDrop.ts +367 -0
  222. package/src/utils.tsx +69 -50
  223. package/src/virtualizer/Layout.ts +192 -0
  224. package/dist/index.ssr.js +0 -9785
  225. package/dist/index.ssr.js.map +0 -7
package/src/Color.tsx CHANGED
@@ -8,25 +8,33 @@
8
8
  import {
9
9
  type JSX,
10
10
  createContext,
11
+ createEffect,
11
12
  createMemo,
13
+ createSignal,
14
+ onCleanup,
12
15
  splitProps,
16
+ untrack,
13
17
  useContext,
14
18
  Show,
15
- } from 'solid-js';
19
+ } from "solid-js";
16
20
  import {
17
21
  createColorSlider,
18
22
  createColorArea,
19
23
  createColorWheel,
20
24
  createColorField,
21
25
  createColorSwatch,
26
+ createListBox,
27
+ createOption,
22
28
  createFocusRing,
23
29
  createHover,
30
+ mergeProps,
24
31
  type AriaColorSliderOptions,
25
32
  type AriaColorAreaOptions,
26
33
  type AriaColorWheelOptions,
27
34
  type AriaColorFieldOptions,
28
- } from '@proyecto-viviana/solidaria';
35
+ } from "@proyecto-viviana/solidaria";
29
36
  import {
37
+ createListState,
30
38
  createColorSliderState,
31
39
  createColorAreaState,
32
40
  createColorWheelState,
@@ -35,11 +43,14 @@ import {
35
43
  type Color,
36
44
  type ColorChannel,
37
45
  type ColorFormat,
46
+ type ColorSpace,
38
47
  type ColorSliderState,
39
48
  type ColorAreaState,
40
49
  type ColorWheelState,
41
50
  type ColorFieldState,
42
- } from '@proyecto-viviana/solid-stately';
51
+ type ListState,
52
+ type Key,
53
+ } from "@proyecto-viviana/solid-stately";
43
54
  import {
44
55
  type RenderChildren,
45
56
  type ClassNameOrFunction,
@@ -47,11 +58,35 @@ import {
47
58
  type SlotProps,
48
59
  useRenderProps,
49
60
  filterDOMProps,
50
- } from './utils';
61
+ } from "./utils";
51
62
 
52
- // ============================================
53
- // COLOR SLIDER
54
- // ============================================
63
+ interface ColorPickerChannelContextValue {
64
+ value?: Color | string;
65
+ onChange?: (color: Color) => void;
66
+ }
67
+
68
+ interface ColorPickerStateContextValue {
69
+ color: () => Color;
70
+ setColor: (color: Color) => void;
71
+ }
72
+
73
+ interface ColorSwatchPickerItemData {
74
+ key: string;
75
+ color: Color;
76
+ textValue: string;
77
+ isDisabled?: boolean;
78
+ }
79
+
80
+ interface ColorSwatchPickerContextValue {
81
+ state: ListState<ColorSwatchPickerItemData>;
82
+ registerItem: (item: ColorSwatchPickerItemData) => void;
83
+ unregisterItem: (key: string) => void;
84
+ }
85
+
86
+ const ColorPickerContextInternal = createContext<ColorPickerChannelContextValue | null>(null);
87
+ const ColorPickerStateContextInternal = createContext<ColorPickerStateContextValue | null>(null);
88
+ const ColorSwatchContextInternal = createContext<{ color?: Color | string } | null>(null);
89
+ const ColorSwatchPickerContextInternal = createContext<ColorSwatchPickerContextValue | null>(null);
55
90
 
56
91
  export interface ColorSliderRenderProps {
57
92
  /** Whether the slider is disabled. */
@@ -60,13 +95,19 @@ export interface ColorSliderRenderProps {
60
95
  isDragging: boolean;
61
96
  /** The color channel being controlled. */
62
97
  channel: ColorChannel;
98
+ /** The slider orientation. */
99
+ orientation: "horizontal" | "vertical";
63
100
  /** The current value. */
64
101
  value: number;
102
+ /** The formatted current value. */
103
+ valueLabel: string;
65
104
  /** The current color. */
66
105
  color: Color;
106
+ /** The default inline styles applied by the color slider hook. */
107
+ defaultStyle: JSX.CSSProperties;
67
108
  }
68
109
 
69
- export interface ColorSliderProps extends Omit<AriaColorSliderOptions, 'channel'>, SlotProps {
110
+ export interface ColorSliderProps extends Omit<AriaColorSliderOptions, "channel">, SlotProps {
70
111
  /** The current color value (controlled). */
71
112
  value?: Color | string;
72
113
  /** The default color value (uncontrolled). */
@@ -75,6 +116,8 @@ export interface ColorSliderProps extends Omit<AriaColorSliderOptions, 'channel'
75
116
  onChange?: (color: Color) => void;
76
117
  /** Handler called when dragging ends. */
77
118
  onChangeEnd?: (color: Color) => void;
119
+ /** Color space used for channel values. */
120
+ colorSpace?: ColorSpace;
78
121
  /** The color channel to control. */
79
122
  channel: ColorChannel;
80
123
  /** A visible label for the slider. */
@@ -92,6 +135,10 @@ export interface ColorSliderTrackRenderProps {
92
135
  isDisabled: boolean;
93
136
  /** Whether the slider is being dragged. */
94
137
  isDragging: boolean;
138
+ /** The slider orientation. */
139
+ orientation: "horizontal" | "vertical";
140
+ /** The default inline styles applied by the color slider hook. */
141
+ defaultStyle: JSX.CSSProperties;
95
142
  }
96
143
 
97
144
  export interface ColorSliderTrackProps extends SlotProps {
@@ -103,6 +150,24 @@ export interface ColorSliderTrackProps extends SlotProps {
103
150
  style?: StyleOrFunction<ColorSliderTrackRenderProps>;
104
151
  }
105
152
 
153
+ export interface ColorSliderLabelProps extends SlotProps {
154
+ /** The label contents. */
155
+ children?: JSX.Element;
156
+ /** The CSS className for the element. */
157
+ class?: string;
158
+ /** The inline style for the element. */
159
+ style?: JSX.CSSProperties;
160
+ }
161
+
162
+ export interface ColorSliderOutputProps extends SlotProps {
163
+ /** The output contents. Defaults to the formatted slider value. */
164
+ children?: JSX.Element | ((renderProps: ColorSliderRenderProps) => JSX.Element);
165
+ /** The CSS className for the element. */
166
+ class?: string;
167
+ /** The inline style for the element. */
168
+ style?: JSX.CSSProperties;
169
+ }
170
+
106
171
  export interface ColorSliderThumbRenderProps {
107
172
  /** Whether the slider is disabled. */
108
173
  isDisabled: boolean;
@@ -114,6 +179,10 @@ export interface ColorSliderThumbRenderProps {
114
179
  isFocusVisible: boolean;
115
180
  /** Whether the thumb is hovered. */
116
181
  isHovered: boolean;
182
+ /** The current display color. */
183
+ color: Color;
184
+ /** The default inline styles applied by the color slider hook. */
185
+ defaultStyle: JSX.CSSProperties;
117
186
  }
118
187
 
119
188
  export interface ColorSliderThumbProps extends SlotProps {
@@ -123,16 +192,20 @@ export interface ColorSliderThumbProps extends SlotProps {
123
192
  class?: ClassNameOrFunction<ColorSliderThumbRenderProps>;
124
193
  /** The inline style for the element. */
125
194
  style?: StyleOrFunction<ColorSliderThumbRenderProps>;
195
+ /** Ref callback for the thumb element. */
196
+ ref?: (element: HTMLDivElement) => void;
126
197
  }
127
198
 
128
- // Context
129
199
  interface ColorSliderContextValue {
130
200
  state: ColorSliderState;
131
201
  trackProps: JSX.HTMLAttributes<HTMLDivElement>;
132
202
  thumbProps: JSX.HTMLAttributes<HTMLDivElement>;
133
203
  inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
204
+ outputProps: JSX.HTMLAttributes<HTMLOutputElement>;
205
+ labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
134
206
  trackRef: HTMLDivElement | undefined;
135
207
  setTrackRef: (el: HTMLDivElement) => void;
208
+ setInputRef: (el: HTMLInputElement) => void;
136
209
  }
137
210
 
138
211
  export const ColorSliderContext = createContext<ColorSliderContextValue | null>(null);
@@ -141,80 +214,116 @@ export const ColorSliderContext = createContext<ColorSliderContextValue | null>(
141
214
  * A color slider allows users to adjust a single color channel.
142
215
  */
143
216
  export function ColorSlider(props: ColorSliderProps): JSX.Element {
217
+ const pickerContext = useContext(ColorPickerContextInternal);
144
218
  const [local, stateProps, ariaProps, rest] = splitProps(
145
219
  props,
146
- ['children', 'class', 'style', 'slot', 'label'],
147
- ['value', 'defaultValue', 'onChange', 'onChangeEnd', 'channel'],
148
- ['aria-label', 'aria-labelledby', 'aria-describedby', 'isDisabled', 'channelName']
220
+ ["children", "class", "style", "slot", "label"],
221
+ ["value", "defaultValue", "onChange", "onChangeEnd", "channel", "colorSpace"],
222
+ [
223
+ "id",
224
+ "aria-label",
225
+ "aria-labelledby",
226
+ "aria-describedby",
227
+ "aria-details",
228
+ "isDisabled",
229
+ "name",
230
+ "form",
231
+ "orientation",
232
+ "channelName",
233
+ ],
149
234
  );
150
235
 
151
236
  // Create color slider state
152
237
  const state = createColorSliderState(() => ({
153
- value: stateProps.value,
238
+ value: stateProps.value ?? pickerContext?.value,
154
239
  defaultValue: stateProps.defaultValue,
155
- onChange: stateProps.onChange,
240
+ onChange: stateProps.onChange ?? pickerContext?.onChange,
156
241
  onChangeEnd: stateProps.onChangeEnd,
157
242
  channel: stateProps.channel,
243
+ colorSpace: stateProps.colorSpace,
244
+ orientation: ariaProps.orientation,
158
245
  isDisabled: ariaProps.isDisabled,
159
246
  }));
160
247
 
161
- // Track ref
162
248
  let trackRef: HTMLDivElement | undefined;
163
249
  const setTrackRef = (el: HTMLDivElement) => {
164
250
  trackRef = el;
165
251
  };
252
+ let inputRef: HTMLInputElement | undefined;
253
+ const setInputRef = (el: HTMLInputElement) => {
254
+ inputRef = el;
255
+ };
166
256
 
167
257
  // Create color slider aria props
168
- const {
169
- trackProps,
170
- thumbProps,
171
- inputProps,
172
- labelProps,
173
- } = createColorSlider(
258
+ const colorSliderAria = createColorSlider(
174
259
  () => ({
260
+ id: ariaProps.id,
175
261
  channel: stateProps.channel,
176
- 'aria-label': ariaProps['aria-label'],
177
- 'aria-labelledby': ariaProps['aria-labelledby'],
178
- 'aria-describedby': ariaProps['aria-describedby'],
262
+ label: local.label,
263
+ "aria-label": ariaProps["aria-label"],
264
+ "aria-labelledby": ariaProps["aria-labelledby"],
265
+ "aria-describedby": ariaProps["aria-describedby"],
266
+ "aria-details": ariaProps["aria-details"],
179
267
  isDisabled: ariaProps.isDisabled,
268
+ name: ariaProps.name,
269
+ form: ariaProps.form,
270
+ orientation: ariaProps.orientation,
180
271
  channelName: ariaProps.channelName,
181
272
  }),
182
273
  () => state,
183
- () => trackRef ?? null
274
+ () => trackRef ?? null,
275
+ () => inputRef ?? null,
184
276
  );
185
277
 
186
- // Render props values
187
278
  const renderValues = createMemo<ColorSliderRenderProps>(() => ({
188
279
  isDisabled: state.isDisabled,
189
280
  isDragging: state.isDragging,
190
281
  channel: state.channel,
282
+ orientation: state.orientation,
191
283
  value: state.getThumbValue(),
284
+ valueLabel: state.getThumbValueLabel(),
192
285
  color: state.value,
286
+ defaultStyle: (colorSliderAria.trackProps as { style?: JSX.CSSProperties }).style ?? {},
193
287
  }));
194
288
 
195
- // Resolve render props
196
289
  const renderProps = useRenderProps(
197
290
  {
198
291
  children: props.children,
199
292
  class: local.class,
200
293
  style: local.style,
201
- defaultClassName: 'solidaria-ColorSlider',
294
+ defaultClassName: "solidaria-ColorSlider",
202
295
  },
203
- renderValues
296
+ renderValues,
204
297
  );
205
298
 
206
- // Filter DOM props
207
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
299
+ const domProps = createMemo(() =>
300
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
301
+ );
208
302
 
209
303
  return (
210
304
  <ColorSliderContext.Provider
211
305
  value={{
212
306
  state,
213
- trackProps,
214
- thumbProps,
215
- inputProps,
216
- trackRef,
307
+ get trackProps() {
308
+ return colorSliderAria.trackProps;
309
+ },
310
+ get thumbProps() {
311
+ return colorSliderAria.thumbProps;
312
+ },
313
+ get inputProps() {
314
+ return colorSliderAria.inputProps;
315
+ },
316
+ get outputProps() {
317
+ return colorSliderAria.outputProps;
318
+ },
319
+ get labelProps() {
320
+ return colorSliderAria.labelProps;
321
+ },
322
+ get trackRef() {
323
+ return trackRef;
324
+ },
217
325
  setTrackRef,
326
+ setInputRef,
218
327
  }}
219
328
  >
220
329
  <div
@@ -223,73 +332,129 @@ export function ColorSlider(props: ColorSliderProps): JSX.Element {
223
332
  style={renderProps.style()}
224
333
  data-disabled={state.isDisabled || undefined}
225
334
  data-dragging={state.isDragging || undefined}
226
- data-channel={state.channel}
335
+ data-orientation={state.orientation}
336
+ slot={local.slot || undefined}
227
337
  >
228
- {/* Label */}
229
- <Show when={local.label}>
230
- <label {...labelProps}>{local.label}</label>
231
- </Show>
232
-
233
338
  {renderProps.renderChildren()}
234
-
235
- {/* Hidden input for accessibility */}
236
- <input {...inputProps} />
237
339
  </div>
238
340
  </ColorSliderContext.Provider>
239
341
  );
240
342
  }
241
343
 
344
+ /**
345
+ * The label element of a color slider.
346
+ */
347
+ export function ColorSliderLabel(props: ColorSliderLabelProps): JSX.Element {
348
+ const [local, domProps] = splitProps(props, ["class", "slot", "children"]);
349
+
350
+ const context = useContext(ColorSliderContext);
351
+ if (!context) {
352
+ throw new Error("ColorSliderLabel must be used within a ColorSlider");
353
+ }
354
+
355
+ const labelProps = () => {
356
+ const { ref: _ref, ...rest } = context.labelProps as Record<string, unknown>;
357
+ return rest;
358
+ };
359
+
360
+ return (
361
+ <label {...domProps} {...labelProps()} class={local.class}>
362
+ {local.children}
363
+ </label>
364
+ );
365
+ }
366
+
367
+ /**
368
+ * The output element of a color slider.
369
+ */
370
+ export function ColorSliderOutput(props: ColorSliderOutputProps): JSX.Element {
371
+ const [local, domProps] = splitProps(props, ["class", "slot", "children"]);
372
+
373
+ const context = useContext(ColorSliderContext);
374
+ if (!context) {
375
+ throw new Error("ColorSliderOutput must be used within a ColorSlider");
376
+ }
377
+
378
+ const state = context.state;
379
+
380
+ const renderValues = createMemo<ColorSliderRenderProps>(() => ({
381
+ isDisabled: state.isDisabled,
382
+ isDragging: state.isDragging,
383
+ channel: state.channel,
384
+ orientation: state.orientation,
385
+ value: state.getThumbValue(),
386
+ valueLabel: state.getThumbValueLabel(),
387
+ color: state.value,
388
+ defaultStyle: (context.trackProps as { style?: JSX.CSSProperties }).style ?? {},
389
+ }));
390
+
391
+ const children = () =>
392
+ typeof local.children === "function"
393
+ ? local.children(renderValues())
394
+ : (local.children ?? renderValues().valueLabel);
395
+
396
+ return (
397
+ <output {...domProps} {...context.outputProps} class={local.class}>
398
+ {children()}
399
+ </output>
400
+ );
401
+ }
402
+
242
403
  /**
243
404
  * The track element of a color slider.
244
405
  */
245
406
  export function ColorSliderTrack(props: ColorSliderTrackProps): JSX.Element {
246
- const [local] = splitProps(props, ['class', 'style', 'slot']);
407
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
247
408
 
248
409
  const context = useContext(ColorSliderContext);
249
410
  if (!context) {
250
- throw new Error('ColorSliderTrack must be used within a ColorSlider');
411
+ throw new Error("ColorSliderTrack must be used within a ColorSlider");
251
412
  }
252
413
 
253
- const { state, trackProps, setTrackRef } = context;
414
+ const state = context.state;
254
415
 
255
- // Render props values
256
416
  const renderValues = createMemo<ColorSliderTrackRenderProps>(() => ({
257
417
  isDisabled: state.isDisabled,
258
418
  isDragging: state.isDragging,
419
+ orientation: state.orientation,
420
+ defaultStyle: (context.trackProps as { style?: JSX.CSSProperties }).style ?? {},
259
421
  }));
260
422
 
261
- // Resolve render props
262
423
  const renderProps = useRenderProps(
263
424
  {
264
425
  children: props.children,
265
426
  class: local.class,
266
427
  style: local.style,
267
- defaultClassName: 'solidaria-ColorSlider-track',
428
+ defaultClassName: "solidaria-ColorSlider-track",
268
429
  },
269
- renderValues
430
+ renderValues,
270
431
  );
271
432
 
272
- // Clean props
273
433
  const cleanTrackProps = () => {
274
- const { ref: _ref, style: _trackStyle, ...rest } = trackProps as Record<string, unknown>;
434
+ const {
435
+ ref: _ref,
436
+ style: _trackStyle,
437
+ ...rest
438
+ } = context.trackProps as Record<string, unknown>;
275
439
  return rest;
276
440
  };
277
441
 
278
- // Merge styles
279
442
  const mergedStyle = () => {
280
- const trackStyle = (trackProps as { style?: Record<string, string> }).style || {};
443
+ const trackStyle = (context.trackProps as { style?: Record<string, string> }).style || {};
281
444
  const renderStyle = renderProps.style() || {};
282
445
  return { ...trackStyle, ...renderStyle };
283
446
  };
284
447
 
285
448
  return (
286
449
  <div
287
- ref={setTrackRef}
450
+ {...domProps}
451
+ ref={context.setTrackRef}
288
452
  {...cleanTrackProps()}
289
453
  class={renderProps.class()}
290
454
  style={mergedStyle()}
291
455
  data-disabled={state.isDisabled || undefined}
292
456
  data-dragging={state.isDragging || undefined}
457
+ data-orientation={state.orientation}
293
458
  >
294
459
  {renderProps.renderChildren()}
295
460
  </div>
@@ -300,48 +465,49 @@ export function ColorSliderTrack(props: ColorSliderTrackProps): JSX.Element {
300
465
  * The thumb element of a color slider.
301
466
  */
302
467
  export function ColorSliderThumb(props: ColorSliderThumbProps): JSX.Element {
303
- const [local] = splitProps(props, ['class', 'style', 'slot']);
468
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children", "ref"]);
304
469
 
305
470
  const context = useContext(ColorSliderContext);
306
471
  if (!context) {
307
- throw new Error('ColorSliderThumb must be used within a ColorSlider');
472
+ throw new Error("ColorSliderThumb must be used within a ColorSlider");
308
473
  }
309
474
 
310
- const { state, thumbProps } = context;
475
+ const state = context.state;
311
476
 
312
- // Create focus ring
313
477
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
314
478
 
315
- // Create hover
316
479
  const { isHovered, hoverProps } = createHover({
317
480
  get isDisabled() {
318
481
  return state.isDisabled;
319
482
  },
320
483
  });
321
484
 
322
- // Render props values
323
485
  const renderValues = createMemo<ColorSliderThumbRenderProps>(() => ({
324
486
  isDisabled: state.isDisabled,
325
487
  isDragging: state.isDragging,
326
488
  isFocused: isFocused(),
327
489
  isFocusVisible: isFocusVisible(),
328
490
  isHovered: isHovered(),
491
+ color: state.getDisplayColor(),
492
+ defaultStyle: (context.thumbProps as { style?: JSX.CSSProperties }).style ?? {},
329
493
  }));
330
494
 
331
- // Resolve render props
332
495
  const renderProps = useRenderProps(
333
496
  {
334
497
  children: props.children,
335
498
  class: local.class,
336
499
  style: local.style,
337
- defaultClassName: 'solidaria-ColorSlider-thumb',
500
+ defaultClassName: "solidaria-ColorSlider-thumb",
338
501
  },
339
- renderValues
502
+ renderValues,
340
503
  );
341
504
 
342
- // Clean props
343
505
  const cleanThumbProps = () => {
344
- const { ref: _ref, style: _thumbStyle, ...rest } = thumbProps as Record<string, unknown>;
506
+ const {
507
+ ref: _ref,
508
+ style: _thumbStyle,
509
+ ...rest
510
+ } = context.thumbProps as Record<string, unknown>;
345
511
  return rest;
346
512
  };
347
513
  const cleanFocusProps = () => {
@@ -352,19 +518,25 @@ export function ColorSliderThumb(props: ColorSliderThumbProps): JSX.Element {
352
518
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
353
519
  return rest;
354
520
  };
521
+ const mergedInputProps = () => {
522
+ return mergeProps(
523
+ context.inputProps as Record<string, unknown>,
524
+ cleanFocusProps(),
525
+ ) as JSX.InputHTMLAttributes<HTMLInputElement>;
526
+ };
355
527
 
356
- // Merge styles
357
528
  const mergedStyle = () => {
358
- const thumbStyle = (thumbProps as { style?: Record<string, string> }).style || {};
529
+ const thumbStyle = (context.thumbProps as { style?: Record<string, string> }).style || {};
359
530
  const renderStyle = renderProps.style() || {};
360
531
  return { ...thumbStyle, ...renderStyle };
361
532
  };
362
533
 
363
534
  return (
364
535
  <div
536
+ {...domProps}
365
537
  {...cleanThumbProps()}
366
- {...cleanFocusProps()}
367
538
  {...cleanHoverProps()}
539
+ ref={local.ref}
368
540
  class={renderProps.class()}
369
541
  style={mergedStyle()}
370
542
  data-disabled={state.isDisabled || undefined}
@@ -373,18 +545,16 @@ export function ColorSliderThumb(props: ColorSliderThumbProps): JSX.Element {
373
545
  data-focus-visible={isFocusVisible() || undefined}
374
546
  data-hovered={isHovered() || undefined}
375
547
  >
548
+ <input ref={context.setInputRef} {...mergedInputProps()} />
376
549
  {renderProps.renderChildren()}
377
550
  </div>
378
551
  );
379
552
  }
380
553
 
381
- // Attach sub-components
382
554
  ColorSlider.Track = ColorSliderTrack;
383
555
  ColorSlider.Thumb = ColorSliderThumb;
384
-
385
- // ============================================
386
- // COLOR AREA
387
- // ============================================
556
+ ColorSlider.Label = ColorSliderLabel;
557
+ ColorSlider.Output = ColorSliderOutput;
388
558
 
389
559
  export interface ColorAreaRenderProps {
390
560
  /** Whether the area is disabled. */
@@ -397,6 +567,8 @@ export interface ColorAreaRenderProps {
397
567
  yChannel: ColorChannel;
398
568
  /** The current color. */
399
569
  color: Color;
570
+ /** The default inline styles applied by the color area hook. */
571
+ defaultStyle: JSX.CSSProperties;
400
572
  }
401
573
 
402
574
  export interface ColorAreaProps extends AriaColorAreaOptions, SlotProps {
@@ -439,6 +611,8 @@ export interface ColorAreaThumbRenderProps {
439
611
  isDisabled: boolean;
440
612
  /** Whether the thumb is being dragged. */
441
613
  isDragging: boolean;
614
+ /** The current display color. */
615
+ color: Color;
442
616
  /** Whether the thumb is focused. */
443
617
  isFocused: boolean;
444
618
  /** Whether the thumb has keyboard focus. */
@@ -454,9 +628,10 @@ export interface ColorAreaThumbProps extends SlotProps {
454
628
  class?: ClassNameOrFunction<ColorAreaThumbRenderProps>;
455
629
  /** The inline style for the element. */
456
630
  style?: StyleOrFunction<ColorAreaThumbRenderProps>;
631
+ /** Ref callback for the thumb element. */
632
+ ref?: (element: HTMLDivElement) => void;
457
633
  }
458
634
 
459
- // Context
460
635
  interface ColorAreaContextValue {
461
636
  state: ColorAreaState;
462
637
  colorAreaProps: JSX.HTMLAttributes<HTMLDivElement>;
@@ -474,21 +649,33 @@ export const ColorAreaContext = createContext<ColorAreaContextValue | null>(null
474
649
  * A color area allows users to select a color using a 2D gradient.
475
650
  */
476
651
  export function ColorArea(props: ColorAreaProps): JSX.Element {
652
+ const pickerContext = useContext(ColorPickerContextInternal);
477
653
  const [local, stateProps, ariaProps, rest] = splitProps(
478
654
  props,
479
- ['children', 'class', 'style', 'slot'],
480
- ['value', 'defaultValue', 'onChange', 'onChangeEnd', 'xChannel', 'yChannel'],
481
- ['aria-label', 'aria-labelledby', 'aria-describedby', 'isDisabled']
655
+ ["children", "class", "style", "slot"],
656
+ ["value", "defaultValue", "onChange", "onChangeEnd", "xChannel", "yChannel", "colorSpace"],
657
+ [
658
+ "id",
659
+ "aria-label",
660
+ "aria-labelledby",
661
+ "aria-describedby",
662
+ "aria-details",
663
+ "isDisabled",
664
+ "xName",
665
+ "yName",
666
+ "form",
667
+ ],
482
668
  );
483
669
 
484
670
  // Create color area state
485
671
  const state = createColorAreaState(() => ({
486
- value: stateProps.value,
672
+ value: stateProps.value ?? pickerContext?.value,
487
673
  defaultValue: stateProps.defaultValue,
488
- onChange: stateProps.onChange,
674
+ onChange: stateProps.onChange ?? pickerContext?.onChange,
489
675
  onChangeEnd: stateProps.onChangeEnd,
490
676
  xChannel: stateProps.xChannel,
491
677
  yChannel: stateProps.yChannel,
678
+ colorSpace: stateProps.colorSpace,
492
679
  isDisabled: ariaProps.isDisabled,
493
680
  }));
494
681
 
@@ -499,55 +686,83 @@ export function ColorArea(props: ColorAreaProps): JSX.Element {
499
686
  };
500
687
 
501
688
  // Create color area aria props
502
- const {
503
- colorAreaProps,
504
- gradientProps,
505
- thumbProps,
506
- xInputProps,
507
- yInputProps,
508
- } = createColorArea(
689
+ const colorAreaAria = createColorArea(
509
690
  () => ({
510
- 'aria-label': ariaProps['aria-label'],
511
- 'aria-labelledby': ariaProps['aria-labelledby'],
512
- 'aria-describedby': ariaProps['aria-describedby'],
691
+ id: ariaProps.id,
692
+ "aria-label": ariaProps["aria-label"],
693
+ "aria-labelledby": ariaProps["aria-labelledby"],
694
+ "aria-describedby": ariaProps["aria-describedby"],
695
+ "aria-details": ariaProps["aria-details"],
513
696
  isDisabled: ariaProps.isDisabled,
697
+ xName: ariaProps.xName,
698
+ yName: ariaProps.yName,
699
+ form: ariaProps.form,
514
700
  }),
515
701
  () => state,
516
- () => areaRef ?? null
702
+ () => areaRef ?? null,
517
703
  );
518
704
 
519
- // Render props values
520
705
  const renderValues = createMemo<ColorAreaRenderProps>(() => ({
521
706
  isDisabled: state.isDisabled,
522
707
  isDragging: state.isDragging,
523
708
  xChannel: state.xChannel,
524
709
  yChannel: state.yChannel,
525
710
  color: state.value,
711
+ defaultStyle: (colorAreaAria.colorAreaProps as { style?: JSX.CSSProperties }).style ?? {},
526
712
  }));
527
713
 
528
- // Resolve render props
714
+ const childRenderValues: ColorAreaRenderProps = {
715
+ get isDisabled() {
716
+ return state.isDisabled;
717
+ },
718
+ get isDragging() {
719
+ return state.isDragging;
720
+ },
721
+ get xChannel() {
722
+ return state.xChannel;
723
+ },
724
+ get yChannel() {
725
+ return state.yChannel;
726
+ },
727
+ get color() {
728
+ return state.value;
729
+ },
730
+ get defaultStyle() {
731
+ return (colorAreaAria.colorAreaProps as { style?: JSX.CSSProperties }).style ?? {};
732
+ },
733
+ };
734
+
735
+ const colorAreaChildren = () => {
736
+ const children = props.children;
737
+ return typeof children === "function" ? children(childRenderValues) : children;
738
+ };
739
+
529
740
  const renderProps = useRenderProps(
530
741
  {
531
742
  children: props.children,
532
743
  class: local.class,
533
744
  style: local.style,
534
- defaultClassName: 'solidaria-ColorArea',
745
+ defaultClassName: "solidaria-ColorArea",
535
746
  },
536
- renderValues
747
+ renderValues,
537
748
  );
538
749
 
539
- // Filter DOM props
540
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
750
+ const domProps = createMemo(() =>
751
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
752
+ );
541
753
 
542
- // Clean props
543
754
  const cleanColorAreaProps = () => {
544
- const { ref: _ref, style: _areaStyle, ...rest } = colorAreaProps as Record<string, unknown>;
755
+ const {
756
+ ref: _ref,
757
+ style: _areaStyle,
758
+ ...rest
759
+ } = colorAreaAria.colorAreaProps as Record<string, unknown>;
545
760
  return rest;
546
761
  };
547
762
 
548
- // Merge styles
549
763
  const mergedStyle = () => {
550
- const areaStyle = (colorAreaProps as { style?: Record<string, string> }).style || {};
764
+ const areaStyle =
765
+ (colorAreaAria.colorAreaProps as { style?: Record<string, string> }).style || {};
551
766
  const renderStyle = renderProps.style() || {};
552
767
  return { ...areaStyle, ...renderStyle };
553
768
  };
@@ -556,11 +771,21 @@ export function ColorArea(props: ColorAreaProps): JSX.Element {
556
771
  <ColorAreaContext.Provider
557
772
  value={{
558
773
  state,
559
- colorAreaProps,
560
- gradientProps,
561
- thumbProps,
562
- xInputProps,
563
- yInputProps,
774
+ get colorAreaProps() {
775
+ return colorAreaAria.colorAreaProps;
776
+ },
777
+ get gradientProps() {
778
+ return colorAreaAria.gradientProps;
779
+ },
780
+ get thumbProps() {
781
+ return colorAreaAria.thumbProps;
782
+ },
783
+ get xInputProps() {
784
+ return colorAreaAria.xInputProps;
785
+ },
786
+ get yInputProps() {
787
+ return colorAreaAria.yInputProps;
788
+ },
564
789
  areaRef,
565
790
  setAreaRef,
566
791
  }}
@@ -571,14 +796,11 @@ export function ColorArea(props: ColorAreaProps): JSX.Element {
571
796
  {...cleanColorAreaProps()}
572
797
  class={renderProps.class()}
573
798
  style={mergedStyle()}
799
+ slot={local.slot ?? undefined}
574
800
  data-disabled={state.isDisabled || undefined}
575
801
  data-dragging={state.isDragging || undefined}
576
802
  >
577
- {renderProps.renderChildren()}
578
-
579
- {/* Hidden inputs for accessibility */}
580
- <input {...xInputProps} />
581
- <input {...yInputProps} />
803
+ {colorAreaChildren()}
582
804
  </div>
583
805
  </ColorAreaContext.Provider>
584
806
  );
@@ -588,46 +810,47 @@ export function ColorArea(props: ColorAreaProps): JSX.Element {
588
810
  * The gradient background of a color area.
589
811
  */
590
812
  export function ColorAreaGradient(props: ColorAreaGradientProps): JSX.Element {
591
- const [local] = splitProps(props, ['class', 'style', 'slot']);
813
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
592
814
 
593
815
  const context = useContext(ColorAreaContext);
594
816
  if (!context) {
595
- throw new Error('ColorAreaGradient must be used within a ColorArea');
817
+ throw new Error("ColorAreaGradient must be used within a ColorArea");
596
818
  }
597
819
 
598
- const { state, gradientProps } = context;
820
+ const { state } = context;
599
821
 
600
- // Render props values
601
822
  const renderValues = createMemo<ColorAreaGradientRenderProps>(() => ({
602
823
  isDisabled: state.isDisabled,
603
824
  }));
604
825
 
605
- // Resolve render props
606
826
  const renderProps = useRenderProps(
607
827
  {
608
828
  children: props.children,
609
829
  class: local.class,
610
830
  style: local.style,
611
- defaultClassName: 'solidaria-ColorArea-gradient',
831
+ defaultClassName: "solidaria-ColorArea-gradient",
612
832
  },
613
- renderValues
833
+ renderValues,
614
834
  );
615
835
 
616
- // Clean props
617
836
  const cleanGradientProps = () => {
618
- const { ref: _ref, style: _gradStyle, ...rest } = gradientProps as Record<string, unknown>;
837
+ const {
838
+ ref: _ref,
839
+ style: _gradStyle,
840
+ ...rest
841
+ } = context.gradientProps as Record<string, unknown>;
619
842
  return rest;
620
843
  };
621
844
 
622
- // Merge styles
623
845
  const mergedStyle = () => {
624
- const gradStyle = (gradientProps as { style?: Record<string, string> }).style || {};
846
+ const gradStyle = (context.gradientProps as { style?: Record<string, string> }).style || {};
625
847
  const renderStyle = renderProps.style() || {};
626
848
  return { ...gradStyle, ...renderStyle };
627
849
  };
628
850
 
629
851
  return (
630
852
  <div
853
+ {...domProps}
631
854
  {...cleanGradientProps()}
632
855
  class={renderProps.class()}
633
856
  style={mergedStyle()}
@@ -642,48 +865,50 @@ export function ColorAreaGradient(props: ColorAreaGradientProps): JSX.Element {
642
865
  * The thumb element of a color area.
643
866
  */
644
867
  export function ColorAreaThumb(props: ColorAreaThumbProps): JSX.Element {
645
- const [local] = splitProps(props, ['class', 'style', 'slot']);
868
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children", "ref"]);
646
869
 
647
870
  const context = useContext(ColorAreaContext);
648
871
  if (!context) {
649
- throw new Error('ColorAreaThumb must be used within a ColorArea');
872
+ throw new Error("ColorAreaThumb must be used within a ColorArea");
650
873
  }
651
874
 
652
- const { state, thumbProps } = context;
875
+ const { state } = context;
653
876
 
654
- // Create focus ring
655
877
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
878
+ let xInputRef: HTMLInputElement | undefined;
879
+ let yInputRef: HTMLInputElement | undefined;
656
880
 
657
- // Create hover
658
881
  const { isHovered, hoverProps } = createHover({
659
882
  get isDisabled() {
660
883
  return state.isDisabled;
661
884
  },
662
885
  });
663
886
 
664
- // Render props values
665
887
  const renderValues = createMemo<ColorAreaThumbRenderProps>(() => ({
666
888
  isDisabled: state.isDisabled,
667
889
  isDragging: state.isDragging,
890
+ color: state.getDisplayColor(),
668
891
  isFocused: isFocused(),
669
892
  isFocusVisible: isFocusVisible(),
670
893
  isHovered: isHovered(),
671
894
  }));
672
895
 
673
- // Resolve render props
674
896
  const renderProps = useRenderProps(
675
897
  {
676
898
  children: props.children,
677
899
  class: local.class,
678
900
  style: local.style,
679
- defaultClassName: 'solidaria-ColorArea-thumb',
901
+ defaultClassName: "solidaria-ColorArea-thumb",
680
902
  },
681
- renderValues
903
+ renderValues,
682
904
  );
683
905
 
684
- // Clean props
685
906
  const cleanThumbProps = () => {
686
- const { ref: _ref, style: _thumbStyle, ...rest } = thumbProps as Record<string, unknown>;
907
+ const {
908
+ ref: _ref,
909
+ style: _thumbStyle,
910
+ ...rest
911
+ } = context.thumbProps as Record<string, unknown>;
687
912
  return rest;
688
913
  };
689
914
  const cleanFocusProps = () => {
@@ -694,18 +919,46 @@ export function ColorAreaThumb(props: ColorAreaThumbProps): JSX.Element {
694
919
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
695
920
  return rest;
696
921
  };
922
+ const mergedXInputProps = () => {
923
+ const { value: _value, ...inputProps } = context.xInputProps as Record<string, unknown>;
924
+ return mergeProps(inputProps, cleanFocusProps()) as JSX.InputHTMLAttributes<HTMLInputElement>;
925
+ };
926
+ const mergedYInputProps = () => {
927
+ const { value: _value, ...inputProps } = context.yInputProps as Record<string, unknown>;
928
+ return mergeProps(inputProps, cleanFocusProps()) as JSX.InputHTMLAttributes<HTMLInputElement>;
929
+ };
697
930
 
698
- // Merge styles
699
931
  const mergedStyle = () => {
700
- const thumbStyle = (thumbProps as { style?: Record<string, string> }).style || {};
932
+ const thumbStyle = (context.thumbProps as { style?: Record<string, string> }).style || {};
701
933
  const renderStyle = renderProps.style() || {};
702
934
  return { ...thumbStyle, ...renderStyle };
703
935
  };
704
936
 
937
+ const syncInputValue = (input: HTMLInputElement | undefined, value: number) => {
938
+ const nextValue = String(value);
939
+ const update = () => {
940
+ if (input && input.value !== nextValue) {
941
+ input.value = nextValue;
942
+ }
943
+ };
944
+
945
+ update();
946
+ queueMicrotask(update);
947
+ };
948
+
949
+ createEffect(() => {
950
+ syncInputValue(xInputRef, state.getXValue());
951
+ });
952
+
953
+ createEffect(() => {
954
+ syncInputValue(yInputRef, state.getYValue());
955
+ });
956
+
705
957
  return (
706
958
  <div
959
+ {...domProps}
960
+ ref={local.ref}
707
961
  {...cleanThumbProps()}
708
- {...cleanFocusProps()}
709
962
  {...cleanHoverProps()}
710
963
  class={renderProps.class()}
711
964
  style={mergedStyle()}
@@ -715,19 +968,28 @@ export function ColorAreaThumb(props: ColorAreaThumbProps): JSX.Element {
715
968
  data-focus-visible={isFocusVisible() || undefined}
716
969
  data-hovered={isHovered() || undefined}
717
970
  >
971
+ <input
972
+ {...mergedXInputProps()}
973
+ ref={(el) => {
974
+ xInputRef = el;
975
+ syncInputValue(el, state.getXValue());
976
+ }}
977
+ />
978
+ <input
979
+ {...mergedYInputProps()}
980
+ ref={(el) => {
981
+ yInputRef = el;
982
+ syncInputValue(el, state.getYValue());
983
+ }}
984
+ />
718
985
  {renderProps.renderChildren()}
719
986
  </div>
720
987
  );
721
988
  }
722
989
 
723
- // Attach sub-components
724
990
  ColorArea.Gradient = ColorAreaGradient;
725
991
  ColorArea.Thumb = ColorAreaThumb;
726
992
 
727
- // ============================================
728
- // COLOR WHEEL
729
- // ============================================
730
-
731
993
  export interface ColorWheelRenderProps {
732
994
  /** Whether the wheel is disabled. */
733
995
  isDisabled: boolean;
@@ -737,6 +999,8 @@ export interface ColorWheelRenderProps {
737
999
  hue: number;
738
1000
  /** The current color. */
739
1001
  color: Color;
1002
+ /** The default inline styles applied by the color wheel hook. */
1003
+ defaultStyle: JSX.CSSProperties;
740
1004
  }
741
1005
 
742
1006
  export interface ColorWheelProps extends AriaColorWheelOptions, SlotProps {
@@ -761,6 +1025,8 @@ export interface ColorWheelTrackRenderProps {
761
1025
  isDisabled: boolean;
762
1026
  /** Whether the wheel is being dragged. */
763
1027
  isDragging: boolean;
1028
+ /** The default inline styles applied by the color wheel hook. */
1029
+ defaultStyle: JSX.CSSProperties;
764
1030
  }
765
1031
 
766
1032
  export interface ColorWheelTrackProps extends SlotProps {
@@ -777,12 +1043,16 @@ export interface ColorWheelThumbRenderProps {
777
1043
  isDisabled: boolean;
778
1044
  /** Whether the thumb is being dragged. */
779
1045
  isDragging: boolean;
1046
+ /** The current display color. */
1047
+ color: Color;
780
1048
  /** Whether the thumb is focused. */
781
1049
  isFocused: boolean;
782
1050
  /** Whether the thumb has keyboard focus. */
783
1051
  isFocusVisible: boolean;
784
1052
  /** Whether the thumb is hovered. */
785
1053
  isHovered: boolean;
1054
+ /** The default inline styles applied by the color wheel hook. */
1055
+ defaultStyle: JSX.CSSProperties;
786
1056
  }
787
1057
 
788
1058
  export interface ColorWheelThumbProps extends SlotProps {
@@ -792,9 +1062,10 @@ export interface ColorWheelThumbProps extends SlotProps {
792
1062
  class?: ClassNameOrFunction<ColorWheelThumbRenderProps>;
793
1063
  /** The inline style for the element. */
794
1064
  style?: StyleOrFunction<ColorWheelThumbRenderProps>;
1065
+ /** Ref callback for the thumb element. */
1066
+ ref?: (element: HTMLDivElement) => void;
795
1067
  }
796
1068
 
797
- // Context
798
1069
  interface ColorWheelContextValue {
799
1070
  state: ColorWheelState;
800
1071
  trackProps: JSX.HTMLAttributes<HTMLDivElement>;
@@ -810,88 +1081,110 @@ export const ColorWheelContext = createContext<ColorWheelContextValue | null>(nu
810
1081
  * A color wheel allows users to select a hue using a circular control.
811
1082
  */
812
1083
  export function ColorWheel(props: ColorWheelProps): JSX.Element {
1084
+ const pickerContext = useContext(ColorPickerContextInternal);
813
1085
  const [local, stateProps, ariaProps, rest] = splitProps(
814
1086
  props,
815
- ['children', 'class', 'style', 'slot'],
816
- ['value', 'defaultValue', 'onChange', 'onChangeEnd'],
817
- ['aria-label', 'aria-labelledby', 'aria-describedby', 'isDisabled']
1087
+ ["children", "class", "style", "slot"],
1088
+ ["value", "defaultValue", "onChange", "onChangeEnd"],
1089
+ [
1090
+ "id",
1091
+ "aria-label",
1092
+ "aria-labelledby",
1093
+ "aria-describedby",
1094
+ "aria-details",
1095
+ "aria-errormessage",
1096
+ "isDisabled",
1097
+ "name",
1098
+ "form",
1099
+ "outerRadius",
1100
+ "innerRadius",
1101
+ ],
818
1102
  );
819
1103
 
820
1104
  // Create color wheel state
821
1105
  const state = createColorWheelState(() => ({
822
- value: stateProps.value,
1106
+ value: stateProps.value ?? pickerContext?.value,
823
1107
  defaultValue: stateProps.defaultValue,
824
- onChange: stateProps.onChange,
1108
+ onChange: stateProps.onChange ?? pickerContext?.onChange,
825
1109
  onChangeEnd: stateProps.onChangeEnd,
826
1110
  isDisabled: ariaProps.isDisabled,
827
1111
  }));
828
1112
 
829
- // Wheel ref
830
1113
  let wheelRef: HTMLDivElement | undefined;
831
1114
  const setWheelRef = (el: HTMLDivElement) => {
832
1115
  wheelRef = el;
833
1116
  };
834
1117
 
835
1118
  // Create color wheel aria props
836
- const {
837
- trackProps,
838
- thumbProps,
839
- inputProps,
840
- } = createColorWheel(
1119
+ const colorWheelAria = createColorWheel(
841
1120
  () => ({
842
- 'aria-label': ariaProps['aria-label'],
843
- 'aria-labelledby': ariaProps['aria-labelledby'],
844
- 'aria-describedby': ariaProps['aria-describedby'],
1121
+ id: ariaProps.id,
1122
+ "aria-label": ariaProps["aria-label"],
1123
+ "aria-labelledby": ariaProps["aria-labelledby"],
1124
+ "aria-describedby": ariaProps["aria-describedby"],
1125
+ "aria-details": ariaProps["aria-details"],
1126
+ "aria-errormessage": ariaProps["aria-errormessage"],
845
1127
  isDisabled: ariaProps.isDisabled,
1128
+ name: ariaProps.name,
1129
+ form: ariaProps.form,
1130
+ outerRadius: ariaProps.outerRadius,
1131
+ innerRadius: ariaProps.innerRadius,
846
1132
  }),
847
1133
  () => state,
848
- () => wheelRef ?? null
1134
+ () => wheelRef ?? null,
849
1135
  );
850
1136
 
851
- // Render props values
852
1137
  const renderValues = createMemo<ColorWheelRenderProps>(() => ({
853
1138
  isDisabled: state.isDisabled,
854
1139
  isDragging: state.isDragging,
855
1140
  hue: state.getHue(),
856
1141
  color: state.value,
1142
+ defaultStyle: { position: "relative" },
857
1143
  }));
858
1144
 
859
- // Resolve render props
860
1145
  const renderProps = useRenderProps(
861
1146
  {
862
1147
  children: props.children,
863
1148
  class: local.class,
864
1149
  style: local.style,
865
- defaultClassName: 'solidaria-ColorWheel',
1150
+ defaultClassName: "solidaria-ColorWheel",
866
1151
  },
867
- renderValues
1152
+ renderValues,
868
1153
  );
869
1154
 
870
- // Filter DOM props
871
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
1155
+ const domProps = createMemo(() =>
1156
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
1157
+ );
872
1158
 
873
1159
  return (
874
1160
  <ColorWheelContext.Provider
875
1161
  value={{
876
1162
  state,
877
- trackProps,
878
- thumbProps,
879
- inputProps,
880
- wheelRef,
1163
+ get trackProps() {
1164
+ return colorWheelAria.trackProps;
1165
+ },
1166
+ get thumbProps() {
1167
+ return colorWheelAria.thumbProps;
1168
+ },
1169
+ get inputProps() {
1170
+ return colorWheelAria.inputProps;
1171
+ },
1172
+ get wheelRef() {
1173
+ return wheelRef;
1174
+ },
881
1175
  setWheelRef,
882
1176
  }}
883
1177
  >
884
1178
  <div
1179
+ ref={setWheelRef}
885
1180
  {...domProps()}
886
1181
  class={renderProps.class()}
887
1182
  style={renderProps.style()}
1183
+ slot={local.slot || undefined}
888
1184
  data-disabled={state.isDisabled || undefined}
889
1185
  data-dragging={state.isDragging || undefined}
890
1186
  >
891
1187
  {renderProps.renderChildren()}
892
-
893
- {/* Hidden input for accessibility */}
894
- <input {...inputProps} />
895
1188
  </div>
896
1189
  </ColorWheelContext.Provider>
897
1190
  );
@@ -901,48 +1194,49 @@ export function ColorWheel(props: ColorWheelProps): JSX.Element {
901
1194
  * The track element of a color wheel.
902
1195
  */
903
1196
  export function ColorWheelTrack(props: ColorWheelTrackProps): JSX.Element {
904
- const [local] = splitProps(props, ['class', 'style', 'slot']);
1197
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
905
1198
 
906
1199
  const context = useContext(ColorWheelContext);
907
1200
  if (!context) {
908
- throw new Error('ColorWheelTrack must be used within a ColorWheel');
1201
+ throw new Error("ColorWheelTrack must be used within a ColorWheel");
909
1202
  }
910
1203
 
911
- const { state, trackProps, setWheelRef } = context;
1204
+ const state = context.state;
912
1205
 
913
- // Render props values
914
1206
  const renderValues = createMemo<ColorWheelTrackRenderProps>(() => ({
915
1207
  isDisabled: state.isDisabled,
916
1208
  isDragging: state.isDragging,
1209
+ defaultStyle: (context.trackProps as { style?: JSX.CSSProperties }).style ?? {},
917
1210
  }));
918
1211
 
919
- // Resolve render props
920
1212
  const renderProps = useRenderProps(
921
1213
  {
922
1214
  children: props.children,
923
1215
  class: local.class,
924
1216
  style: local.style,
925
- defaultClassName: 'solidaria-ColorWheel-track',
1217
+ defaultClassName: "solidaria-ColorWheel-track",
926
1218
  },
927
- renderValues
1219
+ renderValues,
928
1220
  );
929
1221
 
930
- // Clean props
931
1222
  const cleanTrackProps = () => {
932
- const { ref: _ref, style: _trackStyle, ...rest } = trackProps as Record<string, unknown>;
1223
+ const {
1224
+ ref: _ref,
1225
+ style: _trackStyle,
1226
+ ...rest
1227
+ } = context.trackProps as Record<string, unknown>;
933
1228
  return rest;
934
1229
  };
935
1230
 
936
- // Merge styles
937
1231
  const mergedStyle = () => {
938
- const trackStyle = (trackProps as { style?: Record<string, string> }).style || {};
1232
+ const trackStyle = (context.trackProps as { style?: Record<string, string> }).style || {};
939
1233
  const renderStyle = renderProps.style() || {};
940
1234
  return { ...trackStyle, ...renderStyle };
941
1235
  };
942
1236
 
943
1237
  return (
944
1238
  <div
945
- ref={setWheelRef}
1239
+ {...domProps}
946
1240
  {...cleanTrackProps()}
947
1241
  class={renderProps.class()}
948
1242
  style={mergedStyle()}
@@ -958,48 +1252,49 @@ export function ColorWheelTrack(props: ColorWheelTrackProps): JSX.Element {
958
1252
  * The thumb element of a color wheel.
959
1253
  */
960
1254
  export function ColorWheelThumb(props: ColorWheelThumbProps): JSX.Element {
961
- const [local] = splitProps(props, ['class', 'style', 'slot']);
1255
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children", "ref"]);
962
1256
 
963
1257
  const context = useContext(ColorWheelContext);
964
1258
  if (!context) {
965
- throw new Error('ColorWheelThumb must be used within a ColorWheel');
1259
+ throw new Error("ColorWheelThumb must be used within a ColorWheel");
966
1260
  }
967
1261
 
968
- const { state, thumbProps } = context;
1262
+ const state = context.state;
969
1263
 
970
- // Create focus ring
971
1264
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
972
1265
 
973
- // Create hover
974
1266
  const { isHovered, hoverProps } = createHover({
975
1267
  get isDisabled() {
976
1268
  return state.isDisabled;
977
1269
  },
978
1270
  });
979
1271
 
980
- // Render props values
981
1272
  const renderValues = createMemo<ColorWheelThumbRenderProps>(() => ({
982
1273
  isDisabled: state.isDisabled,
983
1274
  isDragging: state.isDragging,
1275
+ color: state.getDisplayColor(),
984
1276
  isFocused: isFocused(),
985
1277
  isFocusVisible: isFocusVisible(),
986
1278
  isHovered: isHovered(),
1279
+ defaultStyle: (context.thumbProps as { style?: JSX.CSSProperties }).style ?? {},
987
1280
  }));
988
1281
 
989
- // Resolve render props
990
1282
  const renderProps = useRenderProps(
991
1283
  {
992
1284
  children: props.children,
993
1285
  class: local.class,
994
1286
  style: local.style,
995
- defaultClassName: 'solidaria-ColorWheel-thumb',
1287
+ defaultClassName: "solidaria-ColorWheel-thumb",
996
1288
  },
997
- renderValues
1289
+ renderValues,
998
1290
  );
999
1291
 
1000
- // Clean props
1001
1292
  const cleanThumbProps = () => {
1002
- const { ref: _ref, style: _thumbStyle, ...rest } = thumbProps as Record<string, unknown>;
1293
+ const {
1294
+ ref: _ref,
1295
+ style: _thumbStyle,
1296
+ ...rest
1297
+ } = context.thumbProps as Record<string, unknown>;
1003
1298
  return rest;
1004
1299
  };
1005
1300
  const cleanFocusProps = () => {
@@ -1010,18 +1305,24 @@ export function ColorWheelThumb(props: ColorWheelThumbProps): JSX.Element {
1010
1305
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
1011
1306
  return rest;
1012
1307
  };
1308
+ const mergedInputProps = () => {
1309
+ return mergeProps(
1310
+ context.inputProps as Record<string, unknown>,
1311
+ cleanFocusProps(),
1312
+ ) as JSX.InputHTMLAttributes<HTMLInputElement>;
1313
+ };
1013
1314
 
1014
- // Merge styles
1015
1315
  const mergedStyle = () => {
1016
- const thumbStyle = (thumbProps as { style?: Record<string, string> }).style || {};
1316
+ const thumbStyle = (context.thumbProps as { style?: Record<string, string> }).style || {};
1017
1317
  const renderStyle = renderProps.style() || {};
1018
1318
  return { ...thumbStyle, ...renderStyle };
1019
1319
  };
1020
1320
 
1021
1321
  return (
1022
1322
  <div
1323
+ {...domProps}
1324
+ ref={local.ref}
1023
1325
  {...cleanThumbProps()}
1024
- {...cleanFocusProps()}
1025
1326
  {...cleanHoverProps()}
1026
1327
  class={renderProps.class()}
1027
1328
  style={mergedStyle()}
@@ -1031,30 +1332,28 @@ export function ColorWheelThumb(props: ColorWheelThumbProps): JSX.Element {
1031
1332
  data-focus-visible={isFocusVisible() || undefined}
1032
1333
  data-hovered={isHovered() || undefined}
1033
1334
  >
1335
+ <input {...mergedInputProps()} />
1034
1336
  {renderProps.renderChildren()}
1035
1337
  </div>
1036
1338
  );
1037
1339
  }
1038
1340
 
1039
- // Attach sub-components
1040
1341
  ColorWheel.Track = ColorWheelTrack;
1041
1342
  ColorWheel.Thumb = ColorWheelThumb;
1042
1343
 
1043
- // ============================================
1044
- // COLOR FIELD
1045
- // ============================================
1046
-
1047
1344
  export interface ColorFieldRenderProps {
1048
1345
  /** Whether the field is disabled. */
1049
1346
  isDisabled: boolean;
1050
1347
  /** Whether the field is read-only. */
1051
1348
  isReadOnly: boolean;
1349
+ /** Whether the field is required. */
1350
+ isRequired: boolean;
1052
1351
  /** Whether the input value is invalid. */
1053
1352
  isInvalid: boolean;
1054
1353
  /** The current color value (null if invalid). */
1055
1354
  color: Color | null;
1056
- /** The color channel being edited (if single channel mode). */
1057
- channel: ColorChannel | undefined;
1355
+ /** The color channel being edited, or "hex" for full color mode. */
1356
+ channel: ColorChannel | "hex";
1058
1357
  }
1059
1358
 
1060
1359
  export interface ColorFieldProps extends AriaColorFieldOptions, SlotProps {
@@ -1066,10 +1365,16 @@ export interface ColorFieldProps extends AriaColorFieldOptions, SlotProps {
1066
1365
  onChange?: (color: Color | null) => void;
1067
1366
  /** The color channel to edit (for single channel mode). */
1068
1367
  channel?: ColorChannel;
1368
+ /** The color space to use for channel mode. */
1369
+ colorSpace?: ColorSpace;
1069
1370
  /** The color format for parsing/displaying. */
1070
1371
  colorFormat?: ColorFormat;
1071
1372
  /** A visible label for the field. */
1072
1373
  label?: JSX.Element;
1374
+ /** Description text for the field. */
1375
+ description?: JSX.Element;
1376
+ /** Error message for the field. */
1377
+ errorMessage?: JSX.Element;
1073
1378
  /** The children of the component. */
1074
1379
  children?: RenderChildren<ColorFieldRenderProps>;
1075
1380
  /** The CSS className for the element. */
@@ -1100,13 +1405,18 @@ export interface ColorFieldInputProps extends SlotProps {
1100
1405
  class?: ClassNameOrFunction<ColorFieldInputRenderProps>;
1101
1406
  /** The inline style for the element. */
1102
1407
  style?: StyleOrFunction<ColorFieldInputRenderProps>;
1408
+ /** Ref callback for the input element. */
1409
+ ref?: (element: HTMLInputElement) => void;
1103
1410
  }
1104
1411
 
1105
- // Context
1106
1412
  interface ColorFieldContextValue {
1107
1413
  state: ColorFieldState;
1108
1414
  inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
1109
1415
  labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
1416
+ descriptionProps: JSX.HTMLAttributes<HTMLElement>;
1417
+ errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
1418
+ setInputRef: (el: HTMLInputElement) => void;
1419
+ setLabelElement: (isPresent: boolean) => void;
1110
1420
  }
1111
1421
 
1112
1422
  export const ColorFieldContext = createContext<ColorFieldContextValue | null>(null);
@@ -1115,90 +1425,221 @@ export const ColorFieldContext = createContext<ColorFieldContextValue | null>(nu
1115
1425
  * A color field allows users to enter a color value as text.
1116
1426
  */
1117
1427
  export function ColorField(props: ColorFieldProps): JSX.Element {
1428
+ const pickerContext = useContext(ColorPickerContextInternal);
1118
1429
  const [local, stateProps, ariaProps, rest] = splitProps(
1119
1430
  props,
1120
- ['children', 'class', 'style', 'slot', 'label'],
1121
- ['value', 'defaultValue', 'onChange', 'channel', 'colorFormat'],
1122
- ['aria-label', 'aria-labelledby', 'aria-describedby', 'isDisabled', 'isReadOnly']
1431
+ ["children", "class", "style", "slot", "label", "description", "errorMessage"],
1432
+ ["value", "defaultValue", "onChange", "channel", "colorSpace", "colorFormat"],
1433
+ [
1434
+ "id",
1435
+ "aria-label",
1436
+ "aria-labelledby",
1437
+ "aria-describedby",
1438
+ "aria-details",
1439
+ "aria-errormessage",
1440
+ "name",
1441
+ "form",
1442
+ "isWheelDisabled",
1443
+ "isDisabled",
1444
+ "isReadOnly",
1445
+ "isRequired",
1446
+ "isInvalid",
1447
+ "validationBehavior",
1448
+ "autoFocus",
1449
+ "excludeFromTabOrder",
1450
+ "placeholder",
1451
+ ],
1123
1452
  );
1453
+ const [hasRegisteredLabelElement, setHasRegisteredLabelElement] = createSignal(false);
1124
1454
 
1125
1455
  // Create color field state
1126
1456
  const state = createColorFieldState(() => ({
1127
- value: stateProps.value,
1457
+ value: stateProps.value ?? pickerContext?.value,
1128
1458
  defaultValue: stateProps.defaultValue,
1129
- onChange: stateProps.onChange,
1459
+ onChange:
1460
+ stateProps.onChange ??
1461
+ ((color) => {
1462
+ if (color) {
1463
+ pickerContext?.onChange?.(color);
1464
+ }
1465
+ }),
1130
1466
  channel: stateProps.channel,
1467
+ colorSpace: stateProps.colorSpace,
1131
1468
  colorFormat: stateProps.colorFormat,
1132
1469
  isDisabled: ariaProps.isDisabled,
1133
1470
  isReadOnly: ariaProps.isReadOnly,
1471
+ isInvalid: ariaProps.isInvalid,
1472
+ isRequired: ariaProps.isRequired,
1134
1473
  }));
1135
1474
 
1136
1475
  // Input ref
1137
1476
  let inputRef: HTMLInputElement | undefined;
1477
+ const setInputRef = (el: HTMLInputElement) => {
1478
+ inputRef = el;
1479
+ };
1138
1480
 
1139
1481
  // Create color field aria props
1140
- const {
1141
- inputProps,
1142
- labelProps,
1143
- } = createColorField(
1482
+ const colorFieldAria = createColorField(
1144
1483
  () => ({
1145
- 'aria-label': ariaProps['aria-label'],
1146
- 'aria-labelledby': ariaProps['aria-labelledby'],
1147
- 'aria-describedby': ariaProps['aria-describedby'],
1484
+ id: ariaProps.id,
1485
+ "aria-label": ariaProps["aria-label"],
1486
+ "aria-labelledby": ariaProps["aria-labelledby"],
1487
+ "aria-describedby": ariaProps["aria-describedby"],
1488
+ "aria-details": ariaProps["aria-details"],
1489
+ "aria-errormessage": ariaProps["aria-errormessage"],
1490
+ name: ariaProps.name,
1491
+ form: ariaProps.form,
1492
+ isWheelDisabled: ariaProps.isWheelDisabled,
1148
1493
  isDisabled: ariaProps.isDisabled,
1149
1494
  isReadOnly: ariaProps.isReadOnly,
1495
+ isRequired: ariaProps.isRequired,
1496
+ isInvalid: ariaProps.isInvalid,
1497
+ validationBehavior: ariaProps.validationBehavior,
1498
+ autoFocus: ariaProps.autoFocus,
1499
+ excludeFromTabOrder: ariaProps.excludeFromTabOrder,
1500
+ placeholder: ariaProps.placeholder,
1150
1501
  channel: stateProps.channel,
1502
+ colorSpace: stateProps.colorSpace,
1151
1503
  }),
1152
1504
  () => state,
1153
- () => inputRef ?? null
1505
+ () => inputRef ?? null,
1506
+ );
1507
+
1508
+ const describedBy = () => {
1509
+ const invalid = ariaProps.isInvalid || state.isInvalid;
1510
+ const ids = [
1511
+ ariaProps["aria-describedby"],
1512
+ local.description && !invalid ? colorFieldAria.descriptionProps.id : undefined,
1513
+ invalid && local.errorMessage ? colorFieldAria.errorMessageProps.id : undefined,
1514
+ ];
1515
+ return ids.filter(Boolean).join(" ") || undefined;
1516
+ };
1517
+
1518
+ const fieldInputProps = () => {
1519
+ const labelledBy =
1520
+ ariaProps["aria-labelledby"] ??
1521
+ (!ariaProps["aria-label"] && (local.label || hasRegisteredLabelElement())
1522
+ ? colorFieldAria.labelProps.id
1523
+ : undefined);
1524
+ return {
1525
+ ...colorFieldAria.inputProps,
1526
+ "aria-label": labelledBy ? undefined : colorFieldAria.inputProps["aria-label"],
1527
+ "aria-labelledby": labelledBy,
1528
+ "aria-describedby": describedBy(),
1529
+ "aria-errormessage":
1530
+ (ariaProps.isInvalid || state.isInvalid) && local.errorMessage
1531
+ ? colorFieldAria.errorMessageProps.id
1532
+ : colorFieldAria.inputProps["aria-errormessage"],
1533
+ } as JSX.InputHTMLAttributes<HTMLInputElement>;
1534
+ };
1535
+
1536
+ const hiddenInputValue = createMemo(() =>
1537
+ Number.isNaN(state.numberValue) ? "" : String(state.numberValue),
1154
1538
  );
1155
1539
 
1156
- // Render props values
1157
1540
  const renderValues = createMemo<ColorFieldRenderProps>(() => ({
1158
1541
  isDisabled: state.isDisabled,
1159
1542
  isReadOnly: state.isReadOnly,
1543
+ isRequired: state.isRequired,
1160
1544
  isInvalid: state.isInvalid,
1161
1545
  color: state.value,
1162
- channel: state.channel,
1546
+ channel: state.channel ?? "hex",
1163
1547
  }));
1164
1548
 
1165
- // Resolve render props
1166
1549
  const renderProps = useRenderProps(
1167
1550
  {
1168
1551
  children: props.children,
1169
1552
  class: local.class,
1170
1553
  style: local.style,
1171
- defaultClassName: 'solidaria-ColorField',
1554
+ defaultClassName: "solidaria-ColorField",
1172
1555
  },
1173
- renderValues
1556
+ renderValues,
1174
1557
  );
1175
1558
 
1176
- // Filter DOM props
1177
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
1559
+ const childRenderValues: ColorFieldRenderProps = {
1560
+ get isDisabled() {
1561
+ return state.isDisabled;
1562
+ },
1563
+ get isReadOnly() {
1564
+ return state.isReadOnly;
1565
+ },
1566
+ get isRequired() {
1567
+ return state.isRequired;
1568
+ },
1569
+ get isInvalid() {
1570
+ return state.isInvalid;
1571
+ },
1572
+ get color() {
1573
+ return state.value;
1574
+ },
1575
+ get channel() {
1576
+ return state.channel ?? "hex";
1577
+ },
1578
+ };
1579
+
1580
+ let hasRenderedChildren = false;
1581
+ let renderedChildren: JSX.Element;
1582
+ const renderChildren = () => {
1583
+ if (!hasRenderedChildren) {
1584
+ const children = local.children;
1585
+ renderedChildren =
1586
+ typeof children === "function" ? untrack(() => children(childRenderValues)) : children;
1587
+ hasRenderedChildren = true;
1588
+ }
1589
+ return renderedChildren;
1590
+ };
1591
+
1592
+ const domProps = createMemo(() =>
1593
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
1594
+ );
1178
1595
 
1179
1596
  return (
1180
1597
  <ColorFieldContext.Provider
1181
1598
  value={{
1182
1599
  state,
1183
- inputProps,
1184
- labelProps,
1600
+ get inputProps() {
1601
+ return fieldInputProps();
1602
+ },
1603
+ get labelProps() {
1604
+ return colorFieldAria.labelProps;
1605
+ },
1606
+ get descriptionProps() {
1607
+ return colorFieldAria.descriptionProps;
1608
+ },
1609
+ get errorMessageProps() {
1610
+ return colorFieldAria.errorMessageProps;
1611
+ },
1612
+ setInputRef,
1613
+ setLabelElement: setHasRegisteredLabelElement,
1185
1614
  }}
1186
1615
  >
1187
- <div
1188
- {...domProps()}
1189
- class={renderProps.class()}
1190
- style={renderProps.style()}
1191
- data-disabled={state.isDisabled || undefined}
1192
- data-readonly={state.isReadOnly || undefined}
1193
- data-invalid={state.isInvalid || undefined}
1194
- >
1195
- {/* Label */}
1196
- <Show when={local.label}>
1197
- <label {...labelProps}>{local.label}</label>
1616
+ <>
1617
+ <div
1618
+ {...domProps()}
1619
+ class={renderProps.class()}
1620
+ style={renderProps.style()}
1621
+ slot={local.slot ?? undefined}
1622
+ data-disabled={state.isDisabled || undefined}
1623
+ data-readonly={state.isReadOnly || undefined}
1624
+ data-invalid={state.isInvalid || undefined}
1625
+ data-required={state.isRequired || undefined}
1626
+ data-channel={state.channel ?? "hex"}
1627
+ >
1628
+ <Show when={local.label}>
1629
+ <label {...colorFieldAria.labelProps}>{local.label}</label>
1630
+ </Show>
1631
+
1632
+ {renderChildren()}
1633
+ </div>
1634
+ <Show when={state.channel && ariaProps.name}>
1635
+ <input
1636
+ type="hidden"
1637
+ name={ariaProps.name}
1638
+ form={ariaProps.form}
1639
+ value={hiddenInputValue()}
1640
+ />
1198
1641
  </Show>
1199
-
1200
- {renderProps.renderChildren()}
1201
- </div>
1642
+ </>
1202
1643
  </ColorFieldContext.Provider>
1203
1644
  );
1204
1645
  }
@@ -1207,26 +1648,24 @@ export function ColorField(props: ColorFieldProps): JSX.Element {
1207
1648
  * The input element of a color field.
1208
1649
  */
1209
1650
  export function ColorFieldInput(props: ColorFieldInputProps): JSX.Element {
1210
- const [local] = splitProps(props, ['class', 'style', 'slot']);
1651
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children", "ref"]);
1211
1652
 
1212
1653
  const context = useContext(ColorFieldContext);
1213
1654
  if (!context) {
1214
- throw new Error('ColorFieldInput must be used within a ColorField');
1655
+ throw new Error("ColorFieldInput must be used within a ColorField");
1215
1656
  }
1216
1657
 
1217
- const { state, inputProps } = context;
1658
+ const state = context.state;
1659
+ const inputValue = createMemo(() => state.inputValue);
1218
1660
 
1219
- // Create focus ring
1220
1661
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
1221
1662
 
1222
- // Create hover
1223
1663
  const { isHovered, hoverProps } = createHover({
1224
1664
  get isDisabled() {
1225
1665
  return state.isDisabled;
1226
1666
  },
1227
1667
  });
1228
1668
 
1229
- // Render props values
1230
1669
  const renderValues = createMemo<ColorFieldInputRenderProps>(() => ({
1231
1670
  isDisabled: state.isDisabled,
1232
1671
  isReadOnly: state.isReadOnly,
@@ -1236,20 +1675,23 @@ export function ColorFieldInput(props: ColorFieldInputProps): JSX.Element {
1236
1675
  isHovered: isHovered(),
1237
1676
  }));
1238
1677
 
1239
- // Resolve render props
1240
1678
  const renderProps = useRenderProps(
1241
1679
  {
1242
1680
  children: props.children,
1243
1681
  class: local.class,
1244
1682
  style: local.style,
1245
- defaultClassName: 'solidaria-ColorField-input',
1683
+ defaultClassName: "solidaria-ColorField-input",
1246
1684
  },
1247
- renderValues
1685
+ renderValues,
1248
1686
  );
1249
1687
 
1250
- // Clean props
1251
1688
  const cleanInputProps = () => {
1252
- const { ref: _ref, style: _inputStyle, ...rest } = inputProps as Record<string, unknown>;
1689
+ const {
1690
+ ref: _ref,
1691
+ style: _inputStyle,
1692
+ value: _value,
1693
+ ...rest
1694
+ } = context.inputProps as Record<string, unknown>;
1253
1695
  return rest;
1254
1696
  };
1255
1697
  const cleanFocusProps = () => {
@@ -1263,11 +1705,17 @@ export function ColorFieldInput(props: ColorFieldInputProps): JSX.Element {
1263
1705
 
1264
1706
  return (
1265
1707
  <input
1708
+ {...domProps}
1266
1709
  {...cleanInputProps()}
1267
1710
  {...cleanFocusProps()}
1268
1711
  {...cleanHoverProps()}
1712
+ ref={(el) => {
1713
+ context.setInputRef(el);
1714
+ local.ref?.(el);
1715
+ }}
1269
1716
  class={renderProps.class()}
1270
1717
  style={renderProps.style()}
1718
+ value={inputValue()}
1271
1719
  data-disabled={state.isDisabled || undefined}
1272
1720
  data-readonly={state.isReadOnly || undefined}
1273
1721
  data-invalid={state.isInvalid || undefined}
@@ -1278,13 +1726,8 @@ export function ColorFieldInput(props: ColorFieldInputProps): JSX.Element {
1278
1726
  );
1279
1727
  }
1280
1728
 
1281
- // Attach sub-components
1282
1729
  ColorField.Input = ColorFieldInput;
1283
1730
 
1284
- // ============================================
1285
- // COLOR SWATCH
1286
- // ============================================
1287
-
1288
1731
  export interface ColorSwatchRenderProps {
1289
1732
  /** The color being displayed. */
1290
1733
  color: Color;
@@ -1294,9 +1737,17 @@ export interface ColorSwatchRenderProps {
1294
1737
 
1295
1738
  export interface ColorSwatchProps extends SlotProps {
1296
1739
  /** The color to display. */
1297
- color: Color | string;
1740
+ color?: Color | string;
1741
+ /** Localized color name override. */
1742
+ colorName?: string;
1298
1743
  /** Accessible label for the swatch. */
1299
- 'aria-label'?: string;
1744
+ "aria-label"?: string;
1745
+ /** ID of element that labels the swatch. */
1746
+ "aria-labelledby"?: string;
1747
+ /** ID of element that describes the swatch. */
1748
+ "aria-describedby"?: string;
1749
+ /** ID of element that provides detailed information about the swatch. */
1750
+ "aria-details"?: string;
1300
1751
  /** The children of the component. */
1301
1752
  children?: RenderChildren<ColorSwatchRenderProps>;
1302
1753
  /** The CSS className for the element. */
@@ -1309,62 +1760,652 @@ export interface ColorSwatchProps extends SlotProps {
1309
1760
  * A color swatch displays a preview of a color.
1310
1761
  */
1311
1762
  export function ColorSwatch(props: ColorSwatchProps): JSX.Element {
1763
+ const swatchContext = useContext(ColorSwatchContextInternal);
1764
+ const pickerContext = useContext(ColorPickerContextInternal);
1312
1765
  const [local, ariaProps, rest] = splitProps(
1313
1766
  props,
1314
- ['children', 'class', 'style', 'slot', 'color'],
1315
- ['aria-label']
1767
+ ["children", "class", "style", "slot", "color", "colorName"],
1768
+ ["aria-label", "aria-labelledby", "aria-describedby", "aria-details"],
1316
1769
  );
1317
1770
 
1318
- // Create color swatch aria props
1319
- const { swatchProps } = createColorSwatch(() => ({
1320
- color: local.color,
1321
- 'aria-label': ariaProps['aria-label'],
1322
- }));
1771
+ const resolvedColor = createMemo<Color | string>(() => {
1772
+ return local.color ?? swatchContext?.color ?? pickerContext?.value ?? "#fff0";
1773
+ });
1323
1774
 
1324
- // Normalize color
1325
- const color = createMemo(() => normalizeColor(local.color));
1775
+ const swatchAria = createColorSwatch(() => ({
1776
+ id: (rest as Record<string, unknown>).id as string | undefined,
1777
+ slot: local.slot,
1778
+ color: resolvedColor(),
1779
+ colorName: local.colorName,
1780
+ "aria-label": ariaProps["aria-label"],
1781
+ "aria-labelledby": ariaProps["aria-labelledby"],
1782
+ "aria-describedby": ariaProps["aria-describedby"],
1783
+ "aria-details": ariaProps["aria-details"],
1784
+ }));
1326
1785
 
1327
- // Render props values
1328
1786
  const renderValues = createMemo<ColorSwatchRenderProps>(() => ({
1329
- color: color(),
1330
- colorValue: color().toString('css'),
1787
+ color: swatchAria.color,
1788
+ colorValue: swatchAria.color.toString("css"),
1331
1789
  }));
1332
1790
 
1333
- // Resolve render props
1334
1791
  const renderProps = useRenderProps(
1335
1792
  {
1336
1793
  children: props.children,
1337
1794
  class: local.class,
1338
1795
  style: local.style,
1339
- defaultClassName: 'solidaria-ColorSwatch',
1796
+ defaultClassName: "solidaria-ColorSwatch",
1340
1797
  },
1341
- renderValues
1798
+ renderValues,
1342
1799
  );
1343
1800
 
1344
- // Filter DOM props
1345
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
1801
+ const domProps = createMemo(() =>
1802
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
1803
+ );
1346
1804
 
1347
- // Clean props
1348
1805
  const cleanSwatchProps = () => {
1349
- const { ref: _ref, style: _swatchStyle, ...rest } = swatchProps as Record<string, unknown>;
1806
+ const {
1807
+ ref: _ref,
1808
+ style: _swatchStyle,
1809
+ ...rest
1810
+ } = swatchAria.swatchProps as Record<string, unknown>;
1350
1811
  return rest;
1351
1812
  };
1352
1813
 
1353
- // Merge styles
1354
1814
  const mergedStyle = () => {
1355
- const swatchStyle = (swatchProps as { style?: Record<string, string> }).style || {};
1815
+ const swatchStyle = (swatchAria.swatchProps as { style?: Record<string, string> }).style || {};
1356
1816
  const renderStyle = renderProps.style() || {};
1357
1817
  return { ...swatchStyle, ...renderStyle };
1358
1818
  };
1359
1819
 
1820
+ return (
1821
+ <div {...domProps()} {...cleanSwatchProps()} class={renderProps.class()} style={mergedStyle()}>
1822
+ {renderProps.renderChildren()}
1823
+ </div>
1824
+ );
1825
+ }
1826
+
1827
+ export const ColorSliderStateContext = ColorSliderContext;
1828
+ export const ColorAreaStateContext = ColorAreaContext;
1829
+ export const ColorWheelStateContext = ColorWheelContext;
1830
+ export const ColorWheelTrackContext = ColorWheelContext;
1831
+ export const ColorFieldStateContext = ColorFieldContext;
1832
+ export const ColorSwatchContext = ColorSwatchContextInternal;
1833
+ export const ColorPickerContext = ColorPickerContextInternal;
1834
+ export const ColorPickerStateContext = ColorPickerStateContextInternal;
1835
+ export const ColorSwatchPickerContext = ColorSwatchPickerContextInternal;
1836
+ export const ColorThumb = ColorSliderThumb;
1837
+
1838
+ export interface ColorPickerRenderProps {
1839
+ /** The currently selected color. */
1840
+ color: Color;
1841
+ }
1842
+
1843
+ export interface ColorPickerProps extends SlotProps {
1844
+ /** The current color value (controlled). */
1845
+ value?: Color | string;
1846
+ /** The default color value (uncontrolled). */
1847
+ defaultValue?: Color | string;
1848
+ /** Handler called when the color changes. */
1849
+ onChange?: (color: Color) => void;
1850
+ /** The children of the color picker. */
1851
+ children?: RenderChildren<ColorPickerRenderProps>;
1852
+ /** The CSS className for the element. */
1853
+ class?: ClassNameOrFunction<ColorPickerRenderProps>;
1854
+ /** The inline style for the element. */
1855
+ style?: StyleOrFunction<ColorPickerRenderProps>;
1856
+ }
1857
+
1858
+ export interface ColorSwatchPickerRenderProps {
1859
+ /** Whether the swatch picker has focus. */
1860
+ isFocused: boolean;
1861
+ /** Whether the swatch picker has keyboard focus. */
1862
+ isFocusVisible: boolean;
1863
+ /** The currently selected color. */
1864
+ selectedColor: Color;
1865
+ /** Item arrangement mode. */
1866
+ layout: "grid" | "stack";
1867
+ }
1868
+
1869
+ export interface ColorSwatchPickerProps extends SlotProps {
1870
+ /** The element's unique identifier. */
1871
+ id?: string;
1872
+ /** The current color value (controlled). */
1873
+ value?: Color | string;
1874
+ /** The default color value (uncontrolled). */
1875
+ defaultValue?: Color | string;
1876
+ /** Handler called when the selected color changes. */
1877
+ onChange?: (color: Color) => void;
1878
+ /** Accessible label for the swatch picker. */
1879
+ "aria-label"?: string;
1880
+ /** ID of element that labels the swatch picker. */
1881
+ "aria-labelledby"?: string;
1882
+ /** ID of element that describes the swatch picker. */
1883
+ "aria-describedby"?: string;
1884
+ /** ID of element that provides detailed information about the swatch picker. */
1885
+ "aria-details"?: string;
1886
+ /** Whether swatches are arranged as a grid or stack. */
1887
+ layout?: "grid" | "stack";
1888
+ /** The children (ColorSwatchPickerItem elements). */
1889
+ children?: JSX.Element;
1890
+ /** The CSS className for the element. */
1891
+ class?: ClassNameOrFunction<ColorSwatchPickerRenderProps>;
1892
+ /** The inline style for the element. */
1893
+ style?: StyleOrFunction<ColorSwatchPickerRenderProps>;
1894
+ }
1895
+
1896
+ export interface ColorSwatchPickerItemRenderProps {
1897
+ /** Whether the item is selected. */
1898
+ isSelected: boolean;
1899
+ /** Whether the item is focused. */
1900
+ isFocused: boolean;
1901
+ /** Whether the item has keyboard focus. */
1902
+ isFocusVisible: boolean;
1903
+ /** Whether the item is pressed. */
1904
+ isPressed: boolean;
1905
+ /** Whether the item is disabled. */
1906
+ isDisabled: boolean;
1907
+ /** The color represented by the item. */
1908
+ color: Color;
1909
+ }
1910
+
1911
+ export interface ColorSwatchPickerItemProps extends SlotProps {
1912
+ /** The color represented by this swatch item. */
1913
+ color: Color | string;
1914
+ /** Whether this item is disabled. */
1915
+ isDisabled?: boolean;
1916
+ /** Accessible label for this item. */
1917
+ "aria-label"?: string;
1918
+ /** The children of the swatch item. */
1919
+ children?: RenderChildren<ColorSwatchPickerItemRenderProps>;
1920
+ /** The CSS className for the element. */
1921
+ class?: ClassNameOrFunction<ColorSwatchPickerItemRenderProps>;
1922
+ /** The inline style for the element. */
1923
+ style?: StyleOrFunction<ColorSwatchPickerItemRenderProps>;
1924
+ }
1925
+
1926
+ export function ColorPicker(props: ColorPickerProps): JSX.Element {
1927
+ const [local] = splitProps(props, [
1928
+ "value",
1929
+ "defaultValue",
1930
+ "onChange",
1931
+ "children",
1932
+ "class",
1933
+ "style",
1934
+ "slot",
1935
+ ]);
1936
+
1937
+ const [internalColor, setInternalColor] = createSignal<Color>(
1938
+ normalizeColor(local.defaultValue ?? "#ff0000"),
1939
+ );
1940
+
1941
+ const color = createMemo<Color>(() => {
1942
+ if (local.value !== undefined) {
1943
+ return normalizeColor(local.value);
1944
+ }
1945
+ return internalColor();
1946
+ });
1947
+
1948
+ const setColor = (nextColor: Color) => {
1949
+ if (local.value === undefined) {
1950
+ setInternalColor(nextColor);
1951
+ }
1952
+ local.onChange?.(nextColor);
1953
+ };
1954
+
1955
+ const renderValues = createMemo<ColorPickerRenderProps>(() => ({
1956
+ color: color(),
1957
+ }));
1958
+
1959
+ const renderProps = useRenderProps(
1960
+ {
1961
+ children: local.children,
1962
+ class: local.class,
1963
+ style: local.style,
1964
+ defaultClassName: "solidaria-ColorPicker",
1965
+ },
1966
+ renderValues,
1967
+ );
1968
+
1969
+ return (
1970
+ <ColorPickerStateContextInternal.Provider
1971
+ value={{
1972
+ color: () => color(),
1973
+ setColor,
1974
+ }}
1975
+ >
1976
+ <ColorPickerContextInternal.Provider
1977
+ value={{
1978
+ get value() {
1979
+ return color();
1980
+ },
1981
+ onChange: setColor,
1982
+ }}
1983
+ >
1984
+ <div class={renderProps.class()} style={renderProps.style()}>
1985
+ {renderProps.renderChildren()}
1986
+ </div>
1987
+ </ColorPickerContextInternal.Provider>
1988
+ </ColorPickerStateContextInternal.Provider>
1989
+ );
1990
+ }
1991
+
1992
+ export function ColorSwatchPicker(props: ColorSwatchPickerProps): JSX.Element {
1993
+ const pickerContext = useContext(ColorPickerContextInternal);
1994
+ const [local, rest] = splitProps(props, [
1995
+ "value",
1996
+ "defaultValue",
1997
+ "onChange",
1998
+ "id",
1999
+ "aria-label",
2000
+ "aria-labelledby",
2001
+ "aria-describedby",
2002
+ "aria-details",
2003
+ "layout",
2004
+ "children",
2005
+ "class",
2006
+ "style",
2007
+ "slot",
2008
+ ]);
2009
+
2010
+ const [itemMap, setItemMap] = createSignal<Map<string, ColorSwatchPickerItemData>>(new Map());
2011
+ const [itemOrder, setItemOrder] = createSignal<string[]>([]);
2012
+ const [internalColor, setInternalColor] = createSignal<Color>(
2013
+ normalizeColor(local.defaultValue ?? pickerContext?.value ?? "#ff0000"),
2014
+ );
2015
+
2016
+ const selectedColor = createMemo<Color>(() => {
2017
+ if (local.value !== undefined) {
2018
+ return normalizeColor(local.value);
2019
+ }
2020
+ if (pickerContext?.value !== undefined) {
2021
+ return normalizeColor(pickerContext.value);
2022
+ }
2023
+ return internalColor();
2024
+ });
2025
+
2026
+ const selectedKey = createMemo(() => selectedColor().toString("hexa"));
2027
+ const isControlled = createMemo(
2028
+ () => local.value !== undefined || pickerContext?.value !== undefined,
2029
+ );
2030
+
2031
+ const registerItem = (item: ColorSwatchPickerItemData) => {
2032
+ setItemMap((prev) => {
2033
+ const next = new Map(prev);
2034
+ next.set(item.key, item);
2035
+ return next;
2036
+ });
2037
+ setItemOrder((prev) => (prev.includes(item.key) ? prev : [...prev, item.key]));
2038
+ };
2039
+
2040
+ const unregisterItem = (key: string) => {
2041
+ setItemMap((prev) => {
2042
+ if (!prev.has(key)) return prev;
2043
+ const next = new Map(prev);
2044
+ next.delete(key);
2045
+ return next;
2046
+ });
2047
+ setItemOrder((prev) => prev.filter((itemKey) => itemKey !== key));
2048
+ };
2049
+
2050
+ const items = createMemo(() => {
2051
+ const map = itemMap();
2052
+ return itemOrder()
2053
+ .map((key) => map.get(key))
2054
+ .filter((item): item is ColorSwatchPickerItemData => item != null);
2055
+ });
2056
+
2057
+ const state = createListState<ColorSwatchPickerItemData>({
2058
+ get items() {
2059
+ return items();
2060
+ },
2061
+ get getKey() {
2062
+ return (item: ColorSwatchPickerItemData) => item.key;
2063
+ },
2064
+ get getTextValue() {
2065
+ return (item: ColorSwatchPickerItemData) => item.textValue;
2066
+ },
2067
+ get getDisabled() {
2068
+ return (item: ColorSwatchPickerItemData) => !!item.isDisabled;
2069
+ },
2070
+ selectionMode: "single",
2071
+ disallowEmptySelection: true,
2072
+ get selectedKeys() {
2073
+ return [selectedKey()];
2074
+ },
2075
+ onSelectionChange(keys) {
2076
+ if (keys === "all") return;
2077
+ const key = keys.values().next().value as string | undefined;
2078
+ if (!key) return;
2079
+ const item = itemMap().get(key);
2080
+ if (!item) return;
2081
+ if (!isControlled()) {
2082
+ setInternalColor(item.color);
2083
+ }
2084
+ (local.onChange ?? pickerContext?.onChange)?.(item.color);
2085
+ },
2086
+ });
2087
+
2088
+ const listBoxAria = createListBox(
2089
+ () => ({
2090
+ id: local.id,
2091
+ "aria-label":
2092
+ local["aria-label"] ?? (!local["aria-labelledby"] ? "Color swatch picker" : undefined),
2093
+ "aria-labelledby": local["aria-labelledby"],
2094
+ "aria-describedby": local["aria-describedby"],
2095
+ "aria-details": local["aria-details"],
2096
+ shouldFocusWrap: true,
2097
+ }),
2098
+ state,
2099
+ );
2100
+
2101
+ const resolveDirection = (): "ltr" | "rtl" => {
2102
+ if (typeof document === "undefined") return "ltr";
2103
+ const rootDir = document.dir;
2104
+ return rootDir === "rtl" ? "rtl" : "ltr";
2105
+ };
2106
+
2107
+ const findNextEnabledKey = (from: Key | null, direction: "next" | "prev") => {
2108
+ const collection = state.collection();
2109
+ const getAdjacent =
2110
+ direction === "next"
2111
+ ? (key: Key) => collection.getKeyAfter(key)
2112
+ : (key: Key) => collection.getKeyBefore(key);
2113
+ const getBoundary =
2114
+ direction === "next" ? () => collection.getFirstKey() : () => collection.getLastKey();
2115
+
2116
+ let key = from != null ? getAdjacent(from) : getBoundary();
2117
+ while (key != null && state.isDisabled(key)) {
2118
+ key = getAdjacent(key);
2119
+ }
2120
+
2121
+ return key;
2122
+ };
2123
+
2124
+ const getBoundaryEnabledKey = (direction: "next" | "prev") => {
2125
+ const collection = state.collection();
2126
+ const getAdjacent =
2127
+ direction === "next"
2128
+ ? (key: Key) => collection.getKeyAfter(key)
2129
+ : (key: Key) => collection.getKeyBefore(key);
2130
+ const getBoundary =
2131
+ direction === "next" ? () => collection.getFirstKey() : () => collection.getLastKey();
2132
+
2133
+ let key = getBoundary();
2134
+ while (key != null && state.isDisabled(key)) {
2135
+ key = getAdjacent(key);
2136
+ }
2137
+
2138
+ return key;
2139
+ };
2140
+
2141
+ const getOptionElementForKey = (
2142
+ listbox: HTMLElement | null,
2143
+ key: Key | null,
2144
+ ): HTMLElement | null => {
2145
+ if (!listbox || key == null) return null;
2146
+ const keyString = String(key);
2147
+ for (const optionElement of listbox.querySelectorAll<HTMLElement>('[role="option"]')) {
2148
+ if (optionElement.id === keyString) {
2149
+ return optionElement;
2150
+ }
2151
+ }
2152
+ return null;
2153
+ };
2154
+
2155
+ const findGridKey = (
2156
+ listbox: HTMLElement | null,
2157
+ key: Key,
2158
+ nextKey: (current: Key) => Key | null,
2159
+ shouldSkip: (prevRect: DOMRect, itemRect: DOMRect) => boolean,
2160
+ ): Key | null => {
2161
+ let candidate: Key | null = key;
2162
+ const previousRect = getOptionElementForKey(listbox, candidate)?.getBoundingClientRect();
2163
+ if (!previousRect) {
2164
+ return null;
2165
+ }
2166
+
2167
+ while (candidate != null) {
2168
+ candidate = nextKey(candidate);
2169
+ if (candidate == null) {
2170
+ return null;
2171
+ }
2172
+
2173
+ const itemRect = getOptionElementForKey(listbox, candidate)?.getBoundingClientRect();
2174
+ if (!itemRect) {
2175
+ return null;
2176
+ }
2177
+
2178
+ if (!shouldSkip(previousRect, itemRect)) {
2179
+ return candidate;
2180
+ }
2181
+ }
2182
+
2183
+ return null;
2184
+ };
2185
+
2186
+ const isSameRow = (prevRect: DOMRect, itemRect: DOMRect) =>
2187
+ prevRect.y === itemRect.y || prevRect.x !== itemRect.x;
2188
+ const getGridKeyBelow = (listbox: HTMLElement | null, key: Key) =>
2189
+ findGridKey(listbox, key, (current) => findNextEnabledKey(current, "next"), isSameRow);
2190
+ const getGridKeyAbove = (listbox: HTMLElement | null, key: Key) =>
2191
+ findGridKey(listbox, key, (current) => findNextEnabledKey(current, "prev"), isSameRow);
2192
+ const getGridKeyRightOf = (key: Key) =>
2193
+ resolveDirection() === "rtl"
2194
+ ? findNextEnabledKey(key, "prev")
2195
+ : findNextEnabledKey(key, "next");
2196
+ const getGridKeyLeftOf = (key: Key) =>
2197
+ resolveDirection() === "rtl"
2198
+ ? findNextEnabledKey(key, "next")
2199
+ : findNextEnabledKey(key, "prev");
2200
+
2201
+ const handleGridKeyDown = (e: KeyboardEvent): boolean => {
2202
+ if ((local.layout ?? "grid") !== "grid") return false;
2203
+ if (
2204
+ e.key !== "ArrowRight" &&
2205
+ e.key !== "ArrowLeft" &&
2206
+ e.key !== "ArrowDown" &&
2207
+ e.key !== "ArrowUp"
2208
+ ) {
2209
+ return false;
2210
+ }
2211
+
2212
+ const listbox = e.currentTarget as HTMLElement | null;
2213
+ const focusedKey = state.focusedKey();
2214
+ const initialKey =
2215
+ focusedKey ??
2216
+ (e.key === "ArrowUp" || e.key === "ArrowLeft"
2217
+ ? getBoundaryEnabledKey("prev")
2218
+ : getBoundaryEnabledKey("next"));
2219
+ if (initialKey == null) return false;
2220
+
2221
+ let nextKey: Key | null = null;
2222
+ switch (e.key) {
2223
+ case "ArrowDown":
2224
+ nextKey = getGridKeyBelow(listbox, initialKey) ?? getBoundaryEnabledKey("next");
2225
+ break;
2226
+ case "ArrowUp":
2227
+ nextKey = getGridKeyAbove(listbox, initialKey) ?? getBoundaryEnabledKey("prev");
2228
+ break;
2229
+ case "ArrowRight":
2230
+ nextKey =
2231
+ getGridKeyRightOf(initialKey) ??
2232
+ (resolveDirection() === "rtl"
2233
+ ? getBoundaryEnabledKey("prev")
2234
+ : getBoundaryEnabledKey("next"));
2235
+ break;
2236
+ case "ArrowLeft":
2237
+ nextKey =
2238
+ getGridKeyLeftOf(initialKey) ??
2239
+ (resolveDirection() === "rtl"
2240
+ ? getBoundaryEnabledKey("next")
2241
+ : getBoundaryEnabledKey("prev"));
2242
+ break;
2243
+ }
2244
+
2245
+ if (nextKey == null) return false;
2246
+
2247
+ state.setFocusedKey(nextKey);
2248
+ if (state.selectionMode() === "single") {
2249
+ state.replaceSelection(nextKey);
2250
+ }
2251
+
2252
+ e.preventDefault();
2253
+ e.stopPropagation();
2254
+ return true;
2255
+ };
2256
+
2257
+ const getListBoxKeyDown = () => {
2258
+ const props = listBoxAria.listBoxProps as Record<string, unknown>;
2259
+ return props.onKeyDown as JSX.EventHandler<HTMLDivElement, KeyboardEvent> | undefined;
2260
+ };
2261
+
2262
+ const onColorSwatchPickerKeyDown: JSX.EventHandler<HTMLDivElement, KeyboardEvent> = (e) => {
2263
+ if (handleGridKeyDown(e)) {
2264
+ return;
2265
+ }
2266
+ getListBoxKeyDown()?.(e);
2267
+ };
2268
+
2269
+ createEffect(() => {
2270
+ const key = selectedKey();
2271
+ if (key) {
2272
+ state.setFocusedKey(key);
2273
+ }
2274
+ });
2275
+
2276
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing({ within: true });
2277
+ const domProps = createMemo(() =>
2278
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
2279
+ );
2280
+ const renderValues = createMemo<ColorSwatchPickerRenderProps>(() => ({
2281
+ isFocused: state.isFocused() || isFocused(),
2282
+ isFocusVisible: isFocusVisible(),
2283
+ selectedColor: selectedColor(),
2284
+ layout: local.layout ?? "grid",
2285
+ }));
2286
+
2287
+ const renderProps = useRenderProps(
2288
+ {
2289
+ class: local.class,
2290
+ style: local.style,
2291
+ defaultClassName: "solidaria-ColorSwatchPicker",
2292
+ },
2293
+ renderValues,
2294
+ );
2295
+
2296
+ const cleanListBoxProps = () => {
2297
+ const {
2298
+ ref: _ref,
2299
+ onKeyDown: _onKeyDown,
2300
+ ...restListBoxProps
2301
+ } = listBoxAria.listBoxProps as Record<string, unknown>;
2302
+ return restListBoxProps;
2303
+ };
2304
+ const cleanFocusProps = () => {
2305
+ const { ref: _ref, ...restFocusProps } = focusProps as Record<string, unknown>;
2306
+ return restFocusProps;
2307
+ };
2308
+
2309
+ return (
2310
+ <ColorSwatchPickerContextInternal.Provider
2311
+ value={{
2312
+ state,
2313
+ registerItem,
2314
+ unregisterItem,
2315
+ }}
2316
+ >
2317
+ <div
2318
+ {...mergeProps(domProps(), cleanListBoxProps(), cleanFocusProps(), {
2319
+ onKeyDown: onColorSwatchPickerKeyDown,
2320
+ })}
2321
+ class={renderProps.class()}
2322
+ style={renderProps.style()}
2323
+ slot={local.slot ?? undefined}
2324
+ data-focused={state.isFocused() || undefined}
2325
+ data-focus-visible={isFocusVisible() || undefined}
2326
+ data-layout={local.layout ?? "grid"}
2327
+ >
2328
+ {local.children}
2329
+ </div>
2330
+ </ColorSwatchPickerContextInternal.Provider>
2331
+ );
2332
+ }
2333
+
2334
+ export function ColorSwatchPickerItem(props: ColorSwatchPickerItemProps): JSX.Element {
2335
+ const context = useContext(ColorSwatchPickerContextInternal);
2336
+ if (!context) {
2337
+ throw new Error("ColorSwatchPickerItem must be used within a ColorSwatchPicker");
2338
+ }
2339
+
2340
+ const [local, ariaProps, rest] = splitProps(
2341
+ props,
2342
+ ["children", "class", "style", "slot", "color"],
2343
+ ["isDisabled", "aria-label"],
2344
+ );
2345
+
2346
+ const color = createMemo(() => normalizeColor(local.color));
2347
+ const key = createMemo(() => color().toString("hexa"));
2348
+ const textValue = createMemo(() => {
2349
+ const locale = globalThis.navigator?.language ?? "en-US";
2350
+ return color().getColorName(locale);
2351
+ });
2352
+
2353
+ createEffect(() => {
2354
+ const itemKey = key();
2355
+ context.registerItem({
2356
+ key: itemKey,
2357
+ color: color(),
2358
+ textValue: textValue(),
2359
+ isDisabled: ariaProps.isDisabled,
2360
+ });
2361
+ onCleanup(() => context.unregisterItem(itemKey));
2362
+ });
2363
+
2364
+ const optionAria = createOption(
2365
+ () => ({
2366
+ key: key(),
2367
+ isDisabled: ariaProps.isDisabled,
2368
+ "aria-label": ariaProps["aria-label"] ?? textValue(),
2369
+ }),
2370
+ context.state,
2371
+ );
2372
+
2373
+ const renderValues = createMemo<ColorSwatchPickerItemRenderProps>(() => ({
2374
+ isSelected: optionAria.isSelected(),
2375
+ isFocused: optionAria.isFocused(),
2376
+ isFocusVisible: optionAria.isFocusVisible(),
2377
+ isPressed: optionAria.isPressed(),
2378
+ isDisabled: optionAria.isDisabled(),
2379
+ color: color(),
2380
+ }));
2381
+
2382
+ const renderProps = useRenderProps(
2383
+ {
2384
+ children: local.children,
2385
+ class: local.class,
2386
+ style: local.style,
2387
+ defaultClassName: "solidaria-ColorSwatchPickerItem",
2388
+ },
2389
+ renderValues,
2390
+ );
2391
+
2392
+ const domProps = createMemo(() =>
2393
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
2394
+ );
2395
+ const cleanOptionProps = () => {
2396
+ const { ref: _ref, ...restOptionProps } = optionAria.optionProps as Record<string, unknown>;
2397
+ return restOptionProps;
2398
+ };
2399
+
1360
2400
  return (
1361
2401
  <div
1362
- {...domProps()}
1363
- {...cleanSwatchProps()}
2402
+ {...mergeProps(domProps(), cleanOptionProps())}
1364
2403
  class={renderProps.class()}
1365
- style={mergedStyle()}
2404
+ style={renderProps.style()}
1366
2405
  >
1367
- {renderProps.renderChildren()}
2406
+ <ColorSwatchContextInternal.Provider value={{ color: color() }}>
2407
+ {renderProps.children ? renderProps.renderChildren() : <ColorSwatch />}
2408
+ </ColorSwatchContextInternal.Provider>
1368
2409
  </div>
1369
2410
  );
1370
2411
  }