@the-package-labs/idocs-core 0.3.1 → 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.
package/dist/browser.js CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  createIconResolver,
7
7
  normalizeRoot,
8
8
  route
9
- } from "./chunk-5BMICZRC.js";
10
- import "./chunk-FRVCGXIG.js";
9
+ } from "./chunk-S7M3BRFI.js";
10
+ import "./chunk-ITBBPZE6.js";
11
11
  export {
12
12
  KNOWN_KEYS,
13
13
  RootSchema,
@@ -0,0 +1,127 @@
1
+ import {
2
+ didYouMean
3
+ } from "./chunk-ITBBPZE6.js";
4
+
5
+ // src/nesting.ts
6
+ function nestedDiagrams(all, name) {
7
+ const out = {};
8
+ const queue = [name];
9
+ for (let cur = queue.shift(); cur !== void 0; cur = queue.shift()) {
10
+ for (const node of Object.values(all[cur]?.nodes ?? {})) {
11
+ const next = node.diagram?.name;
12
+ if (next === void 0 || next === name || out[next] || !all[next]) continue;
13
+ out[next] = all[next];
14
+ queue.push(next);
15
+ }
16
+ }
17
+ return out;
18
+ }
19
+ function attachNested(all) {
20
+ const flat = {};
21
+ for (const [name, diagram] of Object.entries(all)) {
22
+ const { nested: _, ...rest } = diagram;
23
+ flat[name] = rest;
24
+ }
25
+ const out = {};
26
+ for (const [name, diagram] of Object.entries(flat)) {
27
+ const nested = nestedDiagrams(flat, name);
28
+ out[name] = Object.keys(nested).length ? { ...diagram, nested } : diagram;
29
+ }
30
+ return out;
31
+ }
32
+ function linkDiagrams(entries) {
33
+ const out = [];
34
+ const names = Object.keys(entries).sort();
35
+ const known = names.filter((n) => entries[n].diagram);
36
+ const report = (from, path, message, hint) => {
37
+ const at = entries[from].where?.(path);
38
+ out.push({
39
+ severity: "error",
40
+ message,
41
+ file: at?.file ?? `diagrams/${from}/diagram.yaml`,
42
+ line: at?.line,
43
+ col: at?.col,
44
+ path: path.join("."),
45
+ hint
46
+ });
47
+ };
48
+ const edges = /* @__PURE__ */ new Map();
49
+ for (const from of names) {
50
+ const diagram = entries[from].diagram;
51
+ if (!diagram) continue;
52
+ for (const node of Object.values(diagram.nodes)) {
53
+ const ref = node.diagram;
54
+ if (!ref) continue;
55
+ const at = ["nodes", node.id, "diagram"];
56
+ const sub = (key) => [...at, key];
57
+ const target = entries[ref.name];
58
+ if (!target) {
59
+ report(
60
+ from,
61
+ at,
62
+ `Node "${node.id}" opens diagram "${ref.name}", which does not exist`,
63
+ didYouMean(ref.name, names) ?? (names.length > 1 ? `Diagrams: ${names.filter((n) => n !== from).join(", ")}` : void 0)
64
+ );
65
+ continue;
66
+ }
67
+ if (!target.diagram) continue;
68
+ edges.set(from, (edges.get(from) ?? /* @__PURE__ */ new Set()).add(ref.name));
69
+ const views = target.diagram.views.map((v) => v.id);
70
+ const scenario = ref.scenario ? target.diagram.scenarios.find((s) => s.id === ref.scenario) : void 0;
71
+ if (ref.view && !views.includes(ref.view)) {
72
+ report(
73
+ from,
74
+ sub("view"),
75
+ `Diagram "${ref.name}" has no view "${ref.view}"`,
76
+ didYouMean(ref.view, views) ?? `Views: ${views.join(", ")}`
77
+ );
78
+ }
79
+ if (ref.scenario && !scenario) {
80
+ const ids = target.diagram.scenarios.map((s) => s.id);
81
+ report(
82
+ from,
83
+ sub("scenario"),
84
+ `Diagram "${ref.name}" has no scenario "${ref.scenario}"`,
85
+ didYouMean(ref.scenario, ids) ?? (ids.length ? `Scenarios: ${ids.join(", ")}` : "It has no scenarios.")
86
+ );
87
+ } else if (scenario && ref.view && scenario.view !== ref.view) {
88
+ report(
89
+ from,
90
+ sub("scenario"),
91
+ `Scenario "${scenario.id}" plays on view "${scenario.view}", not "${ref.view}"`,
92
+ "Drop `view`, and the diagram opens on the scenario's own view."
93
+ );
94
+ }
95
+ }
96
+ }
97
+ const state = /* @__PURE__ */ new Map();
98
+ const stack = [];
99
+ const visit = (name) => {
100
+ state.set(name, "open");
101
+ stack.push(name);
102
+ for (const next of edges.get(name) ?? /* @__PURE__ */ new Set()) {
103
+ if (state.get(next) === "open") {
104
+ const circle = [...stack.slice(stack.indexOf(next)), next];
105
+ const node = Object.values(entries[name].diagram.nodes).find(
106
+ (n) => n.diagram?.name === next
107
+ );
108
+ report(
109
+ name,
110
+ ["nodes", node.id, "diagram"],
111
+ next === name ? `Node "${node.id}" opens its own diagram "${name}"` : `Node "${node.id}" opens "${next}", which leads back to "${name}"`,
112
+ `A diagram cannot be inside itself: ${circle.join(" \u2192 ")}`
113
+ );
114
+ } else if (!state.has(next)) visit(next);
115
+ }
116
+ stack.pop();
117
+ state.set(name, "done");
118
+ };
119
+ for (const name of known) if (!state.has(name)) visit(name);
120
+ return out;
121
+ }
122
+
123
+ export {
124
+ nestedDiagrams,
125
+ attachNested,
126
+ linkDiagrams
127
+ };
@@ -104,6 +104,33 @@ function letterKey(index) {
104
104
  return out;
105
105
  }
106
106
 
107
+ // src/suggest.ts
108
+ function didYouMean(word, options) {
109
+ let best;
110
+ let bestDist = Math.max(2, Math.floor(word.length / 3)) + 1;
111
+ for (const o of options) {
112
+ const d = levenshtein(word.toLowerCase(), o.toLowerCase());
113
+ if (d < bestDist) {
114
+ best = o;
115
+ bestDist = d;
116
+ }
117
+ }
118
+ return best ? `Did you mean "${best}"?` : void 0;
119
+ }
120
+ function levenshtein(a, b) {
121
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
122
+ for (let i = 1; i <= a.length; i++) {
123
+ let prev = row[0];
124
+ row[0] = i;
125
+ for (let j = 1; j <= b.length; j++) {
126
+ const tmp = row[j];
127
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
128
+ prev = tmp;
129
+ }
130
+ }
131
+ return row[b.length];
132
+ }
133
+
107
134
  export {
108
135
  BUILTIN_KINDS,
109
136
  BUILTIN_EDGE_KINDS,
@@ -113,5 +140,6 @@ export {
113
140
  dist,
114
141
  pointAt,
115
142
  roundedPath,
116
- letterKey
143
+ letterKey,
144
+ didYouMean
117
145
  };
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  BUILTIN_EDGE_KINDS,
3
3
  BUILTIN_KINDS,
4
+ didYouMean,
4
5
  letterKey
5
- } from "./chunk-FRVCGXIG.js";
6
+ } from "./chunk-ITBBPZE6.js";
6
7
 
7
8
  // src/icon-resolver.ts
8
9
  import { getIconData, iconToSVG } from "@iconify/utils";
@@ -231,7 +232,23 @@ var NodeSchema = z.strictObject({
231
232
  doc: z.string().optional().describe("Markdown file for the drawer. Defaults to nodes/<id>.md when present."),
232
233
  links: z.array(z.strictObject({ label: z.string(), url: z.string() })).optional(),
233
234
  refs: z.array(z.string()).optional().describe("Source files or URLs this node is based on."),
234
- tags: z.array(z.string()).optional()
235
+ tags: z.array(z.string()).optional(),
236
+ diagram: z.union([
237
+ z.string().min(1),
238
+ z.strictObject({
239
+ name: z.string().min(1).describe("Folder name of the diagram, under diagrams/."),
240
+ view: Id.optional().describe(
241
+ "View to open first. Defaults to the scenario view, then the first."
242
+ ),
243
+ scenario: Id.optional().describe("Scenario to select when the diagram opens."),
244
+ mode: z.enum(["both", "expand", "open"]).optional().describe(
245
+ "both (default): the reader can expand it in place or open it. expand: only in place. open: only move into it."
246
+ ),
247
+ expanded: z.boolean().optional().describe("Start expanded in place, so the whole inner diagram shows as a group.")
248
+ })
249
+ ]).optional().describe(
250
+ "Another diagram (its folder name) that shows the inside of this node. The card gets buttons to expand it in place, as a group, or to open it one level deeper, with a way back."
251
+ )
235
252
  });
236
253
  var GroupSchema = z.strictObject({
237
254
  label: z.string(),
@@ -449,15 +466,17 @@ function compileDiagram(src, assets) {
449
466
  scenarios[f.id] = body;
450
467
  scenarioSources.set(f.id, s);
451
468
  }
452
- const report = (severity, message, path, hint) => {
469
+ const where = (path) => {
453
470
  let source = main;
454
471
  let p = path;
455
472
  if (path[0] === "scenarios" && typeof path[1] === "string" && scenarioSources.has(path[1])) {
456
473
  source = scenarioSources.get(path[1]);
457
474
  p = path.slice(2);
458
475
  }
459
- const pos = locate(source, p);
460
- diagnostics.push({ severity, message, file: source.file, ...pos, path: path.join("."), hint });
476
+ return { file: source.file, ...locate(source, p) };
477
+ };
478
+ const report = (severity, message, path, hint) => {
479
+ diagnostics.push({ severity, message, ...where(path), path: path.join("."), hint });
461
480
  };
462
481
  const error = (message, path, hint) => report("error", message, path, hint);
463
482
  const warn = (message, path, hint) => report("warning", message, path, hint);
@@ -473,7 +492,7 @@ function compileDiagram(src, assets) {
473
492
  }
474
493
  const diagram = build(parsed.data, src, error, warn, assets);
475
494
  if (diagnostics.some((d) => d.severity === "error")) return { diagnostics };
476
- return { diagram, diagnostics };
495
+ return Object.defineProperty({ diagram, diagnostics }, "where", { value: where });
477
496
  }
478
497
  function build(data, src, error, warn, { resolveIcon }) {
479
498
  const kinds = { ...BUILTIN_KINDS };
@@ -586,6 +605,13 @@ function build(data, src, error, warn, { resolveIcon }) {
586
605
  didYouMean(n.in, Object.keys(groups))
587
606
  );
588
607
  }
608
+ if (typeof n.diagram === "object" && n.diagram.expanded && n.diagram.mode === "open") {
609
+ warn(
610
+ "`expanded` has no effect with `mode: open`",
611
+ ["nodes", id, "diagram", "expanded"],
612
+ "Use `mode: expand` or `mode: both`, or drop `expanded`."
613
+ );
614
+ }
589
615
  const docPath = n.doc ?? `nodes/${id}.md`;
590
616
  const docText = src.readFile?.(docPath);
591
617
  if (n.doc && docText === void 0)
@@ -608,7 +634,8 @@ function build(data, src, error, warn, { resolveIcon }) {
608
634
  docHtml: docHtml || void 0,
609
635
  links: n.links ?? [],
610
636
  refs: n.refs ?? [],
611
- tags: n.tags ?? []
637
+ tags: n.tags ?? [],
638
+ diagram: typeof n.diagram === "string" ? { name: n.diagram } : n.diagram
612
639
  };
613
640
  }
614
641
  const edges = {};
@@ -1031,31 +1058,6 @@ function allowedKeysFor(path) {
1031
1058
  return void 0;
1032
1059
  }
1033
1060
  }
1034
- function didYouMean(word, options) {
1035
- let best;
1036
- let bestDist = Math.max(2, Math.floor(word.length / 3)) + 1;
1037
- for (const o of options) {
1038
- const d = levenshtein(word.toLowerCase(), o.toLowerCase());
1039
- if (d < bestDist) {
1040
- best = o;
1041
- bestDist = d;
1042
- }
1043
- }
1044
- return best ? `Did you mean "${best}"?` : void 0;
1045
- }
1046
- function levenshtein(a, b) {
1047
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
1048
- for (let i = 1; i <= a.length; i++) {
1049
- let prev = row[0];
1050
- row[0] = i;
1051
- for (let j = 1; j <= b.length; j++) {
1052
- const tmp = row[j];
1053
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1054
- prev = tmp;
1055
- }
1056
- }
1057
- return row[b.length];
1058
- }
1059
1061
 
1060
1062
  export {
1061
1063
  createIconResolver,
@@ -23,9 +23,20 @@ export interface DiagramSource {
23
23
  /** Read a file relative to the diagram directory (docs, local icons). Undefined when missing. */
24
24
  readFile?: (relPath: string) => string | undefined;
25
25
  }
26
+ /** Where a dotted path of the diagram lives in the files it was compiled from. */
27
+ export type Where = (path: (string | number)[]) => {
28
+ file: string;
29
+ line?: number;
30
+ col?: number;
31
+ };
26
32
  export interface CompileResult {
27
33
  diagram?: CompiledDiagram;
28
34
  diagnostics: Diagnostic[];
35
+ /**
36
+ * Lets a later pass (linking diagrams to each other) report at a line. A closure over the parsed
37
+ * files, so it is not enumerable: spreading, cloning or serialising a result leaves it behind.
38
+ */
39
+ readonly where?: Where;
29
40
  }
30
41
  export interface CompilerAssets {
31
42
  resolveIcon: (spec: string) => IconResult;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export * from './defaults';
2
2
  export * from './geometry';
3
3
  export * from './layout';
4
+ export * from './nested-layout';
5
+ export * from './nesting';
6
+ export * from './outline';
7
+ export * from './phases';
4
8
  export * from './route';
5
9
  export * from './story';
6
10
  export * from './svg';
package/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ attachNested,
3
+ linkDiagrams,
4
+ nestedDiagrams
5
+ } from "./chunk-3QBMUILO.js";
1
6
  import {
2
7
  BUILTIN_EDGE_KINDS,
3
8
  BUILTIN_KINDS,
@@ -8,7 +13,7 @@ import {
8
13
  pointAt,
9
14
  polylineLength,
10
15
  roundedPath
11
- } from "./chunk-FRVCGXIG.js";
16
+ } from "./chunk-ITBBPZE6.js";
12
17
 
13
18
  // src/layout.ts
14
19
  import ELK from "elkjs/lib/elk.bundled.js";
@@ -951,6 +956,236 @@ function overlap(a, b, pad) {
951
956
  return a.x < b.x + b.w + pad && a.x + a.w + pad > b.x && a.y < b.y + b.h + pad && a.y + a.h + pad > b.y;
952
957
  }
953
958
 
959
+ // src/nested-layout.ts
960
+ var INLINE_PAD = { top: 40, left: 4, right: 4, bottom: 4 };
961
+ function openingView(diagram, ref) {
962
+ const has = (id) => diagram.views.some((v) => v.id === id);
963
+ if (has(ref.view)) return ref.view;
964
+ const fromScenario = diagram.scenarios.find((s) => s.id === ref.scenario)?.view;
965
+ return has(fromScenario) ? fromScenario : diagram.views[0].id;
966
+ }
967
+ var canExpand = (ref, diagrams) => !!ref && ref.mode !== "open" && !!diagrams[ref.name];
968
+ var canOpen = (ref, diagrams) => !!ref && ref.mode !== "expand" && !!diagrams[ref.name];
969
+ var pathJoin = (prefix, id) => `${prefix}${id}`;
970
+ function defaultExpanded(diagram, diagrams, prefix = "", seen = []) {
971
+ const out = [];
972
+ for (const node of Object.values(diagram.nodes)) {
973
+ const ref = node.diagram;
974
+ const child = ref && diagrams[ref.name];
975
+ if (!ref || !child || seen.includes(ref.name) || !ref.expanded || !canExpand(ref, diagrams))
976
+ continue;
977
+ const path = pathJoin(prefix, node.id);
978
+ out.push(path, ...defaultExpanded(child, diagrams, `${path}/`, [...seen, diagram.name]));
979
+ }
980
+ return out;
981
+ }
982
+ function measurable(diagram, viewId, expanded, diagrams, prefix = "") {
983
+ const view = diagram.views.find((v) => v.id === viewId);
984
+ if (!view) return [];
985
+ const out = [];
986
+ for (const id of view.nodeIds) {
987
+ const node = diagram.nodes[id];
988
+ const path = pathJoin(prefix, id);
989
+ out.push({ key: path, node });
990
+ const ref = node.diagram;
991
+ const child = ref && expanded.has(path) && canExpand(ref, diagrams) ? diagrams[ref.name] : void 0;
992
+ if (ref && child) {
993
+ out.push(...measurable(child, openingView(child, ref), expanded, diagrams, `${path}/`));
994
+ }
995
+ }
996
+ return out;
997
+ }
998
+ async function layoutNested(diagram, viewId, { sizes = {}, expanded, diagrams = {}, prefix = "" } = {}) {
999
+ const view = diagram.views.find((v) => v.id === viewId);
1000
+ if (!view) throw new Error(`Unknown view "${viewId}"`);
1001
+ const own = {};
1002
+ for (const id of view.nodeIds) {
1003
+ const size = sizes[pathJoin(prefix, id)];
1004
+ if (size) own[id] = size;
1005
+ }
1006
+ const inner = {};
1007
+ await Promise.all(
1008
+ view.nodeIds.map(async (id) => {
1009
+ const ref = diagram.nodes[id].diagram;
1010
+ const path = pathJoin(prefix, id);
1011
+ const child = ref && expanded?.has(path) && canExpand(ref, diagrams) ? diagrams[ref.name] : void 0;
1012
+ if (!ref || !child) return;
1013
+ const childView = openingView(child, ref);
1014
+ const layout = await layoutNested(child, childView, {
1015
+ sizes,
1016
+ expanded,
1017
+ diagrams,
1018
+ prefix: `${path}/`
1019
+ });
1020
+ inner[id] = {
1021
+ name: ref.name,
1022
+ viewId: childView,
1023
+ offset: { x: INLINE_PAD.left, y: INLINE_PAD.top },
1024
+ layout
1025
+ };
1026
+ own[id] = {
1027
+ w: layout.layout.width + INLINE_PAD.left + INLINE_PAD.right,
1028
+ h: layout.layout.height + INLINE_PAD.top + INLINE_PAD.bottom
1029
+ };
1030
+ })
1031
+ );
1032
+ return { layout: await layoutView(diagram, viewId, own), inner };
1033
+ }
1034
+ function rectOfPath(nested, path) {
1035
+ let level = nested;
1036
+ let at = { x: 0, y: 0 };
1037
+ const ids = path.split("/");
1038
+ for (const [i, id] of ids.entries()) {
1039
+ const r = level?.layout.nodes[id];
1040
+ if (!level || !r) return void 0;
1041
+ if (i === ids.length - 1) return { x: at.x + r.x, y: at.y + r.y, w: r.w, h: r.h };
1042
+ const inner = level.inner[id];
1043
+ if (!inner) return void 0;
1044
+ at = { x: at.x + r.x + inner.offset.x, y: at.y + r.y + inner.offset.y };
1045
+ level = inner.layout;
1046
+ }
1047
+ return void 0;
1048
+ }
1049
+
1050
+ // src/outline.ts
1051
+ function buildOutline(diagram, view, { diagrams = {}, expanded = /* @__PURE__ */ new Set(), prefix = "", seen = [] } = {}) {
1052
+ const inView = new Set(view.nodeIds);
1053
+ const groups = new Set(view.groupIds);
1054
+ const drawn = view.edgeIds.map((id) => diagram.edges[id]).filter((e) => !e.hidden);
1055
+ const connectionsOf = (id) => {
1056
+ const out = [];
1057
+ for (const e of drawn) {
1058
+ const sends = e.from === id;
1059
+ const gets = e.to === id;
1060
+ if (!sends && !gets) continue;
1061
+ const other = sends ? e.to : e.from;
1062
+ out.push({
1063
+ edge: e.id,
1064
+ dir: e.both ? "both" : sends ? "out" : "in",
1065
+ other,
1066
+ otherTitle: diagram.nodes[other]?.title ?? other,
1067
+ label: e.label,
1068
+ kind: e.kind,
1069
+ status: e.status
1070
+ });
1071
+ }
1072
+ const rank = { out: 0, both: 1, in: 2 };
1073
+ return out.sort((a, b) => rank[a.dir] - rank[b.dir]);
1074
+ };
1075
+ const nodeItem = (id) => {
1076
+ const node = diagram.nodes[id];
1077
+ const path = pathJoin(prefix, id);
1078
+ const item = {
1079
+ type: "node",
1080
+ path,
1081
+ id,
1082
+ title: node.title,
1083
+ sub: node.sub,
1084
+ icon: node.icon,
1085
+ status: node.status,
1086
+ connections: connectionsOf(id)
1087
+ };
1088
+ const ref = node.diagram;
1089
+ const child = ref && diagrams[ref.name];
1090
+ if (ref && child && !seen.includes(ref.name) && (canExpand(ref, diagrams) || canOpen(ref, diagrams))) {
1091
+ const shown = canExpand(ref, diagrams) && expanded.has(path);
1092
+ const childView = child.views.find((v) => v.id === openingView(child, ref));
1093
+ item.inside = {
1094
+ title: child.title,
1095
+ expand: canExpand(ref, diagrams),
1096
+ open: canOpen(ref, diagrams),
1097
+ expanded: shown,
1098
+ items: shown && childView ? buildOutline(child, childView, {
1099
+ diagrams,
1100
+ expanded,
1101
+ prefix: `${path}/`,
1102
+ seen: [...seen, diagram.name]
1103
+ }) : []
1104
+ };
1105
+ }
1106
+ return item;
1107
+ };
1108
+ const childGroups = /* @__PURE__ */ new Map();
1109
+ for (const gid of view.groupIds) {
1110
+ const parent = diagram.groups[gid]?.parent;
1111
+ const key = parent && groups.has(parent) ? parent : "";
1112
+ childGroups.set(key, [...childGroups.get(key) ?? [], gid]);
1113
+ }
1114
+ const childNodes = /* @__PURE__ */ new Map();
1115
+ for (const id of view.nodeIds) {
1116
+ if (!inView.has(id)) continue;
1117
+ const group = diagram.nodes[id].group;
1118
+ const key = group && groups.has(group) ? group : "";
1119
+ childNodes.set(key, [...childNodes.get(key) ?? [], id]);
1120
+ }
1121
+ const position = new Map(view.nodeIds.map((id, i) => [id, i]));
1122
+ const firstNode = (gid) => Math.min(
1123
+ ...(childNodes.get(gid) ?? []).map((n) => position.get(n)),
1124
+ ...(childGroups.get(gid) ?? []).map(firstNode)
1125
+ );
1126
+ const groupItem = (gid) => {
1127
+ const g = diagram.groups[gid];
1128
+ const items = fill(gid);
1129
+ return {
1130
+ type: "group",
1131
+ path: pathJoin(prefix, `#${gid}`),
1132
+ id: gid,
1133
+ title: g.label,
1134
+ caption: g.caption,
1135
+ icon: g.icon,
1136
+ status: g.status,
1137
+ items,
1138
+ count: items.reduce((n, i) => n + (i.type === "group" ? i.count : 1), 0)
1139
+ };
1140
+ };
1141
+ function fill(key) {
1142
+ const ranked = [
1143
+ ...(childGroups.get(key) ?? []).map((gid) => ({
1144
+ at: firstNode(gid),
1145
+ make: () => groupItem(gid)
1146
+ })),
1147
+ ...(childNodes.get(key) ?? []).map((id) => ({
1148
+ at: position.get(id),
1149
+ make: () => nodeItem(id)
1150
+ }))
1151
+ ];
1152
+ return ranked.sort((a, b) => a.at - b.at).map((r) => r.make());
1153
+ }
1154
+ return fill("");
1155
+ }
1156
+ function outlineNodes(items) {
1157
+ const out = [];
1158
+ for (const item of items) {
1159
+ if (item.type === "group") out.push(...outlineNodes(item.items));
1160
+ else out.push(item, ...outlineNodes(item.inside?.items ?? []));
1161
+ }
1162
+ return out;
1163
+ }
1164
+
1165
+ // src/phases.ts
1166
+ var hasPhases = (scenario) => scenario.phases.some((p) => p.title);
1167
+ function phaseSpans(scenario) {
1168
+ const out = [];
1169
+ scenario.steps.forEach((step, i) => {
1170
+ const last = out[out.length - 1];
1171
+ if (last && last.index === step.phase) {
1172
+ last.to = i;
1173
+ last.count++;
1174
+ return;
1175
+ }
1176
+ const phase = scenario.phases[step.phase];
1177
+ out.push({
1178
+ index: step.phase,
1179
+ title: phase?.title ?? "",
1180
+ caption: phase?.caption,
1181
+ from: i,
1182
+ to: i,
1183
+ count: 1
1184
+ });
1185
+ });
1186
+ return out;
1187
+ }
1188
+
954
1189
  // src/story.ts
955
1190
  function buildStory(diagram, scenario) {
956
1191
  const steps = scenario.steps;
@@ -1250,17 +1485,33 @@ export {
1250
1485
  BUILTIN_EDGE_KINDS,
1251
1486
  BUILTIN_KINDS,
1252
1487
  DEFAULT_NODE_WIDTH,
1488
+ INLINE_PAD,
1253
1489
  STEP_KIND_COLORS,
1490
+ attachNested,
1491
+ buildOutline,
1254
1492
  buildStory,
1255
1493
  buildTimeline,
1494
+ canExpand,
1495
+ canOpen,
1496
+ defaultExpanded,
1256
1497
  dist,
1257
1498
  frameAt,
1499
+ hasPhases,
1258
1500
  labelWidth,
1501
+ layoutNested,
1259
1502
  layoutView,
1260
1503
  letterKey,
1504
+ linkDiagrams,
1505
+ measurable,
1506
+ nestedDiagrams,
1507
+ openingView,
1508
+ outlineNodes,
1261
1509
  packGrid,
1510
+ pathJoin,
1511
+ phaseSpans,
1262
1512
  pointAt,
1263
1513
  polylineLength,
1514
+ rectOfPath,
1264
1515
  roundedPath,
1265
1516
  routeEdges,
1266
1517
  runBounds,
@@ -0,0 +1,66 @@
1
+ import { type NodeSize } from './layout';
2
+ import type { CompiledDiagram, CompiledNode, Layout, NodeDiagram, Pt, Rect } from './types';
3
+ /**
4
+ * What an expanded node adds around the inner diagram: the header that names it, and a margin.
5
+ * The inner layout already keeps a margin of its own, so the sides stay thin.
6
+ */
7
+ export declare const INLINE_PAD: {
8
+ top: number;
9
+ left: number;
10
+ right: number;
11
+ bottom: number;
12
+ };
13
+ /** A diagram laid out together with the diagrams expanded inside it, at any depth. */
14
+ export interface NestedLayout {
15
+ layout: Layout;
16
+ /** By node id: the layout of the diagram that node shows in place. */
17
+ inner: Record<string, InnerLayout>;
18
+ }
19
+ export interface InnerLayout {
20
+ /** Name of the diagram shown. */
21
+ name: string;
22
+ viewId: string;
23
+ /** Where the inner diagram's origin sits inside the expanded node's rectangle. */
24
+ offset: Pt;
25
+ layout: NestedLayout;
26
+ }
27
+ export type Diagrams = Record<string, CompiledDiagram | undefined>;
28
+ /** The view a node's diagram shows when opened: the one named, the scenario's, or the first. */
29
+ export declare function openingView(diagram: CompiledDiagram, ref: NodeDiagram): string;
30
+ /** Whether a reader may show this node's diagram in place: it is at hand and not limited to `open`. */
31
+ export declare const canExpand: (ref: NodeDiagram | undefined, diagrams: Diagrams) => boolean;
32
+ /** Whether a reader may move into this node's diagram: it is at hand and not limited to `expand`. */
33
+ export declare const canOpen: (ref: NodeDiagram | undefined, diagrams: Diagrams) => boolean;
34
+ /** Paths are node ids joined with `/` (ids cannot contain it): `orders`, `orders/stock`. */
35
+ export declare const pathJoin: (prefix: string, id: string) => string;
36
+ /**
37
+ * The paths the author asked to start expanded (`expanded: true`), at every depth. A node counts
38
+ * wherever it sits in its diagram, in whichever view, so switching views loses nothing.
39
+ */
40
+ export declare function defaultExpanded(diagram: CompiledDiagram, diagrams: Diagrams, prefix?: string, seen?: readonly string[]): string[];
41
+ /**
42
+ * Every node whose card has to be measured to lay `viewId` out with these paths expanded: the
43
+ * view's own, and those of each diagram shown in place. `key` is the path, which is what the
44
+ * layout reads sizes by.
45
+ */
46
+ export declare function measurable(diagram: CompiledDiagram, viewId: string, expanded: ReadonlySet<string>, diagrams: Diagrams, prefix?: string): {
47
+ key: string;
48
+ node: CompiledNode;
49
+ }[];
50
+ export interface NestedOptions {
51
+ /** Measured card sizes by path. A plain node id for the diagram itself, `orders/api` inside. */
52
+ sizes?: Record<string, Partial<NodeSize>>;
53
+ /** Paths shown in place. */
54
+ expanded?: ReadonlySet<string>;
55
+ /** Where the diagrams a node opens are found, by name. */
56
+ diagrams?: Diagrams;
57
+ prefix?: string;
58
+ }
59
+ /** Lay out `viewId` with the nodes in `expanded` shown in place, each as a group around its diagram. */
60
+ export declare function layoutNested(diagram: CompiledDiagram, viewId: string, { sizes, expanded, diagrams, prefix }?: NestedOptions): Promise<NestedLayout>;
61
+ /**
62
+ * Where a node sits in the world of the diagram laid out first, however deep it is: the rectangle
63
+ * of `orders/stock` is `stock`'s inside `orders`, moved to where `orders` is. Undefined when a
64
+ * step of the path is not laid out (it is not expanded, or not in the view).
65
+ */
66
+ export declare function rectOfPath(nested: NestedLayout, path: string): Rect | undefined;
@@ -0,0 +1,24 @@
1
+ import type { Diagnostic, Where } from './compiler';
2
+ import type { CompiledDiagram } from './types';
3
+ /** What the linker needs to know about one diagram folder. `diagram` is missing when it failed to compile. */
4
+ export interface LinkEntry {
5
+ diagram?: CompiledDiagram;
6
+ where?: Where;
7
+ }
8
+ /**
9
+ * Every diagram that `name` can reach by opening nodes, directly or through other diagrams, but
10
+ * not `name` itself.
11
+ */
12
+ export declare function nestedDiagrams(all: Record<string, CompiledDiagram>, name: string): Record<string, CompiledDiagram>;
13
+ /**
14
+ * Give every diagram that opens others the diagrams it can reach, as `nested`, so wherever a
15
+ * compiled diagram goes its insides go with it and the viewer is told nothing. Diagrams that open
16
+ * nothing are returned as they are. Safe to call twice.
17
+ */
18
+ export declare function attachNested(all: Record<string, CompiledDiagram>): Record<string, CompiledDiagram>;
19
+ /**
20
+ * Check what no single diagram can: that a node opens a diagram which exists, that the view and
21
+ * scenario it asks for exist there, and that diagrams do not open each other in a circle (a node
22
+ * cannot contain itself).
23
+ */
24
+ export declare function linkDiagrams(entries: Record<string, LinkEntry>): Diagnostic[];
package/dist/node.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { CompiledDiagram } from './types';
3
3
  export * from './compile';
4
4
  export { loadIconSets, resolveIcon, searchIcons } from './icons';
5
5
  export { renderMarkdown } from './markdown';
6
+ export { attachNested, type LinkEntry, linkDiagrams, nestedDiagrams } from './nesting';
6
7
  export { authoringJsonSchema, FamilySchema, KNOWN_KEYS, RootSchema } from './schema';
7
8
  export interface LoadedProject {
8
9
  diagrams: Record<string, CompiledDiagram>;
package/dist/node.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ attachNested,
3
+ linkDiagrams,
4
+ nestedDiagrams
5
+ } from "./chunk-3QBMUILO.js";
1
6
  import {
2
7
  FamilySchema,
3
8
  KNOWN_KEYS,
@@ -6,8 +11,8 @@ import {
6
11
  compileDiagram,
7
12
  createIconResolver,
8
13
  renderMarkdown
9
- } from "./chunk-5BMICZRC.js";
10
- import "./chunk-FRVCGXIG.js";
14
+ } from "./chunk-S7M3BRFI.js";
15
+ import "./chunk-ITBBPZE6.js";
11
16
 
12
17
  // src/node.ts
13
18
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -48,6 +53,7 @@ function loadProject(root, dir = "diagrams") {
48
53
  const base = diagramsDir(root, dir);
49
54
  const result = { diagrams: {}, diagnostics: [], watch: [base] };
50
55
  if (!existsSync(base)) return result;
56
+ const entries = {};
51
57
  for (const name of readdirSync(base).sort()) {
52
58
  const folder = join(base, name);
53
59
  if (!statSync(folder).isDirectory()) continue;
@@ -59,7 +65,7 @@ function loadProject(root, dir = "diagrams") {
59
65
  file: join(scenarioDir, f),
60
66
  text: readFileSync2(join(scenarioDir, f), "utf8")
61
67
  })) : [];
62
- const { diagram, diagnostics } = compileDiagram2({
68
+ const { diagram, diagnostics, where } = compileDiagram2({
63
69
  name,
64
70
  file: relative(root, file),
65
71
  text: readFileSync2(file, "utf8"),
@@ -68,8 +74,11 @@ function loadProject(root, dir = "diagrams") {
68
74
  });
69
75
  result.diagnostics.push(...diagnostics);
70
76
  if (diagram) result.diagrams[name] = diagram;
77
+ entries[name] = { diagram, where };
71
78
  result.watch.push(folder);
72
79
  }
80
+ result.diagnostics.push(...linkDiagrams(entries));
81
+ result.diagrams = attachNested(result.diagrams);
73
82
  return result;
74
83
  }
75
84
  function readInside(folder, rel) {
@@ -91,12 +100,15 @@ export {
91
100
  FamilySchema,
92
101
  KNOWN_KEYS,
93
102
  RootSchema,
103
+ attachNested,
94
104
  authoringJsonSchema,
95
105
  compileDiagram2 as compileDiagram,
96
106
  diagramsDir,
97
107
  formatDiagnostic,
108
+ linkDiagrams,
98
109
  loadIconSets,
99
110
  loadProject,
111
+ nestedDiagrams,
100
112
  renderMarkdown,
101
113
  resolveIcon,
102
114
  searchIcons
@@ -0,0 +1,63 @@
1
+ import { type Diagrams } from './nested-layout';
2
+ import type { CompiledDiagram, CompiledView, IconData, Status } from './types';
3
+ /** One edge of a node, seen from that node. */
4
+ export interface OutlineConnection {
5
+ edge: string;
6
+ /** `out`: this node sends to `other`; `in`: it receives from `other`; `both`: edge goes either way. */
7
+ dir: 'out' | 'in' | 'both';
8
+ /** Id of the node at the other end, in the same diagram. */
9
+ other: string;
10
+ /** Its title, so a row needs nothing else to be read. */
11
+ otherTitle: string;
12
+ label?: string;
13
+ kind: string;
14
+ status: Status;
15
+ }
16
+ export interface OutlineNode {
17
+ type: 'node';
18
+ /** The node id, or ids joined with `/` for a node inside one shown in place (`orders/stock`). */
19
+ path: string;
20
+ id: string;
21
+ title: string;
22
+ sub?: string;
23
+ icon?: IconData;
24
+ status: Status;
25
+ connections: OutlineConnection[];
26
+ /** Present when the node holds a diagram the reader can reach. */
27
+ inside?: {
28
+ title: string;
29
+ /** The reader may show it in place, and may move into it. */
30
+ expand: boolean;
31
+ open: boolean;
32
+ /** Shown in place right now; `items` are then the nodes of that diagram. */
33
+ expanded: boolean;
34
+ items: OutlineItem[];
35
+ };
36
+ }
37
+ export interface OutlineGroup {
38
+ type: 'group';
39
+ /** Group id, prefixed like a node path so it is unique across the levels shown in place. */
40
+ path: string;
41
+ id: string;
42
+ title: string;
43
+ caption?: string;
44
+ icon?: IconData;
45
+ status: Status;
46
+ items: OutlineItem[];
47
+ /** How many nodes it holds, however deep. */
48
+ count: number;
49
+ }
50
+ export type OutlineItem = OutlineNode | OutlineGroup;
51
+ export interface OutlineOptions {
52
+ /** Where the diagrams a node holds are found, by name. */
53
+ diagrams?: Diagrams;
54
+ /** Paths shown in place. */
55
+ expanded?: ReadonlySet<string>;
56
+ prefix?: string;
57
+ /** Diagrams already on the way down, so a loop cannot run forever. */
58
+ seen?: readonly string[];
59
+ }
60
+ /** The nodes and groups of `view` as a tree, in the order the author wrote them. */
61
+ export declare function buildOutline(diagram: CompiledDiagram, view: CompiledView, { diagrams, expanded, prefix, seen }?: OutlineOptions): OutlineItem[];
62
+ /** Every node of an outline, in reading order, at any depth. */
63
+ export declare function outlineNodes(items: readonly OutlineItem[]): OutlineNode[];
@@ -0,0 +1,16 @@
1
+ import type { CompiledScenario } from './types';
2
+ export interface PhaseSpan {
3
+ /** Index in `scenario.phases`. */
4
+ index: number;
5
+ title: string;
6
+ caption?: string;
7
+ /** Index of its first and last step in `scenario.steps`. */
8
+ from: number;
9
+ to: number;
10
+ /** How many steps it holds. */
11
+ count: number;
12
+ }
13
+ /** Whether the author named the phases. A scenario written as plain `steps` has one nameless phase. */
14
+ export declare const hasPhases: (scenario: CompiledScenario) => boolean;
15
+ /** The phases that hold steps, in order. Steps follow their phase, so each phase is one run. */
16
+ export declare function phaseSpans(scenario: CompiledScenario): PhaseSpan[];
package/dist/schema.d.ts CHANGED
@@ -65,6 +65,17 @@ export declare const NodeSchema: z.ZodObject<{
65
65
  }, z.core.$strict>>>;
66
66
  refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
67
67
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
68
+ diagram: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
69
+ name: z.ZodString;
70
+ view: z.ZodOptional<z.ZodString>;
71
+ scenario: z.ZodOptional<z.ZodString>;
72
+ mode: z.ZodOptional<z.ZodEnum<{
73
+ both: "both";
74
+ expand: "expand";
75
+ open: "open";
76
+ }>>;
77
+ expanded: z.ZodOptional<z.ZodBoolean>;
78
+ }, z.core.$strict>]>>;
68
79
  }, z.core.$strict>;
69
80
  export declare const GroupSchema: z.ZodObject<{
70
81
  label: z.ZodString;
@@ -521,6 +532,17 @@ export declare const RootSchema: z.ZodObject<{
521
532
  }, z.core.$strict>>>;
522
533
  refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
523
534
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
535
+ diagram: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
536
+ name: z.ZodString;
537
+ view: z.ZodOptional<z.ZodString>;
538
+ scenario: z.ZodOptional<z.ZodString>;
539
+ mode: z.ZodOptional<z.ZodEnum<{
540
+ both: "both";
541
+ expand: "expand";
542
+ open: "open";
543
+ }>>;
544
+ expanded: z.ZodOptional<z.ZodBoolean>;
545
+ }, z.core.$strict>]>>;
524
546
  }, z.core.$strict>>;
525
547
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
526
548
  id: z.ZodOptional<z.ZodString>;
@@ -0,0 +1 @@
1
+ export declare function didYouMean(word: string, options: readonly string[]): string | undefined;
package/dist/types.d.ts CHANGED
@@ -19,6 +19,16 @@ export interface NodeLink {
19
19
  label: string;
20
20
  url: string;
21
21
  }
22
+ /** Another diagram that shows the inside of a node. Resolved by name against the project's diagrams. */
23
+ export interface NodeDiagram {
24
+ name: string;
25
+ view?: string;
26
+ scenario?: string;
27
+ /** How a reader can get at it: shown in place (`expand`), moved into (`open`), or either. */
28
+ mode?: 'both' | 'expand' | 'open';
29
+ /** Starts shown in place. */
30
+ expanded?: boolean;
31
+ }
22
32
  export interface CompiledNode {
23
33
  id: string;
24
34
  title: string;
@@ -37,6 +47,8 @@ export interface CompiledNode {
37
47
  links: NodeLink[];
38
48
  refs: string[];
39
49
  tags: string[];
50
+ /** Opens one level deeper: the diagram that describes this node's inside. */
51
+ diagram?: NodeDiagram;
40
52
  }
41
53
  export interface CompiledGroup {
42
54
  id: string;
@@ -145,6 +157,12 @@ export interface CompiledDiagram {
145
157
  edgeKinds: Record<string, EdgeKindDef>;
146
158
  views: CompiledView[];
147
159
  scenarios: CompiledScenario[];
160
+ /**
161
+ * The diagrams its nodes open, by name, and the ones those open in turn. Added once every
162
+ * diagram of the project has compiled (`attachNested`), so a diagram is all a viewer needs. The
163
+ * entries themselves carry no `nested`.
164
+ */
165
+ nested?: Record<string, CompiledDiagram>;
148
166
  }
149
167
  export interface Pt {
150
168
  x: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-package-labs/idocs-core",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {