@fusion.dev/helix-headless 0.110.5 → 0.111.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.
@@ -9993,6 +9993,14 @@
9993
9993
  "description": "The card's name as text. A group shows it, and another card's connections\nread it out.\n\nA group needs one because it has no text of its own; a card that has text\nis named by it, and one holding a picture or a figure is named by nothing\nshort enough to read out in another card's connections.",
9994
9994
  "attribute": "label"
9995
9995
  },
9996
+ {
9997
+ "kind": "field",
9998
+ "name": "placement",
9999
+ "privacy": "private",
10000
+ "readonly": true,
10001
+ "default": "new PlacementController(this)",
10002
+ "description": "Where the card was placed, kept in its own shadow root."
10003
+ },
9996
10004
  {
9997
10005
  "kind": "field",
9998
10006
  "name": "name",
@@ -10510,6 +10518,10 @@
10510
10518
  "description": "The card that receives focus when you tab into the board.",
10511
10519
  "name": "current"
10512
10520
  },
10521
+ {
10522
+ "description": "Placed on the board.",
10523
+ "name": "placed"
10524
+ },
10513
10525
  {
10514
10526
  "description": "The card is selected.",
10515
10527
  "name": "selected"
@@ -11218,6 +11230,16 @@
11218
11230
  "description": "The text announced when a connection is stopped before it is requested.",
11219
11231
  "attribute": "stopped-label"
11220
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
+ },
11221
11243
  {
11222
11244
  "kind": "field",
11223
11245
  "name": "stop",
@@ -11676,6 +11698,23 @@
11676
11698
  "privacy": "private",
11677
11699
  "description": "A card being resized from a corner, with the box it started from."
11678
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
+ },
11679
11718
  {
11680
11719
  "kind": "field",
11681
11720
  "name": "dragging",
@@ -12684,6 +12723,39 @@
12684
12723
  ],
12685
12724
  "description": "The box a Shift-drag is drawing, on the plane, while it is."
12686
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
+ },
12687
12759
  {
12688
12760
  "kind": "method",
12689
12761
  "name": "zoomAbout",
@@ -13953,6 +14025,15 @@
13953
14025
  "description": "The text announced when a connection is stopped before it is requested.",
13954
14026
  "fieldName": "stoppedLabel"
13955
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
+ },
13956
14037
  {
13957
14038
  "name": "controls",
13958
14039
  "type": {
@@ -64521,6 +64602,14 @@
64521
64602
  "description": "Longitude, in degrees.",
64522
64603
  "attribute": "lng"
64523
64604
  },
64605
+ {
64606
+ "kind": "field",
64607
+ "name": "placement",
64608
+ "privacy": "private",
64609
+ "readonly": true,
64610
+ "default": "new PlacementController(this)",
64611
+ "description": "Where the marker was placed, kept in its own shadow root."
64612
+ },
64524
64613
  {
64525
64614
  "kind": "field",
64526
64615
  "name": "name",
@@ -65021,6 +65110,10 @@
65021
65110
  "description": "Marked as passed.",
65022
65111
  "name": "passed"
65023
65112
  },
65113
+ {
65114
+ "description": "Placed on the map by the connector.",
65115
+ "name": "placed"
65116
+ },
65024
65117
  {
65025
65118
  "description": "Shows its name before the point, because there was no room after it.",
65026
65119
  "name": "start"
@@ -65885,6 +65978,14 @@
65885
65978
  "description": "Longitude, in degrees.",
65886
65979
  "attribute": "lng"
65887
65980
  },
65981
+ {
65982
+ "kind": "field",
65983
+ "name": "placement",
65984
+ "privacy": "private",
65985
+ "readonly": true,
65986
+ "default": "new PlacementController(this)",
65987
+ "description": "Where the position was placed, kept in its own shadow root."
65988
+ },
65888
65989
  {
65889
65990
  "kind": "field",
65890
65991
  "name": "spoken",
@@ -66316,6 +66417,20 @@
66316
66417
  "fieldName": "lng"
66317
66418
  }
66318
66419
  ],
66420
+ "cssStates": [
66421
+ {
66422
+ "description": "Placed on the map by the connector.",
66423
+ "name": "placed"
66424
+ },
66425
+ {
66426
+ "description": "Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.",
66427
+ "name": "settled",
66428
+ "inheritedFrom": {
66429
+ "name": "HelixElement",
66430
+ "module": "src/helix-element/helix-element.ts"
66431
+ }
66432
+ }
66433
+ ],
66319
66434
  "superclass": {
66320
66435
  "name": "HelixElement",
66321
66436
  "package": "@fusion.dev/helix-core"
@@ -66338,16 +66453,6 @@
66338
66453
  },
66339
66454
  "tagName": "helix-headless-map-position",
66340
66455
  "customElement": true,
66341
- "cssStates": [
66342
- {
66343
- "description": "Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.",
66344
- "name": "settled",
66345
- "inheritedFrom": {
66346
- "name": "HelixElement",
66347
- "module": "src/helix-element/helix-element.ts"
66348
- }
66349
- }
66350
- ],
66351
66456
  "modulePath": "src/map/position/map-position.ts",
66352
66457
  "definitionPath": "src/map/position/map-position.ts"
66353
66458
  }
@@ -57,6 +57,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
57
57
  *
58
58
  * @cssstate candidate - While a connection is being drawn and this card is somewhere it could arrive.
59
59
  * @cssstate current - The card that receives focus when you tab into the board.
60
+ * @cssstate placed - Placed on the board.
60
61
  * @cssstate selected - The card is selected.
61
62
  * @cssstate target - A connection being drawn would end here if you released it.
62
63
  */
@@ -97,6 +98,16 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
97
98
  * short enough to read out in another card's connections.
98
99
  */
99
100
  label: string;
101
+ /** Where the card was placed, kept in its own shadow root. */
102
+ private readonly placement;
103
+ /**
104
+ * @internal Places the card, written by the board: each custom property
105
+ * given, and each one given as `undefined` taken away. Kept in a sheet in the
106
+ * element's own shadow root, since its `style` is the page's.
107
+ */
108
+ place(properties: Readonly<Record<string, string | undefined>>): void;
109
+ /** @internal What a custom property was placed at, or `undefined` where it was not. */
110
+ placed(name: string): string | undefined;
100
111
  /**
101
112
  * The name that connections and events use for this card, unique on its
102
113
  * board.
@@ -111,6 +122,12 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
111
122
  * hears what the card says first and where it leads second.
112
123
  */
113
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;
114
131
  /**
115
132
  * Prevents the card from being moved, connected, or removed.
116
133
  *
@@ -193,12 +210,15 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
193
210
  */
194
211
  focus(options?: FocusOptions): void;
195
212
  /**
196
- * 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.
197
215
  *
198
216
  * **`aria-describedby` at a hidden element, which is allowed and is the
199
217
  * point.** A description is computed from its target whether or not the
200
218
  * target is rendered, so the connections are read out and never drawn: the
201
- * 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.
202
222
  */
203
223
  protected render(): TemplateResult;
204
224
  }
@@ -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, HelixElement } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, PlacementController } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  const DRAWN = [
@@ -28,8 +28,10 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
28
28
  this.current = false;
29
29
  this.height = 0;
30
30
  this.label = '';
31
+ this.placement = new PlacementController(this);
31
32
  this.name = '';
32
33
  this.links = '';
34
+ this.meaning = '';
33
35
  this.readOnly = false;
34
36
  this.selected = false;
35
37
  this.movable = false;
@@ -39,6 +41,13 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
39
41
  this.x = 0;
40
42
  this.y = 0;
41
43
  }
44
+ place(properties) {
45
+ this.placement.set(properties);
46
+ this.toggleState('placed', this.placement.get('--_canvas-card-x') !== undefined);
47
+ }
48
+ placed(name) {
49
+ return this.placement.get(name);
50
+ }
42
51
  willUpdate(changed) {
43
52
  super.willUpdate(changed);
44
53
  this.toggleState('candidate', this.candidate);
@@ -63,7 +72,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
63
72
  </svg>`
64
73
  : nothing}<div
65
74
  part="node"
66
- aria-describedby=${this.links ? 'links' : nothing}
75
+ aria-describedby=${this.meaning || this.links ? 'about' : nothing}
67
76
  aria-label=${this.group ? this.label || nothing : nothing}
68
77
  aria-keyshortcuts=${this.connectable ? 'Enter F2 Space Shift+ArrowRight Shift+ArrowDown Alt+ArrowRight Alt+ArrowDown C Delete' : nothing}
69
78
  role=${this.group ? 'group' : 'article'}
@@ -71,7 +80,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
71
80
  >
72
81
  <slot></slot>
73
82
  <slot name="description"></slot>
74
- <span id="links" hidden>${this.links}</span>
83
+ <span id="about" hidden>${[this.meaning, this.links].filter(Boolean).join(', ')}</span>
75
84
  </div>
76
85
  ${this.connectable
77
86
  ? html `<span part="handle handle-top" aria-hidden="true" data-side="top"></span>
@@ -112,6 +121,9 @@ __decorate([
112
121
  __decorate([
113
122
  state()
114
123
  ], HelixHeadlessCanvasNode.prototype, "links", void 0);
124
+ __decorate([
125
+ state()
126
+ ], HelixHeadlessCanvasNode.prototype, "meaning", void 0);
115
127
  __decorate([
116
128
  property({ attribute: 'readonly', reflect: true, type: Boolean })
117
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. */
@@ -33,14 +33,7 @@ const onPlane = (node, origin) => ({
33
33
  const INTERACTIVE = 'input, textarea, select, button, a[href], [contenteditable]:not([contenteditable="false"])';
34
34
  const custom = (element, name) => element?.style.getPropertyValue(name).trim() ?? '';
35
35
  const finite = (value) => (Number.isFinite(value) ? value : 0);
36
- const size = (element, name, value) => {
37
- if (value > 0) {
38
- element.style.setProperty(name, `${value}px`);
39
- }
40
- else {
41
- element.style.removeProperty(name);
42
- }
43
- };
36
+ const size = (value) => (value > 0 ? `${value}px` : undefined);
44
37
  let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixElement) {
45
38
  constructor() {
46
39
  super(...arguments);
@@ -55,6 +48,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
55
48
  this.reconnectingLabel = 'Moving the end of the connection from {from} to {to}. Move to a card and press Enter, or Escape to stop.';
56
49
  this.selected = '';
57
50
  this.stoppedLabel = 'Stopped connecting.';
51
+ this.toneLabels = {};
58
52
  this.said = '';
59
53
  this.at = { x: 0, y: 0, zoom: 1 };
60
54
  this.direct = false;
@@ -65,6 +59,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
65
59
  this.sides = new Map();
66
60
  this.pressing = false;
67
61
  this.pointing = false;
62
+ this.fingers = new Map();
68
63
  this.keying = undefined;
69
64
  this.edgeKeys = new WeakMap();
70
65
  this.handlePointerDown = (event) => {
@@ -74,6 +69,15 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
74
69
  if (event.button !== 0 || on('controls') || !(card || on('viewport'))) {
75
70
  return;
76
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
+ }
77
81
  if (this.held) {
78
82
  event.preventDefault();
79
83
  this.putDown(event);
@@ -185,6 +189,13 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
185
189
  };
186
190
  this.handlePointerMove = (event) => {
187
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
+ }
188
199
  const pressed = this.carrying ?? this.dragging ?? this.sizing ?? this.marquee ?? this.linking?.pointer;
189
200
  if (event.pointerType === 'mouse' &&
190
201
  event.buttons === 0 &&
@@ -265,6 +276,27 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
265
276
  };
266
277
  };
267
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
+ }
268
300
  this.pressing = false;
269
301
  const pointing = this.pointing;
270
302
  this.pointing = false;
@@ -1270,6 +1302,48 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1270
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`}
1271
1303
  ></div>`;
1272
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
+ }
1273
1347
  zoomAbout(by, about) {
1274
1348
  const from = this.at.zoom;
1275
1349
  const to = Math.min(Math.max(from * by, ZOOM_MIN), ZOOM_MAX);
@@ -1495,12 +1569,6 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1495
1569
  return true;
1496
1570
  }
1497
1571
  const target = record.target;
1498
- if (target instanceof HelixHeadlessCanvasNode &&
1499
- this.placed.has(target) &&
1500
- custom(target, '--_canvas-card-x') === '') {
1501
- this.placed.delete(target);
1502
- return true;
1503
- }
1504
1572
  const name = target instanceof HelixHeadlessCanvasNode
1505
1573
  ? '--canvas-node-color'
1506
1574
  : target instanceof HelixHeadlessCanvasEdge
@@ -1599,13 +1667,16 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1599
1667
  const placed = `${x} ${y} ${finite(card.width)} ${finite(card.height)}`;
1600
1668
  if (this.placed.get(card) !== placed) {
1601
1669
  this.placed.set(card, placed);
1602
- card.style.setProperty('--_canvas-card-x', `${x}px`);
1603
- card.style.setProperty('--_canvas-card-y', `${y}px`);
1604
- size(card, '--_canvas-card-inline-size', finite(card.width));
1605
- size(card, '--_canvas-card-block-size', finite(card.height));
1670
+ card.place({
1671
+ '--_canvas-card-block-size': size(finite(card.height)),
1672
+ '--_canvas-card-inline-size': size(finite(card.width)),
1673
+ '--_canvas-card-x': `${x}px`,
1674
+ '--_canvas-card-y': `${y}px`,
1675
+ });
1606
1676
  }
1607
1677
  const node = this.byId.get(card.name);
1608
1678
  card.links = node ? this.linksOf(node) : '';
1679
+ card.meaning = card.tone ? (this.toneLabels[card.tone] ?? '') : '';
1609
1680
  card.current = card.name === stop;
1610
1681
  card.selected = picked.has(card.name);
1611
1682
  card.resizable = alone === card.name && !this.fixed(card);
@@ -1672,7 +1743,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1672
1743
  const ends = heads(edge);
1673
1744
  return svg `<path
1674
1745
  part="edge-target"
1675
- aria-description=${edge.label || nothing}
1746
+ aria-description=${[element?.tone ? this.toneLabels[element.tone] : undefined, edge.label].filter(Boolean).join(', ') || nothing}
1676
1747
  aria-label=${fillTemplate(this.connectionLabel, { from: this.textOf(from), to: this.textOf(to) })}
1677
1748
  d=${drawn.d}
1678
1749
  data-edge=${key}
@@ -1855,6 +1926,9 @@ __decorate([
1855
1926
  __decorate([
1856
1927
  property({ attribute: 'stopped-label' })
1857
1928
  ], HelixHeadlessCanvas.prototype, "stoppedLabel", void 0);
1929
+ __decorate([
1930
+ property({ attribute: 'tone-labels', type: Object })
1931
+ ], HelixHeadlessCanvas.prototype, "toneLabels", void 0);
1858
1932
  __decorate([
1859
1933
  state()
1860
1934
  ], HelixHeadlessCanvas.prototype, "stop", void 0);
@@ -36,6 +36,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
36
36
  * @cssstate fanned - Spread out from a location it shares with other markers, after their cluster was pressed.
37
37
  * @cssstate icon - Has content in the icon slot.
38
38
  * @cssstate passed - Marked as passed.
39
+ * @cssstate placed - Placed on the map by the connector.
39
40
  * @cssstate start - Shows its name before the point, because there was no room after it.
40
41
  */
41
42
  export declare class HelixHeadlessMapMarker extends HelixElement {
@@ -86,6 +87,16 @@ export declare class HelixHeadlessMapMarker extends HelixElement {
86
87
  * the point unless the view's edge would cut the name off there.
87
88
  */
88
89
  side: 'end' | 'start';
90
+ /** Where the marker was placed, kept in its own shadow root. */
91
+ private readonly placement;
92
+ /**
93
+ * @internal Places the marker, written by the connector: each custom property
94
+ * given, and each one given as `undefined` taken away. Kept in a sheet in the
95
+ * element's own shadow root, since its `style` is the page's.
96
+ */
97
+ place(properties: Readonly<Record<string, string | undefined>>): void;
98
+ /** @internal What a custom property was placed at, or `undefined` where it was not. */
99
+ placed(name: string): string | undefined;
89
100
  /** The value `helix-activate` reports. Defaults to the label. */
90
101
  name: string;
91
102
  /**
@@ -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, HelixElement, HelixMapPartChangeEvent } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, HelixMapPartChangeEvent, PlacementController, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  const BOOKKEEPING = new Set([
@@ -25,11 +25,19 @@ let HelixHeadlessMapMarker = class HelixHeadlessMapMarker extends HelixElement {
25
25
  this.gatheredBy = undefined;
26
26
  this.isMapMarker = true;
27
27
  this.side = 'end';
28
+ this.placement = new PlacementController(this);
28
29
  this.name = '';
29
30
  this.passed = false;
30
31
  this.passedLabel = 'passed';
31
32
  this.handingOver = false;
32
33
  }
34
+ place(properties) {
35
+ this.placement.set(properties);
36
+ this.toggleState('placed', this.placement.get('--_map-marker-x') !== undefined);
37
+ }
38
+ placed(name) {
39
+ return this.placement.get(name);
40
+ }
33
41
  willUpdate(changed) {
34
42
  super.willUpdate(changed);
35
43
  this.handingOver =
@@ -28,6 +28,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
28
28
  * @csspart position - The image of the position, named by its label.
29
29
  * @csspart pulse - A ring that spreads out from the point, showing that the position is live.
30
30
  * @csspart status - The description, announced when it changes.
31
+ *
32
+ * @cssstate placed - Placed on the map by the connector.
31
33
  */
32
34
  export declare class HelixHeadlessMapPosition extends HelixElement {
33
35
  /** How accurate the position is, in meters. Drawn as a ring with this radius. */
@@ -49,6 +51,16 @@ export declare class HelixHeadlessMapPosition extends HelixElement {
49
51
  lat?: number;
50
52
  /** Longitude, in degrees. */
51
53
  lng?: number;
54
+ /** Where the position was placed, kept in its own shadow root. */
55
+ private readonly placement;
56
+ /**
57
+ * @internal Places the position, written by the connector: each custom property
58
+ * given, and each one given as `undefined` taken away. Kept in a sheet in the
59
+ * element's own shadow root, since its `style` is the page's.
60
+ */
61
+ place(properties: Readonly<Record<string, string | undefined>>): void;
62
+ /** @internal What a custom property was placed at, or `undefined` where it was not. */
63
+ placed(name: string): string | undefined;
52
64
  /** The words last spoken, which is what the status holds. */
53
65
  private spoken;
54
66
  /** When they were spoken, for the interval. */
@@ -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, HelixElement, HelixMapPartChangeEvent } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, HelixMapPartChangeEvent, PlacementController, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessMapPosition = class HelixHeadlessMapPosition extends HelixElement {
@@ -13,9 +13,17 @@ let HelixHeadlessMapPosition = class HelixHeadlessMapPosition extends HelixEleme
13
13
  this.announceEvery = 5000;
14
14
  this.isMapPosition = true;
15
15
  this.label = 'Live position';
16
+ this.placement = new PlacementController(this);
16
17
  this.spoken = '';
17
18
  this.spokenAt = Number.NEGATIVE_INFINITY;
18
19
  }
20
+ place(properties) {
21
+ this.placement.set(properties);
22
+ this.toggleState('placed', this.placement.get('--_map-position-x') !== undefined);
23
+ }
24
+ placed(name) {
25
+ return this.placement.get(name);
26
+ }
19
27
  willUpdate(changed) {
20
28
  super.willUpdate(changed);
21
29
  if (!changed.has('description') || !this.hasUpdated) {
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.5",
4
+ "version": "0.111.0",
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.5",
50
- "@fusion.dev/helix-icons": "0.110.5",
49
+ "@fusion.dev/helix-core": "0.111.0",
50
+ "@fusion.dev/helix-icons": "0.111.0",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@fusion.dev/helix-cem": "0.110.5",
55
- "@fusion.dev/helix-testing": "0.110.5",
54
+ "@fusion.dev/helix-cem": "0.111.0",
55
+ "@fusion.dev/helix-testing": "0.111.0",
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
 
@@ -24517,6 +24553,7 @@ export type CustomElements = {
24517
24553
  *
24518
24554
  * - `candidate`: While a connection is being drawn and this card is somewhere it could arrive.
24519
24555
  * - `current`: The card that receives focus when you tab into the board.
24556
+ * - `placed`: Placed on the board.
24520
24557
  * - `selected`: The card is selected.
24521
24558
  * - `target`: A connection being drawn would end here if you released it.
24522
24559
  * - `settled`: Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.
@@ -24657,6 +24694,14 @@ export type CustomElements = {
24657
24694
  *
24658
24695
  * Written to select from outside, and read to act on the selection.
24659
24696
  * - `stopped-label`/`stoppedLabel`: The text announced when a connection is stopped before it is requested.
24697
+ * - `tone-labels`/`toneLabels`: What each tone means on this board, in words read out with the cards and
24698
+ * connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
24699
+ *
24700
+ * **Named by the page, since the tone names a color and the page gives it
24701
+ * its meaning.** JSON Canvas leaves its six colors to the application, so
24702
+ * red is "blocked" on one board and "urgent" on the next. A reader who can
24703
+ * see the board is told by the color; this is how a reader who cannot is
24704
+ * told the same. A tone left out is not read.
24660
24705
  * - `controls`: Which controls to show, as a list such as `pan zoom fullscreen`, or
24661
24706
  * `none`. A family can add words of its own.
24662
24707
  *
@@ -32754,6 +32799,7 @@ export type CustomElements = {
32754
32799
  * - `fanned`: Spread out from a location it shares with other markers, after their cluster was pressed.
32755
32800
  * - `icon`: Has content in the icon slot.
32756
32801
  * - `passed`: Marked as passed.
32802
+ * - `placed`: Placed on the map by the connector.
32757
32803
  * - `start`: Shows its name before the point, because there was no room after it.
32758
32804
  * - `settled`: Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.
32759
32805
  */
@@ -32908,6 +32954,7 @@ export type CustomElements = {
32908
32954
  *
32909
32955
  * These can be used to apply styling when a component is in a given state.
32910
32956
  *
32957
+ * - `placed`: Placed on the map by the connector.
32911
32958
  * - `settled`: Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.
32912
32959
  */
32913
32960
  "helix-headless-map-position": Partial<
@@ -44449,6 +44496,7 @@ export type CustomElementsSolidJs = {
44449
44496
  *
44450
44497
  * - `candidate`: While a connection is being drawn and this card is somewhere it could arrive.
44451
44498
  * - `current`: The card that receives focus when you tab into the board.
44499
+ * - `placed`: Placed on the board.
44452
44500
  * - `selected`: The card is selected.
44453
44501
  * - `target`: A connection being drawn would end here if you released it.
44454
44502
  * - `settled`: Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.
@@ -44590,6 +44638,14 @@ export type CustomElementsSolidJs = {
44590
44638
  *
44591
44639
  * Written to select from outside, and read to act on the selection.
44592
44640
  * - `stopped-label`/`stoppedLabel`: The text announced when a connection is stopped before it is requested.
44641
+ * - `tone-labels`/`toneLabels`: What each tone means on this board, in words read out with the cards and
44642
+ * connections in it, such as `{"danger": "Blocked", "success": "Done"}`.
44643
+ *
44644
+ * **Named by the page, since the tone names a color and the page gives it
44645
+ * its meaning.** JSON Canvas leaves its six colors to the application, so
44646
+ * red is "blocked" on one board and "urgent" on the next. A reader who can
44647
+ * see the board is told by the color; this is how a reader who cannot is
44648
+ * told the same. A tone left out is not read.
44593
44649
  * - `controls`: Which controls to show, as a list such as `pan zoom fullscreen`, or
44594
44650
  * `none`. A family can add words of its own.
44595
44651
  *
@@ -52799,6 +52855,7 @@ export type CustomElementsSolidJs = {
52799
52855
  * - `fanned`: Spread out from a location it shares with other markers, after their cluster was pressed.
52800
52856
  * - `icon`: Has content in the icon slot.
52801
52857
  * - `passed`: Marked as passed.
52858
+ * - `placed`: Placed on the map by the connector.
52802
52859
  * - `start`: Shows its name before the point, because there was no room after it.
52803
52860
  * - `settled`: Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.
52804
52861
  */
@@ -52959,6 +53016,7 @@ export type CustomElementsSolidJs = {
52959
53016
  *
52960
53017
  * These can be used to apply styling when a component is in a given state.
52961
53018
  *
53019
+ * - `placed`: Placed on the map by the connector.
52962
53020
  * - `settled`: Set one frame after the first render. Transitions wait for it, so an element does not animate its own arrival.
52963
53021
  */
52964
53022
  "helix-headless-map-position": Partial<
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