@proyecto-viviana/solid-stately 0.3.0 → 0.4.1

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 (77) hide show
  1. package/dist/calendar/IncompleteDate.d.ts +61 -0
  2. package/dist/calendar/createCalendarState.d.ts +20 -10
  3. package/dist/calendar/createDateFieldState.d.ts +65 -20
  4. package/dist/calendar/createRangeCalendarState.d.ts +20 -2
  5. package/dist/calendar/createTimeFieldState.d.ts +38 -67
  6. package/dist/calendar/index.d.ts +1 -1
  7. package/dist/calendar/intl/index.d.ts +10 -0
  8. package/dist/collections/ListCollection.d.ts +8 -0
  9. package/dist/collections/createListState.d.ts +16 -1
  10. package/dist/collections/createSelectionState.d.ts +17 -5
  11. package/dist/collections/getChildNodes.d.ts +19 -0
  12. package/dist/collections/index.d.ts +3 -2
  13. package/dist/collections/types.d.ts +9 -0
  14. package/dist/color/intl/index.d.ts +14 -0
  15. package/dist/combobox/createComboBoxState.d.ts +7 -0
  16. package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
  17. package/dist/dnd/index.d.ts +1 -1
  18. package/dist/dnd/types.d.ts +3 -1
  19. package/dist/grid/types.d.ts +17 -3
  20. package/dist/index.d.ts +6 -6
  21. package/dist/index.js +4372 -1087
  22. package/dist/index.js.map +1 -1
  23. package/dist/select/createSelectState.d.ts +6 -0
  24. package/dist/selection/Selection.d.ts +16 -0
  25. package/dist/selection/SelectionManager.d.ts +119 -0
  26. package/dist/selection/createMultipleSelectionState.d.ts +59 -0
  27. package/dist/selection/index.d.ts +1 -0
  28. package/dist/table/TableCollection.d.ts +30 -0
  29. package/dist/table/createTreeGridState.d.ts +15 -0
  30. package/dist/table/index.d.ts +2 -1
  31. package/dist/table/types.d.ts +58 -0
  32. package/dist/toast/createToastState.d.ts +16 -0
  33. package/dist/toast/index.d.ts +1 -1
  34. package/dist/tree/types.d.ts +14 -6
  35. package/package.json +1 -1
  36. package/src/calendar/IncompleteDate.ts +430 -0
  37. package/src/calendar/createCalendarState.ts +129 -40
  38. package/src/calendar/createDateFieldState.ts +633 -477
  39. package/src/calendar/createRangeCalendarState.ts +343 -30
  40. package/src/calendar/createTimeFieldState.ts +179 -467
  41. package/src/calendar/index.ts +2 -0
  42. package/src/calendar/intl/index.ts +364 -0
  43. package/src/collections/ListCollection.ts +31 -0
  44. package/src/collections/createListState.ts +139 -17
  45. package/src/collections/createMenuState.ts +3 -0
  46. package/src/collections/createSelectionState.ts +38 -28
  47. package/src/collections/getChildNodes.ts +129 -0
  48. package/src/collections/index.ts +12 -0
  49. package/src/collections/types.ts +10 -0
  50. package/src/color/Color.ts +104 -133
  51. package/src/color/intl/index.ts +1409 -0
  52. package/src/combobox/createComboBoxState.ts +38 -13
  53. package/src/dnd/createDroppableCollectionState.ts +125 -6
  54. package/src/dnd/index.d.ts +1 -0
  55. package/src/dnd/index.ts +1 -0
  56. package/src/dnd/types.d.ts +3 -1
  57. package/src/dnd/types.ts +4 -1
  58. package/src/grid/createGridState.ts +36 -2
  59. package/src/grid/types.ts +17 -3
  60. package/src/index.d.ts +1 -0
  61. package/src/index.ts +14 -0
  62. package/src/select/createSelectState.ts +15 -2
  63. package/src/selection/Selection.ts +28 -0
  64. package/src/selection/SelectionManager.ts +541 -0
  65. package/src/selection/createMultipleSelectionState.ts +247 -0
  66. package/src/selection/index.ts +6 -0
  67. package/src/steplist/createStepListState.ts +27 -4
  68. package/src/table/TableCollection.ts +213 -43
  69. package/src/table/createTableState.ts +20 -0
  70. package/src/table/createTreeGridState.ts +146 -0
  71. package/src/table/index.ts +3 -0
  72. package/src/table/types.ts +66 -0
  73. package/src/tabs/createTabListState.ts +11 -9
  74. package/src/toast/createToastState.ts +43 -10
  75. package/src/toast/index.ts +1 -0
  76. package/src/tree/createTreeState.ts +37 -6
  77. package/src/tree/types.ts +14 -6
@@ -74,11 +74,24 @@ export interface SelectionState {
74
74
  /** Extend selection to a key (for shift-click). */
75
75
  extendSelection(toKey: Key, collection: Collection): void;
76
76
  /** Select a key based on interaction. */
77
- select(
78
- key: Key,
79
- e?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean },
80
- collection?: Collection,
81
- ): void;
77
+ select(key: Key, e?: SelectionPressEvent): void;
78
+ }
79
+
80
+ /**
81
+ * The press/pointer interaction consulted by {@link SelectionState.select}.
82
+ * Mirrors react-stately's `SelectionManager.select(e)`: only `pointerType` is
83
+ * read here (touch / virtual force toggle, since those devices have no modifier
84
+ * keys). Modifier keys (ctrl / meta / shift) are resolved one layer up in the
85
+ * aria-layer `onSelect` (solidaria `selectItem`), matching upstream's split
86
+ * between react-stately and react-aria. The modifier fields are accepted so a
87
+ * full press/pointer event is assignable, but they are not read here.
88
+ */
89
+ export interface SelectionPressEvent {
90
+ pointerType?: "mouse" | "pen" | "touch" | "keyboard" | "virtual";
91
+ shiftKey?: boolean;
92
+ ctrlKey?: boolean;
93
+ metaKey?: boolean;
94
+ altKey?: boolean;
82
95
  }
83
96
 
84
97
  /**
@@ -268,33 +281,30 @@ export function createSelectionState(
268
281
  updateSelection(new Set(rangeKeys));
269
282
  };
270
283
 
271
- const select = (
272
- key: Key,
273
- e?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean },
274
- collection?: Collection,
275
- ) => {
276
- if (isDisabled(key)) return;
284
+ const select = (key: Key, e?: SelectionPressEvent) => {
285
+ // Mirrors react-stately's SelectionManager.select: only pointerType +
286
+ // selectionBehavior are consulted here. Modifier-key handling (ctrl / meta
287
+ // ⇒ toggle, shift ⇒ extend) lives one layer up in solidaria's aria-layer
288
+ // `selectItem`, which is where the platform-aware isCtrlKeyPressed is
289
+ // available. The primitives self-guard disabled keys, so no guard is
290
+ // needed here (matching upstream).
277
291
  if (selectionMode() === "none") return;
278
292
 
279
- const mode = selectionMode();
280
- const behavior = selectionBehavior();
281
-
282
- if (mode === "single") {
283
- if (behavior === "replace" || !isSelected(key)) {
293
+ if (selectionMode() === "single") {
294
+ // Single mode ignores selectionBehavior. Re-selecting the current item
295
+ // deselects it when empty selection is allowed; otherwise it stays
296
+ // selected (replace).
297
+ if (isSelected(key) && !disallowEmptySelection()) {
298
+ toggleSelection(key);
299
+ } else {
284
300
  replaceSelection(key);
285
- } else if (!disallowEmptySelection()) {
286
- clearSelection();
287
301
  }
288
- return;
289
- }
290
-
291
- // Multiple selection
292
- if (e?.shiftKey && collection) {
293
- extendSelection(key, collection);
294
- return;
295
- }
296
-
297
- if (e?.ctrlKey || e?.metaKey || behavior === "toggle") {
302
+ } else if (
303
+ selectionBehavior() === "toggle" ||
304
+ (e != null && (e.pointerType === "touch" || e.pointerType === "virtual"))
305
+ ) {
306
+ // touch / virtual (VoiceOver) have no modifier keys, so multi-select is
307
+ // only reachable by toggling.
298
308
  toggleSelection(key);
299
309
  } else {
300
310
  replaceSelection(key);
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Helpers for traversing collection nodes and comparing their document order.
3
+ *
4
+ * Based on @react-stately/collections.
5
+ */
6
+
7
+ import type { Collection, CollectionNode } from "./types";
8
+
9
+ export function getChildNodes<T>(
10
+ node: CollectionNode<T>,
11
+ collection: Collection<T>,
12
+ ): Iterable<CollectionNode<T>> {
13
+ // New API: call collection.getChildren with the node key.
14
+ if (typeof collection.getChildren === "function") {
15
+ return collection.getChildren(node.key);
16
+ }
17
+
18
+ // Old API: access childNodes directly.
19
+ return node.childNodes;
20
+ }
21
+
22
+ export function getFirstItem<T>(iterable: Iterable<T>): T | undefined {
23
+ return getNthItem(iterable, 0);
24
+ }
25
+
26
+ export function getNthItem<T>(iterable: Iterable<T>, index: number): T | undefined {
27
+ if (index < 0) {
28
+ return undefined;
29
+ }
30
+
31
+ let i = 0;
32
+ for (const item of iterable) {
33
+ if (i === index) {
34
+ return item;
35
+ }
36
+
37
+ i++;
38
+ }
39
+ }
40
+
41
+ export function getLastItem<T>(iterable: Iterable<T>): T | undefined {
42
+ let lastItem: T | undefined = undefined;
43
+ for (const value of iterable) {
44
+ lastItem = value;
45
+ }
46
+
47
+ return lastItem;
48
+ }
49
+
50
+ const itemCountCache = new WeakMap<Collection<unknown>, number>();
51
+
52
+ /**
53
+ * Counts the item nodes (recursing into sections, ignoring section headers and
54
+ * other non-item nodes) in a collection. Faithful port of
55
+ * @react-stately/collections `getItemCount`; used by createOption to populate
56
+ * `aria-setsize` when the listbox is virtualized. Cached per (immutable)
57
+ * collection identity, exactly like upstream.
58
+ */
59
+ export function getItemCount<T>(collection: Collection<T>): number {
60
+ const cached = itemCountCache.get(collection);
61
+ if (cached != null) {
62
+ return cached;
63
+ }
64
+
65
+ let counter = 0;
66
+ const countItems = (items: Iterable<CollectionNode<T>>) => {
67
+ for (const item of items) {
68
+ if (item.type === "section") {
69
+ countItems(getChildNodes(item, collection));
70
+ } else if (item.type === "item") {
71
+ counter++;
72
+ }
73
+ }
74
+ };
75
+
76
+ countItems(collection);
77
+ itemCountCache.set(collection, counter);
78
+ return counter;
79
+ }
80
+
81
+ export function compareNodeOrder<T>(
82
+ collection: Collection<T>,
83
+ a: CollectionNode<T>,
84
+ b: CollectionNode<T>,
85
+ ): number {
86
+ // If the two nodes have the same parent, compare their indices.
87
+ if (a.parentKey === b.parentKey) {
88
+ return a.index - b.index;
89
+ }
90
+
91
+ // Otherwise, collect all of the ancestors from each node, and find the first one that doesn't match starting from the root.
92
+ // Include the base nodes in case we are comparing nodes of different levels so that we can compare the higher node to the lower level node's
93
+ // ancestor of the same level
94
+ const aAncestors = [...getAncestors(collection, a), a];
95
+ const bAncestors = [...getAncestors(collection, b), b];
96
+ const firstNonMatchingAncestor = aAncestors
97
+ .slice(0, bAncestors.length)
98
+ .findIndex((a, i) => a !== bAncestors[i]);
99
+ if (firstNonMatchingAncestor !== -1) {
100
+ // Compare the indices of two children within the common ancestor.
101
+ a = aAncestors[firstNonMatchingAncestor];
102
+ b = bAncestors[firstNonMatchingAncestor];
103
+ return a.index - b.index;
104
+ }
105
+
106
+ // If there isn't a non matching ancestor, we might be in a case where one of the nodes is the ancestor of the other.
107
+ if (aAncestors.findIndex((node) => node === b) >= 0) {
108
+ return 1;
109
+ } else if (bAncestors.findIndex((node) => node === a) >= 0) {
110
+ return -1;
111
+ }
112
+
113
+ // 🤷
114
+ return -1;
115
+ }
116
+
117
+ function getAncestors<T>(collection: Collection<T>, node: CollectionNode<T>): CollectionNode<T>[] {
118
+ const parents: CollectionNode<T>[] = [];
119
+
120
+ let currNode: CollectionNode<T> | null = node;
121
+ while (currNode?.parentKey != null) {
122
+ currNode = collection.getItem(currNode.parentKey);
123
+ if (currNode) {
124
+ parents.unshift(currNode);
125
+ }
126
+ }
127
+
128
+ return parents;
129
+ }
@@ -16,17 +16,29 @@ export type {
16
16
 
17
17
  export { ListCollection, createListCollection } from "./ListCollection";
18
18
 
19
+ export {
20
+ getChildNodes,
21
+ getFirstItem,
22
+ getNthItem,
23
+ getLastItem,
24
+ getItemCount,
25
+ compareNodeOrder,
26
+ } from "./getChildNodes";
27
+
19
28
  export {
20
29
  createSelectionState,
21
30
  type SelectionStateProps,
22
31
  type SelectionState,
32
+ type SelectionPressEvent,
23
33
  } from "./createSelectionState";
24
34
 
25
35
  export {
26
36
  createListState,
37
+ createFilteredListState,
27
38
  createSingleSelectListState,
28
39
  type ListStateProps,
29
40
  type ListState,
41
+ type ListFilterFn,
30
42
  type SingleSelectListStateProps,
31
43
  type SingleSelectListState,
32
44
  } from "./createListState";
@@ -88,6 +88,16 @@ export interface Collection<T = unknown> extends Iterable<CollectionNode<T>> {
88
88
 
89
89
  /** Get the text value for a key. */
90
90
  getTextValue(key: Key): string;
91
+
92
+ /**
93
+ * Filter the collection, returning a new collection containing only the item
94
+ * nodes for which `filterFn` returns true. Sections whose children are all
95
+ * filtered out are dropped. Mirrors @react-aria/collections
96
+ * `BaseCollection.filter` (and the optional `Collection.filter` in
97
+ * @react-types/shared). Used by Autocomplete to narrow the collection as the
98
+ * user types.
99
+ */
100
+ filter?(filterFn: (nodeValue: string, node: CollectionNode<T>) => boolean): Collection<T>;
91
101
  }
92
102
 
93
103
  /**
@@ -16,6 +16,11 @@ import type {
16
16
  HSLColor,
17
17
  HSBColor,
18
18
  } from "./types";
19
+ import {
20
+ getColorStringForLocale,
21
+ formatColorMessage,
22
+ type ColorStringKey,
23
+ } from "./intl";
19
24
 
20
25
  // Channel ranges
21
26
  const RGB_CHANNEL_RANGE: ColorChannelRange = {
@@ -46,18 +51,6 @@ const PERCENT_CHANNEL_RANGE: ColorChannelRange = {
46
51
  pageSize: 10,
47
52
  };
48
53
 
49
- // Channel names (English only for now)
50
- const CHANNEL_NAMES: Record<ColorChannel, string> = {
51
- hue: "Hue",
52
- saturation: "Saturation",
53
- brightness: "Brightness",
54
- lightness: "Lightness",
55
- red: "Red",
56
- green: "Green",
57
- blue: "Blue",
58
- alpha: "Alpha",
59
- };
60
-
61
54
  // React Stately uses OKLCH for color names so that lightness is perceptually
62
55
  // consistent across hues.
63
56
  const ORANGE_LIGHTNESS_THRESHOLD = 0.68;
@@ -122,46 +115,36 @@ function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: n
122
115
  }
123
116
  }
124
117
 
118
+ // 2-decimal precision mirrors upstream RGBColor.toHSL (toFixedNumber(_, 2)); the
119
+ // old integer rounding silently shifted converted hues by up to half a degree,
120
+ // which surfaced as a 1-LSB gradient divergence in ColorArea.
125
121
  return {
126
- h: Math.round(h * 360),
127
- s: Math.round(s * 100),
128
- l: Math.round(l * 100),
122
+ h: toFixed(h * 360, 2),
123
+ s: toFixed(s * 100, 2),
124
+ l: toFixed(l * 100, 2),
129
125
  };
130
126
  }
131
127
 
132
128
  /**
133
- * Convert HSL to RGB.
129
+ * Convert HSL to RGB. Mirrors upstream @react-stately/color HSLColor.toRGB
130
+ * bit-for-bit — the Wikipedia "HSL to RGB alternative" formula
131
+ * (https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative) — so the
132
+ * float64 LSB rounding matches the oracle exactly. The classic hue2rgb algorithm
133
+ * is equivalent in the reals but rounds a channel differently at .5 boundaries
134
+ * (e.g. hsl(50,100%,50%) green computes to 212.5 → 213 here, 212 upstream).
135
+ * h is in degrees [0,360]; s and l are percentages [0,100].
134
136
  */
135
137
  function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
136
- h /= 360;
137
- s /= 100;
138
- l /= 100;
139
-
140
- let r: number, g: number, b: number;
141
-
142
- if (s === 0) {
143
- r = g = b = l;
144
- } else {
145
- const hue2rgb = (p: number, q: number, t: number) => {
146
- if (t < 0) t += 1;
147
- if (t > 1) t -= 1;
148
- if (t < 1 / 6) return p + (q - p) * 6 * t;
149
- if (t < 1 / 2) return q;
150
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
151
- return p;
152
- };
153
-
154
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
155
- const p = 2 * l - q;
156
- r = hue2rgb(p, q, h + 1 / 3);
157
- g = hue2rgb(p, q, h);
158
- b = hue2rgb(p, q, h - 1 / 3);
159
- }
138
+ const saturation = s / 100;
139
+ const lightness = l / 100;
140
+ const a = saturation * Math.min(lightness, 1 - lightness);
141
+ const fn = (n: number, k = (n + h / 30) % 12) =>
142
+ lightness - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
160
143
 
161
144
  return {
162
- r: Math.round(r * 255),
163
- g: Math.round(g * 255),
164
- b: Math.round(b * 255),
145
+ r: Math.round(fn(0) * 255),
146
+ g: Math.round(fn(8) * 255),
147
+ b: Math.round(fn(4) * 255),
165
148
  };
166
149
  }
167
150
 
@@ -194,72 +177,38 @@ function rgbToHsb(r: number, g: number, b: number): { h: number; s: number; b: n
194
177
  }
195
178
  }
196
179
 
180
+ // 2-decimal precision mirrors upstream RGBColor.toHSB (toFixedNumber(_, 2)).
197
181
  return {
198
- h: Math.round(h * 360),
199
- s: Math.round(s * 100),
200
- b: Math.round(v * 100),
182
+ h: toFixed(h * 360, 2),
183
+ s: toFixed(s * 100, 2),
184
+ b: toFixed(v * 100, 2),
201
185
  };
202
186
  }
203
187
 
204
188
  /**
205
- * Convert HSB to RGB.
189
+ * Convert HSB (HSV) to RGB. Mirrors upstream @react-stately/color HSBColor.toRGB
190
+ * bit-for-bit — the Wikipedia "HSV to RGB alternative" formula
191
+ * (https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative) — so the
192
+ * float64 LSB rounding matches the oracle exactly. The classic sextant algorithm
193
+ * is equivalent in the reals but can round a channel differently at .5 boundaries.
194
+ * h is in degrees [0,360]; s and b are percentages [0,100].
206
195
  */
207
196
  function hsbToRgb(h: number, s: number, b: number): { r: number; g: number; b: number } {
208
- h /= 360;
209
- s /= 100;
210
- b /= 100;
211
-
212
- let r: number, g: number, bl: number;
213
-
214
- const i = Math.floor(h * 6);
215
- const f = h * 6 - i;
216
- const p = b * (1 - s);
217
- const q = b * (1 - f * s);
218
- const t = b * (1 - (1 - f) * s);
219
-
220
- switch (i % 6) {
221
- case 0:
222
- r = b;
223
- g = t;
224
- bl = p;
225
- break;
226
- case 1:
227
- r = q;
228
- g = b;
229
- bl = p;
230
- break;
231
- case 2:
232
- r = p;
233
- g = b;
234
- bl = t;
235
- break;
236
- case 3:
237
- r = p;
238
- g = q;
239
- bl = b;
240
- break;
241
- case 4:
242
- r = t;
243
- g = p;
244
- bl = b;
245
- break;
246
- default:
247
- r = b;
248
- g = p;
249
- bl = q;
250
- break;
251
- }
197
+ const saturation = s / 100;
198
+ const brightness = b / 100;
199
+ const fn = (n: number, k = (n + h / 60) % 6) =>
200
+ brightness - saturation * brightness * Math.max(Math.min(k, 4 - k, 1), 0);
252
201
 
253
202
  return {
254
- r: Math.round(r * 255),
255
- g: Math.round(g * 255),
256
- b: Math.round(bl * 255),
203
+ r: Math.round(fn(5) * 255),
204
+ g: Math.round(fn(3) * 255),
205
+ b: Math.round(fn(1) * 255),
257
206
  };
258
207
  }
259
208
 
260
- function getOklchHueName(l: number, c: number, h: number): [string, number] {
209
+ function getOklchHueName(l: number, c: number, h: number, locale: string): [string, number] {
261
210
  if (c < GRAY_THRESHOLD) {
262
- return ["gray", l];
211
+ return [getColorStringForLocale("gray", locale), l];
263
212
  }
264
213
 
265
214
  for (let i = 0; i < OKLCH_HUES.length; i += 1) {
@@ -280,27 +229,29 @@ function getOklchHueName(l: number, c: number, h: number): [string, number] {
280
229
  hueName = "yellow green";
281
230
  }
282
231
 
283
- return [hueName.toLocaleLowerCase(), l];
232
+ const name = getColorStringForLocale(hueName as ColorStringKey, locale).toLocaleLowerCase(
233
+ locale
234
+ );
235
+ return [name, l];
284
236
  }
285
237
  }
286
238
 
287
- return ["pink", l];
239
+ return [getColorStringForLocale("pink", locale).toLocaleLowerCase(locale), l];
288
240
  }
289
241
 
290
- function getColorNameFromColor(color: Color): string {
242
+ function getColorNameFromColor(color: Color, locale: string): string {
291
243
  let [l, c, h] = toOKLCH(color);
292
- const alpha = color.getChannelValue("alpha");
293
244
 
294
245
  if (l > 0.999) {
295
- return alpha < 1 ? `white ${Math.round((1 - alpha) * 100)}% transparent` : "white";
246
+ return getColorStringForLocale("white", locale);
296
247
  }
297
248
 
298
249
  if (l < 0.001) {
299
- return alpha < 1 ? `black ${Math.round((1 - alpha) * 100)}% transparent` : "black";
250
+ return getColorStringForLocale("black", locale);
300
251
  }
301
252
 
302
253
  let hue: string;
303
- [hue, l] = getOklchHueName(l, c, h);
254
+ [hue, l] = getOklchHueName(l, c, h, locale);
304
255
 
305
256
  let chroma = "";
306
257
  if (c <= 0.1 && c >= GRAY_THRESHOLD) {
@@ -322,17 +273,34 @@ function getColorNameFromColor(color: Color): string {
322
273
  lightness = "very light";
323
274
  }
324
275
 
325
- const name = [lightness, chroma, hue].filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
326
- if (alpha < 1) {
327
- return `${Math.round((1 - alpha) * 100)}% transparent ${name}`.trim();
276
+ if (chroma) {
277
+ chroma = getColorStringForLocale(chroma as ColorStringKey, locale);
278
+ }
279
+ if (lightness) {
280
+ lightness = getColorStringForLocale(lightness as ColorStringKey, locale);
328
281
  }
329
282
 
330
- return name;
283
+ const alpha = color.getChannelValue("alpha");
284
+ if (alpha < 1) {
285
+ const percentTransparent = new Intl.NumberFormat(locale, { style: "percent" }).format(1 - alpha);
286
+ return formatColorMessage("transparentColorName", locale, {
287
+ lightness,
288
+ chroma,
289
+ hue,
290
+ percentTransparent,
291
+ })
292
+ .replace(/\s+/g, " ")
293
+ .trim();
294
+ }
295
+
296
+ return formatColorMessage("colorName", locale, { lightness, chroma, hue })
297
+ .replace(/\s+/g, " ")
298
+ .trim();
331
299
  }
332
300
 
333
- function getHueNameFromColor(color: Color): string {
301
+ function getHueNameFromColor(color: Color, locale: string): string {
334
302
  const [l, c, h] = toOKLCH(color);
335
- const [name] = getOklchHueName(l, c, h);
303
+ const [name] = getOklchHueName(l, c, h, locale);
336
304
  return name;
337
305
  }
338
306
 
@@ -543,8 +511,8 @@ class RGBColorImpl implements Color {
543
511
  }
544
512
  }
545
513
 
546
- getChannelName(channel: ColorChannel, _locale: string): string {
547
- return CHANNEL_NAMES[channel] || channel;
514
+ getChannelName(channel: ColorChannel, locale: string): string {
515
+ return getColorStringForLocale(channel as ColorStringKey, locale);
548
516
  }
549
517
 
550
518
  getChannelFormatOptions(channel: ColorChannel): Intl.NumberFormatOptions {
@@ -576,12 +544,12 @@ class RGBColorImpl implements Color {
576
544
  return ["red", "green", "blue"];
577
545
  }
578
546
 
579
- getColorName(_locale: string): string {
580
- return getColorNameFromColor(this);
547
+ getColorName(locale: string): string {
548
+ return getColorNameFromColor(this, locale);
581
549
  }
582
550
 
583
- getHueName(_locale: string): string {
584
- return getHueNameFromColor(this);
551
+ getHueName(locale: string): string {
552
+ return getHueNameFromColor(this, locale);
585
553
  }
586
554
  }
587
555
 
@@ -595,9 +563,11 @@ class HSLColorImpl implements Color {
595
563
  private alpha: number;
596
564
 
597
565
  constructor(hue: number, saturation: number, lightness: number, alpha: number = 1) {
598
- this.hue = clamp(Math.round(hue) % 360, 0, 360);
599
- this.saturation = clamp(Math.round(saturation), 0, 100);
600
- this.lightness = clamp(Math.round(lightness), 0, 100);
566
+ // Store raw like upstream HSLColor's constructor — precision comes from the
567
+ // conversion (toFixed(_, 2)); rounding to integers here diverged from S2.
568
+ this.hue = clamp(hue % 360, 0, 360);
569
+ this.saturation = clamp(saturation, 0, 100);
570
+ this.lightness = clamp(lightness, 0, 100);
601
571
  this.alpha = clamp(toFixed(alpha, 2), 0, 1);
602
572
  }
603
573
 
@@ -711,8 +681,8 @@ class HSLColorImpl implements Color {
711
681
  }
712
682
  }
713
683
 
714
- getChannelName(channel: ColorChannel, _locale: string): string {
715
- return CHANNEL_NAMES[channel] || channel;
684
+ getChannelName(channel: ColorChannel, locale: string): string {
685
+ return getColorStringForLocale(channel as ColorStringKey, locale);
716
686
  }
717
687
 
718
688
  getChannelFormatOptions(channel: ColorChannel): Intl.NumberFormatOptions {
@@ -750,12 +720,12 @@ class HSLColorImpl implements Color {
750
720
  return ["hue", "saturation", "lightness"];
751
721
  }
752
722
 
753
- getColorName(_locale: string): string {
754
- return getColorNameFromColor(this);
723
+ getColorName(locale: string): string {
724
+ return getColorNameFromColor(this, locale);
755
725
  }
756
726
 
757
- getHueName(_locale: string): string {
758
- return getHueNameFromColor(this);
727
+ getHueName(locale: string): string {
728
+ return getHueNameFromColor(this, locale);
759
729
  }
760
730
  }
761
731
 
@@ -769,9 +739,10 @@ class HSBColorImpl implements Color {
769
739
  private alpha: number;
770
740
 
771
741
  constructor(hue: number, saturation: number, brightness: number, alpha: number = 1) {
772
- this.hue = clamp(Math.round(hue) % 360, 0, 360);
773
- this.saturation = clamp(Math.round(saturation), 0, 100);
774
- this.brightness = clamp(Math.round(brightness), 0, 100);
742
+ // Store raw like upstream HSBColor's constructor (see HSLColorImpl).
743
+ this.hue = clamp(hue % 360, 0, 360);
744
+ this.saturation = clamp(saturation, 0, 100);
745
+ this.brightness = clamp(brightness, 0, 100);
775
746
  this.alpha = clamp(toFixed(alpha, 2), 0, 1);
776
747
  }
777
748
 
@@ -884,8 +855,8 @@ class HSBColorImpl implements Color {
884
855
  }
885
856
  }
886
857
 
887
- getChannelName(channel: ColorChannel, _locale: string): string {
888
- return CHANNEL_NAMES[channel] || channel;
858
+ getChannelName(channel: ColorChannel, locale: string): string {
859
+ return getColorStringForLocale(channel as ColorStringKey, locale);
889
860
  }
890
861
 
891
862
  getChannelFormatOptions(channel: ColorChannel): Intl.NumberFormatOptions {
@@ -923,12 +894,12 @@ class HSBColorImpl implements Color {
923
894
  return ["hue", "saturation", "brightness"];
924
895
  }
925
896
 
926
- getColorName(_locale: string): string {
927
- return getColorNameFromColor(this);
897
+ getColorName(locale: string): string {
898
+ return getColorNameFromColor(this, locale);
928
899
  }
929
900
 
930
- getHueName(_locale: string): string {
931
- return getHueNameFromColor(this);
901
+ getHueName(locale: string): string {
902
+ return getHueNameFromColor(this, locale);
932
903
  }
933
904
  }
934
905