@the-package-labs/idocs-core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  BUILTIN_EDGE_KINDS,
3
3
  BUILTIN_KINDS,
4
+ DEFAULT_ENTITY_WIDTH,
4
5
  didYouMean,
5
- letterKey
6
- } from "./chunk-ITBBPZE6.js";
6
+ highlightSegments,
7
+ languageLabel,
8
+ letterKey,
9
+ splitPlaceholders,
10
+ takesPlaceholders
11
+ } from "./chunk-H77C3DSX.js";
7
12
 
8
13
  // src/icon-resolver.ts
9
14
  import { getIconData, iconToSVG } from "@iconify/utils";
@@ -103,7 +108,62 @@ import remarkGfm from "remark-gfm";
103
108
  import remarkParse from "remark-parse";
104
109
  import remarkRehype from "remark-rehype";
105
110
  import { unified } from "unified";
106
- var processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeStringify);
111
+ var isElement = (n, tag) => n.type === "element" && (tag === void 0 || n.tagName === tag);
112
+ var classesOf = (el2) => {
113
+ const c = el2.properties.className;
114
+ return Array.isArray(c) ? c.map(String) : typeof c === "string" ? c.split(/\s+/) : [];
115
+ };
116
+ var titleOf = (meta) => {
117
+ const m = meta?.match(/\btitle=(?:"([^"]*)"|'([^']*)')/);
118
+ return m?.[1] ?? m?.[2];
119
+ };
120
+ var el = (tagName, properties, children = []) => ({ type: "element", tagName, properties, children });
121
+ function commandBlocks() {
122
+ const wrap = (pre) => {
123
+ const code = pre.children.find((c) => isElement(c, "code"));
124
+ if (!code || !isElement(code)) return pre;
125
+ const lang = classesOf(code).find((c) => c.startsWith("language-"))?.slice("language-".length);
126
+ const title = titleOf(code.data?.meta);
127
+ if (code.children.every((c) => c.type === "text")) {
128
+ const text = code.children.map((c) => c.value).join("");
129
+ const segments = takesPlaceholders(lang, code.data?.meta) ? splitPlaceholders(text) : [{ kind: "text", text }];
130
+ code.children = highlightSegments(lang, segments).map((piece) => {
131
+ if (piece.kind === "var") {
132
+ const { key, raw } = piece.segment;
133
+ return el("span", { className: ["idocs-ph"], dataKey: key, dataRaw: raw }, [
134
+ { type: "text", value: raw }
135
+ ]);
136
+ }
137
+ return piece.cls ? el("span", { className: [`hl-${piece.cls}`] }, [{ type: "text", value: piece.text }]) : { type: "text", value: piece.text };
138
+ });
139
+ }
140
+ return el(
141
+ "div",
142
+ { className: ["idocs-cmd"], dataIdocsCmd: "", ...lang ? { dataLang: lang } : {} },
143
+ [
144
+ el("div", { className: ["idocs-cmd-head"] }, [
145
+ el("span", { className: ["idocs-cmd-lang"] }, [
146
+ { type: "text", value: languageLabel(lang) }
147
+ ]),
148
+ ...title ? [el("span", { className: ["idocs-cmd-title"] }, [{ type: "text", value: title }])] : [],
149
+ el("div", { className: ["idocs-cmd-bar"] })
150
+ ]),
151
+ pre,
152
+ el("div", { className: ["idocs-cmd-panel"] })
153
+ ]
154
+ );
155
+ };
156
+ const walk = (node) => {
157
+ if (!("children" in node) || !node.children) return;
158
+ node.children = node.children.map((c) => {
159
+ if (isElement(c, "pre")) return wrap(c);
160
+ walk(c);
161
+ return c;
162
+ });
163
+ };
164
+ return (tree) => walk(tree);
165
+ }
166
+ var processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(commandBlocks).use(rehypeStringify);
107
167
  function renderMarkdown(md) {
108
168
  const body = stripFrontmatter(md).trim();
109
169
  if (!body) return "";
@@ -215,10 +275,28 @@ var ChipSchema = z.union([
215
275
  z.string(),
216
276
  z.strictObject({ label: z.string(), sub: z.string().optional() })
217
277
  ]);
278
+ var FieldProps = z.strictObject({
279
+ type: z.string().optional().describe("Its type: `keyword`, `uuid`, `string[]`."),
280
+ key: z.boolean().optional().describe("Part of the primary key. Shown with a `#` in front."),
281
+ ref: z.string().optional().describe(
282
+ "The `node` or `node.field` this one points at, like a foreign key. Draws a relation between the two cards."
283
+ ),
284
+ note: z.string().optional().describe("Shown on hover and in the drawer.")
285
+ });
286
+ var FieldSchema = z.union([
287
+ z.string().min(1),
288
+ z.record(z.string().min(1), z.union([z.string(), z.null(), FieldProps])).refine((o) => Object.keys(o).length === 1, {
289
+ message: "One field per list item: `- name: type`"
290
+ })
291
+ ]);
218
292
  var NodeSchema = z.strictObject({
219
293
  title: z.string().optional().describe("Display name. Defaults to the node id."),
220
294
  sub: z.string().optional().describe("One line under the title, rendered in mono (ports, stack, version)."),
295
+ pill: z.string().optional().describe("Short label in the card header, like `TABLE` or `ES INDEX`."),
221
296
  lines: z.array(z.string()).optional().describe("Short body lines inside the card."),
297
+ fields: z.array(FieldSchema).optional().describe(
298
+ "Makes the card an entity: a table of fields, each `name: type`. `ref` points a field at another entity."
299
+ ),
222
300
  icon: z.string().optional().describe("`postgresql`, `logos:redis`, `lucide:server` or ./path/to.svg."),
223
301
  kind: z.string().optional().describe("Gives a default icon and colour family. See `kinds`."),
224
302
  family: FamilySchema.optional().describe("Accent colour family; overrides the kind default."),
@@ -354,10 +432,31 @@ var ScenarioSchema = ScenarioBase.refine(
354
432
  (s) => (s.phases?.length ?? 0) + (s.steps?.length ?? 0) > 0,
355
433
  { message: "A scenario needs `steps` or `phases`" }
356
434
  );
435
+ var DefaultsSchema = z.strictObject({
436
+ view: Id.optional().describe(
437
+ "View shown first. Defaults to the view of `scenario`, or else the first view."
438
+ ),
439
+ scenario: Id.optional().describe(
440
+ "Scenario selected first. Without it the diagram opens on its overview. It does not play until a reader presses play, unless `autoplay` is on."
441
+ ),
442
+ mode: z.enum(["flow", "sequence", "story"]).optional().describe(
443
+ "How a scenario is drawn when it has no `mode` of its own: packets over the diagram, a sequence diagram, or a swimlane story."
444
+ ),
445
+ sidebar: z.boolean().optional().describe(
446
+ "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."
447
+ ),
448
+ subtitles: z.boolean().optional().describe("Words over the picture saying what the scenario is doing. On by default."),
449
+ autoplay: z.boolean().optional().describe(
450
+ "Play `scenario` as soon as the diagram is on screen. Needs `scenario`, and is ignored when the system asks for reduced motion."
451
+ )
452
+ });
357
453
  var RootSchema = z.strictObject({
358
454
  $schema: z.string().optional(),
359
455
  title: z.string(),
360
456
  description: z.string().optional().describe("Markdown shown above the first view."),
457
+ defaults: DefaultsSchema.optional().describe(
458
+ "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>`."
459
+ ),
361
460
  kinds: z.record(Id, z.strictObject({ icon: z.string().optional(), family: FamilySchema.optional() })).optional(),
362
461
  edgeKinds: z.record(
363
462
  Id,
@@ -380,7 +479,8 @@ var KNOWN_KEYS = {
380
479
  group: Object.keys(GroupSchema.shape),
381
480
  edge: Object.keys(EdgeSchema.shape),
382
481
  view: Object.keys(ViewSchema.shape),
383
- scenario: Object.keys(ScenarioBase.shape)
482
+ scenario: Object.keys(ScenarioBase.shape),
483
+ defaults: Object.keys(DefaultsSchema.shape)
384
484
  };
385
485
  function authoringJsonSchema() {
386
486
  const schema = z.toJSONSchema(RootSchema, { io: "input", unrepresentable: "any" });
@@ -589,6 +689,7 @@ function build(data, src, error, warn, { resolveIcon }) {
589
689
  const fixParents = Object.values(groups);
590
690
  for (const g of fixParents) g.style = g.parent ? "frame" : "zone";
591
691
  const nodes = {};
692
+ const refTexts = [];
592
693
  for (const [id, n] of Object.entries(data.nodes)) {
593
694
  if (n.kind && !kinds[n.kind]) {
594
695
  warn(
@@ -617,11 +718,26 @@ function build(data, src, error, warn, { resolveIcon }) {
617
718
  if (n.doc && docText === void 0)
618
719
  warn(`Doc file "${n.doc}" was not found`, ["nodes", id, "doc"]);
619
720
  const docHtml = [n.description, docText].filter((t) => !!t).map(renderMarkdown).join("");
721
+ const fields = (n.fields ?? []).map(toField);
722
+ const seenFields = /* @__PURE__ */ new Set();
723
+ fields.forEach((f, i) => {
724
+ if (seenFields.has(f.name)) {
725
+ error(`Field "${f.name}" appears twice in "${id}"`, ["nodes", id, "fields", i]);
726
+ }
727
+ seenFields.add(f.name);
728
+ });
729
+ n.fields?.forEach((item, i) => {
730
+ const ref = typeof item === "object" && item ? Object.values(item)[0] : void 0;
731
+ if (ref && typeof ref === "object" && ref.ref !== void 0)
732
+ refTexts.push({ node: id, index: i, text: ref.ref });
733
+ });
620
734
  nodes[id] = {
621
735
  id,
622
736
  title: n.title ?? id,
623
737
  sub: n.sub,
738
+ pill: n.pill,
624
739
  lines: n.lines ?? [],
740
+ fields,
625
741
  icon: iconFor(n.icon ?? kind?.icon, ["nodes", id, n.icon ? "icon" : "kind"]),
626
742
  kind: n.kind,
627
743
  family: n.family ?? kind?.family,
@@ -630,7 +746,7 @@ function build(data, src, error, warn, { resolveIcon }) {
630
746
  badge: n.badge === void 0 ? void 0 : String(n.badge),
631
747
  uses: n.uses ?? [],
632
748
  chips: (n.chips ?? []).map((c) => typeof c === "string" ? { label: c } : c),
633
- w: n.w,
749
+ w: n.w ?? (fields.length ? DEFAULT_ENTITY_WIDTH : void 0),
634
750
  docHtml: docHtml || void 0,
635
751
  links: n.links ?? [],
636
752
  refs: n.refs ?? [],
@@ -638,18 +754,39 @@ function build(data, src, error, warn, { resolveIcon }) {
638
754
  diagram: typeof n.diagram === "string" ? { name: n.diagram } : n.diagram
639
755
  };
640
756
  }
757
+ for (const { node: id, index, text } of refTexts) {
758
+ const found = resolveEnd(text, nodes);
759
+ if ("end" in found) {
760
+ nodes[id].fields[index].ref = found.end;
761
+ continue;
762
+ }
763
+ const name = nodes[id].fields[index].name;
764
+ error(
765
+ found.message ?? `Field "${name}" points at "${text}", which is not a node`,
766
+ ["nodes", id, "fields", index, name, "ref"],
767
+ found.hint ?? didYouMean(text, Object.keys(nodes))
768
+ );
769
+ }
641
770
  const edges = {};
642
771
  const nodeIds = Object.keys(nodes);
643
772
  (data.edges ?? []).forEach((e, i) => {
644
773
  const path = ["edges", i];
645
774
  let ok = true;
775
+ const ends = {};
646
776
  for (const end of ["from", "to"]) {
647
- if (!nodes[e[end]]) {
648
- error(`Edge ${end} "${e[end]}" is not a node`, [...path, end], didYouMean(e[end], nodeIds));
777
+ const found = resolveEnd(e[end], nodes);
778
+ if ("end" in found) ends[end] = found.end;
779
+ else {
780
+ error(
781
+ found.message ?? `Edge ${end} "${e[end]}" is not a node`,
782
+ [...path, end],
783
+ found.hint ?? didYouMean(e[end], nodeIds)
784
+ );
649
785
  ok = false;
650
786
  }
651
787
  }
652
- const kind = e.kind ?? "http";
788
+ const between = ends.from && ends.to && [ends.from, ends.to].every((x) => nodes[x.node].fields.length);
789
+ const kind = e.kind ?? (between ? "relation" : "http");
653
790
  if (!edgeKinds[kind]) {
654
791
  error(
655
792
  `Unknown edge kind "${kind}"`,
@@ -658,7 +795,7 @@ function build(data, src, error, warn, { resolveIcon }) {
658
795
  );
659
796
  ok = false;
660
797
  }
661
- if (e.from === e.to) {
798
+ if (ok && ends.from.node === ends.to.node) {
662
799
  warn("Edge connects a node to itself and is not drawn", path);
663
800
  ok = false;
664
801
  }
@@ -670,8 +807,10 @@ function build(data, src, error, warn, { resolveIcon }) {
670
807
  }
671
808
  edges[id] = {
672
809
  id,
673
- from: e.from,
674
- to: e.to,
810
+ from: ends.from.node,
811
+ to: ends.to.node,
812
+ fromField: ends.from.field,
813
+ toField: ends.to.field,
675
814
  label: e.label,
676
815
  kind,
677
816
  both: e.both ?? false,
@@ -684,6 +823,32 @@ function build(data, src, error, warn, { resolveIcon }) {
684
823
  noteHtml: e.note ? renderMarkdown(e.note) : void 0
685
824
  };
686
825
  });
826
+ const written = new Set(
827
+ Object.values(edges).flatMap((e) => {
828
+ const a = `${e.from}.${e.fromField ?? ""}`;
829
+ const b = `${e.to}.${e.toField ?? ""}`;
830
+ return [`${a}>${b}`, `${b}>${a}`];
831
+ })
832
+ );
833
+ for (const node of Object.values(nodes)) {
834
+ for (const f of node.fields) {
835
+ if (!f.ref || f.ref.node === node.id) continue;
836
+ const key = `${node.id}.${f.name}>${f.ref.node}.${f.ref.field ?? ""}`;
837
+ if (written.has(key)) continue;
838
+ const id = `ref:${node.id}.${f.name}`;
839
+ edges[id] = {
840
+ id,
841
+ from: node.id,
842
+ to: f.ref.node,
843
+ fromField: f.name,
844
+ toField: f.ref.field,
845
+ kind: "relation",
846
+ both: false,
847
+ hidden: false,
848
+ status: "built"
849
+ };
850
+ }
851
+ }
687
852
  const viewDefs = Object.keys(data.views ?? {}).length ? data.views : { main: { title: data.title } };
688
853
  const views = Object.entries(viewDefs).map(
689
854
  ([id, v]) => buildView(
@@ -701,10 +866,12 @@ function build(data, src, error, warn, { resolveIcon }) {
701
866
  const sc = buildScenario(id, s, views, nodes, edges, error, warn);
702
867
  if (sc) scenarios.push(sc);
703
868
  }
869
+ const defaults = buildDefaults(data.defaults, views, scenarios, error);
704
870
  return {
705
871
  name: src.name,
706
872
  title: data.title,
707
873
  descriptionHtml: data.description ? renderMarkdown(data.description) : void 0,
874
+ defaults,
708
875
  nodes,
709
876
  groups,
710
877
  edges,
@@ -713,6 +880,37 @@ function build(data, src, error, warn, { resolveIcon }) {
713
880
  scenarios
714
881
  };
715
882
  }
883
+ function toField(item) {
884
+ if (typeof item === "string") return { name: item };
885
+ const [name, value] = Object.entries(item)[0];
886
+ if (value == null) return { name };
887
+ if (typeof value === "string") return { name, type: value };
888
+ const { ref: _ref, ...rest } = value;
889
+ return { name, ...rest };
890
+ }
891
+ function resolveEnd(text, nodes) {
892
+ if (nodes[text]) return { end: { node: text } };
893
+ const matches = [];
894
+ let known;
895
+ for (let i = text.indexOf("."); i > 0; i = text.indexOf(".", i + 1)) {
896
+ const node = nodes[text.slice(0, i)];
897
+ if (!node) continue;
898
+ known ??= node;
899
+ const field2 = text.slice(i + 1);
900
+ if (node.fields.some((f) => f.name === field2)) matches.push({ node: node.id, field: field2 });
901
+ }
902
+ if (matches.length === 1) return { end: matches[0] };
903
+ if (matches.length > 1) {
904
+ return { message: `"${text}" could be more than one field`, hint: "Rename a node or a field." };
905
+ }
906
+ if (!known) return {};
907
+ const field = text.slice(known.id.length + 1);
908
+ const names = known.fields.map((f) => f.name);
909
+ return {
910
+ message: `Node "${known.id}" has no field "${field}"`,
911
+ hint: didYouMean(field, names) ?? (names.length ? `Fields: ${names.join(", ")}` : void 0)
912
+ };
913
+ }
716
914
  function buildView(id, v, diagramTitle, nodes, groups, edges, error) {
717
915
  const known = [...Object.keys(nodes), ...Object.keys(groups)];
718
916
  const members = (gid, out) => {
@@ -765,6 +963,38 @@ function buildView(id, v, diagramTitle, nodes, groups, edges, error) {
765
963
  interfaces: v.interfaces ?? false
766
964
  };
767
965
  }
966
+ function buildDefaults(d, views, scenarios, error) {
967
+ if (!d) return void 0;
968
+ const ids = (list) => list.map((x) => x.id);
969
+ const view = views.find((v) => v.id === d.view);
970
+ if (d.view !== void 0 && !view) {
971
+ error(
972
+ `defaults.view "${d.view}" is not a view`,
973
+ ["defaults", "view"],
974
+ didYouMean(d.view, ids(views))
975
+ );
976
+ }
977
+ const scenario = scenarios.find((s) => s.id === d.scenario);
978
+ if (d.scenario !== void 0 && !scenario) {
979
+ error(
980
+ `defaults.scenario "${d.scenario}" is not a scenario`,
981
+ ["defaults", "scenario"],
982
+ didYouMean(d.scenario, ids(scenarios))
983
+ );
984
+ }
985
+ if (view && scenario && scenario.view !== view.id) {
986
+ error(
987
+ `defaults.scenario "${scenario.id}" plays on view "${scenario.view}", not "${view.id}"`,
988
+ ["defaults", "scenario"],
989
+ `Drop \`view\` and the diagram opens on "${scenario.view}", or pick a scenario of "${view.id}".`
990
+ );
991
+ }
992
+ if (d.autoplay && d.scenario === void 0) {
993
+ error("defaults.autoplay needs a defaults.scenario to play", ["defaults", "autoplay"]);
994
+ }
995
+ const out = Object.fromEntries(Object.entries(d).filter(([, v]) => v !== void 0));
996
+ return Object.keys(out).length ? out : void 0;
997
+ }
768
998
  function buildScenario(id, s, views, nodes, edges, error, warn) {
769
999
  const viewId = s.view ?? views[0].id;
770
1000
  const view = views.find((v) => v.id === viewId);
@@ -1042,6 +1272,7 @@ function describeIssue(issue) {
1042
1272
  function allowedKeysFor(path) {
1043
1273
  const [section] = path;
1044
1274
  if (path.length === 0) return KNOWN_KEYS.root;
1275
+ if (path.length === 1) return section === "defaults" ? KNOWN_KEYS.defaults : void 0;
1045
1276
  if (path.length !== 2) return void 0;
1046
1277
  switch (section) {
1047
1278
  case "nodes":
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  didYouMean
3
- } from "./chunk-ITBBPZE6.js";
3
+ } from "./chunk-H77C3DSX.js";
4
4
 
5
5
  // src/nesting.ts
6
6
  function nestedDiagrams(all, name) {
@@ -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;
@@ -0,0 +1,21 @@
1
+ import type { PlaceholderSegment } from './placeholders';
2
+ export type TokenClass = 'comment' | 'string' | 'keyword' | 'flag' | 'number' | 'variable' | 'key' | 'literal' | 'operator' | 'section';
3
+ export interface Token {
4
+ text: string;
5
+ cls?: TokenClass;
6
+ }
7
+ /** The name of a language as a block's header shows it. Blocks with none are text. */
8
+ export declare function languageLabel(lang: string | undefined): string;
9
+ export declare function highlight(lang: string | undefined, text: string): Token[];
10
+ export type Piece = {
11
+ kind: 'text';
12
+ text: string;
13
+ cls?: TokenClass;
14
+ } | {
15
+ kind: 'var';
16
+ segment: Extract<PlaceholderSegment, {
17
+ kind: 'var';
18
+ }>;
19
+ };
20
+ /** A block's text as pieces to draw: coloured runs, with each placeholder where it was. */
21
+ export declare function highlightSegments(lang: string | undefined, segments: PlaceholderSegment[]): Piece[];
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  export * from './defaults';
2
2
  export * from './geometry';
3
+ export * from './highlight';
3
4
  export * from './layout';
4
5
  export * from './nested-layout';
5
6
  export * from './nesting';
6
7
  export * from './outline';
7
8
  export * from './phases';
9
+ export * from './placeholders';
8
10
  export * from './route';
11
+ export * from './scrubber';
12
+ export * from './start';
9
13
  export * from './story';
10
14
  export * from './svg';
11
15
  export * from './timeline';
package/dist/index.js CHANGED
@@ -2,18 +2,31 @@ import {
2
2
  attachNested,
3
3
  linkDiagrams,
4
4
  nestedDiagrams
5
- } from "./chunk-3QBMUILO.js";
5
+ } from "./chunk-RJUH342Z.js";
6
6
  import {
7
7
  BUILTIN_EDGE_KINDS,
8
8
  BUILTIN_KINDS,
9
+ DEFAULT_ENTITY_WIDTH,
9
10
  DEFAULT_NODE_WIDTH,
10
11
  STEP_KIND_COLORS,
11
12
  dist,
13
+ fillPlaceholders,
14
+ fillSegments,
15
+ highlight,
16
+ highlightSegments,
17
+ isSecretKey,
18
+ languageLabel,
12
19
  letterKey,
20
+ placeholdersOf,
13
21
  pointAt,
14
22
  polylineLength,
15
- roundedPath
16
- } from "./chunk-ITBBPZE6.js";
23
+ roundedPath,
24
+ shownPlaceholder,
25
+ splitPlaceholders,
26
+ takesPlaceholders,
27
+ unsetPlaceholders,
28
+ varKey
29
+ } from "./chunk-H77C3DSX.js";
17
30
 
18
31
  // src/layout.ts
19
32
  import ELK from "elkjs/lib/elk.bundled.js";
@@ -960,9 +973,10 @@ function overlap(a, b, pad) {
960
973
  var INLINE_PAD = { top: 40, left: 4, right: 4, bottom: 4 };
961
974
  function openingView(diagram, ref) {
962
975
  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;
976
+ const of = (scenario) => diagram.scenarios.find((s) => s.id === scenario)?.view;
977
+ return [ref.view, of(ref.scenario), of(diagram.defaults?.scenario), diagram.defaults?.view].find(
978
+ has
979
+ ) ?? diagram.views[0].id;
966
980
  }
967
981
  var canExpand = (ref, diagrams) => !!ref && ref.mode !== "open" && !!diagrams[ref.name];
968
982
  var canOpen = (ref, diagrams) => !!ref && ref.mode !== "expand" && !!diagrams[ref.name];
@@ -1186,6 +1200,81 @@ function phaseSpans(scenario) {
1186
1200
  return out;
1187
1201
  }
1188
1202
 
1203
+ // src/scrubber.ts
1204
+ function buildScrubber(tl) {
1205
+ const steps = tl.scenario.steps;
1206
+ const slots = [];
1207
+ let i = 0;
1208
+ while (i < steps.length) {
1209
+ let j = i + 1;
1210
+ const par = steps[i].par;
1211
+ if (par !== void 0) while (j < steps.length && steps[j].par === par) j++;
1212
+ const run = tl.steps.slice(i, j);
1213
+ slots.push({ from: i, to: j - 1, t0: Math.min(...run.map((s) => s.start)), t1: 0 });
1214
+ i = j;
1215
+ }
1216
+ slots.forEach((slot, k) => {
1217
+ slot.t1 = slots[k + 1]?.t0 ?? tl.duration;
1218
+ });
1219
+ if (slots[0]) slots[0].t0 = 0;
1220
+ const slotOf = (step) => slots.findIndex((s) => s.from <= step && step <= s.to);
1221
+ const sections = phaseSpans(tl.scenario).map((span) => ({
1222
+ phase: span.index,
1223
+ title: span.title,
1224
+ caption: span.caption,
1225
+ from: slotOf(span.from),
1226
+ to: slotOf(span.to)
1227
+ }));
1228
+ return { slots, sections, duration: tl.duration };
1229
+ }
1230
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
1231
+ function positionAt(s, t) {
1232
+ const n = s.slots.length;
1233
+ if (!n) return 0;
1234
+ let k = 0;
1235
+ s.slots.forEach((slot2, i) => {
1236
+ if (slot2.t0 <= t) k = i;
1237
+ });
1238
+ const slot = s.slots[k];
1239
+ const length = slot.t1 - slot.t0;
1240
+ return (k + (length > 0 ? clamp01((t - slot.t0) / length) : 0)) / n;
1241
+ }
1242
+ function timeAt(s, position) {
1243
+ const n = s.slots.length;
1244
+ if (!n) return 0;
1245
+ const x = clamp01(position) * n;
1246
+ const k = Math.min(n - 1, Math.floor(x));
1247
+ const slot = s.slots[k];
1248
+ return slot.t0 + (x - k) * (slot.t1 - slot.t0);
1249
+ }
1250
+ function slotAt(s, position) {
1251
+ const n = s.slots.length;
1252
+ return n ? Math.min(n - 1, Math.floor(clamp01(position) * n)) : -1;
1253
+ }
1254
+ function slotOfStep(s, index) {
1255
+ return s.slots.findIndex((slot) => slot.from <= index && index <= slot.to);
1256
+ }
1257
+
1258
+ // src/start.ts
1259
+ var defaultMode = (diagram) => diagram.defaults?.mode ?? "flow";
1260
+ function startOf(diagram, want = {}) {
1261
+ const d = diagram.defaults ?? {};
1262
+ const scenarioById = (id) => diagram.scenarios.find((s) => s.id === id);
1263
+ const hasView = (id) => diagram.views.some((v) => v.id === id);
1264
+ const first = diagram.views[0].id;
1265
+ let view;
1266
+ let scenario;
1267
+ if (want.view !== void 0 || want.scenario !== void 0) {
1268
+ view = [want.view, scenarioById(want.scenario)?.view].find(hasView) ?? first;
1269
+ scenario = scenarioById(want.scenario)?.id ?? (scenarioById(d.scenario)?.view === view ? d.scenario ?? null : null);
1270
+ } else {
1271
+ scenario = scenarioById(d.scenario)?.id ?? null;
1272
+ view = [scenarioById(scenario ?? void 0)?.view, d.view].find(hasView) ?? first;
1273
+ }
1274
+ const mode = want.mode ?? scenarioById(scenario ?? void 0)?.mode ?? defaultMode(diagram);
1275
+ return { view, scenario, mode };
1276
+ }
1277
+
1189
1278
  // src/story.ts
1190
1279
  function buildStory(diagram, scenario) {
1191
1280
  const steps = scenario.steps;
@@ -1484,20 +1573,29 @@ function runBounds(tl, index) {
1484
1573
  export {
1485
1574
  BUILTIN_EDGE_KINDS,
1486
1575
  BUILTIN_KINDS,
1576
+ DEFAULT_ENTITY_WIDTH,
1487
1577
  DEFAULT_NODE_WIDTH,
1488
1578
  INLINE_PAD,
1489
1579
  STEP_KIND_COLORS,
1490
1580
  attachNested,
1491
1581
  buildOutline,
1582
+ buildScrubber,
1492
1583
  buildStory,
1493
1584
  buildTimeline,
1494
1585
  canExpand,
1495
1586
  canOpen,
1496
1587
  defaultExpanded,
1588
+ defaultMode,
1497
1589
  dist,
1590
+ fillPlaceholders,
1591
+ fillSegments,
1498
1592
  frameAt,
1499
1593
  hasPhases,
1594
+ highlight,
1595
+ highlightSegments,
1596
+ isSecretKey,
1500
1597
  labelWidth,
1598
+ languageLabel,
1501
1599
  layoutNested,
1502
1600
  layoutView,
1503
1601
  letterKey,
@@ -1509,14 +1607,25 @@ export {
1509
1607
  packGrid,
1510
1608
  pathJoin,
1511
1609
  phaseSpans,
1610
+ placeholdersOf,
1512
1611
  pointAt,
1513
1612
  polylineLength,
1613
+ positionAt,
1514
1614
  rectOfPath,
1515
1615
  roundedPath,
1516
1616
  routeEdges,
1517
1617
  runBounds,
1518
1618
  scopeSvgIds,
1619
+ shownPlaceholder,
1620
+ slotAt,
1621
+ slotOfStep,
1622
+ splitPlaceholders,
1623
+ startOf,
1519
1624
  stepAt,
1520
1625
  stepEnd,
1521
- stepStart
1626
+ stepStart,
1627
+ takesPlaceholders,
1628
+ timeAt,
1629
+ unsetPlaceholders,
1630
+ varKey
1522
1631
  };
@@ -25,7 +25,10 @@ export interface InnerLayout {
25
25
  layout: NestedLayout;
26
26
  }
27
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. */
28
+ /**
29
+ * The view a node's diagram shows when opened: the one the node names, its scenario's, the one the
30
+ * diagram opens on by itself, or the first.
31
+ */
29
32
  export declare function openingView(diagram: CompiledDiagram, ref: NodeDiagram): string;
30
33
  /** Whether a reader may show this node's diagram in place: it is at hand and not limited to `open`. */
31
34
  export declare const canExpand: (ref: NodeDiagram | undefined, diagrams: Diagrams) => boolean;
package/dist/node.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  attachNested,
3
3
  linkDiagrams,
4
4
  nestedDiagrams
5
- } from "./chunk-3QBMUILO.js";
5
+ } from "./chunk-RJUH342Z.js";
6
6
  import {
7
7
  FamilySchema,
8
8
  KNOWN_KEYS,
@@ -11,8 +11,8 @@ import {
11
11
  compileDiagram,
12
12
  createIconResolver,
13
13
  renderMarkdown
14
- } from "./chunk-S7M3BRFI.js";
15
- import "./chunk-ITBBPZE6.js";
14
+ } from "./chunk-MUGDM3EQ.js";
15
+ import "./chunk-H77C3DSX.js";
16
16
 
17
17
  // src/node.ts
18
18
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";