@fusion.dev/helix-headless 0.164.0 → 0.166.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 (44) hide show
  1. package/custom-elements.json +837 -64
  2. package/dist/callout/root/callout-root.d.ts +8 -2
  3. package/dist/callout/root/callout-root.js +14 -1
  4. package/dist/code-block/root/code-block-root.d.ts +63 -0
  5. package/dist/code-block/root/code-block-root.js +117 -6
  6. package/dist/date-input/root/date-input-root.d.ts +3 -3
  7. package/dist/date-input/root/date-input-root.js +8 -5
  8. package/dist/element-events.generated.js +15 -2
  9. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  10. package/dist/filter-input/menu/filter-menu.js +1 -1
  11. package/dist/format-date/root/format-date-root.d.ts +3 -1
  12. package/dist/format-date/root/format-date-root.js +4 -1
  13. package/dist/lightbox/item/lightbox-item.js +3 -1
  14. package/dist/lightbox/root/lightbox-root.d.ts +28 -0
  15. package/dist/lightbox/root/lightbox-root.js +21 -2
  16. package/dist/listbox/root/listbox-root.js +2 -5
  17. package/dist/menu/item/menu-item.d.ts +20 -0
  18. package/dist/menu/item/menu-item.js +53 -20
  19. package/dist/menu/root/menu-root.d.ts +84 -1
  20. package/dist/menu/root/menu-root.js +187 -44
  21. package/dist/number-input/root/number-input-root.d.ts +32 -9
  22. package/dist/number-input/root/number-input-root.js +53 -9
  23. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -0
  24. package/dist/object-inspector/root/object-inspector-root.js +5 -0
  25. package/dist/option/root/index.d.ts +1 -0
  26. package/dist/option/root/index.js +1 -0
  27. package/dist/steps/root/steps-root.d.ts +7 -0
  28. package/dist/steps/root/steps-root.js +7 -0
  29. package/dist/tag/root/index.d.ts +1 -0
  30. package/dist/tag/root/index.js +1 -0
  31. package/dist/time-input/root/time-input-root.d.ts +4 -3
  32. package/dist/time-input/root/time-input-root.js +5 -4
  33. package/dist/time-picker/root/time-picker-root.d.ts +1 -1
  34. package/dist/time-picker/root/time-picker-root.js +5 -2
  35. package/dist/video/root/video-root.d.ts +2 -0
  36. package/dist/video/root/video-root.js +4 -2
  37. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  38. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  39. package/dist/video-playlist/root/video-playlist-root.js +15 -1
  40. package/lint.json +1 -1
  41. package/package.json +5 -5
  42. package/types/jsx.d.ts +310 -44
  43. package/types/vue.d.ts +68 -11
  44. package/types/words.d.ts +18 -0
@@ -1,5 +1,5 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
1
+ import { type HelixDismissEvent, HelixElement } from '@fusion.dev/helix-core';
2
+ import { type CSSResultGroup, type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessCallout_base: (new (...args: any[]) => import("@fusion.dev/helix-core").DismissibleElement) & typeof HelixElement;
4
4
  /**
5
5
  * @summary A passage set apart as a note, information, a warning, or an error.
@@ -52,6 +52,7 @@ declare const HelixHeadlessCallout_base: (new (...args: any[]) => import("@fusio
52
52
  * @cssstate has-icon - An icon of your own is slotted into `icon`, in place of the tone's.
53
53
  */
54
54
  export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
55
+ static styles: CSSResultGroup;
55
56
  /**
56
57
  * The key this family's words are registered under, its styled tag:
57
58
  * `registerWords('de', { 'helix-callout': { toneLabels: { danger: 'Fehler' } } })`.
@@ -141,6 +142,11 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
141
142
  * heading.
142
143
  */
143
144
  private dismissName;
145
+ /**
146
+ * The event a dismissal sends, naming the note by its heading where it has
147
+ * one, the words its dismiss button is named by, and otherwise by its text.
148
+ */
149
+ protected dismissEvent(): HelixDismissEvent;
144
150
  protected firstUpdated(changed: PropertyValues): void;
145
151
  protected updated(changed: PropertyValues): void;
146
152
  /**
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { animationsSettled, customElement, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
8
- import { html, nothing } from 'lit';
8
+ import { css, html, nothing, } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
11
11
  let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(HelixElement) {
@@ -32,6 +32,11 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
32
32
  this.toggleState('has-icon', filled('icon'));
33
33
  };
34
34
  }
35
+ static { this.styles = css `
36
+ :host {
37
+ display: block;
38
+ }
39
+ `; }
35
40
  static { this.words = 'helix-callout'; }
36
41
  get present() {
37
42
  return this.isConnected && !this.hasAttribute('hidden');
@@ -66,6 +71,14 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
66
71
  ? formatWords(this, 'dismissLabel', { label: heading || tone }).trim()
67
72
  : this.dismissLabel;
68
73
  }
74
+ dismissEvent() {
75
+ const event = super.dismissEvent();
76
+ const heading = slottedWords(this, ['heading']);
77
+ if (heading) {
78
+ event.detail.label = heading;
79
+ }
80
+ return event;
81
+ }
69
82
  firstUpdated(changed) {
70
83
  super.firstUpdated(changed);
71
84
  this.finishHydration();
@@ -71,6 +71,18 @@ import '../../copy-button/index.js';
71
71
  * keeps hydration clean, and draws the diff once it can read the content. Diff
72
72
  * mode is experimental. The rest of the family is stable.
73
73
  *
74
+ * **Several blocks are files**, one shown at a time under a row of tabs, as
75
+ * Sandbox shows its source panes. Put Code Blocks in the default slot of a
76
+ * Code Block, and each is a file named on its tab by its `label` or, without
77
+ * one, its `language`. `file` holds the name of the file shown. The tabs are a
78
+ * tab list: the arrow keys move to a tab and show its file, the two swapping
79
+ * on a right-to-left page, and Home and End go to the first and the last.
80
+ * Choosing one sends `helix-before-change` with the file's name, which you can
81
+ * cancel, and `change` once `file` holds it. The files not shown carry
82
+ * `hidden`, and the one shown draws no name of its own, its tab naming it. One
83
+ * block draws no tabs. A server cannot see the blocks inside, so the client
84
+ * draws the tabs once it upgrades.
85
+ *
74
86
  * @since 0.1.0
75
87
  * @status stable
76
88
  * @category typography
@@ -82,7 +94,11 @@ import '../../copy-button/index.js';
82
94
  * @slot copied-icon - The copy button's icon after a copy. Defaults to a check mark.
83
95
  * @slot copy-icon - The copy button's icon before a copy. Defaults to a clipboard.
84
96
  *
97
+ * @event {Event} change - A file tab showed its file, whose name `file` now holds.
98
+ * @event {HelixBeforeChangeEvent} helix-before-change - A file tab is about to show its file, with the file's name in `detail.value`. Cancelable: cancel it to keep the file shown.
85
99
  * @event {HelixBeforeToggleEvent} helix-before-toggle - The wrap button was pressed, with the `wrap` it asks for in `detail.checked`. Cancelable: cancel it to keep the lines as they are.
100
+ * @event {HelixCopyEvent} helix-copy - The copy button put the code on the clipboard, with the text in `detail.value`.
101
+ * @event {HelixCopyErrorEvent} helix-copy-error - A press on the copy button put nothing on the clipboard: the browser did not allow the write, or there was no code.
86
102
  * @event {HelixToggleEvent} helix-toggle - Fired after the wrap button changed `wrap`, with the new value in `detail.checked`.
87
103
  *
88
104
  * @csspart actions - The box in the header that holds the `actions` slot, drawn while something is slotted there.
@@ -109,6 +125,8 @@ import '../../copy-button/index.js';
109
125
  * @csspart copy-tooltip-top - The tooltip while it is above the copy button. Applied with `copy-tooltip`.
110
126
  * @csspart expand - The button under a capped block that shows every line, and fewer again. Drawn when `expandable` is set and the cap hides something.
111
127
  * @csspart file - In a diff, one file's header and block.
128
+ * @csspart file-tab - One file tab, `aria-selected` while its file is shown.
129
+ * @csspart files - The file tabs over the blocks a block holds, where there are several.
112
130
  * @csspart filler - In a split diff, the empty row opposite a line the other side does not have.
113
131
  * @csspart fold - In a diff with `context-lines`, the button that shows a folded run of unchanged lines, and hides it again, saying which with `aria-expanded`.
114
132
  * @csspart frame - The code, and the copy button beside it where there is no header.
@@ -132,6 +150,7 @@ import '../../copy-button/index.js';
132
150
  * @cssproperty --code-block-mark-start - Written by the block on each `mark`: how far down the code its run of lines starts. Do not set it.
133
151
  *
134
152
  * @cssstate expanded - The expand button has shown every line of a capped block.
153
+ * @cssstate tabbed - The block is one of several files under another block's tabs, which name it, so it draws no name of its own.
135
154
  */
136
155
  export declare class HelixHeadlessCodeBlock extends HelixElement {
137
156
  /**
@@ -198,6 +217,17 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
198
217
  * The expand button's words while the cap hides lines.
199
218
  */
200
219
  expandLabel: string;
220
+ /**
221
+ * The name of the file shown, where the block holds several Code Blocks:
222
+ * the `label` of one, or its `language` without one.
223
+ *
224
+ * Unset, or a name no file has, shows the first.
225
+ */
226
+ file?: string;
227
+ /**
228
+ * The name of the file tabs, read by screen readers.
229
+ */
230
+ filesLabel: string;
201
231
  /**
202
232
  * The words of a button that hides a run of unchanged lines it showed, in a
203
233
  * diff with `context-lines`. `{count}` is how many, and each language
@@ -395,6 +425,39 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
395
425
  * block computed and CSS cannot, and no render follows from them.
396
426
  */
397
427
  private placeLines;
428
+ /** The Code Blocks this block holds as its code, once its children can be read. */
429
+ private get blocks();
430
+ /**
431
+ * Whether the block is one of several files under another block's tabs,
432
+ * which name it. Read from the parent, so a block needs no telling.
433
+ */
434
+ private get tabbed();
435
+ /** The file shown among several: the one `file` names, or the first. */
436
+ private shownFile;
437
+ /** Shows the file a tab names, once nobody refused. */
438
+ private chooseFile;
439
+ /**
440
+ * Arrow keys, `Home` and `End` on the file tabs, which move to a tab and
441
+ * show its file, as a tab list that follows focus does and as Sandbox's file
442
+ * tabs do. Left and Right swap on a right-to-left page.
443
+ */
444
+ private readonly handleFileKey;
445
+ /** The blocks this element hid, to show them again. */
446
+ private readonly hid;
447
+ /** How many files the block last held, so each is told when that changes. */
448
+ private fileCount;
449
+ /**
450
+ * Hides the files not shown, shows any this element hid that is now, and
451
+ * tells each file when how many there are changes, since a file draws its
452
+ * own name only while it is alone.
453
+ */
454
+ private showFile;
455
+ /**
456
+ * The tabs over several files, and the file shown under them. One file
457
+ * draws no tabs, only itself.
458
+ */
459
+ private renderFiles;
460
+ protected willUpdate(changed: PropertyValues): void;
398
461
  protected updated(changed: PropertyValues): void;
399
462
  connectedCallback(): void;
400
463
  disconnectedCallback(): void;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, formatWords, HelixBeforeToggleEvent, HelixElement, HelixToggleEvent, requestChange, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, formatWords, HelixBeforeToggleEvent, HelixElement, HelixToggleEvent, inlineStep, requestChange, requestValue, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
@@ -13,6 +13,17 @@ import '../../copy-button/index.js';
13
13
  import { highlightedLines } from '../highlight.js';
14
14
  import { gutterNumbers, lineCount, lineRuns, lineSpans } from '../lines.js';
15
15
  import { foldContext, pairLines, parsePatch, } from '../patch.js';
16
+ const stopPress = (event) => event.stopPropagation();
17
+ const isBlock = (element) => element?.localName.endsWith('code-block') === true;
18
+ const blocksIn = (host) => [...host.children].filter((child) => child.slot === '' && isBlock(child));
19
+ const nameOf = (block) => {
20
+ const { label, language } = block;
21
+ return ((typeof label === 'string' && label) ||
22
+ block.getAttribute('label') ||
23
+ (typeof language === 'string' && language) ||
24
+ block.getAttribute('language') ||
25
+ '');
26
+ };
16
27
  let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
17
28
  constructor() {
18
29
  super(...arguments);
@@ -25,6 +36,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
25
36
  this.diff = false;
26
37
  this.expandable = false;
27
38
  this.expandLabel = 'Show all lines';
39
+ this.filesLabel = 'Files';
28
40
  this.hideUnchangedLabel = '{count, plural, one {Hide # unchanged line} other {Hide # unchanged lines}}';
29
41
  this.label = '';
30
42
  this.language = '';
@@ -49,6 +61,30 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
49
61
  [...this.renderRoot.querySelectorAll('[part="block"], [part="code"]')].some((box) => box.scrollHeight > box.clientHeight + 1);
50
62
  }
51
63
  };
64
+ this.handleFileKey = (event) => {
65
+ const blocks = this.blocks;
66
+ const count = blocks.length;
67
+ const at = blocks.indexOf(this.shownFile(blocks));
68
+ const step = inlineStep(this, event.key);
69
+ const to = event.key === 'Home'
70
+ ? 0
71
+ : event.key === 'End'
72
+ ? count - 1
73
+ : step === 0
74
+ ? -1
75
+ : (at + step + count) % count;
76
+ const block = blocks[to];
77
+ if (!block) {
78
+ return;
79
+ }
80
+ event.preventDefault();
81
+ this.chooseFile(block);
82
+ void this.updateComplete.then(() => {
83
+ this.renderRoot.querySelector('[part="file-tab"][aria-selected="true"]')?.focus();
84
+ });
85
+ };
86
+ this.hid = new Set();
87
+ this.fileCount = 0;
52
88
  }
53
89
  static { this.words = 'helix-code-block'; }
54
90
  toggleWrap() {
@@ -213,6 +249,67 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
213
249
  }
214
250
  });
215
251
  }
252
+ get blocks() {
253
+ return this.childrenKnown ? blocksIn(this) : [];
254
+ }
255
+ get tabbed() {
256
+ const parent = this.childrenKnown ? this.parentElement : null;
257
+ return this.slot === '' && isBlock(parent) && blocksIn(parent).length > 1;
258
+ }
259
+ shownFile(blocks) {
260
+ return blocks.find((block) => nameOf(block) === this.file) ?? blocks[0];
261
+ }
262
+ chooseFile(block) {
263
+ if (block !== this.shownFile(this.blocks)) {
264
+ requestValue(this, nameOf(block), (name) => {
265
+ this.file = name;
266
+ });
267
+ }
268
+ }
269
+ showFile() {
270
+ const blocks = this.blocks;
271
+ const files = blocks.length > 1 ? blocks : [];
272
+ const shown = this.shownFile(files);
273
+ for (const block of files) {
274
+ if (block !== shown && !block.hidden) {
275
+ block.hidden = true;
276
+ this.hid.add(block);
277
+ }
278
+ }
279
+ for (const block of this.hid) {
280
+ if (block === shown || !files.includes(block)) {
281
+ ;
282
+ block.hidden = false;
283
+ this.hid.delete(block);
284
+ }
285
+ }
286
+ if (blocks.length !== this.fileCount) {
287
+ this.fileCount = blocks.length;
288
+ for (const block of blocks) {
289
+ block.requestUpdate?.();
290
+ }
291
+ }
292
+ }
293
+ renderFiles(blocks) {
294
+ const slot = html `<slot @slotchange=${() => this.requestUpdate()}></slot>`;
295
+ if (blocks.length < 2) {
296
+ return slot;
297
+ }
298
+ const shown = this.shownFile(blocks);
299
+ return html `<div part="files" role="tablist" aria-label=${this.filesLabel} @keydown=${this.handleFileKey}>
300
+ ${blocks.map((block, at) => {
301
+ const selected = block === shown;
302
+ return html `<button part="file-tab" id=${`file-${at}`} role="tab" type="button"
303
+ aria-controls="files-panel" aria-selected=${selected ? 'true' : 'false'}
304
+ tabindex=${selected ? '0' : '-1'} @click=${() => this.chooseFile(block)}>${nameOf(block)}</button>`;
305
+ })}
306
+ </div>
307
+ <div id="files-panel" role="tabpanel" aria-labelledby=${`file-${blocks.indexOf(shown)}`}>${slot}</div>`;
308
+ }
309
+ willUpdate(changed) {
310
+ super.willUpdate(changed);
311
+ this.toggleState('tabbed', this.tabbed);
312
+ }
216
313
  updated(changed) {
217
314
  super.updated(changed);
218
315
  if (typeof ResizeObserver !== 'undefined') {
@@ -220,6 +317,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
220
317
  this.sizes.observe(this);
221
318
  }
222
319
  this.measure();
320
+ this.showFile();
223
321
  }
224
322
  connectedCallback() {
225
323
  super.connectedCallback();
@@ -239,6 +337,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
239
337
  this.finishHydration();
240
338
  }
241
339
  render() {
340
+ const blocks = this.blocks;
341
+ if (blocks.length > 0) {
342
+ return this.renderFiles(blocks);
343
+ }
242
344
  const files = this.files;
243
345
  if (files.length > 0) {
244
346
  return this.renderDiff(files);
@@ -273,10 +375,11 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
273
375
  ? html `<span part="gutter" aria-hidden="true">${numbers}</span>`
274
376
  : nothing;
275
377
  const name = this.label || this.language;
276
- return html `${this.renderHeader(name)}
378
+ const drawn = this.tabbed ? '' : name;
379
+ return html `${this.renderHeader(name, undefined, drawn)}
277
380
  <div part="frame">
278
381
  <pre id="block" part="block" translate="no">${gutter}${code}</pre>
279
- ${this.headed(name) ? nothing : this.renderCopy()}
382
+ ${this.headed(drawn) ? nothing : this.renderCopy()}
280
383
  </div>${this.renderExpand()}`;
281
384
  }
282
385
  renderExpand() {
@@ -299,10 +402,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
299
402
  headed(name) {
300
403
  return name !== '' || (this.wrappable && !this.diff) || this.slotted('actions');
301
404
  }
302
- renderHeader(name, value) {
405
+ renderHeader(name, value, drawn = name) {
303
406
  const acting = this.slotted('actions');
304
- return this.headed(name)
305
- ? html `<div part="header">${name ? html `<span part="label">${name}</span>` : nothing}${acting ? html `<span part="actions"><slot name="actions"></slot></span>` : nothing}${this.renderWrapToggle()}${this.renderCopy(value, name)}</div>`
407
+ return this.headed(drawn)
408
+ ? html `<div part="header">${drawn ? html `<span part="label">${drawn}</span>` : nothing}${acting ? html `<span part="actions"><slot name="actions"></slot></span>` : nothing}${this.renderWrapToggle()}${this.renderCopy(value, name)}</div>`
306
409
  : nothing;
307
410
  }
308
411
  renderWrapToggle() {
@@ -331,6 +434,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
331
434
  tooltip-arrow-top: copy-tooltip-arrow-top, tooltip-bottom: copy-tooltip-bottom,
332
435
  tooltip-left: copy-tooltip-left, tooltip-right: copy-tooltip-right, tooltip-top: copy-tooltip-top"
333
436
  copied-label=${this.copiedLabel}
437
+ @helix-press=${stopPress}
334
438
  label=${name ? formatWords(this, 'copyNameLabel', { copy: this.copyLabel, name }) : this.copyLabel}
335
439
  .value=${value}
336
440
  >${this.renderCopyIcons()}</${this.tag('copy-button')}>`;
@@ -434,6 +538,13 @@ __decorate([
434
538
  words,
435
539
  property({ attribute: 'expand-label' })
436
540
  ], HelixHeadlessCodeBlock.prototype, "expandLabel", void 0);
541
+ __decorate([
542
+ property()
543
+ ], HelixHeadlessCodeBlock.prototype, "file", void 0);
544
+ __decorate([
545
+ words,
546
+ property({ attribute: 'files-label' })
547
+ ], HelixHeadlessCodeBlock.prototype, "filesLabel", void 0);
437
548
  __decorate([
438
549
  words,
439
550
  property({ attribute: 'hide-unchanged-label' })
@@ -1,7 +1,7 @@
1
1
  import { AriaForwardController, type DateCalendar, HelixFormElement, type MonthCalendar, type MonthSystem } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../date-picker/index.js';
4
- import '../../tag/index.js';
4
+ import '../../tag/root/index.js';
5
5
  declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSegmentedFieldInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinPickerEditingInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinPickerFieldInterface) & (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;
6
6
  /**
7
7
  * @summary A date, typed or picked from a calendar.
@@ -474,8 +474,8 @@ export declare class HelixHeadlessDateInput extends HelixHeadlessDateInput_base
474
474
  /**
475
475
  * Each date, where `mode` holds several, sent one entry each.
476
476
  *
477
- * One date that is text and no date, such as `August 5`, is sent as an empty
478
- * entry, as a native date field sends a date it cannot read.
477
+ * Text that is no date, such as `August 5`, is sent as one empty entry, as
478
+ * a native date field sends a date it cannot read, in every mode.
479
479
  */
480
480
  protected get answers(): readonly string[] | undefined;
481
481
  /** What is wrong with the dates, which the text input cannot say. */
@@ -11,7 +11,7 @@ import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import '../../date-picker/index.js';
14
- import '../../tag/index.js';
14
+ import '../../tag/root/index.js';
15
15
  const dates = (value) => value
16
16
  .replace(/\s+[–-]\s+/g, ',')
17
17
  .split(',')
@@ -70,9 +70,11 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
70
70
  };
71
71
  this.handleTextCommit = (event) => {
72
72
  if (this.mode !== 'multiple') {
73
- const held = this.mode === 'range' && /\s[–-]\s/.test(this.value)
74
- ? this.value.split(/\s+[–-]\s+/).map((one) => one.trim())
75
- : dates(this.value);
73
+ const held = this.mode !== 'range'
74
+ ? [this.value.trim()].filter(Boolean)
75
+ : /\s[–-]\s/.test(this.value)
76
+ ? this.value.split(/\s+[–-]\s+/).map((one) => one.trim())
77
+ : dates(this.value);
76
78
  const read = held.map((one) => this.read(one));
77
79
  if (held.length > 0 && !held.every(isDateValue) && read.every(Boolean)) {
78
80
  this.requestValue(read.join(','));
@@ -220,7 +222,8 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
220
222
  }
221
223
  get answers() {
222
224
  if (this.mode !== undefined) {
223
- return dates(this.value);
225
+ const held = dates(this.value);
226
+ return held.every(isDateValue) ? held : [''];
224
227
  }
225
228
  const text = this.value.trim();
226
229
  return text === '' || isDateValue(text) ? undefined : [''];
@@ -36,7 +36,14 @@ registerElementEvents({
36
36
  'helix-headless-chat-message': ['helix-send'],
37
37
  'helix-headless-checkbox': ['change', 'helix-before-toggle', 'input'],
38
38
  'helix-headless-checkbox-group': ['change', 'helix-before-change', 'input'],
39
- 'helix-headless-code-block': ['helix-before-toggle', 'helix-toggle'],
39
+ 'helix-headless-code-block': [
40
+ 'change',
41
+ 'helix-before-change',
42
+ 'helix-before-toggle',
43
+ 'helix-copy',
44
+ 'helix-copy-error',
45
+ 'helix-toggle',
46
+ ],
40
47
  'helix-headless-color-input': [
41
48
  'change',
42
49
  'helix-before-change',
@@ -186,7 +193,13 @@ registerElementEvents({
186
193
  'helix-headless-map-route': ['helix-map-part-change'],
187
194
  'helix-headless-map-terrain': ['helix-map-part-change'],
188
195
  'helix-headless-megamenu': ['helix-before-close', 'helix-before-open', 'toggle'],
189
- 'helix-headless-menu': ['helix-before-close', 'helix-before-open', 'helix-select', 'toggle'],
196
+ 'helix-headless-menu': [
197
+ 'helix-before-close',
198
+ 'helix-before-open',
199
+ 'helix-filter',
200
+ 'helix-select',
201
+ 'toggle',
202
+ ],
190
203
  'helix-headless-menu-item': ['helix-before-toggle', 'helix-select'],
191
204
  'helix-headless-meter': ['helix-band'],
192
205
  'helix-headless-notification': ['helix-dismiss', 'helix-read'],
@@ -82,6 +82,7 @@ import '../../menu/index.js';
82
82
  * @csspart menu-header - The bar above a field's page, with the back button and the field's name.
83
83
  * @csspart menu-live - The inner menu's `live` part: the region that screen readers announce the empty and loading messages from.
84
84
  * @csspart menu-loading - Shown while the fields are loading.
85
+ * @csspart menu-search-field - The inner menu's filter field, drawn with `filterable`.
85
86
  * @csspart menu-surface - The floating panel the menu is drawn on.
86
87
  * @csspart menu-title - The name of the field whose page is showing.
87
88
  */
@@ -126,7 +126,7 @@ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement
126
126
  return html `<${menu}
127
127
  part="menu"
128
128
  exportparts="back: menu-back, back-mark: menu-back-mark, empty: menu-empty, header: menu-header,
129
- live: menu-live, loading: menu-loading, surface: menu-surface, title: menu-title"
129
+ live: menu-live, loading: menu-loading, search-field: menu-search-field, surface: menu-surface, title: menu-title"
130
130
  label=${this.label}
131
131
  @helix-select=${this.handleSelect}
132
132
  >
@@ -200,7 +200,9 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
200
200
  * `narrow` ("3h ago").
201
201
  *
202
202
  * The reading refreshes as it goes stale, as `elapsed` does, and the whole
203
- * date is its title. `elapsed` wins where both are set.
203
+ * date is its title. `elapsed` wins where both are set. Written with no
204
+ * value, as `elapsed` is, or with a style `Intl` does not know, it reads
205
+ * `long`.
204
206
  */
205
207
  relative?: 'long' | 'narrow' | 'short';
206
208
  /**
@@ -10,6 +10,7 @@ import { property } from 'lit/decorators.js';
10
10
  import { drawFields } from '../fields.js';
11
11
  import { formatOptions } from '../options.js';
12
12
  const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
13
+ const STYLES = ['long', 'narrow', 'short'];
13
14
  let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement {
14
15
  constructor() {
15
16
  super(...arguments);
@@ -53,7 +54,9 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
53
54
  ? elapsedTime(this.language)(moment, now)
54
55
  : relativeTime(this.language, {
55
56
  numeric: this.numeric ?? 'auto',
56
- style: this.relative ?? 'long',
57
+ style: STYLES.includes(this.relative)
58
+ ? this.relative
59
+ : 'long',
57
60
  })(moment, now);
58
61
  }
59
62
  connectedCallback() {
@@ -47,7 +47,9 @@ __decorate([
47
47
  property({ type: Boolean })
48
48
  ], HelixHeadlessLightboxItem.prototype, "current", void 0);
49
49
  __decorate([
50
- property()
50
+ property({
51
+ converter: { fromAttribute: (value) => value ?? '' },
52
+ })
51
53
  ], HelixHeadlessLightboxItem.prototype, "label", void 0);
52
54
  __decorate([
53
55
  property({ attribute: false })
@@ -258,6 +258,32 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
258
258
  * which is the first time anything asked it for its items on a server.
259
259
  */
260
260
  private get items();
261
+ /**
262
+ * The set changed: render it again.
263
+ *
264
+ * `items` is read on each render and nothing else marks a render as due when
265
+ * an item arrives or leaves, so an item appended after the first render had
266
+ * no role description, no place in the set, and no part in the count until
267
+ * some unrelated property changed.
268
+ */
269
+ private readonly handleSlotChange;
270
+ /**
271
+ * Watches each item's `label` and `value`, which no slot reports.
272
+ *
273
+ * `value` decides which item `active` names, and `label` fills `{label}` in
274
+ * `status-label` and `slide-label`. A change to either left the old item
275
+ * shown, or the old words in the counter, until some unrelated property
276
+ * changed. `Select` watches its options for the same reason.
277
+ *
278
+ * The subtree, since an observer on the host sees no attribute on its
279
+ * children otherwise. Only changes on the items themselves count: a `<progress
280
+ * value>` inside an item, updated while a video plays, would otherwise render
281
+ * the viewer again on each update.
282
+ *
283
+ * Made on connection, there being no `MutationObserver` on a server.
284
+ */
285
+ private itemWatch;
286
+ connectedCallback(): void;
261
287
  /** The native dialog, or nothing where there is no DOM to hold one. */
262
288
  private get dialog();
263
289
  /** Opens the viewer, which makes the rest of the page inert. */
@@ -322,6 +348,8 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
322
348
  private zoomOut;
323
349
  /** Home and End, which are an index rather than a step. */
324
350
  private toEnd;
351
+ /** The item shown at the last update, whose zoom a different item does not keep. */
352
+ private zoomedItem;
325
353
  /** The item on screen: the one `active` names, or the first where it names none. */
326
354
  private get shown();
327
355
  /**
@@ -92,6 +92,10 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
92
92
  '--toggle': () => this.toggleOpen(),
93
93
  },
94
94
  });
95
+ this.handleSlotChange = () => {
96
+ this.requestUpdate();
97
+ };
98
+ this.itemWatch = undefined;
95
99
  this.handleCancel = (event) => {
96
100
  event.preventDefault();
97
101
  this.requestOpen(false);
@@ -129,6 +133,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
129
133
  this.toEnd(this.items.length - 1);
130
134
  }
131
135
  };
136
+ this.zoomedItem = undefined;
132
137
  this.handleBackdrop = (event) => {
133
138
  if (this.dragged(event) || !this.onEmptyArea(event)) {
134
139
  return;
@@ -177,6 +182,16 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
177
182
  get items() {
178
183
  return this.knownChildren.filter((child) => child instanceof HelixHeadlessLightboxItem);
179
184
  }
185
+ connectedCallback() {
186
+ super.connectedCallback();
187
+ this.itemWatch = new MutationObserver((records) => {
188
+ if (records.some((record) => record.target instanceof HelixHeadlessLightboxItem &&
189
+ record.target.parentElement === this)) {
190
+ this.requestUpdate();
191
+ }
192
+ });
193
+ this.itemWatch.observe(this, { attributeFilter: ['label', 'value'], subtree: true });
194
+ }
180
195
  get dialog() {
181
196
  return this.renderRoot?.querySelector('dialog') ?? null;
182
197
  }
@@ -292,16 +307,20 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
292
307
  this.toggleState('fullscreen', this.filled);
293
308
  this.toggleState('idle', this.resting);
294
309
  this.toggleState('zoomable', this.maxZoom > 1);
295
- if (changed.has('active') || (changed.has('open') && !this.open)) {
310
+ const shown = this.shown;
311
+ if (shown !== this.zoomedItem || (changed.has('open') && !this.open)) {
296
312
  this.quietZoom = true;
297
313
  this.zoomer.reset();
298
314
  this.quietZoom = false;
299
315
  }
316
+ this.zoomedItem = shown;
300
317
  this.say(changed);
301
318
  }
302
319
  disconnectedCallback() {
303
320
  super.disconnectedCallback();
304
321
  unlockScroll(this);
322
+ this.itemWatch?.disconnect();
323
+ this.itemWatch = undefined;
305
324
  }
306
325
  updated(changed) {
307
326
  super.updated(changed);
@@ -395,7 +414,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
395
414
  >
396
415
  <slot name="header" part="header"></slot>
397
416
  <div part="viewport">
398
- <slot></slot>
417
+ <slot @slotchange=${this.handleSlotChange}></slot>
399
418
  </div>
400
419
  <slot name="footer" part="footer"></slot>
401
420
  ${this.controls ? this.renderControls() : nothing}
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, centerInView, chosenValues, columnStep, countLabel, customElement, DelayController, DisabledCascadeController, drawAutofill, endInSight, filterKeeps, formatWords, HelixFilterEvent, HelixFormElement, HelixLoadMoreEvent, hostAria, inlineStep, MixinCollection, MixinControllerValue, MixinField, markMatches, OptionWindowController, onApple, pageRows, rangeAdd, reportValues, requestChange, scrollItemIntoView, selectsAll, Typeahead, toggleEvery, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { magnifyingGlass } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -400,12 +400,9 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
400
400
  this.forgetRemoved();
401
401
  const typed = this.filter.trim();
402
402
  const needle = typed.toLowerCase();
403
- const match = this.filterMatch;
404
403
  const chosen = new Set(this.values);
405
404
  for (const option of this.entries) {
406
- option.hidden =
407
- needle !== '' &&
408
- !(match ? match(option.text, typed) : option.text.toLowerCase().includes(needle));
405
+ option.hidden = !filterKeeps(option.text, typed, this.filterMatch);
409
406
  option.selected = chosen.has(option.value);
410
407
  }
411
408
  for (const group of this.groups) {