@fusion.dev/helix-headless 0.110.6 → 0.111.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11230,6 +11230,16 @@
11230
11230
  "description": "The text announced when a connection is stopped before it is requested.",
11231
11231
  "attribute": "stopped-label"
11232
11232
  },
11233
+ {
11234
+ "kind": "field",
11235
+ "name": "toneLabels",
11236
+ "type": {
11237
+ "text": "Partial<Record<CanvasTone, string>>"
11238
+ },
11239
+ "default": "{}",
11240
+ "description": "What each tone means on this board, in words read out with the cards and\nconnections in it, such as `{\"danger\": \"Blocked\", \"success\": \"Done\"}`.\n\n**Named by the page, since the tone names a color and the page gives it\nits meaning.** JSON Canvas leaves its six colors to the application, so\nred is \"blocked\" on one board and \"urgent\" on the next. A reader who can\nsee the board is told by the color; this is how a reader who cannot is\ntold the same. A tone left out is not read.",
11241
+ "attribute": "tone-labels"
11242
+ },
11233
11243
  {
11234
11244
  "kind": "field",
11235
11245
  "name": "stop",
@@ -11688,6 +11698,23 @@
11688
11698
  "privacy": "private",
11689
11699
  "description": "A card being resized from a corner, with the box it started from."
11690
11700
  },
11701
+ {
11702
+ "kind": "field",
11703
+ "name": "fingers",
11704
+ "privacy": "private",
11705
+ "readonly": true,
11706
+ "default": "new Map<number, { x: number; y: number }>()",
11707
+ "description": "Where each finger on the board is, by pointer, which a pinch is measured from."
11708
+ },
11709
+ {
11710
+ "kind": "field",
11711
+ "name": "pinching",
11712
+ "type": {
11713
+ "text": "| {\n at: { x: number; y: number; zoom: number }\n middle: { x: number; y: number }\n spread: number\n }\n | undefined"
11714
+ },
11715
+ "privacy": "private",
11716
+ "description": "A pinch under way: how far apart its fingers were and the point between\nthem, in the box, and the view, when the second finger landed."
11717
+ },
11691
11718
  {
11692
11719
  "kind": "field",
11693
11720
  "name": "dragging",
@@ -12696,6 +12723,39 @@
12696
12723
  ],
12697
12724
  "description": "The box a Shift-drag is drawing, on the plane, while it is."
12698
12725
  },
12726
+ {
12727
+ "kind": "method",
12728
+ "name": "fingerSpan",
12729
+ "privacy": "private",
12730
+ "return": {
12731
+ "type": {
12732
+ "text": "{ middle: { x: number; y: number }; spread: number }"
12733
+ }
12734
+ },
12735
+ "description": "The fingers' spread and the point between them, in the box. Taken from\nthe first two fingers down, where a third has landed as well."
12736
+ },
12737
+ {
12738
+ "kind": "method",
12739
+ "name": "startPinch",
12740
+ "privacy": "private",
12741
+ "return": {
12742
+ "type": {
12743
+ "text": "void"
12744
+ }
12745
+ },
12746
+ "description": "Turns what the first finger was doing into a pinch, where that was a pan\nor a press that had not yet moved anything. A finger already carrying a\ncard, drawing a line, resizing or drawing a box goes on doing that."
12747
+ },
12748
+ {
12749
+ "kind": "method",
12750
+ "name": "pinch",
12751
+ "privacy": "private",
12752
+ "return": {
12753
+ "type": {
12754
+ "text": "void"
12755
+ }
12756
+ },
12757
+ "description": "Scales the board by how far the fingers have spread since the pinch\nbegan, about the point between them, and carries it with that point, so\nwhat was under the fingers stays under them."
12758
+ },
12699
12759
  {
12700
12760
  "kind": "method",
12701
12761
  "name": "zoomAbout",
@@ -13965,6 +14025,15 @@
13965
14025
  "description": "The text announced when a connection is stopped before it is requested.",
13966
14026
  "fieldName": "stoppedLabel"
13967
14027
  },
14028
+ {
14029
+ "name": "tone-labels",
14030
+ "type": {
14031
+ "text": "Partial<Record<CanvasTone, string>>"
14032
+ },
14033
+ "default": "{}",
14034
+ "description": "What each tone means on this board, in words read out with the cards and\nconnections in it, such as `{\"danger\": \"Blocked\", \"success\": \"Done\"}`.\n\n**Named by the page, since the tone names a color and the page gives it\nits meaning.** JSON Canvas leaves its six colors to the application, so\nred is \"blocked\" on one board and \"urgent\" on the next. A reader who can\nsee the board is told by the color; this is how a reader who cannot is\ntold the same. A tone left out is not read.",
14035
+ "fieldName": "toneLabels"
14036
+ },
13968
14037
  {
13969
14038
  "name": "controls",
13970
14039
  "type": {
@@ -20034,6 +20103,14 @@
20034
20103
  "readonly": true,
20035
20104
  "default": "new AriaForwardController(this, { target: () => this.partNamed('control'), })"
20036
20105
  },
20106
+ {
20107
+ "kind": "field",
20108
+ "name": "clicks",
20109
+ "privacy": "protected",
20110
+ "readonly": true,
20111
+ "default": "new HostClickController(this, () => this.partNamed('control'))",
20112
+ "description": "A click on the element itself, `click()` or a page's `<label for>`, toggles it."
20113
+ },
20037
20114
  {
20038
20115
  "kind": "field",
20039
20116
  "name": "indeterminate",
@@ -80097,7 +80174,7 @@
80097
80174
  "text": "string"
80098
80175
  },
80099
80176
  "default": "''",
80100
- "description": "A shorter name for this choice, for where the row does not fit.\n\n`<option label>`'s own meaning: the text a chooser shows when it has less\nroom than the row does, which is what a select's closed trigger has. Empty\nmeans the row's own content, which is the common case and is why this is not\nsimply the accessible name.",
80177
+ "description": "A shorter name for this choice, for where the row does not fit.\n\n`<option label>`'s own meaning: the text a chooser shows when it has less\nroom than the row does, which is what a select's closed trigger has. Empty\nmeans the row's own content, which is the common case and is why this is not\nsimply the accessible name.\n\n**An option with a label and nothing in it shows the label**, as\n`<option label>` does: written the platform's way, a select listed a row of\nnothing for every choice.",
80101
80178
  "attribute": "label"
80102
80179
  },
80103
80180
  {
@@ -80126,7 +80203,7 @@
80126
80203
  "type": {
80127
80204
  "text": "string"
80128
80205
  },
80129
- "description": "The option's text, which a listbox matches typed text against.\n\nRead from the rendered content rather than required as a property, because\nthe content is where a consumer naturally puts it and asking for both is\nasking for two things to disagree.\n\n**The default slot's content, never `this.textContent`.** An option holds\nthree slots: the words, plus `start` for an icon and `end` for a shortcut\nor a count. Neither of the latter two is what a reader searched for, and\n`textContent` runs both onto the end of it. Measured on a font picker whose\noptions carry a two-letter specimen in `end`: the field read\n`Noto Sans Linear BAg`, and typing `ag` matched 1,892 of 1,893 options,\nevery row containing the sample. An option with a keyboard shortcut in its\nend slot has the same fault and a worse one, every row carrying one matching\nevery letter of it.\n\nSix things read this: the collapsed field of Select and of Combobox, the\nlistbox's filter, Combobox's match against typed text, its inline\ncompletion, and the label on a chosen tag.\n\nA node goes to the default slot when it names no other, which is the rule\nthe platform applies at assignment. Asking the same question here rather\nthan a second one is what stops the two disagreeing. Text nodes are\nalways the default slot's and cannot say otherwise.\n\n**Text and elements, named rather than \"not an element\".** The filter read\n`!(node instanceof Element) || node.slot === ''`, which admits a comment,\nand `textContent` on a comment is the comment's own body. Every option Lit\nrenders carries one: a child binding is marked with `<!--?lit$1234$-->`, so\nan option written as `` html`<${option}>${size}</${option}>` `` read back as\n`?lit$090648746$25`. Measured in `pagination/footer`, where the trigger\npainted exactly that instead of `25`, on both twins.\n\nIt is worse than one wrong label, because of the six readers listed above:\nthe filter and Combobox's match compare against this string, so a marker in\nit is in every comparison too.",
80206
+ "description": "The option's text, which a listbox matches typed text against.\n\nRead from the rendered content rather than required as a property, because\nthe content is where a consumer naturally puts it and asking for both is\nasking for two things to disagree.\n\n**The default slot's content, never `this.textContent`.** An option holds\nthree slots: the words, plus `start` for an icon and `end` for a shortcut\nor a count. Neither of the latter two is what a reader searched for, and\n`textContent` runs both onto the end of it. Measured on a font picker whose\noptions carry a two-letter specimen in `end`: the field read\n`Noto Sans Linear BAg`, and typing `ag` matched 1,892 of 1,893 options,\nevery row containing the sample. An option with a keyboard shortcut in its\nend slot has the same fault and a worse one, every row carrying one matching\nevery letter of it.\n\nSix things read this: the collapsed field of Select and of Combobox, the\nlistbox's filter, Combobox's match against typed text, its inline\ncompletion, and the label on a chosen tag.\n\nA node goes to the default slot when it names no other, which is the rule\nthe platform applies at assignment. Asking the same question here rather\nthan a second one is what stops the two disagreeing. Text nodes are\nalways the default slot's and cannot say otherwise.\n\n**Text and elements, named rather than \"not an element\".** The filter read\n`!(node instanceof Element) || node.slot === ''`, which admits a comment,\nand `textContent` on a comment is the comment's own body. Every option Lit\nrenders carries one: a child binding is marked with `<!--?lit$1234$-->`, so\nan option written as `` html`<${option}>${size}</${option}>` `` read back as\n`?lit$090648746$25`. Measured in `pagination/footer`, where the trigger\npainted exactly that instead of `25`, on both twins.\n\nIt is worse than one wrong label, because of the six readers listed above:\nthe filter and Combobox's match compare against this string, so a marker in\nit is in every comparison too.\n\n**The `label` where the slot holds nothing**, which is what the row shows\nthen, so the six read the words a reader sees.",
80130
80207
  "readonly": true
80131
80208
  },
80132
80209
  {
@@ -80528,7 +80605,7 @@
80528
80605
  "text": "string"
80529
80606
  },
80530
80607
  "default": "''",
80531
- "description": "A shorter name for this choice, for where the row does not fit.\n\n`<option label>`'s own meaning: the text a chooser shows when it has less\nroom than the row does, which is what a select's closed trigger has. Empty\nmeans the row's own content, which is the common case and is why this is not\nsimply the accessible name.",
80608
+ "description": "A shorter name for this choice, for where the row does not fit.\n\n`<option label>`'s own meaning: the text a chooser shows when it has less\nroom than the row does, which is what a select's closed trigger has. Empty\nmeans the row's own content, which is the common case and is why this is not\nsimply the accessible name.\n\n**An option with a label and nothing in it shows the label**, as\n`<option label>` does: written the platform's way, a select listed a row of\nnothing for every choice.",
80532
80609
  "fieldName": "label"
80533
80610
  },
80534
80611
  {
@@ -88769,6 +88846,14 @@
88769
88846
  }
88770
88847
  ],
88771
88848
  "members": [
88849
+ {
88850
+ "kind": "field",
88851
+ "name": "clicks",
88852
+ "privacy": "protected",
88853
+ "readonly": true,
88854
+ "default": "new HostClickController(this, () => this.partNamed('item'))",
88855
+ "description": "A click on the element itself, `click()` included, chooses it."
88856
+ },
88772
88857
  {
88773
88858
  "kind": "field",
88774
88859
  "name": "defaultSelected",
@@ -101707,6 +101792,14 @@
101707
101792
  "readonly": true,
101708
101793
  "default": "new AriaForwardController(this, { target: () => this.partNamed('control'), })"
101709
101794
  },
101795
+ {
101796
+ "kind": "field",
101797
+ "name": "clicks",
101798
+ "privacy": "protected",
101799
+ "readonly": true,
101800
+ "default": "new HostClickController(this, () => this.partNamed('control'))",
101801
+ "description": "A click on the element itself, `click()` or a page's `<label for>`, toggles it."
101802
+ },
101710
101803
  {
101711
101804
  "kind": "field",
101712
101805
  "name": "value",
@@ -122,6 +122,12 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
122
122
  * hears what the card says first and where it leads second.
123
123
  */
124
124
  links: string;
125
+ /**
126
+ * @internal What the card's tone means, in the page's words, written by the
127
+ * board from its `toneLabels`. Read out before where the card leads, since
128
+ * a reader who can see the board is told it by the card's color first.
129
+ */
130
+ meaning: string;
125
131
  /**
126
132
  * Prevents the card from being moved, connected, or removed.
127
133
  *
@@ -204,12 +210,15 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
204
210
  */
205
211
  focus(options?: FocusOptions): void;
206
212
  /**
207
- * The card, and the sentence that says where it leads.
213
+ * The card, and the sentence that says what its tone means and where it
214
+ * leads.
208
215
  *
209
216
  * **`aria-describedby` at a hidden element, which is allowed and is the
210
217
  * point.** A description is computed from its target whether or not the
211
218
  * target is rendered, so the connections are read out and never drawn: the
212
- * arrows already draw them.
219
+ * arrows already draw them. **One element, the two joined with a comma**:
220
+ * two elements named in the reference are joined with a space alone, which
221
+ * read "Done leads to Launch" as one phrase.
213
222
  */
214
223
  protected render(): TemplateResult;
215
224
  }
@@ -31,6 +31,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
31
31
  this.placement = new PlacementController(this);
32
32
  this.name = '';
33
33
  this.links = '';
34
+ this.meaning = '';
34
35
  this.readOnly = false;
35
36
  this.selected = false;
36
37
  this.movable = false;
@@ -71,7 +72,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
71
72
  </svg>`
72
73
  : nothing}<div
73
74
  part="node"
74
- aria-describedby=${this.links ? 'links' : nothing}
75
+ aria-describedby=${this.meaning || this.links ? 'about' : nothing}
75
76
  aria-label=${this.group ? this.label || nothing : nothing}
76
77
  aria-keyshortcuts=${this.connectable ? 'Enter F2 Space Shift+ArrowRight Shift+ArrowDown Alt+ArrowRight Alt+ArrowDown C Delete' : nothing}
77
78
  role=${this.group ? 'group' : 'article'}
@@ -79,7 +80,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
79
80
  >
80
81
  <slot></slot>
81
82
  <slot name="description"></slot>
82
- <span id="links" hidden>${this.links}</span>
83
+ <span id="about" hidden>${[this.meaning, this.links].filter(Boolean).join(', ')}</span>
83
84
  </div>
84
85
  ${this.connectable
85
86
  ? html `<span part="handle handle-top" aria-hidden="true" data-side="top"></span>
@@ -120,6 +121,9 @@ __decorate([
120
121
  __decorate([
121
122
  state()
122
123
  ], HelixHeadlessCanvasNode.prototype, "links", void 0);
124
+ __decorate([
125
+ state()
126
+ ], HelixHeadlessCanvasNode.prototype, "meaning", void 0);
123
127
  __decorate([
124
128
  property({ attribute: 'readonly', reflect: true, type: Boolean })
125
129
  ], HelixHeadlessCanvasNode.prototype, "readOnly", void 0);
@@ -1,6 +1,6 @@
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 } from './canvas-document.js';
3
+ import { type CanvasDocument, type CanvasTone } from './canvas-document.js';
4
4
  export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, 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;
@@ -227,6 +227,17 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
227
227
  private editing;
228
228
  /** The text announced when a connection is stopped before it is requested. */
229
229
  stoppedLabel: string;
230
+ /**
231
+ * What each tone means on this board, in words read out with the cards and
232
+ * connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
233
+ *
234
+ * **Named by the page, since the tone names a color and the page gives it
235
+ * its meaning.** JSON Canvas leaves its six colors to the application, so
236
+ * red is "blocked" on one board and "urgent" on the next. A reader who can
237
+ * see the board is told by the color; this is how a reader who cannot is
238
+ * told the same. A tone left out is not read.
239
+ */
240
+ toneLabels: Partial<Record<CanvasTone, string>>;
230
241
  /** The card or connection that holds the board's one tab stop, by its key. */
231
242
  private stop;
232
243
  /**
@@ -419,6 +430,13 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
419
430
  private marquee;
420
431
  /** A card being resized from a corner, with the box it started from. */
421
432
  private sizing;
433
+ /** Where each finger on the board is, by pointer, which a pinch is measured from. */
434
+ private readonly fingers;
435
+ /**
436
+ * A pinch under way: how far apart its fingers were and the point between
437
+ * them, in the box, and the view, when the second finger landed.
438
+ */
439
+ private pinching;
422
440
  /** Where a drag started, and where the board was when it did. */
423
441
  private dragging;
424
442
  /**
@@ -749,6 +767,23 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
749
767
  private renderEditor;
750
768
  /** The box a Shift-drag is drawing, on the plane, while it is. */
751
769
  private renderMarquee;
770
+ /**
771
+ * The fingers' spread and the point between them, in the box. Taken from
772
+ * the first two fingers down, where a third has landed as well.
773
+ */
774
+ private fingerSpan;
775
+ /**
776
+ * Turns what the first finger was doing into a pinch, where that was a pan
777
+ * or a press that had not yet moved anything. A finger already carrying a
778
+ * card, drawing a line, resizing or drawing a box goes on doing that.
779
+ */
780
+ private startPinch;
781
+ /**
782
+ * Scales the board by how far the fingers have spread since the pinch
783
+ * began, about the point between them, and carries it with that point, so
784
+ * what was under the fingers stays under them.
785
+ */
786
+ private pinch;
752
787
  /** Scales about a point in the box, which is what both the buttons and a pinch want. */
753
788
  private zoomAbout;
754
789
  /** What one card says in the reading list, its links included. */
@@ -48,6 +48,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
48
48
  this.reconnectingLabel = 'Moving the end of the connection from {from} to {to}. Move to a card and press Enter, or Escape to stop.';
49
49
  this.selected = '';
50
50
  this.stoppedLabel = 'Stopped connecting.';
51
+ this.toneLabels = {};
51
52
  this.said = '';
52
53
  this.at = { x: 0, y: 0, zoom: 1 };
53
54
  this.direct = false;
@@ -58,6 +59,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
58
59
  this.sides = new Map();
59
60
  this.pressing = false;
60
61
  this.pointing = false;
62
+ this.fingers = new Map();
61
63
  this.keying = undefined;
62
64
  this.edgeKeys = new WeakMap();
63
65
  this.handlePointerDown = (event) => {
@@ -67,6 +69,15 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
67
69
  if (event.button !== 0 || on('controls') || !(card || on('viewport'))) {
68
70
  return;
69
71
  }
72
+ if (event.pointerType === 'touch') {
73
+ this.fingers.set(event.pointerId, { x: event.clientX, y: event.clientY });
74
+ if (this.fingers.size > 1) {
75
+ if (this.fingers.size === 2 && !this.pinching) {
76
+ this.startPinch();
77
+ }
78
+ return;
79
+ }
80
+ }
70
81
  if (this.held) {
71
82
  event.preventDefault();
72
83
  this.putDown(event);
@@ -178,6 +189,13 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
178
189
  };
179
190
  this.handlePointerMove = (event) => {
180
191
  this.idle.stir();
192
+ if (this.fingers.has(event.pointerId)) {
193
+ this.fingers.set(event.pointerId, { x: event.clientX, y: event.clientY });
194
+ if (this.pinching) {
195
+ this.pinch();
196
+ return;
197
+ }
198
+ }
181
199
  const pressed = this.carrying ?? this.dragging ?? this.sizing ?? this.marquee ?? this.linking?.pointer;
182
200
  if (event.pointerType === 'mouse' &&
183
201
  event.buttons === 0 &&
@@ -258,6 +276,27 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
258
276
  };
259
277
  };
260
278
  this.handlePointerUp = (event) => {
279
+ const finger = this.fingers.delete(event.pointerId);
280
+ if (this.pinching) {
281
+ if (this.fingers.size < 2) {
282
+ this.pinching = undefined;
283
+ this.direct = false;
284
+ }
285
+ if (this.fingers.size === 0) {
286
+ this.pressing = false;
287
+ }
288
+ return;
289
+ }
290
+ const owner = [
291
+ this.sizing?.pointer,
292
+ this.marquee?.pointer,
293
+ this.linking?.pointer,
294
+ this.carrying?.pointer,
295
+ this.dragging?.pointer,
296
+ ].includes(event.pointerId);
297
+ if (finger && !owner && this.fingers.size > 0) {
298
+ return;
299
+ }
261
300
  this.pressing = false;
262
301
  const pointing = this.pointing;
263
302
  this.pointing = false;
@@ -1263,6 +1302,48 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1263
1302
  style=${`--_canvas-marquee-x: ${left}px; --_canvas-marquee-y: ${top}px; --_canvas-marquee-inline-size: ${Math.abs(box.to.x - box.from.x)}px; --_canvas-marquee-block-size: ${Math.abs(box.to.y - box.from.y)}px`}
1264
1303
  ></div>`;
1265
1304
  }
1305
+ fingerSpan() {
1306
+ const [one, two] = [...this.fingers.values()];
1307
+ const box = this.partNamed('viewport')?.getBoundingClientRect();
1308
+ return {
1309
+ middle: {
1310
+ x: (one.x + two.x) / 2 - (box?.left ?? 0),
1311
+ y: (one.y + two.y) / 2 - (box?.top ?? 0),
1312
+ },
1313
+ spread: Math.hypot(two.x - one.x, two.y - one.y),
1314
+ };
1315
+ }
1316
+ startPinch() {
1317
+ if (this.sizing || this.marquee || this.linking || this.held || this.carrying?.started) {
1318
+ return;
1319
+ }
1320
+ this.carrying = undefined;
1321
+ this.dragging = undefined;
1322
+ this.pointing = false;
1323
+ const { middle, spread } = this.fingerSpan();
1324
+ if (spread === 0) {
1325
+ return;
1326
+ }
1327
+ for (const pointer of this.fingers.keys()) {
1328
+ capturePointer(this, pointer);
1329
+ }
1330
+ this.touched = true;
1331
+ this.direct = true;
1332
+ this.pinching = { at: { ...this.at }, middle, spread };
1333
+ }
1334
+ pinch() {
1335
+ const start = this.pinching;
1336
+ if (!start) {
1337
+ return;
1338
+ }
1339
+ const { middle, spread } = this.fingerSpan();
1340
+ const zoom = Math.min(Math.max((start.at.zoom * spread) / start.spread, ZOOM_MIN), ZOOM_MAX);
1341
+ this.at = {
1342
+ x: middle.x - ((start.middle.x - start.at.x) * zoom) / start.at.zoom,
1343
+ y: middle.y - ((start.middle.y - start.at.y) * zoom) / start.at.zoom,
1344
+ zoom,
1345
+ };
1346
+ }
1266
1347
  zoomAbout(by, about) {
1267
1348
  const from = this.at.zoom;
1268
1349
  const to = Math.min(Math.max(from * by, ZOOM_MIN), ZOOM_MAX);
@@ -1595,6 +1676,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1595
1676
  }
1596
1677
  const node = this.byId.get(card.name);
1597
1678
  card.links = node ? this.linksOf(node) : '';
1679
+ card.meaning = card.tone ? (this.toneLabels[card.tone] ?? '') : '';
1598
1680
  card.current = card.name === stop;
1599
1681
  card.selected = picked.has(card.name);
1600
1682
  card.resizable = alone === card.name && !this.fixed(card);
@@ -1661,7 +1743,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1661
1743
  const ends = heads(edge);
1662
1744
  return svg `<path
1663
1745
  part="edge-target"
1664
- aria-description=${edge.label || nothing}
1746
+ aria-description=${[element?.tone ? this.toneLabels[element.tone] : undefined, edge.label].filter(Boolean).join(', ') || nothing}
1665
1747
  aria-label=${fillTemplate(this.connectionLabel, { from: this.textOf(from), to: this.textOf(to) })}
1666
1748
  d=${drawn.d}
1667
1749
  data-edge=${key}
@@ -1844,6 +1926,9 @@ __decorate([
1844
1926
  __decorate([
1845
1927
  property({ attribute: 'stopped-label' })
1846
1928
  ], HelixHeadlessCanvas.prototype, "stoppedLabel", void 0);
1929
+ __decorate([
1930
+ property({ attribute: 'tone-labels', type: Object })
1931
+ ], HelixHeadlessCanvas.prototype, "toneLabels", void 0);
1847
1932
  __decorate([
1848
1933
  state()
1849
1934
  ], HelixHeadlessCanvas.prototype, "stop", void 0);
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElement, HostClickController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessCheckbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerCheckedInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
4
4
  /**
@@ -49,6 +49,8 @@ declare const HelixHeadlessCheckbox_base: (new (...args: any[]) => import("@fusi
49
49
  */
50
50
  export declare class HelixHeadlessCheckbox extends HelixHeadlessCheckbox_base {
51
51
  protected readonly aria: AriaForwardController;
52
+ /** A click on the element itself, `click()` or a page's `<label for>`, toggles it. */
53
+ protected readonly clicks: HostClickController;
52
54
  /**
53
55
  * Shows the box as neither checked nor unchecked.
54
56
  *
@@ -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, customElement, HelixFormElement, MixinControllerChecked, MixinField, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixFormElement, HostClickController, MixinControllerChecked, MixinField, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerChecked(MixinField(HelixFormElement)) {
@@ -13,6 +13,7 @@ let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerC
13
13
  this.aria = new AriaForwardController(this, {
14
14
  target: () => this.partNamed('control'),
15
15
  });
16
+ this.clicks = new HostClickController(this, () => this.partNamed('control'));
16
17
  this.indeterminate = false;
17
18
  this.value = 'on';
18
19
  this.handleChange = (event) => {
@@ -152,6 +152,10 @@ export declare class HelixHeadlessOption extends HelixElement {
152
152
  * room than the row does, which is what a select's closed trigger has. Empty
153
153
  * means the row's own content, which is the common case and is why this is not
154
154
  * simply the accessible name.
155
+ *
156
+ * **An option with a label and nothing in it shows the label**, as
157
+ * `<option label>` does: written the platform's way, a select listed a row of
158
+ * nothing for every choice.
155
159
  */
156
160
  label: string;
157
161
  /**
@@ -201,6 +205,9 @@ export declare class HelixHeadlessOption extends HelixElement {
201
205
  * It is worse than one wrong label, because of the six readers listed above:
202
206
  * the filter and Combobox's match compare against this string, so a marker in
203
207
  * it is in every comparison too.
208
+ *
209
+ * **The `label` where the slot holds nothing**, which is what the row shows
210
+ * then, so the six read the words a reader sees.
204
211
  */
205
212
  get text(): string;
206
213
  /** Sends `helix-select` for this option. Does nothing when it is disabled. */
@@ -29,11 +29,11 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
29
29
  this.toggleAttribute('selected', next);
30
30
  }
31
31
  get text() {
32
- return [...this.childNodes]
32
+ return ([...this.childNodes]
33
33
  .filter((node) => node.nodeType === Node.TEXT_NODE || (node instanceof Element && node.slot === ''))
34
34
  .map((node) => node.textContent ?? '')
35
35
  .join('')
36
- .trim();
36
+ .trim() || this.label);
37
37
  }
38
38
  select() {
39
39
  if (this.disabled) {
@@ -57,7 +57,7 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
57
57
  >
58
58
  <slot name="start"></slot>
59
59
  <span part="text">
60
- <span part="label"><slot></slot></span>
60
+ <span part="label"><slot>${this.label}</slot></span>
61
61
  ${this.description ? html `<span part="description">${this.description}</span>` : nothing}
62
62
  </span>
63
63
  <slot name="end"></slot>
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { HelixElement, HostClickController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessRadio_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
@@ -41,6 +41,8 @@ declare const HelixHeadlessRadio_base: (new (...args: any[]) => import("@fusion.
41
41
  * @cssstate selected - The radio is chosen.
42
42
  */
43
43
  export declare class HelixHeadlessRadio extends HelixHeadlessRadio_base {
44
+ /** A click on the element itself, `click()` included, chooses it. */
45
+ protected readonly clicks: HostClickController;
44
46
  /**
45
47
  * Whether the markup declares the radio chosen. A form reset returns to it.
46
48
  *
@@ -4,12 +4,13 @@ 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, HelixElement, MixinSelectableItem } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, HostClickController, MixinSelectableItem, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessRadio = class HelixHeadlessRadio extends MixinSelectableItem(HelixElement) {
11
11
  constructor() {
12
12
  super(...arguments);
13
+ this.clicks = new HostClickController(this, () => this.partNamed('item'));
13
14
  this.focusable = false;
14
15
  }
15
16
  get defaultSelected() {
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElement, HostClickController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessSwitch_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerCheckedInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
4
4
  /**
@@ -45,6 +45,8 @@ declare const HelixHeadlessSwitch_base: (new (...args: any[]) => import("@fusion
45
45
  */
46
46
  export declare class HelixHeadlessSwitch extends HelixHeadlessSwitch_base {
47
47
  protected readonly aria: AriaForwardController;
48
+ /** A click on the element itself, `click()` or a page's `<label for>`, toggles it. */
49
+ protected readonly clicks: HostClickController;
48
50
  /**
49
51
  * Value submitted when the switch is on.
50
52
  */
@@ -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, customElement, HelixFormElement, MixinControllerChecked, MixinField, markRegion, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixFormElement, HostClickController, MixinControllerChecked, MixinField, markRegion, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessSwitch = class HelixHeadlessSwitch extends MixinControllerChecked(MixinField(HelixFormElement)) {
@@ -13,6 +13,7 @@ let HelixHeadlessSwitch = class HelixHeadlessSwitch extends MixinControllerCheck
13
13
  this.aria = new AriaForwardController(this, {
14
14
  target: () => this.partNamed('control'),
15
15
  });
16
+ this.clicks = new HostClickController(this, () => this.partNamed('control'));
16
17
  this.value = 'on';
17
18
  this.handleChange = (event) => {
18
19
  event.stopPropagation();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-headless",
3
3
  "description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
4
- "version": "0.110.6",
4
+ "version": "0.111.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -46,13 +46,13 @@
46
46
  },
47
47
  "bugs": "https://github.com/project-fusion/fusion/issues",
48
48
  "dependencies": {
49
- "@fusion.dev/helix-core": "0.110.6",
50
- "@fusion.dev/helix-icons": "0.110.6",
49
+ "@fusion.dev/helix-core": "0.111.1",
50
+ "@fusion.dev/helix-icons": "0.111.1",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@fusion.dev/helix-cem": "0.110.6",
55
- "@fusion.dev/helix-testing": "0.110.6",
54
+ "@fusion.dev/helix-cem": "0.111.1",
55
+ "@fusion.dev/helix-testing": "0.111.1",
56
56
  "@fusion.dev/scripts": "0.0.0-dev",
57
57
  "@happy-dom/global-registrator": "^20.0.0",
58
58
  "@lit-labs/ssr": "^4.1.0",
package/types/jsx.d.ts CHANGED
@@ -2242,6 +2242,24 @@ Written to select from outside, and read to act on the selection. */
2242
2242
  "stopped-label"?: (HelixHeadlessCanvas["stoppedLabel"]) | undefined;
2243
2243
  /** The text announced when a connection is stopped before it is requested. */
2244
2244
  stoppedLabel?: (HelixHeadlessCanvas["stoppedLabel"]) | undefined;
2245
+ /** What each tone means on this board, in words read out with the cards and
2246
+ connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
2247
+
2248
+ **Named by the page, since the tone names a color and the page gives it
2249
+ its meaning.** JSON Canvas leaves its six colors to the application, so
2250
+ red is "blocked" on one board and "urgent" on the next. A reader who can
2251
+ see the board is told by the color; this is how a reader who cannot is
2252
+ told the same. A tone left out is not read. */
2253
+ "tone-labels"?: (HelixHeadlessCanvas["toneLabels"]) | undefined;
2254
+ /** What each tone means on this board, in words read out with the cards and
2255
+ connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
2256
+
2257
+ **Named by the page, since the tone names a color and the page gives it
2258
+ its meaning.** JSON Canvas leaves its six colors to the application, so
2259
+ red is "blocked" on one board and "urgent" on the next. A reader who can
2260
+ see the board is told by the color; this is how a reader who cannot is
2261
+ told the same. A tone left out is not read. */
2262
+ toneLabels?: (HelixHeadlessCanvas["toneLabels"]) | undefined;
2245
2263
  /** Which controls to show, as a list such as `pan zoom fullscreen`, or
2246
2264
  `none`. A family can add words of its own.
2247
2265
 
@@ -2542,6 +2560,24 @@ Written to select from outside, and read to act on the selection. */
2542
2560
  "attr:stopped-label"?: (HelixHeadlessCanvas["stoppedLabel"]) | undefined;
2543
2561
  /** The text announced when a connection is stopped before it is requested. */
2544
2562
  "prop:stoppedLabel"?: (HelixHeadlessCanvas["stoppedLabel"]) | undefined;
2563
+ /** What each tone means on this board, in words read out with the cards and
2564
+ connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
2565
+
2566
+ **Named by the page, since the tone names a color and the page gives it
2567
+ its meaning.** JSON Canvas leaves its six colors to the application, so
2568
+ red is "blocked" on one board and "urgent" on the next. A reader who can
2569
+ see the board is told by the color; this is how a reader who cannot is
2570
+ told the same. A tone left out is not read. */
2571
+ "attr:tone-labels"?: (HelixHeadlessCanvas["toneLabels"]) | undefined;
2572
+ /** What each tone means on this board, in words read out with the cards and
2573
+ connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
2574
+
2575
+ **Named by the page, since the tone names a color and the page gives it
2576
+ its meaning.** JSON Canvas leaves its six colors to the application, so
2577
+ red is "blocked" on one board and "urgent" on the next. A reader who can
2578
+ see the board is told by the color; this is how a reader who cannot is
2579
+ told the same. A tone left out is not read. */
2580
+ "prop:toneLabels"?: (HelixHeadlessCanvas["toneLabels"]) | undefined;
2545
2581
  /** Which controls to show, as a list such as `pan zoom fullscreen`, or
2546
2582
  `none`. A family can add words of its own.
2547
2583
 
@@ -14363,7 +14399,11 @@ string is not a fact a selector needs. */
14363
14399
  `<option label>`'s own meaning: the text a chooser shows when it has less
14364
14400
  room than the row does, which is what a select's closed trigger has. Empty
14365
14401
  means the row's own content, which is the common case and is why this is not
14366
- simply the accessible name. */
14402
+ simply the accessible name.
14403
+
14404
+ **An option with a label and nothing in it shows the label**, as
14405
+ `<option label>` does: written the platform's way, a select listed a row of
14406
+ nothing for every choice. */
14367
14407
  label?: (HelixHeadlessOption["label"]) | undefined;
14368
14408
  /** Whether this choice is currently chosen.
14369
14409
 
@@ -14470,7 +14510,11 @@ string is not a fact a selector needs. */
14470
14510
  `<option label>`'s own meaning: the text a chooser shows when it has less
14471
14511
  room than the row does, which is what a select's closed trigger has. Empty
14472
14512
  means the row's own content, which is the common case and is why this is not
14473
- simply the accessible name. */
14513
+ simply the accessible name.
14514
+
14515
+ **An option with a label and nothing in it shows the label**, as
14516
+ `<option label>` does: written the platform's way, a select listed a row of
14517
+ nothing for every choice. */
14474
14518
  "prop:label"?: (HelixHeadlessOption["label"]) | undefined;
14475
14519
  /** Whether this choice is currently chosen.
14476
14520
 
@@ -24658,6 +24702,14 @@ export type CustomElements = {
24658
24702
  *
24659
24703
  * Written to select from outside, and read to act on the selection.
24660
24704
  * - `stopped-label`/`stoppedLabel`: The text announced when a connection is stopped before it is requested.
24705
+ * - `tone-labels`/`toneLabels`: What each tone means on this board, in words read out with the cards and
24706
+ * connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
24707
+ *
24708
+ * **Named by the page, since the tone names a color and the page gives it
24709
+ * its meaning.** JSON Canvas leaves its six colors to the application, so
24710
+ * red is "blocked" on one board and "urgent" on the next. A reader who can
24711
+ * see the board is told by the color; this is how a reader who cannot is
24712
+ * told the same. A tone left out is not read.
24661
24713
  * - `controls`: Which controls to show, as a list such as `pan zoom fullscreen`, or
24662
24714
  * `none`. A family can add words of its own.
24663
24715
  *
@@ -34982,6 +35034,10 @@ export type CustomElements = {
34982
35034
  * room than the row does, which is what a select's closed trigger has. Empty
34983
35035
  * means the row's own content, which is the common case and is why this is not
34984
35036
  * simply the accessible name.
35037
+ *
35038
+ * **An option with a label and nothing in it shows the label**, as
35039
+ * `<option label>` does: written the platform's way, a select listed a row of
35040
+ * nothing for every choice.
34985
35041
  * - `selected`: Whether this choice is currently chosen.
34986
35042
  *
34987
35043
  * Written by the listbox.
@@ -35046,7 +35102,10 @@ export type CustomElements = {
35046
35102
  *
35047
35103
  * It is worse than one wrong label, because of the six readers listed above:
35048
35104
  * the filter and Combobox's match compare against this string, so a marker in
35049
- * it is in every comparison too. (property only) (readonly)
35105
+ * it is in every comparison too.
35106
+ *
35107
+ * **The `label` where the slot holds nothing**, which is what the row shows
35108
+ * then, so the six read the words a reader sees. (property only) (readonly)
35050
35109
  * - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
35051
35110
  *
35052
35111
  * **Named `whenSettled` because `settled` is taken four times over**, by
@@ -44594,6 +44653,14 @@ export type CustomElementsSolidJs = {
44594
44653
  *
44595
44654
  * Written to select from outside, and read to act on the selection.
44596
44655
  * - `stopped-label`/`stoppedLabel`: The text announced when a connection is stopped before it is requested.
44656
+ * - `tone-labels`/`toneLabels`: What each tone means on this board, in words read out with the cards and
44657
+ * connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
44658
+ *
44659
+ * **Named by the page, since the tone names a color and the page gives it
44660
+ * its meaning.** JSON Canvas leaves its six colors to the application, so
44661
+ * red is "blocked" on one board and "urgent" on the next. A reader who can
44662
+ * see the board is told by the color; this is how a reader who cannot is
44663
+ * told the same. A tone left out is not read.
44597
44664
  * - `controls`: Which controls to show, as a list such as `pan zoom fullscreen`, or
44598
44665
  * `none`. A family can add words of its own.
44599
44666
  *
@@ -55066,6 +55133,10 @@ export type CustomElementsSolidJs = {
55066
55133
  * room than the row does, which is what a select's closed trigger has. Empty
55067
55134
  * means the row's own content, which is the common case and is why this is not
55068
55135
  * simply the accessible name.
55136
+ *
55137
+ * **An option with a label and nothing in it shows the label**, as
55138
+ * `<option label>` does: written the platform's way, a select listed a row of
55139
+ * nothing for every choice.
55069
55140
  * - `selected`: Whether this choice is currently chosen.
55070
55141
  *
55071
55142
  * Written by the listbox.
@@ -55130,7 +55201,10 @@ export type CustomElementsSolidJs = {
55130
55201
  *
55131
55202
  * It is worse than one wrong label, because of the six readers listed above:
55132
55203
  * the filter and Combobox's match compare against this string, so a marker in
55133
- * it is in every comparison too. (property only) (readonly)
55204
+ * it is in every comparison too.
55205
+ *
55206
+ * **The `label` where the slot holds nothing**, which is what the row shows
55207
+ * then, so the six read the words a reader sees. (property only) (readonly)
55134
55208
  * - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
55135
55209
  *
55136
55210
  * **Named `whenSettled` because `settled` is taken four times over**, by
package/types/svelte.d.ts CHANGED
@@ -24,6 +24,8 @@ declare module 'svelte/elements' {
24
24
  'helix-headless-callout': CustomElements['helix-headless-callout']
25
25
  'helix-headless-canvas-edge': CustomElements['helix-headless-canvas-edge']
26
26
  'helix-headless-canvas-node': CustomElements['helix-headless-canvas-node']
27
+ 'danger': CustomElements['danger']
28
+ 'success': CustomElements['success']
27
29
  'helix-headless-canvas': CustomElements['helix-headless-canvas']
28
30
  'helix-headless-card': CustomElements['helix-headless-card']
29
31
  'helix-headless-carousel': CustomElements['helix-headless-carousel']
package/types/vue.d.ts CHANGED
@@ -1246,6 +1246,24 @@ Written to select from outside, and read to act on the selection. */
1246
1246
  "stopped-label"?: (HelixHeadlessCanvas["stoppedLabel"]) | undefined;
1247
1247
  /** The text announced when a connection is stopped before it is requested. */
1248
1248
  stoppedLabel?: (HelixHeadlessCanvas["stoppedLabel"]) | undefined;
1249
+ /** What each tone means on this board, in words read out with the cards and
1250
+ connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
1251
+
1252
+ **Named by the page, since the tone names a color and the page gives it
1253
+ its meaning.** JSON Canvas leaves its six colors to the application, so
1254
+ red is "blocked" on one board and "urgent" on the next. A reader who can
1255
+ see the board is told by the color; this is how a reader who cannot is
1256
+ told the same. A tone left out is not read. */
1257
+ "tone-labels"?: (HelixHeadlessCanvas["toneLabels"]) | undefined;
1258
+ /** What each tone means on this board, in words read out with the cards and
1259
+ connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
1260
+
1261
+ **Named by the page, since the tone names a color and the page gives it
1262
+ its meaning.** JSON Canvas leaves its six colors to the application, so
1263
+ red is "blocked" on one board and "urgent" on the next. A reader who can
1264
+ see the board is told by the color; this is how a reader who cannot is
1265
+ told the same. A tone left out is not read. */
1266
+ toneLabels?: (HelixHeadlessCanvas["toneLabels"]) | undefined;
1249
1267
  /** Which controls to show, as a list such as `pan zoom fullscreen`, or
1250
1268
  `none`. A family can add words of its own.
1251
1269
 
@@ -7403,7 +7421,11 @@ string is not a fact a selector needs. */
7403
7421
  `<option label>`'s own meaning: the text a chooser shows when it has less
7404
7422
  room than the row does, which is what a select's closed trigger has. Empty
7405
7423
  means the row's own content, which is the common case and is why this is not
7406
- simply the accessible name. */
7424
+ simply the accessible name.
7425
+
7426
+ **An option with a label and nothing in it shows the label**, as
7427
+ `<option label>` does: written the platform's way, a select listed a row of
7428
+ nothing for every choice. */
7407
7429
  label?: (HelixHeadlessOption["label"]) | undefined;
7408
7430
  /** Whether this choice is currently chosen.
7409
7431