@rocksoliddigital/payload-lineage 0.0.0-stage → 0.3.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/client.js ADDED
@@ -0,0 +1,1111 @@
1
+ 'use client';
2
+
3
+ // src/view/ErdCanvas.tsx
4
+ import "@xyflow/react/dist/style.css";
5
+ import "./lineage.css";
6
+ import {
7
+ Background,
8
+ Controls,
9
+ Handle,
10
+ MiniMap,
11
+ Panel,
12
+ Position,
13
+ ReactFlow,
14
+ ReactFlowProvider,
15
+ useReactFlow
16
+ } from "@xyflow/react";
17
+
18
+ // ../lineage-core/src/react.ts
19
+ import { useCallback, useEffect, useState } from "react";
20
+
21
+ // ../lineage-core/src/layout.ts
22
+ import ELK from "elkjs/lib/elk.bundled.js";
23
+ function layoutInput(nodes, edges, { layerSpacing, nodeSpacing, placement }) {
24
+ return {
25
+ id: "root",
26
+ layoutOptions: {
27
+ "elk.algorithm": "layered",
28
+ "elk.direction": "RIGHT",
29
+ "elk.layered.spacing.nodeNodeBetweenLayers": String(layerSpacing),
30
+ "elk.spacing.nodeNode": String(nodeSpacing),
31
+ "elk.layered.nodePlacement.strategy": placement
32
+ },
33
+ children: nodes.map(({ id, width, height }) => ({ id, width, height })),
34
+ edges: edges.filter((e) => e.source !== e.target).map((e, i) => ({
35
+ id: `e${i}`,
36
+ sources: [e.source],
37
+ targets: [e.target],
38
+ ...e.priority !== void 0 && {
39
+ layoutOptions: { "elk.layered.priority.direction": String(e.priority) }
40
+ }
41
+ }))
42
+ };
43
+ }
44
+ var elk;
45
+ async function layout(nodes, edges, options) {
46
+ elk ??= new ELK();
47
+ const result = await elk.layout(layoutInput(nodes, edges, options));
48
+ return new Map(result.children?.map((c) => [c.id, { x: c.x ?? 0, y: c.y ?? 0 }]));
49
+ }
50
+ function layoutKey(nodes, edges, { layerSpacing, nodeSpacing, placement }) {
51
+ return [
52
+ nodes.map((n) => `${n.id}:${n.width}x${n.height}`).sort().join("|"),
53
+ edges.map((e) => `${e.source}>${e.target}:${e.priority ?? 0}`).sort().join("|"),
54
+ `${layerSpacing}/${nodeSpacing}/${placement}`
55
+ ].join("#");
56
+ }
57
+
58
+ // ../lineage-core/src/react.ts
59
+ function useLayout(nodes, edges, options, onLayout) {
60
+ const [positions, setPositions] = useState(null);
61
+ const key = layoutKey(nodes, edges, options);
62
+ useEffect(() => {
63
+ let cancelled = false;
64
+ layout(nodes, edges, options).then((next) => {
65
+ if (cancelled) return;
66
+ setPositions(next);
67
+ onLayout?.();
68
+ });
69
+ return () => {
70
+ cancelled = true;
71
+ };
72
+ }, [key]);
73
+ return positions;
74
+ }
75
+ async function copyText(text) {
76
+ try {
77
+ await navigator.clipboard.writeText(text);
78
+ return true;
79
+ } catch {
80
+ const area = document.createElement("textarea");
81
+ area.value = text;
82
+ area.setAttribute("readonly", "");
83
+ area.style.position = "fixed";
84
+ area.style.opacity = "0";
85
+ document.body.appendChild(area);
86
+ area.select();
87
+ try {
88
+ return document.execCommand("copy");
89
+ } catch {
90
+ return false;
91
+ } finally {
92
+ area.remove();
93
+ }
94
+ }
95
+ }
96
+ function useCopy(resetAfterMs = 2e3) {
97
+ const [status, setStatus] = useState("idle");
98
+ const copy = useCallback(async (text) => {
99
+ setStatus(await copyText(text) ? "copied" : "failed");
100
+ }, []);
101
+ useEffect(() => {
102
+ if (status === "idle") return;
103
+ const timer = setTimeout(() => setStatus("idle"), resetAfterMs);
104
+ return () => clearTimeout(timer);
105
+ }, [status, resetAfterMs]);
106
+ return { status, copy };
107
+ }
108
+
109
+ // src/view/ErdCanvas.tsx
110
+ import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
111
+
112
+ // ../lineage-core/src/traverse.ts
113
+ function adjacency(links) {
114
+ const up = /* @__PURE__ */ new Map();
115
+ const down = /* @__PURE__ */ new Map();
116
+ const add = (map, key, value) => {
117
+ const list = map.get(key);
118
+ if (list) list.push(value);
119
+ else map.set(key, [value]);
120
+ };
121
+ for (const { source, target } of links) {
122
+ add(up, target, source);
123
+ add(down, source, target);
124
+ }
125
+ return { up, down };
126
+ }
127
+ function walk(start, next, maxDepth = Infinity) {
128
+ const seen = /* @__PURE__ */ new Set();
129
+ let frontier = [start];
130
+ for (let level = 0; level < maxDepth && frontier.length; level++) {
131
+ const reached = [];
132
+ for (const id of frontier) {
133
+ for (const to of next.get(id) ?? []) {
134
+ if (to === start || seen.has(to)) continue;
135
+ seen.add(to);
136
+ reached.push(to);
137
+ }
138
+ }
139
+ frontier = reached;
140
+ }
141
+ return seen;
142
+ }
143
+ function trace(id, links) {
144
+ const { up, down } = adjacency(links);
145
+ return /* @__PURE__ */ new Set([id, ...walk(id, up), ...walk(id, down)]);
146
+ }
147
+
148
+ // ../lineage-core/src/mermaid.ts
149
+ var MERMAID_READABLE_NODES = 50;
150
+ var ARROW = { solid: "-->", dashed: "-.->", thick: "==>" };
151
+ var MERMAID_COLOR = /^(#[0-9a-f]{3,8}|[a-z]+)$/i;
152
+ function toMermaidFlowchart({
153
+ nodes,
154
+ edges,
155
+ groups = [],
156
+ direction = "LR"
157
+ }) {
158
+ const ids = new Map(nodes.map((n, i) => [n.id, `n${i}`]));
159
+ const lines = [`flowchart ${direction}`];
160
+ for (const n of nodes) lines.push(` ${ids.get(n.id)}["${escapeLabel(n.label)}"]`);
161
+ for (const e of edges) {
162
+ const from = ids.get(e.source);
163
+ const to = ids.get(e.target);
164
+ if (!from || !to) continue;
165
+ const arrow = ARROW[e.style ?? "solid"];
166
+ const label = e.label ? `|"${escapeLabel(e.label)}"|` : "";
167
+ lines.push(` ${from} ${arrow}${label} ${to}`);
168
+ }
169
+ const used = new Set(nodes.map((n) => n.group).filter(Boolean));
170
+ groups.forEach((g, i) => {
171
+ if (!used.has(g.id) || !MERMAID_COLOR.test(g.color)) return;
172
+ const members = nodes.filter((n) => n.group === g.id).map((n) => ids.get(n.id));
173
+ lines.push(` classDef g${i} fill:${g.color},stroke:${g.color},color:#fff`);
174
+ lines.push(` class ${members.join(",")} g${i}`);
175
+ });
176
+ return `${lines.join("\n")}
177
+ `;
178
+ }
179
+ function escapeLabel(text) {
180
+ return text.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;");
181
+ }
182
+
183
+ // src/mermaid.ts
184
+ var KIND_COLORS = {
185
+ collection: "#0f7c74",
186
+ block: "#b4630c",
187
+ global: "#6b4bb8"
188
+ };
189
+ var EDGE_STYLE = { relationship: "solid", upload: "dashed", blocks: "thick" };
190
+ function contentGraphToMermaid(graph) {
191
+ return toMermaidFlowchart({
192
+ nodes: graph.nodes.map((n) => ({ id: n.id, label: n.label, group: n.kind })),
193
+ edges: graph.edges.map((e) => ({
194
+ source: e.source,
195
+ target: e.target,
196
+ label: e.fields.join(", "),
197
+ style: EDGE_STYLE[e.kind]
198
+ })),
199
+ groups: Object.entries(KIND_COLORS).map(([id, color]) => ({ id, color }))
200
+ });
201
+ }
202
+ function schemaToMermaid(schema) {
203
+ const names = entityNames(schema.entities);
204
+ const byId = new Map(schema.entities.map((e) => [e.id, e]));
205
+ const lines = ["erDiagram"];
206
+ for (const entity of schema.entities) {
207
+ const keys = new Set(schema.relations.filter((r) => r.from === entity.id).map((r) => r.field));
208
+ lines.push(` ${names.get(entity.id)}["${escapeLabel(entity.label)}"] {`);
209
+ for (const field of entity.fields) lines.push(` ${attribute(field, keys.has(field.name))}`);
210
+ lines.push(" }");
211
+ }
212
+ const lineGroups = /* @__PURE__ */ new Map();
213
+ for (const r of schema.relations) {
214
+ if (!names.has(r.from) || !names.has(r.to)) continue;
215
+ const key = `${r.from}
216
+ ${r.field}
217
+ ${r.to}`;
218
+ lineGroups.set(key, [...lineGroups.get(key) ?? [], r]);
219
+ }
220
+ for (const group of lineGroups.values()) {
221
+ const { from, to, field } = group[0];
222
+ const row = byId.get(from)?.fields.find((f) => f.name === field);
223
+ const label = [...new Set(group.map((r) => r.path))].join(", ");
224
+ lines.push(
225
+ ` ${names.get(from)} ${crowsFoot(group, from, !!row?.required)} ${names.get(to)} : "${escapeLabel(label)}"`
226
+ );
227
+ }
228
+ return `${lines.join("\n")}
229
+ `;
230
+ }
231
+ function entityNames(entities) {
232
+ const names = /* @__PURE__ */ new Map();
233
+ const taken = /* @__PURE__ */ new Set();
234
+ for (const e of entities) {
235
+ const base = `${e.kind === "global" ? "global_" : ""}${e.slug}`.replace(/[^A-Za-z0-9_-]/g, "_");
236
+ let name = /^[A-Za-z]/.test(base) ? base : `e_${base}`;
237
+ for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
238
+ taken.add(name);
239
+ names.set(e.id, name);
240
+ }
241
+ return names;
242
+ }
243
+ function attribute(field, isReference) {
244
+ const type = `${safeWord(field.type)}${field.hasMany ? "[]" : ""}`;
245
+ const name = safeWord(field.name);
246
+ const key = field.type === "id" || field.name === "id" ? " PK" : isReference ? " FK" : "";
247
+ const notes = [
248
+ ...name !== field.name ? [field.name] : [],
249
+ ...field.required ? ["required"] : [],
250
+ ...field.unique ? ["unique"] : [],
251
+ ...field.relationTo ? [`\u2192 ${field.relationTo.join(" | ")}`] : [],
252
+ ...field.blocks ? [`blocks: ${field.blocks.join(", ")}`] : []
253
+ ];
254
+ const comment = notes.length ? ` "${escapeLabel(notes.join(", "))}"` : "";
255
+ return `${type} ${name}${key}${comment}`;
256
+ }
257
+ function safeWord(text) {
258
+ const word = text.replace(/[^A-Za-z0-9_\-[\]()]/g, "_");
259
+ return /^[A-Za-z]/.test(word) ? word : `f_${word}`;
260
+ }
261
+ function crowsFoot(relations, from, required) {
262
+ const many = relations.some((r) => r.many);
263
+ const atMostOne = from.startsWith("global:") || relations.every((r) => r.unique) && !many;
264
+ const fromSide = atMostOne ? "|o" : "}o";
265
+ const atLeastOne = required && relations.every((r) => !r.polymorphic && r.field === r.path);
266
+ const toSide = many ? atLeastOne ? "|{" : "o{" : atLeastOne ? "||" : "o|";
267
+ return `${fromSide}--${toSide}`;
268
+ }
269
+
270
+ // src/view/CopyMermaid.tsx
271
+ import { jsxs } from "react/jsx-runtime";
272
+ var CopyMermaid = ({ text, size }) => {
273
+ const { status, copy } = useCopy();
274
+ const large = size > MERMAID_READABLE_NODES;
275
+ return /* @__PURE__ */ jsxs(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ className: "rs-lineage__copy",
280
+ onClick: () => copy(text()),
281
+ title: large ? `${size} is a lot for one Mermaid diagram; narrow it with the filters first for one that reads well` : "Copy what the page shows as a Mermaid diagram, for a PR, doc or ticket",
282
+ "aria-live": "polite",
283
+ children: [
284
+ status === "copied" ? "Copied" : status === "failed" ? "Couldn't copy" : "Copy Mermaid",
285
+ large && status === "idle" ? " (large)" : ""
286
+ ]
287
+ }
288
+ );
289
+ };
290
+
291
+ // src/view/erdModel.ts
292
+ var TABLE_WIDTH = 260;
293
+ var HEADER_HEIGHT = 44;
294
+ var ROW_HEIGHT = 24;
295
+ var FOOTER_HEIGHT = 26;
296
+ var ROW_LIMIT = 10;
297
+ function cardinalityOf(relation) {
298
+ if (relation.from.startsWith("global:")) return relation.many ? "one-to-many" : "one-to-one";
299
+ if (relation.many) return "many-to-many";
300
+ return relation.unique ? "one-to-one" : "many-to-one";
301
+ }
302
+ var LOOSENESS = ["one-to-one", "one-to-many", "many-to-one", "many-to-many"];
303
+ function describeCardinality(cardinality, from, to) {
304
+ if (from.kind === "global") {
305
+ return cardinality === "one-to-many" ? `The ${from.label} references many ${to.label}.` : `The ${from.label} references one ${to.singular}.`;
306
+ }
307
+ if (cardinality === "one-to-one") {
308
+ return `Each ${from.singular} references one ${to.singular}, and a ${to.singular} can be referenced by only one ${from.singular}.`;
309
+ }
310
+ if (cardinality === "many-to-one") {
311
+ return `Each ${from.singular} references one ${to.singular}; a ${to.singular} can be referenced by many ${from.label}.`;
312
+ }
313
+ return `Each ${from.singular} references many ${to.label}; a ${to.singular} can be referenced by many ${from.label}.`;
314
+ }
315
+ function erdEdges(relations) {
316
+ const edges = /* @__PURE__ */ new Map();
317
+ for (const r of relations) {
318
+ if (r.from === r.to) continue;
319
+ const id = `${r.from}.${r.field}->${r.to}`;
320
+ const edge = edges.get(id) ?? {
321
+ id,
322
+ from: r.from,
323
+ field: r.field,
324
+ to: r.to,
325
+ cardinality: cardinalityOf(r),
326
+ relations: []
327
+ };
328
+ edge.relations.push(r);
329
+ const own = cardinalityOf(r);
330
+ if (LOOSENESS.indexOf(own) > LOOSENESS.indexOf(edge.cardinality)) edge.cardinality = own;
331
+ edges.set(id, edge);
332
+ }
333
+ return [...edges.values()];
334
+ }
335
+ function selfReferences(entity, relations) {
336
+ return new Set(
337
+ relations.filter((r) => r.from === entity.id && r.to === entity.id).map((r) => r.field)
338
+ );
339
+ }
340
+ function visibleRows(entity, relations, { keysOnly, expanded }) {
341
+ const linked = new Set(relations.filter((r) => r.from === entity.id).map((r) => r.field));
342
+ const isKey = (f) => f.type === "id" || f.name === "id" || linked.has(f.name) || f.type === "join";
343
+ if (keysOnly) {
344
+ const rows2 = entity.fields.filter(isKey);
345
+ return { rows: rows2, hidden: entity.fields.length - rows2.length };
346
+ }
347
+ if (expanded || entity.fields.length <= ROW_LIMIT) return { rows: entity.fields, hidden: 0 };
348
+ const keys = entity.fields.filter(isKey).length;
349
+ let budget = Math.max(ROW_LIMIT - keys, 0);
350
+ const rows = entity.fields.filter((f) => isKey(f) || budget-- > 0);
351
+ return { rows, hidden: entity.fields.length - rows.length };
352
+ }
353
+ function tableHeight(rows, hidden) {
354
+ return HEADER_HEIGHT + rows * ROW_HEIGHT + (hidden ? FOOTER_HEIGHT : 0) + 4;
355
+ }
356
+ var ERD_LAYOUT = {
357
+ layerSpacing: 110,
358
+ nodeSpacing: 28,
359
+ placement: "NETWORK_SIMPLEX"
360
+ };
361
+
362
+ // src/view/model.ts
363
+ var NODE_WIDTH = 200;
364
+ var STACKED_BELOW = 960;
365
+ function fitPadding(layoutWidth) {
366
+ const right = layoutWidth > STACKED_BELOW ? "360px" : "24px";
367
+ return { top: "96px", right, bottom: "24px", left: "24px" };
368
+ }
369
+ var NODE_HEIGHT = 48;
370
+ function visibleGraph(graph, { hiddenKinds, hideUnconnected }) {
371
+ let nodes = graph.nodes.filter((n) => !hiddenKinds.has(n.kind));
372
+ let edges = edgesBetween(nodes, graph.edges);
373
+ if (hideUnconnected) {
374
+ const linked = new Set(
375
+ edges.filter((e) => e.source !== e.target).flatMap((e) => [e.source, e.target])
376
+ );
377
+ nodes = nodes.filter((n) => linked.has(n.id));
378
+ edges = edgesBetween(nodes, edges);
379
+ }
380
+ return { nodes, edges };
381
+ }
382
+ var LINEAGE_LAYOUT = {
383
+ layerSpacing: 80,
384
+ nodeSpacing: 16,
385
+ placement: "NETWORK_SIMPLEX"
386
+ };
387
+ function layoutEdges(edges) {
388
+ return edges.map(({ source, target, kind }) => ({
389
+ source,
390
+ target,
391
+ priority: kind === "relationship" ? 0 : 10
392
+ }));
393
+ }
394
+ function linksToItself(id, edges) {
395
+ return edges.some((e) => e.source === id && e.target === id);
396
+ }
397
+ function edgesBetween(nodes, edges) {
398
+ const ids = new Set(nodes.map((n) => n.id));
399
+ return edges.filter((e) => ids.has(e.source) && ids.has(e.target));
400
+ }
401
+
402
+ // src/view/ErdCanvas.tsx
403
+ import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
404
+ var ErdCanvas = (props) => /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(Canvas, { ...props }) });
405
+ var Canvas = ({ schema, adminURLs }) => {
406
+ const { fitView } = useReactFlow();
407
+ const layoutRef = useRef(null);
408
+ const [showGlobals, setShowGlobals] = useState2(true);
409
+ const [keysOnly, setKeysOnly] = useState2(false);
410
+ const [expanded, setExpanded] = useState2(/* @__PURE__ */ new Set());
411
+ const [hovered, setHovered] = useState2(null);
412
+ const [selected, setSelected] = useState2(null);
413
+ const entities = useMemo(
414
+ () => schema.entities.filter((e) => showGlobals || e.kind !== "global"),
415
+ [schema, showGlobals]
416
+ );
417
+ const relations = useMemo(() => {
418
+ const ids = new Set(entities.map((e) => e.id));
419
+ return schema.relations.filter((r) => ids.has(r.from) && ids.has(r.to));
420
+ }, [schema, entities]);
421
+ const lines = useMemo(() => erdEdges(relations), [relations]);
422
+ const tables = useMemo(
423
+ () => entities.map((entity) => {
424
+ const isExpanded = expanded.has(entity.id);
425
+ const { rows, hidden } = visibleRows(entity, relations, { keysOnly, expanded: isExpanded });
426
+ return {
427
+ entity,
428
+ rows,
429
+ hidden,
430
+ expanded: isExpanded,
431
+ // Room for the "+N more" or "Show fewer" button under the rows
432
+ height: tableHeight(rows.length, !keysOnly && (hidden || isExpanded) ? 1 : 0),
433
+ selfRefs: selfReferences(entity, relations),
434
+ linked: new Set(relations.filter((r) => r.from === entity.id).map((r) => r.field))
435
+ };
436
+ }),
437
+ [entities, relations, keysOnly, expanded]
438
+ );
439
+ const positions = useLayout(
440
+ useMemo(
441
+ () => tables.map((t) => ({ id: t.entity.id, width: TABLE_WIDTH, height: t.height })),
442
+ [tables]
443
+ ),
444
+ useMemo(() => lines.map((l) => ({ source: l.from, target: l.to })), [lines]),
445
+ ERD_LAYOUT,
446
+ () => requestAnimationFrame(
447
+ () => fitView({ padding: fitPadding(layoutRef.current?.clientWidth ?? 0) })
448
+ )
449
+ );
450
+ const neighbours = useMemo(() => {
451
+ if (!hovered) return null;
452
+ const ids = /* @__PURE__ */ new Set([hovered]);
453
+ for (const l of lines) {
454
+ if (l.from === hovered) ids.add(l.to);
455
+ if (l.to === hovered) ids.add(l.from);
456
+ }
457
+ return ids;
458
+ }, [hovered, lines]);
459
+ const nodes = useMemo(() => {
460
+ if (!positions) return [];
461
+ return tables.flatMap((t) => {
462
+ const position = positions.get(t.entity.id);
463
+ if (!position) return [];
464
+ let state = "default";
465
+ if (neighbours)
466
+ state = t.entity.id === hovered ? "active" : neighbours.has(t.entity.id) ? "related" : "dimmed";
467
+ else if (t.entity.id === selected) state = "selected";
468
+ return [
469
+ {
470
+ id: t.entity.id,
471
+ type: "table",
472
+ position,
473
+ width: TABLE_WIDTH,
474
+ height: t.height,
475
+ data: {
476
+ entity: t.entity,
477
+ rows: t.rows,
478
+ hidden: t.hidden,
479
+ expanded: t.expanded,
480
+ canExpand: !keysOnly,
481
+ selfRefs: t.selfRefs,
482
+ linked: t.linked,
483
+ state,
484
+ onSelect: setSelected,
485
+ onTrace: setHovered,
486
+ onToggle: () => setExpanded((current) => {
487
+ const next = new Set(current);
488
+ if (next.has(t.entity.id)) next.delete(t.entity.id);
489
+ else next.add(t.entity.id);
490
+ return next;
491
+ })
492
+ }
493
+ }
494
+ ];
495
+ });
496
+ }, [positions, tables, neighbours, hovered, selected, keysOnly]);
497
+ const edges = useMemo(
498
+ () => lines.map((l) => {
499
+ const on = neighbours && (l.from === hovered || l.to === hovered);
500
+ const state = neighbours ? on ? " rs-lineage__edge--active" : " rs-lineage__edge--dimmed" : "";
501
+ const kind = l.relations.every((r) => r.kind === "upload") ? "upload" : "relationship";
502
+ return {
503
+ id: l.id,
504
+ source: l.from,
505
+ sourceHandle: `row:${l.field}`,
506
+ target: l.to,
507
+ targetHandle: "table",
508
+ type: "smoothstep",
509
+ label: l.cardinality,
510
+ labelClassName: "rs-erd__cardinality",
511
+ labelBgClassName: "rs-erd__cardinality-bg",
512
+ className: `rs-lineage__edge rs-lineage__edge--${kind}${state}`,
513
+ animated: !!on
514
+ };
515
+ }),
516
+ [lines, neighbours, hovered]
517
+ );
518
+ useEffect2(() => {
519
+ const onKey = (event) => event.key === "Escape" && setSelected(null);
520
+ window.addEventListener("keydown", onKey);
521
+ return () => window.removeEventListener("keydown", onKey);
522
+ }, []);
523
+ const byId = useMemo(() => new Map(schema.entities.map((e) => [e.id, e])), [schema]);
524
+ const focused = selected ? byId.get(selected) : void 0;
525
+ const fieldCount = entities.reduce((n, e) => n + e.fields.length, 0);
526
+ return /* @__PURE__ */ jsxs2("div", { className: "rs-lineage-layout", ref: layoutRef, children: [
527
+ /* @__PURE__ */ jsx("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */ jsxs2(
528
+ ReactFlow,
529
+ {
530
+ nodes,
531
+ edges,
532
+ nodeTypes,
533
+ nodesDraggable: false,
534
+ nodesFocusable: false,
535
+ edgesFocusable: false,
536
+ nodesConnectable: false,
537
+ elementsSelectable: false,
538
+ minZoom: 0.1,
539
+ onNodeMouseEnter: (_, node) => setHovered(node.id),
540
+ onNodeMouseLeave: () => setHovered(null),
541
+ onNodeClick: (_, node) => setSelected(node.id),
542
+ onPaneClick: () => setSelected(null),
543
+ proOptions: { hideAttribution: true },
544
+ children: [
545
+ /* @__PURE__ */ jsx(Background, { gap: 24 }),
546
+ /* @__PURE__ */ jsx(Controls, { showInteractive: false }),
547
+ /* @__PURE__ */ jsx(
548
+ MiniMap,
549
+ {
550
+ pannable: true,
551
+ zoomable: true,
552
+ className: "rs-lineage__minimap",
553
+ nodeClassName: (node) => `rs-lineage__minimap-node--${node.data.entity.kind}`,
554
+ ariaLabel: "Data model overview"
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsx(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs2("div", { className: "rs-lineage__toolbar", children: [
558
+ /* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
559
+ /* @__PURE__ */ jsx(
560
+ "input",
561
+ {
562
+ type: "checkbox",
563
+ checked: showGlobals,
564
+ onChange: () => setShowGlobals((v) => !v)
565
+ }
566
+ ),
567
+ /* @__PURE__ */ jsx("span", { className: "rs-lineage__swatch rs-lineage__swatch--global" }),
568
+ "Globals"
569
+ ] }),
570
+ /* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
571
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: keysOnly, onChange: () => setKeysOnly((v) => !v) }),
572
+ "Key fields only"
573
+ ] }),
574
+ /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__count", children: [
575
+ entities.length,
576
+ " tables, ",
577
+ fieldCount,
578
+ " fields, ",
579
+ lines.length,
580
+ " references"
581
+ ] }),
582
+ /* @__PURE__ */ jsx(
583
+ CopyMermaid,
584
+ {
585
+ text: () => schemaToMermaid({
586
+ entities: tables.map(
587
+ (t) => keysOnly ? { ...t.entity, fields: t.rows } : t.entity
588
+ ),
589
+ relations
590
+ }),
591
+ size: entities.length
592
+ }
593
+ )
594
+ ] }) })
595
+ ]
596
+ }
597
+ ) }),
598
+ /* @__PURE__ */ jsx(
599
+ Details,
600
+ {
601
+ entity: focused,
602
+ lines,
603
+ byId,
604
+ adminURLs,
605
+ onSelect: setSelected
606
+ }
607
+ )
608
+ ] });
609
+ };
610
+ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
611
+ if (!entity) {
612
+ return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", children: [
613
+ /* @__PURE__ */ jsx("p", { className: "rs-lineage__hint", children: "Hover a table to see what it references and what references it. Click it for the field paths behind each line. Esc clears the selection." }),
614
+ /* @__PURE__ */ jsxs2("dl", { className: "rs-lineage__legend", children: [
615
+ /* @__PURE__ */ jsxs2("div", { children: [
616
+ /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "many-to-one" }),
617
+ /* @__PURE__ */ jsx("dd", { children: "each document references one; many can share it" })
618
+ ] }),
619
+ /* @__PURE__ */ jsxs2("div", { children: [
620
+ /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "many-to-many" }),
621
+ /* @__PURE__ */ jsx("dd", { children: "a list of references: hasMany, or inside an array or blocks" })
622
+ ] }),
623
+ /* @__PURE__ */ jsxs2("div", { children: [
624
+ /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "one-to-one" }),
625
+ /* @__PURE__ */ jsx("dd", { children: "a unique reference, or a global's single one" })
626
+ ] }),
627
+ /* @__PURE__ */ jsxs2("div", { children: [
628
+ /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "one-to-many" }),
629
+ /* @__PURE__ */ jsx("dd", { children: "a global's list of references" })
630
+ ] }),
631
+ /* @__PURE__ */ jsxs2("div", { children: [
632
+ /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "\u21BB" }),
633
+ /* @__PURE__ */ jsx("dd", { children: "points at its own collection" })
634
+ ] }),
635
+ /* @__PURE__ */ jsxs2("div", { children: [
636
+ /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "*" }),
637
+ /* @__PURE__ */ jsx("dd", { children: "required" })
638
+ ] })
639
+ ] })
640
+ ] });
641
+ }
642
+ const out = lines.filter((l) => l.from === entity.id);
643
+ const into = lines.filter((l) => l.to === entity.id);
644
+ const url = adminURLs[entity.id];
645
+ const item = (line, otherId) => {
646
+ const other = byId.get(otherId);
647
+ const from = byId.get(line.from);
648
+ const to = byId.get(line.to);
649
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs2("button", { type: "button", className: "rs-lineage__link", onClick: () => onSelect(other.id), children: [
650
+ /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__link-name", children: [
651
+ /* @__PURE__ */ jsx("span", { className: `rs-lineage__swatch rs-lineage__swatch--${other.kind}` }),
652
+ other.label,
653
+ /* @__PURE__ */ jsx("span", { className: "rs-erd__cardinality-tag", children: line.cardinality })
654
+ ] }),
655
+ /* @__PURE__ */ jsx("span", { className: "rs-erd__sentence", children: describeCardinality(line.cardinality, from, to) }),
656
+ /* @__PURE__ */ jsx("span", { className: "rs-lineage__via", children: line.relations.map((r) => r.path).join(", ") })
657
+ ] }) }, line.id);
658
+ };
659
+ return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
660
+ /* @__PURE__ */ jsxs2("div", { children: [
661
+ /* @__PURE__ */ jsxs2("span", { className: `rs-lineage__kind rs-lineage__kind--${entity.kind}`, children: [
662
+ entity.kind,
663
+ entity.auth ? " \xB7 auth" : "",
664
+ entity.upload ? " \xB7 upload" : ""
665
+ ] }),
666
+ /* @__PURE__ */ jsx("h2", { className: "rs-lineage__name", children: entity.label }),
667
+ /* @__PURE__ */ jsxs2("code", { className: "rs-lineage__id", children: [
668
+ entity.slug,
669
+ " \xB7 ",
670
+ entity.fields.length,
671
+ " fields"
672
+ ] })
673
+ ] }),
674
+ url && /* @__PURE__ */ jsx("a", { className: "rs-lineage__open", href: url, children: "Open in the admin" }),
675
+ /* @__PURE__ */ jsxs2("section", { children: [
676
+ /* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
677
+ "References \xB7 ",
678
+ out.length
679
+ ] }),
680
+ out.length ? /* @__PURE__ */ jsx("ul", { className: "rs-lineage__list", children: out.map((l) => item(l, l.to)) }) : /* @__PURE__ */ jsx("p", { className: "rs-lineage__hint", children: "None of its fields reference another collection." })
681
+ ] }),
682
+ /* @__PURE__ */ jsxs2("section", { children: [
683
+ /* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
684
+ "Referenced by \xB7 ",
685
+ into.length
686
+ ] }),
687
+ into.length ? /* @__PURE__ */ jsx("ul", { className: "rs-lineage__list", children: into.map((l) => item(l, l.from)) }) : /* @__PURE__ */ jsx("p", { className: "rs-lineage__hint", children: "Nothing references it." })
688
+ ] })
689
+ ] });
690
+ };
691
+ var TableView = ({
692
+ data: {
693
+ entity,
694
+ rows,
695
+ hidden,
696
+ expanded,
697
+ canExpand,
698
+ selfRefs,
699
+ linked,
700
+ state,
701
+ onToggle,
702
+ onSelect,
703
+ onTrace
704
+ }
705
+ }) => /* @__PURE__ */ jsxs2("div", { className: `rs-erd__table rs-lineage__node--${entity.kind} rs-erd__table--${state}`, children: [
706
+ /* @__PURE__ */ jsx(
707
+ Handle,
708
+ {
709
+ id: "table",
710
+ className: "rs-lineage__handle",
711
+ type: "target",
712
+ position: Position.Left,
713
+ isConnectable: false
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsxs2(
717
+ "button",
718
+ {
719
+ type: "button",
720
+ className: "rs-erd__header",
721
+ title: entity.id,
722
+ "aria-label": `${entity.label}, ${entity.kind}: show its references`,
723
+ onClick: () => onSelect(entity.id),
724
+ onFocus: () => onTrace(entity.id),
725
+ onBlur: () => onTrace(null),
726
+ children: [
727
+ /* @__PURE__ */ jsx("span", { className: "rs-erd__title", children: entity.label }),
728
+ /* @__PURE__ */ jsx("span", { className: "rs-erd__badge", children: entity.kind === "global" ? "global" : entity.auth ? "auth" : entity.upload ? "upload" : entity.slug })
729
+ ]
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx("ul", { className: "rs-erd__rows", children: rows.map((f) => /* @__PURE__ */ jsxs2(
733
+ "li",
734
+ {
735
+ className: `rs-erd__row${linked.has(f.name) ? " rs-erd__row--link" : ""}`,
736
+ title: describe(f),
737
+ children: [
738
+ /* @__PURE__ */ jsxs2("span", { className: "rs-erd__field", children: [
739
+ f.name,
740
+ f.required && /* @__PURE__ */ jsx("span", { className: "rs-erd__required", children: "*" }),
741
+ selfRefs.has(f.name) && /* @__PURE__ */ jsx("span", { className: "rs-erd__self", title: "Points at its own collection", children: "\u21BB" })
742
+ ] }),
743
+ /* @__PURE__ */ jsxs2("span", { className: "rs-erd__type", children: [
744
+ f.type === "blocks" ? `blocks (${f.blocks?.length ?? 0})` : f.type,
745
+ f.hasMany ? "[]" : ""
746
+ ] }),
747
+ linked.has(f.name) && /* @__PURE__ */ jsx(
748
+ Handle,
749
+ {
750
+ id: `row:${f.name}`,
751
+ className: "rs-lineage__handle",
752
+ type: "source",
753
+ position: Position.Right,
754
+ isConnectable: false
755
+ }
756
+ )
757
+ ]
758
+ },
759
+ f.name
760
+ )) }),
761
+ canExpand && (hidden > 0 || expanded) && /* @__PURE__ */ jsx(
762
+ "button",
763
+ {
764
+ type: "button",
765
+ className: "rs-erd__more nodrag",
766
+ onClick: (event) => {
767
+ event.stopPropagation();
768
+ onToggle();
769
+ },
770
+ children: expanded ? "Show fewer fields" : `+${hidden} more field${hidden === 1 ? "" : "s"}`
771
+ }
772
+ )
773
+ ] });
774
+ function describe(f) {
775
+ const parts = [f.type];
776
+ if (f.hasMany) parts.push("has many");
777
+ if (f.required) parts.push("required");
778
+ if (f.localized) parts.push("localized");
779
+ if (f.unique) parts.push("unique");
780
+ if (f.relationTo) parts.push(`\u2192 ${f.relationTo.join(" | ")}`);
781
+ if (f.blocks) parts.push(`blocks: ${f.blocks.join(", ")}`);
782
+ return `${f.name}: ${parts.join(", ")}`;
783
+ }
784
+ var nodeTypes = { table: TableView };
785
+
786
+ // src/view/LineageCanvas.tsx
787
+ import "@xyflow/react/dist/style.css";
788
+ import "./lineage.css";
789
+ import {
790
+ Background as Background2,
791
+ Controls as Controls2,
792
+ Handle as Handle2,
793
+ MiniMap as MiniMap2,
794
+ Panel as Panel2,
795
+ Position as Position2,
796
+ ReactFlow as ReactFlow2,
797
+ ReactFlowProvider as ReactFlowProvider2,
798
+ useReactFlow as useReactFlow2
799
+ } from "@xyflow/react";
800
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
801
+ import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
802
+ var KINDS = [
803
+ { kind: "collection", label: "Collections" },
804
+ { kind: "block", label: "Blocks" },
805
+ { kind: "global", label: "Globals" }
806
+ ];
807
+ var LineageCanvas = (props) => /* @__PURE__ */ jsx2(ReactFlowProvider2, { children: /* @__PURE__ */ jsx2(Canvas2, { ...props }) });
808
+ var Canvas2 = ({ graph, adminURLs }) => {
809
+ const { fitView } = useReactFlow2();
810
+ const layoutRef = useRef2(null);
811
+ const [hiddenKinds, setHiddenKinds] = useState3(/* @__PURE__ */ new Set());
812
+ const [hideUnconnected, setHideUnconnected] = useState3(true);
813
+ const [hovered, setHovered] = useState3(null);
814
+ const [selected, setSelected] = useState3(null);
815
+ const byId = useMemo2(() => new Map(graph.nodes.map((n) => [n.id, n])), [graph]);
816
+ const visible = useMemo2(
817
+ () => visibleGraph(graph, { hiddenKinds, hideUnconnected }),
818
+ [graph, hiddenKinds, hideUnconnected]
819
+ );
820
+ const positions = useLayout(
821
+ useMemo2(
822
+ () => visible.nodes.map((n) => ({ id: n.id, width: NODE_WIDTH, height: NODE_HEIGHT })),
823
+ [visible.nodes]
824
+ ),
825
+ useMemo2(() => layoutEdges(visible.edges), [visible.edges]),
826
+ LINEAGE_LAYOUT,
827
+ // Leave room on the right for the details panel when it's beside the graph
828
+ () => requestAnimationFrame(
829
+ () => fitView({ padding: fitPadding(layoutRef.current?.clientWidth ?? 0) })
830
+ )
831
+ );
832
+ const traced = useMemo2(
833
+ () => hovered ? trace(hovered, visible.edges) : null,
834
+ [hovered, visible.edges]
835
+ );
836
+ const nodes = useMemo2(() => {
837
+ if (!positions) return [];
838
+ return visible.nodes.flatMap((node) => {
839
+ const position = positions.get(node.id);
840
+ if (!position) return [];
841
+ let state = "default";
842
+ if (traced)
843
+ state = node.id === hovered ? "active" : traced.has(node.id) ? "related" : "dimmed";
844
+ else if (node.id === selected) state = "selected";
845
+ return [
846
+ {
847
+ id: node.id,
848
+ type: "content",
849
+ position,
850
+ width: NODE_WIDTH,
851
+ height: NODE_HEIGHT,
852
+ data: {
853
+ node,
854
+ state,
855
+ selfLink: linksToItself(node.id, visible.edges),
856
+ onSelect: setSelected,
857
+ onTrace: setHovered
858
+ }
859
+ }
860
+ ];
861
+ });
862
+ }, [positions, visible, traced, hovered, selected]);
863
+ const edges = useMemo2(
864
+ () => visible.edges.filter((e) => e.source !== e.target).map((e) => {
865
+ const onTrace = traced?.has(e.source) && traced.has(e.target);
866
+ const state = traced ? onTrace ? " rs-lineage__edge--active" : " rs-lineage__edge--dimmed" : "";
867
+ return {
868
+ id: `${e.source}->${e.target}:${e.kind}`,
869
+ source: e.source,
870
+ target: e.target,
871
+ className: `rs-lineage__edge rs-lineage__edge--${e.kind}${state}`,
872
+ animated: !!onTrace
873
+ };
874
+ }),
875
+ [visible.edges, traced]
876
+ );
877
+ useEffect3(() => {
878
+ const onKey = (event) => event.key === "Escape" && setSelected(null);
879
+ window.addEventListener("keydown", onKey);
880
+ return () => window.removeEventListener("keydown", onKey);
881
+ }, []);
882
+ const toggleKind = (kind) => setHiddenKinds((current) => {
883
+ const next = new Set(current);
884
+ if (next.has(kind)) next.delete(kind);
885
+ else next.add(kind);
886
+ return next;
887
+ });
888
+ const focused = selected ? byId.get(selected) : void 0;
889
+ const links = visible.edges.length;
890
+ return (
891
+ // `data-ready` once the first layout is done, for tests to wait on
892
+ /* @__PURE__ */ jsxs3("div", { className: "rs-lineage-layout", ref: layoutRef, children: [
893
+ /* @__PURE__ */ jsx2("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */ jsxs3(
894
+ ReactFlow2,
895
+ {
896
+ nodes,
897
+ edges,
898
+ nodeTypes: nodeTypes2,
899
+ nodesDraggable: false,
900
+ nodesFocusable: false,
901
+ edgesFocusable: false,
902
+ nodesConnectable: false,
903
+ elementsSelectable: false,
904
+ minZoom: 0.1,
905
+ onNodeMouseEnter: (_, node) => setHovered(node.id),
906
+ onNodeMouseLeave: () => setHovered(null),
907
+ onNodeClick: (_, node) => setSelected(node.id),
908
+ onPaneClick: () => setSelected(null),
909
+ proOptions: { hideAttribution: true },
910
+ children: [
911
+ /* @__PURE__ */ jsx2(Background2, { gap: 24 }),
912
+ /* @__PURE__ */ jsx2(Controls2, { showInteractive: false }),
913
+ /* @__PURE__ */ jsx2(
914
+ MiniMap2,
915
+ {
916
+ pannable: true,
917
+ zoomable: true,
918
+ className: "rs-lineage__minimap",
919
+ nodeClassName: (node) => `rs-lineage__minimap-node--${node.data.node.kind}`,
920
+ ariaLabel: "Content lineage overview"
921
+ }
922
+ ),
923
+ /* @__PURE__ */ jsx2(Panel2, { position: "top-left", children: /* @__PURE__ */ jsxs3("div", { className: "rs-lineage__toolbar", children: [
924
+ KINDS.map(({ kind, label }) => /* @__PURE__ */ jsxs3("label", { className: "rs-lineage__toggle", children: [
925
+ /* @__PURE__ */ jsx2(
926
+ "input",
927
+ {
928
+ type: "checkbox",
929
+ checked: !hiddenKinds.has(kind),
930
+ onChange: () => toggleKind(kind)
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsx2("span", { className: `rs-lineage__swatch rs-lineage__swatch--${kind}` }),
934
+ label
935
+ ] }, kind)),
936
+ /* @__PURE__ */ jsxs3("label", { className: "rs-lineage__toggle", children: [
937
+ /* @__PURE__ */ jsx2(
938
+ "input",
939
+ {
940
+ type: "checkbox",
941
+ checked: hideUnconnected,
942
+ onChange: () => setHideUnconnected((current) => !current)
943
+ }
944
+ ),
945
+ "Hide unconnected"
946
+ ] }),
947
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__count", children: [
948
+ visible.nodes.length,
949
+ " nodes, ",
950
+ links,
951
+ " link",
952
+ links === 1 ? "" : "s"
953
+ ] }),
954
+ /* @__PURE__ */ jsx2(
955
+ CopyMermaid,
956
+ {
957
+ text: () => contentGraphToMermaid({ nodes: visible.nodes, edges: visible.edges }),
958
+ size: visible.nodes.length
959
+ }
960
+ )
961
+ ] }) })
962
+ ]
963
+ }
964
+ ) }),
965
+ /* @__PURE__ */ jsx2(
966
+ Details2,
967
+ {
968
+ node: focused,
969
+ edges: visible.edges,
970
+ byId,
971
+ adminURLs,
972
+ onSelect: setSelected
973
+ }
974
+ )
975
+ ] })
976
+ );
977
+ };
978
+ var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
979
+ if (!node) {
980
+ return /* @__PURE__ */ jsxs3("aside", { className: "rs-lineage__details", children: [
981
+ /* @__PURE__ */ jsx2("p", { className: "rs-lineage__hint", children: "Hover a node to trace everything it's built from and everything that uses it. Click it to see the fields behind each link. Esc clears the selection." }),
982
+ /* @__PURE__ */ jsxs3("dl", { className: "rs-lineage__legend", children: [
983
+ /* @__PURE__ */ jsxs3("div", { children: [
984
+ /* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--relationship" }),
985
+ /* @__PURE__ */ jsx2("dd", { children: "relationship" })
986
+ ] }),
987
+ /* @__PURE__ */ jsxs3("div", { children: [
988
+ /* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--upload" }),
989
+ /* @__PURE__ */ jsx2("dd", { children: "upload" })
990
+ ] }),
991
+ /* @__PURE__ */ jsxs3("div", { children: [
992
+ /* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--blocks" }),
993
+ /* @__PURE__ */ jsx2("dd", { children: "blocks field" })
994
+ ] })
995
+ ] })
996
+ ] });
997
+ }
998
+ const builtFrom = edges.filter((e) => e.target === node.id);
999
+ const usedBy = edges.filter((e) => e.source === node.id);
1000
+ const url = adminURLs[node.id];
1001
+ const item = (edge, otherId) => {
1002
+ const other = byId.get(otherId);
1003
+ return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs3("button", { type: "button", className: "rs-lineage__link", onClick: () => onSelect(other.id), children: [
1004
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__link-name", children: [
1005
+ /* @__PURE__ */ jsx2("span", { className: `rs-lineage__swatch rs-lineage__swatch--${other.kind}` }),
1006
+ edge.source === edge.target ? "Itself" : other.label
1007
+ ] }),
1008
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__via", children: [
1009
+ edge.kind,
1010
+ " \xB7 ",
1011
+ edge.fields.join(", ")
1012
+ ] })
1013
+ ] }) }, `${edge.source}->${edge.target}:${edge.kind}`);
1014
+ };
1015
+ return /* @__PURE__ */ jsxs3("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
1016
+ /* @__PURE__ */ jsxs3("div", { children: [
1017
+ /* @__PURE__ */ jsx2("span", { className: `rs-lineage__kind rs-lineage__kind--${node.kind}`, children: node.kind }),
1018
+ /* @__PURE__ */ jsx2("h2", { className: "rs-lineage__name", children: node.label }),
1019
+ /* @__PURE__ */ jsx2("code", { className: "rs-lineage__id", children: node.id })
1020
+ ] }),
1021
+ url && /* @__PURE__ */ jsx2("a", { className: "rs-lineage__open", href: url, children: "Open in the admin" }),
1022
+ /* @__PURE__ */ jsxs3("section", { children: [
1023
+ /* @__PURE__ */ jsxs3("h3", { className: "rs-lineage__heading", children: [
1024
+ "Built from \xB7 ",
1025
+ builtFrom.length
1026
+ ] }),
1027
+ builtFrom.length ? /* @__PURE__ */ jsx2("ul", { className: "rs-lineage__list", children: builtFrom.map((e) => item(e, e.source)) }) : /* @__PURE__ */ jsx2("p", { className: "rs-lineage__hint", children: "None of its fields point at another collection or block." })
1028
+ ] }),
1029
+ /* @__PURE__ */ jsxs3("section", { children: [
1030
+ /* @__PURE__ */ jsxs3("h3", { className: "rs-lineage__heading", children: [
1031
+ "Used by \xB7 ",
1032
+ usedBy.length
1033
+ ] }),
1034
+ usedBy.length ? /* @__PURE__ */ jsx2("ul", { className: "rs-lineage__list", children: usedBy.map((e) => item(e, e.target)) }) : /* @__PURE__ */ jsx2("p", { className: "rs-lineage__hint", children: "Nothing uses it." })
1035
+ ] })
1036
+ ] });
1037
+ };
1038
+ var ContentNodeView = ({
1039
+ data: { node, state, selfLink, onSelect, onTrace }
1040
+ }) => /* @__PURE__ */ jsxs3(
1041
+ "div",
1042
+ {
1043
+ className: `rs-lineage__node rs-lineage__node--${node.kind} rs-lineage__node--${state}`,
1044
+ title: node.id,
1045
+ children: [
1046
+ /* @__PURE__ */ jsx2(
1047
+ Handle2,
1048
+ {
1049
+ className: "rs-lineage__handle",
1050
+ type: "target",
1051
+ position: Position2.Left,
1052
+ isConnectable: false
1053
+ }
1054
+ ),
1055
+ /* @__PURE__ */ jsxs3(
1056
+ "button",
1057
+ {
1058
+ type: "button",
1059
+ className: "rs-lineage__select",
1060
+ "aria-label": `${node.label}, ${node.kind}: show what it's built from and what uses it`,
1061
+ onClick: () => onSelect(node.id),
1062
+ onFocus: () => onTrace(node.id),
1063
+ onBlur: () => onTrace(null),
1064
+ children: [
1065
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__label", children: [
1066
+ node.label,
1067
+ selfLink && /* @__PURE__ */ jsx2("span", { className: "rs-lineage__self", title: "Links to itself", children: "\u21BB" })
1068
+ ] }),
1069
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__meta", children: [
1070
+ node.kind,
1071
+ " \xB7 ",
1072
+ node.slug
1073
+ ] })
1074
+ ]
1075
+ }
1076
+ ),
1077
+ /* @__PURE__ */ jsx2(
1078
+ Handle2,
1079
+ {
1080
+ className: "rs-lineage__handle",
1081
+ type: "source",
1082
+ position: Position2.Right,
1083
+ isConnectable: false
1084
+ }
1085
+ )
1086
+ ]
1087
+ }
1088
+ );
1089
+ var nodeTypes2 = { content: ContentNodeView };
1090
+
1091
+ // src/view/NavLink.tsx
1092
+ import { Link, useConfig } from "@payloadcms/ui";
1093
+ import { usePathname } from "next/navigation";
1094
+ import { formatAdminURL } from "payload/shared";
1095
+ import { Fragment, jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
1096
+ var LineageNavLinkClient = () => {
1097
+ const { config } = useConfig();
1098
+ const pathname = usePathname();
1099
+ const href = formatAdminURL({ adminRoute: config.routes.admin, path: "/lineage" });
1100
+ const active = pathname === href || pathname.startsWith(`${href}/`);
1101
+ const label = /* @__PURE__ */ jsxs4(Fragment, { children: [
1102
+ active && /* @__PURE__ */ jsx3("div", { className: "nav__link-indicator" }),
1103
+ /* @__PURE__ */ jsx3("span", { className: "nav__link-label", children: "Lineage" })
1104
+ ] });
1105
+ return pathname === href ? /* @__PURE__ */ jsx3("div", { className: "nav__link", id: "nav-lineage", children: label }) : /* @__PURE__ */ jsx3(Link, { className: "nav__link", href, id: "nav-lineage", prefetch: false, children: label });
1106
+ };
1107
+ export {
1108
+ ErdCanvas,
1109
+ LineageCanvas,
1110
+ LineageNavLinkClient
1111
+ };