structured-context-map-views 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -2
  3. package/dist/definition-KuPoA1mO.js +582 -0
  4. package/dist/definition-KuPoA1mO.js.map +1 -0
  5. package/dist/index.js +889 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/node.js +106 -0
  8. package/dist/node.js.map +1 -0
  9. package/dist/styles.css +497 -0
  10. package/dist/types/core/definition.d.ts +112 -0
  11. package/dist/types/core/graph.d.ts +16 -0
  12. package/dist/types/core/index.d.ts +6 -0
  13. package/dist/types/core/landscape.d.ts +44 -0
  14. package/dist/types/core/occurrence.d.ts +11 -0
  15. package/dist/types/core/schemas/definition.d.ts +163 -0
  16. package/dist/types/core/schemas/index.d.ts +418 -0
  17. package/dist/types/core/schemas/landscape-layout.d.ts +102 -0
  18. package/dist/types/core/schemas/param-ref.d.ts +10 -0
  19. package/dist/types/core/schemas/schema-url.d.ts +7 -0
  20. package/dist/types/core/schemas/settings.d.ts +27 -0
  21. package/dist/types/core/schemas/tree-layout.d.ts +40 -0
  22. package/dist/types/core/schemas/xy-layout.d.ts +94 -0
  23. package/dist/types/core/tree.d.ts +45 -0
  24. package/dist/types/core/xy.d.ts +43 -0
  25. package/dist/types/index.d.ts +7 -0
  26. package/dist/types/layouts/EdgeOverlay.d.ts +36 -0
  27. package/dist/types/layouts/LandscapeLayout.d.ts +8 -0
  28. package/dist/types/layouts/MapView.d.ts +12 -0
  29. package/dist/types/layouts/NodeCard.d.ts +16 -0
  30. package/dist/types/layouts/ParameterControls.d.ts +13 -0
  31. package/dist/types/layouts/TreeLayout.d.ts +16 -0
  32. package/dist/types/layouts/XYLayout.d.ts +9 -0
  33. package/dist/types/node/bundled-definitions.d.ts +1 -0
  34. package/dist/types/node/index.d.ts +17 -0
  35. package/package.json +69 -4
package/dist/index.js ADDED
@@ -0,0 +1,889 @@
1
+ import { a as resolveRegions, i as resolveParameterValues, n as mapViewsForSchema, o as selectionFilter, r as parseMapViewDefinition, s as MAP_VIEW_SCHEMAS, t as MapViewDefinitionError } from "./definition-KuPoA1mO.js";
2
+ import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/core/graph.ts
5
+ var NONE = [];
6
+ /**
7
+ * Edges to a node's parents, in the order the node declares them. A graph narrowed by a filter keeps
8
+ * edges to parents outside it; those are left out.
9
+ */
10
+ function parentEdges(graph, id) {
11
+ return (graph.parentEdges.get(id) ?? NONE).filter((edge) => graph.nodes.has(edge.parent));
12
+ }
13
+ /** Edges to a node's children, in graph order of the children. */
14
+ function childEdges(graph, id) {
15
+ return graph.childEdges.get(id) ?? NONE;
16
+ }
17
+ /** Every edge whose parent and child are both in the graph. */
18
+ function edgesWithin(graph) {
19
+ return [...graph.nodes.keys()].flatMap((id) => parentEdges(graph, id));
20
+ }
21
+ /** Whether an edge belongs to the given hierarchy (as opposed to a relationship or another hierarchy). */
22
+ function isHierarchyEdge(edge, hierarchy) {
23
+ return hierarchy !== void 0 && edge.kind === "hierarchy" && edge.hierarchy === hierarchy.name;
24
+ }
25
+ //#endregion
26
+ //#region src/core/occurrence.ts
27
+ /**
28
+ * Path-based key for one occurrence of a node: region, then the steps from the outermost
29
+ * container down to the node. Unique within a map view even when a node appears several times.
30
+ * Hosts can render several map views at once, so layouts scope keys to the map view instance.
31
+ */
32
+ function occurrenceKey(regionId, path) {
33
+ return JSON.stringify([regionId, ...path]);
34
+ }
35
+ //#endregion
36
+ //#region src/core/landscape.ts
37
+ /**
38
+ * Lay a space out as nested containers following the layout's grouping levels.
39
+ *
40
+ * Adjacent node levels are joined by any edge between their types, in either direction, or only
41
+ * edges from `edge.field`. A `via` chain joins them through the skipped types. A field level
42
+ * splits the next node level by that field's values. A node reached from several containers
43
+ * appears in each, in full: the levels are finite, so repeating contents can't recurse.
44
+ */
45
+ function buildLandscape(graph, regionId, layout) {
46
+ const levels = layout.group;
47
+ const order = new Map([...graph.nodes.keys()].map((title, i) => [title, i]));
48
+ const first = /* @__PURE__ */ new Map();
49
+ const occurrenceFor = (candidate, levelIdx, path) => {
50
+ const { node } = candidate;
51
+ const nodePath = [...path, node.id];
52
+ const key = occurrenceKey(regionId, nodePath);
53
+ const repeatOf = first.get(node.id);
54
+ if (repeatOf === void 0) first.set(node.id, key);
55
+ return {
56
+ key,
57
+ level: levelIdx,
58
+ item: {
59
+ kind: "node",
60
+ node
61
+ },
62
+ via: candidate.via,
63
+ ...repeatOf !== void 0 && { repeatOf },
64
+ children: place(levelIdx + 1, node, [], nodePath)
65
+ };
66
+ };
67
+ /** Occurrences for level `levelIdx` inside `anchor`, the nearest enclosing node. */
68
+ const place = (levelIdx, anchor, filters, path) => {
69
+ const level = levels[levelIdx];
70
+ if (level === void 0) return [];
71
+ if (isTypeLevel(level)) return candidates(level, anchor, filters).map((c) => occurrenceFor(c, levelIdx, path));
72
+ const nodeLevel = levels.slice(levelIdx).find(isTypeLevel);
73
+ return distinctValues(candidates(nodeLevel, anchor, filters).map((c) => c.node), level.field).map((value) => {
74
+ const valuePath = [...path, [level.field, value]];
75
+ return {
76
+ key: occurrenceKey(regionId, valuePath),
77
+ level: levelIdx,
78
+ item: {
79
+ kind: "value",
80
+ field: level.field,
81
+ value
82
+ },
83
+ via: [],
84
+ children: place(levelIdx + 1, anchor, [...filters, {
85
+ field: level.field,
86
+ value
87
+ }], valuePath)
88
+ };
89
+ });
90
+ };
91
+ /** Nodes of a type level joined to `anchor` (all of the type at the top) and matching the filters. */
92
+ const candidates = (level, anchor, filters) => {
93
+ return (anchor === void 0 ? ofType(level.type).map((node) => ({
94
+ node,
95
+ via: []
96
+ })) : walkChain(anchor, level.via ?? [], level.type, level.edge?.field)).filter(({ node }) => filters.every((f) => hasValue(node, f)));
97
+ };
98
+ const ofType = (type) => [...graph.nodes.values()].filter((node) => node.resolvedType === type);
99
+ /** Neighbours of a node with the given type, across edges in either direction, in space order. */
100
+ const neighbours = (title, type, field) => {
101
+ return [.../* @__PURE__ */ new Set([...childEdges(graph, title).filter((e) => field === void 0 || e.field === field).map((e) => e.child), ...parentEdges(graph, title).filter((e) => field === void 0 || e.field === field).map((e) => e.parent)])].map((t) => graph.nodes.get(t)).filter((node) => node.resolvedType === type).sort((a, b) => order.get(a.id) - order.get(b.id));
102
+ };
103
+ /**
104
+ * Follow edges from `anchor` through each `via` type to `target`, keeping the skipped nodes on
105
+ * the way to each target node. Several chains to one target merge into one candidate.
106
+ */
107
+ const walkChain = (anchor, via, target, field) => {
108
+ let frontier = [{
109
+ node: anchor,
110
+ trail: []
111
+ }];
112
+ for (const type of via) frontier = frontier.flatMap(({ node, trail }) => neighbours(node.id, type, void 0).map((next) => ({
113
+ node: next,
114
+ trail: [...trail, next]
115
+ })));
116
+ const byTitle = /* @__PURE__ */ new Map();
117
+ for (const { node, trail } of frontier) for (const next of neighbours(node.id, target, field)) {
118
+ const candidate = byTitle.get(next.id) ?? {
119
+ node: next,
120
+ via: via.map((type) => ({
121
+ type,
122
+ nodes: []
123
+ }))
124
+ };
125
+ trail.forEach((skipped, i) => {
126
+ const step = candidate.via[i];
127
+ if (!step.nodes.includes(skipped)) step.nodes.push(skipped);
128
+ });
129
+ byTitle.set(next.id, candidate);
130
+ }
131
+ return [...byTitle.values()].sort((a, b) => order.get(a.node.id) - order.get(b.node.id));
132
+ };
133
+ const groups = place(0, void 0, [], []);
134
+ const ungrouped = [];
135
+ levels.forEach((level, levelIdx) => {
136
+ if (levelIdx === 0 || !isTypeLevel(level)) return;
137
+ for (const node of ofType(level.type)) if (!first.has(node.id)) ungrouped.push(occurrenceFor({
138
+ node,
139
+ via: []
140
+ }, levelIdx, [null]));
141
+ });
142
+ return {
143
+ groups,
144
+ ungrouped
145
+ };
146
+ }
147
+ function isTypeLevel(level) {
148
+ return "type" in level;
149
+ }
150
+ /** A field's values as group labels: strings, an entry per array item, `null` when empty. */
151
+ function valuesOf(node, field) {
152
+ const raw = node.schemaData[field];
153
+ const list = (Array.isArray(raw) ? raw : [raw]).filter((v) => v !== void 0 && v !== null && v !== "");
154
+ return list.length === 0 ? [null] : list.map((v) => typeof v === "object" ? JSON.stringify(v) : String(v));
155
+ }
156
+ /** Distinct values in first-seen order, with the empty group last. */
157
+ function distinctValues(nodes, field) {
158
+ const values = new Set(nodes.flatMap((node) => valuesOf(node, field)));
159
+ const present = [...values].filter((v) => v !== null);
160
+ return values.has(null) ? [...present, null] : present;
161
+ }
162
+ function hasValue(node, { field, value }) {
163
+ return valuesOf(node, field).includes(value);
164
+ }
165
+ //#endregion
166
+ //#region src/core/tree.ts
167
+ /**
168
+ * Lay a space out as a tree over its hierarchy.
169
+ *
170
+ * Roots are root-type nodes with no hierarchy parent. Traversal is depth-first in space order,
171
+ * so a node's primary occurrence is under the parent reached first. Under its other parents a
172
+ * node is either repeated without its subtree (`repeat`), or left out and linked by a cross-edge
173
+ * (`cross-edge`). Either way traversal stops there, which also keeps cycles from recursing.
174
+ */
175
+ function buildTree(graph, regionId, dag = "repeat") {
176
+ const { hierarchy } = graph;
177
+ const levelTypes = new Set(hierarchy?.levels.map((level) => level.type));
178
+ const rootType = hierarchy?.levels[0]?.type;
179
+ const primary = /* @__PURE__ */ new Map();
180
+ const pendingCross = [];
181
+ const hierarchyParents = (title) => parentEdges(graph, title).filter((edge) => isHierarchyEdge(edge, hierarchy));
182
+ const visit = (node, path) => {
183
+ const nodePath = [...path, node.id];
184
+ const key = occurrenceKey(regionId, nodePath);
185
+ primary.set(node.id, key);
186
+ const childTitles = new Set(childEdges(graph, node.id).filter((edge) => isHierarchyEdge(edge, hierarchy)).map((edge) => edge.child));
187
+ const children = [];
188
+ for (const title of childTitles) {
189
+ const child = graph.nodes.get(title);
190
+ const repeatOf = primary.get(title);
191
+ if (repeatOf === void 0) children.push(visit(child, nodePath));
192
+ else if (dag === "repeat") children.push({
193
+ key: occurrenceKey(regionId, [...nodePath, title]),
194
+ node: child,
195
+ children: [],
196
+ repeatOf
197
+ });
198
+ else pendingCross.push({
199
+ from: key,
200
+ child: title
201
+ });
202
+ }
203
+ return {
204
+ key,
205
+ node,
206
+ children
207
+ };
208
+ };
209
+ const levelNodes = [...graph.nodes.values()].filter((node) => levelTypes.has(node.resolvedType));
210
+ const unparented = levelNodes.filter((node) => hierarchyParents(node.id).length === 0);
211
+ const roots = unparented.filter((node) => node.resolvedType === rootType).map((node) => visit(node, []));
212
+ const orphans = unparented.filter((node) => node.resolvedType !== rootType).map((node) => visit(node, []));
213
+ for (const node of levelNodes) if (!primary.has(node.id)) orphans.push(visit(node, []));
214
+ return {
215
+ roots,
216
+ orphans,
217
+ primary,
218
+ crossLinks: pendingCross.map(({ from, child }) => ({
219
+ from,
220
+ to: primary.get(child),
221
+ kind: "cross"
222
+ }))
223
+ };
224
+ }
225
+ /** Parent → child pairs of occurrence keys: one per rendered parent-child link, then cross-links. */
226
+ function treeLinks(tree) {
227
+ const links = [];
228
+ const walk = (occurrence) => {
229
+ for (const child of occurrence.children) {
230
+ links.push({
231
+ from: occurrence.key,
232
+ to: child.key,
233
+ kind: "tree"
234
+ });
235
+ walk(child);
236
+ }
237
+ };
238
+ for (const root of [...tree.roots, ...tree.orphans]) walk(root);
239
+ return [...links, ...tree.crossLinks];
240
+ }
241
+ //#endregion
242
+ //#region src/core/xy.ts
243
+ /**
244
+ * Position nodes by two numeric fields. Every node of a plotted type appears once: on the plot
245
+ * when both its values are numbers inside the axes' domains, otherwise in `unplaced` with the
246
+ * reason. Positions are normalised; layouts decide the plot's size.
247
+ */
248
+ function buildXYPlot(graph, regionId, layout) {
249
+ const { x, y } = layout;
250
+ const nodes = [...graph.nodes.values()];
251
+ const types = new Set(layout.types.length > 0 ? layout.types : nodes.filter((n) => isNumber(n.schemaData[x.field]) || isNumber(n.schemaData[y.field])).map((n) => n.resolvedType));
252
+ const plotted = nodes.filter((n) => types.has(n.resolvedType));
253
+ const domains = {
254
+ x: domainOf(x, plotted),
255
+ y: domainOf(y, plotted)
256
+ };
257
+ const points = [];
258
+ const unplaced = [];
259
+ for (const node of plotted) {
260
+ const key = occurrenceKey(regionId, [node.id]);
261
+ const px = position(node, x, domains.x);
262
+ const py = position(node, y, domains.y);
263
+ if (typeof px === "number" && typeof py === "number") points.push({
264
+ key,
265
+ node,
266
+ x: px,
267
+ y: py
268
+ });
269
+ else unplaced.push({
270
+ key,
271
+ node,
272
+ reasons: [px, py].filter((r) => typeof r === "string")
273
+ });
274
+ }
275
+ const keys = new Map(points.map((p) => [p.node.id, p.key]));
276
+ const fields = new Set(layout.edgeFields);
277
+ const links = edgesWithin(graph).filter((e) => keys.has(e.parent) && keys.has(e.child) && (fields.size === 0 || fields.has(e.field))).map((e) => ({
278
+ from: keys.get(e.parent),
279
+ to: keys.get(e.child),
280
+ field: e.field,
281
+ hierarchy: isHierarchyEdge(e, graph.hierarchy),
282
+ lane: 0
283
+ }));
284
+ assignLanes(links);
285
+ return {
286
+ points,
287
+ unplaced,
288
+ links,
289
+ domains
290
+ };
291
+ }
292
+ function isNumber(value) {
293
+ return typeof value === "number" && Number.isFinite(value);
294
+ }
295
+ /** The axis's fixed domain, or the range of plotted values (widened when they're all equal). */
296
+ function domainOf(axis, nodes) {
297
+ if (Array.isArray(axis.domain)) return [axis.domain[0], axis.domain[1]];
298
+ const values = nodes.map((n) => n.schemaData[axis.field]).filter(isNumber);
299
+ if (values.length === 0) return [0, 1];
300
+ const min = Math.min(...values);
301
+ const max = Math.max(...values);
302
+ return min === max ? [min - 1, max + 1] : [min, max];
303
+ }
304
+ /** The node's normalised position on the axis, or why it has none. */
305
+ function position(node, axis, [min, max]) {
306
+ const value = node.schemaData[axis.field];
307
+ if (value === void 0 || value === null) return `no ${axis.field}`;
308
+ if (!isNumber(value)) return `${axis.field} is not a number`;
309
+ if (value < min || value > max) return `${axis.field} ${value} is outside ${min}–${max}`;
310
+ const t = (value - min) / (max - min);
311
+ return axis.direction === "descending" ? 1 - t : t;
312
+ }
313
+ /** Spread links joining the same pair of nodes (in either direction) into lanes around 0. */
314
+ function assignLanes(links) {
315
+ const byPair = /* @__PURE__ */ new Map();
316
+ for (const link of links) {
317
+ const pair = [link.from, link.to].sort().join("\n");
318
+ byPair.set(pair, [...byPair.get(pair) ?? [], link]);
319
+ }
320
+ for (const group of byPair.values()) group.forEach((link, i) => {
321
+ link.lane = i - (group.length - 1) / 2;
322
+ });
323
+ }
324
+ //#endregion
325
+ //#region src/layouts/NodeCard.tsx
326
+ /**
327
+ * A clickable node, drawn as a card (title plus selected fields) or a compact shape with a
328
+ * tooltip. Deliberately a `div` with button semantics rather than a `<button>`: hosts style
329
+ * native form controls globally (Obsidian fixes button height and disables wrapping).
330
+ */
331
+ function NodeCard({ node, repeat, nodes, onSelect }) {
332
+ const select = () => onSelect?.(node);
333
+ const label = `${node.resolvedType}: ${node.title}${repeat ? " (shown in full elsewhere)" : ""}`;
334
+ const onKeyDown = (e) => {
335
+ if (e.key === "Enter" || e.key === " ") {
336
+ e.preventDefault();
337
+ select();
338
+ }
339
+ };
340
+ const border = repeat ? "mv:border-dashed mv:border-muted/60 mv:opacity-75" : "mv:border-edge";
341
+ if (nodes.style === "shape") return /* @__PURE__ */ jsx("div", {
342
+ role: "button",
343
+ tabIndex: 0,
344
+ title: label,
345
+ onClick: select,
346
+ onKeyDown,
347
+ "aria-label": label,
348
+ className: `mv:flex mv:size-6 mv:cursor-pointer mv:items-center mv:justify-center mv:rounded-full mv:border-2 mv:bg-card mv:text-[0.65rem] mv:font-semibold mv:uppercase mv:text-muted mv:hover:border-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent ${border}`,
349
+ children: /* @__PURE__ */ jsx("span", {
350
+ "aria-hidden": "true",
351
+ children: node.resolvedType.charAt(0)
352
+ })
353
+ });
354
+ const fields = nodes.fields.map((field) => [field, formatValue(node.schemaData[field])]).filter(([, value]) => value !== void 0);
355
+ return /* @__PURE__ */ jsxs("div", {
356
+ role: "button",
357
+ tabIndex: 0,
358
+ title: label,
359
+ onClick: select,
360
+ onKeyDown,
361
+ className: `mv:w-44 mv:cursor-pointer mv:rounded-md mv:border mv:bg-card mv:px-2.5 mv:py-1.5 mv:text-left mv:shadow-sm mv:hover:border-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent ${border}`,
362
+ children: [
363
+ /* @__PURE__ */ jsxs("span", {
364
+ className: "mv:block mv:text-[0.65rem] mv:uppercase mv:tracking-wide mv:text-muted",
365
+ children: [node.resolvedType, repeat && " · repeat"]
366
+ }),
367
+ /* @__PURE__ */ jsx("span", {
368
+ className: "mv:block mv:text-sm mv:leading-snug",
369
+ children: node.title
370
+ }),
371
+ fields.length > 0 && /* @__PURE__ */ jsx("dl", {
372
+ className: "mv:m-0 mv:mt-1 mv:text-xs mv:text-muted",
373
+ children: fields.map(([field, value]) => /* @__PURE__ */ jsxs("div", {
374
+ className: "mv:flex mv:gap-1",
375
+ children: [/* @__PURE__ */ jsxs("dt", {
376
+ className: "mv:shrink-0",
377
+ children: [field, ":"]
378
+ }), /* @__PURE__ */ jsx("dd", {
379
+ className: "mv:m-0 mv:truncate mv:text-ink",
380
+ children: value
381
+ })]
382
+ }, field))
383
+ })
384
+ ]
385
+ });
386
+ }
387
+ function formatValue(value) {
388
+ if (value === void 0 || value === null || value === "") return void 0;
389
+ if (Array.isArray(value)) return value.map(String).join(", ");
390
+ if (typeof value === "object") return JSON.stringify(value);
391
+ return String(value);
392
+ }
393
+ //#endregion
394
+ //#region src/layouts/LandscapeLayout.tsx
395
+ /**
396
+ * Nested containers following the layout's grouping levels; the last level is drawn as nodes.
397
+ * Flexbox does the nesting and wrapping. Containment shows the grouping edges, so no connectors
398
+ * are drawn. PROTOTYPE: `edges.show` has no effect here; other edges aren't drawn yet.
399
+ */
400
+ function LandscapeLayout({ graph, region, instanceId, onNodeSelect }) {
401
+ const { layout } = region;
402
+ const landscape = useMemo(() => buildLandscape(graph, region.id, layout), [
403
+ graph,
404
+ region.id,
405
+ layout
406
+ ]);
407
+ const last = layout.group.length - 1;
408
+ const breadcrumb = (occurrence) => {
409
+ const level = layout.group[occurrence.level];
410
+ if (!level || !("type" in level) || level.intermediate !== "breadcrumb") return null;
411
+ const steps = occurrence.via.filter((step) => step.nodes.length > 0);
412
+ if (steps.length === 0) return null;
413
+ return /* @__PURE__ */ jsxs("span", {
414
+ className: "mv:block mv:max-w-44 mv:text-[0.65rem] mv:leading-tight mv:text-muted",
415
+ children: ["via ", steps.map((step) => step.nodes.map((n) => n.title).join(", ")).join(" › ")]
416
+ });
417
+ };
418
+ const render = (occurrence, direction) => {
419
+ const repeat = occurrence.repeatOf !== void 0;
420
+ const marker = { "data-occurrence": `${instanceId}${occurrence.key}` };
421
+ if (occurrence.item.kind === "node" && occurrence.level === last) return /* @__PURE__ */ jsxs("li", {
422
+ ...marker,
423
+ className: "mv:flex mv:flex-col mv:gap-0.5",
424
+ children: [/* @__PURE__ */ jsx(NodeCard, {
425
+ node: occurrence.item.node,
426
+ repeat,
427
+ nodes: region.nodes,
428
+ onSelect: onNodeSelect
429
+ }), breadcrumb(occurrence)]
430
+ }, occurrence.key);
431
+ const inner = direction === "horizontal" ? "vertical" : "horizontal";
432
+ const holdsNodes = occurrence.level + 1 === last;
433
+ return /* @__PURE__ */ jsxs("li", {
434
+ ...marker,
435
+ className: `mv:flex mv:flex-col mv:gap-2 mv:rounded-lg mv:border mv:bg-surface mv:p-2 ${repeat ? "mv:border-dashed mv:border-muted/60" : "mv:border-edge"}`,
436
+ children: [occurrence.item.kind === "node" ? /* @__PURE__ */ jsx(ContainerHeading, {
437
+ node: occurrence.item.node,
438
+ repeat,
439
+ onSelect: onNodeSelect,
440
+ children: breadcrumb(occurrence)
441
+ }) : /* @__PURE__ */ jsxs("span", {
442
+ className: "mv:text-xs mv:font-semibold mv:text-muted",
443
+ children: [
444
+ occurrence.item.field,
445
+ ": ",
446
+ occurrence.item.value === null ? "(none)" : displayValue(occurrence.item.value)
447
+ ]
448
+ }), /* @__PURE__ */ jsx(Items, {
449
+ direction: holdsNodes ? "wrap" : inner,
450
+ children: occurrence.children.map((child) => render(child, inner))
451
+ })]
452
+ }, occurrence.key);
453
+ };
454
+ const top = layout.orientation;
455
+ return /* @__PURE__ */ jsx("div", {
456
+ className: "mv:w-max mv:min-w-full mv:p-4",
457
+ children: /* @__PURE__ */ jsxs(Items, {
458
+ direction: last === 0 ? "wrap" : top,
459
+ children: [landscape.groups.map((group) => render(group, top)), landscape.ungrouped.length > 0 && /* @__PURE__ */ jsxs("li", {
460
+ className: "mv:flex mv:flex-col mv:gap-2 mv:rounded-lg mv:border mv:border-dashed mv:border-edge mv:p-2",
461
+ children: [/* @__PURE__ */ jsx("span", {
462
+ className: "mv:text-xs mv:font-semibold mv:text-muted",
463
+ children: "Ungrouped"
464
+ }), /* @__PURE__ */ jsx(Items, {
465
+ direction: "wrap",
466
+ children: landscape.ungrouped.map((o) => render(o, top))
467
+ })]
468
+ })]
469
+ })
470
+ });
471
+ }
472
+ function Items({ direction, children }) {
473
+ const layout = {
474
+ horizontal: "mv:flex-row mv:items-start mv:gap-3",
475
+ vertical: "mv:flex-col mv:items-stretch mv:gap-2",
476
+ wrap: "mv:max-w-[36rem] mv:flex-row mv:flex-wrap mv:items-start mv:gap-2"
477
+ }[direction];
478
+ return /* @__PURE__ */ jsx("ul", {
479
+ className: `mv:m-0 mv:flex mv:list-none mv:p-0 ${layout}`,
480
+ children
481
+ });
482
+ }
483
+ /** A container's node: type and title, selectable like a card. */
484
+ function ContainerHeading({ node, repeat, onSelect, children }) {
485
+ const select = () => onSelect?.(node);
486
+ return /* @__PURE__ */ jsxs("div", {
487
+ role: "button",
488
+ tabIndex: 0,
489
+ title: `${node.resolvedType}: ${node.title}${repeat ? " (also shown elsewhere)" : ""}`,
490
+ onClick: select,
491
+ onKeyDown: (e) => {
492
+ if (e.key === "Enter" || e.key === " ") {
493
+ e.preventDefault();
494
+ select();
495
+ }
496
+ },
497
+ className: "mv:cursor-pointer mv:rounded mv:px-1 mv:hover:text-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent",
498
+ children: [
499
+ /* @__PURE__ */ jsxs("span", {
500
+ className: "mv:block mv:text-[0.65rem] mv:uppercase mv:tracking-wide mv:text-muted",
501
+ children: [node.resolvedType, repeat && " · repeat"]
502
+ }),
503
+ /* @__PURE__ */ jsx("span", {
504
+ className: "mv:block mv:text-sm mv:font-semibold mv:leading-snug",
505
+ children: node.title
506
+ }),
507
+ children
508
+ ]
509
+ });
510
+ }
511
+ /** Field values that are wikilinks show as the linked title. */
512
+ function displayValue(value) {
513
+ return value.replace(/^\[\[([^\]|]+)(\|[^\]]*)?\]\]$/, "$1");
514
+ }
515
+ //#endregion
516
+ //#region src/layouts/EdgeOverlay.tsx
517
+ /**
518
+ * SVG layer drawing connectors between rendered occurrences.
519
+ *
520
+ * Measures occurrence elements after layout and redraws when the container or any occurrence
521
+ * resizes, so it follows embedded panes that resize independently of the window, and reflows
522
+ * such as late font loads. The SVG fills the container instead of being sized from it, so it
523
+ * never holds the container open after the layout shrinks.
524
+ */
525
+ function EdgeOverlay({ links, elements, container, routing }) {
526
+ const [paths, setPaths] = useState([]);
527
+ useLayoutEffect(() => {
528
+ if (!container) return;
529
+ const measure = () => {
530
+ const origin = container.getBoundingClientRect();
531
+ const paths = [];
532
+ for (const { from, to, kind, id, lane = 0 } of links) {
533
+ const a = elements.get(from)?.getBoundingClientRect();
534
+ const b = elements.get(to)?.getBoundingClientRect();
535
+ if (!a || !b) continue;
536
+ paths.push({
537
+ key: `${from}→${to}${id ?? ""}`,
538
+ d: routing === "straight" ? line(a, b, origin, lane) : connector(a, b, origin, routing),
539
+ cross: kind === "cross"
540
+ });
541
+ }
542
+ setPaths(paths);
543
+ };
544
+ measure();
545
+ const observer = new ResizeObserver(measure);
546
+ observer.observe(container);
547
+ for (const element of elements.values()) observer.observe(element);
548
+ return () => observer.disconnect();
549
+ }, [
550
+ links,
551
+ elements,
552
+ container,
553
+ routing
554
+ ]);
555
+ return /* @__PURE__ */ jsx("svg", {
556
+ "aria-hidden": "true",
557
+ className: "mv:pointer-events-none mv:absolute mv:inset-0 mv:h-full mv:w-full mv:overflow-visible",
558
+ children: paths.map(({ key, d, cross }) => /* @__PURE__ */ jsx("path", {
559
+ d,
560
+ className: `mv:fill-none ${cross ? "mv:stroke-accent/60" : "mv:stroke-muted/50"}`,
561
+ strokeWidth: 1.5,
562
+ strokeDasharray: cross ? "4 3" : void 0
563
+ }, key))
564
+ });
565
+ }
566
+ /** Cubic curve from the parent's trailing edge to the child's leading edge. */
567
+ function connector(a, b, origin, orientation) {
568
+ if (orientation === "vertical") {
569
+ const x1 = a.left + a.width / 2 - origin.left;
570
+ const y1 = a.bottom - origin.top;
571
+ const x2 = b.left + b.width / 2 - origin.left;
572
+ const y2 = b.top - origin.top;
573
+ const my = (y1 + y2) / 2;
574
+ return `M${x1},${y1} C${x1},${my} ${x2},${my} ${x2},${y2}`;
575
+ }
576
+ const x1 = a.right - origin.left;
577
+ const y1 = a.top + a.height / 2 - origin.top;
578
+ const x2 = b.left - origin.left;
579
+ const y2 = b.top + b.height / 2 - origin.top;
580
+ const mx = (x1 + x2) / 2;
581
+ return `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`;
582
+ }
583
+ /** Pixels between parallel straight links. */
584
+ var LANE_GAP = 6;
585
+ /**
586
+ * Straight line between centres, shifted sideways by its lane; the layout draws nodes over the
587
+ * overlay to hide the ends. The shift is relative to a fixed orientation of the pair, so links in
588
+ * opposite directions between the same nodes still separate.
589
+ */
590
+ function line(a, b, origin, lane) {
591
+ const x1 = a.left + a.width / 2 - origin.left;
592
+ const y1 = a.top + a.height / 2 - origin.top;
593
+ const x2 = b.left + b.width / 2 - origin.left;
594
+ const y2 = b.top + b.height / 2 - origin.top;
595
+ const flip = x1 < x2 || x1 === x2 && y1 < y2 ? 1 : -1;
596
+ const length = Math.hypot(x2 - x1, y2 - y1) || 1;
597
+ const dx = -(y2 - y1) / length * lane * LANE_GAP * flip;
598
+ const dy = (x2 - x1) / length * lane * LANE_GAP * flip;
599
+ return `M${x1 + dx},${y1 + dy} L${x2 + dx},${y2 + dy}`;
600
+ }
601
+ //#endregion
602
+ //#region src/layouts/TreeLayout.tsx
603
+ /**
604
+ * Tree over the schema's hierarchy. Flexbox does the branching; the SVG overlay draws connectors.
605
+ */
606
+ function TreeLayout({ graph, region, instanceId, onNodeSelect }) {
607
+ const { orientation, dag } = region.layout;
608
+ const tree = useMemo(() => buildTree(graph, region.id, dag), [
609
+ graph,
610
+ region.id,
611
+ dag
612
+ ]);
613
+ const links = useMemo(() => region.edges.show ? treeLinks(tree) : [], [tree, region.edges.show]);
614
+ const [container, setContainer] = useState(null);
615
+ const elements = useRef(/* @__PURE__ */ new Map()).current;
616
+ const register = useCallback((key) => (el) => {
617
+ if (el) elements.set(key, el);
618
+ else elements.delete(key);
619
+ }, [elements]);
620
+ const branch = (occurrence) => /* @__PURE__ */ jsxs("li", {
621
+ className: `mv:flex mv:items-center ${orientation === "vertical" ? "mv:flex-col" : "mv:flex-row"}`,
622
+ children: [/* @__PURE__ */ jsx("div", {
623
+ ref: register(occurrence.key),
624
+ "data-occurrence": `${instanceId}${occurrence.key}`,
625
+ children: /* @__PURE__ */ jsx(NodeCard, {
626
+ node: occurrence.node,
627
+ repeat: occurrence.repeatOf !== void 0,
628
+ nodes: region.nodes,
629
+ onSelect: onNodeSelect
630
+ })
631
+ }), occurrence.children.length > 0 && /* @__PURE__ */ jsx(Branches, {
632
+ orientation,
633
+ nested: true,
634
+ children: occurrence.children.map(branch)
635
+ })]
636
+ }, occurrence.key);
637
+ return /* @__PURE__ */ jsxs("div", {
638
+ ref: setContainer,
639
+ className: "mv:relative mv:w-max mv:min-w-full mv:p-4",
640
+ children: [/* @__PURE__ */ jsx(Branches, {
641
+ orientation,
642
+ children: [...tree.roots, ...tree.orphans].map(branch)
643
+ }), /* @__PURE__ */ jsx(EdgeOverlay, {
644
+ links,
645
+ elements,
646
+ container,
647
+ routing: orientation
648
+ })]
649
+ });
650
+ }
651
+ function Branches({ orientation, nested = false, children }) {
652
+ return /* @__PURE__ */ jsx("ul", {
653
+ className: `mv:m-0 mv:flex mv:list-none mv:p-0 ${orientation === "vertical" ? `mv:flex-row mv:items-start mv:justify-center mv:gap-3 ${nested ? "mv:pt-10" : ""}` : `mv:flex-col mv:items-start mv:justify-center mv:gap-2 ${nested ? "mv:pl-14" : ""}`}`,
654
+ children
655
+ });
656
+ }
657
+ //#endregion
658
+ //#region src/layouts/XYLayout.tsx
659
+ /**
660
+ * Nodes positioned by two numeric fields, with straight connectors between them. Positions are
661
+ * percentages of the plot area, which fills the container (scrolling below a minimum size). Nodes that can't be
662
+ * placed are listed under the plot with the reason.
663
+ * PROTOTYPE: labels may overlap where nodes sit close together; no collision handling.
664
+ */
665
+ function XYLayout({ graph, region, instanceId, onNodeSelect }) {
666
+ const { layout } = region;
667
+ const plot = useMemo(() => buildXYPlot(graph, region.id, layout), [
668
+ graph,
669
+ region.id,
670
+ layout
671
+ ]);
672
+ const links = useMemo(() => region.edges.show ? plot.links.map(({ from, to, field, hierarchy, lane }) => ({
673
+ from,
674
+ to,
675
+ id: field,
676
+ lane,
677
+ kind: hierarchy ? "tree" : "cross"
678
+ })) : [], [plot, region.edges.show]);
679
+ const shape = region.nodes.style === "shape";
680
+ const [container, setContainer] = useState(null);
681
+ const elements = useRef(/* @__PURE__ */ new Map()).current;
682
+ const register = useCallback((key) => (el) => {
683
+ if (el) elements.set(key, el);
684
+ else elements.delete(key);
685
+ }, [elements]);
686
+ return /* @__PURE__ */ jsxs("div", {
687
+ className: "mv:box-border mv:flex mv:h-full mv:min-h-[28rem] mv:min-w-[36rem] mv:flex-col mv:gap-4 mv:p-4",
688
+ children: [/* @__PURE__ */ jsxs("div", {
689
+ className: "mv:grid mv:min-h-0 mv:flex-1 mv:grid-cols-[auto_1fr] mv:grid-rows-[1fr_auto] mv:gap-1",
690
+ children: [
691
+ /* @__PURE__ */ jsx(AxisTitle, {
692
+ axis: layout.y,
693
+ domain: plot.domains.y,
694
+ vertical: true
695
+ }),
696
+ /* @__PURE__ */ jsxs("div", {
697
+ ref: setContainer,
698
+ className: "mv:relative mv:border-b mv:border-l mv:border-edge",
699
+ children: [/* @__PURE__ */ jsx(EdgeOverlay, {
700
+ links,
701
+ elements,
702
+ container,
703
+ routing: "straight"
704
+ }), /* @__PURE__ */ jsx("div", {
705
+ className: "mv:absolute mv:inset-8",
706
+ children: plot.points.map((point) => /* @__PURE__ */ jsxs("div", {
707
+ "data-occurrence": `${instanceId}${point.key}`,
708
+ className: `mv:absolute mv:flex mv:items-center mv:gap-1 ${shape ? "mv:-translate-x-3 mv:translate-y-3" : "mv:-translate-x-1/2 mv:translate-y-1/2"}`,
709
+ style: {
710
+ left: `${point.x * 100}%`,
711
+ bottom: `${point.y * 100}%`
712
+ },
713
+ children: [/* @__PURE__ */ jsx("div", {
714
+ ref: register(point.key),
715
+ children: /* @__PURE__ */ jsx(NodeCard, {
716
+ node: point.node,
717
+ repeat: false,
718
+ nodes: region.nodes,
719
+ onSelect: onNodeSelect
720
+ })
721
+ }), shape && /* @__PURE__ */ jsx("span", {
722
+ className: "mv:whitespace-nowrap mv:rounded mv:bg-surface/80 mv:px-0.5 mv:text-xs",
723
+ children: point.node.title
724
+ })]
725
+ }, point.key))
726
+ })]
727
+ }),
728
+ /* @__PURE__ */ jsx("div", {}),
729
+ /* @__PURE__ */ jsx(AxisTitle, {
730
+ axis: layout.x,
731
+ domain: plot.domains.x
732
+ })
733
+ ]
734
+ }), plot.unplaced.length > 0 && /* @__PURE__ */ jsxs("section", {
735
+ className: "mv:flex mv:flex-col mv:gap-2",
736
+ children: [/* @__PURE__ */ jsx("span", {
737
+ className: "mv:text-xs mv:font-semibold mv:text-muted",
738
+ children: "Not plotted"
739
+ }), /* @__PURE__ */ jsx("ul", {
740
+ className: "mv:m-0 mv:flex mv:list-none mv:flex-wrap mv:gap-3 mv:p-0",
741
+ children: plot.unplaced.map(({ key, node, reasons }) => /* @__PURE__ */ jsxs("li", {
742
+ "data-occurrence": `${instanceId}${key}`,
743
+ className: "mv:flex mv:items-center mv:gap-1",
744
+ children: [/* @__PURE__ */ jsx(NodeCard, {
745
+ node,
746
+ repeat: false,
747
+ nodes: region.nodes,
748
+ onSelect: onNodeSelect
749
+ }), /* @__PURE__ */ jsxs("span", {
750
+ className: "mv:text-xs",
751
+ children: [shape && `${node.title} `, /* @__PURE__ */ jsxs("span", {
752
+ className: "mv:text-muted",
753
+ children: [
754
+ "(",
755
+ reasons.join("; "),
756
+ ")"
757
+ ]
758
+ })]
759
+ })]
760
+ }, key))
761
+ })]
762
+ })]
763
+ });
764
+ }
765
+ /** Axis title with the values at each end, in drawing order. */
766
+ function AxisTitle({ axis, domain: [min, max], vertical = false }) {
767
+ const [start, end] = axis.direction === "descending" ? [max, min] : [min, max];
768
+ return /* @__PURE__ */ jsxs("div", {
769
+ className: `mv:flex mv:items-center mv:justify-between mv:gap-2 mv:text-xs mv:text-muted ${vertical ? "mv:flex-col-reverse mv:py-8" : "mv:px-8"}`,
770
+ children: [
771
+ /* @__PURE__ */ jsx("span", { children: format(start) }),
772
+ /* @__PURE__ */ jsx("span", {
773
+ className: `mv:font-semibold ${vertical ? "mv:[writing-mode:vertical-rl] mv:rotate-180" : ""}`,
774
+ children: axis.label ?? axis.field
775
+ }),
776
+ /* @__PURE__ */ jsx("span", { children: format(end) })
777
+ ]
778
+ });
779
+ }
780
+ function format(value) {
781
+ return Number.isInteger(value) ? String(value) : value.toPrecision(3).replace(/\.?0+$/, "");
782
+ }
783
+ //#endregion
784
+ //#region src/layouts/MapView.tsx
785
+ /** Renders a map view definition against a space. Sizes to its container and scrolls within it. */
786
+ function MapView({ definition, graph, values, onNodeSelect }) {
787
+ const instanceId = useId();
788
+ const region = useMemo(() => resolveRegions(definition, values), [definition, values])[0];
789
+ return /* @__PURE__ */ jsx("div", {
790
+ className: "mv:h-full mv:w-full mv:overflow-auto mv:bg-surface mv:text-ink",
791
+ children: layoutFor(region, {
792
+ graph,
793
+ instanceId,
794
+ onNodeSelect
795
+ })
796
+ });
797
+ }
798
+ function layoutFor(region, props) {
799
+ const { layout } = region;
800
+ switch (layout.type) {
801
+ case "tree": return /* @__PURE__ */ jsx(TreeLayout, {
802
+ ...props,
803
+ region: {
804
+ ...region,
805
+ layout
806
+ }
807
+ });
808
+ case "landscape": return /* @__PURE__ */ jsx(LandscapeLayout, {
809
+ ...props,
810
+ region: {
811
+ ...region,
812
+ layout
813
+ }
814
+ });
815
+ case "xy": return /* @__PURE__ */ jsx(XYLayout, {
816
+ ...props,
817
+ region: {
818
+ ...region,
819
+ layout
820
+ }
821
+ });
822
+ }
823
+ }
824
+ //#endregion
825
+ //#region src/layouts/ParameterControls.tsx
826
+ /**
827
+ * Controls generated from a map view's declared parameters. Hosts own the values (session
828
+ * state) and decide where the controls go.
829
+ */
830
+ function ParameterControls({ definition, values, onChange }) {
831
+ const id = useId();
832
+ const resolved = useMemo(() => resolveParameterValues(definition, values), [definition, values]);
833
+ const { parameters } = definition;
834
+ if (parameters.length === 0) return null;
835
+ return /* @__PURE__ */ jsx("div", {
836
+ className: "mv:flex mv:flex-wrap mv:items-center mv:gap-x-4 mv:gap-y-2 mv:text-sm",
837
+ children: parameters.map((param) => {
838
+ const value = resolved[param.name];
839
+ const controlId = `${id}-${param.name}`;
840
+ const { control } = param;
841
+ return /* @__PURE__ */ jsx("div", {
842
+ className: "mv:flex mv:items-center mv:gap-1.5",
843
+ title: param.description,
844
+ children: control.kind === "toggle" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
845
+ id: controlId,
846
+ type: "checkbox",
847
+ checked: value === true,
848
+ onChange: (e) => onChange(param.name, e.target.checked)
849
+ }), /* @__PURE__ */ jsx("label", {
850
+ htmlFor: controlId,
851
+ children: param.title
852
+ })] }) : control.kind === "select" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("label", {
853
+ htmlFor: controlId,
854
+ className: "mv:text-muted",
855
+ children: param.title
856
+ }), /* @__PURE__ */ jsx("select", {
857
+ id: controlId,
858
+ value: JSON.stringify(value),
859
+ onChange: (e) => onChange(param.name, JSON.parse(e.target.value)),
860
+ children: control.options.map((option) => /* @__PURE__ */ jsx("option", {
861
+ value: JSON.stringify(option),
862
+ children: String(option)
863
+ }, JSON.stringify(option)))
864
+ })] }) : /* @__PURE__ */ jsxs("fieldset", {
865
+ className: "mv:m-0 mv:flex mv:flex-wrap mv:items-center mv:gap-2 mv:border-0 mv:p-0",
866
+ children: [/* @__PURE__ */ jsx("legend", {
867
+ className: "mv:float-left mv:mr-1.5 mv:p-0 mv:text-muted",
868
+ children: param.title
869
+ }), control.options.map((option) => {
870
+ const selected = Array.isArray(value) ? value : [];
871
+ const checked = selected.some((v) => JSON.stringify(v) === JSON.stringify(option));
872
+ return /* @__PURE__ */ jsxs("label", {
873
+ className: "mv:flex mv:items-center mv:gap-1",
874
+ children: [/* @__PURE__ */ jsx("input", {
875
+ type: "checkbox",
876
+ checked,
877
+ onChange: () => onChange(param.name, control.options.filter((o) => o === option ? !checked : selected.some((v) => JSON.stringify(v) === JSON.stringify(o))))
878
+ }), String(option)]
879
+ }, JSON.stringify(option));
880
+ })]
881
+ })
882
+ }, param.name);
883
+ })
884
+ });
885
+ }
886
+ //#endregion
887
+ export { MAP_VIEW_SCHEMAS, MapView, MapViewDefinitionError, ParameterControls, mapViewsForSchema, parseMapViewDefinition, resolveParameterValues, selectionFilter };
888
+
889
+ //# sourceMappingURL=index.js.map