@the-package-labs/idocs-core 0.3.1 → 0.5.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-Q2NHZ6TV.js";
10
+ import "./chunk-ZYFFV7Y7.js";
11
11
  export {
12
12
  KNOWN_KEYS,
13
13
  RootSchema,
@@ -0,0 +1,127 @@
1
+ import {
2
+ didYouMean
3
+ } from "./chunk-ZYFFV7Y7.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
+ };
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  BUILTIN_EDGE_KINDS,
3
3
  BUILTIN_KINDS,
4
+ DEFAULT_ENTITY_WIDTH,
5
+ didYouMean,
4
6
  letterKey
5
- } from "./chunk-FRVCGXIG.js";
7
+ } from "./chunk-ZYFFV7Y7.js";
6
8
 
7
9
  // src/icon-resolver.ts
8
10
  import { getIconData, iconToSVG } from "@iconify/utils";
@@ -214,10 +216,28 @@ var ChipSchema = z.union([
214
216
  z.string(),
215
217
  z.strictObject({ label: z.string(), sub: z.string().optional() })
216
218
  ]);
219
+ var FieldProps = z.strictObject({
220
+ type: z.string().optional().describe("Its type: `keyword`, `uuid`, `string[]`."),
221
+ key: z.boolean().optional().describe("Part of the primary key. Shown with a `#` in front."),
222
+ ref: z.string().optional().describe(
223
+ "The `node` or `node.field` this one points at, like a foreign key. Draws a relation between the two cards."
224
+ ),
225
+ note: z.string().optional().describe("Shown on hover and in the drawer.")
226
+ });
227
+ var FieldSchema = z.union([
228
+ z.string().min(1),
229
+ z.record(z.string().min(1), z.union([z.string(), z.null(), FieldProps])).refine((o) => Object.keys(o).length === 1, {
230
+ message: "One field per list item: `- name: type`"
231
+ })
232
+ ]);
217
233
  var NodeSchema = z.strictObject({
218
234
  title: z.string().optional().describe("Display name. Defaults to the node id."),
219
235
  sub: z.string().optional().describe("One line under the title, rendered in mono (ports, stack, version)."),
236
+ pill: z.string().optional().describe("Short label in the card header, like `TABLE` or `ES INDEX`."),
220
237
  lines: z.array(z.string()).optional().describe("Short body lines inside the card."),
238
+ fields: z.array(FieldSchema).optional().describe(
239
+ "Makes the card an entity: a table of fields, each `name: type`. `ref` points a field at another entity."
240
+ ),
221
241
  icon: z.string().optional().describe("`postgresql`, `logos:redis`, `lucide:server` or ./path/to.svg."),
222
242
  kind: z.string().optional().describe("Gives a default icon and colour family. See `kinds`."),
223
243
  family: FamilySchema.optional().describe("Accent colour family; overrides the kind default."),
@@ -231,7 +251,23 @@ var NodeSchema = z.strictObject({
231
251
  doc: z.string().optional().describe("Markdown file for the drawer. Defaults to nodes/<id>.md when present."),
232
252
  links: z.array(z.strictObject({ label: z.string(), url: z.string() })).optional(),
233
253
  refs: z.array(z.string()).optional().describe("Source files or URLs this node is based on."),
234
- tags: z.array(z.string()).optional()
254
+ tags: z.array(z.string()).optional(),
255
+ diagram: z.union([
256
+ z.string().min(1),
257
+ z.strictObject({
258
+ name: z.string().min(1).describe("Folder name of the diagram, under diagrams/."),
259
+ view: Id.optional().describe(
260
+ "View to open first. Defaults to the scenario view, then the first."
261
+ ),
262
+ scenario: Id.optional().describe("Scenario to select when the diagram opens."),
263
+ mode: z.enum(["both", "expand", "open"]).optional().describe(
264
+ "both (default): the reader can expand it in place or open it. expand: only in place. open: only move into it."
265
+ ),
266
+ expanded: z.boolean().optional().describe("Start expanded in place, so the whole inner diagram shows as a group.")
267
+ })
268
+ ]).optional().describe(
269
+ "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."
270
+ )
235
271
  });
236
272
  var GroupSchema = z.strictObject({
237
273
  label: z.string(),
@@ -337,10 +373,31 @@ var ScenarioSchema = ScenarioBase.refine(
337
373
  (s) => (s.phases?.length ?? 0) + (s.steps?.length ?? 0) > 0,
338
374
  { message: "A scenario needs `steps` or `phases`" }
339
375
  );
376
+ var DefaultsSchema = z.strictObject({
377
+ view: Id.optional().describe(
378
+ "View shown first. Defaults to the view of `scenario`, or else the first view."
379
+ ),
380
+ scenario: Id.optional().describe(
381
+ "Scenario selected first. Without it the diagram opens on its overview. It does not play until a reader presses play, unless `autoplay` is on."
382
+ ),
383
+ mode: z.enum(["flow", "sequence", "story"]).optional().describe(
384
+ "How a scenario is drawn when it has no `mode` of its own: packets over the diagram, a sequence diagram, or a swimlane story."
385
+ ),
386
+ sidebar: z.boolean().optional().describe(
387
+ "Whether the panel over the picture starts open. By default it does when there is a scenario or another view to choose, and the frame is wide enough."
388
+ ),
389
+ subtitles: z.boolean().optional().describe("Words over the picture saying what the scenario is doing. On by default."),
390
+ autoplay: z.boolean().optional().describe(
391
+ "Play `scenario` as soon as the diagram is on screen. Needs `scenario`, and is ignored when the system asks for reduced motion."
392
+ )
393
+ });
340
394
  var RootSchema = z.strictObject({
341
395
  $schema: z.string().optional(),
342
396
  title: z.string(),
343
397
  description: z.string().optional().describe("Markdown shown above the first view."),
398
+ defaults: DefaultsSchema.optional().describe(
399
+ "How the diagram opens: its view, scenario and mode, and whether the panel and subtitles show. A page can still choose otherwise with the props of `<Diagram>`."
400
+ ),
344
401
  kinds: z.record(Id, z.strictObject({ icon: z.string().optional(), family: FamilySchema.optional() })).optional(),
345
402
  edgeKinds: z.record(
346
403
  Id,
@@ -363,7 +420,8 @@ var KNOWN_KEYS = {
363
420
  group: Object.keys(GroupSchema.shape),
364
421
  edge: Object.keys(EdgeSchema.shape),
365
422
  view: Object.keys(ViewSchema.shape),
366
- scenario: Object.keys(ScenarioBase.shape)
423
+ scenario: Object.keys(ScenarioBase.shape),
424
+ defaults: Object.keys(DefaultsSchema.shape)
367
425
  };
368
426
  function authoringJsonSchema() {
369
427
  const schema = z.toJSONSchema(RootSchema, { io: "input", unrepresentable: "any" });
@@ -449,15 +507,17 @@ function compileDiagram(src, assets) {
449
507
  scenarios[f.id] = body;
450
508
  scenarioSources.set(f.id, s);
451
509
  }
452
- const report = (severity, message, path, hint) => {
510
+ const where = (path) => {
453
511
  let source = main;
454
512
  let p = path;
455
513
  if (path[0] === "scenarios" && typeof path[1] === "string" && scenarioSources.has(path[1])) {
456
514
  source = scenarioSources.get(path[1]);
457
515
  p = path.slice(2);
458
516
  }
459
- const pos = locate(source, p);
460
- diagnostics.push({ severity, message, file: source.file, ...pos, path: path.join("."), hint });
517
+ return { file: source.file, ...locate(source, p) };
518
+ };
519
+ const report = (severity, message, path, hint) => {
520
+ diagnostics.push({ severity, message, ...where(path), path: path.join("."), hint });
461
521
  };
462
522
  const error = (message, path, hint) => report("error", message, path, hint);
463
523
  const warn = (message, path, hint) => report("warning", message, path, hint);
@@ -473,7 +533,7 @@ function compileDiagram(src, assets) {
473
533
  }
474
534
  const diagram = build(parsed.data, src, error, warn, assets);
475
535
  if (diagnostics.some((d) => d.severity === "error")) return { diagnostics };
476
- return { diagram, diagnostics };
536
+ return Object.defineProperty({ diagram, diagnostics }, "where", { value: where });
477
537
  }
478
538
  function build(data, src, error, warn, { resolveIcon }) {
479
539
  const kinds = { ...BUILTIN_KINDS };
@@ -570,6 +630,7 @@ function build(data, src, error, warn, { resolveIcon }) {
570
630
  const fixParents = Object.values(groups);
571
631
  for (const g of fixParents) g.style = g.parent ? "frame" : "zone";
572
632
  const nodes = {};
633
+ const refTexts = [];
573
634
  for (const [id, n] of Object.entries(data.nodes)) {
574
635
  if (n.kind && !kinds[n.kind]) {
575
636
  warn(
@@ -586,16 +647,38 @@ function build(data, src, error, warn, { resolveIcon }) {
586
647
  didYouMean(n.in, Object.keys(groups))
587
648
  );
588
649
  }
650
+ if (typeof n.diagram === "object" && n.diagram.expanded && n.diagram.mode === "open") {
651
+ warn(
652
+ "`expanded` has no effect with `mode: open`",
653
+ ["nodes", id, "diagram", "expanded"],
654
+ "Use `mode: expand` or `mode: both`, or drop `expanded`."
655
+ );
656
+ }
589
657
  const docPath = n.doc ?? `nodes/${id}.md`;
590
658
  const docText = src.readFile?.(docPath);
591
659
  if (n.doc && docText === void 0)
592
660
  warn(`Doc file "${n.doc}" was not found`, ["nodes", id, "doc"]);
593
661
  const docHtml = [n.description, docText].filter((t) => !!t).map(renderMarkdown).join("");
662
+ const fields = (n.fields ?? []).map(toField);
663
+ const seenFields = /* @__PURE__ */ new Set();
664
+ fields.forEach((f, i) => {
665
+ if (seenFields.has(f.name)) {
666
+ error(`Field "${f.name}" appears twice in "${id}"`, ["nodes", id, "fields", i]);
667
+ }
668
+ seenFields.add(f.name);
669
+ });
670
+ n.fields?.forEach((item, i) => {
671
+ const ref = typeof item === "object" && item ? Object.values(item)[0] : void 0;
672
+ if (ref && typeof ref === "object" && ref.ref !== void 0)
673
+ refTexts.push({ node: id, index: i, text: ref.ref });
674
+ });
594
675
  nodes[id] = {
595
676
  id,
596
677
  title: n.title ?? id,
597
678
  sub: n.sub,
679
+ pill: n.pill,
598
680
  lines: n.lines ?? [],
681
+ fields,
599
682
  icon: iconFor(n.icon ?? kind?.icon, ["nodes", id, n.icon ? "icon" : "kind"]),
600
683
  kind: n.kind,
601
684
  family: n.family ?? kind?.family,
@@ -604,25 +687,47 @@ function build(data, src, error, warn, { resolveIcon }) {
604
687
  badge: n.badge === void 0 ? void 0 : String(n.badge),
605
688
  uses: n.uses ?? [],
606
689
  chips: (n.chips ?? []).map((c) => typeof c === "string" ? { label: c } : c),
607
- w: n.w,
690
+ w: n.w ?? (fields.length ? DEFAULT_ENTITY_WIDTH : void 0),
608
691
  docHtml: docHtml || void 0,
609
692
  links: n.links ?? [],
610
693
  refs: n.refs ?? [],
611
- tags: n.tags ?? []
694
+ tags: n.tags ?? [],
695
+ diagram: typeof n.diagram === "string" ? { name: n.diagram } : n.diagram
612
696
  };
613
697
  }
698
+ for (const { node: id, index, text } of refTexts) {
699
+ const found = resolveEnd(text, nodes);
700
+ if ("end" in found) {
701
+ nodes[id].fields[index].ref = found.end;
702
+ continue;
703
+ }
704
+ const name = nodes[id].fields[index].name;
705
+ error(
706
+ found.message ?? `Field "${name}" points at "${text}", which is not a node`,
707
+ ["nodes", id, "fields", index, name, "ref"],
708
+ found.hint ?? didYouMean(text, Object.keys(nodes))
709
+ );
710
+ }
614
711
  const edges = {};
615
712
  const nodeIds = Object.keys(nodes);
616
713
  (data.edges ?? []).forEach((e, i) => {
617
714
  const path = ["edges", i];
618
715
  let ok = true;
716
+ const ends = {};
619
717
  for (const end of ["from", "to"]) {
620
- if (!nodes[e[end]]) {
621
- error(`Edge ${end} "${e[end]}" is not a node`, [...path, end], didYouMean(e[end], nodeIds));
718
+ const found = resolveEnd(e[end], nodes);
719
+ if ("end" in found) ends[end] = found.end;
720
+ else {
721
+ error(
722
+ found.message ?? `Edge ${end} "${e[end]}" is not a node`,
723
+ [...path, end],
724
+ found.hint ?? didYouMean(e[end], nodeIds)
725
+ );
622
726
  ok = false;
623
727
  }
624
728
  }
625
- const kind = e.kind ?? "http";
729
+ const between = ends.from && ends.to && [ends.from, ends.to].every((x) => nodes[x.node].fields.length);
730
+ const kind = e.kind ?? (between ? "relation" : "http");
626
731
  if (!edgeKinds[kind]) {
627
732
  error(
628
733
  `Unknown edge kind "${kind}"`,
@@ -631,7 +736,7 @@ function build(data, src, error, warn, { resolveIcon }) {
631
736
  );
632
737
  ok = false;
633
738
  }
634
- if (e.from === e.to) {
739
+ if (ok && ends.from.node === ends.to.node) {
635
740
  warn("Edge connects a node to itself and is not drawn", path);
636
741
  ok = false;
637
742
  }
@@ -643,8 +748,10 @@ function build(data, src, error, warn, { resolveIcon }) {
643
748
  }
644
749
  edges[id] = {
645
750
  id,
646
- from: e.from,
647
- to: e.to,
751
+ from: ends.from.node,
752
+ to: ends.to.node,
753
+ fromField: ends.from.field,
754
+ toField: ends.to.field,
648
755
  label: e.label,
649
756
  kind,
650
757
  both: e.both ?? false,
@@ -657,6 +764,32 @@ function build(data, src, error, warn, { resolveIcon }) {
657
764
  noteHtml: e.note ? renderMarkdown(e.note) : void 0
658
765
  };
659
766
  });
767
+ const written = new Set(
768
+ Object.values(edges).flatMap((e) => {
769
+ const a = `${e.from}.${e.fromField ?? ""}`;
770
+ const b = `${e.to}.${e.toField ?? ""}`;
771
+ return [`${a}>${b}`, `${b}>${a}`];
772
+ })
773
+ );
774
+ for (const node of Object.values(nodes)) {
775
+ for (const f of node.fields) {
776
+ if (!f.ref || f.ref.node === node.id) continue;
777
+ const key = `${node.id}.${f.name}>${f.ref.node}.${f.ref.field ?? ""}`;
778
+ if (written.has(key)) continue;
779
+ const id = `ref:${node.id}.${f.name}`;
780
+ edges[id] = {
781
+ id,
782
+ from: node.id,
783
+ to: f.ref.node,
784
+ fromField: f.name,
785
+ toField: f.ref.field,
786
+ kind: "relation",
787
+ both: false,
788
+ hidden: false,
789
+ status: "built"
790
+ };
791
+ }
792
+ }
660
793
  const viewDefs = Object.keys(data.views ?? {}).length ? data.views : { main: { title: data.title } };
661
794
  const views = Object.entries(viewDefs).map(
662
795
  ([id, v]) => buildView(
@@ -674,10 +807,12 @@ function build(data, src, error, warn, { resolveIcon }) {
674
807
  const sc = buildScenario(id, s, views, nodes, edges, error, warn);
675
808
  if (sc) scenarios.push(sc);
676
809
  }
810
+ const defaults = buildDefaults(data.defaults, views, scenarios, error);
677
811
  return {
678
812
  name: src.name,
679
813
  title: data.title,
680
814
  descriptionHtml: data.description ? renderMarkdown(data.description) : void 0,
815
+ defaults,
681
816
  nodes,
682
817
  groups,
683
818
  edges,
@@ -686,6 +821,37 @@ function build(data, src, error, warn, { resolveIcon }) {
686
821
  scenarios
687
822
  };
688
823
  }
824
+ function toField(item) {
825
+ if (typeof item === "string") return { name: item };
826
+ const [name, value] = Object.entries(item)[0];
827
+ if (value == null) return { name };
828
+ if (typeof value === "string") return { name, type: value };
829
+ const { ref: _ref, ...rest } = value;
830
+ return { name, ...rest };
831
+ }
832
+ function resolveEnd(text, nodes) {
833
+ if (nodes[text]) return { end: { node: text } };
834
+ const matches = [];
835
+ let known;
836
+ for (let i = text.indexOf("."); i > 0; i = text.indexOf(".", i + 1)) {
837
+ const node = nodes[text.slice(0, i)];
838
+ if (!node) continue;
839
+ known ??= node;
840
+ const field2 = text.slice(i + 1);
841
+ if (node.fields.some((f) => f.name === field2)) matches.push({ node: node.id, field: field2 });
842
+ }
843
+ if (matches.length === 1) return { end: matches[0] };
844
+ if (matches.length > 1) {
845
+ return { message: `"${text}" could be more than one field`, hint: "Rename a node or a field." };
846
+ }
847
+ if (!known) return {};
848
+ const field = text.slice(known.id.length + 1);
849
+ const names = known.fields.map((f) => f.name);
850
+ return {
851
+ message: `Node "${known.id}" has no field "${field}"`,
852
+ hint: didYouMean(field, names) ?? (names.length ? `Fields: ${names.join(", ")}` : void 0)
853
+ };
854
+ }
689
855
  function buildView(id, v, diagramTitle, nodes, groups, edges, error) {
690
856
  const known = [...Object.keys(nodes), ...Object.keys(groups)];
691
857
  const members = (gid, out) => {
@@ -738,6 +904,38 @@ function buildView(id, v, diagramTitle, nodes, groups, edges, error) {
738
904
  interfaces: v.interfaces ?? false
739
905
  };
740
906
  }
907
+ function buildDefaults(d, views, scenarios, error) {
908
+ if (!d) return void 0;
909
+ const ids = (list) => list.map((x) => x.id);
910
+ const view = views.find((v) => v.id === d.view);
911
+ if (d.view !== void 0 && !view) {
912
+ error(
913
+ `defaults.view "${d.view}" is not a view`,
914
+ ["defaults", "view"],
915
+ didYouMean(d.view, ids(views))
916
+ );
917
+ }
918
+ const scenario = scenarios.find((s) => s.id === d.scenario);
919
+ if (d.scenario !== void 0 && !scenario) {
920
+ error(
921
+ `defaults.scenario "${d.scenario}" is not a scenario`,
922
+ ["defaults", "scenario"],
923
+ didYouMean(d.scenario, ids(scenarios))
924
+ );
925
+ }
926
+ if (view && scenario && scenario.view !== view.id) {
927
+ error(
928
+ `defaults.scenario "${scenario.id}" plays on view "${scenario.view}", not "${view.id}"`,
929
+ ["defaults", "scenario"],
930
+ `Drop \`view\` and the diagram opens on "${scenario.view}", or pick a scenario of "${view.id}".`
931
+ );
932
+ }
933
+ if (d.autoplay && d.scenario === void 0) {
934
+ error("defaults.autoplay needs a defaults.scenario to play", ["defaults", "autoplay"]);
935
+ }
936
+ const out = Object.fromEntries(Object.entries(d).filter(([, v]) => v !== void 0));
937
+ return Object.keys(out).length ? out : void 0;
938
+ }
741
939
  function buildScenario(id, s, views, nodes, edges, error, warn) {
742
940
  const viewId = s.view ?? views[0].id;
743
941
  const view = views.find((v) => v.id === viewId);
@@ -1015,6 +1213,7 @@ function describeIssue(issue) {
1015
1213
  function allowedKeysFor(path) {
1016
1214
  const [section] = path;
1017
1215
  if (path.length === 0) return KNOWN_KEYS.root;
1216
+ if (path.length === 1) return section === "defaults" ? KNOWN_KEYS.defaults : void 0;
1018
1217
  if (path.length !== 2) return void 0;
1019
1218
  switch (section) {
1020
1219
  case "nodes":
@@ -1031,31 +1230,6 @@ function allowedKeysFor(path) {
1031
1230
  return void 0;
1032
1231
  }
1033
1232
  }
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
1233
 
1060
1234
  export {
1061
1235
  createIconResolver,
@@ -17,7 +17,8 @@ var BUILTIN_KINDS = {
17
17
  network: { icon: "lucide:network", family: "violet" },
18
18
  vm: { icon: "lucide:box", family: "violet" },
19
19
  auth: { icon: "lucide:key-round", family: "teal" },
20
- monitor: { icon: "lucide:activity", family: "teal" }
20
+ monitor: { icon: "lucide:activity", family: "teal" },
21
+ entity: { icon: "lucide:table-2", family: "blue" }
21
22
  };
22
23
  var BUILTIN_EDGE_KINDS = {
23
24
  http: { label: "HTTP request / response", color: "ink", width: 1.5 },
@@ -25,7 +26,8 @@ var BUILTIN_EDGE_KINDS = {
25
26
  tunnel: { label: "Overlay tunnel", color: "tunnel", width: 5 },
26
27
  queue: { label: "Queue / broker", color: "ok", width: 2, dash: "2 4" },
27
28
  ws: { label: "WebSocket", color: "ink", width: 1.6, dash: "7 4" },
28
- data: { label: "Data access", color: "muted", width: 1.3 }
29
+ data: { label: "Data access", color: "muted", width: 1.3 },
30
+ relation: { label: "Relation between entities", color: "muted", width: 1.4 }
29
31
  };
30
32
  var STEP_KIND_COLORS = {
31
33
  request: "accent",
@@ -35,6 +37,7 @@ var STEP_KIND_COLORS = {
35
37
  event: "warn"
36
38
  };
37
39
  var DEFAULT_NODE_WIDTH = 178;
40
+ var DEFAULT_ENTITY_WIDTH = 280;
38
41
 
39
42
  // src/geometry.ts
40
43
  function polylineLength(points) {
@@ -104,14 +107,43 @@ function letterKey(index) {
104
107
  return out;
105
108
  }
106
109
 
110
+ // src/suggest.ts
111
+ function didYouMean(word, options) {
112
+ let best;
113
+ let bestDist = Math.max(2, Math.floor(word.length / 3)) + 1;
114
+ for (const o of options) {
115
+ const d = levenshtein(word.toLowerCase(), o.toLowerCase());
116
+ if (d < bestDist) {
117
+ best = o;
118
+ bestDist = d;
119
+ }
120
+ }
121
+ return best ? `Did you mean "${best}"?` : void 0;
122
+ }
123
+ function levenshtein(a, b) {
124
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
125
+ for (let i = 1; i <= a.length; i++) {
126
+ let prev = row[0];
127
+ row[0] = i;
128
+ for (let j = 1; j <= b.length; j++) {
129
+ const tmp = row[j];
130
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
131
+ prev = tmp;
132
+ }
133
+ }
134
+ return row[b.length];
135
+ }
136
+
107
137
  export {
108
138
  BUILTIN_KINDS,
109
139
  BUILTIN_EDGE_KINDS,
110
140
  STEP_KIND_COLORS,
111
141
  DEFAULT_NODE_WIDTH,
142
+ DEFAULT_ENTITY_WIDTH,
112
143
  polylineLength,
113
144
  dist,
114
145
  pointAt,
115
146
  roundedPath,
116
- letterKey
147
+ letterKey,
148
+ didYouMean
117
149
  };
@@ -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;
@@ -8,3 +8,5 @@ export declare const BUILTIN_KINDS: Record<string, KindDef>;
8
8
  export declare const BUILTIN_EDGE_KINDS: Record<string, EdgeKindDef>;
9
9
  export declare const STEP_KIND_COLORS: Record<string, string>;
10
10
  export declare const DEFAULT_NODE_WIDTH = 178;
11
+ /** Cards with fields need room for a name and a type side by side. */
12
+ export declare const DEFAULT_ENTITY_WIDTH = 280;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,13 @@
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';
9
+ export * from './scrubber';
10
+ export * from './start';
5
11
  export * from './story';
6
12
  export * from './svg';
7
13
  export * from './timeline';