@fusion.dev/helix-headless 0.2.0 → 0.4.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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -8,152 +8,22 @@ import { AriaForwardController, customElement, HelixFormElement, MixinController
8
8
  import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
- /* Side-effect import, because this renders a picker in its own shadow root and
12
- a custom element registers on import. The path is relative, so this registers
13
- the base picker and the styled file's identical import registers the styled
14
- one; `tier` picks which name the template asks for. */
15
11
  import '../../color-picker/index.js';
16
12
  import { parseColor, toCss } from '../../color-picker/color.js';
17
- /**
18
- * @summary A color, typed in any syntax or picked from a panel.
19
- *
20
- * @description
21
- * **The picker is `helix-color-picker`, not a second one written here.** It owns the
22
- * square, the ramps, the eyedropper and the syntax toggle, and it is worth
23
- * having on its own: a settings pane wants a picker flat on the page with
24
- * nothing to open. This element is the field, the overlay, and the form.
25
- * Exactly the arrangement Date Input has with `helix-date-picker`.
26
- *
27
- * **Typed as well as picked, and the typing is the reason this is not
28
- * `<input type="color">`.** The native control gives a free picker and takes
29
- * nearly all styling with it, which is a fair trade until a product needs the
30
- * panel to match everything around it. What is kept from the native one is the
31
- * part worth keeping: a real text field, so a reader who has the hex pastes it
32
- * and never opens anything.
33
- *
34
- * **Every syntax is read; the one `format` names is written.** `#f00`,
35
- * `#ff0000`, `rgb(255 0 0)`, `rgba(255,0,0,.5)`, `rgb(255 0 0 / 50%)` and
36
- * `hsl(0 100% 50%)` are all colors, and a field that refuses one because it
37
- * prefers another is arguing with the person using it. What the field *shows* is
38
- * the value itself, for the reason Date Input gives about its dates: half-typed
39
- * input does not survive a round trip through a formatter.
40
- *
41
- * The custom property below is written by this component and never set by a
42
- * consumer: it carries a value this element resolved and CSS cannot, which is
43
- * what lets a stylesheet on this tier paint the swatch without parsing anything
44
- * itself.
45
- *
46
- * @since 0.1.0
47
- * @status experimental
48
- * @category forms
49
- * @aliases ColorField, ColourInput, InputColor
50
- * @documentation https://helix.fusion.dev/components/color-input
51
- *
52
- * @slot end - Content at the trailing end, before the trigger.
53
- * @slot feedback - What is wrong with it.
54
- * @slot hint - Help under the field.
55
- * @slot start - Content at the leading end of the field, inside its border.
56
- * @slot trigger - The mark on the field saying a panel opens.
57
- *
58
- * @event {Event} change - Announced once the value has moved.
59
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the color changes. Cancelable: refusing it takes the decision and nothing moves.
60
- * @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes. Cancelable: refusing it takes the decision and nothing moves.
61
- * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the panel opens. Cancelable: refusing it takes the decision and nothing moves.
62
- * @event {Event} input - Announced once the value has moved.
63
- * @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
64
- *
65
- * @csspart control - The text input carrying the value. Absent in `swatch`.
66
- * @csspart feedback - What is wrong with it.
67
- * @csspart field - The box holding the text and the trigger.
68
- * @csspart hint - Help under the field, while nothing is wrong.
69
- * @csspart label - The question, over the field.
70
- * @csspart picker - The color picker inside it.
71
- * @csspart picker-actions - The group holding the eyedropper and the syntax toggle.
72
- * @csspart picker-channel - One ramp.
73
- * @csspart picker-channel-control - The range input.
74
- * @csspart picker-channel-feedback - What is wrong with it.
75
- * @csspart picker-channel-field - The row: the control and its readout.
76
- * @csspart picker-channel-hint - Help under the track, while nothing is wrong.
77
- * @csspart picker-channel-label - The question, over the track.
78
- * @csspart picker-channel-marker - One mark.
79
- * @csspart picker-channel-marker-label - The value a mark stands at, when `marker-labels` is set.
80
- * @csspart picker-channel-markers - The row of marks along the track.
81
- * @csspart picker-channel-output - The current value, shown beside it.
82
- * @csspart picker-channels - The ramps beside it.
83
- * @csspart picker-controls - The row under the square.
84
- * @csspart picker-eyedropper - The control that samples a color from the screen.
85
- * @csspart picker-eyedropper-button - The inner control.
86
- * @csspart picker-footer - The row holding the value and the tools.
87
- * @csspart picker-format - The control that changes the syntax the value is written in.
88
- * @csspart picker-format-button - The inner control.
89
- * @csspart picker-grid - The saturation and brightness square.
90
- * @csspart picker-grid-brightness - The range input for its vertical axis.
91
- * @csspart picker-grid-handle - The mark showing where in the square the color sits.
92
- * @csspart picker-grid-saturation - The range input for the square's horizontal axis.
93
- * @csspart picker-icon - A glyph this component draws.
94
- * @csspart picker-sample - The chip of the current color, which copies it when pressed.
95
- * @csspart picker-swatch - One preset.
96
- * @csspart picker-swatches - The row of presets.
97
- * @csspart picker-value - The color as text, which can be typed or pasted.
98
- * @csspart preview - The chip showing the color, inside the trigger.
99
- * @csspart surface - The floating panel.
100
- * @csspart trigger - The control that opens the panel.
101
- *
102
- * @cssproperty --color-input-swatch - The chosen color, to paint over the transparency check.
103
- */
104
13
  let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
105
14
  constructor() {
106
15
  super(...arguments);
107
- /* A consumer's `aria-describedby` names ids in their own tree and the control
108
- is in this shadow root, so the reference has to be carried down as an
109
- element rather than copied as an id. See `AriaForwardController`: measured
110
- in Chrome's own tree, the host held the description and the control the
111
- reader is actually on was exposed without it. */
112
16
  this.aria = new AriaForwardController(this, {
113
17
  target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
114
18
  });
115
- /**
116
- * Whether the color carries an alpha channel.
117
- */
118
19
  this.alpha = false;
119
- /**
120
- * What the browser may autofill in the text field, as a platform token.
121
- *
122
- * **Defaults to `off`, which is not the platform's default and is the
123
- * point.** The field holds a color, so a browser offering to fill it with a
124
- * saved address is competing for the same keystrokes as the picker. A
125
- * consumer who wants autofill can still ask.
126
- */
127
20
  this.autocomplete = 'off';
128
- /**
129
- * Whether the reader may change the format from the panel.
130
- */
131
21
  this.noFormat = false;
132
- /**
133
- * What the field says before a color is in it.
134
- */
135
22
  this.placeholder = '#000000';
136
- /**
137
- * Colors offered under the picker, as a comma-separated list.
138
- */
139
23
  this.swatches = '';
140
- /**
141
- * Accessible name for the control that opens the panel.
142
- */
143
24
  this.triggerLabel = 'Choose a color';
144
- /** The overlay, placed against the field for the reason Date Input gives. */
145
25
  this.panel = new PopoverController(this, {
146
26
  anchor: () => this.renderRoot?.querySelector('[part="field"]'),
147
- /* The surface *and* the trigger.
148
- *
149
- * A control is not always one element: the button that opens this panel is
150
- * part of it, and a press on it is not a press outside. Scoped to the
151
- * surface alone, `pointerdown` on the trigger closes the panel and the
152
- * trigger's own `toggleOpen` reopens it on the `click` that follows, so the
153
- * panel can never be closed by the button that opened it.
154
- *
155
- * `DismissController` documents this exact failure and takes a list for
156
- * it. */
157
27
  boundary: () => [
158
28
  this.renderRoot?.querySelector('[part="surface"]'),
159
29
  this.renderRoot?.querySelector('[part="trigger"]'),
@@ -163,23 +33,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
163
33
  placement: 'bottom-start',
164
34
  surface: () => this.renderRoot?.querySelector('[part="surface"]'),
165
35
  });
166
- /**
167
- * The last value that was a color.
168
- *
169
- * Half-typed input reaches here on every keystroke and most of it is not a
170
- * color, so the picker is given the last thing that was one rather than a
171
- * fallback. Typing `#00ff00` one character at a time goes `#`, `#0`, `#00`,
172
- * `#00f`, `#00ff`, `#00ff0`, `#00ff00`: measured, the square went red, red,
173
- * red, blue, blue, **red**, green, because two of those are colors and the
174
- * ones between are not. Holding the last good one takes the flicker out and
175
- * leaves the square on blue until green arrives.
176
- *
177
- * Not state, because it is only ever read on a render the value already
178
- * caused. The same reasoning Date Input records for its calendar, which will
179
- * not follow a date until the field says something complete.
180
- */
181
36
  this.settled = '';
182
- /** Relayed under this element's name, so a consumer hears one control. */
183
37
  this.handleInput = (event) => {
184
38
  event.stopPropagation();
185
39
  this.requestValue(event.target.value);
@@ -190,21 +44,12 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
190
44
  this.requestValue(event.detail.value);
191
45
  this.restore();
192
46
  };
193
- /**
194
- * The picker is about to take the pointer over, so the panel gets out of the
195
- * way and comes back after.
196
- *
197
- * `helix-color-picker` says what it is doing rather than reaching for a panel it
198
- * cannot know it is in. A floating panel left up during an eyedropper is one
199
- * the reader cannot dismiss and may well be trying to sample from underneath.
200
- */
201
47
  this.handleSampleStart = () => {
202
48
  this.requestOpen(false);
203
49
  };
204
50
  this.handleSampleEnd = () => {
205
51
  this.requestOpen(true);
206
52
  };
207
- /** Escape shuts the panel without moving the color. */
208
53
  this.handleKeydown = (event) => {
209
54
  if (event.key === 'Escape' && this.open) {
210
55
  event.stopPropagation();
@@ -213,14 +58,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
213
58
  }
214
59
  };
215
60
  }
216
- /**
217
- * What the label points at and validity hangs off.
218
- *
219
- * The input where there is one, the trigger where there is not. `setValidity`
220
- * needs an element to anchor the message to and a label needs something
221
- * focusable to name, and in `swatch` the trigger is the only control in the
222
- * field. It carries `id="control"` there for the same reason.
223
- */
224
61
  get anchor() {
225
62
  if (typeof this.renderRoot?.querySelector !== 'function') {
226
63
  return null;
@@ -233,22 +70,13 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
233
70
  }
234
71
  return this.renderRoot.querySelector('[part="control"]');
235
72
  }
236
- /** What the picker should show, which is only ever a color. */
237
73
  get picked() {
238
74
  return parseColor(this.value) ? this.value : this.settled;
239
75
  }
240
- /** What the trigger's swatch paints, which is only ever a color. */
241
76
  get painted() {
242
77
  const parsed = parseColor(this.value);
243
78
  return parsed ? toCss(parsed) : 'transparent';
244
79
  }
245
- /*
246
- * Puts the field back where the answer differs from what was typed.
247
- *
248
- * `requestValue` announces and decides; this is the other half, which Input
249
- * and Date Input both need for the same reason: the native control has already
250
- * changed, so wherever the answer does not follow it, it has to be restored.
251
- */
252
80
  restore() {
253
81
  const control = this.control;
254
82
  if (control && control.value !== this.value) {
@@ -256,7 +84,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
256
84
  }
257
85
  this.sync();
258
86
  }
259
- /** Reports the value and the validity the host stands behind. */
260
87
  sync() {
261
88
  this.internals.setFormValue(this.value);
262
89
  const anchor = this.anchor;
@@ -264,10 +91,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
264
91
  return;
265
92
  }
266
93
  const control = this.control;
267
- /* Checked here rather than left to the input, which is `text` and has no
268
- idea what a color is. `type="color"` is what was given up to own the
269
- panel, and it would not have helped: it has no invalid state, because it
270
- cannot hold one. */
271
94
  if (this.value !== '' && parseColor(this.value) === null) {
272
95
  this.internals.setValidity({ badInput: true }, 'Enter a color, like #ff0000, rgb(255 0 0) or hsl(0 100% 50%).', anchor);
273
96
  }
@@ -275,36 +98,20 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
275
98
  this.internals.setValidity(control.validity, control.validationMessage, control);
276
99
  }
277
100
  else {
278
- /* A swatch has no native validity of its own, so `required` is the only
279
- thing left to check and this element has to check it: an empty value on
280
- a required field is the one state a block of color can be wrong in. */
281
101
  this.internals.setValidity(this.required && this.value === '' ? { valueMissing: true } : {}, this.required && this.value === '' ? 'Choose a color.' : '', anchor);
282
102
  }
283
103
  this.refreshFeedback();
284
104
  }
285
105
  firstUpdated(changed) {
286
106
  super.firstUpdated(changed);
287
- // Whatever the markup arrived with, so a form knows before anything is typed.
288
107
  this.sync();
289
108
  }
290
109
  updated(changed) {
291
110
  super.updated(changed);
292
- /* Every update, not only the ones that name `value`.
293
- `value` is an accessor over `valueState ?? defaultValue`, so a consumer
294
- moving `default-value` on an untouched control moves what the form
295
- submits without ever putting `value` in the changed map, and the reported
296
- entry goes stale. Native `<input>` updates in exactly that case, which is
297
- the behavior the mixin's own notes take as the reference. */
298
111
  this.sync();
299
112
  if (changed.has('value') && parseColor(this.value)) {
300
113
  this.settled = this.value;
301
114
  }
302
- /* Driven from here rather than from the toggle, because `open` is a property
303
- a consumer may set directly and the panel has to follow either way. The
304
- controller is idempotent, so arriving twice for one open costs nothing.
305
- Without this the surface stays `display: none`, a `popover` element being
306
- hidden until something shows it, and the square inside measures zero,
307
- which is the one measurement a drag cannot work without. */
308
115
  if (this.open) {
309
116
  this.panel.show();
310
117
  }
@@ -312,33 +119,9 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinControl
312
119
  this.panel.hide();
313
120
  }
314
121
  }
315
- /** Puts the caret in the field, the way a label pointing at one would. */
316
122
  focus(options) {
317
123
  this.anchor?.focus(options);
318
124
  }
319
- /*
320
- * The picker is told `no-value` because the field above it is that input.
321
- *
322
- * Standing on a page, helix-color-picker states its own color as text: a square and
323
- * two ramps can build one and cannot state one, and a reader who already has a
324
- * hex should not have to find it by dragging. Here that row would be a second
325
- * editable field for the same value, which is two places to type with no
326
- * answer to which one is authoritative or which has the caret.
327
- *
328
- * Not the other way round, this element dropping its own field and letting the
329
- * panel's be the only one: the value could then not be read at a glance or
330
- * pasted without opening a panel.
331
- */
332
- /**
333
- * The picker the field opens, behind a seam the styled tier can move.
334
- *
335
- * A method rather than a fragment inline, so a tier above can render its own,
336
- * wrap this one, pass it a property the base does not know about, or leave it
337
- * out, without reimplementing the whole template to do any of them. The tag is
338
- * interpolated rather than written, so `tier` resolves it: this renders the
339
- * headless element, and a styled subclass that sets `tier` renders the styled one
340
- * from the same template.
341
- */
342
125
  renderPicker() {
343
126
  return html `<${this.tag('color-picker')}
344
127
  part="picker"
@@ -1,31 +1,4 @@
1
- /**
2
- * Reading and writing the color syntaxes a field is allowed to hold.
3
- *
4
- * **The working model is HSB**, hue and saturation and brightness, because that
5
- * is what the panel is: a square of saturation against brightness with a hue
6
- * ramp beside it. HSL would put the square's corners somewhere a reader does not
7
- * expect, since full saturation in HSL is a mid-lightness band rather than a
8
- * corner.
9
- *
10
- * **Every syntax here is read; three are written.** A field that refuses
11
- * `rgb(255 0 0)` because it prefers hex is a field arguing with the person using
12
- * it, so the space-separated forms, the comma forms, `rgba` and `hsla` are all
13
- * taken. What is written back is whichever of hex, rgb or hsl the component is
14
- * set to, so a value stays in the shape the consumer asked for however it was
15
- * entered.
16
- *
17
- * **What is written is the comma form.** Both are valid, and the commas are what
18
- * a reader recognizes, what most code around them holds, and what pastes into
19
- * anything without a second thought. A syntax a value is handed *out* in is not
20
- * the place to be early. `rgba` and `hsla` come with them, since a comma form
21
- * with a fourth argument is spelled that way.
22
- *
23
- * Its own module rather than sitting in the element, because it is arithmetic
24
- * with no DOM in it: this is the half worth testing directly.
25
- */
26
- /** Keeps a number inside a range, since every channel here has one. */
27
1
  const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
28
- /** `50%` and `0.5` mean the same thing wherever an alpha is accepted. */
29
2
  const toAlpha = (token) => {
30
3
  if (token === undefined) {
31
4
  return 100;
@@ -36,7 +9,6 @@ const toAlpha = (token) => {
36
9
  }
37
10
  return clamp(token.includes('%') ? number : number * 100, 0, 100);
38
11
  };
39
- /** A channel written either as `255` or as `100%`. */
40
12
  const toByte = (token) => {
41
13
  const number = Number.parseFloat(token);
42
14
  return clamp(token.includes('%') ? (number / 100) * 255 : number, 0, 255);
@@ -68,7 +40,6 @@ const rgbToHsb = (red, green, blue, alpha) => {
68
40
  saturation: Math.round(high === 0 ? 0 : (span / high) * 100),
69
41
  };
70
42
  };
71
- /** HSB to bytes, which is the road every written syntax goes down. */
72
43
  export const toRgb = (color) => {
73
44
  const hue = (((color.hue % 360) + 360) % 360) / 60;
74
45
  const saturation = color.saturation / 100;
@@ -91,7 +62,6 @@ export const toRgb = (color) => {
91
62
  red: Math.round((red + base) * 255),
92
63
  };
93
64
  };
94
- /** HSB to HSL, which is a different pair of numbers for the same color. */
95
65
  const toHsl = (color) => {
96
66
  const saturation = color.saturation / 100;
97
67
  const brightness = color.brightness / 100;
@@ -104,19 +74,6 @@ const toHsl = (color) => {
104
74
  : (brightness - lightness) / Math.min(lightness, 1 - lightness)) * 100),
105
75
  };
106
76
  };
107
- /**
108
- * Splits a comma-separated list of colors, ignoring the commas inside them.
109
- *
110
- * A plain `split(',')` cannot hold the legacy syntaxes:
111
- * `#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,
112
- * four of them not colors, and the row quietly renders three swatches instead of
113
- * four. The modern space-separated forms survive it, which is what makes such a
114
- * list look like it works.
115
- *
116
- * Depth rather than a regular expression, because the thing that matters is
117
- * whether a comma is inside a function call and that is exactly what a counter
118
- * answers. No color syntax nests, so a single counter is enough.
119
- */
120
77
  export const splitColors = (value) => {
121
78
  const found = [];
122
79
  let depth = 0;
@@ -139,14 +96,6 @@ export const splitColors = (value) => {
139
96
  found.push(current.trim());
140
97
  return found.filter(Boolean);
141
98
  };
142
- /**
143
- * Any of the syntaxes to the working model, or `null` where it is not a color.
144
- *
145
- * Named colors are not here. Resolving `rebeccapurple` means either shipping
146
- * the table of a hundred and forty or asking the browser, and the browser is not
147
- * present on the server: a field that accepted a name client-side and rejected
148
- * it during a server render would be worse than one that never took it.
149
- */
150
99
  export const parseColor = (value) => {
151
100
  const text = value.trim().toLowerCase();
152
101
  if (text === '') {
@@ -167,16 +116,10 @@ export const parseColor = (value) => {
167
116
  const carriesAlpha = digits.length === 4 || digits.length === 8;
168
117
  return rgbToHsb(channel(0), channel(1), channel(2), carriesAlpha ? Math.round((channel(3) / 255) * 100) : 100);
169
118
  }
170
- /* Commas and the slash both become spaces, which collapses every legal
171
- spelling into one list: `rgba(255,0,0,0.5)` and `rgb(255 0 0 / 50%)` are the
172
- same four tokens by the time they are read. */
173
119
  const call = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
174
120
  if (!call?.[1] || call[2] === undefined) {
175
121
  return null;
176
122
  }
177
- /* Destructured with a fallback rather than indexed, because an index into an
178
- array is `string | undefined` here and three of these are read twice. Four
179
- names cost less than six assertions. */
180
123
  const [first = '', second = '', third = '', fourth] = call[2]
181
124
  .replace(/[,/]/g, ' ')
182
125
  .split(/\s+/)
@@ -194,7 +137,6 @@ export const parseColor = (value) => {
194
137
  if (Number.isNaN(hue) || Number.isNaN(saturation) || Number.isNaN(lightness)) {
195
138
  return null;
196
139
  }
197
- // HSL to HSB, the inverse of `toHsl` above.
198
140
  const brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
199
141
  return {
200
142
  alpha,
@@ -203,18 +145,8 @@ export const parseColor = (value) => {
203
145
  saturation: Math.round((brightness === 0 ? 0 : 2 * (1 - lightness / brightness)) * 100),
204
146
  };
205
147
  };
206
- /**
207
- * The working model back out, in the syntax asked for.
208
- *
209
- * The alpha is written only where the component carries one. A consumer who did
210
- * not ask for transparency should not have to strip `ff` off every hex or read
211
- * past a fourth argument they never wanted.
212
- */
213
148
  export const formatColor = (color, format, withAlpha) => {
214
149
  const alpha = clamp(color.alpha, 0, 100) / 100;
215
- /* Two decimal places at most, and no trailing zeros: an alpha is a fraction
216
- here and `0.5` is what a stylesheet holds, not `0.50` and not
217
- `0.5019607843137255`. */
218
150
  const opacity = Number(alpha.toFixed(2));
219
151
  if (format === 'hsl') {
220
152
  const { hue, lightness, saturation } = toHsl(color);
@@ -230,7 +162,6 @@ export const formatColor = (color, format, withAlpha) => {
230
162
  const hex = `#${byte(red)}${byte(green)}${byte(blue)}`;
231
163
  return withAlpha ? hex + byte(Math.round(alpha * 255)) : hex;
232
164
  };
233
- /** The color as CSS, for the swatches and the tracks. Always with its alpha. */
234
165
  export const toCss = (color) => {
235
166
  const { blue, green, red } = toRgb(color);
236
167
  return `rgb(${red} ${green} ${blue} / ${clamp(color.alpha, 0, 100)}%)`;