@lensmap/starlight 0.2.0 → 0.3.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.
@@ -12,6 +12,169 @@ import {
12
12
 
13
13
  // ../../apps/web/src/document/subtree.ts
14
14
  import { polygonHull } from "d3-polygon";
15
+ function silhouette(contour, distance, padding) {
16
+ let low = Infinity;
17
+ let high = -Infinity;
18
+ for (const [x, y] of contour) {
19
+ const theta = Math.atan2(y, distance + x);
20
+ const half = Math.asin(padding / Math.hypot(distance + x, y));
21
+ low = Math.min(low, theta - half);
22
+ high = Math.max(high, theta + half);
23
+ }
24
+ return { low, high };
25
+ }
26
+ function planSubtree(key, children, fullCircle, step, padding) {
27
+ if (children.length === 0) return { key, contour: [[0, 0]], children: [] };
28
+ const atDistance = (distance2) => children.map((child) => silhouette(child.contour, distance2, padding));
29
+ const width = (bounds2) => bounds2.reduce((sum, bound) => sum + bound.high - bound.low, 0);
30
+ let distance = step;
31
+ let bounds = atDistance(distance);
32
+ const span = fullCircle ? Math.PI * 2 : Math.min(Math.PI, Math.max(Math.PI * 2 / 3, width(bounds)));
33
+ if (width(bounds) > span) {
34
+ let lower = distance;
35
+ while (width(atDistance(distance)) > span) distance *= 2;
36
+ while (distance - lower > 0.5) {
37
+ const middle = (lower + distance) / 2;
38
+ if (width(atDistance(middle)) > span) lower = middle;
39
+ else distance = middle;
40
+ }
41
+ bounds = atDistance(distance);
42
+ }
43
+ const required = width(bounds);
44
+ const gap = fullCircle ? (span - required) / children.length : children.length > 1 ? (span - required) / (children.length - 1) : 0;
45
+ let cursor = fullCircle ? -Math.PI / 2 + bounds[0].low : children.length === 1 ? -required / 2 : -span / 2;
46
+ const contour = [[0, 0]];
47
+ const placements = children.map((subtree, index) => {
48
+ const bound = bounds[index];
49
+ const angle = cursor - bound.low;
50
+ cursor += bound.high - bound.low + gap;
51
+ const cos = Math.cos(angle), sin = Math.sin(angle);
52
+ for (const [x, y] of subtree.contour) {
53
+ contour.push([
54
+ (distance + x) * cos - y * sin,
55
+ (distance + x) * sin + y * cos
56
+ ]);
57
+ }
58
+ return { subtree, angle, distance };
59
+ });
60
+ return {
61
+ key,
62
+ contour: polygonHull(contour) ?? contour,
63
+ children: placements
64
+ };
65
+ }
66
+
67
+ // ../../apps/web/src/document/layout.ts
68
+ function layoutGraph(nodes, edges) {
69
+ if (nodes.length === 0) return { nodes: [], edges };
70
+ const byKey2 = new Map(nodes.map((node) => [node.key, node]));
71
+ const children = new Map(nodes.map((node) => [node.key, []]));
72
+ const childKeys = /* @__PURE__ */ new Set();
73
+ for (const edge of edges) {
74
+ if (edge.kind !== "branch") continue;
75
+ const child = byKey2.get(edge.to);
76
+ if (child) children.get(edge.from)?.push(child);
77
+ childKeys.add(edge.to);
78
+ }
79
+ const roots = nodes.filter((node) => !childKeys.has(node.key));
80
+ const placed = /* @__PURE__ */ new Map();
81
+ const maximum = Math.max(43, ...nodes.map((n) => n.baseRadius ?? 43));
82
+ const step = Math.max(180, maximum * 2 + 24);
83
+ const padding = Math.max(56, maximum + 13);
84
+ function plan(node, isRoot) {
85
+ const descendants = (children.get(node.key) ?? []).map(
86
+ (child) => plan(child, false)
87
+ );
88
+ return planSubtree(node.key, descendants, isRoot, step, padding);
89
+ }
90
+ const plans = roots.map((root) => plan(root, true));
91
+ function seed(node, subtree, point, heading, isRoot) {
92
+ const actual = node.position ?? point;
93
+ const fixed = Boolean(node.position) || isRoot;
94
+ placed.set(node.key, {
95
+ key: node.key,
96
+ x: actual.x,
97
+ y: actual.y,
98
+ targetX: actual.x,
99
+ targetY: actual.y,
100
+ fx: fixed ? actual.x : void 0,
101
+ fy: fixed ? actual.y : void 0
102
+ });
103
+ subtree.children.forEach((placement) => {
104
+ const child = byKey2.get(placement.subtree.key);
105
+ if (!child)
106
+ throw new Error(
107
+ `Automatic layout could not find child "${placement.subtree.key}".`
108
+ );
109
+ const angle = heading + placement.angle;
110
+ const target = {
111
+ x: actual.x + Math.cos(angle) * placement.distance,
112
+ y: actual.y + Math.sin(angle) * placement.distance
113
+ };
114
+ const childPoint = child.position ?? target;
115
+ const direction = childPoint.x === actual.x && childPoint.y === actual.y ? angle : Math.atan2(childPoint.y - actual.y, childPoint.x - actual.x);
116
+ seed(child, placement.subtree, target, direction, false);
117
+ });
118
+ }
119
+ const anchor = nodes[0].position ?? { x: 0, y: 0 };
120
+ const rootRadius = roots.length > 1 ? step * 2 / Math.sin(Math.PI / roots.length) : 0;
121
+ roots.forEach((root, index) => {
122
+ const angle = -Math.PI / 2 + Math.PI * 2 * index / roots.length;
123
+ seed(
124
+ root,
125
+ plans[index],
126
+ {
127
+ x: anchor.x + Math.cos(angle) * rootRadius,
128
+ y: anchor.y + rootRadius + Math.sin(angle) * rootRadius
129
+ },
130
+ 0,
131
+ true
132
+ );
133
+ });
134
+ const simulation = forceSimulation([...placed.values()]).stop().force("x", forceX((node) => node.targetX).strength(0.2)).force("y", forceY((node) => node.targetY).strength(0.2)).force("collide", forceCollide(padding).iterations(2));
135
+ const ticks = Math.ceil(
136
+ Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())
137
+ );
138
+ simulation.tick(ticks);
139
+ return {
140
+ nodes: nodes.map(({ position, ...node }) => {
141
+ const point = placed.get(node.key);
142
+ if (!point)
143
+ throw new Error(`Automatic layout could not place node "${node.key}".`);
144
+ return {
145
+ ...node,
146
+ ...position ? { position } : {},
147
+ x: point.x,
148
+ y: point.y
149
+ };
150
+ }),
151
+ edges
152
+ };
153
+ }
154
+
155
+ // ../../apps/web/src/node-style.ts
156
+ function baseRadius(style) {
157
+ return 43 * (typeof style?.scale === "number" ? style.scale : 1);
158
+ }
159
+ var properties = /^(color|background-color|font-family|font-size|font-weight|font-style|line-height|border(?:-(?:color|width|style|top|right|bottom|left)(?:-(?:color|width|style))?)?|padding(?:-(?:top|right|bottom|left))?)$/;
160
+ var applied = /* @__PURE__ */ new WeakMap();
161
+ function applyNodeStyle(element, style, borderColour) {
162
+ for (const property of applied.get(element) ?? [])
163
+ element.style.removeProperty(property);
164
+ if (borderColour) element.style.borderColor = borderColour;
165
+ applied.set(
166
+ element,
167
+ (style?.declarations ?? []).map((d) => d.property).filter((p) => properties.test(p))
168
+ );
169
+ for (const declaration of style?.declarations ?? []) {
170
+ if (properties.test(declaration.property))
171
+ element.style.setProperty(
172
+ declaration.property,
173
+ declaration.value,
174
+ declaration.important ? "important" : ""
175
+ );
176
+ }
177
+ }
15
178
 
16
179
  // ../../apps/web/src/Viewer.tsx
17
180
  import {
@@ -23,7 +186,7 @@ import {
23
186
  useCallback as useCallback2,
24
187
  useEffect as useEffect4,
25
188
  useEffectEvent as useEffectEvent2,
26
- useId as useId3,
189
+ useId as useId2,
27
190
  useLayoutEffect,
28
191
  useMemo,
29
192
  useRef as useRef4,
@@ -93,7 +256,11 @@ function ConnectionChooser({
93
256
  /* @__PURE__ */ jsxs("span", { id: `connection-details-${candidate.node.key}`, children: [
94
257
  candidate.routes.map((route, routeIndex) => /* @__PURE__ */ jsxs("span", { children: [
95
258
  routeIndex > 0 ? " \xB7 " : "",
96
- connectionWording(route.edge.type, route.direction),
259
+ connectionWording(
260
+ route.edge.type,
261
+ route.direction,
262
+ route.edge.presentation
263
+ ),
97
264
  ` (${route.direction} ${route.edge.kind === "branch" ? "branch" : "cross-link"})`
98
265
  ] }, `${route.edge.key}:${route.direction}`)),
99
266
  !visibleKeys.has(candidate.node.key) && /* @__PURE__ */ jsx("span", { className: "outside-view", children: "outside this view" })
@@ -134,69 +301,197 @@ function ConnectionChooser({
134
301
  // ../../apps/web/src/ConnectionGap.tsx
135
302
  import { useId } from "react";
136
303
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
137
- function BlockingMarker({
138
- id,
304
+ function Shape({
305
+ kind,
139
306
  colour,
140
- gap = false
307
+ cut = false
141
308
  }) {
142
- return /* @__PURE__ */ jsx2(
143
- "marker",
144
- {
145
- id,
146
- viewBox: "0 0 10 10",
147
- refX: "7",
148
- refY: "5",
149
- markerWidth: "8",
150
- markerHeight: "8",
151
- orient: "auto",
152
- overflow: gap ? "visible" : void 0,
153
- children: gap ? /* @__PURE__ */ jsx2("rect", { x: "3", y: "-2", width: "3", height: "14", fill: "black" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
154
- /* @__PURE__ */ jsx2("line", { x1: "2", x2: "2", y1: "0", y2: "10", stroke: colour, strokeWidth: "2" }),
155
- /* @__PURE__ */ jsx2("line", { x1: "7", x2: "7", y1: "0", y2: "10", stroke: colour, strokeWidth: "2" })
156
- ] })
157
- }
158
- );
309
+ if (kind === "circle")
310
+ return /* @__PURE__ */ jsx2(
311
+ "circle",
312
+ {
313
+ cx: "-3",
314
+ cy: "0",
315
+ r: cut ? 2.5 : 3,
316
+ fill: cut ? "black" : "none",
317
+ stroke: cut ? "black" : colour,
318
+ strokeWidth: cut ? 0 : 1
319
+ }
320
+ );
321
+ if (kind === "diamond")
322
+ return /* @__PURE__ */ jsx2(
323
+ "path",
324
+ {
325
+ d: cut ? "M -0.7 0 L -3 -2.3 L -5.3 0 L -3 2.3 Z" : "M 0 0 L -3 -3 L -6 0 L -3 3 Z",
326
+ fill: cut ? "black" : "none",
327
+ stroke: cut ? "black" : colour,
328
+ strokeWidth: cut ? 0 : 1
329
+ }
330
+ );
331
+ if (kind === "bar")
332
+ return cut ? /* @__PURE__ */ jsx2("rect", { x: "-4.5", y: "-5", width: "3", height: "10", fill: "black" }) : /* @__PURE__ */ jsxs2("g", { stroke: colour, strokeWidth: "1", children: [
333
+ /* @__PURE__ */ jsx2("line", { x1: "-5", x2: "-5", y1: "-3", y2: "3" }),
334
+ /* @__PURE__ */ jsx2("line", { x1: "-1", x2: "-1", y1: "-3", y2: "3" })
335
+ ] });
336
+ if (kind === "arrow" && !cut)
337
+ return /* @__PURE__ */ jsx2(
338
+ "path",
339
+ {
340
+ d: "M -5 -3 L 0 0 L -5 3",
341
+ fill: "none",
342
+ stroke: colour,
343
+ strokeWidth: "1"
344
+ }
345
+ );
346
+ return null;
159
347
  }
160
- function ConnectionGap({
348
+ function StyledConnection({
161
349
  path,
162
350
  points,
351
+ presentation,
352
+ width,
353
+ colour,
163
354
  className,
164
- markerId,
165
- children
355
+ attributes,
356
+ continuation
166
357
  }) {
167
358
  const id = useId();
168
- const padding = 64;
169
- const x = Math.min(...points.map((point) => point.x)) - padding;
170
- const y = Math.min(...points.map((point) => point.y)) - padding;
171
- const width = Math.max(...points.map((point) => point.x)) + padding - x;
172
- const height = Math.max(...points.map((point) => point.y)) + padding - y;
173
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
174
- /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsxs2(
175
- "mask",
176
- {
177
- id,
178
- maskUnits: "userSpaceOnUse",
179
- x,
180
- y,
181
- width,
182
- height,
183
- style: { maskType: "luminance" },
184
- children: [
185
- /* @__PURE__ */ jsx2("rect", { x, y, width, height, fill: "white" }),
186
- /* @__PURE__ */ jsx2(
187
- "path",
359
+ const start = continuation === "incoming" ? "none" : presentation.startMarker;
360
+ const end = continuation === "outgoing" ? "none" : presentation.endMarker;
361
+ const paint = presentation.lineColor === "auto" ? colour : presentation.lineColor;
362
+ const marker = (kind, side, cut = false) => /* @__PURE__ */ jsx2(
363
+ "marker",
364
+ {
365
+ id: `${id}-${kind}-${side}`,
366
+ markerUnits: "strokeWidth",
367
+ markerWidth: 14,
368
+ markerHeight: 14,
369
+ viewBox: "-7 -7 14 14",
370
+ refX: "0",
371
+ refY: "0",
372
+ orient: "auto-start-reverse",
373
+ overflow: "visible",
374
+ children: /* @__PURE__ */ jsx2(Shape, { kind, colour: paint, cut })
375
+ },
376
+ side
377
+ );
378
+ const pad = Math.max(64, width * 16);
379
+ const x = Math.min(...points.map((p) => p.x)) - pad;
380
+ const y = Math.min(...points.map((p) => p.y)) - pad;
381
+ const w = Math.max(...points.map((p) => p.x)) - x + pad;
382
+ const h = Math.max(...points.map((p) => p.y)) - y + pad;
383
+ const style = { strokeWidth: width, color: paint };
384
+ return /* @__PURE__ */ jsxs2(
385
+ "g",
386
+ {
387
+ "data-presentation-width": width,
388
+ mask: continuation ? `url(#${id}-fade)` : void 0,
389
+ children: [
390
+ /* @__PURE__ */ jsxs2("defs", { children: [
391
+ continuation && /* @__PURE__ */ jsxs2(Fragment, { children: [
392
+ /* @__PURE__ */ jsxs2(
393
+ "linearGradient",
394
+ {
395
+ id: `${id}-gradient`,
396
+ gradientUnits: "userSpaceOnUse",
397
+ x1: points[0].x,
398
+ y1: points[0].y,
399
+ x2: points[1].x,
400
+ y2: points[1].y,
401
+ children: [
402
+ /* @__PURE__ */ jsx2("stop", { offset: "0%", stopColor: "white", stopOpacity: 0.65 }),
403
+ /* @__PURE__ */ jsx2("stop", { offset: "100%", stopColor: "white", stopOpacity: 0 })
404
+ ]
405
+ }
406
+ ),
407
+ /* @__PURE__ */ jsx2(
408
+ "mask",
409
+ {
410
+ id: `${id}-fade`,
411
+ maskUnits: "userSpaceOnUse",
412
+ x,
413
+ y,
414
+ width: w,
415
+ height: h,
416
+ style: { maskType: "luminance" },
417
+ children: /* @__PURE__ */ jsx2(
418
+ "rect",
419
+ {
420
+ x,
421
+ y,
422
+ width: w,
423
+ height: h,
424
+ fill: `url(#${id}-gradient)`
425
+ }
426
+ )
427
+ }
428
+ )
429
+ ] }),
430
+ marker(start, "start"),
431
+ marker(end, "end"),
432
+ marker(start, "start-cut", true),
433
+ marker(end, "end-cut", true),
434
+ /* @__PURE__ */ jsxs2(
435
+ "mask",
188
436
  {
189
- d: path,
190
- className,
191
- style: { stroke: "none", opacity: 1, filter: "none" },
192
- markerEnd: `url(#${markerId})`
437
+ id: `${id}-mask`,
438
+ maskUnits: "userSpaceOnUse",
439
+ x,
440
+ y,
441
+ width: w,
442
+ height: h,
443
+ style: { maskType: "luminance" },
444
+ children: [
445
+ /* @__PURE__ */ jsx2("rect", { x, y, width: w, height: h, fill: "white" }),
446
+ /* @__PURE__ */ jsx2(
447
+ "path",
448
+ {
449
+ d: path,
450
+ fill: "none",
451
+ stroke: "none",
452
+ strokeWidth: width,
453
+ markerStart: `url(#${id}-${start}-start-cut)`,
454
+ markerEnd: `url(#${id}-${end}-end-cut)`
455
+ }
456
+ )
457
+ ]
193
458
  }
194
459
  )
195
- ]
196
- }
197
- ) }),
198
- /* @__PURE__ */ jsx2("g", { mask: `url(#${id})`, children })
199
- ] });
460
+ ] }),
461
+ /* @__PURE__ */ jsxs2(
462
+ "g",
463
+ {
464
+ visibility: width === 0 ? "hidden" : void 0,
465
+ mask: `url(#${id}-mask)`,
466
+ children: [
467
+ /* @__PURE__ */ jsx2(
468
+ "path",
469
+ {
470
+ d: path,
471
+ className: "edge-halo",
472
+ style: { strokeWidth: width * 3.2 }
473
+ }
474
+ ),
475
+ /* @__PURE__ */ jsx2(
476
+ "path",
477
+ {
478
+ ...attributes,
479
+ d: path,
480
+ className,
481
+ stroke: paint,
482
+ style,
483
+ markerStart: start === "none" ? void 0 : `url(#${id}-${start}-start)`,
484
+ markerEnd: end === "none" ? void 0 : `url(#${id}-${end}-end)`,
485
+ strokeLinecap: presentation.lineStyle === "dotted" ? "round" : void 0,
486
+ strokeDasharray: presentation.lineStyle === "dotted" ? `0 ${width * 2.5}` : presentation.lineStyle === "dashed" ? `${width * 2.8} ${width * 2.4}` : void 0
487
+ }
488
+ )
489
+ ]
490
+ }
491
+ )
492
+ ]
493
+ }
494
+ );
200
495
  }
201
496
 
202
497
  // ../../apps/web/src/clearance.ts
@@ -204,7 +499,9 @@ function limitMagnification(points, nodeRadius2, strength) {
204
499
  const amount = Math.max(0, Math.min(1, strength));
205
500
  if (amount === 0 || points.length < 2)
206
501
  return points.map((point) => ({ ...point }));
207
- const radii = points.map((point) => point.scale * nodeRadius2);
502
+ const radii = points.map(
503
+ (point) => point.scale * (point.baseRadius ?? nodeRadius2)
504
+ );
208
505
  const minimum = radii.map((radius2) => Math.min(radius2, 2));
209
506
  const limited = [...radii];
210
507
  for (let i = 0; i < points.length; i++) {
@@ -230,7 +527,7 @@ function limitMagnification(points, nodeRadius2, strength) {
230
527
  }
231
528
  return points.map((point, index) => ({
232
529
  ...point,
233
- scale: point.scale + (limited[index] / nodeRadius2 - point.scale) * amount
530
+ scale: point.scale + (limited[index] / (point.baseRadius ?? nodeRadius2) - point.scale) * amount
234
531
  }));
235
532
  }
236
533
 
@@ -242,6 +539,39 @@ function connectionAppearance(type, continuation) {
242
539
  ...continuation !== "outgoing" && (type === "precedes" || type === "prevents") ? { end: type === "precedes" ? "arrow" : "bar" } : {}
243
540
  };
244
541
  }
542
+ function fallbackPresentation(type) {
543
+ const appearance = connectionAppearance(type);
544
+ return {
545
+ strokeWidth: 2.5,
546
+ lineStyle: appearance.dashed ? "dashed" : "solid",
547
+ lineColor: "auto",
548
+ startMarker: appearance.start ?? "none",
549
+ endMarker: appearance.end ?? "none",
550
+ text: "connected to",
551
+ reverseText: "connected to"
552
+ };
553
+ }
554
+
555
+ // ../../apps/web/src/connection-style.ts
556
+ function markerClearance(marker, width) {
557
+ if (marker === "arrow") return width * Math.hypot(5, 3) / 6;
558
+ if (marker === "diamond") return width / Math.SQRT2;
559
+ return marker === "circle" ? width / 2 : 0;
560
+ }
561
+ function resolveConnectionWidth(value, root, width, height) {
562
+ if (typeof value === "number") return value;
563
+ const container = document.createElement("div");
564
+ container.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;height:0;overflow:hidden";
565
+ container.style.width = `${Math.hypot(width, height) / Math.SQRT2}px`;
566
+ const probe = document.createElement("div");
567
+ probe.style.width = CSS.supports("width", value) ? value : `calc((${value}) * 1px)`;
568
+ probe.style.height = "0";
569
+ container.append(probe);
570
+ root.append(container);
571
+ const result = Number.parseFloat(getComputedStyle(probe).width);
572
+ container.remove();
573
+ return Number.isFinite(result) ? Math.max(0, result) : 0;
574
+ }
245
575
 
246
576
  // ../../apps/web/src/edge-routing.ts
247
577
  function edgeLanes(edges) {
@@ -296,10 +626,10 @@ function zoomCamera(camera, factor) {
296
626
  }
297
627
  function fitCamera(points, size, radius2) {
298
628
  if (points.length === 0) return { x: 0, y: 0, zoom: 1 };
299
- const minX = Math.min(...points.map((p) => p.x)) - radius2;
300
- const maxX = Math.max(...points.map((p) => p.x)) + radius2;
301
- const minY = Math.min(...points.map((p) => p.y)) - radius2;
302
- const maxY = Math.max(...points.map((p) => p.y)) + radius2;
629
+ const minX = Math.min(...points.map((p) => p.x - (p.baseRadius ?? radius2)));
630
+ const maxX = Math.max(...points.map((p) => p.x + (p.baseRadius ?? radius2)));
631
+ const minY = Math.min(...points.map((p) => p.y - (p.baseRadius ?? radius2)));
632
+ const maxY = Math.max(...points.map((p) => p.y + (p.baseRadius ?? radius2)));
303
633
  return {
304
634
  x: (minX + maxX) / 2,
305
635
  y: (minY + maxY) / 2,
@@ -316,7 +646,6 @@ function fitCamera(points, size, radius2) {
316
646
 
317
647
  // ../../apps/web/src/NeighbourhoodContinuations.tsx
318
648
  import { connectionWording as connectionWording2 } from "@lensmap/document";
319
- import { useId as useId2 } from "react";
320
649
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
321
650
  function NeighbourhoodContinuations({
322
651
  continuations,
@@ -325,25 +654,25 @@ function NeighbourhoodContinuations({
325
654
  nodes,
326
655
  nodeRadius: nodeRadius2,
327
656
  previewEdges,
328
- markerPrefix
657
+ widths
329
658
  }) {
330
- const prefix = useId2();
331
659
  const origin = positions.get(centreKey);
332
660
  const byKey2 = new Map(nodes.map((node) => [node.key, node]));
333
- const segments = continuations.flatMap((continuation, index) => {
661
+ const segments = continuations.flatMap((continuation) => {
334
662
  const point = positions.get(continuation.at);
335
663
  const source = byKey2.get(continuation.edge.from);
336
664
  const target = byKey2.get(continuation.edge.to);
337
665
  if (!point || !origin || !source || !target) return [];
338
666
  const incoming = continuation.edge.to === continuation.at;
339
- const appearance = connectionAppearance(
340
- continuation.edge.type,
341
- incoming ? "incoming" : "outgoing"
342
- );
343
667
  const angle = Math.atan2(point.y - origin.y, point.x - origin.x) + continuation.offset;
344
668
  const ux = Math.cos(angle), uy = Math.sin(angle);
345
- const marked = incoming ? appearance.end : appearance.start;
346
- const radius2 = nodeRadius2 * point.scale + (marked ? incoming ? 7 : 3 : 0);
669
+ const width = widths.get(continuation.edge.key) ?? 2.5;
670
+ const presentation = continuation.edge.presentation ?? fallbackPresentation(continuation.edge.type);
671
+ const clearance = markerClearance(
672
+ incoming ? presentation.endMarker : presentation.startMarker,
673
+ width
674
+ );
675
+ const radius2 = (byKey2.get(continuation.at)?.baseRadius ?? nodeRadius2) * point.scale + clearance;
347
676
  const near = { x: point.x + ux * radius2, y: point.y + uy * radius2 };
348
677
  const far = {
349
678
  x: near.x + ux * 70 * point.scale,
@@ -353,91 +682,49 @@ function NeighbourhoodContinuations({
353
682
  return [
354
683
  {
355
684
  ...continuation,
356
- appearance,
685
+ presentation,
357
686
  incoming,
358
687
  near,
359
688
  far,
360
689
  target,
361
- description: `${label(source)} ${connectionWording2(continuation.edge.type, "outgoing")} ${label(target)}`,
362
- id: `${prefix}-${index}`,
690
+ description: `${label(source)} ${connectionWording2(continuation.edge.type, "outgoing", continuation.edge.presentation)} ${label(target)}`,
363
691
  path: `M ${from.x} ${from.y} L ${to.x} ${to.y}`
364
692
  }
365
693
  ];
366
694
  });
367
- return /* @__PURE__ */ jsxs3("g", { className: "continuations", children: [
368
- /* @__PURE__ */ jsx3("defs", { children: segments.map((segment) => /* @__PURE__ */ jsxs3(
369
- "linearGradient",
370
- {
371
- id: segment.id,
372
- gradientUnits: "userSpaceOnUse",
373
- x1: segment.near.x,
374
- y1: segment.near.y,
375
- x2: segment.far.x,
376
- y2: segment.far.y,
377
- children: [
378
- /* @__PURE__ */ jsx3(
379
- "stop",
380
- {
381
- offset: "0%",
382
- stopColor: colours[segment.target.branch],
383
- stopOpacity: 0.65
384
- }
385
- ),
386
- /* @__PURE__ */ jsx3(
387
- "stop",
388
- {
389
- offset: "100%",
390
- stopColor: colours[segment.target.branch],
391
- stopOpacity: 0
392
- }
393
- )
394
- ]
395
- },
396
- segment.edge.key
397
- )) }),
398
- segments.map((segment) => {
399
- const appearance = segment.appearance;
400
- const edgeClass = `edge continuation ${segment.edge.kind} ${previewEdges.has(segment.edge.key) ? "connection-preview" : ""}`;
401
- const painted = /* @__PURE__ */ jsx3(
695
+ return /* @__PURE__ */ jsx3("g", { className: "continuations", children: segments.map((segment) => {
696
+ const edgeClass = `edge continuation ${segment.edge.kind} ${previewEdges.has(segment.edge.key) ? "connection-preview" : ""}`;
697
+ return /* @__PURE__ */ jsxs3("g", { children: [
698
+ /* @__PURE__ */ jsx3(
699
+ StyledConnection,
700
+ {
701
+ path: segment.path,
702
+ points: [segment.near, segment.far],
703
+ presentation: segment.presentation,
704
+ width: widths.get(segment.edge.key) ?? 2.5,
705
+ colour: colours[segment.target.branch],
706
+ className: edgeClass,
707
+ continuation: segment.incoming ? "incoming" : "outgoing",
708
+ attributes: {
709
+ "data-continuation": segment.edge.key,
710
+ "data-connection-preview": previewEdges.has(segment.edge.key) ? "true" : void 0,
711
+ "data-kind": segment.edge.kind,
712
+ "data-type": segment.edge.type ?? "connected",
713
+ "data-direction": segment.incoming ? "incoming" : "outgoing"
714
+ }
715
+ }
716
+ ),
717
+ /* @__PURE__ */ jsx3(
402
718
  "path",
403
719
  {
404
- "data-continuation": segment.edge.key,
405
- "data-connection-preview": previewEdges.has(segment.edge.key) ? "true" : void 0,
406
- "data-kind": segment.edge.kind,
407
- "data-type": segment.edge.type ?? "connected",
408
- strokeDasharray: appearance.dashed ? "7 6" : void 0,
409
- "data-direction": segment.incoming ? "incoming" : "outgoing",
720
+ className: "edge-hit",
721
+ "data-continuation-hit": segment.edge.key,
410
722
  d: segment.path,
411
- stroke: `url(#${segment.id})`,
412
- style: { color: colours[segment.target.branch] },
413
- className: edgeClass,
414
- markerStart: appearance.start ? `url(#${markerPrefix}-arrow-${segment.target.branch})` : void 0,
415
- markerEnd: appearance.end ? `url(#${markerPrefix}-${appearance.end}-${segment.target.branch})` : void 0
723
+ children: /* @__PURE__ */ jsx3("title", { children: segment.description })
416
724
  }
417
- );
418
- return /* @__PURE__ */ jsxs3("g", { children: [
419
- appearance.end === "bar" ? /* @__PURE__ */ jsx3(
420
- ConnectionGap,
421
- {
422
- path: segment.path,
423
- points: [segment.near, segment.far],
424
- className: edgeClass,
425
- markerId: `${markerPrefix}-bar-gap`,
426
- children: painted
427
- }
428
- ) : painted,
429
- /* @__PURE__ */ jsx3(
430
- "path",
431
- {
432
- className: "edge-hit",
433
- "data-continuation-hit": segment.edge.key,
434
- d: segment.path,
435
- children: /* @__PURE__ */ jsx3("title", { children: segment.description })
436
- }
437
- )
438
- ] }, segment.edge.key);
439
- })
440
- ] });
725
+ )
726
+ ] }, segment.edge.key);
727
+ }) });
441
728
  }
442
729
 
443
730
  // ../../apps/web/src/navigation.ts
@@ -557,9 +844,14 @@ function neighbourhood(graph, key, nodeRadius2 = 43) {
557
844
  neighbours.reduce((sum, n, i) => sum + Math.sin(n.bearing - i * step), 0),
558
845
  neighbours.reduce((sum, n, i) => sum + Math.cos(n.bearing - i * step), 0)
559
846
  );
847
+ const maximum = Math.max(
848
+ nodeRadius2,
849
+ ...graph.nodes.filter((n) => visible.has(n.key)).map((n) => n.baseRadius ?? nodeRadius2)
850
+ );
560
851
  const radius2 = Math.max(
852
+ (centre.baseRadius ?? nodeRadius2) + maximum + 24,
561
853
  260,
562
- (2 * nodeRadius2 + 24) / (2 * Math.sin(Math.PI / Math.max(2, neighbours.length)))
854
+ (2 * maximum + 24) / (2 * Math.sin(Math.PI / Math.max(2, neighbours.length)))
563
855
  );
564
856
  const nodes = [
565
857
  { ...centre, x: 0, y: 0 },
@@ -825,8 +1117,9 @@ function ViewerConnections({
825
1117
  "data-focus-key": key,
826
1118
  onClick: () => onFocus(key),
827
1119
  children: [
828
- connectionWording3(edge.type, direction),
829
- " \xB7 ",
1120
+ connectionWording3(edge.type, direction, edge.presentation),
1121
+ " ",
1122
+ "\xB7 ",
830
1123
  getLabel(node)
831
1124
  ]
832
1125
  },
@@ -935,11 +1228,11 @@ function reconcileViewer(previous, next, selected, camera, mode) {
935
1228
  }
936
1229
 
937
1230
  // ../../apps/web/src/Viewer.tsx
938
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1231
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
939
1232
  var initialCamera = { x: 0, y: 0, zoom: 0.85 };
940
1233
  var nodeRadius = 43;
941
1234
  function Viewer({
942
- graph,
1235
+ graph: inputGraph,
943
1236
  embedded = false,
944
1237
  navigationControls,
945
1238
  thoughtLink,
@@ -955,7 +1248,21 @@ function Viewer({
955
1248
  renderSidebar,
956
1249
  renderExternalInspector
957
1250
  }) {
958
- const scope = useId3();
1251
+ const [measured, setMeasured] = useState2(
1252
+ /* @__PURE__ */ new Map()
1253
+ );
1254
+ const graph = useMemo(() => {
1255
+ const nodes2 = inputGraph.nodes.map((node) => ({
1256
+ ...node,
1257
+ baseRadius: (node.style?.scale === "auto" ? measured.get(node.key) : void 0) ?? baseRadius(node.style)
1258
+ }));
1259
+ if (!nodes2.some(
1260
+ (node, i) => node.baseRadius !== (inputGraph.nodes[i].baseRadius ?? 43)
1261
+ ))
1262
+ return inputGraph;
1263
+ return layoutGraph(nodes2, inputGraph.edges);
1264
+ }, [inputGraph, measured]);
1265
+ const scope = useId2();
959
1266
  const anchors = useMemo(() => thoughtAnchors2(graph.nodes), [graph.nodes]);
960
1267
  const entryNode = graph.nodes.find((n) => n.key === initialTarget);
961
1268
  const restoredNode = graph.nodes.find(
@@ -974,6 +1281,24 @@ function Viewer({
974
1281
  const drag = useRef4(null);
975
1282
  const suppressPointerClick = useRef4(false);
976
1283
  const [size, setSize] = useState2({ width: 900, height: 640 });
1284
+ const [widths, setWidths] = useState2(/* @__PURE__ */ new Map());
1285
+ useLayoutEffect(() => {
1286
+ const root = canvas.current;
1287
+ if (!root) return;
1288
+ setWidths(
1289
+ new Map(
1290
+ graph.edges.map((edge) => [
1291
+ edge.key,
1292
+ resolveConnectionWidth(
1293
+ edge.presentation?.strokeWidth ?? 2.5,
1294
+ root,
1295
+ size.width,
1296
+ size.height
1297
+ )
1298
+ ])
1299
+ )
1300
+ );
1301
+ }, [graph, size]);
977
1302
  const [camera, setCamera] = useState2(
978
1303
  initialState?.camera ?? (entryNode ? { ...initialCamera, x: entryNode.x, y: entryNode.y } : initialCamera)
979
1304
  );
@@ -1063,6 +1388,57 @@ function Viewer({
1063
1388
  () => inNeighbourhood ? neighbourhood(graph, selected, nodeRadius) : null,
1064
1389
  [graph, inNeighbourhood, selected]
1065
1390
  );
1391
+ const measurementView = inNeighbourhood ? selected : null;
1392
+ useLayoutEffect(() => {
1393
+ let alive = true;
1394
+ const measure = () => {
1395
+ if (!alive || !canvas.current) return;
1396
+ const visible = /* @__PURE__ */ new Map();
1397
+ canvas.current.querySelectorAll(".node[data-auto-size='true']").forEach((element) => {
1398
+ const key = element.dataset.node;
1399
+ if (!key || !inputGraph.nodes.some((node) => node.key === key))
1400
+ return;
1401
+ const clone = element.cloneNode(true);
1402
+ clone.removeAttribute("id");
1403
+ clone.classList.remove("selected", "connected");
1404
+ clone.style.cssText = element.style.cssText;
1405
+ Object.assign(clone.style, {
1406
+ transform: "none",
1407
+ visibility: "hidden",
1408
+ position: "absolute",
1409
+ width: "max-content",
1410
+ maxWidth: "260px",
1411
+ height: "auto",
1412
+ minHeight: "0",
1413
+ left: "0",
1414
+ top: "0"
1415
+ });
1416
+ canvas.current?.append(clone);
1417
+ const box = clone.getBoundingClientRect();
1418
+ visible.set(
1419
+ key,
1420
+ Math.max(43, Math.ceil(Math.hypot(box.width, box.height) / 2))
1421
+ );
1422
+ clone.remove();
1423
+ });
1424
+ setMeasured((previous) => {
1425
+ const next = /* @__PURE__ */ new Map();
1426
+ for (const node of inputGraph.nodes) {
1427
+ if (node.style?.scale !== "auto") continue;
1428
+ const radius2 = visible.get(node.key) ?? previous.get(node.key);
1429
+ if (radius2 !== void 0) next.set(node.key, radius2);
1430
+ }
1431
+ return JSON.stringify([...previous]) === JSON.stringify([...next]) ? previous : next;
1432
+ });
1433
+ };
1434
+ measure();
1435
+ void document.fonts.ready.then(measure);
1436
+ document.fonts.addEventListener("loadingdone", measure);
1437
+ return () => {
1438
+ alive = false;
1439
+ document.fonts.removeEventListener("loadingdone", measure);
1440
+ };
1441
+ }, [inputGraph, measurementView]);
1066
1442
  useEffect4(() => cancelActivation, [cancelActivation]);
1067
1443
  useEffect4(() => {
1068
1444
  const element = canvas.current;
@@ -1106,7 +1482,19 @@ function Viewer({
1106
1482
  keyboard.stop,
1107
1483
  dismissConnectionChoice
1108
1484
  ]);
1109
- const shownCamera = inNeighbourhood ? neighbourhoodCamera : overview ? fitCamera(nodes, size, nodeRadius) : camera;
1485
+ const overviewPoints = nodes.flatMap((node) => {
1486
+ const loops = edges.filter(
1487
+ (edge) => edge.from === node.key && edge.to === node.key
1488
+ );
1489
+ if (!node.style || loops.length === 0) return [node];
1490
+ const reach = (node.baseRadius ?? nodeRadius) * (3 + Math.max(...loops.map((edge) => lanes.get(edge.key)?.index ?? 0)));
1491
+ return [
1492
+ node,
1493
+ { x: node.x - reach, y: node.y - reach, baseRadius: 0 },
1494
+ { x: node.x + reach, y: node.y - reach, baseRadius: 0 }
1495
+ ];
1496
+ });
1497
+ const shownCamera = inNeighbourhood ? neighbourhoodCamera : overview ? fitCamera(overviewPoints, size, nodeRadius) : camera;
1110
1498
  const projected = (local?.nodes ?? nodes).map((node) => ({
1111
1499
  ...projectPoint(
1112
1500
  node,
@@ -1114,7 +1502,8 @@ function Viewer({
1114
1502
  size,
1115
1503
  overview ? 0 : shownStrength
1116
1504
  ),
1117
- node
1505
+ node,
1506
+ baseRadius: node.baseRadius
1118
1507
  }));
1119
1508
  const circles = overview ? projected : limitMagnification(projected, nodeRadius, shownStrength);
1120
1509
  const positions = new Map(circles.map((point) => [point.node.key, point]));
@@ -1617,39 +2006,6 @@ function Viewer({
1617
2006
  height: size.height,
1618
2007
  children: [
1619
2008
  /* @__PURE__ */ jsx7("title", { children: "Explicit thought connections" }),
1620
- /* @__PURE__ */ jsxs6("defs", { children: [
1621
- Object.entries(colours).map(([branch, colour]) => /* @__PURE__ */ jsx7(
1622
- "marker",
1623
- {
1624
- id: `${scope}-arrow-${branch}`,
1625
- viewBox: "0 0 10 10",
1626
- refX: "8",
1627
- refY: "5",
1628
- markerWidth: "7",
1629
- markerHeight: "7",
1630
- orient: "auto-start-reverse",
1631
- children: /* @__PURE__ */ jsx7(
1632
- "path",
1633
- {
1634
- d: "M 1 1 L 9 5 L 1 9",
1635
- fill: "none",
1636
- stroke: colour,
1637
- strokeWidth: "1.5"
1638
- }
1639
- )
1640
- },
1641
- branch
1642
- )),
1643
- Object.entries(colours).map(([branch, colour]) => /* @__PURE__ */ jsx7(
1644
- BlockingMarker,
1645
- {
1646
- id: `${scope}-bar-${branch}`,
1647
- colour
1648
- },
1649
- `bar-${branch}`
1650
- )),
1651
- /* @__PURE__ */ jsx7(BlockingMarker, { id: `${scope}-bar-gap`, colour: "black", gap: true })
1652
- ] }),
1653
2009
  local && /* @__PURE__ */ jsx7(
1654
2010
  NeighbourhoodContinuations,
1655
2011
  {
@@ -1659,23 +2015,32 @@ function Viewer({
1659
2015
  nodes,
1660
2016
  nodeRadius,
1661
2017
  previewEdges,
1662
- markerPrefix: scope
2018
+ widths
1663
2019
  }
1664
2020
  ),
1665
2021
  drawnEdges.map((edge) => {
1666
- const appearance = connectionAppearance(edge.type);
2022
+ const presentation = edge.presentation ?? fallbackPresentation(edge.type);
2023
+ const width = widths.get(edge.key) ?? 2.5;
2024
+ const startClearance = markerClearance(
2025
+ presentation.startMarker,
2026
+ width
2027
+ );
2028
+ const endClearance = markerClearance(
2029
+ presentation.endMarker,
2030
+ width
2031
+ );
1667
2032
  const from = positions.get(edge.from);
1668
2033
  const to = positions.get(edge.to);
1669
2034
  if (!from || !to) return null;
1670
- const description = `${label(from.node)} ${connectionWording4(edge.type, "outgoing")} ${label(to.node)}`;
2035
+ const description = `${label(from.node)} ${connectionWording4(edge.type, "outgoing", edge.presentation)} ${label(to.node)}`;
1671
2036
  const dx = to.x - from.x;
1672
2037
  const dy = to.y - from.y;
1673
2038
  const distance = Math.max(1, Math.hypot(dx, dy));
1674
2039
  const start = {
1675
- x: from.x + dx / distance * nodeRadius * from.scale,
1676
- y: from.y + dy / distance * nodeRadius * from.scale
2040
+ x: from.x + dx / distance * ((from.node.baseRadius ?? nodeRadius) * from.scale + startClearance),
2041
+ y: from.y + dy / distance * ((from.node.baseRadius ?? nodeRadius) * from.scale + startClearance)
1677
2042
  };
1678
- const targetRadius = nodeRadius * to.scale + (appearance.end ? 7 : 0);
2043
+ const targetRadius = (to.node.baseRadius ?? nodeRadius) * to.scale + endClearance;
1679
2044
  const end = {
1680
2045
  x: to.x - dx / distance * targetRadius,
1681
2046
  y: to.y - dy / distance * targetRadius
@@ -1687,18 +2052,18 @@ function Viewer({
1687
2052
  const side = (((start.x + end.x) / 2 - centre.x) * -dy + ((start.y + end.y) / 2 - centre.y) * dx) / distance;
1688
2053
  bend = (side < 0 ? -1 : 1) * (190 + (lane?.index ?? 0) * 60);
1689
2054
  }
1690
- const radius2 = nodeRadius * from.scale;
2055
+ const radius2 = (from.node.baseRadius ?? nodeRadius) * from.scale;
1691
2056
  const loop = radius2 * (3 + (lane?.index ?? 0));
1692
2057
  const points = edge.from === edge.to ? [
1693
2058
  {
1694
- x: from.x + radius2 * 0.7,
1695
- y: from.y - radius2 * 0.7
2059
+ x: from.x + (radius2 + startClearance) * 0.7,
2060
+ y: from.y - (radius2 + startClearance) * 0.7
1696
2061
  },
1697
2062
  { x: from.x + loop, y: from.y - loop },
1698
2063
  { x: from.x - loop, y: from.y - loop },
1699
2064
  {
1700
- x: from.x - radius2 * 0.7,
1701
- y: from.y - radius2 * 0.7
2065
+ x: from.x - (radius2 + endClearance) * 0.7,
2066
+ y: from.y - (radius2 + endClearance) * 0.7
1702
2067
  }
1703
2068
  ] : [
1704
2069
  start,
@@ -1710,37 +2075,25 @@ function Viewer({
1710
2075
  ];
1711
2076
  const path = points.length === 4 ? `M ${points[0].x} ${points[0].y} C ${points[1].x} ${points[1].y} ${points[2].x} ${points[2].y} ${points[3].x} ${points[3].y}` : `M ${points[0].x} ${points[0].y} Q ${points[1].x} ${points[1].y} ${points[2].x} ${points[2].y}`;
1712
2077
  const edgeClass = `edge ${edge.kind} ${connectedEdges.has(edge.key) ? "connected" : local ? "context" : ""} ${previewEdges.has(edge.key) ? "connection-preview" : ""}`;
1713
- const painted = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1714
- /* @__PURE__ */ jsx7("path", { d: path, className: "edge-halo" }),
1715
- /* @__PURE__ */ jsx7(
1716
- "path",
1717
- {
1718
- "data-edge": `${edge.from}-${edge.to}`,
1719
- "data-edge-key": edge.key,
1720
- "data-connection-preview": previewEdges.has(edge.key) ? "true" : void 0,
1721
- "data-kind": edge.kind,
1722
- "data-type": edge.type ?? "connected",
1723
- strokeDasharray: appearance.dashed ? "7 6" : void 0,
1724
- d: path,
1725
- stroke: colours[to.node.branch],
1726
- style: { color: colours[to.node.branch] },
1727
- className: edgeClass,
1728
- markerStart: appearance.start ? `url(#${scope}-${appearance.start}-${to.node.branch})` : void 0,
1729
- markerEnd: appearance.end ? `url(#${scope}-${appearance.end}-${to.node.branch})` : void 0
1730
- }
1731
- )
1732
- ] });
1733
2078
  return /* @__PURE__ */ jsxs6("g", { children: [
1734
- appearance.end === "bar" ? /* @__PURE__ */ jsx7(
1735
- ConnectionGap,
2079
+ /* @__PURE__ */ jsx7(
2080
+ StyledConnection,
1736
2081
  {
1737
2082
  path,
1738
2083
  points,
2084
+ presentation,
2085
+ width,
2086
+ colour: colours[to.node.branch],
1739
2087
  className: edgeClass,
1740
- markerId: `${scope}-bar-gap`,
1741
- children: painted
2088
+ attributes: {
2089
+ "data-edge": `${edge.from}-${edge.to}`,
2090
+ "data-edge-key": edge.key,
2091
+ "data-kind": edge.kind,
2092
+ "data-type": edge.type ?? "connected",
2093
+ "data-connection-preview": previewEdges.has(edge.key) ? "true" : void 0
2094
+ }
1742
2095
  }
1743
- ) : painted,
2096
+ ),
1744
2097
  /* @__PURE__ */ jsx7(
1745
2098
  "path",
1746
2099
  {
@@ -1762,6 +2115,15 @@ function Viewer({
1762
2115
  "button",
1763
2116
  {
1764
2117
  type: "button",
2118
+ ref: (element) => {
2119
+ if (element)
2120
+ applyNodeStyle(
2121
+ element,
2122
+ node.style,
2123
+ colours[node.branch]
2124
+ );
2125
+ },
2126
+ "data-auto-size": node.style?.scale === "auto" ? "true" : void 0,
1765
2127
  id: `${scope}-thought-${node.key}`,
1766
2128
  "data-anchor": anchors.get(node.key),
1767
2129
  className: `node ${selected === node.key ? "selected" : ""} ${connectedNodes.has(node.key) ? "connected" : ""} ${node.unresolved ? "unresolved" : ""}`,
@@ -1772,6 +2134,8 @@ function Viewer({
1772
2134
  "aria-pressed": selected === node.key,
1773
2135
  title: label(node),
1774
2136
  style: {
2137
+ width: 2 * (node.baseRadius ?? nodeRadius),
2138
+ height: 2 * (node.baseRadius ?? nodeRadius),
1775
2139
  left: point.x,
1776
2140
  top: point.y,
1777
2141
  transform: `translate(-50%, -50%) scale(${point.scale})`,
@@ -1787,6 +2151,7 @@ function Viewer({
1787
2151
  else focusNode(node.key);
1788
2152
  },
1789
2153
  children: [
2154
+ node.style?.icon && /* @__PURE__ */ jsx7("span", { className: "node-icon", "aria-hidden": "true", children: node.style.icon }),
1790
2155
  node.destination && /* @__PURE__ */ jsx7(
1791
2156
  "span",
1792
2157
  {
@@ -1822,7 +2187,7 @@ function Viewer({
1822
2187
  "aria-hidden": "true"
1823
2188
  }
1824
2189
  ),
1825
- /* @__PURE__ */ jsx7("span", { children: node.unresolved ? "?" : /* @__PURE__ */ jsx7(
2190
+ /* @__PURE__ */ jsx7("span", { className: "node-summary", children: node.unresolved ? "?" : /* @__PURE__ */ jsx7(
1826
2191
  Markdown2,
1827
2192
  {
1828
2193
  allowedElements: [
@@ -1950,7 +2315,7 @@ function Viewer({
1950
2315
  }
1951
2316
 
1952
2317
  // src/SelectedThought.tsx
1953
- import { useId as useId4, useMemo as useMemo2, useState as useState3 } from "react";
2318
+ import { useId as useId3, useMemo as useMemo2, useState as useState3 } from "react";
1954
2319
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1955
2320
  function readingExcerpt(html) {
1956
2321
  const template = document.createElement("template");
@@ -1968,7 +2333,7 @@ function readingExcerpt(html) {
1968
2333
  function SelectedThought({ html }) {
1969
2334
  const excerpt = useMemo2(() => readingExcerpt(html), [html]);
1970
2335
  const [expanded, setExpanded] = useState3(false);
1971
- const fullId = useId4();
2336
+ const fullId = useId3();
1972
2337
  if (!excerpt) {
1973
2338
  return /* @__PURE__ */ jsx8(
1974
2339
  "div",
@@ -2062,8 +2427,32 @@ function ViewerHost({
2062
2427
  graph,
2063
2428
  rendered,
2064
2429
  inspectorId = "default",
2065
- getLabel
2430
+ getLabel,
2431
+ mapId
2066
2432
  }) {
2433
+ const [liveGraph, setLiveGraph] = useState4(graph);
2434
+ const [stale, setStale] = useState4(false);
2435
+ useEffect5(() => setLiveGraph(graph), [graph]);
2436
+ useEffect5(() => {
2437
+ const hot = import.meta.hot;
2438
+ if (!hot || !mapId) return;
2439
+ const update = (data) => {
2440
+ if (data.id !== mapId) return;
2441
+ setStale(data.stale);
2442
+ const edges = data.edges;
2443
+ if (!data.stale && edges)
2444
+ setLiveGraph((previous) => ({
2445
+ ...previous,
2446
+ edges,
2447
+ nodes: previous.nodes.map((node) => ({
2448
+ ...node,
2449
+ destination: data.nodes?.find((update2) => update2.key === node.key)?.destination
2450
+ }))
2451
+ }));
2452
+ };
2453
+ hot.on("lensmap:vocabulary", update);
2454
+ return () => hot.off("lensmap:vocabulary", update);
2455
+ }, [mapId]);
2067
2456
  const [targets, setTargets] = useState4([]);
2068
2457
  useEffect5(() => {
2069
2458
  setTargets(
@@ -2076,28 +2465,31 @@ function ViewerHost({
2076
2465
  )
2077
2466
  );
2078
2467
  }, [inspectorId]);
2079
- return /* @__PURE__ */ jsx9(
2080
- Viewer,
2081
- {
2082
- graph,
2083
- embedded: true,
2084
- showInspector: false,
2085
- renderExternalInspector: (context) => targets.map(
2086
- (target, index) => createPortal(
2087
- /* @__PURE__ */ jsx9(
2088
- Inspector,
2089
- {
2090
- context,
2091
- rendered,
2092
- getLabel
2093
- }
2094
- ),
2095
- target,
2096
- String(index)
2468
+ return /* @__PURE__ */ jsxs8("div", { "data-lensmap-stale": String(stale), children: [
2469
+ stale && /* @__PURE__ */ jsx9("p", { role: "status", children: "Connection vocabulary is invalid. Showing the last valid map." }),
2470
+ /* @__PURE__ */ jsx9(
2471
+ Viewer,
2472
+ {
2473
+ graph: liveGraph,
2474
+ embedded: true,
2475
+ showInspector: false,
2476
+ renderExternalInspector: (context) => targets.map(
2477
+ (target, index) => createPortal(
2478
+ /* @__PURE__ */ jsx9(
2479
+ Inspector,
2480
+ {
2481
+ context,
2482
+ rendered,
2483
+ getLabel
2484
+ }
2485
+ ),
2486
+ target,
2487
+ String(index)
2488
+ )
2097
2489
  )
2098
- )
2099
- }
2100
- );
2490
+ }
2491
+ )
2492
+ ] });
2101
2493
  }
2102
2494
  export {
2103
2495
  ViewerHost as default