@proyecto-viviana/solid-stately 0.2.4 → 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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -283
  3. package/dist/autocomplete/createAutocompleteState.d.ts +3 -2
  4. package/dist/autocomplete/index.d.ts +1 -1
  5. package/dist/calendar/createCalendarState.d.ts +22 -7
  6. package/dist/calendar/createDateFieldState.d.ts +30 -7
  7. package/dist/calendar/createDatePickerState.d.ts +60 -0
  8. package/dist/calendar/createRangeCalendarState.d.ts +27 -8
  9. package/dist/calendar/createTimeFieldState.d.ts +34 -7
  10. package/dist/calendar/index.d.ts +7 -6
  11. package/dist/checkbox/createCheckboxGroupState.d.ts +12 -3
  12. package/dist/checkbox/index.d.ts +1 -1
  13. package/dist/collections/ListCollection.d.ts +1 -1
  14. package/dist/collections/createListState.d.ts +8 -8
  15. package/dist/collections/createMenuState.d.ts +7 -7
  16. package/dist/collections/createSelectionState.d.ts +5 -5
  17. package/dist/collections/index.d.ts +5 -5
  18. package/dist/collections/types.d.ts +23 -12
  19. package/dist/color/Color.d.ts +1 -1
  20. package/dist/color/createColorAreaState.d.ts +4 -2
  21. package/dist/color/createColorFieldState.d.ts +38 -12
  22. package/dist/color/createColorSliderState.d.ts +9 -2
  23. package/dist/color/createColorWheelState.d.ts +13 -2
  24. package/dist/color/getColorChannels.d.ts +20 -0
  25. package/dist/color/index.d.ts +6 -6
  26. package/dist/color/types.d.ts +4 -4
  27. package/dist/combobox/createComboBoxState.d.ts +27 -13
  28. package/dist/combobox/index.d.ts +1 -1
  29. package/dist/data/createAsyncList.d.ts +111 -0
  30. package/dist/data/createListData.d.ts +65 -0
  31. package/dist/data/createTreeData.d.ts +65 -0
  32. package/dist/data/index.d.ts +3 -0
  33. package/dist/datepicker/index.d.ts +8 -0
  34. package/dist/disclosure/createDisclosureState.d.ts +2 -2
  35. package/dist/disclosure/index.d.ts +1 -1
  36. package/dist/dnd/createDragState.d.ts +2 -2
  37. package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
  38. package/dist/dnd/createDropState.d.ts +2 -2
  39. package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
  40. package/dist/dnd/index.d.ts +5 -5
  41. package/dist/dnd/types.d.ts +16 -16
  42. package/dist/form/createFormValidationState.d.ts +3 -3
  43. package/dist/form/index.d.ts +1 -1
  44. package/dist/grid/createGridState.d.ts +2 -2
  45. package/dist/grid/index.d.ts +2 -2
  46. package/dist/grid/types.d.ts +17 -11
  47. package/dist/index.d.ts +31 -25
  48. package/dist/index.js +7990 -6264
  49. package/dist/index.js.map +1 -7
  50. package/dist/menu/index.d.ts +8 -0
  51. package/dist/numberfield/createNumberFieldState.d.ts +2 -2
  52. package/dist/numberfield/index.d.ts +1 -1
  53. package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
  54. package/dist/overlays/index.d.ts +1 -1
  55. package/dist/radio/createRadioGroupState.d.ts +13 -4
  56. package/dist/radio/index.d.ts +1 -1
  57. package/dist/searchfield/createSearchFieldState.d.ts +2 -2
  58. package/dist/searchfield/index.d.ts +2 -2
  59. package/dist/select/createSelectState.d.ts +20 -3
  60. package/dist/select/index.d.ts +1 -1
  61. package/dist/selection/index.d.ts +11 -0
  62. package/dist/slider/createSliderState.d.ts +3 -3
  63. package/dist/slider/index.d.ts +2 -2
  64. package/dist/steplist/createStepListState.d.ts +48 -0
  65. package/dist/steplist/index.d.ts +1 -0
  66. package/dist/table/TableCollection.d.ts +3 -3
  67. package/dist/table/createTableColumnResizeState.d.ts +54 -0
  68. package/dist/table/createTableState.d.ts +2 -2
  69. package/dist/table/index.d.ts +5 -3
  70. package/dist/table/types.d.ts +10 -10
  71. package/dist/tabs/createTabListState.d.ts +5 -5
  72. package/dist/tabs/index.d.ts +1 -1
  73. package/dist/textfield/createTextFieldState.d.ts +2 -2
  74. package/dist/textfield/index.d.ts +1 -1
  75. package/dist/toast/createToastState.d.ts +15 -3
  76. package/dist/toast/index.d.ts +1 -1
  77. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  78. package/dist/toggle/createToggleState.d.ts +2 -2
  79. package/dist/toggle/index.d.ts +2 -1
  80. package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
  81. package/dist/tooltip/index.d.ts +1 -1
  82. package/dist/tree/TreeCollection.d.ts +5 -4
  83. package/dist/tree/createTreeState.d.ts +2 -2
  84. package/dist/tree/index.d.ts +3 -3
  85. package/dist/tree/types.d.ts +19 -13
  86. package/dist/utils/index.d.ts +1 -1
  87. package/dist/utils/reactivity.d.ts +1 -1
  88. package/package.json +30 -30
  89. package/src/autocomplete/createAutocompleteState.d.ts +19 -17
  90. package/src/autocomplete/createAutocompleteState.ts +25 -32
  91. package/src/autocomplete/index.d.ts +6 -2
  92. package/src/autocomplete/index.ts +1 -1
  93. package/src/calendar/createCalendarState.d.ts +139 -117
  94. package/src/calendar/createCalendarState.ts +221 -74
  95. package/src/calendar/createDateFieldState.d.ts +130 -95
  96. package/src/calendar/createDateFieldState.ts +286 -151
  97. package/src/calendar/createDatePickerState.ts +347 -0
  98. package/src/calendar/createRangeCalendarState.d.ts +154 -129
  99. package/src/calendar/createRangeCalendarState.ts +293 -79
  100. package/src/calendar/createTimeFieldState.d.ts +81 -79
  101. package/src/calendar/createTimeFieldState.ts +187 -108
  102. package/src/calendar/index.d.ts +80 -7
  103. package/src/calendar/index.ts +21 -12
  104. package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
  105. package/src/checkbox/createCheckboxGroupState.ts +57 -30
  106. package/src/checkbox/index.d.ts +6 -2
  107. package/src/checkbox/index.ts +1 -1
  108. package/src/collections/ListCollection.d.ts +25 -22
  109. package/src/collections/ListCollection.ts +152 -146
  110. package/src/collections/createListState.d.ts +73 -58
  111. package/src/collections/createListState.ts +267 -264
  112. package/src/collections/createMenuState.d.ts +36 -31
  113. package/src/collections/createMenuState.ts +112 -106
  114. package/src/collections/createSelectionState.d.ts +78 -65
  115. package/src/collections/createSelectionState.ts +340 -336
  116. package/src/collections/index.d.ts +38 -6
  117. package/src/collections/index.ts +41 -46
  118. package/src/collections/types.d.ts +101 -101
  119. package/src/collections/types.ts +181 -169
  120. package/src/color/Color.d.ts +20 -5
  121. package/src/color/Color.ts +1054 -951
  122. package/src/color/createColorAreaState.d.ts +67 -65
  123. package/src/color/createColorAreaState.ts +301 -293
  124. package/src/color/createColorFieldState.d.ts +46 -44
  125. package/src/color/createColorFieldState.ts +428 -292
  126. package/src/color/createColorSliderState.d.ts +58 -56
  127. package/src/color/createColorSliderState.ts +272 -241
  128. package/src/color/createColorWheelState.d.ts +42 -40
  129. package/src/color/createColorWheelState.ts +248 -211
  130. package/src/color/getColorChannels.ts +36 -0
  131. package/src/color/index.d.ts +36 -7
  132. package/src/color/index.ts +44 -47
  133. package/src/color/types.d.ts +68 -63
  134. package/src/color/types.ts +119 -127
  135. package/src/combobox/createComboBoxState.d.ts +107 -105
  136. package/src/combobox/createComboBoxState.ts +790 -703
  137. package/src/combobox/index.d.ts +10 -2
  138. package/src/combobox/index.ts +13 -13
  139. package/src/data/createAsyncList.ts +403 -0
  140. package/src/data/createListData.ts +298 -0
  141. package/src/data/createTreeData.ts +646 -0
  142. package/src/data/index.ts +25 -0
  143. package/src/datepicker/index.ts +32 -0
  144. package/src/disclosure/createDisclosureState.d.ts +47 -43
  145. package/src/disclosure/createDisclosureState.ts +9 -23
  146. package/src/disclosure/index.d.ts +10 -2
  147. package/src/disclosure/index.ts +1 -1
  148. package/src/dnd/createDragState.d.ts +51 -42
  149. package/src/dnd/createDragState.ts +151 -153
  150. package/src/dnd/createDraggableCollectionState.d.ts +52 -41
  151. package/src/dnd/createDraggableCollectionState.ts +158 -165
  152. package/src/dnd/createDropState.d.ts +64 -45
  153. package/src/dnd/createDropState.ts +202 -212
  154. package/src/dnd/createDroppableCollectionState.d.ts +88 -59
  155. package/src/dnd/createDroppableCollectionState.ts +354 -357
  156. package/src/dnd/index.d.ts +54 -6
  157. package/src/dnd/index.ts +63 -76
  158. package/src/dnd/types.d.ts +167 -156
  159. package/src/dnd/types.ts +316 -317
  160. package/src/form/createFormValidationState.d.ts +47 -45
  161. package/src/form/createFormValidationState.ts +356 -389
  162. package/src/form/index.d.ts +16 -2
  163. package/src/form/index.ts +15 -15
  164. package/src/grid/createGridState.d.ts +7 -4
  165. package/src/grid/createGridState.ts +44 -51
  166. package/src/grid/index.d.ts +3 -3
  167. package/src/grid/index.ts +2 -8
  168. package/src/grid/types.d.ts +136 -129
  169. package/src/grid/types.ts +24 -17
  170. package/src/index.d.ts +310 -26
  171. package/src/index.ts +87 -79
  172. package/src/menu/index.ts +17 -0
  173. package/src/numberfield/createNumberFieldState.d.ts +56 -54
  174. package/src/numberfield/createNumberFieldState.ts +422 -383
  175. package/src/numberfield/index.d.ts +6 -2
  176. package/src/numberfield/index.ts +5 -5
  177. package/src/overlays/createOverlayTriggerState.d.ts +22 -20
  178. package/src/overlays/createOverlayTriggerState.ts +66 -67
  179. package/src/overlays/index.d.ts +6 -2
  180. package/src/overlays/index.ts +5 -5
  181. package/src/radio/createRadioGroupState.d.ts +54 -52
  182. package/src/radio/createRadioGroupState.ts +66 -30
  183. package/src/radio/index.d.ts +7 -2
  184. package/src/radio/index.ts +1 -1
  185. package/src/searchfield/createSearchFieldState.d.ts +16 -14
  186. package/src/searchfield/createSearchFieldState.ts +56 -62
  187. package/src/searchfield/index.d.ts +3 -3
  188. package/src/searchfield/index.ts +2 -5
  189. package/src/select/createSelectState.d.ts +63 -61
  190. package/src/select/createSelectState.ts +290 -181
  191. package/src/select/index.d.ts +2 -2
  192. package/src/select/index.ts +1 -5
  193. package/src/selection/index.ts +27 -0
  194. package/src/slider/createSliderState.d.ts +60 -60
  195. package/src/slider/createSliderState.ts +233 -211
  196. package/src/slider/index.d.ts +3 -3
  197. package/src/slider/index.ts +2 -6
  198. package/src/ssr/index.d.ts +1 -1
  199. package/src/ssr/index.ts +2 -2
  200. package/src/steplist/createStepListState.ts +191 -0
  201. package/src/steplist/index.ts +5 -0
  202. package/src/table/TableCollection.d.ts +42 -40
  203. package/src/table/TableCollection.ts +61 -64
  204. package/src/table/createTableColumnResizeState.ts +242 -0
  205. package/src/table/createTableState.d.ts +7 -4
  206. package/src/table/createTableState.ts +10 -7
  207. package/src/table/index.d.ts +14 -4
  208. package/src/table/index.ts +10 -3
  209. package/src/table/types.d.ts +98 -92
  210. package/src/table/types.ts +17 -13
  211. package/src/tabs/createTabListState.d.ts +59 -57
  212. package/src/tabs/createTabListState.ts +58 -39
  213. package/src/tabs/index.d.ts +8 -2
  214. package/src/tabs/index.ts +1 -1
  215. package/src/textfield/createTextFieldState.d.ts +16 -14
  216. package/src/textfield/createTextFieldState.ts +7 -18
  217. package/src/textfield/index.d.ts +6 -2
  218. package/src/textfield/index.ts +1 -1
  219. package/src/toast/createToastState.d.ts +92 -81
  220. package/src/toast/createToastState.ts +56 -44
  221. package/src/toast/index.d.ts +12 -2
  222. package/src/toast/index.ts +1 -1
  223. package/src/toggle/createToggleGroupState.ts +117 -0
  224. package/src/toggle/createToggleState.d.ts +19 -19
  225. package/src/toggle/createToggleState.ts +2 -16
  226. package/src/toggle/index.d.ts +2 -2
  227. package/src/toggle/index.ts +6 -4
  228. package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
  229. package/src/tooltip/createTooltipTriggerState.ts +184 -183
  230. package/src/tooltip/index.d.ts +7 -2
  231. package/src/tooltip/index.ts +6 -6
  232. package/src/tree/TreeCollection.d.ts +26 -23
  233. package/src/tree/TreeCollection.ts +215 -175
  234. package/src/tree/createTreeState.d.ts +7 -4
  235. package/src/tree/createTreeState.ts +390 -392
  236. package/src/tree/index.d.ts +10 -4
  237. package/src/tree/index.ts +13 -13
  238. package/src/tree/types.d.ts +131 -126
  239. package/src/tree/types.ts +183 -174
  240. package/src/utils/index.d.ts +2 -2
  241. package/src/utils/index.ts +1 -1
  242. package/src/utils/reactivity.d.ts +2 -2
  243. package/src/utils/reactivity.ts +3 -3
@@ -1,292 +1,428 @@
1
- /**
2
- * ColorField state management.
3
- * Based on @react-stately/color useColorFieldState.
4
- */
5
-
6
- import { createSignal, createMemo, type Accessor } from 'solid-js';
7
- import type { Color, ColorChannel, ColorFormat } from './types';
8
- import { normalizeColor, parseColor } from './Color';
9
-
10
- export interface ColorFieldStateOptions {
11
- /** The current color value (controlled). */
12
- value?: Color | string | null;
13
- /** The default color value (uncontrolled). */
14
- defaultValue?: Color | string;
15
- /** Handler called when the color changes. */
16
- onChange?: (color: Color | null) => void;
17
- /** The color channel to edit (for single channel mode). */
18
- channel?: ColorChannel;
19
- /** The color format for parsing/displaying. */
20
- colorFormat?: ColorFormat;
21
- /** Whether the field is disabled. */
22
- isDisabled?: boolean;
23
- /** Whether the field is read-only. */
24
- isReadOnly?: boolean;
25
- }
26
-
27
- export interface ColorFieldState {
28
- /** The current color value (null if invalid). */
29
- readonly value: Color | null;
30
- /** The current input text. */
31
- readonly inputValue: string;
32
- /** Whether the input is invalid. */
33
- readonly isInvalid: boolean;
34
- /** Whether the field is disabled. */
35
- readonly isDisabled: boolean;
36
- /** Whether the field is read-only. */
37
- readonly isReadOnly: boolean;
38
- /** The color channel being edited (if single channel mode). */
39
- readonly channel: ColorChannel | undefined;
40
-
41
- /** Set the input text value. */
42
- setInputValue(value: string): void;
43
- /** Commit the current input value. */
44
- commit(): void;
45
- /** Increment the color channel value. */
46
- increment(): void;
47
- /** Decrement the color channel value. */
48
- decrement(): void;
49
- /** Increment by page size. */
50
- incrementToMax(): void;
51
- /** Decrement to minimum. */
52
- decrementToMin(): void;
53
- /** Validate the current input. */
54
- validate(): boolean;
55
- }
56
-
57
- /**
58
- * Creates state for a color field (text input for color values).
59
- */
60
- export function createColorFieldState(
61
- options: Accessor<ColorFieldStateOptions>
62
- ): ColorFieldState {
63
- const getOptions = () => options();
64
-
65
- // Internal value state
66
- const [internalValue, setInternalValue] = createSignal<Color | null>(null);
67
- const [inputValue, setInputValueInternal] = createSignal('');
68
- const [isInvalid, setIsInvalid] = createSignal(false);
69
-
70
- // Initialize internal value
71
- const initValue = () => {
72
- const opts = getOptions();
73
- if (opts.defaultValue) {
74
- const color = normalizeColor(opts.defaultValue);
75
- return color;
76
- }
77
- return null;
78
- };
79
-
80
- // Set initial value
81
- if (internalValue() === null) {
82
- const init = initValue();
83
- if (init) {
84
- setInternalValue(init);
85
- setInputValueInternal(formatColorValue(init, getOptions().channel, getOptions().colorFormat));
86
- }
87
- }
88
-
89
- // Controlled vs uncontrolled value
90
- const value = createMemo(() => {
91
- const opts = getOptions();
92
- if (opts.value !== undefined) {
93
- if (opts.value === null) return null;
94
- return normalizeColor(opts.value);
95
- }
96
- return internalValue();
97
- });
98
-
99
- const channel = createMemo(() => getOptions().channel);
100
- const isDisabled = createMemo(() => getOptions().isDisabled ?? false);
101
- const isReadOnly = createMemo(() => getOptions().isReadOnly ?? false);
102
-
103
- // Format color value for display
104
- function formatColorValue(
105
- color: Color | null,
106
- chan?: ColorChannel,
107
- format?: ColorFormat
108
- ): string {
109
- if (!color) return '';
110
-
111
- // Single channel mode
112
- if (chan) {
113
- return String(color.getChannelValue(chan));
114
- }
115
-
116
- // Full color mode
117
- return color.toString(format ?? 'hex');
118
- }
119
-
120
- // Update value
121
- const updateValue = (newColor: Color | null) => {
122
- const opts = getOptions();
123
-
124
- // Controlled mode
125
- if (opts.value !== undefined) {
126
- opts.onChange?.(newColor);
127
- return;
128
- }
129
-
130
- // Uncontrolled mode
131
- setInternalValue(newColor);
132
- opts.onChange?.(newColor);
133
- };
134
-
135
- // Set input value
136
- const setInputValue = (text: string) => {
137
- setInputValueInternal(text);
138
- setIsInvalid(false);
139
- };
140
-
141
- // Commit the input value
142
- const commit = () => {
143
- const text = inputValue().trim();
144
- const opts = getOptions();
145
- const chan = channel();
146
-
147
- if (!text) {
148
- updateValue(null);
149
- setIsInvalid(false);
150
- return;
151
- }
152
-
153
- try {
154
- let newColor: Color;
155
-
156
- if (chan) {
157
- // Single channel mode - parse as number and update channel
158
- const numValue = parseFloat(text);
159
- if (isNaN(numValue)) {
160
- setIsInvalid(true);
161
- return;
162
- }
163
-
164
- const currentColor = value();
165
- if (!currentColor) {
166
- setIsInvalid(true);
167
- return;
168
- }
169
-
170
- const range = currentColor.getChannelRange(chan);
171
- const clamped = Math.max(range.minValue, Math.min(range.maxValue, numValue));
172
- newColor = currentColor.withChannelValue(chan, clamped);
173
- } else {
174
- // Full color mode - parse the color string
175
- newColor = parseColor(text);
176
- if (opts.colorFormat) {
177
- newColor = newColor.toFormat(opts.colorFormat);
178
- }
179
- }
180
-
181
- updateValue(newColor);
182
- setInputValueInternal(formatColorValue(newColor, chan, opts.colorFormat));
183
- setIsInvalid(false);
184
- } catch {
185
- setIsInvalid(true);
186
- }
187
- };
188
-
189
- // Increment channel value
190
- const increment = () => {
191
- const chan = channel();
192
- const currentColor = value();
193
- if (!currentColor || !chan) return;
194
-
195
- const currentVal = currentColor.getChannelValue(chan);
196
- const range = currentColor.getChannelRange(chan);
197
- const newVal = Math.min(range.maxValue, currentVal + range.step);
198
- const newColor = currentColor.withChannelValue(chan, newVal);
199
-
200
- updateValue(newColor);
201
- setInputValueInternal(formatColorValue(newColor, chan, getOptions().colorFormat));
202
- };
203
-
204
- // Decrement channel value
205
- const decrement = () => {
206
- const chan = channel();
207
- const currentColor = value();
208
- if (!currentColor || !chan) return;
209
-
210
- const currentVal = currentColor.getChannelValue(chan);
211
- const range = currentColor.getChannelRange(chan);
212
- const newVal = Math.max(range.minValue, currentVal - range.step);
213
- const newColor = currentColor.withChannelValue(chan, newVal);
214
-
215
- updateValue(newColor);
216
- setInputValueInternal(formatColorValue(newColor, chan, getOptions().colorFormat));
217
- };
218
-
219
- // Increment to max
220
- const incrementToMax = () => {
221
- const chan = channel();
222
- const currentColor = value();
223
- if (!currentColor || !chan) return;
224
-
225
- const range = currentColor.getChannelRange(chan);
226
- const newColor = currentColor.withChannelValue(chan, range.maxValue);
227
-
228
- updateValue(newColor);
229
- setInputValueInternal(formatColorValue(newColor, chan, getOptions().colorFormat));
230
- };
231
-
232
- // Decrement to min
233
- const decrementToMin = () => {
234
- const chan = channel();
235
- const currentColor = value();
236
- if (!currentColor || !chan) return;
237
-
238
- const range = currentColor.getChannelRange(chan);
239
- const newColor = currentColor.withChannelValue(chan, range.minValue);
240
-
241
- updateValue(newColor);
242
- setInputValueInternal(formatColorValue(newColor, chan, getOptions().colorFormat));
243
- };
244
-
245
- // Validate input
246
- const validate = () => {
247
- const text = inputValue().trim();
248
- const chan = channel();
249
-
250
- if (!text) return true;
251
-
252
- try {
253
- if (chan) {
254
- const numValue = parseFloat(text);
255
- return !isNaN(numValue);
256
- } else {
257
- parseColor(text);
258
- return true;
259
- }
260
- } catch {
261
- return false;
262
- }
263
- };
264
-
265
- return {
266
- get value() {
267
- return value();
268
- },
269
- get inputValue() {
270
- return inputValue();
271
- },
272
- get isInvalid() {
273
- return isInvalid();
274
- },
275
- get isDisabled() {
276
- return isDisabled();
277
- },
278
- get isReadOnly() {
279
- return isReadOnly();
280
- },
281
- get channel() {
282
- return channel();
283
- },
284
- setInputValue,
285
- commit,
286
- increment,
287
- decrement,
288
- incrementToMax,
289
- decrementToMin,
290
- validate,
291
- };
292
- }
1
+ /**
2
+ * ColorField state management.
3
+ * Based on @react-stately/color useColorFieldState and useColorChannelFieldState.
4
+ */
5
+
6
+ import { createEffect, createMemo, createSignal, type Accessor } from "solid-js";
7
+ import type { Color, ColorChannel, ColorChannelRange, ColorFormat, ColorSpace } from "./types";
8
+ import { createRGBColor, normalizeColor, parseColor } from "./Color";
9
+
10
+ const HEX_COLOR_MAX = 0xffffff;
11
+ const PARTIAL_HEX_PATTERN = /^#?[0-9a-f]{0,6}$/i;
12
+
13
+ export interface ColorFieldStateOptions {
14
+ /** The current color value (controlled). */
15
+ value?: Color | string | null;
16
+ /** The default color value (uncontrolled). */
17
+ defaultValue?: Color | string | null;
18
+ /** Handler called when the color changes. */
19
+ onChange?: (color: Color | null) => void;
20
+ /** The color channel to edit (for single channel mode). */
21
+ channel?: ColorChannel;
22
+ /** The color space to use for channel mode. */
23
+ colorSpace?: ColorSpace;
24
+ /** The color format for parsing/displaying in hex mode. */
25
+ colorFormat?: ColorFormat;
26
+ /** Locale used to format channel values. */
27
+ locale?: string;
28
+ /** Whether the field is disabled. */
29
+ isDisabled?: boolean;
30
+ /** Whether the field is read-only. */
31
+ isReadOnly?: boolean;
32
+ /** Whether the field is invalid. */
33
+ isInvalid?: boolean;
34
+ /** Whether the field is required. */
35
+ isRequired?: boolean;
36
+ }
37
+
38
+ export interface ColorFieldState {
39
+ /** The current color value (null if empty). */
40
+ readonly value: Color | null;
41
+ /** The color used for channel calculations. */
42
+ readonly colorValue: Color;
43
+ /** The current input text. */
44
+ readonly inputValue: string;
45
+ /** The numeric value exposed by channel fields. */
46
+ readonly numberValue: number;
47
+ /** The channel minimum value after formatting normalization. */
48
+ readonly minValue: number;
49
+ /** The channel maximum value after formatting normalization. */
50
+ readonly maxValue: number;
51
+ /** The channel keyboard step after formatting normalization. */
52
+ readonly step: number;
53
+ /** The channel page step after formatting normalization. */
54
+ readonly pageSize: number;
55
+ /** Number formatter options for channel fields. */
56
+ readonly formatOptions: Intl.NumberFormatOptions;
57
+ /** Whether the input is invalid. */
58
+ readonly isInvalid: boolean;
59
+ /** Whether the field is disabled. */
60
+ readonly isDisabled: boolean;
61
+ /** Whether the field is read-only. */
62
+ readonly isReadOnly: boolean;
63
+ /** Whether the field is required. */
64
+ readonly isRequired: boolean;
65
+ /** The color channel being edited (if single channel mode). */
66
+ readonly channel: ColorChannel | undefined;
67
+
68
+ /** Set the input text value. */
69
+ setInputValue(value: string): void;
70
+ /** Set the color value. */
71
+ setColorValue(value: Color | null): void;
72
+ /** Commit the current input value. */
73
+ commit(): void;
74
+ /** Increment the color value. */
75
+ increment(): void;
76
+ /** Decrement the color value. */
77
+ decrement(): void;
78
+ /** Increment by page size or to the maximum. */
79
+ incrementToMax(): void;
80
+ /** Decrement by page size or to the minimum. */
81
+ decrementToMin(): void;
82
+ /** Validate the current input or a candidate input. */
83
+ validate(value?: string): boolean;
84
+ }
85
+
86
+ function normalizeNullableColor(value: Color | string | null | undefined): Color | null {
87
+ if (value == null) {
88
+ return null;
89
+ }
90
+ return normalizeColor(value);
91
+ }
92
+
93
+ function getDefaultValue(options: ColorFieldStateOptions): Color | null {
94
+ if (options.defaultValue !== undefined) {
95
+ return normalizeNullableColor(options.defaultValue);
96
+ }
97
+
98
+ if (options.value !== undefined) {
99
+ return normalizeNullableColor(options.value);
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ function convertColor(color: Color, colorSpace: ColorSpace | undefined): Color {
106
+ return colorSpace ? color.toFormat(colorSpace) : color;
107
+ }
108
+
109
+ function getDisplayColor(color: Color | null, colorSpace: ColorSpace | undefined): Color {
110
+ return convertColor(color ?? createRGBColor(0, 0, 0), colorSpace);
111
+ }
112
+
113
+ function getChannelMultiplier(color: Color, channel: ColorChannel): number {
114
+ const range = color.getChannelRange(channel);
115
+ const formatOptions = color.getChannelFormatOptions(channel);
116
+ return formatOptions.style === "percent" && range.maxValue === 100 ? 100 : 1;
117
+ }
118
+
119
+ function getNormalizedRange(color: Color, channel: ColorChannel): ColorChannelRange {
120
+ const multiplier = getChannelMultiplier(color, channel);
121
+ const range = color.getChannelRange(channel);
122
+
123
+ return {
124
+ minValue: range.minValue / multiplier,
125
+ maxValue: range.maxValue / multiplier,
126
+ step: range.step / multiplier,
127
+ pageSize: range.pageSize / multiplier,
128
+ };
129
+ }
130
+
131
+ function formatColorValue(
132
+ color: Color | null,
133
+ channel: ColorChannel | undefined,
134
+ colorSpace: ColorSpace | undefined,
135
+ format: ColorFormat | undefined,
136
+ locale: string,
137
+ ): string {
138
+ if (!color) {
139
+ return "";
140
+ }
141
+
142
+ const displayColor = getDisplayColor(color, colorSpace);
143
+
144
+ if (channel) {
145
+ return displayColor.formatChannelValue(channel, locale);
146
+ }
147
+
148
+ return displayColor.toString(format ?? "hex");
149
+ }
150
+
151
+ function normalizeHexInput(value: string): string {
152
+ return value.startsWith("#") ? value : `#${value}`;
153
+ }
154
+
155
+ function parseHexValue(value: string, format: ColorFormat | undefined): Color {
156
+ const color = parseColor(normalizeHexInput(value));
157
+ return format ? color.toFormat(format) : color;
158
+ }
159
+
160
+ function parseChannelValue(text: string, color: Color, channel: ColorChannel): number | null {
161
+ const trimmed = text.trim();
162
+ const hasPercent = trimmed.endsWith("%");
163
+ const parsed = Number.parseFloat(hasPercent ? trimmed.slice(0, -1) : trimmed);
164
+
165
+ if (Number.isNaN(parsed)) {
166
+ return null;
167
+ }
168
+
169
+ const range = color.getChannelRange(channel);
170
+ const formatOptions = color.getChannelFormatOptions(channel);
171
+ const multiplier = getChannelMultiplier(color, channel);
172
+ const rawValue = hasPercent && formatOptions.style === "percent" ? parsed : parsed * multiplier;
173
+
174
+ return Math.min(range.maxValue, Math.max(range.minValue, rawValue));
175
+ }
176
+
177
+ function hexIntToColor(value: number): Color {
178
+ const clamped = Math.min(HEX_COLOR_MAX, Math.max(0, Math.round(value)));
179
+ const red = (clamped >> 16) & 0xff;
180
+ const green = (clamped >> 8) & 0xff;
181
+ const blue = clamped & 0xff;
182
+ return createRGBColor(red, green, blue);
183
+ }
184
+
185
+ /**
186
+ * Creates state for a color field (text input for color values).
187
+ */
188
+ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>): ColorFieldState {
189
+ const getOptions = () => options();
190
+ const initialOptions = getOptions();
191
+ const initialValue = getDefaultValue(initialOptions);
192
+
193
+ const [internalValue, setInternalValue] = createSignal<Color | null>(initialValue);
194
+ const [inputValue, setInputValueInternal] = createSignal(
195
+ formatColorValue(
196
+ initialValue,
197
+ initialOptions.channel,
198
+ initialOptions.colorSpace,
199
+ initialOptions.colorFormat,
200
+ initialOptions.locale ?? "en-US",
201
+ ),
202
+ );
203
+ const [invalidInput, setInvalidInput] = createSignal(false);
204
+
205
+ const channel = createMemo(() => getOptions().channel);
206
+ const colorSpace = createMemo(() => getOptions().colorSpace);
207
+ const locale = createMemo(() => getOptions().locale ?? "en-US");
208
+
209
+ const value = createMemo(() => {
210
+ const opts = getOptions();
211
+ if (opts.value !== undefined) {
212
+ return normalizeNullableColor(opts.value);
213
+ }
214
+ return internalValue();
215
+ });
216
+
217
+ const colorValue = createMemo(() => getDisplayColor(value(), colorSpace()));
218
+ const isDisabled = createMemo(() => getOptions().isDisabled ?? false);
219
+ const isReadOnly = createMemo(() => getOptions().isReadOnly ?? false);
220
+ const isRequired = createMemo(() => getOptions().isRequired ?? false);
221
+ const isInvalid = createMemo(() => (getOptions().isInvalid ?? false) || invalidInput());
222
+
223
+ const formatCurrentValue = (color: Color | null = value()) =>
224
+ formatColorValue(color, channel(), colorSpace(), getOptions().colorFormat, locale());
225
+
226
+ createEffect(() => {
227
+ setInputValueInternal(formatCurrentValue());
228
+ setInvalidInput(false);
229
+ });
230
+
231
+ const updateValue = (newColor: Color | null) => {
232
+ const opts = getOptions();
233
+
234
+ if (opts.value === undefined) {
235
+ setInternalValue(newColor);
236
+ }
237
+
238
+ opts.onChange?.(newColor);
239
+ setInputValueInternal(formatCurrentValue(newColor));
240
+ setInvalidInput(false);
241
+ };
242
+
243
+ const setInputValue = (text: string) => {
244
+ setInputValueInternal(text);
245
+ setInvalidInput(false);
246
+ };
247
+
248
+ const setColorValue = (newColor: Color | null) => {
249
+ updateValue(newColor);
250
+ };
251
+
252
+ const commit = () => {
253
+ const text = inputValue().trim();
254
+ const opts = getOptions();
255
+ const chan = channel();
256
+
257
+ if (!text) {
258
+ updateValue(null);
259
+ return;
260
+ }
261
+
262
+ if (chan) {
263
+ const displayColor = colorValue();
264
+ const rawValue = parseChannelValue(text, displayColor, chan);
265
+ if (rawValue == null) {
266
+ setInputValueInternal(formatCurrentValue());
267
+ setInvalidInput(false);
268
+ return;
269
+ }
270
+
271
+ updateValue(displayColor.withChannelValue(chan, rawValue));
272
+ return;
273
+ }
274
+
275
+ try {
276
+ updateValue(parseHexValue(text, opts.colorFormat));
277
+ } catch {
278
+ setInputValueInternal(formatCurrentValue());
279
+ setInvalidInput(false);
280
+ }
281
+ };
282
+
283
+ const incrementHex = (amount: number) => {
284
+ const currentColor = value();
285
+ if (!currentColor) {
286
+ return;
287
+ }
288
+
289
+ const newColor = hexIntToColor(currentColor.toFormat("rgb").toHexInt() + amount);
290
+ updateValue(getOptions().colorFormat ? newColor.toFormat(getOptions().colorFormat!) : newColor);
291
+ };
292
+
293
+ const updateChannel = (amount: number | "min" | "max") => {
294
+ const chan = channel();
295
+ if (!chan) {
296
+ return;
297
+ }
298
+
299
+ const displayColor = colorValue();
300
+ const range = displayColor.getChannelRange(chan);
301
+ const currentValue = displayColor.getChannelValue(chan);
302
+ const nextValue =
303
+ amount === "min"
304
+ ? range.minValue
305
+ : amount === "max"
306
+ ? range.maxValue
307
+ : Math.min(range.maxValue, Math.max(range.minValue, currentValue + amount));
308
+
309
+ updateValue(displayColor.withChannelValue(chan, nextValue));
310
+ };
311
+
312
+ const increment = () => {
313
+ const chan = channel();
314
+ if (!chan) {
315
+ incrementHex(1);
316
+ return;
317
+ }
318
+
319
+ updateChannel(colorValue().getChannelRange(chan).step);
320
+ };
321
+
322
+ const decrement = () => {
323
+ const chan = channel();
324
+ if (!chan) {
325
+ incrementHex(-1);
326
+ return;
327
+ }
328
+
329
+ updateChannel(-colorValue().getChannelRange(chan).step);
330
+ };
331
+
332
+ const incrementToMax = () => {
333
+ const chan = channel();
334
+ if (!chan) {
335
+ incrementHex(HEX_COLOR_MAX);
336
+ return;
337
+ }
338
+
339
+ updateChannel("max");
340
+ };
341
+
342
+ const decrementToMin = () => {
343
+ const chan = channel();
344
+ if (!chan) {
345
+ incrementHex(-HEX_COLOR_MAX);
346
+ return;
347
+ }
348
+
349
+ updateChannel("min");
350
+ };
351
+
352
+ const validate = (candidate?: string) => {
353
+ const text = (candidate ?? inputValue()).trim();
354
+ const chan = channel();
355
+
356
+ if (!text) {
357
+ return true;
358
+ }
359
+
360
+ if (chan) {
361
+ return parseChannelValue(text, colorValue(), chan) != null;
362
+ }
363
+
364
+ return PARTIAL_HEX_PATTERN.test(text);
365
+ };
366
+
367
+ return {
368
+ get value() {
369
+ return value();
370
+ },
371
+ get colorValue() {
372
+ return colorValue();
373
+ },
374
+ get inputValue() {
375
+ return inputValue();
376
+ },
377
+ get numberValue() {
378
+ const chan = channel();
379
+ if (!chan || value() === null) {
380
+ return Number.NaN;
381
+ }
382
+ return colorValue().getChannelValue(chan) / getChannelMultiplier(colorValue(), chan);
383
+ },
384
+ get minValue() {
385
+ const chan = channel();
386
+ return chan ? getNormalizedRange(colorValue(), chan).minValue : 0;
387
+ },
388
+ get maxValue() {
389
+ const chan = channel();
390
+ return chan ? getNormalizedRange(colorValue(), chan).maxValue : HEX_COLOR_MAX;
391
+ },
392
+ get step() {
393
+ const chan = channel();
394
+ return chan ? getNormalizedRange(colorValue(), chan).step : 1;
395
+ },
396
+ get pageSize() {
397
+ const chan = channel();
398
+ return chan ? getNormalizedRange(colorValue(), chan).pageSize : 0x100000;
399
+ },
400
+ get formatOptions() {
401
+ const chan = channel();
402
+ return chan ? colorValue().getChannelFormatOptions(chan) : {};
403
+ },
404
+ get isInvalid() {
405
+ return isInvalid();
406
+ },
407
+ get isDisabled() {
408
+ return isDisabled();
409
+ },
410
+ get isReadOnly() {
411
+ return isReadOnly();
412
+ },
413
+ get isRequired() {
414
+ return isRequired();
415
+ },
416
+ get channel() {
417
+ return channel();
418
+ },
419
+ setInputValue,
420
+ setColorValue,
421
+ commit,
422
+ increment,
423
+ decrement,
424
+ incrementToMax,
425
+ decrementToMin,
426
+ validate,
427
+ };
428
+ }