@lensmap/starlight 0.3.0 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  // src/ViewerHost.tsx
2
- import { useEffect as useEffect5, useState as useState4 } from "react";
2
+ import { useEffect as useEffect5, useState as useState5 } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
 
5
5
  // ../../apps/web/src/document/layout.ts
@@ -10,6 +10,34 @@ import {
10
10
  forceY
11
11
  } from "d3-force";
12
12
 
13
+ // ../../apps/web/src/node-style.ts
14
+ function positionAxis(node, axis) {
15
+ const value = node.style?.[axis];
16
+ return value === "auto" ? void 0 : value ?? node.position?.[axis];
17
+ }
18
+ function baseRadius(style) {
19
+ return 43 * (typeof style?.scale === "number" ? style.scale : 1);
20
+ }
21
+ 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))?)$/;
22
+ var applied = /* @__PURE__ */ new WeakMap();
23
+ function applyNodeStyle(element, style, borderColour) {
24
+ for (const property of applied.get(element) ?? [])
25
+ element.style.removeProperty(property);
26
+ if (borderColour) element.style.borderColor = borderColour;
27
+ applied.set(
28
+ element,
29
+ (style?.declarations ?? []).map((d) => d.property).filter((p) => properties.test(p))
30
+ );
31
+ for (const declaration of style?.declarations ?? []) {
32
+ if (properties.test(declaration.property))
33
+ element.style.setProperty(
34
+ declaration.property,
35
+ declaration.value,
36
+ declaration.important ? "important" : ""
37
+ );
38
+ }
39
+ }
40
+
13
41
  // ../../apps/web/src/document/subtree.ts
14
42
  import { polygonHull } from "d3-polygon";
15
43
  function silhouette(contour, distance, padding) {
@@ -89,16 +117,17 @@ function layoutGraph(nodes, edges) {
89
117
  }
90
118
  const plans = roots.map((root) => plan(root, true));
91
119
  function seed(node, subtree, point, heading, isRoot) {
92
- const actual = node.position ?? point;
93
- const fixed = Boolean(node.position) || isRoot;
120
+ const pinX = positionAxis(node, "x");
121
+ const pinY = positionAxis(node, "y");
122
+ const actual = { x: pinX ?? point.x, y: pinY ?? point.y };
94
123
  placed.set(node.key, {
95
124
  key: node.key,
96
125
  x: actual.x,
97
126
  y: actual.y,
98
127
  targetX: actual.x,
99
128
  targetY: actual.y,
100
- fx: fixed ? actual.x : void 0,
101
- fy: fixed ? actual.y : void 0
129
+ fx: pinX !== void 0 || isRoot ? actual.x : void 0,
130
+ fy: pinY !== void 0 || isRoot ? actual.y : void 0
102
131
  });
103
132
  subtree.children.forEach((placement) => {
104
133
  const child = byKey2.get(placement.subtree.key);
@@ -111,12 +140,18 @@ function layoutGraph(nodes, edges) {
111
140
  x: actual.x + Math.cos(angle) * placement.distance,
112
141
  y: actual.y + Math.sin(angle) * placement.distance
113
142
  };
114
- const childPoint = child.position ?? target;
143
+ const childPoint = {
144
+ x: positionAxis(child, "x") ?? target.x,
145
+ y: positionAxis(child, "y") ?? target.y
146
+ };
115
147
  const direction = childPoint.x === actual.x && childPoint.y === actual.y ? angle : Math.atan2(childPoint.y - actual.y, childPoint.x - actual.x);
116
148
  seed(child, placement.subtree, target, direction, false);
117
149
  });
118
150
  }
119
- const anchor = nodes[0].position ?? { x: 0, y: 0 };
151
+ const anchor = {
152
+ x: positionAxis(nodes[0], "x") ?? 0,
153
+ y: positionAxis(nodes[0], "y") ?? 0
154
+ };
120
155
  const rootRadius = roots.length > 1 ? step * 2 / Math.sin(Math.PI / roots.length) : 0;
121
156
  roots.forEach((root, index) => {
122
157
  const angle = -Math.PI / 2 + Math.PI * 2 * index / roots.length;
@@ -152,30 +187,6 @@ function layoutGraph(nodes, edges) {
152
187
  };
153
188
  }
154
189
 
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
- }
178
-
179
190
  // ../../apps/web/src/Viewer.tsx
180
191
  import {
181
192
  connectionWording as connectionWording4,
@@ -187,10 +198,10 @@ import {
187
198
  useEffect as useEffect4,
188
199
  useEffectEvent as useEffectEvent2,
189
200
  useId as useId2,
190
- useLayoutEffect,
201
+ useLayoutEffect as useLayoutEffect2,
191
202
  useMemo,
192
- useRef as useRef4,
193
- useState as useState2
203
+ useRef as useRef5,
204
+ useState as useState3
194
205
  } from "react";
195
206
  import Markdown2 from "react-markdown";
196
207
 
@@ -886,6 +897,133 @@ function neighbourhood(graph, key, nodeRadius2 = 43) {
886
897
  return { nodes, edges, continuations };
887
898
  }
888
899
 
900
+ // ../../apps/web/src/responsive-style.ts
901
+ import {
902
+ resolveNodePresentation
903
+ } from "@lensmap/document";
904
+ var conditionKey = (condition) => JSON.stringify([condition.kind, condition.query]);
905
+ function styleConditions(graph) {
906
+ const conditions = /* @__PURE__ */ new Map();
907
+ for (const node of graph.nodes)
908
+ for (const rule of node.style?.rules ?? [])
909
+ for (const condition of rule.conditions ?? [])
910
+ conditions.set(conditionKey(condition), condition);
911
+ return [...conditions].sort(([a], [b]) => a.localeCompare(b)).map(([, c]) => c);
912
+ }
913
+ var geometryKey = (graph) => JSON.stringify({
914
+ nodes: graph.nodes.map((node) => [
915
+ node.key,
916
+ positionAxis(node, "x"),
917
+ positionAxis(node, "y"),
918
+ node.baseRadius ?? 43
919
+ ]),
920
+ edges: graph.edges.map((edge) => [edge.key, edge.from, edge.to, edge.kind])
921
+ });
922
+ var placementKey = (graph) => JSON.stringify(graph.nodes.map((node) => [node.key, node.x, node.y]));
923
+ function resolveGraphPresentation(input, matches, measured = /* @__PURE__ */ new Map(), previous) {
924
+ const nodes = input.nodes.map((node) => {
925
+ const style = node.style?.rules ? resolveNodePresentation(node.style.rules, matches) : node.style;
926
+ const radius2 = (style?.scale === "auto" ? measured.get(node.key) : void 0) ?? baseRadius(style);
927
+ return style === node.style && radius2 === (node.baseRadius ?? 43) ? node : { ...node, style, baseRadius: radius2 };
928
+ });
929
+ const next = { nodes, edges: input.edges };
930
+ if (previous && placementKey(previous.input) === placementKey(input) && geometryKey(previous.graph) === geometryKey(next)) {
931
+ const points = new Map(
932
+ previous.graph.nodes.map((node) => [node.key, node])
933
+ );
934
+ return {
935
+ ...next,
936
+ nodes: nodes.map((node) => ({
937
+ ...node,
938
+ x: points.get(node.key)?.x ?? node.x,
939
+ y: points.get(node.key)?.y ?? node.y
940
+ }))
941
+ };
942
+ }
943
+ if (geometryKey(input) === geometryKey(next))
944
+ return nodes.every((node, i) => node === input.nodes[i]) ? input : next;
945
+ return layoutGraph(nodes, input.edges);
946
+ }
947
+ function createStyleEnvironment(conditions, changed) {
948
+ const media = /* @__PURE__ */ new Map();
949
+ const flags = /* @__PURE__ */ new Map();
950
+ const host = document.createElement("div");
951
+ host.setAttribute("aria-hidden", "true");
952
+ host.style.cssText = "position:fixed;left:-100000px;top:0;visibility:hidden;pointer-events:none;container-type:size;box-sizing:content-box;padding:0;border:0;min-width:0;max-width:none;min-height:0;max-height:none;width:0;height:0;";
953
+ const shadow = host.attachShadow({ mode: "open" });
954
+ const sheet = new CSSStyleSheet();
955
+ const sheetRules = [];
956
+ for (const [index, condition] of conditions.entries()) {
957
+ const key = conditionKey(condition);
958
+ if (condition.kind === "media") {
959
+ media.set(key, matchMedia(condition.query));
960
+ } else if (condition.kind === "container") {
961
+ const id = `query-${index}`;
962
+ const text = `@container ${condition.query}{#${id}{--lensmap-query-match:1}}`;
963
+ try {
964
+ const check = new CSSStyleSheet();
965
+ check.insertRule(text);
966
+ const outer = check.cssRules[0];
967
+ if (!(outer instanceof CSSContainerRule) || outer.containerName || outer.cssRules.length !== 1)
968
+ continue;
969
+ const rule = outer.cssRules[0];
970
+ if (!(rule instanceof CSSStyleRule) || rule.selectorText !== `#${id}` || rule.style.length !== 1 || rule.style.getPropertyValue("--lensmap-query-match") !== "1")
971
+ continue;
972
+ const flag = document.createElement("i");
973
+ flag.id = id;
974
+ shadow.append(flag);
975
+ flags.set(key, flag);
976
+ sheetRules.push(text);
977
+ } catch {
978
+ }
979
+ }
980
+ }
981
+ sheet.replaceSync(sheetRules.join("\n"));
982
+ shadow.adoptedStyleSheets = [sheet];
983
+ if (flags.size) document.body.append(host);
984
+ let previous = /* @__PURE__ */ new Map();
985
+ let context;
986
+ const refresh = () => {
987
+ if (context) {
988
+ const style = getComputedStyle(context);
989
+ host.style.font = style.font;
990
+ host.style.writingMode = style.writingMode;
991
+ }
992
+ const next = /* @__PURE__ */ new Map();
993
+ for (const condition of conditions) {
994
+ const key = conditionKey(condition);
995
+ const flag = flags.get(key);
996
+ next.set(
997
+ key,
998
+ media.get(key)?.matches ?? (flag ? getComputedStyle(flag).getPropertyValue("--lensmap-query-match").trim() === "1" : false)
999
+ );
1000
+ }
1001
+ if (JSON.stringify([...previous]) !== JSON.stringify([...next])) {
1002
+ previous = next;
1003
+ changed?.(next);
1004
+ }
1005
+ };
1006
+ for (const query of media.values()) query.addEventListener("change", refresh);
1007
+ window.addEventListener("resize", refresh);
1008
+ document.fonts.addEventListener("loadingdone", refresh);
1009
+ return {
1010
+ matches: (condition) => previous.get(conditionKey(condition)) ?? false,
1011
+ resize(width, height, element) {
1012
+ context = element;
1013
+ host.style.width = `${width}px`;
1014
+ host.style.height = `${height}px`;
1015
+ refresh();
1016
+ },
1017
+ dispose() {
1018
+ for (const query of media.values())
1019
+ query.removeEventListener("change", refresh);
1020
+ window.removeEventListener("resize", refresh);
1021
+ document.fonts.removeEventListener("loadingdone", refresh);
1022
+ host.remove();
1023
+ }
1024
+ };
1025
+ }
1026
+
889
1027
  // ../../apps/web/src/SidebarResizeHandle.tsx
890
1028
  import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
891
1029
  import { jsx as jsx4 } from "react/jsx-runtime";
@@ -1086,6 +1224,48 @@ function useMapKeyboard(ref, controls) {
1086
1224
  return { stop };
1087
1225
  }
1088
1226
 
1227
+ // ../../apps/web/src/useNodeStyleMatches.ts
1228
+ import { useLayoutEffect, useRef as useRef4, useState as useState2 } from "react";
1229
+ function useNodeStyleMatches(graph, canvas, size) {
1230
+ const conditions = styleConditions(graph);
1231
+ const signature = JSON.stringify(conditions);
1232
+ const environment = useRef4(null);
1233
+ const [matches, setMatches] = useState2(
1234
+ /* @__PURE__ */ new Map()
1235
+ );
1236
+ const refreshSize = () => {
1237
+ const element = canvas.current;
1238
+ if (!element || !environment.current) return;
1239
+ const style = getComputedStyle(element);
1240
+ environment.current.resize(
1241
+ element.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight),
1242
+ element.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom),
1243
+ element
1244
+ );
1245
+ };
1246
+ useLayoutEffect(() => {
1247
+ const conditions2 = JSON.parse(signature);
1248
+ if (!conditions2.length) {
1249
+ setMatches((previous) => previous.size ? /* @__PURE__ */ new Map() : previous);
1250
+ return;
1251
+ }
1252
+ const current = createStyleEnvironment(conditions2, setMatches);
1253
+ environment.current = current;
1254
+ refreshSize();
1255
+ return () => {
1256
+ current.dispose();
1257
+ if (environment.current === current) environment.current = null;
1258
+ };
1259
+ }, [signature]);
1260
+ useLayoutEffect(refreshSize, [size.width, size.height]);
1261
+ return {
1262
+ matches,
1263
+ ready: conditions.every(
1264
+ (condition) => matches.has(conditionKey(condition))
1265
+ )
1266
+ };
1267
+ }
1268
+
1089
1269
  // ../../apps/web/src/ViewerInspector.tsx
1090
1270
  import { thoughtAnchors } from "@lensmap/document";
1091
1271
  import Markdown from "react-markdown";
@@ -1248,41 +1428,46 @@ function Viewer({
1248
1428
  renderSidebar,
1249
1429
  renderExternalInspector
1250
1430
  }) {
1251
- const [measured, setMeasured] = useState2(
1431
+ const canvas = useRef5(null);
1432
+ const [size, setSize] = useState3({ width: 900, height: 640 });
1433
+ const { matches, ready: presentationReady } = useNodeStyleMatches(
1434
+ inputGraph,
1435
+ canvas,
1436
+ size
1437
+ );
1438
+ const [measured, setMeasured] = useState3(
1252
1439
  /* @__PURE__ */ new Map()
1253
1440
  );
1441
+ const previousPresentation = useRef5(null);
1254
1442
  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]);
1443
+ const resolved = resolveGraphPresentation(
1444
+ inputGraph,
1445
+ (condition) => matches.get(conditionKey(condition)) ?? false,
1446
+ measured,
1447
+ previousPresentation.current ?? void 0
1448
+ );
1449
+ previousPresentation.current = { input: inputGraph, graph: resolved };
1450
+ return resolved;
1451
+ }, [inputGraph, measured, matches]);
1265
1452
  const scope = useId2();
1266
1453
  const anchors = useMemo(() => thoughtAnchors2(graph.nodes), [graph.nodes]);
1267
1454
  const entryNode = graph.nodes.find((n) => n.key === initialTarget);
1268
1455
  const restoredNode = graph.nodes.find(
1269
1456
  (n) => n.key === initialState?.selected
1270
1457
  );
1271
- const canvas = useRef4(null);
1272
- const scene = useRef4(null);
1273
- const pendingActivation = useRef4(null);
1274
- const doubleCandidate = useRef4(null);
1458
+ const scene = useRef5(null);
1459
+ const pendingActivation = useRef5(null);
1460
+ const doubleCandidate = useRef5(null);
1275
1461
  const cancelActivation = useCallback2(() => {
1276
1462
  if (pendingActivation.current !== null)
1277
1463
  clearTimeout(pendingActivation.current);
1278
1464
  pendingActivation.current = null;
1279
1465
  doubleCandidate.current = null;
1280
1466
  }, []);
1281
- const drag = useRef4(null);
1282
- const suppressPointerClick = useRef4(false);
1283
- const [size, setSize] = useState2({ width: 900, height: 640 });
1284
- const [widths, setWidths] = useState2(/* @__PURE__ */ new Map());
1285
- useLayoutEffect(() => {
1467
+ const drag = useRef5(null);
1468
+ const suppressPointerClick = useRef5(false);
1469
+ const [widths, setWidths] = useState3(/* @__PURE__ */ new Map());
1470
+ useLayoutEffect2(() => {
1286
1471
  const root = canvas.current;
1287
1472
  if (!root) return;
1288
1473
  setWidths(
@@ -1299,38 +1484,38 @@ function Viewer({
1299
1484
  )
1300
1485
  );
1301
1486
  }, [graph, size]);
1302
- const [camera, setCamera] = useState2(
1487
+ const [camera, setCamera] = useState3(
1303
1488
  initialState?.camera ?? (entryNode ? { ...initialCamera, x: entryNode.x, y: entryNode.y } : initialCamera)
1304
1489
  );
1305
1490
  const { nodes, edges } = graph;
1306
1491
  const lanes = useMemo(() => edgeLanes(edges), [edges]);
1307
- const [selected, setSelected] = useState2(
1492
+ const [selected, setSelected] = useState3(
1308
1493
  restoredNode?.key ?? entryNode?.key ?? nodes[0]?.key ?? ""
1309
1494
  );
1310
- const [sidebarWidth, setSidebarWidth] = useState2(340);
1311
- const [strength, setStrength] = useState2(initialState?.strength ?? 1.1);
1312
- const [neighbourhoodStrength, setNeighbourhoodStrength] = useState2(
1495
+ const [sidebarWidth, setSidebarWidth] = useState3(340);
1496
+ const [strength, setStrength] = useState3(initialState?.strength ?? 1.1);
1497
+ const [neighbourhoodStrength, setNeighbourhoodStrength] = useState3(
1313
1498
  initialState?.neighbourhoodStrength ?? 1.1
1314
1499
  );
1315
- const [overview, setOverview] = useState2(
1500
+ const [overview, setOverview] = useState3(
1316
1501
  initialState?.overview ?? initialOverview ?? false
1317
1502
  );
1318
- const [inNeighbourhood, setInNeighbourhood] = useState2(
1503
+ const [inNeighbourhood, setInNeighbourhood] = useState3(
1319
1504
  !!restoredNode && (initialState?.inNeighbourhood ?? false)
1320
1505
  );
1321
- const [neighbourhoodCamera, setNeighbourhoodCamera] = useState2(
1506
+ const [neighbourhoodCamera, setNeighbourhoodCamera] = useState3(
1322
1507
  initialState?.neighbourhoodCamera ?? {
1323
1508
  x: 0,
1324
1509
  y: 0,
1325
1510
  zoom: 1
1326
1511
  }
1327
1512
  );
1328
- const [dragging, setDragging] = useState2(false);
1329
- const [message, setMessage] = useState2("");
1330
- const [keyboardTarget, setKeyboardTarget] = useState2();
1331
- const [keyboardActive, setKeyboardActive] = useState2(false);
1332
- const [maximumPanSpeed, setMaximumPanSpeed] = useState2(900);
1333
- const [connectionChoice, setConnectionChoice] = useState2();
1513
+ const [dragging, setDragging] = useState3(false);
1514
+ const [message, setMessage] = useState3("");
1515
+ const [keyboardTarget, setKeyboardTarget] = useState3();
1516
+ const [keyboardActive, setKeyboardActive] = useState3(false);
1517
+ const [maximumPanSpeed, setMaximumPanSpeed] = useState3(900);
1518
+ const [connectionChoice, setConnectionChoice] = useState3();
1334
1519
  const dismissConnectionChoice = useCallback2(() => {
1335
1520
  const focused = document.activeElement;
1336
1521
  if (focused instanceof Element && canvas.current?.contains(focused) && focused.closest("[data-connection-chooser]")) {
@@ -1338,7 +1523,7 @@ function Viewer({
1338
1523
  }
1339
1524
  setConnectionChoice(void 0);
1340
1525
  }, []);
1341
- const returnOverview = useRef4(initialState?.returnOverview ?? false);
1526
+ const returnOverview = useRef5(initialState?.returnOverview ?? false);
1342
1527
  const reportState = useEffectEvent2(
1343
1528
  () => onStateChange?.({
1344
1529
  selected,
@@ -1389,15 +1574,14 @@ function Viewer({
1389
1574
  [graph, inNeighbourhood, selected]
1390
1575
  );
1391
1576
  const measurementView = inNeighbourhood ? selected : null;
1392
- useLayoutEffect(() => {
1577
+ useLayoutEffect2(() => {
1393
1578
  let alive = true;
1394
1579
  const measure = () => {
1395
1580
  if (!alive || !canvas.current) return;
1396
1581
  const visible = /* @__PURE__ */ new Map();
1397
1582
  canvas.current.querySelectorAll(".node[data-auto-size='true']").forEach((element) => {
1398
1583
  const key = element.dataset.node;
1399
- if (!key || !inputGraph.nodes.some((node) => node.key === key))
1400
- return;
1584
+ if (!key || !graph.nodes.some((node) => node.key === key)) return;
1401
1585
  const clone = element.cloneNode(true);
1402
1586
  clone.removeAttribute("id");
1403
1587
  clone.classList.remove("selected", "connected");
@@ -1423,7 +1607,7 @@ function Viewer({
1423
1607
  });
1424
1608
  setMeasured((previous) => {
1425
1609
  const next = /* @__PURE__ */ new Map();
1426
- for (const node of inputGraph.nodes) {
1610
+ for (const node of graph.nodes) {
1427
1611
  if (node.style?.scale !== "auto") continue;
1428
1612
  const radius2 = visible.get(node.key) ?? previous.get(node.key);
1429
1613
  if (radius2 !== void 0) next.set(node.key, radius2);
@@ -1434,11 +1618,13 @@ function Viewer({
1434
1618
  measure();
1435
1619
  void document.fonts.ready.then(measure);
1436
1620
  document.fonts.addEventListener("loadingdone", measure);
1621
+ window.addEventListener("resize", measure);
1437
1622
  return () => {
1438
1623
  alive = false;
1439
1624
  document.fonts.removeEventListener("loadingdone", measure);
1625
+ window.removeEventListener("resize", measure);
1440
1626
  };
1441
- }, [inputGraph, measurementView]);
1627
+ }, [graph, measurementView, size.width, size.height]);
1442
1628
  useEffect4(() => cancelActivation, [cancelActivation]);
1443
1629
  useEffect4(() => {
1444
1630
  const element = canvas.current;
@@ -1685,8 +1871,9 @@ function Viewer({
1685
1871
  if (node) focusNode(node.key);
1686
1872
  else setMessage(`No thought has anchor "${id}".`);
1687
1873
  }
1688
- const previousGraph = useRef4(graph);
1689
- const lastRequest = useRef4(
1874
+ const previousGraph = useRef5(graph);
1875
+ const previousInput = useRef5(inputGraph);
1876
+ const lastRequest = useRef5(
1690
1877
  void 0
1691
1878
  );
1692
1879
  const focusedBeforeUpdate = typeof document === "undefined" ? null : document.activeElement;
@@ -1698,9 +1885,10 @@ function Viewer({
1698
1885
  graph,
1699
1886
  selected,
1700
1887
  camera,
1701
- updateMode
1888
+ previousInput.current === inputGraph ? "live" : updateMode
1702
1889
  );
1703
1890
  previousGraph.current = graph;
1891
+ previousInput.current = inputGraph;
1704
1892
  cancelActivation();
1705
1893
  keyboard.stop();
1706
1894
  stopDrag();
@@ -1712,11 +1900,20 @@ function Viewer({
1712
1900
  setInNeighbourhood(false);
1713
1901
  setMessage("");
1714
1902
  });
1715
- useLayoutEffect(() => {
1903
+ useLayoutEffect2(() => {
1716
1904
  reconcileGraph();
1717
1905
  if (hadViewerFocus && focusedBeforeUpdate && !focusedBeforeUpdate.isConnected)
1718
1906
  canvas.current?.focus({ preventScroll: true });
1719
1907
  });
1908
+ const initializedPresentation = useRef5(false);
1909
+ useLayoutEffect2(() => {
1910
+ if (!presentationReady || initializedPresentation.current) return;
1911
+ initializedPresentation.current = true;
1912
+ if (entryNode && !initialState)
1913
+ setCamera(
1914
+ (current) => current.x === entryNode.x && current.y === entryNode.y ? current : { ...current, x: entryNode.x, y: entryNode.y }
1915
+ );
1916
+ }, [presentationReady, entryNode, initialState]);
1720
1917
  const receiveFocus = useEffectEvent2(() => {
1721
1918
  if (!focusRequest || lastRequest.current?.key === focusRequest.key && lastRequest.current?.sequence === focusRequest.sequence)
1722
1919
  return;
@@ -1736,7 +1933,7 @@ function Viewer({
1736
1933
  setCamera((current) => ({ ...current, x: node.x, y: node.y }));
1737
1934
  setMessage("");
1738
1935
  });
1739
- useLayoutEffect(() => {
1936
+ useLayoutEffect2(() => {
1740
1937
  receiveFocus();
1741
1938
  });
1742
1939
  function stopDrag() {
@@ -2315,7 +2512,7 @@ function Viewer({
2315
2512
  }
2316
2513
 
2317
2514
  // src/SelectedThought.tsx
2318
- import { useId as useId3, useMemo as useMemo2, useState as useState3 } from "react";
2515
+ import { useId as useId3, useMemo as useMemo2, useState as useState4 } from "react";
2319
2516
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2320
2517
  function readingExcerpt(html) {
2321
2518
  const template = document.createElement("template");
@@ -2332,7 +2529,7 @@ function readingExcerpt(html) {
2332
2529
  }
2333
2530
  function SelectedThought({ html }) {
2334
2531
  const excerpt = useMemo2(() => readingExcerpt(html), [html]);
2335
- const [expanded, setExpanded] = useState3(false);
2532
+ const [expanded, setExpanded] = useState4(false);
2336
2533
  const fullId = useId3();
2337
2534
  if (!excerpt) {
2338
2535
  return /* @__PURE__ */ jsx8(
@@ -2430,8 +2627,8 @@ function ViewerHost({
2430
2627
  getLabel,
2431
2628
  mapId
2432
2629
  }) {
2433
- const [liveGraph, setLiveGraph] = useState4(graph);
2434
- const [stale, setStale] = useState4(false);
2630
+ const [liveGraph, setLiveGraph] = useState5(graph);
2631
+ const [stale, setStale] = useState5(false);
2435
2632
  useEffect5(() => setLiveGraph(graph), [graph]);
2436
2633
  useEffect5(() => {
2437
2634
  const hot = import.meta.hot;
@@ -2453,7 +2650,7 @@ function ViewerHost({
2453
2650
  hot.on("lensmap:vocabulary", update);
2454
2651
  return () => hot.off("lensmap:vocabulary", update);
2455
2652
  }, [mapId]);
2456
- const [targets, setTargets] = useState4([]);
2653
+ const [targets, setTargets] = useState5([]);
2457
2654
  useEffect5(() => {
2458
2655
  setTargets(
2459
2656
  [
package/dist/layout.js CHANGED
@@ -6,6 +6,15 @@ import {
6
6
  forceY
7
7
  } from "d3-force";
8
8
 
9
+ // ../../apps/web/src/node-style.ts
10
+ function positionAxis(node, axis) {
11
+ const value = node.style?.[axis];
12
+ return value === "auto" ? void 0 : value ?? node.position?.[axis];
13
+ }
14
+ function baseRadius(style) {
15
+ return 43 * (typeof style?.scale === "number" ? style.scale : 1);
16
+ }
17
+
9
18
  // ../../apps/web/src/document/subtree.ts
10
19
  import { polygonHull } from "d3-polygon";
11
20
  function silhouette(contour, distance, padding) {
@@ -85,16 +94,17 @@ function layoutGraph(nodes, edges) {
85
94
  }
86
95
  const plans = roots.map((root) => plan(root, true));
87
96
  function seed(node, subtree, point, heading, isRoot) {
88
- const actual = node.position ?? point;
89
- const fixed = Boolean(node.position) || isRoot;
97
+ const pinX = positionAxis(node, "x");
98
+ const pinY = positionAxis(node, "y");
99
+ const actual = { x: pinX ?? point.x, y: pinY ?? point.y };
90
100
  placed.set(node.key, {
91
101
  key: node.key,
92
102
  x: actual.x,
93
103
  y: actual.y,
94
104
  targetX: actual.x,
95
105
  targetY: actual.y,
96
- fx: fixed ? actual.x : void 0,
97
- fy: fixed ? actual.y : void 0
106
+ fx: pinX !== void 0 || isRoot ? actual.x : void 0,
107
+ fy: pinY !== void 0 || isRoot ? actual.y : void 0
98
108
  });
99
109
  subtree.children.forEach((placement) => {
100
110
  const child = byKey.get(placement.subtree.key);
@@ -107,12 +117,18 @@ function layoutGraph(nodes, edges) {
107
117
  x: actual.x + Math.cos(angle) * placement.distance,
108
118
  y: actual.y + Math.sin(angle) * placement.distance
109
119
  };
110
- const childPoint = child.position ?? target;
120
+ const childPoint = {
121
+ x: positionAxis(child, "x") ?? target.x,
122
+ y: positionAxis(child, "y") ?? target.y
123
+ };
111
124
  const direction = childPoint.x === actual.x && childPoint.y === actual.y ? angle : Math.atan2(childPoint.y - actual.y, childPoint.x - actual.x);
112
125
  seed(child, placement.subtree, target, direction, false);
113
126
  });
114
127
  }
115
- const anchor = nodes[0].position ?? { x: 0, y: 0 };
128
+ const anchor = {
129
+ x: positionAxis(nodes[0], "x") ?? 0,
130
+ y: positionAxis(nodes[0], "y") ?? 0
131
+ };
116
132
  const rootRadius = roots.length > 1 ? step * 2 / Math.sin(Math.PI / roots.length) : 0;
117
133
  roots.forEach((root, index) => {
118
134
  const angle = -Math.PI / 2 + Math.PI * 2 * index / roots.length;
@@ -148,17 +164,13 @@ function layoutGraph(nodes, edges) {
148
164
  };
149
165
  }
150
166
 
151
- // ../../apps/web/src/node-style.ts
152
- function baseRadius(style) {
153
- return 43 * (typeof style?.scale === "number" ? style.scale : 1);
154
- }
155
-
156
167
  // ../../apps/web/src/semantic-layout.ts
157
168
  var branches = ["blue", "amber", "mint", "rose"];
158
169
  var layoutKey = (graph) => JSON.stringify({
159
170
  nodes: graph.nodes.map((node) => [
160
171
  node.key,
161
- node.position,
172
+ positionAxis(node, "x"),
173
+ positionAxis(node, "y"),
162
174
  baseRadius(node.style)
163
175
  ]),
164
176
  edges: graph.edges.map((edge) => [edge.key, edge.from, edge.to, edge.kind])
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.0",
2
+ "version": "0.4.0",
3
3
  "type": "module",
4
4
  "license": "MIT",
5
5
  "files": [
@@ -33,8 +33,8 @@
33
33
  "./viewer.css": "./dist/ViewerHost.css"
34
34
  },
35
35
  "dependencies": {
36
- "@lensmap/astro-loader": "0.3.0",
37
- "@lensmap/document": "0.3.0",
36
+ "@lensmap/astro-loader": "0.4.0",
37
+ "@lensmap/document": "0.4.0",
38
38
  "d3-force": "3.0.1",
39
39
  "d3-polygon": "3.0.2",
40
40
  "react-markdown": "10.1.0"