@fusion.dev/helix-headless 0.22.0 → 0.24.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 (103) hide show
  1. package/custom-elements.json +48397 -26631
  2. package/dist/announcement/root/announcement-root.d.ts +19 -5
  3. package/dist/area-chart/index.d.ts +1 -0
  4. package/dist/area-chart/index.js +1 -0
  5. package/dist/area-chart/root/area-chart-root.d.ts +182 -0
  6. package/dist/area-chart/root/area-chart-root.js +205 -0
  7. package/dist/carousel/root/carousel-root.d.ts +1 -0
  8. package/dist/carousel/root/carousel-root.js +18 -2
  9. package/dist/chat/index.d.ts +1 -1
  10. package/dist/chat/message/chat-message.d.ts +7 -0
  11. package/dist/chat/message/chat-message.js +2 -1
  12. package/dist/chat/root/chat-root.d.ts +195 -4
  13. package/dist/chat/root/chat-root.js +316 -23
  14. package/dist/checkbox-group/root/checkbox-group-root.d.ts +2 -0
  15. package/dist/checkbox-group/root/checkbox-group-root.js +12 -2
  16. package/dist/code-block/index.d.ts +1 -0
  17. package/dist/code-block/index.js +1 -0
  18. package/dist/code-block/patch.d.ts +62 -0
  19. package/dist/code-block/patch.js +93 -0
  20. package/dist/code-block/root/code-block-root.d.ts +82 -17
  21. package/dist/code-block/root/code-block-root.js +83 -6
  22. package/dist/color-picker/root/color-picker-root.d.ts +4 -3
  23. package/dist/color-picker/root/color-picker-root.js +4 -1
  24. package/dist/data-table/root/data-table-root.d.ts +195 -23
  25. package/dist/data-table/root/data-table-root.js +308 -27
  26. package/dist/description-list/index.d.ts +2 -0
  27. package/dist/description-list/index.js +2 -0
  28. package/dist/description-list/item/description-list-item.d.ts +39 -0
  29. package/dist/description-list/item/description-list-item.js +26 -0
  30. package/dist/description-list/root/description-list-root.d.ts +56 -0
  31. package/dist/description-list/root/description-list-root.js +21 -0
  32. package/dist/element-events.generated.js +27 -6
  33. package/dist/filter-input/chip/filter-chip.d.ts +114 -0
  34. package/dist/filter-input/chip/filter-chip.js +61 -0
  35. package/dist/filter-input/field/filter-field.d.ts +128 -0
  36. package/dist/filter-input/field/filter-field.js +70 -0
  37. package/dist/filter-input/grammar.d.ts +96 -0
  38. package/dist/filter-input/grammar.js +133 -0
  39. package/dist/filter-input/index.d.ts +5 -0
  40. package/dist/filter-input/index.js +5 -0
  41. package/dist/filter-input/menu/filter-menu.d.ts +153 -0
  42. package/dist/filter-input/menu/filter-menu.js +164 -0
  43. package/dist/filter-input/operator/filter-operator.d.ts +105 -0
  44. package/dist/filter-input/operator/filter-operator.js +39 -0
  45. package/dist/filter-input/root/filter-input-root.d.ts +118 -9
  46. package/dist/filter-input/root/filter-input-root.js +96 -2
  47. package/dist/format-date/root/format-date-root.js +1 -1
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.js +5 -0
  50. package/dist/line-chart/root/line-chart-root.d.ts +9 -2
  51. package/dist/listbox/root/listbox-root.d.ts +8 -7
  52. package/dist/listbox/root/listbox-root.js +7 -2
  53. package/dist/menu/item/menu-item.d.ts +54 -6
  54. package/dist/menu/item/menu-item.js +31 -6
  55. package/dist/menu/root/menu-root.d.ts +23 -0
  56. package/dist/menu/root/menu-root.js +13 -2
  57. package/dist/notifications/group/notification-group.d.ts +129 -0
  58. package/dist/notifications/group/notification-group.js +106 -0
  59. package/dist/notifications/index.d.ts +1 -0
  60. package/dist/notifications/index.js +1 -0
  61. package/dist/notifications/notification/notification.d.ts +39 -13
  62. package/dist/notifications/notification/notification.js +11 -8
  63. package/dist/notifications/root/notifications-root.d.ts +57 -1
  64. package/dist/notifications/root/notifications-root.js +35 -4
  65. package/dist/number-input/root/number-input-root.d.ts +21 -1
  66. package/dist/number-input/root/number-input-root.js +18 -5
  67. package/dist/object-inspector/index.d.ts +1 -0
  68. package/dist/object-inspector/index.js +1 -0
  69. package/dist/object-inspector/root/object-inspector-root.d.ts +183 -0
  70. package/dist/object-inspector/root/object-inspector-root.js +348 -0
  71. package/dist/outline/root/outline-root.d.ts +16 -1
  72. package/dist/outline/root/outline-root.js +27 -9
  73. package/dist/radio/group/radio-group.d.ts +1 -0
  74. package/dist/scatter-chart/index.d.ts +1 -0
  75. package/dist/scatter-chart/index.js +1 -0
  76. package/dist/scatter-chart/root/scatter-chart-root.d.ts +144 -0
  77. package/dist/scatter-chart/root/scatter-chart-root.js +205 -0
  78. package/dist/skip-link/index.d.ts +1 -0
  79. package/dist/skip-link/index.js +1 -0
  80. package/dist/skip-link/root/skip-link-root.d.ts +81 -0
  81. package/dist/skip-link/root/skip-link-root.js +58 -0
  82. package/dist/speed-dial/root/speed-dial-root.d.ts +1 -0
  83. package/dist/speed-dial/root/speed-dial-root.js +6 -2
  84. package/dist/steps/root/steps-root.d.ts +1 -0
  85. package/dist/table/column/table-column.d.ts +79 -0
  86. package/dist/table/column/table-column.js +125 -5
  87. package/dist/table/root/table-root.d.ts +2 -0
  88. package/dist/table/root/table-root.js +12 -3
  89. package/dist/time-picker/root/time-picker-root.d.ts +9 -0
  90. package/dist/time-picker/root/time-picker-root.js +5 -0
  91. package/dist/toast/root/toast-root.d.ts +2 -0
  92. package/dist/tooltip/root/tooltip-root.js +1 -1
  93. package/dist/tour/root/tour-root.d.ts +1 -0
  94. package/dist/tree/item/tree-item.d.ts +7 -4
  95. package/dist/tree/item/tree-item.js +4 -1
  96. package/dist/video/root/video-root.d.ts +23 -1
  97. package/dist/video/root/video-root.js +42 -22
  98. package/dist/virtual-list/root/virtual-list-root.d.ts +10 -6
  99. package/dist/virtual-list/root/virtual-list-root.js +10 -4
  100. package/package.json +5 -5
  101. package/types/jsx.d.ts +4535 -346
  102. package/types/svelte.d.ts +11 -0
  103. package/types/vue.d.ts +737 -39
@@ -1,5 +1,6 @@
1
1
  import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import type { HelixHeadlessFilterField } from '../field/filter-field.js';
3
4
  declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
4
5
  /**
5
6
  * @summary A field that narrows something else, beside what is already narrowing it.
@@ -22,13 +23,32 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
22
23
  * `helix-dismiss` its own remove control fires, so a consumer who handled the
23
24
  * pointer has already handled the keyboard.
24
25
  *
25
- * **It can suggest what to type next, and it never learns the grammar.** Slot
26
- * `helix-option` elements into `suggestions` and the field shows them, moves
27
- * through them with the arrows, and lets Enter press one; the option fires its
28
- * own `helix-select` and the consumer decides what choosing meant. Deciding
29
- * *what* to suggest is theirs too: keys before a colon and values after it are
30
- * the same panel with different contents, which is all a two-level filter is,
31
- * and the parse that tells them apart belongs where the domain does.
26
+ * **It reads what was declared, and nothing that was not.** Write
27
+ * `FilterField`s — usually inside the `FilterMenu` slotted into `start`, so the
28
+ * picker and the text work off one set — and Enter reads the box against them:
29
+ * `disposition:refunded,settled` and `-disposition:refunded` announce
30
+ * `helix-facet`, exactly as ticking those values in the picker would, and the
31
+ * text they were read from goes. Everything else stays where it was typed, so
32
+ * `refund tuesday disposition:settled` is a filter and a search at once.
33
+ * `grammar.ts` carries the whole rule and is worth reading before relying on it;
34
+ * the short version is that Helix owns the punctuation and the consumer owns
35
+ * every word in it. A field with no `FilterField` in it parses nothing, which is
36
+ * the behavior this had before it could.
37
+ *
38
+ * **A field whose values cannot be listed belongs here rather than in the
39
+ * picker.** `assignee:ada` is a filter and nobody wants to enumerate every name
40
+ * in the company, but a menu page for it would have nothing on it to press.
41
+ * Written directly inside this element it is read by the grammar and never
42
+ * offered as a page: unslotted, so nothing draws it, which is the same trick
43
+ * `FilterField` already plays on its own children.
44
+ *
45
+ * **It can suggest what to type next, and deciding what to suggest is still the
46
+ * consumer's.** Slot `helix-option` elements into `suggestions` and the field
47
+ * shows them, moves through them with the arrows, and lets Enter press one; the
48
+ * option fires its own `helix-select` and the consumer decides what choosing
49
+ * meant. Keys before a colon and values after it are the same panel with
50
+ * different contents, and `readFilterText` is exported for a consumer narrowing
51
+ * that panel against what a reader has half typed.
32
52
  *
33
53
  * **The panel opens when there is something in it and closes when there is
34
54
  * not**, rather than on a choice. A reader who picks a key wants its values
@@ -55,7 +75,12 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
55
75
  * The tokens are neither, because they are not state. They are the consumer's
56
76
  * own elements in a slot, so there is nothing for this to own and nothing to
57
77
  * report back: a token goes when the consumer removes it from their markup,
58
- * having been asked by `helix-dismiss`.
78
+ * having been asked by `helix-dismiss`. **A `FilterChip` naming a declared field
79
+ * is asked differently**, and it has to be: the picker holds what is ticked, so
80
+ * a chip that removed itself without telling it left the value ticked in a list
81
+ * nobody could see was wrong. That dismissal is answered as the field being
82
+ * filtered by nothing, so it arrives as `helix-facet` like every other change
83
+ * and the chip goes because the filter went.
59
84
  *
60
85
  * @since 0.1.0
61
86
  * @status experimental
@@ -66,12 +91,15 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
66
91
  * @slot end - After the text, for a count or a control.
67
92
  * @slot start - Before the text, for a search icon.
68
93
  * @slot suggestions - What could be typed next, as `Option`s, and `Option.Group`s of them.
69
- * @slot tags - What is already narrowing the results, as `Tag`s.
94
+ * @slot tags - What is already narrowing the results, as `FilterChip`s or `Tag`s.
70
95
  *
71
96
  * @event {Event} change - Relayed from the input once the reader has finished.
72
97
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired as the text changes, and cancelable.
73
98
  * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the suggestions go, and cancelable.
74
99
  * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the suggestions show, and cancelable.
100
+ * @event {HelixDismissEvent} helix-dismiss - A token asking to go, from its close control or from Backspace on an empty field. A `FilterChip` naming a declared field is answered as `helix-facet` instead. Cancelable: refusing it takes the decision and the token stays.
101
+ * @event {HelixFacetEvent} helix-facet - Fired when a filter changes, whether read out of the text, picked, or dismissed. Cancelable: refusing it leaves the text and the chip where they were.
102
+ * @event {HelixSelectEvent} helix-select - A suggestion was chosen: the option's own, heard here because it bubbles out of `suggestions`. Cancelable: refusing it takes the decision from any other listener, and this field never acts on a choice itself, since what it means is the page's.
75
103
  * @event {Event} input - Announced once the text has moved.
76
104
  * @event {Event} toggle - Says the panel moved, carrying `newState` and `oldState`.
77
105
  *
@@ -81,6 +109,7 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
81
109
  * @csspart field - The box holding everything.
82
110
  * @csspart hint - Help under the field, while nothing is wrong.
83
111
  * @csspart label - The question, over the field.
112
+ * @csspart lead - The box before the text, holding whatever is slotted into `start`.
84
113
  * @csspart rest - How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.
85
114
  * @csspart surface - The floating panel the suggestions sit in.
86
115
  * @csspart tags - The row the tokens sit in.
@@ -129,6 +158,24 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
129
158
  * `aria-controls` all arrive with the first option and leave with the last.
130
159
  */
131
160
  private suggesting;
161
+ /**
162
+ * Whether anything is slotted before the text.
163
+ *
164
+ * The leading box is drawn only where there is something to put in it, and it
165
+ * is a box rather than a bare slot because it carries the seam between what a
166
+ * consumer put at the head of the field and the field itself. `Menu.Item`
167
+ * reads its own leading slot the same way and for the same reason: the light
168
+ * DOM answer is not reactive, so a control added after the first render would
169
+ * never get the box.
170
+ *
171
+ * **Empty, the box is `hidden`, not flagged**, for `Checkbox`'s reason: this
172
+ * tier has no sheet, and `::part(lead)` cannot be qualified by an attribute,
173
+ * so a `data-` flag left a headless field an empty flex item it had no way to
174
+ * remove, and the first chip sat one gap further in than the rest.
175
+ */
176
+ private leading;
177
+ /** Whether `leading` has had its first answer from the children. */
178
+ private leadRead;
132
179
  /**
133
180
  * Whether Escape has closed the panel over text that still has suggestions.
134
181
  *
@@ -195,6 +242,64 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
195
242
  private readonly handleInput;
196
243
  /** Relayed, because `change` does not cross a shadow boundary on its own. */
197
244
  private readonly handleChange;
245
+ /**
246
+ * The fields this can be filtered by, wherever they were written.
247
+ *
248
+ * Descendants rather than children, because the usual place to write them is
249
+ * inside the `FilterMenu` slotted into `start`: the picker and the text are
250
+ * two ways at one set of declarations, and asking a consumer to write them
251
+ * twice would be asking which of the two surfaces is the real one.
252
+ */
253
+ get fields(): HelixHeadlessFilterField[];
254
+ /**
255
+ * The picker holding a field, where the consumer wrote one.
256
+ *
257
+ * The picker keeps what is ticked, so anything that changes a filter has to
258
+ * go through it or the two surfaces start disagreeing: a filter typed here
259
+ * that the menu never heard about leaves the value unticked in a list the
260
+ * reader is about to open.
261
+ */
262
+ private menuFor;
263
+ /**
264
+ * Says a field is now filtered by these values, through the picker where
265
+ * there is one, and reports whether it stood.
266
+ *
267
+ * Announced by the menu rather than relayed by this element, so the ticks and
268
+ * the event never come from two places. A consumer who wrote `FilterField`s
269
+ * without a picker gets it from here, which is the only case where nothing
270
+ * else could have said it.
271
+ */
272
+ private announceFacet;
273
+ /**
274
+ * A chip asking to go, answered as the filter going.
275
+ *
276
+ * **Taken rather than passed on, and only for a chip naming a declared
277
+ * field.** Left alone, a dismissed chip removed itself and told the picker
278
+ * nothing, so the menu went on showing the value ticked and the reader's next
279
+ * press on it read as *unticking*: they pressed `Refunded` and nothing came
280
+ * back. Turning the request into `apply(field, [])` puts it down the one road
281
+ * every other filter takes, so the chip goes because the filter went. A
282
+ * consumer refusing that `helix-facet` keeps both, which is the answer a
283
+ * refusal should give.
284
+ *
285
+ * Everything else in the row is untouched: a plain `Tag`, or a chip for
286
+ * something no `FilterField` declares, is the consumer's to answer.
287
+ */
288
+ private readonly handleDismiss;
289
+ /**
290
+ * Reads what has been typed, announces whatever of it was a filter, and takes
291
+ * that much of the text away.
292
+ *
293
+ * **Nothing at all without declarations**, which is what keeps this from
294
+ * changing under a consumer who never asked for it: a field with no
295
+ * `FilterField` in it has no vocabulary to read against, so Enter stays the
296
+ * form's and every keystroke is still just text.
297
+ *
298
+ * Refusing a `helix-facet` leaves that term's text where the reader wrote it,
299
+ * so a consumer who declines one is not left with a box that has silently
300
+ * emptied itself of a filter they did not take.
301
+ */
302
+ private readFilters;
198
303
  /**
199
304
  * Empties the text and gives the caret back.
200
305
  *
@@ -248,6 +353,8 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
248
353
  * this element and must not close what is being pressed.
249
354
  */
250
355
  private readonly handleFocusOut;
356
+ /** The leading box again, whenever what is in front of the text changes. */
357
+ private readonly handleLeading;
251
358
  /** The row again, whenever what is in it changes. */
252
359
  private readonly handleTokens;
253
360
  /**
@@ -261,6 +368,8 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
261
368
  * something it was never given.
262
369
  */
263
370
  protected removeLastToken(): void;
371
+ connectedCallback(): void;
372
+ disconnectedCallback(): void;
264
373
  protected willUpdate(changed: PropertyValues): void;
265
374
  protected firstUpdated(changed: PropertyValues): void;
266
375
  protected updated(changed: PropertyValues): void;
@@ -4,10 +4,11 @@ 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, drawIcon, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, drawIcon, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, } from '@fusion.dev/helix-core';
8
8
  import { x } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
+ import { readFilterText } from '../grammar.js';
11
12
  const asksToGo = (node) => typeof node?.dismiss === 'function';
12
13
  const isOption = (node) => 'active' in node && 'value' in node;
13
14
  let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
@@ -23,6 +24,8 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
23
24
  this.over = 0;
24
25
  this.activeAt = -1;
25
26
  this.suggesting = false;
27
+ this.leading = false;
28
+ this.leadRead = false;
26
29
  this.dismissed = false;
27
30
  this.panel = new PopoverController(this, {
28
31
  anchor: () => this.renderRoot?.querySelector('[part="field"]'),
@@ -47,6 +50,20 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
47
50
  event.stopPropagation();
48
51
  this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
49
52
  };
53
+ this.handleDismiss = (event) => {
54
+ const chip = event.composedPath()[0];
55
+ if (!(chip instanceof HTMLElement) || chip.slot !== 'tags') {
56
+ return;
57
+ }
58
+ const field = chip.getAttribute('name') ?? '';
59
+ const menu = field === '' ? null : this.menuFor(field);
60
+ if (!menu) {
61
+ return;
62
+ }
63
+ event.preventDefault();
64
+ event.stopPropagation();
65
+ menu.apply(field, []);
66
+ };
50
67
  this.handleClear = () => {
51
68
  this.requestValue('');
52
69
  const control = this.control;
@@ -59,6 +76,12 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
59
76
  if (this.tokenKeydown(event, this.value)) {
60
77
  return;
61
78
  }
79
+ if (event.key === 'Enter' && !(this.open && this.suggestions[this.activeAt])) {
80
+ if (this.readFilters() > 0) {
81
+ event.preventDefault();
82
+ }
83
+ return;
84
+ }
62
85
  if (!this.suggesting) {
63
86
  return;
64
87
  }
@@ -119,6 +142,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
119
142
  this.activeAt = -1;
120
143
  this.requestOpen(false);
121
144
  };
145
+ this.handleLeading = (event) => {
146
+ this.leading = event.target.assignedNodes({ flatten: true }).length > 0;
147
+ };
122
148
  this.handleTokens = () => {
123
149
  this.over = this.hideOverflow(this.tokens);
124
150
  };
@@ -164,6 +190,56 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
164
190
  setSelectionRange(start, end, direction) {
165
191
  this.control?.setSelectionRange(start, end, direction);
166
192
  }
193
+ get fields() {
194
+ return [
195
+ ...this.querySelectorAll('helix-headless-filter-field, helix-filter-field'),
196
+ ];
197
+ }
198
+ menuFor(field) {
199
+ for (const menu of this.querySelectorAll('helix-headless-filter-menu, helix-filter-menu')) {
200
+ if (menu.fields.some((declared) => (declared.name ?? '') === field)) {
201
+ return menu;
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+ announceFacet(facet) {
207
+ const menu = this.menuFor(facet.field);
208
+ return menu
209
+ ? menu.apply(facet.field, facet.values, facet.operator)
210
+ : this.dispatchEvent(new HelixFacetEvent(facet));
211
+ }
212
+ readFilters() {
213
+ const fields = this.fields;
214
+ if (fields.length === 0) {
215
+ return 0;
216
+ }
217
+ const reading = readFilterText(this.value, fields.map((field) => field.vocabulary));
218
+ if (reading.terms.length === 0) {
219
+ return 0;
220
+ }
221
+ let taken = 0;
222
+ const refused = [];
223
+ for (const term of reading.terms) {
224
+ if (this.announceFacet(term.facet)) {
225
+ taken += 1;
226
+ }
227
+ else {
228
+ refused.push(...term.written);
229
+ }
230
+ }
231
+ if (taken === 0) {
232
+ return 0;
233
+ }
234
+ this.requestValue([...refused, reading.text].filter(Boolean).join(' '));
235
+ const control = this.control;
236
+ if (control) {
237
+ restoreValue(control, this.value);
238
+ }
239
+ this.dismissed = false;
240
+ this.settleOpen();
241
+ return taken;
242
+ }
167
243
  settleOpen() {
168
244
  const focused = this.matches(':focus-within') || this.contains(document.activeElement);
169
245
  this.requestOpen(this.suggesting && !this.dismissed && !this.isDisabled && focused);
@@ -192,8 +268,20 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
192
268
  last.dismiss();
193
269
  }
194
270
  }
271
+ connectedCallback() {
272
+ super.connectedCallback();
273
+ this.addEventListener('helix-dismiss', this.handleDismiss);
274
+ }
275
+ disconnectedCallback() {
276
+ super.disconnectedCallback();
277
+ this.removeEventListener('helix-dismiss', this.handleDismiss);
278
+ }
195
279
  willUpdate(changed) {
196
280
  super.willUpdate(changed);
281
+ if (!this.leadRead && this.childrenKnown) {
282
+ this.leadRead = true;
283
+ this.leading = this.knownChildren.some((child) => child.slot === 'start');
284
+ }
197
285
  if (!this.hasUpdated) {
198
286
  return;
199
287
  }
@@ -206,6 +294,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
206
294
  }
207
295
  firstUpdated(changed) {
208
296
  super.firstUpdated(changed);
297
+ this.finishHydration();
209
298
  this.handleTokens();
210
299
  this.refreshSuggesting();
211
300
  }
@@ -242,7 +331,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
242
331
  render() {
243
332
  return html `${this.renderLabel()}
244
333
  <div part="field" @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
245
- <slot name="start"></slot>
334
+ <div part="lead" ?hidden=${!this.leading}>
335
+ <slot name="start" @slotchange=${this.handleLeading}></slot>
336
+ </div>
246
337
  ${this.renderTokenRow(html `<slot name="tags" @slotchange=${this.handleTokens}></slot>`, this.over)}
247
338
  <input
248
339
  part="control"
@@ -303,6 +394,9 @@ __decorate([
303
394
  __decorate([
304
395
  state()
305
396
  ], HelixHeadlessFilterInput.prototype, "suggesting", void 0);
397
+ __decorate([
398
+ state()
399
+ ], HelixHeadlessFilterInput.prototype, "leading", void 0);
306
400
  HelixHeadlessFilterInput = __decorate([
307
401
  customElement('helix-headless-filter-input')
308
402
  ], HelixHeadlessFilterInput);
@@ -107,7 +107,7 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
107
107
  const now = new Date();
108
108
  this.clock = now.getTime();
109
109
  const { next } = elapsedTime(this.locale)(moment, now);
110
- if (!Number.isFinite(next)) {
110
+ if (!Number.isFinite(next) || !this.isConnected) {
111
111
  return;
112
112
  }
113
113
  this.stopTicking = subscribe(next, () => this.tick());
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export * from '@fusion.dev/helix-core';
12
12
  export * from './accordion/index.js';
13
13
  export * from './announcement/index.js';
14
14
  export * from './app-header/index.js';
15
+ export * from './area-chart/index.js';
15
16
  export * from './avatar/index.js';
16
17
  export * from './badge/index.js';
17
18
  export * from './bar-chart/index.js';
@@ -35,6 +36,7 @@ export * from './copy-button/index.js';
35
36
  export * from './data-table/index.js';
36
37
  export * from './date-input/index.js';
37
38
  export * from './date-picker/index.js';
39
+ export * from './description-list/index.js';
38
40
  export * from './dialog/index.js';
39
41
  export * from './divider/index.js';
40
42
  export * from './donut-chart/index.js';
@@ -61,6 +63,7 @@ export * from './meter/index.js';
61
63
  export * from './navigation/index.js';
62
64
  export * from './notifications/index.js';
63
65
  export * from './number-input/index.js';
66
+ export * from './object-inspector/index.js';
64
67
  export * from './option/index.js';
65
68
  export * from './outline/index.js';
66
69
  export * from './pagination/index.js';
@@ -71,9 +74,11 @@ export * from './qr-code/index.js';
71
74
  export * from './quote/index.js';
72
75
  export * from './radio/index.js';
73
76
  export * from './rating/index.js';
77
+ export * from './scatter-chart/index.js';
74
78
  export * from './scroll-area/index.js';
75
79
  export * from './select/index.js';
76
80
  export * from './skeleton/index.js';
81
+ export * from './skip-link/index.js';
77
82
  export * from './slider/index.js';
78
83
  export * from './sparkline/index.js';
79
84
  export * from './speed-dial/index.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from '@fusion.dev/helix-core';
3
3
  export * from './accordion/index.js';
4
4
  export * from './announcement/index.js';
5
5
  export * from './app-header/index.js';
6
+ export * from './area-chart/index.js';
6
7
  export * from './avatar/index.js';
7
8
  export * from './badge/index.js';
8
9
  export * from './bar-chart/index.js';
@@ -26,6 +27,7 @@ export * from './copy-button/index.js';
26
27
  export * from './data-table/index.js';
27
28
  export * from './date-input/index.js';
28
29
  export * from './date-picker/index.js';
30
+ export * from './description-list/index.js';
29
31
  export * from './dialog/index.js';
30
32
  export * from './divider/index.js';
31
33
  export * from './donut-chart/index.js';
@@ -52,6 +54,7 @@ export * from './meter/index.js';
52
54
  export * from './navigation/index.js';
53
55
  export * from './notifications/index.js';
54
56
  export * from './number-input/index.js';
57
+ export * from './object-inspector/index.js';
55
58
  export * from './option/index.js';
56
59
  export * from './outline/index.js';
57
60
  export * from './pagination/index.js';
@@ -62,9 +65,11 @@ export * from './qr-code/index.js';
62
65
  export * from './quote/index.js';
63
66
  export * from './radio/index.js';
64
67
  export * from './rating/index.js';
68
+ export * from './scatter-chart/index.js';
65
69
  export * from './scroll-area/index.js';
66
70
  export * from './select/index.js';
67
71
  export * from './skeleton/index.js';
72
+ export * from './skip-link/index.js';
68
73
  export * from './slider/index.js';
69
74
  export * from './sparkline/index.js';
70
75
  export * from './speed-dial/index.js';
@@ -21,6 +21,11 @@ declare const HelixHeadlessLineChart_base: typeof HelixElement & (new (...args:
21
21
  * **One scale across every series**, the same as the bar chart, because two
22
22
  * series drawn to different scales are two charts sharing a picture.
23
23
  *
24
+ * **Not the chart for a total made of parts.** `area` washes each series from
25
+ * the floor on its own, so they overlap; `Area Chart` stacks them, so the top
26
+ * edge is the total and each band is a share. A line chart answers how each
27
+ * series moved, an area chart how the whole is made up.
28
+ *
24
29
  * **The labels are HTML beside the drawing rather than `<text>` inside it**, for
25
30
  * the reason `BarChart` records: an `<svg>` that fills its box scales its type
26
31
  * with everything else, the same `font-size` arriving at 10px in a narrow chart
@@ -57,10 +62,12 @@ declare const HelixHeadlessLineChart_base: typeof HelixElement & (new (...args:
57
62
  */
58
63
  export declare class HelixHeadlessLineChart extends HelixHeadlessLineChart_base {
59
64
  /**
60
- * Whether a wash is drawn under each line.
65
+ * Whether a wash is drawn under each line, each from the floor on its own.
66
+ * The washes overlap rather than add up: for parts that make a total, use
67
+ * `Area Chart`, which stacks them.
61
68
  *
62
69
  * Absent is lines alone, which is what suits more than one series: washes
63
- * stack and the lower ones are read through the upper. One series over a
70
+ * overlap and the lower ones are read through the upper. One series over a
64
71
  * ground it should feel like a quantity against is the case for this.
65
72
  */
66
73
  area: boolean;
@@ -114,11 +114,10 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
114
114
  * Whether anything survived the filter, kept as state rather than counted at
115
115
  * render time.
116
116
  *
117
- * Counting during render reads the hiding from the *previous* pass, because
118
- * hiding happens in `updated` against options that are the consumer's
119
- * elements rather than this component's markup, so a filter matching nothing
120
- * leaves an empty box with no message in it. Written to state instead, which
121
- * schedules the one further render that shows it.
117
+ * The options are the consumer's elements rather than this component's
118
+ * markup, so the hiding is done to them in `willUpdate` and the render reads
119
+ * the answer it left here. Counted during render instead, a filter matching
120
+ * nothing left an empty box with no message in it.
122
121
  */
123
122
  private hasMatches;
124
123
  /**
@@ -127,8 +126,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
127
126
  * Emptiness is a fact about light DOM children, and the server has none to
128
127
  * count: without this the list shipped "Nothing to show" over markup that was
129
128
  * about to have options in it, which is the one thing an empty state must
130
- * never do. `place` runs from `updated`, which the server never reaches, so
131
- * this is false exactly when the answer is unknown.
129
+ * never do. `place` does not run on the server, so this is false exactly
130
+ * when the answer is unknown.
132
131
  */
133
132
  private counted;
134
133
  protected readonly cascade: DisabledCascadeController;
@@ -231,6 +230,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
231
230
  reveal(): void;
232
231
  /** Hides what the filter excludes, and writes the rest onto the options. */
233
232
  private place;
233
+ /** Scrolls the active option into view, which needs the rendered list to scroll. */
234
+ private followActive;
234
235
  /** Reports the answer and the verdict on it. */
235
236
  private sync;
236
237
  connectedCallback(): void;
@@ -274,7 +274,9 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
274
274
  this.hasMatches = anyShown;
275
275
  }
276
276
  this.counted = true;
277
- const active = shown.find((option) => option.value === this.activeValue);
277
+ }
278
+ followActive() {
279
+ const active = this.shown.find((option) => option.value === this.activeValue);
278
280
  if (active) {
279
281
  const items = this.renderRoot.querySelector('[part="items"]');
280
282
  scrollItemIntoView(active, { padding: 4, ...(items ? { scroller: items } : {}) });
@@ -300,6 +302,9 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
300
302
  super.willUpdate(changed);
301
303
  hostAria(this.internals, { label: this.label, role: 'listbox' });
302
304
  this.internals.ariaMultiSelectable = this.multiple ? 'true' : null;
305
+ if (this.children !== undefined && !this.hydrating) {
306
+ this.place();
307
+ }
303
308
  }
304
309
  get currentValue() {
305
310
  return this.activeValue || (this.value.split(',')[0] ?? '');
@@ -314,7 +319,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
314
319
  }
315
320
  updated(changed) {
316
321
  super.updated(changed);
317
- this.place();
322
+ this.followActive();
318
323
  this.sync();
319
324
  this.linkHost();
320
325
  this.linkActive();
@@ -12,8 +12,9 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
12
12
  *
13
13
  * **The role follows the shape.** A row that can be ticked is
14
14
  * `menuitemcheckbox` and carries `aria-checked`, because a reader is told "one
15
- * of these is on" rather than "here is a button". A row that opens a page keeps
16
- * `menuitem` and says `aria-haspopup`, since the page it opens is a menu.
15
+ * of these is on" rather than "here is a button"; `exclusive` makes it
16
+ * `menuitemradio`, for a set where only one can be. A row that opens a page
17
+ * keeps `menuitem` and says `aria-haspopup`, since the page it opens is a menu.
17
18
  *
18
19
  * Whether it is on the page showing is set by the menu as a property, never as
19
20
  * an attribute: an attribute written onto a consumer's element after it
@@ -64,6 +65,19 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
64
65
  * about who may name a file on somebody's disk rather than a gap here.
65
66
  */
66
67
  download: boolean;
68
+ /**
69
+ * Whether only one row of the set it belongs to can be on.
70
+ *
71
+ * Announcement and nothing else, the way `checkable` is: this element applies
72
+ * no choice and polices no group, so what it does is tell a reader which kind
73
+ * of set they are in. `menuitemcheckbox` says every row here is independent,
74
+ * which for a run of comparisons, or the values of a filter that compares
75
+ * against one, is the opposite of true.
76
+ *
77
+ * Meaningless without `checkable`, since a row that cannot be ticked is not
78
+ * one of a set of anything.
79
+ */
80
+ exclusive: boolean;
67
81
  /**
68
82
  * Where the row goes, which makes it a real link.
69
83
  *
@@ -98,13 +112,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
98
112
  */
99
113
  rel: string;
100
114
  /**
101
- * Whether this row's submenu is the page showing.
115
+ * Whether the page showing is this row's submenu, or one below it.
102
116
  *
103
117
  * Separate from `onPage`, and it has to be. A row that opened a page is not
104
118
  * itself on that page, so both are false for it, and rendering nothing on
105
119
  * that basis takes its `slot[name="submenu"]` with it: the rows inside have
106
- * nowhere to be assigned and are never laid out. The element that holds the
107
- * page has to keep the slot even when it shows no row of its own.
120
+ * nowhere to be assigned and are never laid out. Every row on the way down to
121
+ * the open page has to keep its slot, not only the one directly above it.
122
+ * Three levels deep, the topmost row holds the slot the middle one lives in.
108
123
  */
109
124
  showingSubmenu: boolean;
110
125
  /**
@@ -131,7 +146,7 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
131
146
  */
132
147
  get hasSubmenu(): boolean;
133
148
  /**
134
- * Whether anything is slotted into `submenu`. Read after first render.
149
+ * Whether anything is slotted into `submenu`.
135
150
  *
136
151
  * State rather than a getter over `children`. A getter is read during
137
152
  * `render()` with nothing to re-read it, so a submenu arriving after the
@@ -144,7 +159,40 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
144
159
  * once at first render. See the note there.
145
160
  */
146
161
  private submenu;
162
+ /**
163
+ * Whether anything is slotted before the label.
164
+ *
165
+ * The leading box is drawn only where there is something to put in it, the
166
+ * same way the trailing mark is. What keeps the labels on one edge is the
167
+ * track the menu reserves for the page, not a box on every row, so a row with
168
+ * nothing here takes no box and still lines up with its neighbors.
169
+ *
170
+ * Read from the slot rather than from the light DOM, because the light DOM
171
+ * answer is not reactive: a consumer who adds an icon to one row after first
172
+ * render changes nothing this element is told about, and `slotchange` is
173
+ * exactly the thing that is.
174
+ */
175
+ private start;
176
+ /**
177
+ * Both answers, from the children, before each render.
178
+ *
179
+ * The slots exist only once a render has made them: read through them, the
180
+ * first render drew no caret and no leading box and a second render had to
181
+ * follow it. `slotchange` still reads them, for a child that arrives later.
182
+ * Not on the server, where `children` is undefined, nor while hydrating what
183
+ * one sent.
184
+ */
185
+ protected willUpdate(changed: PropertyValues): void;
147
186
  protected firstUpdated(changed: PropertyValues): void;
187
+ /**
188
+ * Re-reads whether anything is before the label.
189
+ *
190
+ * The slot itself is always rendered and it is the box around it that comes
191
+ * and goes, which is what makes this safe where `readSubmenu` needed the note
192
+ * it carries: there is one `slot[name="start"]` in one template branch, so it
193
+ * is never rebuilt and never fires on its way out.
194
+ */
195
+ private readonly readStart;
148
196
  /**
149
197
  * Re-reads whether anything is in the submenu, from whichever slot is current.
150
198
  *