@fusion.dev/helix-headless 0.23.0 → 0.24.1

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 (124) hide show
  1. package/custom-elements.json +50579 -26954
  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/avatar/group/avatar-group.d.ts +1 -0
  8. package/dist/avatar/group/avatar-group.js +5 -1
  9. package/dist/avatar/root/avatar-root.d.ts +1 -0
  10. package/dist/avatar/root/avatar-root.js +11 -0
  11. package/dist/carousel/root/carousel-root.d.ts +3 -0
  12. package/dist/carousel/root/carousel-root.js +23 -4
  13. package/dist/chat/index.d.ts +1 -1
  14. package/dist/chat/message/chat-message.d.ts +7 -0
  15. package/dist/chat/message/chat-message.js +2 -1
  16. package/dist/chat/root/chat-root.d.ts +195 -4
  17. package/dist/chat/root/chat-root.js +316 -23
  18. package/dist/checkbox-group/root/checkbox-group-root.d.ts +2 -0
  19. package/dist/checkbox-group/root/checkbox-group-root.js +12 -2
  20. package/dist/code-block/index.d.ts +1 -0
  21. package/dist/code-block/index.js +1 -0
  22. package/dist/code-block/patch.d.ts +62 -0
  23. package/dist/code-block/patch.js +93 -0
  24. package/dist/code-block/root/code-block-root.d.ts +82 -17
  25. package/dist/code-block/root/code-block-root.js +83 -6
  26. package/dist/color-picker/root/color-picker-root.d.ts +4 -3
  27. package/dist/color-picker/root/color-picker-root.js +4 -1
  28. package/dist/combobox/root/combobox-root.d.ts +1 -0
  29. package/dist/combobox/root/combobox-root.js +6 -1
  30. package/dist/command/root/command-root.d.ts +7 -0
  31. package/dist/command/root/command-root.js +10 -0
  32. package/dist/data-table/root/data-table-root.d.ts +195 -23
  33. package/dist/data-table/root/data-table-root.js +308 -27
  34. package/dist/description-list/index.d.ts +2 -0
  35. package/dist/description-list/index.js +2 -0
  36. package/dist/description-list/item/description-list-item.d.ts +39 -0
  37. package/dist/description-list/item/description-list-item.js +26 -0
  38. package/dist/description-list/root/description-list-root.d.ts +56 -0
  39. package/dist/description-list/root/description-list-root.js +21 -0
  40. package/dist/element-events.generated.js +27 -6
  41. package/dist/filter-input/chip/filter-chip.d.ts +114 -0
  42. package/dist/filter-input/chip/filter-chip.js +61 -0
  43. package/dist/filter-input/field/filter-field.d.ts +128 -0
  44. package/dist/filter-input/field/filter-field.js +70 -0
  45. package/dist/filter-input/grammar.d.ts +96 -0
  46. package/dist/filter-input/grammar.js +133 -0
  47. package/dist/filter-input/index.d.ts +5 -0
  48. package/dist/filter-input/index.js +5 -0
  49. package/dist/filter-input/menu/filter-menu.d.ts +153 -0
  50. package/dist/filter-input/menu/filter-menu.js +164 -0
  51. package/dist/filter-input/operator/filter-operator.d.ts +105 -0
  52. package/dist/filter-input/operator/filter-operator.js +39 -0
  53. package/dist/filter-input/root/filter-input-root.d.ts +118 -9
  54. package/dist/filter-input/root/filter-input-root.js +96 -2
  55. package/dist/format-date/root/format-date-root.js +5 -2
  56. package/dist/image/root/image-root.d.ts +9 -0
  57. package/dist/image/root/image-root.js +16 -3
  58. package/dist/index.d.ts +5 -0
  59. package/dist/index.js +5 -0
  60. package/dist/kbd/root/kbd-root.d.ts +9 -2
  61. package/dist/kbd/root/kbd-root.js +7 -2
  62. package/dist/line-chart/root/line-chart-root.d.ts +9 -2
  63. package/dist/listbox/root/listbox-root.d.ts +8 -7
  64. package/dist/listbox/root/listbox-root.js +7 -2
  65. package/dist/menu/item/menu-item.d.ts +31 -6
  66. package/dist/menu/item/menu-item.js +18 -5
  67. package/dist/menu/root/menu-root.js +7 -2
  68. package/dist/notifications/group/notification-group.d.ts +129 -0
  69. package/dist/notifications/group/notification-group.js +106 -0
  70. package/dist/notifications/index.d.ts +1 -0
  71. package/dist/notifications/index.js +1 -0
  72. package/dist/notifications/notification/notification.d.ts +39 -13
  73. package/dist/notifications/notification/notification.js +11 -8
  74. package/dist/notifications/root/notifications-root.d.ts +57 -1
  75. package/dist/notifications/root/notifications-root.js +35 -4
  76. package/dist/number-input/root/number-input-root.d.ts +21 -1
  77. package/dist/number-input/root/number-input-root.js +18 -5
  78. package/dist/object-inspector/index.d.ts +1 -0
  79. package/dist/object-inspector/index.js +1 -0
  80. package/dist/object-inspector/root/object-inspector-root.d.ts +183 -0
  81. package/dist/object-inspector/root/object-inspector-root.js +348 -0
  82. package/dist/option/root/option-root.d.ts +17 -0
  83. package/dist/option/root/option-root.js +4 -0
  84. package/dist/outline/root/outline-root.d.ts +16 -1
  85. package/dist/outline/root/outline-root.js +27 -9
  86. package/dist/qr-code/root/qr-code-root.d.ts +3 -1
  87. package/dist/qr-code/root/qr-code-root.js +11 -1
  88. package/dist/radio/group/radio-group.d.ts +1 -0
  89. package/dist/rating/root/rating-root.d.ts +3 -0
  90. package/dist/rating/root/rating-root.js +9 -0
  91. package/dist/scatter-chart/index.d.ts +1 -0
  92. package/dist/scatter-chart/index.js +1 -0
  93. package/dist/scatter-chart/root/scatter-chart-root.d.ts +144 -0
  94. package/dist/scatter-chart/root/scatter-chart-root.js +205 -0
  95. package/dist/select/root/select-root.d.ts +8 -0
  96. package/dist/select/root/select-root.js +8 -6
  97. package/dist/skip-link/index.d.ts +1 -0
  98. package/dist/skip-link/index.js +1 -0
  99. package/dist/skip-link/root/skip-link-root.d.ts +81 -0
  100. package/dist/skip-link/root/skip-link-root.js +58 -0
  101. package/dist/speed-dial/root/speed-dial-root.d.ts +1 -0
  102. package/dist/speed-dial/root/speed-dial-root.js +6 -2
  103. package/dist/steps/root/steps-root.d.ts +1 -0
  104. package/dist/steps/root/steps-root.js +5 -2
  105. package/dist/table/column/table-column.d.ts +79 -0
  106. package/dist/table/column/table-column.js +125 -5
  107. package/dist/table/root/table-root.d.ts +2 -0
  108. package/dist/table/root/table-root.js +12 -3
  109. package/dist/time-picker/root/time-picker-root.d.ts +9 -0
  110. package/dist/time-picker/root/time-picker-root.js +5 -0
  111. package/dist/toast/root/toast-root.d.ts +2 -0
  112. package/dist/tooltip/root/tooltip-root.js +1 -1
  113. package/dist/tour/root/tour-root.d.ts +14 -0
  114. package/dist/tour/root/tour-root.js +43 -18
  115. package/dist/tree/item/tree-item.d.ts +7 -4
  116. package/dist/tree/item/tree-item.js +4 -1
  117. package/dist/video/root/video-root.d.ts +23 -1
  118. package/dist/video/root/video-root.js +46 -23
  119. package/dist/virtual-list/root/virtual-list-root.d.ts +10 -6
  120. package/dist/virtual-list/root/virtual-list-root.js +10 -4
  121. package/package.json +5 -5
  122. package/types/jsx.d.ts +4536 -346
  123. package/types/svelte.d.ts +11 -0
  124. package/types/vue.d.ts +738 -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);
@@ -73,11 +73,14 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
73
73
  }
74
74
  firstUpdated(changed) {
75
75
  super.firstUpdated(changed);
76
- this.read();
76
+ this.finishHydration();
77
77
  this.watch();
78
78
  }
79
79
  willUpdate(changed) {
80
80
  super.willUpdate(changed);
81
+ if (this.childrenKnown) {
82
+ this.read();
83
+ }
81
84
  if (changed.has('date') || changed.has('elapsed') || changed.has('locale')) {
82
85
  this.tick();
83
86
  }
@@ -107,7 +110,7 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
107
110
  const now = new Date();
108
111
  this.clock = now.getTime();
109
112
  const { next } = elapsedTime(this.locale)(moment, now);
110
- if (!Number.isFinite(next)) {
113
+ if (!Number.isFinite(next) || !this.isConnected) {
111
114
  return;
112
115
  }
113
116
  this.stopTicking = subscribe(next, () => this.tick());
@@ -150,6 +150,15 @@ export declare class HelixHeadlessImage extends HelixElement {
150
150
  * image it has, so deciding `error` here calls every picture broken the moment
151
151
  * it renders. Failure has an event of its own and that event is reliable, so
152
152
  * this is only here to catch the success that happened too early to hear.
153
+ *
154
+ * **Except for a picture a server sent**, which a real browser's parser has
155
+ * already fetched: finished with no pixels there is a failure, and its `error`
156
+ * fired before anything could listen, so the placeholder stayed up over a
157
+ * picture that was never coming.
158
+ *
159
+ * **Once per picture.** The first update asks from `firstUpdated` and from
160
+ * `updated`, and a picture already finished answered both, so a page heard
161
+ * every cached `load`, and every `error` a server's page had, twice.
153
162
  */
154
163
  private settle;
155
164
  protected willUpdate(changed: PropertyValues): void;
@@ -34,14 +34,27 @@ let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
34
34
  this.relayNativeEvent(event);
35
35
  return;
36
36
  }
37
- this.dispatchEvent(new Event(name, { bubbles: false, cancelable: false, composed: false }));
37
+ const send = () => {
38
+ this.dispatchEvent(new Event(name, { bubbles: false, cancelable: false, composed: false }));
39
+ };
40
+ if (this.serverRendered) {
41
+ setTimeout(send);
42
+ }
43
+ else {
44
+ send();
45
+ }
38
46
  }
39
47
  settle() {
40
48
  const image = this.renderRoot?.querySelector('img');
41
- if (this.src === '' || !image?.complete || image.naturalWidth === 0) {
49
+ if (this.src === '' || this.status !== 'loading' || !image?.complete) {
42
50
  return;
43
51
  }
44
- this.announce('loaded');
52
+ if (image.naturalWidth > 0) {
53
+ this.announce('loaded');
54
+ }
55
+ else if (this.serverRendered) {
56
+ this.announce('error');
57
+ }
45
58
  }
46
59
  willUpdate(changed) {
47
60
  super.willUpdate(changed);
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';
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A keyboard key or chord, set inline with text.
5
5
  *
@@ -57,7 +57,13 @@ export declare class HelixHeadlessKbd extends HelixElement {
57
57
  connectedCallback(): void;
58
58
  disconnectedCallback(): void;
59
59
  protected updated(changed: Map<string, unknown>): void;
60
- /** The chord as this reader's keyboard labels it. */
60
+ /**
61
+ * The chord as this reader's keyboard labels it.
62
+ *
63
+ * As a server labels it while hydrating what one sent: it has no keyboard to
64
+ * ask and serves the words, and a render that differs is a text Lit records
65
+ * and never writes, so a Mac kept Ctrl. The update after hydration asks.
66
+ */
61
67
  private get label();
62
68
  private listen;
63
69
  private deafen;
@@ -72,6 +78,7 @@ export declare class HelixHeadlessKbd extends HelixElement {
72
78
  private get printable();
73
79
  /** Whether a press is this chord, modifiers and all. */
74
80
  private matches_;
81
+ protected firstUpdated(changed: PropertyValues): void;
75
82
  protected render(): TemplateResult;
76
83
  }
77
84
  declare global {
@@ -105,13 +105,14 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
105
105
  }
106
106
  }
107
107
  get label() {
108
- const names = onApple() ? APPLE : ELSEWHERE;
108
+ const apple = !this.hydrating && onApple();
109
+ const names = apple ? APPLE : ELSEWHERE;
109
110
  const parts = this.keys
110
111
  .split('+')
111
112
  .map((part) => part.trim())
112
113
  .filter((part) => part !== '')
113
114
  .map((part) => names[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part));
114
- return parts.join(onApple() ? '' : '+');
115
+ return parts.join(apple ? '' : '+');
115
116
  }
116
117
  listen() {
117
118
  if (!this.within || this.keys === '') {
@@ -157,6 +158,10 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
157
158
  ? true
158
159
  : !press.metaKey));
159
160
  }
161
+ firstUpdated(changed) {
162
+ super.firstUpdated(changed);
163
+ this.finishHydration();
164
+ }
160
165
  render() {
161
166
  return html `<kbd part="kbd">${this.keys === '' ? html `<slot></slot>` : this.label}</kbd>`;
162
167
  }
@@ -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();