@fusion.dev/helix-headless 0.170.0 → 0.171.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 (39) hide show
  1. package/custom-elements.json +610 -93
  2. package/dist/chat/composer/chat-composer.d.ts +2 -1
  3. package/dist/chat/composer/chat-composer.js +7 -1
  4. package/dist/chat/root/chat-root.d.ts +36 -6
  5. package/dist/chat/root/chat-root.js +53 -5
  6. package/dist/code-block/root/code-block-root.d.ts +20 -2
  7. package/dist/code-block/root/code-block-root.js +29 -8
  8. package/dist/color-picker/root/color-picker-root.d.ts +50 -7
  9. package/dist/color-picker/root/color-picker-root.js +62 -1
  10. package/dist/combobox/root/combobox-root.d.ts +16 -6
  11. package/dist/combobox/root/combobox-root.js +9 -2
  12. package/dist/command/root/command-root.d.ts +16 -1
  13. package/dist/command/root/command-root.js +8 -0
  14. package/dist/filter-input/chip/filter-chip.d.ts +13 -3
  15. package/dist/filter-input/chip/filter-chip.js +5 -0
  16. package/dist/filter-input/menu/filter-menu.js +0 -1
  17. package/dist/filter-input/root/filter-input-root.d.ts +71 -4
  18. package/dist/filter-input/root/filter-input-root.js +87 -2
  19. package/dist/format-date/root/format-date-root.d.ts +3 -1
  20. package/dist/input/root/input-root.d.ts +6 -0
  21. package/dist/input/root/input-root.js +13 -0
  22. package/dist/listbox/root/listbox-root.d.ts +6 -0
  23. package/dist/listbox/root/listbox-root.js +6 -1
  24. package/dist/number-input/root/number-input-root.d.ts +16 -6
  25. package/dist/number-input/root/number-input-root.js +15 -4
  26. package/dist/select/root/select-root.js +2 -0
  27. package/dist/steps/root/steps-root.d.ts +32 -8
  28. package/dist/steps/root/steps-root.js +70 -9
  29. package/dist/steps/step/step.d.ts +9 -1
  30. package/dist/steps/step/step.js +5 -0
  31. package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
  32. package/dist/video-playlist/root/video-playlist-root.js +14 -3
  33. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  34. package/dist/virtual-list/root/virtual-list-root.js +7 -2
  35. package/lint.json +1 -1
  36. package/package.json +5 -5
  37. package/types/jsx.d.ts +490 -116
  38. package/types/vue.d.ts +108 -25
  39. package/types/words.d.ts +12 -2
@@ -25,7 +25,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
25
25
  * field taking more. A draft that fails them does not send, and the composer
26
26
  * takes `:state(invalid)` and `aria-invalid` once the reader has tried to send
27
27
  * it, the form was submitted, or the reader edited the field and left it.
28
- * Tabbing past an empty composer marks nothing.
28
+ * Sending an empty required composer counts as trying. Tabbing past an empty
29
+ * composer marks nothing.
29
30
  *
30
31
  * **`typing` says whether the user is writing**, and `helix-typing` is sent
31
32
  * each time it changes: when they start, and when they send or stop for
@@ -69,7 +69,13 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
69
69
  }
70
70
  send() {
71
71
  const value = this.value.trim();
72
- if (value === '' || this.isDisabled) {
72
+ if (this.isDisabled) {
73
+ return false;
74
+ }
75
+ if (value === '') {
76
+ if (this.required) {
77
+ this.touch();
78
+ }
73
79
  return false;
74
80
  }
75
81
  if (!this.internals.validity.valid) {
@@ -43,8 +43,10 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
43
43
  * there when the transcript opened. A turn is said with its author first, such
44
44
  * as "Ada: It ships today" (`arrival-label`), where its author can be named.
45
45
  *
46
- * Pass the turns as children or as `turns` data. Do not use both. Children
47
- * render from the server and suit most conversations. `turns` draws a long
46
+ * Pass the turns as children or as `turns` data. Do not use both: a chat given
47
+ * both draws every turn from `turns` and then the children, and says so once
48
+ * in the console. Children render from the server and suit most
49
+ * conversations. `turns` draws a long
48
50
  * transcript one window at a time, so the log needs a block size of its own.
49
51
  *
50
52
  * **A conversation is built from three elements.** The chat holds the
@@ -60,7 +62,10 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
60
62
  * it takes them away. Anything slotted into `status` is drawn first.
61
63
  *
62
64
  * **Keyboard.** The transcript moves between turns the way a feed does. Each
63
- * turn says its place, such as "3 of 40", windowed or not.
65
+ * turn says its place, such as "3 of 40", windowed or not. The transcript is
66
+ * a tab stop while it holds a turn, and the turn a reader moved to is one
67
+ * too. Moving to an older turn unpins a transcript that scrolls and scrolls
68
+ * the turn into view, and moving to the newest pins it again.
64
69
  *
65
70
  * | Key | What it does |
66
71
  * |---|---|
@@ -142,6 +147,11 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
142
147
  * @csspart status - The status region, outside the log.
143
148
  * @csspart turn - The wrapper around the message drawn for one turn from `turns`.
144
149
  * @csspart typing - The words that say who is typing, in the status band, while `typing` names someone.
150
+ *
151
+ * @cssstate empty - The conversation has no turns.
152
+ * @cssstate loading - Older turns are loading: `history` is `loading`.
153
+ * @cssstate pinned - The transcript follows its newest turn.
154
+ * @cssstate typing - `typing` names someone on the other side.
145
155
  */
146
156
  export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
147
157
  /**
@@ -362,13 +372,16 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
362
372
  /** The element that scrolls, once there is one. */
363
373
  private get log();
364
374
  /**
365
- * Makes the log a tab stop while it scrolls, and takes the stop away when it
366
- * does not.
375
+ * Makes the log a tab stop while it holds a turn or scrolls, and takes the
376
+ * stop away from an empty log.
367
377
  *
368
378
  * **A reader on a keyboard scrolls the history from the log itself.**
369
379
  * Chromium and Firefox make a box that scrolls focusable on their own, and
370
380
  * Safari does not, so there the arrow keys could not reach a long history.
371
- * A log that fits is no stop at all, so Tab goes straight to the composer.
381
+ *
382
+ * **And it is where the turns are entered.** Page Down from the log goes to
383
+ * the turn at the top of the view. A log that fits was no stop at all, so a
384
+ * conversation whose turns held no control had no way into them by keyboard.
372
385
  */
373
386
  private markScroller;
374
387
  /** Whether the reader is within `threshold` of the bottom right now. */
@@ -491,6 +504,10 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
491
504
  connectedCallback(): void;
492
505
  disconnectedCallback(): void;
493
506
  protected willUpdate(changed: PropertyValues): void;
507
+ /** Whether the chat has said it was given turns both ways. */
508
+ private warnedMixed;
509
+ /** Says once that turns arrived both as children and as `turns`. */
510
+ private warnMixed;
494
511
  /**
495
512
  * Keeps the window on the same turns when the set changes around them.
496
513
  *
@@ -522,6 +539,8 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
522
539
  * and never on a turn nobody moved to.
523
540
  */
524
541
  private focusTurn;
542
+ /** Draws turn `at` where it is windowed and not drawn, and visits it. */
543
+ private showTurn;
525
544
  /** The turn that is a tab stop because a reader moved to it. */
526
545
  private stop;
527
546
  /**
@@ -534,6 +553,17 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
534
553
  * `tabindex` you wrote on a turn is left as it is.
535
554
  */
536
555
  private visit;
556
+ /** Where this element last scrolled the transcript to show a turn, until the scroll that follows. */
557
+ private placed;
558
+ /**
559
+ * Scrolls the transcript, and nothing around it, until `turn` is in view,
560
+ * and notes the turn at the top as the reader's place. A pinned transcript
561
+ * is left at the bottom, which is where whoever holds the pin wants it.
562
+ *
563
+ * The edge nearest the turn moves the least, and a turn taller than the
564
+ * transcript is shown from its start.
565
+ */
566
+ private reveal;
537
567
  /** The first control in the regions `names` holds, in their order. */
538
568
  private firstControlIn;
539
569
  /**
@@ -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
  var HelixHeadlessChat_1;
8
- import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
8
+ import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, reportRuntime, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
9
9
  import { arrowDown } from '@fusion.dev/helix-icons';
10
10
  import { nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
@@ -97,6 +97,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
97
97
  this.handleResume = () => {
98
98
  this.resume();
99
99
  };
100
+ this.warnedMixed = false;
100
101
  this.handleKeydown = (event) => {
101
102
  if (event.defaultPrevented || event.altKey || event.metaKey || event.shiftKey) {
102
103
  return;
@@ -138,6 +139,13 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
138
139
  };
139
140
  this.handleScroll = () => {
140
141
  const log = this.log;
142
+ const placed = this.placed;
143
+ this.placed = undefined;
144
+ if (log && placed !== undefined && Math.abs(log.scrollTop - placed) < 1 && !this.pinned) {
145
+ this.remember();
146
+ this.reachHistory();
147
+ return;
148
+ }
141
149
  if (this.pinned && log && log.scrollTop >= this.stuckAt - 1 && !this.atBottom) {
142
150
  this.stick();
143
151
  }
@@ -183,7 +191,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
183
191
  if (!log) {
184
192
  return;
185
193
  }
186
- if (log.scrollHeight > log.clientHeight) {
194
+ const turns = this.fed ? this.turns.length : this.messages.length;
195
+ if (turns > 0 || log.scrollHeight > log.clientHeight) {
187
196
  log.tabIndex = 0;
188
197
  }
189
198
  else {
@@ -449,6 +458,22 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
449
458
  if (changed.has('turns')) {
450
459
  this.follow(changed.get('turns'));
451
460
  }
461
+ this.warnMixed();
462
+ this.toggleState('pinned', this.pinned);
463
+ this.toggleState('typing', this.typing !== '');
464
+ this.toggleState('loading', this.history === 'loading');
465
+ this.toggleState('empty', this.childrenKnown && this.turns.length === 0 && this.messages.length === 0);
466
+ }
467
+ warnMixed() {
468
+ if (this.warnedMixed || this.turns.length === 0 || this.messages.length === 0) {
469
+ return;
470
+ }
471
+ this.warnedMixed = true;
472
+ reportRuntime({
473
+ kind: 'mixed-turns',
474
+ message: `A chat was given ${this.messages.length} turns as children and ${this.turns.length} as \`turns\`, and draws the children after every turn from \`turns\`. Pass one or the other.`,
475
+ tag: this.localName,
476
+ });
452
477
  }
453
478
  follow(previous) {
454
479
  if (!previous?.length || this.turns.length === 0) {
@@ -526,9 +551,15 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
526
551
  const count = this.fed ? this.turns.length : this.messages.length;
527
552
  const log = this.log;
528
553
  const scrolls = log !== null && log !== undefined && log.scrollHeight > log.clientHeight;
529
- if (at < count - 1 && this.pinned && scrolls) {
530
- this.requestPinned(false);
554
+ if (at < count - 1 && this.pinned && scrolls && this.requestPinned(false)) {
555
+ await this.updateComplete;
556
+ }
557
+ await this.showTurn(at);
558
+ if (at === count - 1 && !this.pinned && this.atBottom) {
559
+ this.requestPinned(true);
531
560
  }
561
+ }
562
+ async showTurn(at) {
532
563
  if (this.fed) {
533
564
  const drawn = () => this.renderRoot
534
565
  ?.querySelector(`[part="turn"][data-index="${at}"]`)
@@ -559,7 +590,24 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
559
590
  this.stop = turn;
560
591
  }
561
592
  }
562
- turn.focus();
593
+ turn.focus({ preventScroll: true });
594
+ this.reveal(turn);
595
+ }
596
+ reveal(turn) {
597
+ const log = this.log;
598
+ if (!log || this.pinned) {
599
+ return;
600
+ }
601
+ const box = log.getBoundingClientRect();
602
+ const rect = turn.getBoundingClientRect();
603
+ const by = rect.top < box.top || rect.height > box.height
604
+ ? rect.top - box.top
605
+ : Math.max(0, rect.bottom - box.bottom);
606
+ if (Math.abs(by) >= 1) {
607
+ log.scrollTop += by;
608
+ this.placed = log.scrollTop;
609
+ }
610
+ this.remember();
563
611
  }
564
612
  firstControlIn(names) {
565
613
  for (const name of names) {
@@ -18,7 +18,11 @@ import '../../copy-button/index.js';
18
18
  * from the accessibility tree and left out of a copy. Set `mark-lines` to
19
19
  * mark lines a reader should look at, such as `2, 4-6`: the block draws a
20
20
  * band behind each run, placed where the lines are laid out, so it follows a
21
- * highlighter's markup and lines that wrap. Set `wrap` to wrap long lines
21
+ * highlighter's markup and lines that wrap. `added-lines` and
22
+ * `removed-lines` mark lines the same way as inserted and deleted, as
23
+ * Expressive Code's `ins` and `del` do, in code that is not a diff. The bands
24
+ * are drawn and hidden from screen readers, so say in the text around the
25
+ * block what a marked line means. Set `wrap` to wrap long lines
22
26
  * rather than scroll them; a gutter then numbers each line where it starts.
23
27
  * Set `wrappable` for a button in the header that turns `wrap` on and off,
24
28
  * pressed while the lines wrap.
@@ -93,6 +97,8 @@ import '../../copy-button/index.js';
93
97
  * @slot actions - Controls for the block, in the header before the copy button.
94
98
  * @slot copied-icon - The copy button's icon after a copy. Defaults to a check mark.
95
99
  * @slot copy-icon - The copy button's icon before a copy. Defaults to a clipboard.
100
+ * @slot expand-icon - An icon on the button that shows every line of a capped block, before its words.
101
+ * @slot label - The block's name in the header, such as a file icon beside the file name. Replaces `label` and `language` there, and draws the header.
96
102
  *
97
103
  * @event {Event} change - A file tab showed its file, whose name `file` now holds.
98
104
  * @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.
@@ -134,7 +140,9 @@ import '../../copy-button/index.js';
134
140
  * @csspart header - The bar that holds the name and the copy button, shown when there is a name.
135
141
  * @csspart hunk - In a diff, a `@@` line saying where the lines under it are.
136
142
  * @csspart label - The block's name: its `label`, or its `language` where it has no label.
137
- * @csspart mark - The band behind a run of lines `mark-lines` names. The block writes where the run is laid out into `--code-block-mark-start`, `--code-block-mark-size`, and `--code-block-mark-inline-size` on it. Hidden from the accessibility tree.
143
+ * @csspart mark - The band behind a run of lines `mark-lines`, `added-lines` or `removed-lines` names. The block writes where the run is laid out into `--code-block-mark-start`, `--code-block-mark-size`, and `--code-block-mark-inline-size` on it. Hidden from the accessibility tree.
144
+ * @csspart mark-added - The band behind a run of lines `added-lines` names. Applied with `mark`.
145
+ * @csspart mark-removed - The band behind a run of lines `removed-lines` names. Applied with `mark`.
138
146
  * @csspart marker - In a diff, the `+`, `-`, or space that starts a line.
139
147
  * @csspart new - In a split diff, the new version's `<pre>`.
140
148
  * @csspart number - In a diff, a line number. In a `wrap` block with `line-numbers`, one line's number in the gutter, which the block makes as tall as the line it numbers through `--code-block-line-size`.
@@ -158,6 +166,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
158
166
  * `registerWords('de', { 'helix-code-block': { copyLabel: 'Code kopieren' } })`.
159
167
  */
160
168
  static words: string;
169
+ /**
170
+ * The lines to mark as inserted, as numbers and ranges counted from 1, such
171
+ * as `3-4`. Drawn as `mark-lines` is, in the added color. Not for a diff.
172
+ */
173
+ addedLines: string;
161
174
  /**
162
175
  * The name of a block that scrolls, which is a tab stop while it does.
163
176
  *
@@ -312,6 +325,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
312
325
  * `false`.
313
326
  */
314
327
  noCopy: boolean;
328
+ /**
329
+ * The lines to mark as deleted, as numbers and ranges counted from 1. Drawn
330
+ * as `mark-lines` is, in the removed color. Not for a diff.
331
+ */
332
+ removedLines: string;
315
333
  /**
316
334
  * The words of a button that shows a folded run of unchanged lines, in a
317
335
  * diff with `context-lines`. `{count}` is how many.
@@ -27,6 +27,7 @@ const nameOf = (block) => {
27
27
  let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
28
28
  constructor() {
29
29
  super(...arguments);
30
+ this.addedLines = '';
30
31
  this.codeLabel = 'Code';
31
32
  this.codeNameLabel = '{code}, {name}';
32
33
  this.collapseLabel = 'Show fewer lines';
@@ -43,6 +44,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
43
44
  this.lineNumbers = false;
44
45
  this.markLines = '';
45
46
  this.noCopy = false;
47
+ this.removedLines = '';
46
48
  this.showUnchangedLabel = '{count, plural, one {Show # unchanged line} other {Show # unchanged lines}}';
47
49
  this.wrap = false;
48
50
  this.wrapLabel = 'Wrap lines';
@@ -223,7 +225,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
223
225
  }
224
226
  placeLines() {
225
227
  const code = this.renderRoot.querySelector('[part="code"]');
226
- const marks = [...this.renderRoot.querySelectorAll('[part="mark"]')];
228
+ const marks = [...this.renderRoot.querySelectorAll('[part~="mark"]')];
227
229
  const numbers = [
228
230
  ...this.renderRoot.querySelectorAll('[part="gutter"] > [part="number"]'),
229
231
  ];
@@ -356,8 +358,13 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
356
358
  catch (error) {
357
359
  globalThis.reportError?.(error);
358
360
  }
359
- const runs = this.childrenKnown ? lineRuns(this.markLines) : [];
360
- const marks = runs.map(([first, last]) => html `<span part="mark" aria-hidden="true" data-lines="${first}-${last}"></span>`);
361
+ const marks = this.childrenKnown
362
+ ? [
363
+ ...lineRuns(this.markLines).map(([first, last]) => html `<span part="mark" aria-hidden="true" data-lines="${first}-${last}"></span>`),
364
+ ...lineRuns(this.addedLines).map(([first, last]) => html `<span part="mark mark-added" aria-hidden="true" data-lines="${first}-${last}"></span>`),
365
+ ...lineRuns(this.removedLines).map(([first, last]) => html `<span part="mark mark-removed" aria-hidden="true" data-lines="${first}-${last}"></span>`),
366
+ ]
367
+ : [];
361
368
  const code = highlighted === undefined
362
369
  ? html `<code part="code">${marks}<slot @slotchange=${() => this.requestUpdate()}></slot></code>`
363
370
  : html `<code part="code">${marks}${unsafeHTML(highlighted)}</code><slot
@@ -376,7 +383,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
376
383
  : nothing;
377
384
  const name = this.label || this.language;
378
385
  const drawn = this.tabbed ? '' : name;
379
- return html `${this.renderHeader(name, undefined, drawn)}
386
+ return html `${this.renderHeader(name, undefined, drawn, true)}
380
387
  <div part="frame">
381
388
  <pre id="block" part="block" translate="no">${gutter}${code}</pre>
382
389
  ${this.headed(drawn) ? nothing : this.renderCopy()}
@@ -396,16 +403,24 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
396
403
  this.toggleState('expanded', this.expanded);
397
404
  }}
398
405
  >
399
- ${this.expanded ? this.collapseLabel : this.expandLabel}
406
+ ${this.slotted('expand-icon') ? html `<slot name="expand-icon"></slot>` : nothing}${this.expanded ? this.collapseLabel : this.expandLabel}
400
407
  </button>`;
401
408
  }
402
409
  headed(name) {
403
- return name !== '' || (this.wrappable && !this.diff) || this.slotted('actions');
410
+ return (name !== '' ||
411
+ (this.wrappable && !this.diff) ||
412
+ this.slotted('actions') ||
413
+ (!this.diff && !this.tabbed && this.slotted('label')));
404
414
  }
405
- renderHeader(name, value, drawn = name) {
415
+ renderHeader(name, value, drawn = name, own = false) {
406
416
  const acting = this.slotted('actions');
417
+ const label = own && !this.tabbed && this.slotted('label')
418
+ ? html `<span part="label"><slot name="label">${drawn}</slot></span>`
419
+ : drawn
420
+ ? html `<span part="label">${drawn}</span>`
421
+ : nothing;
407
422
  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>`
423
+ ? html `<div part="header">${label}${acting ? html `<span part="actions"><slot name="actions"></slot></span>` : nothing}${this.renderWrapToggle()}${this.renderCopy(value, name)}</div>`
409
424
  : nothing;
410
425
  }
411
426
  renderWrapToggle() {
@@ -501,6 +516,9 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
501
516
  })}<slot hidden @slotchange=${() => this.requestUpdate()}></slot>`;
502
517
  }
503
518
  };
519
+ __decorate([
520
+ property({ attribute: 'added-lines' })
521
+ ], HelixHeadlessCodeBlock.prototype, "addedLines", void 0);
504
522
  __decorate([
505
523
  words,
506
524
  property({ attribute: 'code-label' })
@@ -567,6 +585,9 @@ __decorate([
567
585
  __decorate([
568
586
  property({ attribute: 'no-copy', reflect: true, type: Boolean })
569
587
  ], HelixHeadlessCodeBlock.prototype, "noCopy", void 0);
588
+ __decorate([
589
+ property({ attribute: 'removed-lines' })
590
+ ], HelixHeadlessCodeBlock.prototype, "removedLines", void 0);
570
591
  __decorate([
571
592
  words,
572
593
  property({ attribute: 'show-unchanged-label' })
@@ -1,5 +1,5 @@
1
- import { type Color, type ColorFormat, type ColorWords, HelixFormElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
1
+ import { type Color, type ColorFormat, type ColorWords, HelixFormElement, KeyboardNavigationController, type PickerFailure } from '@fusion.dev/helix-core';
2
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../button/index.js';
4
4
  import '../../button-group/index.js';
5
5
  import '../../slider/index.js';
@@ -82,7 +82,7 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
82
82
  * @csspart eyedropper-live - The live region of the button inside `eyedropper`, which says its pending label.
83
83
  * @csspart eyedropper-spinner - The ring the eyedropper button turns while it is pending.
84
84
  * @csspart eyedropper-text - The words on the button inside `eyedropper`, trimmed to their capitals and baseline.
85
- * @csspart feedback - The message under the picker, while `feedback` is set.
85
+ * @csspart feedback - The message under the picker: `feedback`, or what is wrong with the answer once the picker is marked invalid.
86
86
  * @csspart field - One channel's number field with its label, in `channel-fields`.
87
87
  * @csspart field-input - The number input in a channel's field.
88
88
  * @csspart field-label - The name above a channel's number input.
@@ -321,10 +321,11 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
321
321
  * the anchor the browser points its bubble at.
322
322
  *
323
323
  * **Text that is not a color is bad input**, so a form does not submit
324
- * `#00` from a half-typed value. The picker draws no message of its own, so
325
- * reporting it as each keystroke arrives shows nothing until the form asks;
326
- * Color Input shows its own message against the field the reader typed
327
- * into.
324
+ * `#00` from a half-typed value. The picker marks it, and says
325
+ * `badInputMessage` under itself, once the reader leaves the value field or
326
+ * presses Enter, and a missing color once the reader has committed one or a
327
+ * form has refused the picker. Color Input shows its own message against the
328
+ * field the reader typed into.
328
329
  */
329
330
  required: boolean;
330
331
  /**
@@ -404,6 +405,48 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
404
405
  private get hinted();
405
406
  /** The ids a control in the picker is described by: the help, then the message. */
406
407
  private get describedBy();
408
+ /**
409
+ * Whether the reader is typing into the value field, between the first
410
+ * keystroke and the `change` that ends it.
411
+ *
412
+ * Half-typed text is rarely a color: `#00ff00` passes through `#0` and
413
+ * `#00ff0` on the way. So text that is not a color is refused once the
414
+ * reader leaves the field or presses Enter, and not on every keystroke.
415
+ */
416
+ private typing;
417
+ /**
418
+ * Text in the value field that is not a color, or undefined while the field
419
+ * holds the value.
420
+ *
421
+ * **Kept in the field and out of the value**, as a native number field keeps
422
+ * "12e": the text stays where the reader can correct it, the value stays the
423
+ * last color, and the picker reports `badInput` until the text is a color or
424
+ * the value moves. Measured before this: typed "nonsense" became the value,
425
+ * so a form read `brand=nonsense` and the square had no color to show.
426
+ */
427
+ private draft;
428
+ /**
429
+ * A required picker with no color is missing, and text that is not a color
430
+ * is refused, once the reader has finished typing it. See
431
+ * `MixinPickerFeedback.marked`.
432
+ *
433
+ * Measured before this: typed "nonsense" and a required picker its form
434
+ * refused both failed `checkValidity()` while `:state(invalid)` stayed off,
435
+ * the value field had no `aria-invalid`, and nothing was said.
436
+ */
437
+ protected get failure(): PickerFailure | undefined;
438
+ /**
439
+ * The line under the picker: `feedback`, or, where the picker is marked by
440
+ * its own validation, what is wrong.
441
+ */
442
+ private get said();
443
+ /**
444
+ * A reset ends any typing and takes back text that was not a color, so the
445
+ * field shows the color the reset put back.
446
+ */
447
+ formResetCallback(): void;
448
+ /** The line `said` holds, or nothing while it is empty. */
449
+ protected renderPickerFeedback(): TemplateResult | typeof nothing;
407
450
  /** Whether a pointer is dragging the square's handle, said as `:state(dragging)`. */
408
451
  private dragging;
409
452
  /** Whether the eyedropper is open, said as `:state(sampling)`. */
@@ -56,6 +56,8 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
56
56
  this.handleNamingSlot = () => {
57
57
  this.requestUpdate();
58
58
  };
59
+ this.typing = false;
60
+ this.draft = undefined;
59
61
  this.dragging = false;
60
62
  this.picking = false;
61
63
  this.owned = false;
@@ -68,7 +70,14 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
68
70
  return;
69
71
  }
70
72
  const typed = event.target.value;
73
+ this.typing = true;
71
74
  const parsed = parseColor(typed);
75
+ if (!parsed && typed.trim() !== '') {
76
+ this.draft = typed;
77
+ this.sync();
78
+ return;
79
+ }
80
+ this.draft = undefined;
72
81
  if (parsed) {
73
82
  this.moved = parsed;
74
83
  this.owned = typed !== this.value;
@@ -261,9 +270,47 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
261
270
  return this.hint !== '' || this.slottedIn('hint').length > 0;
262
271
  }
263
272
  get describedBy() {
264
- const ids = [this.hinted ? 'hint' : '', this.feedback === '' ? '' : 'feedback'].filter(Boolean);
273
+ const ids = [this.hinted ? 'hint' : '', this.said === '' ? '' : 'feedback'].filter(Boolean);
265
274
  return ids.length > 0 ? ids.join(' ') : nothing;
266
275
  }
276
+ get failure() {
277
+ if (this.draft !== undefined) {
278
+ return this.typing ? undefined : 'refused';
279
+ }
280
+ if (this.value === '') {
281
+ return this.required ? 'missing' : undefined;
282
+ }
283
+ return parseColor(this.value) === null ? 'refused' : undefined;
284
+ }
285
+ get said() {
286
+ if (this.feedback !== '') {
287
+ return this.feedback;
288
+ }
289
+ if (!this.marked || this.invalid) {
290
+ return '';
291
+ }
292
+ const failure = this.failure;
293
+ return failure === 'refused'
294
+ ? this.badInputMessage
295
+ : failure === 'missing'
296
+ ? this.valueMissingMessage
297
+ : '';
298
+ }
299
+ formResetCallback() {
300
+ super.formResetCallback();
301
+ this.typing = false;
302
+ if (this.draft !== undefined) {
303
+ this.draft = undefined;
304
+ const field = this.partNamed('value');
305
+ if (field) {
306
+ field.value = this.value;
307
+ }
308
+ }
309
+ }
310
+ renderPickerFeedback() {
311
+ const said = this.said;
312
+ return said === '' ? nothing : html `<p part="feedback" id="feedback">${said}</p>`;
313
+ }
267
314
  settle() {
268
315
  if (this.unsettled) {
269
316
  this.unsettled = false;
@@ -299,6 +346,10 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
299
346
  this.reportRequired();
300
347
  }
301
348
  reportRequired() {
349
+ if (this.draft !== undefined) {
350
+ this.internals.setValidity({ badInput: true }, this.badInputMessage, this.partNamed('value') ?? this.partNamed('grid') ?? undefined);
351
+ return;
352
+ }
302
353
  if (this.required && this.value === '') {
303
354
  this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage, this.partNamed('grid') ?? undefined);
304
355
  return;
@@ -424,6 +475,9 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
424
475
  this.move({ [channel]: Number(control.value) });
425
476
  }
426
477
  willUpdate(changed) {
478
+ if (changed.has('value')) {
479
+ this.draft = undefined;
480
+ }
427
481
  super.willUpdate(changed);
428
482
  const outside = [...(this.internals.labels ?? [])];
429
483
  const named = this.named;
@@ -630,6 +684,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
630
684
  @input=${this.handleValueInput}
631
685
  @change=${(event) => {
632
686
  event.stopPropagation();
687
+ this.typing = false;
633
688
  this.settle();
634
689
  }}
635
690
  />`}
@@ -770,6 +825,12 @@ __decorate([
770
825
  __decorate([
771
826
  state()
772
827
  ], HelixHeadlessColorPicker.prototype, "swatchAt", void 0);
828
+ __decorate([
829
+ state()
830
+ ], HelixHeadlessColorPicker.prototype, "typing", void 0);
831
+ __decorate([
832
+ state()
833
+ ], HelixHeadlessColorPicker.prototype, "draft", void 0);
773
834
  __decorate([
774
835
  state()
775
836
  ], HelixHeadlessColorPicker.prototype, "dragging", void 0);
@@ -240,12 +240,13 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
240
240
  /**
241
241
  * Whether more than one option can be chosen.
242
242
  *
243
- * The list already does the work: `value` comes apart on commas, a press
244
- * toggles rather than replaces, and the arrows move without choosing. What
245
- * this adds is the tags, and keeping the panel open between picks.
243
+ * The list already does the work: `value` is the chosen values joined with
244
+ * commas, a press toggles rather than replaces, and the arrows move without
245
+ * choosing. What this adds is the tags, and keeping the panel open between
246
+ * picks.
246
247
  *
247
- * A comma is therefore not available inside an option's value, which is the
248
- * listbox's contract and not this element's to change.
248
+ * An option's value may hold a comma, and it is read as one value, as
249
+ * `Select` and `Listbox` read it. So may an answer the reader created.
249
250
  *
250
251
  * **The list is told, and the field is not.** `aria-multiselectable` belongs
251
252
  * to `listbox`, `grid`, `tree` and `tablist`, and `combobox` dropped it in
@@ -385,8 +386,17 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
385
386
  */
386
387
  protected readonly upgradeWatch: UpgradeController;
387
388
  protected readonly panel: PopoverController;
388
- /** The values chosen, in the order they were chosen. */
389
+ /**
390
+ * The values chosen, in the order they were chosen, read against the
391
+ * options' values and the answers created here, so a value that holds a
392
+ * comma is one value. See `chosenValues`.
393
+ *
394
+ * Measured before this: choosing `Paris, France` drew two tags, "Paris" and
395
+ * "France", and the form received two entries.
396
+ */
389
397
  private get values();
398
+ /** The answers created here, which no option holds, kept so their commas read as theirs. */
399
+ private readonly created;
390
400
  /**
391
401
  * The chosen options themselves, for their words.
392
402
  *