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