@fusion.dev/helix-headless 0.160.0 → 0.161.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 +1729 -50
  2. package/dist/canvas/root/canvas-root.d.ts +59 -2
  3. package/dist/canvas/root/canvas-root.js +123 -8
  4. package/dist/chat/message/chat-message.d.ts +8 -0
  5. package/dist/chat/message/chat-message.js +11 -0
  6. package/dist/chat/root/chat-root.d.ts +47 -1
  7. package/dist/chat/root/chat-root.js +130 -4
  8. package/dist/code-block/lines.d.ts +12 -0
  9. package/dist/code-block/lines.js +41 -0
  10. package/dist/code-block/root/code-block-root.d.ts +88 -5
  11. package/dist/code-block/root/code-block-root.js +154 -10
  12. package/dist/element-events.generated.js +4 -0
  13. package/dist/splitter/root/splitter-root.d.ts +15 -0
  14. package/dist/splitter/root/splitter-root.js +16 -2
  15. package/dist/table/grid-controller.d.ts +7 -1
  16. package/dist/table/grid-controller.js +9 -1
  17. package/dist/table/root/table-root.d.ts +122 -0
  18. package/dist/table/root/table-root.js +293 -17
  19. package/dist/table/row/table-row.d.ts +41 -0
  20. package/dist/table/row/table-row.js +56 -2
  21. package/dist/timeline/item/timeline-item.d.ts +47 -10
  22. package/dist/timeline/item/timeline-item.js +36 -4
  23. package/dist/video-playlist/item/video-playlist-item.d.ts +11 -0
  24. package/dist/video-playlist/item/video-playlist-item.js +5 -0
  25. package/dist/video-playlist/menu/video-playlist-menu.d.ts +6 -0
  26. package/dist/video-playlist/menu/video-playlist-menu.js +11 -0
  27. package/dist/video-playlist/root/video-playlist-root.d.ts +59 -5
  28. package/dist/video-playlist/root/video-playlist-root.js +82 -10
  29. package/dist/virtual-list/fill.d.ts +6 -1
  30. package/dist/virtual-list/fill.js +1 -1
  31. package/dist/virtual-list/keys.d.ts +8 -0
  32. package/dist/virtual-list/keys.js +20 -0
  33. package/dist/virtual-list/root/virtual-list-root.d.ts +98 -13
  34. package/dist/virtual-list/root/virtual-list-root.js +195 -20
  35. package/lint.json +1 -1
  36. package/package.json +5 -5
  37. package/types/jsx.d.ts +936 -68
  38. package/types/vue.d.ts +226 -12
  39. package/types/words.d.ts +79 -0
@@ -5,7 +5,8 @@ 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, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
8
+ import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, requestChange, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
9
+ import { arrowDown } from '@fusion.dev/helix-icons';
9
10
  import { nothing } from 'lit';
10
11
  import { property, state } from 'lit/decorators.js';
11
12
  import { keyed } from 'lit/directives/keyed.js';
@@ -51,6 +52,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
51
52
  this.label = '';
52
53
  this.meLabel = 'You';
53
54
  this.messageLabel = '{author}, {time}';
55
+ this.resumeLabel = 'Jump to latest';
54
56
  this.threshold = 48;
55
57
  this.turnLabel = 'Message';
56
58
  this.turns = [];
@@ -94,6 +96,38 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
94
96
  this.handleResume = () => {
95
97
  this.resume();
96
98
  };
99
+ this.handleKeydown = (event) => {
100
+ if (event.defaultPrevented || event.altKey || event.metaKey || event.shiftKey) {
101
+ return;
102
+ }
103
+ const step = event.key === 'PageDown' ? 1 : event.key === 'PageUp' ? -1 : 0;
104
+ const leave = event.ctrlKey && (event.key === 'End' || event.key === 'Home');
105
+ if ((step === 0 || event.ctrlKey) && !leave) {
106
+ return;
107
+ }
108
+ const at = this.turnFrom(event.composedPath());
109
+ if (at === undefined) {
110
+ return;
111
+ }
112
+ if (leave) {
113
+ const target = event.key === 'End'
114
+ ? this.firstControlIn(['composer'])
115
+ : (this.firstControlIn(['header', 'actions']) ??
116
+ (this.log?.hasAttribute('tabindex') ? this.log : undefined));
117
+ if (target) {
118
+ event.preventDefault();
119
+ target.focus();
120
+ }
121
+ return;
122
+ }
123
+ const count = this.fed ? this.turns.length : this.messages.length;
124
+ if (count === 0) {
125
+ return;
126
+ }
127
+ event.preventDefault();
128
+ const next = at < 0 ? this.topTurn() : Math.max(0, Math.min(count - 1, at + step));
129
+ void this.focusTurn(next);
130
+ };
97
131
  this.handleSlotChange = () => {
98
132
  this.assign();
99
133
  this.requestUpdate();
@@ -378,6 +412,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
378
412
  }
379
413
  connectedCallback() {
380
414
  super.connectedCallback();
415
+ this.addEventListener('keydown', this.handleKeydown);
381
416
  this.settling = new MutationObserver(this.handleSettle);
382
417
  this.settling.observe(this, { attributeFilter: ['state'], subtree: true });
383
418
  this.opening = true;
@@ -390,6 +425,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
390
425
  }
391
426
  disconnectedCallback() {
392
427
  super.disconnectedCallback();
428
+ this.removeEventListener('keydown', this.handleKeydown);
393
429
  this.settling?.disconnect();
394
430
  this.settling = undefined;
395
431
  this.grower?.disconnect();
@@ -446,11 +482,87 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
446
482
  this.announce();
447
483
  }
448
484
  tellTurns() {
449
- for (const message of this.messages) {
485
+ const messages = this.messages;
486
+ messages.forEach((message, at) => {
450
487
  whenHydrated(message, () => {
451
488
  message.turnWords = this.words;
489
+ message.turnPosition = at + 1;
490
+ message.turnCount = messages.length;
452
491
  });
492
+ });
493
+ }
494
+ turnFrom(path) {
495
+ for (const target of path) {
496
+ if (!(target instanceof HTMLElement)) {
497
+ continue;
498
+ }
499
+ if (this.fed && target.getAttribute('part') === 'turn') {
500
+ return Number(target.dataset.index);
501
+ }
502
+ if (!this.fed && 'isChatMessage' in target) {
503
+ const at = this.messages.indexOf(target);
504
+ if (at >= 0) {
505
+ return at;
506
+ }
507
+ }
508
+ if (target === this.log) {
509
+ return -1;
510
+ }
511
+ }
512
+ return undefined;
513
+ }
514
+ topTurn() {
515
+ const id = this.anchor?.id;
516
+ if (id === undefined) {
517
+ return 0;
453
518
  }
519
+ const at = this.fed
520
+ ? this.turns.findIndex((turn) => this.identity(turn) === id)
521
+ : this.messages.indexOf(id);
522
+ return Math.max(0, at);
523
+ }
524
+ async focusTurn(at) {
525
+ const count = this.fed ? this.turns.length : this.messages.length;
526
+ if (at < count - 1 && this.pinned) {
527
+ this.requestPinned(false);
528
+ }
529
+ if (this.fed) {
530
+ const drawn = () => this.renderRoot
531
+ ?.querySelector(`[part="turn"][data-index="${at}"]`)
532
+ ?.querySelector(':scope > *');
533
+ if (!drawn()) {
534
+ this.virtualize.reveal(at);
535
+ this.virtualize.measure();
536
+ this.requestUpdate();
537
+ await this.updateComplete;
538
+ }
539
+ drawn()?.focus();
540
+ return;
541
+ }
542
+ const message = this.messages[at];
543
+ if (!message) {
544
+ return;
545
+ }
546
+ if (!message.hasAttribute('tabindex')) {
547
+ message.setAttribute('tabindex', '-1');
548
+ }
549
+ message.focus();
550
+ }
551
+ firstControlIn(names) {
552
+ for (const name of names) {
553
+ for (const child of this.given(name)) {
554
+ if (child instanceof HTMLElement) {
555
+ const found = child.localName.includes('-') ||
556
+ child.matches('button, a[href], input, textarea, select, [tabindex]')
557
+ ? child
558
+ : focusableElements(child)[0];
559
+ if (found) {
560
+ return found;
561
+ }
562
+ }
563
+ }
564
+ }
565
+ return undefined;
454
566
  }
455
567
  renderTurns() {
456
568
  const { after, before, first, last } = this.virtualize.window;
@@ -467,12 +579,22 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
467
579
  at=${turn.at ?? nothing}
468
580
  style=${ownEntry(this.authorStyles, turn.author) ?? nothing}
469
581
  .turnWords=${this.words}
582
+ .turnPosition=${first + offset + 1}
583
+ .turnCount=${this.turns.length}
584
+ tabindex="-1"
470
585
  .failedLabel=${this.failedLabel}
471
586
  state=${turn.state ?? nothing}
472
587
  >${turn.text}</${message}>
473
588
  </div>`)}
474
589
  <div part="after" role="presentation" style="block-size: ${after}px; flex: none"></div>`;
475
590
  }
591
+ renderResume() {
592
+ return html `<slot name="resume"
593
+ ><button part="resume-button" type="button">
594
+ <span part="resume-mark" aria-hidden="true">${drawIcon(arrowDown)}</span>${this.resumeLabel}
595
+ </button></slot
596
+ >`;
597
+ }
476
598
  render() {
477
599
  const away = !this.pinned;
478
600
  const fed = this.fed;
@@ -499,8 +621,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
499
621
  ${this.renderStatus(fed || this.messages.length > 0 || !this.childrenKnown)}
500
622
  </div>
501
623
  <span part="live" role="status">${keyed(this.said, this.announcement)}</span>
502
- <div part="resume" ?hidden=${!away || !this.has('resume')} @click=${this.handleResume}>
503
- <slot name="resume"></slot>
624
+ <div part="resume" ?hidden=${!away} @click=${this.handleResume}>
625
+ ${this.renderResume()}
504
626
  </div>
505
627
  <div part="status" role="status" ?data-empty=${!this.has('status') && this.typing === ''}>
506
628
  <slot name="status"></slot>${this.typing === ''
@@ -558,6 +680,10 @@ __decorate([
558
680
  __decorate([
559
681
  property({ type: Boolean })
560
682
  ], HelixHeadlessChat.prototype, "pinned", null);
683
+ __decorate([
684
+ words,
685
+ property({ attribute: 'resume-label' })
686
+ ], HelixHeadlessChat.prototype, "resumeLabel", void 0);
561
687
  __decorate([
562
688
  property({ type: Number })
563
689
  ], HelixHeadlessChat.prototype, "threshold", void 0);
@@ -11,3 +11,15 @@ export declare const lineCount: (code: string) => number;
11
11
  * `<pre>` draws beside the code. The stylesheet aligns them to the end.
12
12
  */
13
13
  export declare const gutterNumbers: (count: number) => string;
14
+ /**
15
+ * The runs of lines a list of numbers and ranges names, such as `2, 4-6`, as
16
+ * `[first, last]` pairs counted from 1, in order and merged where they touch.
17
+ * Anything that is not a whole number or a range of them is passed over, and
18
+ * a range written backward is read forward.
19
+ */
20
+ export declare const lineRuns: (list: string) => Array<[number, number]>;
21
+ /**
22
+ * Where each line of `text` starts and ends, as character offsets, the line
23
+ * break left out. A final line break ends the last line, as `lineCount` reads it.
24
+ */
25
+ export declare const lineSpans: (text: string) => Array<[number, number]>;
@@ -1,2 +1,43 @@
1
1
  export const lineCount = (code) => code === '' ? 0 : code.replace(/\r?\n$/, '').split(/\r?\n/).length;
2
2
  export const gutterNumbers = (count) => Array.from({ length: count }, (_, index) => String(index + 1)).join('\n');
3
+ export const lineRuns = (list) => {
4
+ const runs = [];
5
+ for (const piece of list.split(/[\s,]+/)) {
6
+ const range = /^(\d+)(?:-(\d+))?$/.exec(piece);
7
+ if (!range) {
8
+ continue;
9
+ }
10
+ const a = Number(range[1]);
11
+ const b = Number(range[2] ?? range[1]);
12
+ if (a < 1 || b < 1) {
13
+ continue;
14
+ }
15
+ runs.push([Math.min(a, b), Math.max(a, b)]);
16
+ }
17
+ runs.sort((x, y) => x[0] - y[0]);
18
+ const merged = [];
19
+ for (const run of runs) {
20
+ const last = merged.at(-1);
21
+ if (last && run[0] <= last[1] + 1) {
22
+ last[1] = Math.max(last[1], run[1]);
23
+ }
24
+ else {
25
+ merged.push([run[0], run[1]]);
26
+ }
27
+ }
28
+ return merged;
29
+ };
30
+ export const lineSpans = (text) => {
31
+ const spans = [];
32
+ const body = text.replace(/\r?\n$/, '');
33
+ if (body === '' && text === '') {
34
+ return spans;
35
+ }
36
+ let start = 0;
37
+ for (const match of body.matchAll(/\r?\n/g)) {
38
+ spans.push([start, match.index]);
39
+ start = match.index + match[0].length;
40
+ }
41
+ spans.push([start, body.length]);
42
+ return spans;
43
+ };
@@ -15,7 +15,18 @@ import '../../copy-button/index.js';
15
15
  * For a fragment of code inside a sentence, use `Code`.
16
16
  *
17
17
  * Set `line-numbers` to number the lines in a gutter beside the code, hidden
18
- * from the accessibility tree and left out of a copy. Set `highlight` to a
18
+ * from the accessibility tree and left out of a copy. Set `mark-lines` to
19
+ * mark lines a reader should look at, such as `2, 4-6`: the block draws a
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
22
+ * rather than scroll them; a gutter then numbers each line where it starts.
23
+ * Set `wrappable` for a button in the header that turns `wrap` on and off,
24
+ * pressed while the lines wrap.
25
+ *
26
+ * The header holds the name, the `actions` slot, and the copy button. Slot a
27
+ * control into `actions`, such as a button that sets `wrap` or one that opens
28
+ * the code in an editor, and the block draws a header for it even with no
29
+ * name. `copy-icon` and `copied-icon` replace the copy button's two icons. Set `highlight` to a
19
30
  * function that turns the code into highlighted markup, such as one wrapping
20
31
  * Shiki or Prism, and the block draws what it returns; it is handed the
21
32
  * block's `language`, so one function serves every block. The code stays in
@@ -62,7 +73,14 @@ import '../../copy-button/index.js';
62
73
  * @documentation https://fusion.dev/helix/components/code-block
63
74
  *
64
75
  * @slot - The code.
76
+ * @slot actions - Controls for the block, in the header before the copy button.
77
+ * @slot copied-icon - The copy button's icon after a copy. Defaults to a check mark.
78
+ * @slot copy-icon - The copy button's icon before a copy. Defaults to a clipboard.
79
+ *
80
+ * @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.
81
+ * @event {HelixToggleEvent} helix-toggle - Fired after the wrap button changed `wrap`, with the new value in `detail.checked`.
65
82
  *
83
+ * @csspart actions - The box in the header that holds the `actions` slot, drawn while something is slotted there.
66
84
  * @csspart added - In a diff, a line only the new version has, an `<ins>`.
67
85
  * @csspart block - The scrolling `<pre>`.
68
86
  * @csspart code - The `<code>` inside it.
@@ -91,13 +109,20 @@ import '../../copy-button/index.js';
91
109
  * @csspart header - The bar that holds the name and the copy button, shown when there is a name.
92
110
  * @csspart hunk - In a diff, a `@@` line saying where the lines under it are.
93
111
  * @csspart label - The block's name: its `label`, or its `language` where it has no label.
112
+ * @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.
94
113
  * @csspart marker - In a diff, the `+`, `-`, or space that starts a line.
95
114
  * @csspart new - In a split diff, the new version's `<pre>`.
96
- * @csspart number - In a diff, a line number.
115
+ * @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`.
97
116
  * @csspart old - In a split diff, the old version's `<pre>`.
98
117
  * @csspart removed - In a diff, a line only the old version has, a `<del>`.
99
118
  * @csspart split - In a split diff, the two versions side by side.
100
119
  * @csspart text - In a diff, a line's code.
120
+ * @csspart wrap-toggle - The button in the header that wraps the lines and unwraps them, drawn when `wrappable` is set.
121
+ *
122
+ * @cssproperty --code-block-line-size - Written by the block on each `number` in a `wrap` block with `line-numbers`: how tall the line it numbers is laid out. Do not set it.
123
+ * @cssproperty --code-block-mark-inline-size - Written by the block on each `mark`: how wide the code is, scrolled part included. Do not set it.
124
+ * @cssproperty --code-block-mark-size - Written by the block on each `mark`: how tall its run of lines is. Do not set it.
125
+ * @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.
101
126
  *
102
127
  * @cssstate expanded - The expand button has shown every line of a capped block.
103
128
  */
@@ -210,6 +235,12 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
210
235
  * whether or not this is set.
211
236
  */
212
237
  lineNumbers: boolean;
238
+ /**
239
+ * The lines to mark, as numbers and ranges counted from 1, such as `2, 4-6`.
240
+ * Each run is drawn as a band behind its lines. Not for a diff, which marks
241
+ * its own changes.
242
+ */
243
+ markLines: string;
213
244
  /**
214
245
  * Whether to leave the copy button off.
215
246
  *
@@ -240,6 +271,26 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
240
271
  * than defaulted, so the host carries no attribute the consumer did not write.
241
272
  */
242
273
  variant?: 'split';
274
+ /**
275
+ * Whether long lines wrap. Without it they scroll.
276
+ *
277
+ * Off by default, so long lines scroll, because a line break can be
278
+ * significant in code and a wrapped line reads as two. For prose-like code,
279
+ * such as a long command or a log line, a wrap reads better than a scroll.
280
+ * With `line-numbers`, each number stands beside the line it numbers
281
+ * however many rows that line takes. Reflected, because the stylesheet
282
+ * wraps the lines.
283
+ */
284
+ wrap: boolean;
285
+ /** The wrap button's name, which it keeps whether it is pressed or not. */
286
+ wrapLabel: string;
287
+ /**
288
+ * Whether the header has a button that wraps the lines and unwraps them. The
289
+ * block draws a header for it even with no name.
290
+ */
291
+ wrappable: boolean;
292
+ /** Asks to wrap or unwrap the lines, and says so once it has. */
293
+ private toggleWrap;
243
294
  /** The last text read as a diff, and what it read as, so a render does not parse twice. */
244
295
  private parsed;
245
296
  /** The files in the content, when it is a diff and it can be read yet. */
@@ -294,6 +345,20 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
294
345
  * and a stop on it would scroll nothing.
295
346
  */
296
347
  private markScrollers;
348
+ /**
349
+ * Where each line of the code is laid out in the `<code>`, from the top of
350
+ * its content, as the browser placed the text: one row, or several where it
351
+ * wraps.
352
+ */
353
+ private lineBoxes;
354
+ /**
355
+ * Places each mark behind its run of lines, and makes each number in a
356
+ * wrapping gutter as tall as its line, from where the lines are laid out.
357
+ *
358
+ * **Written onto the elements as custom properties**, which are facts the
359
+ * block computed and CSS cannot, and no render follows from them.
360
+ */
361
+ private placeLines;
297
362
  protected updated(changed: PropertyValues): void;
298
363
  connectedCallback(): void;
299
364
  disconnectedCallback(): void;
@@ -305,6 +370,8 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
305
370
  * measure, so a served block draws none and the client adds it.
306
371
  */
307
372
  private renderExpand;
373
+ /** Whether the block draws a header: it has a name, a wrap button, or a control in `actions`. */
374
+ private headed;
308
375
  /**
309
376
  * The name with the copy button beside it, or nothing when there is no name.
310
377
  *
@@ -313,11 +380,19 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
313
380
  * diff cannot: its washes run to the edge, so the button stood on the hunk
314
381
  * line and the line under it. In the header it is over nothing.
315
382
  *
316
- * **No name draws no header**, rather than a bar to hold one button, which
317
- * turned a `Usage` section of one-line imports into a run of empty strips.
318
- * The button then stands beside the code, in room the block keeps for it.
383
+ * **No name draws no header**, unless the page put a control in `actions`
384
+ * or asked for the wrap button. A bar holding the copy button alone turned a
385
+ * `Usage` section of one-line imports into a run of empty strips. The button
386
+ * then stands beside the code, in room the block keeps for it.
319
387
  */
320
388
  private renderHeader;
389
+ /** The button that wraps the lines and unwraps them, where `wrappable` asks for one. Not for a diff. */
390
+ private renderWrapToggle;
391
+ /**
392
+ * Whether the page slotted something into `name`. Not known on a server or
393
+ * while hydrating what one sent, which render as though nothing were.
394
+ */
395
+ private slotted;
321
396
  private renderCopy;
322
397
  /**
323
398
  * The button that takes the code, behind a seam the styled tier can move.
@@ -333,6 +408,14 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
333
408
  * @param name The name of what it copies, which its label carries.
334
409
  */
335
410
  protected renderCopyButton(value?: string, name?: string): TemplateResult;
411
+ /**
412
+ * The page's own icons for the copy button, passed on through slots of the
413
+ * block's, where the page slotted them. A slot passed on with nothing in it
414
+ * would stand in for the button's own icon and draw nothing, so it is drawn
415
+ * only for an icon the page gave. In a diff, the first file's button takes
416
+ * them, a slotted element going to one slot.
417
+ */
418
+ private renderCopyIcons;
336
419
  /** A number padded to the widest in its file, so the columns line up as text. */
337
420
  private renderNumber;
338
421
  private renderLine;