@fusion.dev/helix-headless 0.2.0 → 0.3.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,15 +8,9 @@ import { valueAsTime as asTime, customElement, timeAsValue as fromTime, HelixFor
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 imports, because this renders these in its own shadow root and a
12
- custom element registers on import. The paths are relative, so this registers
13
- the headless elements and the styled file's identical imports register the styled
14
- ones; `tier` picks which names the template asks for. */
15
11
  import '../../listbox/index.js';
16
12
  import '../../option/index.js';
17
- /** Whether a string is a time the rest of this component can reason about. */
18
13
  export const isTime = (value) => /^([01]\d|2[0-3]):[0-5]\d$/.test(value);
19
- /** Minutes since midnight, or `null` where it is not a time. */
20
14
  export const toMinutes = (value) => {
21
15
  if (!isTime(value)) {
22
16
  return null;
@@ -24,195 +18,40 @@ export const toMinutes = (value) => {
24
18
  const [hours = '0', minutes = '0'] = value.split(':');
25
19
  return Number(hours) * 60 + Number(minutes);
26
20
  };
27
- /** Minutes since midnight back to `HH:mm`, which is the value shape throughout. */
28
21
  export const fromMinutes = (minutes) => {
29
22
  const whole = ((Math.round(minutes) % 1440) + 1440) % 1440;
30
23
  return `${String(Math.floor(whole / 60)).padStart(2, '0')}:${String(whole % 60).padStart(2, '0')}`;
31
24
  };
32
- /**
33
- * A time written the way the reader's locale writes one.
34
- *
35
- * `Intl` decides between a twelve and a twenty-four hour clock, because that is
36
- * a property of the locale rather than of this component: `en-US` gets
37
- * `2:30 PM` and `en-GB` gets `14:30` without either being configured. There is
38
- * no flag to force one, deliberately. A product that needs to override its
39
- * reader's own convention has a locale problem rather than a component one, and
40
- * the value is 24 hour regardless.
41
- */
42
25
  export const writeTime = (value, locale) => {
43
26
  const minutes = toMinutes(value);
44
27
  if (minutes === null) {
45
28
  return value;
46
29
  }
47
30
  const at = new Date(Date.UTC(2000, 0, 1, Math.floor(minutes / 60), minutes % 60));
48
- /* `timeStyle` rather than `hour` and `minute` assembled by hand. Measured in
49
- Chrome: `hour: 'numeric'` gives `9:00` for `en-GB`, unpadded, which is not
50
- how a twenty-four hour clock is written, while `hour: '2-digit'` gives
51
- `09:00 AM` for `en-US`, which is not how a twelve hour one is. `short` is
52
- each locale's own answer: `09:00` and `9:00 AM`. */
53
31
  return new Intl.DateTimeFormat(locale || undefined, {
54
32
  timeStyle: 'short',
55
33
  timeZone: 'UTC',
56
34
  }).format(at);
57
35
  };
58
- /**
59
- * Whether the reader's locale writes times on a twelve hour clock.
60
- *
61
- * Asked of `Intl` rather than derived from a list of locales, because it is
62
- * exactly the question `resolvedOptions` answers and the list is long and
63
- * changes. `en-US` is true, `en-GB`, `fr-FR` and `ja-JP` are false, measured.
64
- */
65
36
  const isTwelveHour = (locale) => new Intl.DateTimeFormat(locale || undefined, { timeStyle: 'short' }).resolvedOptions().hour12 ===
66
37
  true;
67
- /**
68
- * @summary Picks a time, from a list of them.
69
- *
70
- * @description
71
- * **A list rather than three wheels**, and the reason is what a list already
72
- * is. `helix-listbox` brings the keyboard, the typeahead, the scroll into view
73
- * and the announcement of a set of options, and a time at a step is a set of
74
- * options. Three little wheels for hours, minutes and a meridiem are three
75
- * controls to build, three to test, and a tab stop each for something a reader
76
- * answers once.
77
- *
78
- * **A step is what makes it a list at all.** Fifteen minutes is ninety-six rows,
79
- * which a reader scrolls or types into; one minute is one thousand four hundred
80
- * and forty, which is a listbox in name only. So the list is for the times a
81
- * product actually offers, and arbitrary times are the field's job in Input
82
- * Time, the same division Date Input draws between typing a date and using the
83
- * calendar.
84
- *
85
- * **The value is `HH:mm` on a twenty-four hour clock, always.** That is what a
86
- * form submits and what `min` and `max` compare against, and it sorts as a
87
- * string, which is the whole reason the shape is that one. What each row *says*
88
- * is the reader's own convention, from `Intl`.
89
- *
90
- * @since 0.1.0
91
- * @status experimental
92
- * @category forms
93
- * @aliases Time, TimeList
94
- * @documentation https://helix.fusion.dev/components/time-picker
95
- *
96
- * @event {Event} change - Announced once the value has moved.
97
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the time changes. Cancelable: refusing it takes the decision and nothing moves.
98
- * @event {Event} input - Announced once the value has moved.
99
- *
100
- * @csspart column - One of them.
101
- * @csspart column-empty - Shown when a filter has hidden everything.
102
- * @csspart column-feedback - What is wrong with it.
103
- * @csspart column-filter-icon - The mark beside that field.
104
- * @csspart column-filter-mark - The glyph beside the filter field, where none is slotted.
105
- * @csspart column-hint - Help under the list.
106
- * @csspart column-items - The list itself.
107
- * @csspart column-label - The question, over the list.
108
- * @csspart column-loading - Shown while the options are still on their way.
109
- * @csspart column-search - The region holding the field that narrows the list.
110
- * @csspart column-search-field - The field itself.
111
- * @csspart columns - The row of columns, in `wheels`.
112
- * @csspart list - The listbox holding the times, in `list`.
113
- * @csspart list-empty - Shown when a filter has hidden everything.
114
- * @csspart list-feedback - What is wrong with it.
115
- * @csspart list-filter-icon - The mark beside that field.
116
- * @csspart list-filter-mark - The glyph beside the filter field, where none is slotted.
117
- * @csspart list-hint - Help under the list.
118
- * @csspart list-items - The list itself.
119
- * @csspart list-label - The question, over the list.
120
- * @csspart list-loading - Shown while the options are still on their way.
121
- * @csspart list-search - The region holding the field that narrows the list.
122
- * @csspart list-search-field - The field itself.
123
- * @csspart option - One time in it. Carries `data-between` inside a range.
124
- * @csspart option-description - The caption under the words, when there is one.
125
- * @csspart option-item - The row.
126
- * @csspart option-label - The words.
127
- * @csspart option-text - The words and the caption, as one column.
128
- */
129
38
  let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControllerValue(HelixFormElement) {
130
39
  constructor() {
131
40
  super(...arguments);
132
- /**
133
- * Times that cannot be chosen, comma separated, as `HH:mm`.
134
- *
135
- * The counterpart to Date Picker's `disable-dates`, and it exists for the same
136
- * shape of problem: `min` and `max` take away a span, and what a booking
137
- * usually has to take away is a scatter. A morning with 09:30 and 11:00
138
- * already gone is not two bounds.
139
- *
140
- * A time not on the step is simply never offered, so it is not an error to
141
- * name one, and naming every time in the list is how the empty state is
142
- * reached honestly rather than by handing over an empty range.
143
- */
144
41
  this.disableTimes = '';
145
- /**
146
- * Whether the current selection fails validation.
147
- *
148
- * A flag rather than a message, the same split Date Picker draws. A flat
149
- * picker has no room of its own for a sentence: the field that wraps one owns
150
- * the label, the hint and the feedback, and a second message inside the panel
151
- * would be the same error said twice. What is left for the picker is the one
152
- * thing the field cannot draw from outside, the mark on the answer itself.
153
- */
154
42
  this.invalid = false;
155
- /**
156
- * Accessible name for the list, which has no visible one of its own.
157
- */
158
43
  this.label = 'Time';
159
- /**
160
- * Language tag the times are written in.
161
- *
162
- * Absent uses the runtime's own.
163
- */
164
44
  this.locale = '';
165
- /**
166
- * Latest time offered, as `HH:mm`.
167
- */
168
45
  this.max = '23:59';
169
- /**
170
- * Earliest time offered, as `HH:mm`.
171
- */
172
46
  this.min = '00:00';
173
- /**
174
- * Whether the control refuses changes while still submitting its value.
175
- *
176
- * **Enforced by refusing every request in one place**, which is the seam this
177
- * component already has: `requestValue` is what every path here comes through,
178
- * and gating it once is what stops the next call site from being written
179
- * without the check.
180
- *
181
- * Not `disabled`, which is the confusion worth naming: a disabled control
182
- * submits nothing and leaves the tab order, and a read-only one submits its
183
- * value and stays reachable, which is what a form showing a value somebody
184
- * else set needs.
185
- */
186
47
  this.readOnly = false;
187
- /**
188
- * Whether the form refuses to submit while nothing is chosen.
189
- */
190
48
  this.required = false;
191
- /**
192
- * Takes what the list announced and constrains it to what the mode allows.
193
- *
194
- * The listbox is put in `multiple` for both modes, because that is what marks
195
- * more than one row and toggles on press. A range then differs only in what is
196
- * kept: two, and a third pick starts again from that third one. Working out
197
- * which is newest by difference rather than by position, since a toggle can
198
- * remove as easily as add and the announced list is a set rather than a
199
- * sequence.
200
- */
201
49
  this.handleChange = (event) => {
202
50
  event.stopPropagation();
203
- /* The listbox is told as well, which is what grays it and takes it out of
204
- the tab order, and this refuses at the source so every path in is covered
205
- by one check.
206
- `isDisabled` rather than `disabled`, which is only the half a consumer
207
- wrote: an ancestor `<fieldset disabled>` is the other half. */
208
51
  if (this.isDisabled) {
209
52
  return;
210
53
  }
211
54
  const announced = event.detail.value;
212
- /* The listbox already skips a disabled option, so this is the second lock
213
- rather than the first: it covers a value arriving from a consumer's own
214
- dispatch, and it is what makes the refusal a fact about the component
215
- rather than about the listbox it happens to render. */
216
55
  if (announced.split(',').some((one) => this.barred(one.trim()))) {
217
56
  return;
218
57
  }
@@ -232,7 +71,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
232
71
  const added = next.find((one) => !before.has(one));
233
72
  this.requestValue(added ?? '');
234
73
  };
235
- /** Puts a changed column back together into a whole time. */
236
74
  this.moveColumn = (part, to) => {
237
75
  if (this.isDisabled) {
238
76
  return;
@@ -243,10 +81,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
243
81
  next = hour * 60 + Number(to);
244
82
  }
245
83
  else if (part === 'hour') {
246
- /* On a twelve hour clock the column says 1 to 12 and the meridiem says
247
- which half, so the two have to be recombined rather than read straight:
248
- 12 AM is midnight and 12 PM is noon, which is the one pair that does not
249
- follow the pattern. */
250
84
  const shown = Number(to);
251
85
  const whole = isTwelveHour(this.locale) ? (shown % 12) + (meridiem === 'PM' ? 12 : 0) : shown;
252
86
  next = whole * 60 + minute;
@@ -255,35 +89,14 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
255
89
  const half = hour % 12;
256
90
  next = (half + (to === 'PM' ? 12 : 0)) * 60 + minute;
257
91
  }
258
- /* The wheels build a value from three columns and never reach
259
- `handleChange`, so the refusal has to be repeated here. Disabling the
260
- options is not enough on its own: a reader on 09:00 who moves the hour to
261
- 10 lands on 10:00, and 10:00 can be barred while nothing in the hour
262
- column was. */
263
92
  const whole = fromMinutes(next);
264
93
  if (this.barred(whole)) {
265
94
  return;
266
95
  }
267
96
  this.requestValue(whole);
268
97
  };
269
- /**
270
- * How far apart the offered times are, in minutes.
271
- *
272
- * Fifteen, which is four rows an hour and ninety-six in a day: enough that a
273
- * reader scrolls rather than hunts, and few enough that the list is a list.
274
- * Anything under five is a thousand rows and wants a field instead.
275
- */
276
98
  this.step = 15;
277
99
  }
278
- /**
279
- * Every time on offer, as minutes since midnight.
280
- *
281
- * Built from `min` upward rather than from midnight, so a list that starts at
282
- * `09:00` steps from nine rather than offering `09:15` because that is where
283
- * the quarter hours from midnight happen to land. A step that does not divide
284
- * the hour is a real thing to want, twenty minutes being the obvious one, and
285
- * this is the reading that makes it behave.
286
- */
287
100
  get times() {
288
101
  const from = toMinutes(this.min) ?? 0;
289
102
  const to = toMinutes(this.max) ?? 1439;
@@ -294,17 +107,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
294
107
  }
295
108
  return found;
296
109
  }
297
- /**
298
- * Whether a time is barred by name.
299
- *
300
- * `barred`, not `disabled`, for the reason Date Picker records: this is a form
301
- * control and `HelixFormElement.disabled` is a boolean about the whole of it,
302
- * so a method and a flag cannot share the name.
303
- *
304
- * Only the named list. `min` and `max` are not consulted here because they
305
- * never produce a row to bar: `times` builds the list between them, so a time
306
- * outside the bounds is absent rather than present and refused.
307
- */
308
110
  barred(at) {
309
111
  return this.disableTimes
310
112
  .split(',')
@@ -312,30 +114,21 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
312
114
  .filter(Boolean)
313
115
  .includes(at);
314
116
  }
315
- /**
316
- * Whether every offered time in an hour is barred.
317
- *
318
- * `shown` is the meridiem the wheel is on, so `1` means 01:00 under AM and
319
- * 13:00 under PM. Absent is a 24 hour clock, where the number is the hour.
320
- */
321
117
  wholeHourBarred(shown, half) {
322
118
  const whole = half === undefined ? shown : (shown % 12) + (half === 'PM' ? 12 : 0);
323
119
  const within = this.times.filter((at) => Math.floor(at / 60) === whole);
324
120
  return within.length > 0 && within.every((at) => this.barred(fromMinutes(at)));
325
121
  }
326
- /** Whether every offered time in a half of the clock is barred. */
327
122
  halfBarred(half) {
328
123
  const within = this.times.filter((at) => (half === 'PM') === at >= 720);
329
124
  return within.length > 0 && within.every((at) => this.barred(fromMinutes(at)));
330
125
  }
331
- /** The times held, which is a list whether or not a mode is set. */
332
126
  get held() {
333
127
  return this.value
334
128
  .split(',')
335
129
  .map((one) => one.trim())
336
130
  .filter(Boolean);
337
131
  }
338
- /** Whether a time falls inside a range, which is what gets banded. */
339
132
  between(at) {
340
133
  if (this.mode !== 'range') {
341
134
  return false;
@@ -343,41 +136,19 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
343
136
  const [from, to] = this.held;
344
137
  return from !== undefined && to !== undefined && at > from && at < to;
345
138
  }
346
- /** The three readings a wheel shows, from the value or from `min`. */
347
139
  get parts() {
348
140
  const at = toMinutes(this.value) ?? toMinutes(this.min) ?? 0;
349
141
  const hour = Math.floor(at / 60);
350
142
  return { hour, meridiem: hour < 12 ? 'AM' : 'PM', minute: at % 60 };
351
143
  }
352
- /** Every whole hour that has at least one offered time in it. */
353
144
  get hours() {
354
145
  const found = new Set(this.times.map((at) => Math.floor(at / 60)));
355
146
  return [...found];
356
147
  }
357
- /** Every minute offered within the hour currently shown. */
358
148
  get minutes() {
359
149
  const hour = this.parts.hour;
360
150
  return this.times.filter((at) => Math.floor(at / 60) === hour).map((at) => at % 60);
361
151
  }
362
- /**
363
- * Each wheel comes to rest on its own answer.
364
- *
365
- * A dial that opens at midnight when it holds ten in the evening is a dial
366
- * showing the wrong thing, and CSS cannot fix it: `scroll-snap-type` chooses a
367
- * snap point when something scrolls, and nothing has. Measured before this,
368
- * with the value at 22:00, the hour wheel sat 288 pixels above its answer.
369
- *
370
- * On every update rather than on the first, because the columns re-render as
371
- * the value moves and the minute column's contents change with the hour.
372
- *
373
- * **And it is public, because an update is not the only moment that matters.**
374
- * Inside a panel this element renders while the panel is still
375
- * `display: none`, so every box measures zero and there is nothing to center
376
- * against: measured, the hour wheel stayed 288 pixels off its answer because
377
- * the only call happened before anything had a size. Whatever shows the panel
378
- * calls this again afterwards. The guard below makes the early call harmless
379
- * rather than wrong, which is why it can be left in.
380
- */
381
152
  reveal() {
382
153
  if (this.variant !== 'wheels' || this.mode !== undefined) {
383
154
  return;
@@ -386,34 +157,18 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
386
157
  column.reveal?.();
387
158
  }
388
159
  }
389
- /** Refuses every change while read-only. See `readOnly`. */
390
160
  requestValue(next) {
391
161
  if (this.readOnly) {
392
162
  return false;
393
163
  }
394
164
  return super.requestValue(next);
395
165
  }
396
- /**
397
- * Reports "required and empty", or clears it.
398
- *
399
- * Written out rather than taken from `MixinField`, which is where the seven
400
- * form controls get this. That mixin brings a label, a hint and a feedback
401
- * region with it, and this is a grid rather than a field: it is composed
402
- * *into* the field that has those, so taking the mixin would give one control
403
- * two of each. What is shared is five lines, and the anchor is what the
404
- * browser points its bubble at.
405
- */
406
166
  reportRequired() {
407
167
  const anchor = this.renderRoot?.querySelector('[part="list"]') ?? undefined;
408
168
  if (this.required && this.value === '') {
409
169
  this.internals.setValidity({ valueMissing: true }, 'Choose a time.', anchor);
410
170
  return;
411
171
  }
412
- /* A value the wheels cannot read is bad input, the same as on the calendar:
413
- `value="not-a-time"` marks no row, and would otherwise report `:valid` and
414
- be handed to the form. `asTime` rejects the hours and minutes that are
415
- shaped right and out of range as well as the ones that are not shaped
416
- right at all. */
417
172
  const held = this.held;
418
173
  if (held.length > 0 && held.some((one) => asTime(one) === null)) {
419
174
  this.internals.setValidity({ badInput: true }, this.mode === undefined
@@ -423,21 +178,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
423
178
  }
424
179
  this.internals.setValidity({});
425
180
  }
426
- /**
427
- * Reports the times to the form, one entry each.
428
- *
429
- * `FormData` and one entry per time, which is `DatePicker.report` exactly.
430
- * `mode="multiple"` is genuinely several answers, and a server reading
431
- * `times=09:00,17:00` would have to split it on a convention this library
432
- * invented; `range` sends its two in order, so start and end arrive as the
433
- * pair they already are.
434
- *
435
- * **Nothing at all without a `name`.** A `FormData` carries its own names and
436
- * its entries are appended whatever the element is called, so an unnamed
437
- * picker would submit empty-named times rather than being skipped the way a
438
- * control reporting a plain string is. That is the same guard, for the same
439
- * reason, that the calendar's own note spells out.
440
- */
441
181
  report() {
442
182
  const times = this.held;
443
183
  if (this.name === '' || this.disabled || times.length === 0) {
@@ -450,25 +190,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
450
190
  }
451
191
  this.internals.setFormValue(data);
452
192
  }
453
- /**
454
- * Reveals once the wheels have rendered themselves.
455
- *
456
- * `updated` is this element's own moment, and each wheel is a `helix-listbox`
457
- * that was created during this very render: its first update has been
458
- * scheduled and has not run, so it has no rows and no height and `reveal`
459
- * measures zero and gives up. Measured on a standalone picker, every column
460
- * sat at `scrollTop: 0` with the answer nine rows down, which reads as a wheel
461
- * showing the wrong time rather than as one that failed to scroll.
462
- *
463
- * Whatever opens a panel calls `reveal` again afterwards, for the separate
464
- * reason that method documents. Flat on a page there is no panel and no
465
- * second call.
466
- *
467
- * Safe to run on every update: the listbox stops as soon as the chosen row is
468
- * within a pixel of center, so a call with nothing to do costs two rect reads
469
- * and changes nothing. It cannot fight a reader mid-scroll either, since
470
- * scrolling a wheel changes no property and so schedules no update.
471
- */
472
193
  async revealWhenSettled() {
473
194
  const columns = [
474
195
  ...(this.renderRoot?.querySelectorAll('[part="column"]') ?? []),
@@ -476,16 +197,9 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
476
197
  await Promise.all(columns.map((column) => column.updateComplete));
477
198
  this.reveal();
478
199
  }
479
- /* `ariaInvalid` on the host through `ElementInternals`, the same route
480
- Date Picker takes and for the reason the whole library records: a `role` or a
481
- state written onto the host as an attribute is one a React consumer's
482
- server markup never had. */
483
200
  willUpdate(changed) {
484
201
  super.willUpdate(changed);
485
202
  this.internals.ariaInvalid = this.invalid ? 'true' : null;
486
- /* Here rather than in `updated`, which is where `DatePicker` reports from
487
- too: the value is known before the render and a form read between the two
488
- should not see the previous one. */
489
203
  this.report();
490
204
  }
491
205
  updated(changed) {
@@ -494,23 +208,9 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
494
208
  this.reveal();
495
209
  void this.revealWhenSettled();
496
210
  }
497
- /** Puts the keyboard on the list, which is the only control here. */
498
211
  focus(options) {
499
212
  this.renderRoot?.querySelector('[part="list"]')?.focus(options);
500
213
  }
501
- /**
502
- * One wheel, which is a listbox like the whole list is.
503
- *
504
- * `protected` rather than `private`, which is the whole of what a seam is
505
- * here: a tier above has to be able to reach it.
506
- *
507
- * `barred` is passed per column rather than read from `disableTimes` here,
508
- * because a column holds a third of a time and only the caller knows the
509
- * other two thirds. What that means differs by column and the difference is
510
- * not a detail: a minute is barred when *this* hour plus it is, and an hour is
511
- * barred only when every minute in it is, since one free minute is a reason to
512
- * open the wheel rather than to close it.
513
- */
514
214
  renderColumn(part, label, values, written, value, barred = () => false) {
515
215
  return html `<${this.tag('listbox')}
516
216
  part="column"
@@ -536,9 +236,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
536
236
  const twelve = isTwelveHour(this.locale);
537
237
  const { hour, meridiem, minute } = this.parts;
538
238
  const shownHour = twelve ? String(hour % 12 === 0 ? 12 : hour % 12) : String(hour);
539
- /* The hour column reads 1 to 12 on a twelve hour clock, so the whole hours
540
- on offer are mapped into that half and de-duplicated: 13:00 and 01:00 are
541
- both "1", and offering it twice is offering a choice that is not one. */
542
239
  const hours = twelve
543
240
  ? [...new Set(this.hours.map((one) => (one % 12 === 0 ? 12 : one % 12)))].sort((a, b) => a - b)
544
241
  : this.hours;
@@ -550,16 +247,6 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
550
247
  : nothing}
551
248
  </div>`;
552
249
  }
553
- /**
554
- * The flat list of times, for the variant that is not wheels, behind a seam the styled tier can move.
555
- *
556
- * A method rather than a fragment inline, so a tier above can render its own,
557
- * wrap this one, pass it a property the base does not know about, or leave it
558
- * out, without reimplementing the whole template to do any of them. The tag is
559
- * interpolated rather than written, so `tier` resolves it: this renders the
560
- * headless element, and a styled subclass that sets `tier` renders the styled one
561
- * from the same template.
562
- */
563
250
  renderList() {
564
251
  return html `<${this.tag('listbox')}
565
252
  part="list"
@@ -587,30 +274,17 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
587
274
  </${this.tag('listbox')}>`;
588
275
  }
589
276
  render() {
590
- /* A mode falls back to the list, for the reason the property records: a
591
- range on wheels is two sets of wheels and a multiple is a set, which a
592
- wheel cannot hold. */
593
277
  if (this.variant === 'wheels' && this.mode === undefined) {
594
278
  return this.renderSpinner();
595
279
  }
596
280
  return html `${this.renderList()}`;
597
281
  }
598
- /**
599
- * The value as a `Date` on 1 January 1970 UTC, or `null` while there is no time.
600
- *
601
- * The platform's own pair on `<input type="time">`, epoch day included: a time
602
- * is an offset into a day rather than an instant, and putting it on the epoch
603
- * is what makes `valueAsNumber` come out as milliseconds since midnight rather
604
- * than as a moment nobody meant. Assigning any date reads the clock off it, so
605
- * a consumer handing over `new Date()` gets the time of day they expected.
606
- */
607
282
  get valueAsDate() {
608
283
  return asTime(this.value);
609
284
  }
610
285
  set valueAsDate(date) {
611
286
  this.value = fromTime(date);
612
287
  }
613
- /** The value as milliseconds since midnight, or `NaN` while there is no time. */
614
288
  get valueAsNumber() {
615
289
  return asTime(this.value)?.getTime() ?? Number.NaN;
616
290
  }
@@ -9,76 +9,15 @@ import { circle, record } from '@fusion.dev/helix-icons';
9
9
  import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
- /**
13
- * @summary One event on a timeline: when it happened, and what it was.
14
- *
15
- * @description
16
- * **The time is a `<time>`, and only when there is a time to put in it.** Given
17
- * `datetime`, the label a consumer wrote is wrapped in the element the platform
18
- * has for exactly this, so the machine-readable instant travels with the human
19
- * one: a crawler, a reader's own tooling and a copy-paste all get "2026-08-19",
20
- * where "yesterday" on its own is a word. Given no `datetime`, it stays a span,
21
- * because a `<time>` with nothing to be precise about is markup pretending.
22
- *
23
- * **The connector is this element's, not the list's.** Each entry draws the line
24
- * that arrives at it and hides it when it is first, which `:host(:first-child)`
25
- * settles with no index, no counting parent and no script. `Steps` and
26
- * `Breadcrumbs` both do it this way and for the same reason.
27
- *
28
- * `tone` says what kind of event it was, and it is the only thing that colors
29
- * the mark. It takes `Announcement`'s vocabulary rather than a second one.
30
- *
31
- * @since 0.1.0
32
- * @status stable
33
- * @category data
34
- * @aliases Activity item, History entry, Event
35
- * @documentation https://helix.fusion.dev/components/timeline
36
- *
37
- * @slot - What happened.
38
- * @slot marker - The mark on the line. Falls back to a dot, which is the slot's
39
- * own fallback content: whether a consumer slotted a mark is a question only the
40
- * platform can answer, and it answers it at assignment time.
41
- * @slot time - When it happened, as a reader should see it written.
42
- *
43
- * @csspart connector - The line arriving at this entry.
44
- * @csspart content - What happened, and the time above it.
45
- * @csspart dot - The mark drawn where nothing was slotted for one.
46
- * @csspart marker - The mark on the line.
47
- * @csspart rail - The column holding the mark and the line.
48
- * @csspart time - When it happened.
49
- */
50
12
  let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixElement {
51
13
  constructor() {
52
14
  super(...arguments);
53
- /**
54
- * When it happened, machine-readable.
55
- *
56
- * Whatever `<time>` accepts, which is a date, a time, both, a duration or a
57
- * week. Unset, the label is written as it was given and nothing claims to
58
- * know what instant it names.
59
- */
60
15
  this.datetime = '';
61
16
  }
62
17
  willUpdate(changed) {
63
18
  super.willUpdate(changed);
64
19
  hostAria(this.internals, { role: 'listitem' });
65
20
  }
66
- /**
67
- * The mark for an entry that slotted none, which the tone decides.
68
- *
69
- * **A seam is a `protected render…()` and never a getter.** A slot's fallback
70
- * answers a consumer and a seam answers the tier above, and a getter answers
71
- * neither. The check tests for parentheses, `${this.x}` being permitted
72
- * because a property is not markup a tier would redraw, so a getter returning
73
- * a template reads as a bare reference and is let through.
74
- *
75
- * Toned entries are reporting something, so they take the circle with a
76
- * center, which is `Steps` drawing the stage a reader is standing on. Untoned
77
- * ones are just things that happened and take the open circle, which is
78
- * `Steps` drawing a stage nobody has reached. The same two glyphs in both
79
- * components rather than two that resemble each other: a shape drawn twice
80
- * drifts, and these are already the marks a reader has learned one page over.
81
- */
82
21
  renderMarker() {
83
22
  const icon = this.tone === undefined ? circle : record;
84
23
  return html `<span part="marker"
@@ -91,11 +30,6 @@ let HelixHeadlessTimelineItem = class HelixHeadlessTimelineItem extends HelixEle
91
30
  }
92
31
  render() {
93
32
  const when = html `<slot name="time"></slot>`;
94
- /* The time is a sibling of the content rather than the first thing inside
95
- it, so the two are separate rows and the rail can align its mark to the
96
- second of them. Nested, the mark could only ever be a fixed distance from
97
- the top of the entry, which is the time's height on a good day and wrong
98
- the moment a consumer writes two lines of it. */
99
33
  return html `<div part="rail">
100
34
  <div
101
35
  part="connector"
@@ -7,47 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A record of what happened, in the order it happened.
12
- *
13
- * @description
14
- * **Not `Steps`, and the difference is what the thing is for.** A stepper is a
15
- * process a reader is moving through: it has a stage they are on, stages they
16
- * have finished, and stages ahead, so it carries `active` and `complete` and
17
- * numbers itself. A timeline is a record of events that already happened, or are
18
- * scheduled to. Nothing is current, nothing is complete, and what each entry
19
- * carries instead is a time. Building this as an appearance of `Steps` would
20
- * have meant a stepper with no step you are on.
21
- *
22
- * What it does take from `Steps` is the connector. Each entry draws the line
23
- * that arrives at it and hides it when it is first, which needs neither an index
24
- * nor a parent that counts: `:host(:first-child)` is the whole of it, resolved
25
- * at layout with no script, so a server's markup is right before anything runs.
26
- *
27
- * `role="list"` on the host rather than an `<ol>` in the shadow root, for the
28
- * reason `Steps` gives: the entries are the consumer's elements in the light
29
- * DOM, so a list element here would be a list whose items are somewhere else.
30
- *
31
- * **It runs down the page and has no horizontal form.** Laid along a line there
32
- * is no room for the time or the sentence that are the whole reason to reach for
33
- * a timeline, so what survives is a mark and a short label, and that is a
34
- * stepper. `Steps` is already horizontal by default and already draws it. A run
35
- * of stages along a page is `Steps`.
36
- *
37
- * @since 0.1.0
38
- * @status stable
39
- * @category data
40
- * @aliases Activity feed, History, Changelog, Audit trail
41
- * @documentation https://helix.fusion.dev/components/timeline
42
- *
43
- * @slot - The entries.
44
- */
45
10
  let HelixHeadlessTimeline = class HelixHeadlessTimeline extends HelixElement {
46
11
  constructor() {
47
12
  super(...arguments);
48
- /**
49
- * Names the record, for a reader arriving at it out of context.
50
- */
51
13
  this.label = '';
52
14
  }
53
15
  render() {
@@ -55,9 +17,6 @@ let HelixHeadlessTimeline = class HelixHeadlessTimeline extends HelixElement {
55
17
  }
56
18
  willUpdate(changed) {
57
19
  super.willUpdate(changed);
58
- /* On the host through internals rather than as an attribute, so the role is
59
- there for a screen reader without being markup the server never wrote.
60
- `Steps` says the same about its own list. */
61
20
  hostAria(this.internals, { label: this.label, role: 'list' });
62
21
  }
63
22
  };