@fusion.dev/helix-headless 0.154.0 → 0.155.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 (59) hide show
  1. package/custom-elements.json +1427 -88
  2. package/dist/accordion/item/accordion-item.d.ts +15 -0
  3. package/dist/accordion/item/accordion-item.js +12 -1
  4. package/dist/accordion/root/accordion-root.d.ts +2 -1
  5. package/dist/accordion/root/accordion-root.js +1 -0
  6. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
  7. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
  8. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
  9. package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
  10. package/dist/button-group/root/button-group-root.d.ts +2 -0
  11. package/dist/button-group/root/button-group-root.js +1 -0
  12. package/dist/color-input/root/color-input-root.d.ts +9 -3
  13. package/dist/color-input/root/color-input-root.js +3 -1
  14. package/dist/color-picker/channels.d.ts +5 -3
  15. package/dist/color-picker/channels.js +79 -21
  16. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  17. package/dist/color-picker/root/color-picker-root.js +75 -13
  18. package/dist/date-input/root/date-input-root.d.ts +1 -0
  19. package/dist/date-input/root/date-input-root.js +2 -1
  20. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  21. package/dist/date-picker/root/date-picker-root.js +54 -31
  22. package/dist/element-events.generated.js +2 -1
  23. package/dist/navigation/root/navigation-root.d.ts +26 -0
  24. package/dist/navigation/root/navigation-root.js +58 -1
  25. package/dist/select/root/select-root.d.ts +29 -3
  26. package/dist/select/root/select-root.js +34 -9
  27. package/dist/slider/root/slider-root.d.ts +71 -9
  28. package/dist/slider/root/slider-root.js +129 -42
  29. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  30. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  31. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  32. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  33. package/dist/splitter/root/splitter-root.d.ts +22 -0
  34. package/dist/splitter/root/splitter-root.js +36 -7
  35. package/dist/steps/reach.d.ts +5 -1
  36. package/dist/steps/reach.js +2 -2
  37. package/dist/steps/root/steps-root.d.ts +37 -2
  38. package/dist/steps/root/steps-root.js +49 -7
  39. package/dist/steps/step/step.d.ts +6 -0
  40. package/dist/steps/step/step.js +4 -0
  41. package/dist/tabs/root/tabs-root.d.ts +37 -0
  42. package/dist/tabs/root/tabs-root.js +92 -2
  43. package/dist/tabs/tab/tab.d.ts +38 -2
  44. package/dist/tabs/tab/tab.js +45 -4
  45. package/dist/tag/group/tag-group.d.ts +29 -3
  46. package/dist/tag/group/tag-group.js +42 -11
  47. package/dist/toggle/root/toggle-root.d.ts +2 -0
  48. package/dist/toggle/root/toggle-root.js +1 -1
  49. package/dist/tree/item/tree-item.d.ts +10 -0
  50. package/dist/tree/item/tree-item.js +12 -0
  51. package/dist/tree/root/tree-root.d.ts +40 -1
  52. package/dist/tree/root/tree-root.js +120 -4
  53. package/dist/tree/selection.d.ts +6 -0
  54. package/dist/tree/selection.js +11 -0
  55. package/lint.json +1 -1
  56. package/package.json +5 -5
  57. package/types/jsx.d.ts +984 -151
  58. package/types/vue.d.ts +221 -49
  59. package/types/words.d.ts +36 -1
@@ -1,4 +1,16 @@
1
- import { clamp, toHsl, toRgb } from '@fusion.dev/helix-core';
1
+ import { clamp, toHsl, toOklch, toRgb } from '@fusion.dev/helix-core';
2
+ const onStep = (value, step) => {
3
+ const decimals = String(step).split('.')[1]?.length ?? 0;
4
+ return Number((Math.round(value / step) * step).toFixed(decimals));
5
+ };
6
+ const FAMILY = {
7
+ hex: /^rgba?$/,
8
+ hsb: /^hsba?$/,
9
+ hsl: /^hsla?$/,
10
+ oklch: /^oklch$/,
11
+ rgb: /^rgba?$/,
12
+ };
13
+ const OKLCH_STEPS = [0.1, 0.001, 0.1];
2
14
  const percent = (token) => {
3
15
  if (token === undefined) {
4
16
  return 100;
@@ -8,7 +20,8 @@ const percent = (token) => {
8
20
  };
9
21
  const readWritten = (value, format) => {
10
22
  const text = value.trim().toLowerCase();
11
- if (format !== 'hsl' && /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.test(text)) {
23
+ if ((format === 'hex' || format === 'rgb') &&
24
+ /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.test(text)) {
12
25
  const digits = text.slice(1);
13
26
  const width = digits.length <= 4 ? 1 : 2;
14
27
  const byte = (index) => {
@@ -18,23 +31,46 @@ const readWritten = (value, format) => {
18
31
  const count = digits.length / width;
19
32
  return [byte(0), byte(1), byte(2), count === 4 ? Math.round((byte(3) / 255) * 100) : 100];
20
33
  }
21
- const call = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
22
- if (!call?.[1] || call[2] === undefined) {
23
- return null;
24
- }
25
- if (call[1].startsWith('hsl') !== (format === 'hsl')) {
34
+ const call = /^(rgba?|hsla?|hsba?|oklch)\(([^)]*)\)$/.exec(text);
35
+ if (!call?.[1] || call[2] === undefined || !FAMILY[format].test(call[1])) {
26
36
  return null;
27
37
  }
28
38
  const [first, second, third, fourth] = call[2].replace(/[,/]/g, ' ').split(/\s+/).filter(Boolean);
29
- const numbers = [first, second, third].map((one) => Number.parseFloat(one ?? ''));
39
+ const numbers = [first, second, third].map((one) => one === 'none' ? 0 : Number.parseFloat(one ?? ''));
30
40
  if (numbers.some(Number.isNaN)) {
31
41
  return null;
32
42
  }
43
+ if (format === 'oklch') {
44
+ const [lightness = 0, chroma = 0, hue = 0] = numbers;
45
+ return [
46
+ onStep(first?.endsWith('%') ? lightness : lightness * 100, OKLCH_STEPS[0]),
47
+ onStep(second?.endsWith('%') ? (chroma / 100) * 0.4 : chroma, OKLCH_STEPS[1]),
48
+ onStep(hue, OKLCH_STEPS[2]),
49
+ Math.round(percent(fourth)),
50
+ ];
51
+ }
33
52
  return [...numbers, percent(fourth)].map((one) => Math.round(one));
34
53
  };
35
54
  export const channelFields = (value, color, format, alpha) => {
36
55
  const written = readWritten(value, format);
37
56
  const fromModel = () => {
57
+ if (format === 'hsb') {
58
+ return [
59
+ Math.round(color.hue),
60
+ Math.round(color.saturation),
61
+ Math.round(color.brightness),
62
+ Math.round(color.alpha),
63
+ ];
64
+ }
65
+ if (format === 'oklch') {
66
+ const { chroma, hue, lightness } = toOklch(color);
67
+ return [
68
+ onStep(lightness * 100, OKLCH_STEPS[0]),
69
+ onStep(chroma, OKLCH_STEPS[1]),
70
+ onStep(hue, OKLCH_STEPS[2]),
71
+ Math.round(color.alpha),
72
+ ];
73
+ }
38
74
  if (format === 'hsl') {
39
75
  const { hue, lightness, saturation } = toHsl(color);
40
76
  return [Math.round(hue), saturation, lightness, Math.round(color.alpha)];
@@ -43,18 +79,32 @@ export const channelFields = (value, color, format, alpha) => {
43
79
  return [red, green, blue, Math.round(color.alpha)];
44
80
  };
45
81
  const [one = 0, two = 0, three = 0, four = 100] = written ?? fromModel();
46
- const fields = format === 'hsl'
47
- ? [
48
- { channel: 'hue', max: 360, min: 0, value: one },
49
- { channel: 'saturation', max: 100, min: 0, value: two },
50
- { channel: 'lightness', max: 100, min: 0, value: three },
51
- ]
52
- : [
53
- { channel: 'red', max: 255, min: 0, value: one },
54
- { channel: 'green', max: 255, min: 0, value: two },
55
- { channel: 'blue', max: 255, min: 0, value: three },
56
- ];
57
- return alpha ? [...fields, { channel: 'alpha', max: 100, min: 0, value: four }] : fields;
82
+ const byFormat = {
83
+ hex: [],
84
+ hsb: [
85
+ { channel: 'hue', max: 360, min: 0, step: 1, value: one },
86
+ { channel: 'saturation', max: 100, min: 0, step: 1, value: two },
87
+ { channel: 'brightness', max: 100, min: 0, step: 1, value: three },
88
+ ],
89
+ hsl: [
90
+ { channel: 'hue', max: 360, min: 0, step: 1, value: one },
91
+ { channel: 'saturation', max: 100, min: 0, step: 1, value: two },
92
+ { channel: 'lightness', max: 100, min: 0, step: 1, value: three },
93
+ ],
94
+ oklch: [
95
+ { channel: 'lightness', max: 100, min: 0, step: OKLCH_STEPS[0], value: one },
96
+ { channel: 'chroma', max: 0.4, min: 0, step: OKLCH_STEPS[1], value: two },
97
+ { channel: 'hue', max: 360, min: 0, step: OKLCH_STEPS[2], value: three },
98
+ ],
99
+ rgb: [],
100
+ };
101
+ const bytes = [
102
+ { channel: 'red', max: 255, min: 0, step: 1, value: one },
103
+ { channel: 'green', max: 255, min: 0, step: 1, value: two },
104
+ { channel: 'blue', max: 255, min: 0, step: 1, value: three },
105
+ ];
106
+ const fields = byFormat[format].length > 0 ? byFormat[format] : bytes;
107
+ return alpha ? [...fields, { channel: 'alpha', max: 100, min: 0, step: 1, value: four }] : fields;
58
108
  };
59
109
  export const writeChannel = (fields, format, alpha, channel, value) => {
60
110
  if (Number.isNaN(value)) {
@@ -65,7 +115,7 @@ export const writeChannel = (fields, format, alpha, channel, value) => {
65
115
  if (field === undefined) {
66
116
  return null;
67
117
  }
68
- numbers.set(channel, clamp(Math.round(value), field.min, field.max));
118
+ numbers.set(channel, clamp(onStep(value, field.step), field.min, field.max));
69
119
  const get = (name, fallback = 0) => numbers.get(name) ?? fallback;
70
120
  const opacity = get('alpha', 100);
71
121
  const fraction = Number((opacity / 100).toFixed(2));
@@ -73,6 +123,14 @@ export const writeChannel = (fields, format, alpha, channel, value) => {
73
123
  const body = `${get('hue')}, ${get('saturation')}%, ${get('lightness')}%`;
74
124
  return alpha ? `hsla(${body}, ${fraction})` : `hsl(${body})`;
75
125
  }
126
+ if (format === 'hsb') {
127
+ const body = `${get('hue')}, ${get('saturation')}%, ${get('brightness')}%`;
128
+ return alpha ? `hsba(${body}, ${fraction})` : `hsb(${body})`;
129
+ }
130
+ if (format === 'oklch') {
131
+ const body = `${get('lightness')}% ${get('chroma')} ${get('hue')}`;
132
+ return alpha ? `oklch(${body} / ${fraction})` : `oklch(${body})`;
133
+ }
76
134
  if (format === 'rgb') {
77
135
  const body = `${get('red')}, ${get('green')}, ${get('blue')}`;
78
136
  return alpha ? `rgba(${body}, ${fraction})` : `rgb(${body})`;
@@ -32,6 +32,11 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
32
32
  * is named by its color. The opacity ramp is heard as a percentage. Set
33
33
  * `color-words` to speak the names in another language.
34
34
  *
35
+ * `label` names the picker, which is then a group with that name, and the
36
+ * square's ramps say it before their own, as "Brand, Saturation". `hint`
37
+ * writes help under the picker, which the square, the ramps and the value
38
+ * field are described by. Inside Color Input the field carries both.
39
+ *
35
40
  * The presets are a listbox with the current color selected, and one tab
36
41
  * stop. The arrow keys move between them, Home and End go to the ends, and
37
42
  * Enter, Space or a press chooses one.
@@ -43,6 +48,8 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
43
48
  * @documentation https://fusion.dev/helix/components/color-picker
44
49
  *
45
50
  * @slot eyedropper-icon - The icon on the eyedropper button, usually a dropper.
51
+ * @slot hint - Help under the picker, for content richer than the `hint` text.
52
+ * @slot label - The name above the picker, for content richer than the `label` text.
46
53
  *
47
54
  * @event {Event} change - Fired after the value changes.
48
55
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the color changes. Cancelable: call `preventDefault()` to keep the current color.
@@ -62,6 +69,8 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
62
69
  * @csspart channel-marker-label - The value a mark stands at, when `marker-labels` is set.
63
70
  * @csspart channel-markers - The row of marks along the track.
64
71
  * @csspart channel-output - The current value, shown beside the track.
72
+ * @csspart channel-thumb - A ramp's thumb box, which holds no content in a picker.
73
+ * @csspart channel-thumbs - The layer over a ramp's track that holds its thumb box.
65
74
  * @csspart channel-tooltip - A ramp's value in a bubble, where a ramp shows one.
66
75
  * @csspart channel-tooltips - The layer over a ramp's track that holds its bubble.
67
76
  * @csspart channels - The ramps beside the sample.
@@ -88,6 +97,8 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
88
97
  * `--color-picker-brightness`, and this tier ships no CSS.
89
98
  * @csspart grid-brightness - The range input for the square's vertical axis.
90
99
  * @csspart grid-saturation - The range input for the square's horizontal axis.
100
+ * @csspart hint - The help under the picker, while `hint` is set or the `hint` slot has content.
101
+ * @csspart label - The name above the picker, while `label` is set or the `label` slot has content.
91
102
  * @csspart sample - A chip of the current color. Pressing it copies the value.
92
103
  * @csspart swatch - One preset, an option in the `swatches` listbox.
93
104
  * @csspart swatches - The row of presets, a listbox.
@@ -100,7 +111,10 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
100
111
  * @cssproperty --color-picker-saturation - The chosen saturation, 0 to 100, as a percentage.
101
112
  * @cssproperty --color-picker-swatch - The color a chip shows: the current color on the sample, and the preset's color on each preset.
102
113
  *
114
+ * @cssstate copied - The moment after the sample copies the value, while it says `copied-label`.
115
+ * @cssstate dragging - While a pointer drags the square's handle.
103
116
  * @cssstate invalid - While `invalid` is set or `feedback` says what is wrong.
117
+ * @cssstate sampling - While the eyedropper is open over the screen.
104
118
  */
105
119
  export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_base {
106
120
  /**
@@ -142,14 +156,20 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
142
156
  /**
143
157
  * Whether the picker shows a number field for each channel of the color, as
144
158
  * React Aria's ColorField does with `channel`: red, green and blue for hex
145
- * and rgb, hue, saturation and lightness for hsl, and the opacity with
146
- * `alpha`.
159
+ * and rgb, hue, saturation and lightness for hsl, hue, saturation and
160
+ * brightness for hsb, lightness, chroma and hue for oklch, and the opacity
161
+ * with `alpha`.
147
162
  *
148
163
  * For a reader who has the numbers from a brand guide, and for a keyboard,
149
164
  * where typing 128 is quicker than holding an arrow key. The fields follow
150
165
  * the syntax the format toggle sets.
151
166
  */
152
167
  channelFields: boolean;
168
+ /**
169
+ * What the chroma channel's field is called, in `channel-fields` with
170
+ * oklch.
171
+ */
172
+ chromaLabel: string;
153
173
  /**
154
174
  * The words a color's name is spoken in, as JSON keyed by their English,
155
175
  * such as `{"red": "rot", "vibrant": "kräftiges"}`.
@@ -179,10 +199,16 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
179
199
  */
180
200
  eyedropperLabel: string;
181
201
  /**
182
- * The syntax the value is written in: `hex`, `rgb`, or `hsl`.
202
+ * The syntax the value is written in: `hex`, `rgb`, `hsl`, `oklch`, or
203
+ * `hsb`.
183
204
  *
184
205
  * Only what is written. Everything is read whatever this says, so a picker on
185
- * `hex` still takes an `rgb()` handed to it and stores the hex for it.
206
+ * `hex` still takes an `rgb()` or an `oklch()` handed to it and stores the
207
+ * hex for it. `oklch` writes the sRGB color the picker holds, such as
208
+ * `oklch(62.8% 0.258 29.2)`; a color outside sRGB that is handed in is
209
+ * clipped to it. `hsb` writes the picker's own model, `hsb(0, 100%, 100%)`,
210
+ * as React Aria's and Zag's color strings do. It is not CSS, so the toggle
211
+ * walks the other four.
186
212
  */
187
213
  format?: ColorFormat;
188
214
  /**
@@ -193,13 +219,23 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
193
219
  * What the green channel's field is called, in `channel-fields`.
194
220
  */
195
221
  greenLabel: string;
222
+ /**
223
+ * Help under the picker, such as what the color is used for. The `hint`
224
+ * slot takes richer content.
225
+ */
226
+ hint: string;
196
227
  /**
197
228
  * What the hue channel is called.
198
229
  */
199
230
  hueLabel: string;
231
+ /**
232
+ * The name above the picker, such as "Brand color". The picker is then a
233
+ * group with this name. The `label` slot takes richer content.
234
+ */
235
+ label: string;
200
236
  /**
201
237
  * What the lightness channel's field is called, in `channel-fields` with
202
- * hsl.
238
+ * hsl and oklch.
203
239
  */
204
240
  lightnessLabel: string;
205
241
  /**
@@ -353,6 +389,23 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
353
389
  private sampling;
354
390
  /** The preset the reader was last on, which holds the row's one tab stop. */
355
391
  private swatchAt;
392
+ /**
393
+ * The children in a slot, read off the children rather than heard from
394
+ * `slotchange`: a declarative shadow root fires that before anything
395
+ * listens, so a served picker kept its label hidden.
396
+ */
397
+ private slottedIn;
398
+ private readonly handleNamingSlot;
399
+ /** The picker's own name, from `label` or the `label` slot's words. */
400
+ private get named();
401
+ /** Whether help is shown under the picker. */
402
+ private get hinted();
403
+ /** The ids a control in the picker is described by: the help, then the message. */
404
+ private get describedBy();
405
+ /** Whether a pointer is dragging the square's handle, said as `:state(dragging)`. */
406
+ private dragging;
407
+ /** Whether the eyedropper is open, said as `:state(sampling)`. */
408
+ private picking;
356
409
  /**
357
410
  * Whether the value on this element was produced here.
358
411
  *
@@ -15,7 +15,7 @@ import { areaAt, hueAt } from '../wheel.js';
15
15
  import '../../button/index.js';
16
16
  import '../../button-group/index.js';
17
17
  import '../../slider/index.js';
18
- const FORMATS = ['hex', 'rgb', 'hsl'];
18
+ const FORMATS = ['hex', 'rgb', 'hsl', 'oklch'];
19
19
  const START = { alpha: 100, brightness: 100, hue: 0, saturation: 100 };
20
20
  let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPickerFeedback(MixinControllerValue(HelixFormElement)) {
21
21
  constructor() {
@@ -26,13 +26,16 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
26
26
  this.blueLabel = 'Blue';
27
27
  this.brightnessLabel = 'Brightness';
28
28
  this.channelFields = false;
29
+ this.chromaLabel = 'Chroma';
29
30
  this.colorWords = {};
30
31
  this.copiedLabel = 'Copied';
31
32
  this.copyLabel = 'Copy {value}';
32
33
  this.eyedropperLabel = 'Sample a color from the screen';
33
34
  this.formatLabel = 'Syntax: {format}. Change it.';
34
35
  this.greenLabel = 'Green';
36
+ this.hint = '';
35
37
  this.hueLabel = 'Hue';
38
+ this.label = '';
36
39
  this.lightnessLabel = 'Lightness';
37
40
  this.noEyedropper = false;
38
41
  this.noFormat = false;
@@ -50,6 +53,11 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
50
53
  this.valueMissingMessage = 'Choose a color.';
51
54
  this.wheel = false;
52
55
  this.sampling = false;
56
+ this.handleNamingSlot = () => {
57
+ this.requestUpdate();
58
+ };
59
+ this.dragging = false;
60
+ this.picking = false;
53
61
  this.owned = false;
54
62
  this.announcesInput = true;
55
63
  this.unsettled = false;
@@ -132,6 +140,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
132
140
  return;
133
141
  }
134
142
  this.dispatchEvent(new HelixSampleStartEvent());
143
+ this.picking = true;
135
144
  try {
136
145
  const { sRGBHex } = await new Dropper().open();
137
146
  const parsed = parseColor(sRGBHex);
@@ -142,6 +151,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
142
151
  }
143
152
  catch {
144
153
  }
154
+ this.picking = false;
145
155
  this.dispatchEvent(new HelixSampleEndEvent());
146
156
  };
147
157
  this.handleGridPointer = (event) => {
@@ -150,12 +160,14 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
150
160
  }
151
161
  const grid = event.currentTarget;
152
162
  if (event.type === 'pointerup' || event.type === 'pointercancel') {
163
+ this.dragging = false;
153
164
  this.settle();
154
165
  return;
155
166
  }
156
167
  if (event.type === 'pointerdown') {
168
+ this.dragging = true;
157
169
  capturePointer(grid, event.pointerId);
158
- this.partNamed('grid-saturation')?.focus();
170
+ this.partNamed('grid-saturation')?.focus({ preventScroll: true });
159
171
  }
160
172
  else if (!grid.hasPointerCapture(event.pointerId)) {
161
173
  return;
@@ -181,7 +193,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
181
193
  }
182
194
  if (event.type === 'pointerdown') {
183
195
  capturePointer(wheel, event.pointerId);
184
- this.partNamed('wheel-hue')?.focus();
196
+ this.partNamed('wheel-hue')?.focus({ preventScroll: true });
185
197
  }
186
198
  else if (!wheel.hasPointerCapture(event.pointerId)) {
187
199
  return;
@@ -232,6 +244,23 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
232
244
  };
233
245
  }
234
246
  static { this.words = 'helix-color-picker'; }
247
+ slottedIn(name) {
248
+ return this.knownChildren.filter((child) => child.slot === name);
249
+ }
250
+ get named() {
251
+ return (this.label ||
252
+ this.slottedIn('label')
253
+ .map((child) => child.textContent ?? '')
254
+ .join(' ')
255
+ .trim());
256
+ }
257
+ get hinted() {
258
+ return this.hint !== '' || this.slottedIn('hint').length > 0;
259
+ }
260
+ get describedBy() {
261
+ const ids = [this.hinted ? 'hint' : '', this.feedback === '' ? '' : 'feedback'].filter(Boolean);
262
+ return ids.length > 0 ? ids.join(' ') : nothing;
263
+ }
235
264
  settle() {
236
265
  if (this.unsettled) {
237
266
  this.unsettled = false;
@@ -299,6 +328,8 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
299
328
  return {
300
329
  alpha: this.opacityLabel,
301
330
  blue: this.blueLabel,
331
+ brightness: this.brightnessLabel,
332
+ chroma: this.chromaLabel,
302
333
  green: this.greenLabel,
303
334
  hue: this.hueLabel,
304
335
  lightness: this.lightnessLabel,
@@ -327,10 +358,10 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
327
358
  <input
328
359
  part="field-input"
329
360
  type="number"
330
- inputmode="numeric"
361
+ inputmode=${field.step < 1 ? 'decimal' : 'numeric'}
331
362
  min=${field.min}
332
363
  max=${field.max}
333
- step="1"
364
+ step=${field.step}
334
365
  .value=${live(String(field.value))}
335
366
  ?disabled=${this.isDisabled}
336
367
  ?readonly=${this.readOnly}
@@ -350,7 +381,8 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
350
381
  exportparts="control: channel-control, feedback: channel-feedback, field: channel-field,
351
382
  hint: channel-hint, label: channel-label, marker: channel-marker,
352
383
  marker-label: channel-marker-label, markers: channel-markers, output: channel-output,
353
- tooltip: channel-tooltip, tooltips: channel-tooltips"
384
+ thumb: channel-thumb, thumbs: channel-thumbs, tooltip: channel-tooltip,
385
+ tooltips: channel-tooltips"
354
386
  data-channel=${channel}
355
387
  aria-invalid=${this.marked ? 'true' : nothing}
356
388
  label=${label}
@@ -391,10 +423,18 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
391
423
  willUpdate(changed) {
392
424
  super.willUpdate(changed);
393
425
  const outside = [...(this.internals.labels ?? [])];
394
- hostAria(this.internals, { labelledBy: outside, role: outside.length > 0 ? 'group' : '' });
426
+ const named = this.named;
427
+ hostAria(this.internals, {
428
+ label: outside.length > 0 ? '' : named,
429
+ labelledBy: outside,
430
+ role: outside.length > 0 || named !== '' ? 'group' : '',
431
+ });
395
432
  if (!this.hydrating) {
396
433
  this.sampling = 'EyeDropper' in globalThis;
397
434
  }
435
+ this.toggleState('copied', this.copier.copied);
436
+ this.toggleState('dragging', this.dragging);
437
+ this.toggleState('sampling', this.picking);
398
438
  }
399
439
  renderTools() {
400
440
  return html `<${this.tag('button-group')} part="actions" label=${this.toolsLabel}>
@@ -471,7 +511,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
471
511
  const reversed = square('brightness');
472
512
  const hue = hueName({ alpha: 100, brightness: 100, hue: color.hue, saturation: 100 }, this.colorWords, locale);
473
513
  const hueSaid = (value) => new Intl.ListFormat(locale, { style: 'short', type: 'unit' }).format([String(value), hue]);
474
- const owner = this.getAttribute('aria-label')?.trim();
514
+ const owner = this.getAttribute('aria-label')?.trim() || this.named;
475
515
  const axisName = (channel) => (owner ? `${owner}, ${channel}` : channel);
476
516
  const area = html `<div
477
517
  part="grid"
@@ -488,7 +528,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
488
528
  min="0"
489
529
  max="100"
490
530
  aria-label=${axisName(this.saturationLabel)}
491
- aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
531
+ aria-describedby=${this.describedBy}
492
532
  aria-valuetext=${both}
493
533
  aria-invalid=${this.marked ? 'true' : nothing}
494
534
  .value=${String(color.saturation)}
@@ -505,7 +545,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
505
545
  tabindex="-1"
506
546
  style="writing-mode: vertical-lr; direction: rtl"
507
547
  aria-label=${axisName(this.brightnessLabel)}
508
- aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
548
+ aria-describedby=${this.describedBy}
509
549
  aria-valuetext=${reversed}
510
550
  aria-invalid=${this.marked ? 'true' : nothing}
511
551
  .value=${String(color.brightness)}
@@ -514,7 +554,10 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
514
554
  @change=${() => this.settle()}
515
555
  />
516
556
  </div>`;
517
- return html `${this.wheel
557
+ return html `<div part="label" ?hidden=${this.named === ''}>
558
+ <slot name="label" @slotchange=${this.handleNamingSlot}>${this.label}</slot>
559
+ </div>
560
+ ${this.wheel
518
561
  ? html `<div
519
562
  part="wheel"
520
563
  style="--color-picker-hue: ${color.hue}"
@@ -531,7 +574,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
531
574
  max="360"
532
575
  dir="ltr"
533
576
  aria-label=${axisName(this.hueLabel)}
534
- aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
577
+ aria-describedby=${this.describedBy}
535
578
  aria-valuetext=${hueSaid(color.hue)}
536
579
  aria-invalid=${this.marked ? 'true' : nothing}
537
580
  .value=${String(color.hue)}
@@ -574,7 +617,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
574
617
  part="value"
575
618
  type="text"
576
619
  aria-label=${this.valueLabel}
577
- aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
620
+ aria-describedby=${this.describedBy}
578
621
  aria-invalid=${this.marked ? 'true' : nothing}
579
622
  autocomplete=${this.autocomplete || nothing}
580
623
  spellcheck="false"
@@ -591,6 +634,9 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
591
634
  </div>
592
635
 
593
636
  ${this.presets.length === 0 ? nothing : this.renderSwatches(painted)}
637
+ <div part="hint" id="hint" ?hidden=${!this.hinted}>
638
+ <slot name="hint" @slotchange=${this.handleNamingSlot}>${this.hint}</slot>
639
+ </div>
594
640
  ${this.renderPickerFeedback()}`;
595
641
  }
596
642
  };
@@ -615,6 +661,10 @@ __decorate([
615
661
  __decorate([
616
662
  property({ attribute: 'channel-fields', type: Boolean })
617
663
  ], HelixHeadlessColorPicker.prototype, "channelFields", void 0);
664
+ __decorate([
665
+ words,
666
+ property({ attribute: 'chroma-label' })
667
+ ], HelixHeadlessColorPicker.prototype, "chromaLabel", void 0);
618
668
  __decorate([
619
669
  property({ attribute: 'color-words', converter: wordsConverter })
620
670
  ], HelixHeadlessColorPicker.prototype, "colorWords", void 0);
@@ -641,10 +691,16 @@ __decorate([
641
691
  words,
642
692
  property({ attribute: 'green-label' })
643
693
  ], HelixHeadlessColorPicker.prototype, "greenLabel", void 0);
694
+ __decorate([
695
+ property()
696
+ ], HelixHeadlessColorPicker.prototype, "hint", void 0);
644
697
  __decorate([
645
698
  words,
646
699
  property({ attribute: 'hue-label' })
647
700
  ], HelixHeadlessColorPicker.prototype, "hueLabel", void 0);
701
+ __decorate([
702
+ property()
703
+ ], HelixHeadlessColorPicker.prototype, "label", void 0);
648
704
  __decorate([
649
705
  words,
650
706
  property({ attribute: 'lightness-label' })
@@ -711,6 +767,12 @@ __decorate([
711
767
  __decorate([
712
768
  state()
713
769
  ], HelixHeadlessColorPicker.prototype, "swatchAt", void 0);
770
+ __decorate([
771
+ state()
772
+ ], HelixHeadlessColorPicker.prototype, "dragging", void 0);
773
+ __decorate([
774
+ state()
775
+ ], HelixHeadlessColorPicker.prototype, "picking", void 0);
714
776
  HelixHeadlessColorPicker = __decorate([
715
777
  customElement('helix-headless-color-picker')
716
778
  ], HelixHeadlessColorPicker);
@@ -72,6 +72,7 @@ declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fus
72
72
  * @csspart picker-day-outside - A day from a neighboring month. Applied with `picker-day`.
73
73
  * @csspart picker-day-selected - The chosen day, or either end of a chosen range. Applied with `picker-day`.
74
74
  * @csspart picker-day-today - Today. Applied with `picker-day`.
75
+ * @csspart picker-day-weekend - A day of the weekend where the calendar's language is spoken, beside `picker-day`.
75
76
  * @csspart picker-feedback - The calendar's own message, under it, while the calendar's `feedback` is set.
76
77
  * @csspart picker-grid - The grid of days.
77
78
  * @csspart picker-header - The month name and its controls.
@@ -218,7 +218,8 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
218
218
  exportparts="cell: picker-cell, cell-range-end: picker-cell-range-end,
219
219
  cell-range-middle: picker-cell-range-middle, cell-range-start: picker-cell-range-start,
220
220
  day: picker-day, day-disabled: picker-day-disabled, day-outside: picker-day-outside,
221
- day-selected: picker-day-selected, day-today: picker-day-today, feedback: picker-feedback, grid: picker-grid,
221
+ day-selected: picker-day-selected, day-today: picker-day-today, day-weekend: picker-day-weekend,
222
+ feedback: picker-feedback, grid: picker-grid,
222
223
  header: picker-header, heading: picker-heading, heading-button: picker-heading-button,
223
224
  month: picker-month, months: picker-months, next: picker-next,
224
225
  next-mark: picker-next-mark, period: picker-period, period-current: picker-period-current,