@fusion.dev/helix-headless 0.143.0 → 0.144.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 (191) hide show
  1. package/custom-elements.json +8551 -375
  2. package/dist/area-chart/root/area-chart-root.d.ts +12 -4
  3. package/dist/area-chart/root/area-chart-root.js +24 -7
  4. package/dist/avatar/root/avatar-root.d.ts +14 -0
  5. package/dist/avatar/root/avatar-root.js +23 -9
  6. package/dist/badge/root/badge-root.d.ts +11 -7
  7. package/dist/badge/root/badge-root.js +1 -1
  8. package/dist/bar-chart/root/bar-chart-root.d.ts +12 -3
  9. package/dist/bar-chart/root/bar-chart-root.js +17 -10
  10. package/dist/button/root/button-root.d.ts +47 -1
  11. package/dist/button/root/button-root.js +38 -4
  12. package/dist/callout/root/callout-root.d.ts +18 -1
  13. package/dist/callout/root/callout-root.js +15 -4
  14. package/dist/card/root/card-root.d.ts +30 -1
  15. package/dist/card/root/card-root.js +38 -5
  16. package/dist/checkbox/root/checkbox-root.js +2 -0
  17. package/dist/checkbox-group/root/checkbox-group-root.d.ts +53 -3
  18. package/dist/checkbox-group/root/checkbox-group-root.js +71 -22
  19. package/dist/code-block/lines.d.ts +13 -0
  20. package/dist/code-block/lines.js +2 -0
  21. package/dist/code-block/root/code-block-root.d.ts +30 -0
  22. package/dist/code-block/root/code-block-root.js +23 -2
  23. package/dist/color-input/root/color-input-root.d.ts +34 -1
  24. package/dist/color-input/root/color-input-root.js +23 -4
  25. package/dist/color-picker/channels.d.ts +26 -0
  26. package/dist/color-picker/channels.js +84 -0
  27. package/dist/color-picker/root/color-picker-root.d.ts +81 -5
  28. package/dist/color-picker/root/color-picker-root.js +103 -10
  29. package/dist/combobox/root/combobox-root.d.ts +38 -3
  30. package/dist/combobox/root/combobox-root.js +64 -2
  31. package/dist/command/root/command-root.d.ts +30 -4
  32. package/dist/command/root/command-root.js +33 -3
  33. package/dist/copy-button/from.d.ts +22 -0
  34. package/dist/copy-button/from.js +28 -0
  35. package/dist/copy-button/root/copy-button-root.d.ts +27 -1
  36. package/dist/copy-button/root/copy-button-root.js +43 -7
  37. package/dist/date-input/root/date-input-root.d.ts +73 -4
  38. package/dist/date-input/root/date-input-root.js +93 -43
  39. package/dist/date-picker/root/date-picker-root.d.ts +145 -1
  40. package/dist/date-picker/root/date-picker-root.js +239 -48
  41. package/dist/dialog/description.d.ts +9 -0
  42. package/dist/dialog/description.js +14 -0
  43. package/dist/dialog/root/dialog-root.d.ts +64 -1
  44. package/dist/dialog/root/dialog-root.js +49 -23
  45. package/dist/donut-chart/root/donut-chart-root.d.ts +25 -4
  46. package/dist/donut-chart/root/donut-chart-root.js +21 -6
  47. package/dist/drawer/root/drawer-root.d.ts +63 -2
  48. package/dist/drawer/root/drawer-root.js +95 -19
  49. package/dist/drawer/swipe-start.d.ts +9 -0
  50. package/dist/drawer/swipe-start.js +12 -0
  51. package/dist/element-events.generated.js +8 -5
  52. package/dist/empty/root/empty-root.d.ts +1 -1
  53. package/dist/file-input/root/file-input-root.d.ts +47 -0
  54. package/dist/file-input/root/file-input-root.js +60 -22
  55. package/dist/file-input/screen.d.ts +35 -0
  56. package/dist/file-input/screen.js +49 -0
  57. package/dist/filter-input/root/filter-input-root.d.ts +38 -4
  58. package/dist/filter-input/root/filter-input-root.js +67 -3
  59. package/dist/filter-input/suggest.d.ts +17 -0
  60. package/dist/filter-input/suggest.js +23 -0
  61. package/dist/format-date/options.d.ts +33 -0
  62. package/dist/format-date/options.js +42 -0
  63. package/dist/format-date/root/format-date-root.d.ts +51 -0
  64. package/dist/format-date/root/format-date-root.js +58 -38
  65. package/dist/heatmap/root/heatmap-root.d.ts +23 -6
  66. package/dist/heatmap/root/heatmap-root.js +24 -13
  67. package/dist/icon/index.d.ts +2 -0
  68. package/dist/icon/index.js +2 -0
  69. package/dist/icon/registry.d.ts +37 -0
  70. package/dist/icon/registry.js +83 -0
  71. package/dist/icon/root/icon-root.d.ts +30 -4
  72. package/dist/icon/root/icon-root.js +41 -6
  73. package/dist/icon/root/index.d.ts +2 -0
  74. package/dist/icon/root/index.js +2 -0
  75. package/dist/icon/set.d.ts +1 -0
  76. package/dist/icon/set.js +3 -0
  77. package/dist/image/root/image-root.d.ts +14 -0
  78. package/dist/image/root/image-root.js +15 -2
  79. package/dist/input/root/input-root.d.ts +11 -0
  80. package/dist/input/root/input-root.js +15 -0
  81. package/dist/kbd/root/kbd-root.d.ts +20 -13
  82. package/dist/kbd/root/kbd-root.js +35 -37
  83. package/dist/kbd/shortcut.d.ts +18 -0
  84. package/dist/kbd/shortcut.js +35 -0
  85. package/dist/legend/hidden-series.d.ts +8 -0
  86. package/dist/legend/hidden-series.js +13 -0
  87. package/dist/legend/root/legend-root.d.ts +56 -0
  88. package/dist/legend/root/legend-root.js +54 -5
  89. package/dist/lightbox/item/lightbox-item.d.ts +15 -0
  90. package/dist/lightbox/item/lightbox-item.js +10 -1
  91. package/dist/lightbox/root/lightbox-root.d.ts +61 -1
  92. package/dist/lightbox/root/lightbox-root.js +94 -4
  93. package/dist/line-chart/root/line-chart-root.d.ts +11 -3
  94. package/dist/line-chart/root/line-chart-root.js +19 -5
  95. package/dist/listbox/range.d.ts +17 -0
  96. package/dist/listbox/range.js +5 -0
  97. package/dist/listbox/root/listbox-root.d.ts +74 -3
  98. package/dist/listbox/root/listbox-root.js +87 -9
  99. package/dist/loader/busy.d.ts +17 -0
  100. package/dist/loader/busy.js +24 -0
  101. package/dist/loader/root/loader-root.d.ts +37 -0
  102. package/dist/loader/root/loader-root.js +52 -3
  103. package/dist/meter/root/meter-root.d.ts +7 -0
  104. package/dist/meter/root/meter-root.js +6 -3
  105. package/dist/notifications/feed-keys.d.ts +10 -0
  106. package/dist/notifications/feed-keys.js +14 -0
  107. package/dist/notifications/notification/notification.d.ts +17 -0
  108. package/dist/notifications/notification/notification.js +21 -2
  109. package/dist/notifications/root/notifications-root.d.ts +42 -0
  110. package/dist/notifications/root/notifications-root.js +60 -1
  111. package/dist/number-input/root/number-input-root.d.ts +47 -0
  112. package/dist/number-input/root/number-input-root.js +58 -22
  113. package/dist/object-inspector/root/object-inspector-root.d.ts +1 -0
  114. package/dist/object-inspector/root/object-inspector-root.js +1 -1
  115. package/dist/option/root/option-root.d.ts +54 -5
  116. package/dist/option/root/option-root.js +34 -5
  117. package/dist/pin-input/format.d.ts +7 -0
  118. package/dist/pin-input/format.js +16 -0
  119. package/dist/pin-input/root/pin-input-root.d.ts +21 -0
  120. package/dist/pin-input/root/pin-input-root.js +29 -2
  121. package/dist/popover/root/popover-root.d.ts +62 -4
  122. package/dist/popover/root/popover-root.js +53 -5
  123. package/dist/progress/root/progress-root.d.ts +7 -0
  124. package/dist/progress/root/progress-root.js +9 -3
  125. package/dist/qr-code/export.d.ts +31 -0
  126. package/dist/qr-code/export.js +10 -0
  127. package/dist/qr-code/root/qr-code-root.d.ts +26 -0
  128. package/dist/qr-code/root/qr-code-root.js +49 -2
  129. package/dist/radio/group/radio-group.d.ts +23 -0
  130. package/dist/radio/group/radio-group.js +18 -0
  131. package/dist/rating/root/rating-root.d.ts +25 -0
  132. package/dist/rating/root/rating-root.js +45 -3
  133. package/dist/scatter-chart/root/scatter-chart-root.d.ts +17 -3
  134. package/dist/scatter-chart/root/scatter-chart-root.js +25 -7
  135. package/dist/select/root/select-root.d.ts +24 -0
  136. package/dist/select/root/select-root.js +20 -6
  137. package/dist/skeleton/root/skeleton-root.d.ts +25 -0
  138. package/dist/skeleton/root/skeleton-root.js +17 -2
  139. package/dist/slider/root/slider-root.d.ts +39 -0
  140. package/dist/slider/root/slider-root.js +45 -9
  141. package/dist/stat/root/stat-root.d.ts +2 -0
  142. package/dist/stat/root/stat-root.js +1 -1
  143. package/dist/steps/step/step.d.ts +1 -0
  144. package/dist/steps/step/step.js +1 -1
  145. package/dist/switch/root/switch-root.d.ts +12 -2
  146. package/dist/switch/root/switch-root.js +6 -1
  147. package/dist/tag/group/tag-group.d.ts +107 -0
  148. package/dist/tag/group/tag-group.js +157 -0
  149. package/dist/tag/index.d.ts +1 -0
  150. package/dist/tag/index.js +1 -0
  151. package/dist/tag/root/tag-root.d.ts +60 -0
  152. package/dist/tag/root/tag-root.js +74 -18
  153. package/dist/tag/selection.d.ts +10 -0
  154. package/dist/tag/selection.js +12 -0
  155. package/dist/textarea/grow.d.ts +22 -0
  156. package/dist/textarea/grow.js +20 -0
  157. package/dist/textarea/root/textarea-root.d.ts +9 -0
  158. package/dist/textarea/root/textarea-root.js +9 -1
  159. package/dist/time-input/root/time-input-root.d.ts +44 -5
  160. package/dist/time-input/root/time-input-root.js +69 -39
  161. package/dist/time-picker/root/time-picker-root.d.ts +54 -8
  162. package/dist/time-picker/root/time-picker-root.js +121 -91
  163. package/dist/toast/hotkey.d.ts +22 -0
  164. package/dist/toast/hotkey.js +23 -0
  165. package/dist/toast/item/toast-item.d.ts +16 -1
  166. package/dist/toast/item/toast-item.js +41 -13
  167. package/dist/toast/root/toast-root.d.ts +42 -8
  168. package/dist/toast/root/toast-root.js +64 -4
  169. package/dist/toggle/group/toggle-group.d.ts +57 -15
  170. package/dist/toggle/group/toggle-group.js +69 -9
  171. package/dist/toggle/root/toggle-root.d.ts +12 -1
  172. package/dist/toggle/root/toggle-root.js +11 -3
  173. package/dist/tooltip/focus-target.d.ts +6 -0
  174. package/dist/tooltip/focus-target.js +9 -0
  175. package/dist/tooltip/root/tooltip-root.d.ts +42 -17
  176. package/dist/tooltip/root/tooltip-root.js +50 -9
  177. package/dist/tour/naming.d.ts +30 -0
  178. package/dist/tour/naming.js +10 -0
  179. package/dist/tour/root/tour-root.d.ts +35 -0
  180. package/dist/tour/root/tour-root.js +56 -5
  181. package/dist/tour/stop/tour-stop.d.ts +53 -8
  182. package/dist/tour/stop/tour-stop.js +45 -15
  183. package/dist/tour/wait.d.ts +11 -0
  184. package/dist/tour/wait.js +19 -0
  185. package/dist/trend/root/trend-root.d.ts +13 -0
  186. package/dist/trend/root/trend-root.js +14 -9
  187. package/lint.json +1 -1
  188. package/package.json +5 -5
  189. package/types/jsx.d.ts +4862 -254
  190. package/types/svelte.d.ts +1 -0
  191. package/types/vue.d.ts +1149 -67
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, customElement, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerField, parseColor, toCss, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, colorName, customElement, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerField, parseColor, toCss, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
@@ -17,6 +17,8 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
17
17
  });
18
18
  this.alpha = false;
19
19
  this.autocomplete = 'off';
20
+ this.channelFields = false;
21
+ this.colorWords = {};
20
22
  this.noFormat = false;
21
23
  this.placeholder = '#000000';
22
24
  this.readOnly = false;
@@ -70,6 +72,13 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
70
72
  get picked() {
71
73
  return parseColor(this.value) ? this.value : this.settled;
72
74
  }
75
+ get swatchWords() {
76
+ const parsed = parseColor(this.value);
77
+ if (!parsed) {
78
+ return this.value || this.triggerLabel;
79
+ }
80
+ return `${colorName(parsed, this.colorWords, this.lang)} ${this.value}`;
81
+ }
73
82
  get painted() {
74
83
  const parsed = parseColor(this.value);
75
84
  return parsed ? toCss(parsed) : 'transparent';
@@ -107,15 +116,19 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
107
116
  channel-marker: picker-channel-marker, channel-marker-label: picker-channel-marker-label,
108
117
  channel-markers: picker-channel-markers, channel-output: picker-channel-output,
109
118
  channels: picker-channels, controls: picker-controls, eyedropper: picker-eyedropper,
110
- eyedropper-button: picker-eyedropper-button, eyedropper-text: picker-eyedropper-text, footer: picker-footer, format: picker-format,
111
- format-button: picker-format-button, format-text: picker-format-text, grid: picker-grid,
119
+ eyedropper-button: picker-eyedropper-button, eyedropper-live: picker-eyedropper-live, eyedropper-text: picker-eyedropper-text,
120
+ field: picker-field, field-input: picker-field-input, field-label: picker-field-label,
121
+ fields: picker-fields, footer: picker-footer, format: picker-format,
122
+ format-button: picker-format-button, format-live: picker-format-live, format-text: picker-format-text, grid: picker-grid,
112
123
  grid-brightness: picker-grid-brightness, grid-handle: picker-grid-handle,
113
124
  grid-saturation: picker-grid-saturation, icon: picker-icon, sample: picker-sample,
114
125
  swatch: picker-swatch, swatches: picker-swatches, value: picker-value"
115
126
  ?invalid=${this.resolvedFeedback !== ''}
116
127
  ?alpha=${this.alpha}
128
+ ?channel-fields=${this.channelFields}
117
129
  ?no-format=${this.noFormat}
118
130
  ?no-value=${this.variant !== 'swatch'}
131
+ .colorWords=${this.colorWords}
119
132
  .format=${this.format ?? 'hex'}
120
133
  .value=${this.picked}
121
134
  swatches=${this.swatches || nothing}
@@ -182,7 +195,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
182
195
  ></span>
183
196
  <slot name="trigger"></slot>
184
197
  ${this.variant === 'swatch'
185
- ? html `<span id="swatch-value" hidden>${this.value || this.triggerLabel}</span>`
198
+ ? html `<span id="swatch-value" hidden>${this.swatchWords}</span>`
186
199
  : nothing}
187
200
  </button>
188
201
  </div>
@@ -207,6 +220,12 @@ __decorate([
207
220
  __decorate([
208
221
  property()
209
222
  ], HelixHeadlessColorInput.prototype, "autocomplete", void 0);
223
+ __decorate([
224
+ property({ attribute: 'channel-fields', type: Boolean })
225
+ ], HelixHeadlessColorInput.prototype, "channelFields", void 0);
226
+ __decorate([
227
+ property({ attribute: 'color-words', type: Object })
228
+ ], HelixHeadlessColorInput.prototype, "colorWords", void 0);
210
229
  __decorate([
211
230
  property({ reflect: true })
212
231
  ], HelixHeadlessColorInput.prototype, "format", void 0);
@@ -0,0 +1,26 @@
1
+ import { type Color, type ColorFormat } from '@fusion.dev/helix-core';
2
+ /** One number a color is written with, in the syntax the picker writes. */
3
+ export type ChannelName = 'alpha' | 'blue' | 'green' | 'hue' | 'lightness' | 'red' | 'saturation';
4
+ /** A channel as a field shows it: its number and the range it may take. */
5
+ export interface ChannelField {
6
+ channel: ChannelName;
7
+ max: number;
8
+ min: number;
9
+ value: number;
10
+ }
11
+ /**
12
+ * The numbers a color is written with in `format`, for a field each, as React
13
+ * Aria's ColorField takes a `channel`: red, green and blue for hex and rgb,
14
+ * hue, saturation and lightness for hsl, and the opacity where the picker
15
+ * carries one.
16
+ *
17
+ * Hex is written as bytes, so its fields are the bytes: a field of two hex
18
+ * digits would ask a reader to count in base sixteen.
19
+ */
20
+ export declare const channelFields: (value: string, color: Color, format: ColorFormat, alpha: boolean) => ChannelField[];
21
+ /**
22
+ * The value with one channel set, kept inside the channel's range and written
23
+ * in `format` the way the picker writes every value: the comma form, and the
24
+ * opacity only with `alpha`. `null` where the number is no number.
25
+ */
26
+ export declare const writeChannel: (fields: readonly ChannelField[], format: ColorFormat, alpha: boolean, channel: ChannelName, value: number) => string | null;
@@ -0,0 +1,84 @@
1
+ import { toHsl, toRgb } from '@fusion.dev/helix-core';
2
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
3
+ const percent = (token) => {
4
+ if (token === undefined) {
5
+ return 100;
6
+ }
7
+ const number = Number.parseFloat(token);
8
+ return Number.isNaN(number) ? 100 : clamp(token.includes('%') ? number : number * 100, 0, 100);
9
+ };
10
+ const readWritten = (value, format) => {
11
+ const text = value.trim().toLowerCase();
12
+ if (format !== 'hsl' && /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.test(text)) {
13
+ const digits = text.slice(1);
14
+ const width = digits.length <= 4 ? 1 : 2;
15
+ const byte = (index) => {
16
+ const piece = digits.slice(index * width, index * width + width);
17
+ return Number.parseInt(width === 1 ? piece + piece : piece, 16);
18
+ };
19
+ const count = digits.length / width;
20
+ return [byte(0), byte(1), byte(2), count === 4 ? Math.round((byte(3) / 255) * 100) : 100];
21
+ }
22
+ const call = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
23
+ if (!call?.[1] || call[2] === undefined) {
24
+ return null;
25
+ }
26
+ if (call[1].startsWith('hsl') !== (format === 'hsl')) {
27
+ return null;
28
+ }
29
+ const [first, second, third, fourth] = call[2].replace(/[,/]/g, ' ').split(/\s+/).filter(Boolean);
30
+ const numbers = [first, second, third].map((one) => Number.parseFloat(one ?? ''));
31
+ if (numbers.some(Number.isNaN)) {
32
+ return null;
33
+ }
34
+ return [...numbers, percent(fourth)].map((one) => Math.round(one));
35
+ };
36
+ export const channelFields = (value, color, format, alpha) => {
37
+ const written = readWritten(value, format);
38
+ const fromModel = () => {
39
+ if (format === 'hsl') {
40
+ const { hue, lightness, saturation } = toHsl(color);
41
+ return [Math.round(hue), saturation, lightness, Math.round(color.alpha)];
42
+ }
43
+ const { blue, green, red } = toRgb(color);
44
+ return [red, green, blue, Math.round(color.alpha)];
45
+ };
46
+ const [one = 0, two = 0, three = 0, four = 100] = written ?? fromModel();
47
+ const fields = format === 'hsl'
48
+ ? [
49
+ { channel: 'hue', max: 360, min: 0, value: one },
50
+ { channel: 'saturation', max: 100, min: 0, value: two },
51
+ { channel: 'lightness', max: 100, min: 0, value: three },
52
+ ]
53
+ : [
54
+ { channel: 'red', max: 255, min: 0, value: one },
55
+ { channel: 'green', max: 255, min: 0, value: two },
56
+ { channel: 'blue', max: 255, min: 0, value: three },
57
+ ];
58
+ return alpha ? [...fields, { channel: 'alpha', max: 100, min: 0, value: four }] : fields;
59
+ };
60
+ export const writeChannel = (fields, format, alpha, channel, value) => {
61
+ if (Number.isNaN(value)) {
62
+ return null;
63
+ }
64
+ const numbers = new Map(fields.map((field) => [field.channel, field.value]));
65
+ const field = fields.find((one) => one.channel === channel);
66
+ if (field === undefined) {
67
+ return null;
68
+ }
69
+ numbers.set(channel, clamp(Math.round(value), field.min, field.max));
70
+ const get = (name, fallback = 0) => numbers.get(name) ?? fallback;
71
+ const opacity = get('alpha', 100);
72
+ const fraction = Number((opacity / 100).toFixed(2));
73
+ if (format === 'hsl') {
74
+ const body = `${get('hue')}, ${get('saturation')}%, ${get('lightness')}%`;
75
+ return alpha ? `hsla(${body}, ${fraction})` : `hsl(${body})`;
76
+ }
77
+ if (format === 'rgb') {
78
+ const body = `${get('red')}, ${get('green')}, ${get('blue')}`;
79
+ return alpha ? `rgba(${body}, ${fraction})` : `rgb(${body})`;
80
+ }
81
+ const byte = (one) => one.toString(16).padStart(2, '0');
82
+ const hex = `#${byte(get('red'))}${byte(get('green'))}${byte(get('blue'))}`;
83
+ return alpha ? hex + byte(Math.round((opacity / 100) * 255)) : hex;
84
+ };
@@ -1,4 +1,4 @@
1
- import { type Color, type ColorFormat, HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { type Color, type ColorFormat, type ColorWords, HelixFormElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../button/index.js';
4
4
  import '../../button-group/index.js';
@@ -27,6 +27,11 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
27
27
  * when a drag, a key, a typed value or a preset settles it, as
28
28
  * `<input type="color">` does.
29
29
  *
30
+ * A screen reader hears the color's name, such as "vibrant red", after the two
31
+ * numbers on the square and after the number on the hue ramp, and each preset
32
+ * is named by its color. Set `color-words` to speak the names in another
33
+ * language.
34
+ *
30
35
  * @since 0.1.0
31
36
  * @status stable
32
37
  * @category forms
@@ -57,11 +62,17 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
57
62
  * @csspart controls - The row under the square.
58
63
  * @csspart eyedropper - The control that samples a color from the screen.
59
64
  * @csspart eyedropper-button - The button inside `eyedropper`.
60
- * @csspart eyedropper-text - The words on that button, trimmed to their capitals and baseline.
65
+ * @csspart eyedropper-live - The live region of the button inside `eyedropper`, which says its pending label.
66
+ * @csspart eyedropper-text - The words on the button inside `eyedropper`, trimmed to their capitals and baseline.
67
+ * @csspart field - One channel's number field with its label, in `channel-fields`.
68
+ * @csspart field-input - The number input in a channel's field.
69
+ * @csspart field-label - The name above a channel's number input.
70
+ * @csspart fields - The row of channel number fields, in `channel-fields`.
61
71
  * @csspart footer - The row holding the value and the tools.
62
72
  * @csspart format - The control that changes the syntax the value is written in.
63
73
  * @csspart format-button - The button inside `format`.
64
- * @csspart format-text - The words on that button, trimmed to their capitals and baseline.
74
+ * @csspart format-live - The live region of the button inside `format`, which says its pending label.
75
+ * @csspart format-text - The words on the button inside `format`, trimmed to their capitals and baseline.
65
76
  * @csspart grid - The saturation and brightness square. Draw the mark that shows
66
77
  * where the color sits with `::part(grid)::after` in your stylesheet. The
67
78
  * square carries the two channels as `--color-picker-saturation` and
@@ -99,6 +110,10 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
99
110
  * The message shown when the value is text that is not a color.
100
111
  */
101
112
  badInputMessage?: string;
113
+ /**
114
+ * What the blue channel's field is called, in `channel-fields`.
115
+ */
116
+ blueLabel: string;
102
117
  /**
103
118
  * What the square's vertical ramp is called.
104
119
  *
@@ -106,6 +121,32 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
106
121
  * language hears its own rather than English between its own labels.
107
122
  */
108
123
  brightnessLabel: string;
124
+ /**
125
+ * Whether the picker shows a number field for each channel of the color, as
126
+ * React Aria's ColorField does with `channel`: red, green and blue for hex
127
+ * and rgb, hue, saturation and lightness for hsl, and the opacity with
128
+ * `alpha`.
129
+ *
130
+ * For a reader who has the numbers from a brand guide, and for a keyboard,
131
+ * where typing 128 is quicker than holding an arrow key. The fields follow
132
+ * the syntax the format toggle sets.
133
+ */
134
+ channelFields: boolean;
135
+ /**
136
+ * The words a color's name is spoken in, as JSON keyed by their English,
137
+ * such as `{"red": "rot", "vibrant": "kräftiges"}`.
138
+ *
139
+ * **A color is named the way React Aria names one**, as "vibrant red" or
140
+ * "dark grayish blue", and the keys are the ones `@react-stately/color`
141
+ * translates, so a table a page already has for it can be handed over whole.
142
+ * `colorName` and `transparentColorName` are the two sentences that put the
143
+ * words together. A word left out is spoken in English.
144
+ *
145
+ * The name follows the numbers on the square, follows the number on the hue
146
+ * ramp, and names each preset, so a reader who cannot see the color hears
147
+ * which one it is.
148
+ */
149
+ colorWords: Partial<ColorWords>;
109
150
  /**
110
151
  * The text the sample shows for a moment after it copies the value.
111
152
  */
@@ -130,6 +171,10 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
130
171
  * What the syntax toggle is called, with `{format}` filled in.
131
172
  */
132
173
  formatLabel: string;
174
+ /**
175
+ * What the green channel's field is called, in `channel-fields`.
176
+ */
177
+ greenLabel: string;
133
178
  /**
134
179
  * What the hue channel is called.
135
180
  */
@@ -144,6 +189,11 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
144
189
  * thing the field cannot draw from outside, the mark on the answer itself.
145
190
  */
146
191
  invalid: boolean;
192
+ /**
193
+ * What the lightness channel's field is called, in `channel-fields` with
194
+ * hsl.
195
+ */
196
+ lightnessLabel: string;
147
197
  /**
148
198
  * Hides the eyedropper. Without it, the eyedropper appears in browsers that
149
199
  * support one.
@@ -204,6 +254,10 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
204
254
  * letting it open would take a color from the reader and drop it.
205
255
  */
206
256
  readOnly: boolean;
257
+ /**
258
+ * What the red channel's field is called, in `channel-fields`.
259
+ */
260
+ redLabel: string;
207
261
  /**
208
262
  * Requires a color before the form can submit.
209
263
  *
@@ -407,13 +461,35 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
407
461
  protected updated(changed: import('lit').PropertyValues): void;
408
462
  /** Moves focus to the square. */
409
463
  focus(options?: FocusOptions): void;
464
+ /** What each channel's field is called, from the words a page gives. */
465
+ private channelWord;
466
+ /**
467
+ * Takes a number typed into a channel's field. A field emptied or holding
468
+ * something that is not a number moves nothing, so a reader can clear it to
469
+ * type afresh.
470
+ */
471
+ private readonly handleField;
472
+ /**
473
+ * Takes a value written out whole, as `move` takes a part of the model: the
474
+ * square follows it, keeping its hue where the value is a gray with none to
475
+ * give, and a refusal puts the square back.
476
+ */
477
+ private takeWritten;
478
+ /**
479
+ * A number field for each channel, behind a seam the styled tier can move.
480
+ *
481
+ * `live` on the value, so a number put back inside its range, 300 for a byte,
482
+ * is redrawn though the value it settles on is the one drawn before.
483
+ */
484
+ protected renderFields(color: Color): TemplateResult;
410
485
  /**
411
486
  * One of the ramps under the square.
412
487
  *
413
488
  * `protected` rather than `private`, the method being the seam a tier above
414
- * renders its own ramp through.
489
+ * renders its own ramp through. `spoken` is what a screen reader says for the
490
+ * ramp's value, with `{value}` filled in, and empty says the number alone.
415
491
  */
416
- protected renderChannel(channel: 'alpha' | 'hue', label: string, max: number, value: number): TemplateResult;
492
+ protected renderChannel(channel: 'alpha' | 'hue', label: string, max: number, value: number, spoken?: string): TemplateResult;
417
493
  /**
418
494
  * A slider's own request, kept inside.
419
495
  *
@@ -4,11 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { CopyController, capturePointer, customElement, drawIcon, fillTemplate, formatColor, HelixFormElement, HelixSampleEndEvent, HelixSampleStartEvent, hostAria, isRtl, MixinControllerValue, parseColor, splitValues, toCss, } from '@fusion.dev/helix-core';
7
+ import { CopyController, capturePointer, colorName, customElement, drawIcon, fillTemplate, formatColor, HelixFormElement, HelixSampleEndEvent, HelixSampleStartEvent, hostAria, hueName, isRtl, MixinControllerValue, parseColor, splitValues, toCss, } from '@fusion.dev/helix-core';
8
8
  import { eyedropper } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
+ import { live } from 'lit/directives/live.js';
11
12
  import { html } from 'lit/static-html.js';
13
+ import { channelFields, writeChannel } from '../channels.js';
12
14
  import '../../button/index.js';
13
15
  import '../../button-group/index.js';
14
16
  import '../../slider/index.js';
@@ -19,19 +21,25 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
19
21
  super(...arguments);
20
22
  this.alpha = false;
21
23
  this.autocomplete = 'off';
24
+ this.blueLabel = 'Blue';
22
25
  this.brightnessLabel = 'Brightness';
26
+ this.channelFields = false;
27
+ this.colorWords = {};
23
28
  this.copiedLabel = 'Copied';
24
29
  this.copyLabel = 'Copy {value}';
25
30
  this.eyedropperLabel = 'Sample a color from the screen';
26
31
  this.formatLabel = 'Syntax: {format}. Change it.';
32
+ this.greenLabel = 'Green';
27
33
  this.hueLabel = 'Hue';
28
34
  this.invalid = false;
35
+ this.lightnessLabel = 'Lightness';
29
36
  this.noEyedropper = false;
30
37
  this.noFormat = false;
31
38
  this.noValue = false;
32
39
  this.opacityLabel = 'Opacity';
33
40
  this.presetsLabel = 'Presets';
34
41
  this.readOnly = false;
42
+ this.redLabel = 'Red';
35
43
  this.required = false;
36
44
  this.saturationLabel = 'Saturation';
37
45
  this.swatches = '';
@@ -156,6 +164,18 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
156
164
  this.move(next);
157
165
  this.settle();
158
166
  };
167
+ this.handleField = (channel, event) => {
168
+ event.stopPropagation();
169
+ const typed = event.target.value;
170
+ if (typed.trim() === '') {
171
+ return;
172
+ }
173
+ const format = this.format ?? 'hex';
174
+ const written = writeChannel(channelFields(this.value, this.color, format, this.alpha), format, this.alpha, channel, Number(typed));
175
+ if (written !== null) {
176
+ this.takeWritten(written);
177
+ }
178
+ };
159
179
  this.keepChannelRequest = (event) => {
160
180
  event.stopPropagation();
161
181
  };
@@ -223,7 +243,56 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
223
243
  focus(options) {
224
244
  this.partNamed('grid-saturation')?.focus(options);
225
245
  }
226
- renderChannel(channel, label, max, value) {
246
+ channelWord(channel) {
247
+ return {
248
+ alpha: this.opacityLabel,
249
+ blue: this.blueLabel,
250
+ green: this.greenLabel,
251
+ hue: this.hueLabel,
252
+ lightness: this.lightnessLabel,
253
+ red: this.redLabel,
254
+ saturation: this.saturationLabel,
255
+ }[channel];
256
+ }
257
+ takeWritten(next) {
258
+ const parsed = parseColor(next);
259
+ if (this.readOnly || parsed === null) {
260
+ return;
261
+ }
262
+ const before = this.moved;
263
+ const hueless = parsed.saturation === 0 || parsed.brightness === 0;
264
+ this.moved = hueless ? { ...parsed, hue: this.color.hue } : parsed;
265
+ this.owned = next !== this.value;
266
+ if (!this.requestValue(next)) {
267
+ this.moved = before;
268
+ this.owned = false;
269
+ }
270
+ }
271
+ renderFields(color) {
272
+ return html `<div part="fields">
273
+ ${channelFields(this.value, color, this.format ?? 'hex', this.alpha).map((field) => html `<label part="field">
274
+ <span part="field-label">${this.channelWord(field.channel)}</span>
275
+ <input
276
+ part="field-input"
277
+ type="number"
278
+ inputmode="numeric"
279
+ min=${field.min}
280
+ max=${field.max}
281
+ step="1"
282
+ .value=${live(String(field.value))}
283
+ ?disabled=${this.isDisabled}
284
+ ?readonly=${this.readOnly}
285
+ @input=${(event) => this.handleField(field.channel, event)}
286
+ @change=${(event) => {
287
+ event.stopPropagation();
288
+ this.settle();
289
+ this.requestUpdate();
290
+ }}
291
+ />
292
+ </label>`)}
293
+ </div>`;
294
+ }
295
+ renderChannel(channel, label, max, value, spoken = '') {
227
296
  return html `<${this.tag('slider')}
228
297
  part="channel"
229
298
  exportparts="control: channel-control, feedback: channel-feedback, field: channel-field,
@@ -234,6 +303,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
234
303
  label=${label}
235
304
  max=${max}
236
305
  min="0"
306
+ value-text=${spoken || nothing}
237
307
  .value=${String(value)}
238
308
  ?disabled=${this.isDisabled}
239
309
  @input=${(event) => this.takeChannel(channel, event)}
@@ -279,7 +349,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
279
349
  ${this.sampling && !this.noEyedropper
280
350
  ? html `<${this.tag('button')}
281
351
  part="eyedropper"
282
- exportparts="button: eyedropper-button, text: eyedropper-text"
352
+ exportparts="button: eyedropper-button, live: eyedropper-live, text: eyedropper-text"
283
353
  label=${this.eyedropperLabel}
284
354
  ?disabled=${this.isDisabled}
285
355
  @click=${this.handleEyeDropper}
@@ -291,7 +361,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
291
361
  ? nothing
292
362
  : html `<${this.tag('button')}
293
363
  part="format"
294
- exportparts="button: format-button, text: format-text"
364
+ exportparts="button: format-button, live: format-live, text: format-text"
295
365
  label=${fillTemplate(this.formatLabel, { format: this.format ?? 'hex' })}
296
366
  ?disabled=${this.isDisabled}
297
367
  @click=${this.handleFormat}
@@ -303,8 +373,12 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
303
373
  render() {
304
374
  const color = this.color;
305
375
  const painted = toCss(color);
306
- const both = `${this.saturationLabel} ${color.saturation}%, ${this.brightnessLabel} ${color.brightness}%`;
307
- const reversed = `${this.brightnessLabel} ${color.brightness}%, ${this.saturationLabel} ${color.saturation}%`;
376
+ const named = colorName(color, this.colorWords, this.lang);
377
+ const both = `${this.saturationLabel} ${color.saturation}%, ${this.brightnessLabel} ${color.brightness}%, ${named}`;
378
+ const reversed = `${this.brightnessLabel} ${color.brightness}%, ${this.saturationLabel} ${color.saturation}%, ${named}`;
379
+ const hue = hueName({ alpha: 100, brightness: 100, hue: color.hue, saturation: 100 }, this.colorWords, this.lang);
380
+ const owner = this.getAttribute('aria-label')?.trim();
381
+ const axisName = (channel) => (owner ? `${owner}, ${channel}` : channel);
308
382
  return html `<div
309
383
  part="grid"
310
384
  style="--color-picker-hue: ${color.hue}; --color-picker-saturation: ${color.saturation}%; --color-picker-brightness: ${color.brightness}%"
@@ -319,7 +393,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
319
393
  type="range"
320
394
  min="0"
321
395
  max="100"
322
- aria-label=${this.saturationLabel}
396
+ aria-label=${axisName(this.saturationLabel)}
323
397
  aria-valuetext=${both}
324
398
  aria-invalid=${this.invalid ? 'true' : nothing}
325
399
  .value=${String(color.saturation)}
@@ -334,7 +408,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
334
408
  min="0"
335
409
  max="100"
336
410
  tabindex="-1"
337
- aria-label=${this.brightnessLabel}
411
+ aria-label=${axisName(this.brightnessLabel)}
338
412
  aria-valuetext=${reversed}
339
413
  aria-invalid=${this.invalid ? 'true' : nothing}
340
414
  .value=${String(color.brightness)}
@@ -356,10 +430,11 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
356
430
  @click=${() => this.copier.copy(this.value)}
357
431
  ></button>
358
432
  <div part="channels">
359
- ${this.renderChannel('hue', this.hueLabel, 360, color.hue)}
433
+ ${this.renderChannel('hue', this.hueLabel, 360, color.hue, `{value}, ${hue}`)}
360
434
  ${this.alpha ? this.renderChannel('alpha', this.opacityLabel, 100, color.alpha) : nothing}
361
435
  </div>
362
436
  </div>
437
+ ${this.channelFields ? this.renderFields(color) : nothing}
363
438
 
364
439
  <!-- The syntax and the presets share a row, because neither fills one.
365
440
  Above them the ramps get the whole width, which is what they need:
@@ -395,7 +470,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
395
470
  return html `<button
396
471
  part="swatch"
397
472
  type="button"
398
- aria-label=${preset}
473
+ aria-label=${parsed ? colorName(parsed, this.colorWords, this.lang) : preset}
399
474
  aria-pressed=${parsed !== null && toCss(parsed) === painted ? 'true' : 'false'}
400
475
  style="--color-picker-swatch: ${preset}"
401
476
  ?disabled=${this.isDisabled}
@@ -414,9 +489,18 @@ __decorate([
414
489
  __decorate([
415
490
  property({ attribute: 'bad-input-message' })
416
491
  ], HelixHeadlessColorPicker.prototype, "badInputMessage", void 0);
492
+ __decorate([
493
+ property({ attribute: 'blue-label' })
494
+ ], HelixHeadlessColorPicker.prototype, "blueLabel", void 0);
417
495
  __decorate([
418
496
  property({ attribute: 'brightness-label' })
419
497
  ], HelixHeadlessColorPicker.prototype, "brightnessLabel", void 0);
498
+ __decorate([
499
+ property({ attribute: 'channel-fields', type: Boolean })
500
+ ], HelixHeadlessColorPicker.prototype, "channelFields", void 0);
501
+ __decorate([
502
+ property({ attribute: 'color-words', type: Object })
503
+ ], HelixHeadlessColorPicker.prototype, "colorWords", void 0);
420
504
  __decorate([
421
505
  property({ attribute: 'copied-label' })
422
506
  ], HelixHeadlessColorPicker.prototype, "copiedLabel", void 0);
@@ -432,12 +516,18 @@ __decorate([
432
516
  __decorate([
433
517
  property({ attribute: 'format-label' })
434
518
  ], HelixHeadlessColorPicker.prototype, "formatLabel", void 0);
519
+ __decorate([
520
+ property({ attribute: 'green-label' })
521
+ ], HelixHeadlessColorPicker.prototype, "greenLabel", void 0);
435
522
  __decorate([
436
523
  property({ attribute: 'hue-label' })
437
524
  ], HelixHeadlessColorPicker.prototype, "hueLabel", void 0);
438
525
  __decorate([
439
526
  property({ reflect: true, type: Boolean })
440
527
  ], HelixHeadlessColorPicker.prototype, "invalid", void 0);
528
+ __decorate([
529
+ property({ attribute: 'lightness-label' })
530
+ ], HelixHeadlessColorPicker.prototype, "lightnessLabel", void 0);
441
531
  __decorate([
442
532
  property({ attribute: 'no-eyedropper', type: Boolean })
443
533
  ], HelixHeadlessColorPicker.prototype, "noEyedropper", void 0);
@@ -456,6 +546,9 @@ __decorate([
456
546
  __decorate([
457
547
  property({ attribute: 'readonly', reflect: true, type: Boolean })
458
548
  ], HelixHeadlessColorPicker.prototype, "readOnly", void 0);
549
+ __decorate([
550
+ property({ attribute: 'red-label' })
551
+ ], HelixHeadlessColorPicker.prototype, "redLabel", void 0);
459
552
  __decorate([
460
553
  property({ reflect: true, type: Boolean })
461
554
  ], HelixHeadlessColorPicker.prototype, "required", void 0);