@fusion.dev/helix-headless 0.162.1 → 0.164.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 (45) hide show
  1. package/custom-elements.json +1041 -47
  2. package/dist/callout/root/callout-root.d.ts +43 -4
  3. package/dist/callout/root/callout-root.js +58 -2
  4. package/dist/canvas/index.d.ts +1 -1
  5. package/dist/canvas/root/canvas-document.d.ts +33 -0
  6. package/dist/canvas/root/canvas-root.d.ts +50 -2
  7. package/dist/canvas/root/canvas-root.js +98 -3
  8. package/dist/chat/composer/chat-composer.js +1 -1
  9. package/dist/chat/root/chat-root.d.ts +15 -4
  10. package/dist/chat/root/chat-root.js +19 -8
  11. package/dist/code-block/patch.d.ts +42 -0
  12. package/dist/code-block/patch.js +118 -0
  13. package/dist/code-block/root/code-block-root.d.ts +37 -1
  14. package/dist/code-block/root/code-block-root.js +57 -9
  15. package/dist/color-picker/root/color-picker-root.d.ts +7 -3
  16. package/dist/color-picker/root/color-picker-root.js +7 -4
  17. package/dist/combobox/root/combobox-root.d.ts +1 -0
  18. package/dist/combobox/root/combobox-root.js +10 -1
  19. package/dist/data-table/root/data-table-root.d.ts +1 -0
  20. package/dist/data-table/root/data-table-root.js +1 -0
  21. package/dist/element-events.generated.js +7 -1
  22. package/dist/map/root/map-root.d.ts +8 -1
  23. package/dist/map/root/map-root.js +12 -0
  24. package/dist/menu/item/menu-item.d.ts +38 -7
  25. package/dist/menu/item/menu-item.js +20 -9
  26. package/dist/menu/root/menu-root.d.ts +10 -1
  27. package/dist/menu/root/menu-root.js +8 -3
  28. package/dist/object-inspector/root/object-inspector-root.d.ts +23 -0
  29. package/dist/object-inspector/root/object-inspector-root.js +19 -1
  30. package/dist/steps/root/steps-root.d.ts +7 -1
  31. package/dist/steps/root/steps-root.js +6 -1
  32. package/dist/table/grid-controller.d.ts +7 -0
  33. package/dist/table/grid-controller.js +15 -1
  34. package/dist/table/root/table-root.d.ts +10 -0
  35. package/dist/table/root/table-root.js +18 -1
  36. package/dist/video/root/video-root.js +2 -1
  37. package/dist/video-playlist/item/video-playlist-item.d.ts +8 -0
  38. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  39. package/dist/video-playlist/root/video-playlist-root.d.ts +60 -0
  40. package/dist/video-playlist/root/video-playlist-root.js +55 -1
  41. package/lint.json +1 -1
  42. package/package.json +5 -5
  43. package/types/jsx.d.ts +502 -40
  44. package/types/vue.d.ts +126 -14
  45. package/types/words.d.ts +58 -0
@@ -15,8 +15,12 @@ declare const HelixHeadlessCallout_base: (new (...args: any[]) => import("@fusio
15
15
  * region around it.
16
16
  *
17
17
  * Set `dismissible` for a note a reader may put away, as Shoelace's `closable`
18
- * alert does. The button fires `helix-dismiss` and the callout stays until a
19
- * listener removes it, so the page decides whether it comes back.
18
+ * alert does. The button fires `helix-dismiss`, and unless a listener cancels
19
+ * it the callout hides itself: `open` turns false, `:state(exiting)` holds
20
+ * while its exit animations run, and then it is `hidden`. Remove it in your
21
+ * listener to take it out of the page, or set `open` to show it again, which
22
+ * holds `:state(entering)` while it comes back. Focus on its dismiss button
23
+ * moves to the next callout's, or to the tab stop beside it, once it hides.
20
24
  *
21
25
  * A callout sets a passage apart inside a document and is read where it sits.
22
26
  * For page-level chrome above the masthead that is a live region and can be
@@ -33,13 +37,19 @@ declare const HelixHeadlessCallout_base: (new (...args: any[]) => import("@fusio
33
37
  * @slot heading - A heading above the content.
34
38
  * @slot icon - Replaces the icon chosen by `tone`.
35
39
  *
36
- * @event {HelixDismissEvent} helix-dismiss - The dismiss button was pressed. The callout does not remove itself, so remove it in response. Cancelable.
40
+ * @event {HelixDismissEvent} helix-dismiss - The dismiss button was pressed. Unless a listener cancels it, the callout hides itself, and a listener may remove it instead. Cancelable: cancel it to keep the callout showing.
37
41
  *
38
42
  * @csspart content - The content region.
39
43
  * @csspart dismiss - The button that dismisses the callout, shown when `dismissible` is set.
40
44
  * @csspart dismiss-mark - The cross on the dismiss button, shown when the `dismiss-icon` slot is empty.
41
45
  * @csspart icon - The icon region, an image named for the tone.
42
46
  * @csspart icon-mark - The icon for the tone, shown when no icon is slotted.
47
+ *
48
+ * @cssstate closed - While `open` is false: the callout was dismissed, and is leaving or hidden.
49
+ * @cssstate entering - While a callout shown again by `open` runs its entrance animations.
50
+ * @cssstate exiting - While a dismissed callout runs its exit animations, before it is hidden.
51
+ * @cssstate has-heading - Something is slotted into `heading`.
52
+ * @cssstate has-icon - An icon of your own is slotted into `icon`, in place of the tone's.
43
53
  */
44
54
  export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
45
55
  /**
@@ -47,7 +57,6 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
47
57
  * `registerWords('de', { 'helix-callout': { toneLabels: { danger: 'Fehler' } } })`.
48
58
  */
49
59
  static words: string;
50
- protected willUpdate(changed: PropertyValues): void;
51
60
  /**
52
61
  * The dismiss button's name, read by screen readers, with `{label}` for
53
62
  * what the callout is called: its heading, or the tone's name where it has
@@ -58,6 +67,23 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
58
67
  * read as written.
59
68
  */
60
69
  dismissLabel: string;
70
+ /** Which journey is the latest, so an entrance settling late leaves an exit alone. */
71
+ private turn;
72
+ /** Hidden from a reader once hidden, for the focus hand-off. */
73
+ protected get present(): boolean;
74
+ /**
75
+ * Hides the callout once a dismissal stands, unless the page removed it in
76
+ * its listener, which is the page's answer and leaves `open` as it was.
77
+ */
78
+ dismiss(): boolean;
79
+ protected willUpdate(changed: PropertyValues): void;
80
+ /**
81
+ * Whether the callout is showing. A dismissal nobody canceled turns it
82
+ * false and the callout hides itself, and setting it true shows the callout
83
+ * again. Not reflected: open is the resting state, so an `open` attribute
84
+ * would appear on markup a server sent without it.
85
+ */
86
+ open: boolean;
61
87
  /**
62
88
  * What kind of attention the passage needs.
63
89
  *
@@ -116,6 +142,19 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
116
142
  */
117
143
  private dismissName;
118
144
  protected firstUpdated(changed: PropertyValues): void;
145
+ protected updated(changed: PropertyValues): void;
146
+ /**
147
+ * The way out after a dismissal, or back in after `open` is set again.
148
+ *
149
+ * **Hidden only once the exit has run**, since a hidden element draws no
150
+ * animation: `:state(exiting)` holds until every animation on the callout
151
+ * has finished, or two frames where it has none, and then `hidden` takes it
152
+ * out of the page and out of the accessibility tree. Shown again at once,
153
+ * with `:state(entering)` while it arrives.
154
+ */
155
+ private journey;
156
+ /** Says which regions hold something, one call apiece so each pairs with its `@cssstate`. */
157
+ private readonly readRegions;
119
158
  protected render(): TemplateResult;
120
159
  }
121
160
  declare global {
@@ -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, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
7
+ import { animationsSettled, customElement, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -12,6 +12,8 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
12
12
  constructor() {
13
13
  super(...arguments);
14
14
  this.dismissLabel = 'Dismiss {label}';
15
+ this.turn = 0;
16
+ this.open = true;
15
17
  this.toneLabel = '';
16
18
  this.toneLabels = {
17
19
  danger: 'Error',
@@ -20,14 +22,34 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
20
22
  success: 'Success',
21
23
  warning: 'Warning',
22
24
  };
25
+ this.readRegions = () => {
26
+ const filled = (name) => {
27
+ const slot = this.renderRoot?.querySelector(`slot[name="${name}"]`);
28
+ return (slot?.assignedNodes({ flatten: true }) ?? []).some((node) => node.parentNode !== slot &&
29
+ (node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim() !== ''));
30
+ };
31
+ this.toggleState('has-heading', filled('heading'));
32
+ this.toggleState('has-icon', filled('icon'));
33
+ };
23
34
  }
24
35
  static { this.words = 'helix-callout'; }
36
+ get present() {
37
+ return this.isConnected && !this.hasAttribute('hidden');
38
+ }
39
+ dismiss() {
40
+ const stands = super.dismiss();
41
+ if (stands && this.isConnected) {
42
+ this.open = false;
43
+ }
44
+ return stands;
45
+ }
25
46
  willUpdate(changed) {
26
47
  super.willUpdate(changed);
27
48
  hostAria(this.internals, { role: 'note' });
49
+ this.toggleState('closed', !this.open);
28
50
  }
29
51
  renderIcon(icon, _label) {
30
- return html `<slot name="icon" aria-hidden="true">
52
+ return html `<slot name="icon" aria-hidden="true" @slotchange=${this.readRegions}>
31
53
  <svg part="icon-mark" fill="currentColor" viewBox=${icon.viewBox} aria-hidden="true">
32
54
  ${unsafeSVG(icon.content)}
33
55
  </svg>
@@ -48,6 +70,37 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
48
70
  super.firstUpdated(changed);
49
71
  this.finishHydration();
50
72
  }
73
+ updated(changed) {
74
+ super.updated(changed);
75
+ this.readRegions();
76
+ if (changed.has('open') && changed.get('open') !== undefined) {
77
+ this.journey();
78
+ }
79
+ else if (changed.has('open') && !this.open) {
80
+ this.setAttribute('hidden', '');
81
+ }
82
+ }
83
+ journey() {
84
+ this.turn += 1;
85
+ const turn = this.turn;
86
+ const open = this.open;
87
+ this.toggleState('entering', open);
88
+ this.toggleState('exiting', !open);
89
+ if (open) {
90
+ this.removeAttribute('hidden');
91
+ }
92
+ void animationsSettled(this).then(() => {
93
+ if (turn !== this.turn) {
94
+ return;
95
+ }
96
+ this.toggleState('entering', false);
97
+ this.toggleState('exiting', false);
98
+ if (!open) {
99
+ this.setAttribute('hidden', '');
100
+ this.handFocusOn();
101
+ }
102
+ });
103
+ }
51
104
  render() {
52
105
  const tone = this.tone === 'neutral' ? 'neutral' : toneMark(this.tone) ? (this.tone ?? 'info') : 'info';
53
106
  const { icon } = toneMark(tone) ?? TONE_MARKS.info;
@@ -69,6 +122,9 @@ __decorate([
69
122
  words,
70
123
  property({ attribute: 'dismiss-label' })
71
124
  ], HelixHeadlessCallout.prototype, "dismissLabel", void 0);
125
+ __decorate([
126
+ property({ type: Boolean })
127
+ ], HelixHeadlessCallout.prototype, "open", void 0);
72
128
  __decorate([
73
129
  property({ reflect: true })
74
130
  ], HelixHeadlessCallout.prototype, "tone", void 0);
@@ -1,4 +1,4 @@
1
1
  export { HelixHeadlessCanvasEdge } from './edge/canvas-edge.js';
2
2
  export { HelixHeadlessCanvasNode } from './node/canvas-node.js';
3
- export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasSide, CanvasTone, } from './root/canvas-root.js';
3
+ export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasRoute, CanvasRouteEnds, CanvasSide, CanvasTone, } from './root/canvas-root.js';
4
4
  export { fromJsonCanvas, HelixHeadlessCanvas } from './root/canvas-root.js';
@@ -91,6 +91,39 @@ export declare const port: (node: CanvasNode, side: CanvasSide) => {
91
91
  };
92
92
  /** How a connector travels between the two cards it joins. */
93
93
  export type CanvasPath = 'curve' | 'straight' | 'step';
94
+ /** What a board's `edgeRoute` is given for one connection, in the board's own units. */
95
+ export interface CanvasRouteEnds {
96
+ /** The connection. */
97
+ edge: CanvasEdge;
98
+ /** Where it arrives, on the side of the card it arrives at. */
99
+ end: {
100
+ x: number;
101
+ y: number;
102
+ };
103
+ /** The side of the card it leaves. */
104
+ fromSide: CanvasSide;
105
+ /** The route the board would draw: the connection's `path`, or the board's. */
106
+ path: CanvasPath;
107
+ /** Where it leaves, on the side of the card it leaves. */
108
+ start: {
109
+ x: number;
110
+ y: number;
111
+ };
112
+ /** The side of the card it arrives at. */
113
+ toSide: CanvasSide;
114
+ }
115
+ /**
116
+ * Draws a connection's line: its SVG path from `start` to `end`, and where its
117
+ * label sits, the middle of the two ends where it says nothing. Nothing back
118
+ * draws the board's own.
119
+ */
120
+ export type CanvasRoute = (ends: CanvasRouteEnds) => {
121
+ d: string;
122
+ mid?: {
123
+ x: number;
124
+ y: number;
125
+ } | undefined;
126
+ } | undefined;
94
127
  /** A point in the document's own units. */
95
128
  type Point = {
96
129
  x: number;
@@ -1,7 +1,7 @@
1
1
  import { type CanvasView, HelixElement, type ViewportPanDetail, type ViewportWay, type ViewportZoomDetail } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- import { type CanvasDocument, type CanvasTone } from './canvas-document.js';
4
- export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasSide, CanvasTone, } from './canvas-document.js';
3
+ import { type CanvasDocument, type CanvasRoute, type CanvasTone } from './canvas-document.js';
4
+ export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasRoute, CanvasRouteEnds, CanvasSide, CanvasTone, } from './canvas-document.js';
5
5
  export { fromJsonCanvas } from './canvas-document.js';
6
6
  declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion.dev/helix-core").ViewportElement) & typeof HelixElement;
7
7
  /**
@@ -92,6 +92,17 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
92
92
  * connection is a new element, and React discards an element the board adds to
93
93
  * its markup on the next render, so you write the connection.
94
94
  *
95
+ * **`edgeRoute` draws the connections' lines your own way**: a function from
96
+ * where a line leaves and arrives, and the sides it does, to its SVG path, for
97
+ * a route such as one that bends around the cards between. What it returns
98
+ * is the line drawn, the line pressed and focused, and the one an arrowhead
99
+ * sits at the end of. Nothing back draws the board's own.
100
+ *
101
+ * Add `minimap` to `controls` for a picture of the whole board in a corner:
102
+ * each card as a box, and a frame around what is on screen. A press or a drag
103
+ * on it brings that place to the middle of the view. It is for a pointer, and
104
+ * a screen reader skips it: the arrow keys and `show()` reach every card.
105
+ *
95
106
  * Cards and connections are `helix-canvas-node` and `helix-canvas-edge`
96
107
  * elements, so they are in the served markup whichever renderer wrote them. The
97
108
  * library fetches nothing, and an array passed to an element is missing from
@@ -148,6 +159,9 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
148
159
  * @csspart label-editor - The field for editing a connection's label or a group's name, placed over that text.
149
160
  * @csspart live - A live region that screen readers announce while a connection is being made.
150
161
  * @csspart marquee - The box a Shift-drag across the empty board draws, which selects the cards it touches.
162
+ * @csspart minimap - The picture of the whole board, under `controls="minimap"`.
163
+ * @csspart minimap-node - One card in the minimap.
164
+ * @csspart minimap-view - The frame in the minimap around what is on screen.
151
165
  * @csspart pan-down - The button that moves the board down.
152
166
  * @csspart pan-end - The button that moves the board toward the end of a line of text: right in English.
153
167
  * @csspart pan-start - The button that moves the board toward the start of a line of text: left in English.
@@ -210,6 +224,15 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
210
224
  controlsLabel: string;
211
225
  /** What screen readers hear after a copy, with `{count}` for the cards copied. */
212
226
  copiedLabel: string;
227
+ /**
228
+ * Draws every connection's line: a function from where it leaves and
229
+ * arrives, and the sides it does, to its SVG path and where its label sits.
230
+ *
231
+ * Absent, or returning nothing for a connection, draws the board's own
232
+ * route. A function that throws draws the board's own too, and the error is
233
+ * reported to the page as an uncaught one. Set it as a property.
234
+ */
235
+ edgeRoute?: CanvasRoute;
213
236
  /** The board's name where `label` is empty. */
214
237
  fallbackLabel: string;
215
238
  /**
@@ -702,6 +725,8 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
702
725
  * both its cards are on the board; nothing where either is not.
703
726
  */
704
727
  private geometryOf;
728
+ /** One connection's line, by `edgeRoute` where it draws one, and by the board's own where not. */
729
+ private draw;
705
730
  /** The card an event came from, wherever inside it the event began. */
706
731
  private cardOf;
707
732
  /**
@@ -1244,6 +1269,29 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
1244
1269
  * the card a keyboard reader is on.
1245
1270
  */
1246
1271
  private renderDraft;
1272
+ /** The part of the document on screen, in its own units. */
1273
+ private get onScreen();
1274
+ /** What the minimap shows: every card and what is on screen, with room around them. */
1275
+ private get minimapArea();
1276
+ /**
1277
+ * A press or a drag on the minimap, which brings the place under it to the
1278
+ * middle of the view.
1279
+ *
1280
+ * **The point is read through the picture's own scale**: the area is drawn
1281
+ * whole and centered in the box, so the box's spare width or height is split
1282
+ * either side of it.
1283
+ */
1284
+ private readonly handleMinimapPointer;
1285
+ /**
1286
+ * The minimap, under `controls="minimap"`: each card as a box, and a frame
1287
+ * around what is on screen.
1288
+ *
1289
+ * **A picture for a pointer**, hidden from screen readers and taking no
1290
+ * focus, as a map's own overview is: the arrow keys and `show()` already take
1291
+ * a keyboard to any card, and a second way that says nothing a reader can
1292
+ * use would be a stop that does nothing.
1293
+ */
1294
+ private renderMinimap;
1247
1295
  protected render(): TemplateResult;
1248
1296
  }
1249
1297
  declare global {
@@ -733,6 +733,35 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
733
733
  this.handleSlotChange = () => {
734
734
  this.requestUpdate();
735
735
  };
736
+ this.handleMinimapPointer = (event) => {
737
+ if (event.type === 'pointermove' && (event.buttons & 1) === 0) {
738
+ return;
739
+ }
740
+ const picture = event.currentTarget;
741
+ if (event.type === 'pointerdown') {
742
+ if (event.button !== 0) {
743
+ return;
744
+ }
745
+ capturePointer(picture, event.pointerId);
746
+ }
747
+ event.preventDefault();
748
+ const area = this.minimapArea;
749
+ const shown = picture.getBoundingClientRect();
750
+ const scale = Math.min(shown.width / area.width, shown.height / area.height) || 1;
751
+ const point = {
752
+ x: area.x + (event.clientX - shown.left - (shown.width - area.width * scale) / 2) / scale,
753
+ y: area.y + (event.clientY - shown.top - (shown.height - area.height * scale) / 2) / scale,
754
+ };
755
+ const board = extent(this.nodes);
756
+ const { zoom } = this.at;
757
+ this.direct = event.type === 'pointermove';
758
+ this.touched = true;
759
+ this.at = {
760
+ ...this.at,
761
+ x: this.box.width / 2 - (point.x - board.x + MARGIN) * zoom,
762
+ y: this.box.height / 2 - (point.y - board.y + MARGIN) * zoom,
763
+ };
764
+ };
736
765
  }
737
766
  static { this.words = 'helix-canvas'; }
738
767
  static { this.shadowRootOptions = {
@@ -1120,9 +1149,25 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1120
1149
  if (!from || !to) {
1121
1150
  return undefined;
1122
1151
  }
1123
- const drawn = connector(edge, onPlane(from, origin), onPlane(to, origin), edge.path ?? this.path ?? 'curve', heads(edge));
1152
+ const drawn = this.draw(edge, onPlane(from, origin), onPlane(to, origin), edge.path ?? this.path ?? 'curve', heads(edge));
1124
1153
  return { ...drawn, from, to };
1125
1154
  }
1155
+ draw(edge, from, to, path, drawnHeads) {
1156
+ if (this.edgeRoute) {
1157
+ const { end, fromSide, start, toSide } = ends(edge, from, to);
1158
+ let own;
1159
+ try {
1160
+ own = this.edgeRoute({ edge, end, fromSide, path, start, toSide });
1161
+ }
1162
+ catch (error) {
1163
+ globalThis.reportError?.(error);
1164
+ }
1165
+ if (own?.d) {
1166
+ return { d: own.d, mid: own.mid ?? { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 } };
1167
+ }
1168
+ }
1169
+ return connector(edge, from, to, path, drawnHeads);
1170
+ }
1126
1171
  cardOf(event) {
1127
1172
  return event
1128
1173
  .composedPath()
@@ -2134,7 +2179,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2134
2179
  if (target) {
2135
2180
  const reversed = linking.moving === 'from';
2136
2181
  const [from, to] = reversed ? [target, anchor] : [anchor, target];
2137
- const drawn = connector({
2182
+ const drawn = this.draw({
2138
2183
  fromNode: from.id,
2139
2184
  fromSide: reversed ? linking.toSide : linking.side,
2140
2185
  id: '',
@@ -2157,6 +2202,53 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2157
2202
  }
2158
2203
  return nothing;
2159
2204
  }
2205
+ get onScreen() {
2206
+ const board = extent(this.nodes);
2207
+ const { height, width } = this.box;
2208
+ const { x, y, zoom } = this.at;
2209
+ return {
2210
+ height: height / zoom,
2211
+ width: width / zoom,
2212
+ x: board.x - MARGIN - x / zoom,
2213
+ y: board.y - MARGIN - y / zoom,
2214
+ };
2215
+ }
2216
+ get minimapArea() {
2217
+ const board = extent(this.nodes);
2218
+ const view = this.onScreen;
2219
+ const x = Math.min(board.x, view.x);
2220
+ const y = Math.min(board.y, view.y);
2221
+ const width = Math.max(board.x + board.width, view.x + view.width) - x;
2222
+ const height = Math.max(board.y + board.height, view.y + view.height) - y;
2223
+ const room = Math.max(width, height) * 0.05;
2224
+ return {
2225
+ height: height + room * 2,
2226
+ width: width + room * 2,
2227
+ x: x - room,
2228
+ y: y - room,
2229
+ };
2230
+ }
2231
+ renderMinimap() {
2232
+ const nodes = this.nodes;
2233
+ if (!this.drawsControls('minimap') || nodes.length === 0) {
2234
+ return nothing;
2235
+ }
2236
+ const area = this.minimapArea;
2237
+ const view = this.onScreen;
2238
+ return html `<svg
2239
+ part="minimap"
2240
+ aria-hidden="true"
2241
+ height="120"
2242
+ preserveAspectRatio="xMidYMid meet"
2243
+ viewBox=${`${area.x} ${area.y} ${area.width} ${area.height}`}
2244
+ width="160"
2245
+ @pointerdown=${this.handleMinimapPointer}
2246
+ @pointermove=${this.handleMinimapPointer}
2247
+ >
2248
+ ${nodes.map((node) => svg `<rect part="minimap-node" data-group=${node.type === 'group' ? '' : nothing} x=${node.x} y=${node.y} width=${node.width} height=${node.height} />`)}
2249
+ ${svg `<rect part="minimap-view" x=${view.x} y=${view.y} width=${view.width} height=${view.height} />`}
2250
+ </svg>`;
2251
+ }
2160
2252
  render() {
2161
2253
  const nodes = this.nodes;
2162
2254
  const drawn = extent(nodes);
@@ -2219,7 +2311,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2219
2311
  <slot name="empty"></slot>
2220
2312
  ${this.renderGestures()}
2221
2313
  </div>
2222
- ${this.renderControls()}
2314
+ ${this.renderControls()} ${this.renderMinimap()}
2223
2315
  <span part="live" role="status">${this.said}</span>
2224
2316
  <div part="footer">
2225
2317
  <slot name="footer"></slot>
@@ -2255,6 +2347,9 @@ __decorate([
2255
2347
  words,
2256
2348
  property({ attribute: 'copied-label' })
2257
2349
  ], HelixHeadlessCanvas.prototype, "copiedLabel", void 0);
2350
+ __decorate([
2351
+ property({ attribute: false })
2352
+ ], HelixHeadlessCanvas.prototype, "edgeRoute", void 0);
2258
2353
  __decorate([
2259
2354
  words,
2260
2355
  property({ attribute: 'fallback-label' })
@@ -113,7 +113,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
113
113
  }
114
114
  const invalid = this.touched && !this.internals.validity.valid;
115
115
  this.toggleState('invalid', invalid);
116
- this.internals.ariaInvalid = invalid ? 'true' : null;
116
+ this.internals.ariaInvalid = invalid ? 'true' : 'false';
117
117
  if (invalid !== this.shownInvalid) {
118
118
  this.shownInvalid = invalid;
119
119
  }
@@ -517,12 +517,23 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
517
517
  * Moves focus to turn `at`, drawing it first where the transcript is
518
518
  * windowed and it is not drawn.
519
519
  *
520
- * **A turn takes focus only once a reader moves to it**, so the
521
- * `tabindex="-1"` a turn written as a child needs is written then, long
522
- * after any server's markup has hydrated, and never on a turn nobody moved
523
- * to.
520
+ * **A turn takes focus only once a reader moves to it**, so the `tabindex`
521
+ * it needs is written then, long after any server's markup has hydrated,
522
+ * and never on a turn nobody moved to.
524
523
  */
525
524
  private focusTurn;
525
+ /** The turn that is a tab stop because a reader moved to it. */
526
+ private stop;
527
+ /**
528
+ * Focuses a turn, making it the transcript's one stop a reader moved to.
529
+ *
530
+ * **`0`, and never `-1`.** Chromium leaves the whole of a shadow host at a
531
+ * negative `tabindex` out of the Tab order, its own controls and what is
532
+ * slotted into it alike, so a turn at `-1` lost its links and its Try again
533
+ * once a reader had paged through it. The stop moves with the reader, and a
534
+ * `tabindex` you wrote on a turn is left as it is.
535
+ */
536
+ private visit;
526
537
  /** The first control in the regions `names` holds, in their order. */
527
538
  private firstControlIn;
528
539
  /**
@@ -524,7 +524,9 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
524
524
  }
525
525
  async focusTurn(at) {
526
526
  const count = this.fed ? this.turns.length : this.messages.length;
527
- if (at < count - 1 && this.pinned) {
527
+ const log = this.log;
528
+ const scrolls = log !== null && log !== undefined && log.scrollHeight > log.clientHeight;
529
+ if (at < count - 1 && this.pinned && scrolls) {
528
530
  this.requestPinned(false);
529
531
  }
530
532
  if (this.fed) {
@@ -537,17 +539,27 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
537
539
  this.requestUpdate();
538
540
  await this.updateComplete;
539
541
  }
540
- drawn()?.focus();
542
+ const turn = drawn();
543
+ if (turn) {
544
+ this.visit(turn);
545
+ }
541
546
  return;
542
547
  }
543
548
  const message = this.messages[at];
544
- if (!message) {
545
- return;
549
+ if (message) {
550
+ this.visit(message);
546
551
  }
547
- if (!message.hasAttribute('tabindex')) {
548
- message.setAttribute('tabindex', '-1');
552
+ }
553
+ visit(turn) {
554
+ if (this.stop !== turn) {
555
+ this.stop?.removeAttribute('tabindex');
556
+ this.stop = undefined;
557
+ if (!turn.hasAttribute('tabindex')) {
558
+ turn.setAttribute('tabindex', '0');
559
+ this.stop = turn;
560
+ }
549
561
  }
550
- message.focus();
562
+ turn.focus();
551
563
  }
552
564
  firstControlIn(names) {
553
565
  for (const name of names) {
@@ -582,7 +594,6 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
582
594
  .turnWords=${this.words}
583
595
  .turnPosition=${first + offset + 1}
584
596
  .turnCount=${this.turns.length}
585
- tabindex="-1"
586
597
  .failedLabel=${this.failedLabel}
587
598
  state=${turn.state ?? nothing}
588
599
  >${turn.text}</${message}>
@@ -8,11 +8,21 @@
8
8
  * depends on, out of a component library. What is left is punctuation: a header
9
9
  * per file, a `@@` per hunk, and a first character per line.
10
10
  *
11
+ * **Within a changed line it does compare.** A removed line and the added line
12
+ * paired with it are compared word by word, which takes two short strings and
13
+ * no dependency, so a reader sees which words changed without comparing two
14
+ * whole lines by eye.
15
+ *
11
16
  * **Line numbers come from the hunk headers**, so they are the file's numbers
12
17
  * rather than the diff's, which is the only kind a reader can look up. A line
13
18
  * that is not in a hunk is not in the output: file headers, `index` lines and
14
19
  * `diff --git` are metadata, and a name is taken from them and nothing else.
15
20
  */
21
+ /** A run of a changed line's text, and whether the line it is paired with lacks it. */
22
+ export interface DiffPiece {
23
+ changed: boolean;
24
+ text: string;
25
+ }
16
26
  /** One line of a hunk, as the unified format marks it. */
17
27
  export interface DiffLine {
18
28
  kind: 'added' | 'context' | 'removed';
@@ -21,6 +31,11 @@ export interface DiffLine {
21
31
  /** The number on the new side, absent for a line that only the old side has. */
22
32
  new?: number;
23
33
  text: string;
34
+ /**
35
+ * The line's text in runs, where it replaced a line or was replaced by one
36
+ * and the two share some words. Absent for every other line.
37
+ */
38
+ pieces?: DiffPiece[];
24
39
  }
25
40
  /** A `@@` header and the lines under it. */
26
41
  export interface DiffHunk {
@@ -51,6 +66,16 @@ export interface DiffRow {
51
66
  * before it rather than a line of its own, so it is dropped.
52
67
  */
53
68
  export declare const parsePatch: (patch: string) => DiffFile[];
69
+ /**
70
+ * The runs of two paired lines that the other line does not have, by the
71
+ * longest run of words they share in order.
72
+ *
73
+ * **Space between two changes is part of the change**, so "a b" replaced by
74
+ * "c d" is one run, with no gap for the eye to bridge. Two lines
75
+ * that share no word share nothing worth marking: the whole line is the
76
+ * change, which its own wash already says, and `undefined` draws it whole.
77
+ */
78
+ export declare const changedWords: (before: string, after: string) => [DiffPiece[], DiffPiece[]] | undefined;
54
79
  /**
55
80
  * A hunk's lines paired for a side-by-side view.
56
81
  *
@@ -60,3 +85,20 @@ export declare const parsePatch: (patch: string) => DiffFile[];
60
85
  * across rather than hunting for it further down.
61
86
  */
62
87
  export declare const pairLines: (lines: DiffLine[]) => DiffRow[];
88
+ /** A stretch of a hunk's lines: drawn, or unchanged lines folded behind a control. */
89
+ export interface DiffRun {
90
+ /** Whether the lines are unchanged ones folded away. */
91
+ folded: boolean;
92
+ lines: DiffLine[];
93
+ /** Where the run starts among the hunk's lines, which names a fold. */
94
+ start: number;
95
+ }
96
+ /**
97
+ * A hunk's lines with each long run of unchanged lines folded, keeping `keep`
98
+ * lines on the side of each change, as a code host draws a diff with more
99
+ * context than a reader needs.
100
+ *
101
+ * A fold hides two lines or more: a control in place of one line is no
102
+ * shorter than the line.
103
+ */
104
+ export declare const foldContext: (lines: DiffLine[], keep: number) => DiffRun[];