@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.
- package/dist/calendar/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +20 -10
- package/dist/calendar/createDateFieldState.d.ts +65 -20
- package/dist/calendar/createRangeCalendarState.d.ts +20 -2
- package/dist/calendar/createTimeFieldState.d.ts +38 -67
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/intl/index.d.ts +10 -0
- package/dist/collections/ListCollection.d.ts +8 -0
- package/dist/collections/createListState.d.ts +16 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +19 -0
- package/dist/collections/index.d.ts +3 -2
- package/dist/collections/types.d.ts +9 -0
- package/dist/color/intl/index.d.ts +14 -0
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/types.d.ts +3 -1
- package/dist/grid/types.d.ts +17 -3
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4372 -1087
- package/dist/index.js.map +1 -1
- package/dist/select/createSelectState.d.ts +6 -0
- package/dist/selection/Selection.d.ts +16 -0
- package/dist/selection/SelectionManager.d.ts +119 -0
- package/dist/selection/createMultipleSelectionState.d.ts +59 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/table/TableCollection.d.ts +30 -0
- package/dist/table/createTreeGridState.d.ts +15 -0
- package/dist/table/index.d.ts +2 -1
- package/dist/table/types.d.ts +58 -0
- package/dist/toast/createToastState.d.ts +16 -0
- package/dist/toast/index.d.ts +1 -1
- package/dist/tree/types.d.ts +14 -6
- package/package.json +1 -1
- package/src/calendar/IncompleteDate.ts +430 -0
- package/src/calendar/createCalendarState.ts +129 -40
- package/src/calendar/createDateFieldState.ts +633 -477
- package/src/calendar/createRangeCalendarState.ts +343 -30
- package/src/calendar/createTimeFieldState.ts +179 -467
- package/src/calendar/index.ts +2 -0
- package/src/calendar/intl/index.ts +364 -0
- package/src/collections/ListCollection.ts +31 -0
- package/src/collections/createListState.ts +139 -17
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +129 -0
- package/src/collections/index.ts +12 -0
- package/src/collections/types.ts +10 -0
- package/src/color/Color.ts +104 -133
- package/src/color/intl/index.ts +1409 -0
- package/src/combobox/createComboBoxState.ts +38 -13
- package/src/dnd/createDroppableCollectionState.ts +125 -6
- package/src/dnd/index.d.ts +1 -0
- package/src/dnd/index.ts +1 -0
- package/src/dnd/types.d.ts +3 -1
- package/src/dnd/types.ts +4 -1
- package/src/grid/createGridState.ts +36 -2
- package/src/grid/types.ts +17 -3
- package/src/index.d.ts +1 -0
- package/src/index.ts +14 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +541 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- package/src/steplist/createStepListState.ts +27 -4
- package/src/table/TableCollection.ts +213 -43
- package/src/table/createTableState.ts +20 -0
- package/src/table/createTreeGridState.ts +146 -0
- package/src/table/index.ts +3 -0
- package/src/table/types.ts +66 -0
- package/src/tabs/createTabListState.ts +11 -9
- package/src/toast/createToastState.ts +43 -10
- package/src/toast/index.ts +1 -0
- package/src/tree/createTreeState.ts +37 -6
- 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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
}
|
package/src/collections/index.ts
CHANGED
|
@@ -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";
|
package/src/collections/types.ts
CHANGED
|
@@ -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
|
/**
|
package/src/color/Color.ts
CHANGED
|
@@ -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:
|
|
127
|
-
s:
|
|
128
|
-
l:
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
163
|
-
g: Math.round(
|
|
164
|
-
b: Math.round(
|
|
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:
|
|
199
|
-
s:
|
|
200
|
-
b:
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
255
|
-
g: Math.round(
|
|
256
|
-
b: Math.round(
|
|
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
|
-
|
|
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
|
|
246
|
+
return getColorStringForLocale("white", locale);
|
|
296
247
|
}
|
|
297
248
|
|
|
298
249
|
if (l < 0.001) {
|
|
299
|
-
return
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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,
|
|
547
|
-
return
|
|
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(
|
|
580
|
-
return getColorNameFromColor(this);
|
|
547
|
+
getColorName(locale: string): string {
|
|
548
|
+
return getColorNameFromColor(this, locale);
|
|
581
549
|
}
|
|
582
550
|
|
|
583
|
-
getHueName(
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
this.
|
|
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,
|
|
715
|
-
return
|
|
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(
|
|
754
|
-
return getColorNameFromColor(this);
|
|
723
|
+
getColorName(locale: string): string {
|
|
724
|
+
return getColorNameFromColor(this, locale);
|
|
755
725
|
}
|
|
756
726
|
|
|
757
|
-
getHueName(
|
|
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
|
-
|
|
773
|
-
this.
|
|
774
|
-
this.
|
|
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,
|
|
888
|
-
return
|
|
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(
|
|
927
|
-
return getColorNameFromColor(this);
|
|
897
|
+
getColorName(locale: string): string {
|
|
898
|
+
return getColorNameFromColor(this, locale);
|
|
928
899
|
}
|
|
929
900
|
|
|
930
|
-
getHueName(
|
|
931
|
-
return getHueNameFromColor(this);
|
|
901
|
+
getHueName(locale: string): string {
|
|
902
|
+
return getHueNameFromColor(this, locale);
|
|
932
903
|
}
|
|
933
904
|
}
|
|
934
905
|
|