@rocksoliddigital/payload-lineage 0.2.0 → 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/README.md CHANGED
@@ -21,6 +21,10 @@ Both are built from the running config on each visit (well under a millisecond),
21
21
  out of date. A "Lineage" link in the nav opens them. To try them, run the
22
22
  [example](https://github.com/rock-solid/lineage-kit/tree/main/examples/payload).
23
23
 
24
+ **Copy Mermaid**, on both pages, copies what the page shows, with its filters, as a Mermaid diagram:
25
+ the lineage as a flowchart, the data model as an `erDiagram` in crow's-foot notation. Paste it into
26
+ a PR, a doc or a ticket, and GitHub, GitLab and most docs tools draw it.
27
+
24
28
  Works with Payload 3 on Next.js 15 or 16, with React 19.
25
29
 
26
30
  > **Status:** early. Both pages come from the config only. Which pages use which blocks (from the
@@ -29,9 +33,6 @@ Works with Payload 3 on Next.js 15 or 16, with React 19.
29
33
 
30
34
  ## 📥 Install
31
35
 
32
- Not published yet: the first release, 0.2.0, is on the
33
- [roadmap](https://github.com/rock-solid/lineage-kit/blob/main/ROADMAP.md#-payload-plugin). Then:
34
-
35
36
  ```sh
36
37
  pnpm add @rocksoliddigital/payload-lineage
37
38
  ```
@@ -85,14 +86,20 @@ lineagePlugin({
85
86
  });
86
87
  ```
87
88
 
88
- Or build either yourself, e.g. in a script:
89
+ Or build either yourself, e.g. in a script, and keep a diagram in your docs up to date:
89
90
 
90
91
  ```ts
91
- import { buildContentGraph, buildSchema } from '@rocksoliddigital/payload-lineage';
92
+ import {
93
+ buildContentGraph,
94
+ buildSchema,
95
+ contentGraphToMermaid,
96
+ schemaToMermaid,
97
+ } from '@rocksoliddigital/payload-lineage';
92
98
  import config from '@payload-config';
93
99
 
94
100
  const graph = buildContentGraph(await config);
95
101
  const schema = buildSchema(await config);
102
+ const diagram = schemaToMermaid(schema); // or contentGraphToMermaid(graph)
96
103
  ```
97
104
 
98
105
  ## 🔒 Access
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "@xyflow/react";
17
17
 
18
18
  // ../lineage-core/src/react.ts
19
- import { useEffect, useState } from "react";
19
+ import { useCallback, useEffect, useState } from "react";
20
20
 
21
21
  // ../lineage-core/src/layout.ts
22
22
  import ELK from "elkjs/lib/elk.bundled.js";
@@ -72,10 +72,222 @@ function useLayout(nodes, edges, options, onLayout) {
72
72
  }, [key]);
73
73
  return positions;
74
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
+ }
75
108
 
76
109
  // src/view/ErdCanvas.tsx
77
110
  import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
78
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
+
79
291
  // src/view/erdModel.ts
80
292
  var TABLE_WIDTH = 260;
81
293
  var HEADER_HEIGHT = 44;
@@ -188,7 +400,7 @@ function edgesBetween(nodes, edges) {
188
400
  }
189
401
 
190
402
  // src/view/ErdCanvas.tsx
191
- import { jsx, jsxs } from "react/jsx-runtime";
403
+ import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
192
404
  var ErdCanvas = (props) => /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(Canvas, { ...props }) });
193
405
  var Canvas = ({ schema, adminURLs }) => {
194
406
  const { fitView } = useReactFlow();
@@ -311,8 +523,8 @@ var Canvas = ({ schema, adminURLs }) => {
311
523
  const byId = useMemo(() => new Map(schema.entities.map((e) => [e.id, e])), [schema]);
312
524
  const focused = selected ? byId.get(selected) : void 0;
313
525
  const fieldCount = entities.reduce((n, e) => n + e.fields.length, 0);
314
- return /* @__PURE__ */ jsxs("div", { className: "rs-lineage-layout", ref: layoutRef, children: [
315
- /* @__PURE__ */ jsx("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */ jsxs(
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(
316
528
  ReactFlow,
317
529
  {
318
530
  nodes,
@@ -342,8 +554,8 @@ var Canvas = ({ schema, adminURLs }) => {
342
554
  ariaLabel: "Data model overview"
343
555
  }
344
556
  ),
345
- /* @__PURE__ */ jsx(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs("div", { className: "rs-lineage__toolbar", children: [
346
- /* @__PURE__ */ jsxs("label", { className: "rs-lineage__toggle", children: [
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: [
347
559
  /* @__PURE__ */ jsx(
348
560
  "input",
349
561
  {
@@ -355,18 +567,30 @@ var Canvas = ({ schema, adminURLs }) => {
355
567
  /* @__PURE__ */ jsx("span", { className: "rs-lineage__swatch rs-lineage__swatch--global" }),
356
568
  "Globals"
357
569
  ] }),
358
- /* @__PURE__ */ jsxs("label", { className: "rs-lineage__toggle", children: [
570
+ /* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
359
571
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: keysOnly, onChange: () => setKeysOnly((v) => !v) }),
360
572
  "Key fields only"
361
573
  ] }),
362
- /* @__PURE__ */ jsxs("span", { className: "rs-lineage__count", children: [
574
+ /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__count", children: [
363
575
  entities.length,
364
576
  " tables, ",
365
577
  fieldCount,
366
578
  " fields, ",
367
579
  lines.length,
368
580
  " references"
369
- ] })
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
+ )
370
594
  ] }) })
371
595
  ]
372
596
  }
@@ -385,30 +609,30 @@ var Canvas = ({ schema, adminURLs }) => {
385
609
  };
386
610
  var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
387
611
  if (!entity) {
388
- return /* @__PURE__ */ jsxs("aside", { className: "rs-lineage__details", children: [
612
+ return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", children: [
389
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." }),
390
- /* @__PURE__ */ jsxs("dl", { className: "rs-lineage__legend", children: [
391
- /* @__PURE__ */ jsxs("div", { children: [
614
+ /* @__PURE__ */ jsxs2("dl", { className: "rs-lineage__legend", children: [
615
+ /* @__PURE__ */ jsxs2("div", { children: [
392
616
  /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "many-to-one" }),
393
617
  /* @__PURE__ */ jsx("dd", { children: "each document references one; many can share it" })
394
618
  ] }),
395
- /* @__PURE__ */ jsxs("div", { children: [
619
+ /* @__PURE__ */ jsxs2("div", { children: [
396
620
  /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "many-to-many" }),
397
621
  /* @__PURE__ */ jsx("dd", { children: "a list of references: hasMany, or inside an array or blocks" })
398
622
  ] }),
399
- /* @__PURE__ */ jsxs("div", { children: [
623
+ /* @__PURE__ */ jsxs2("div", { children: [
400
624
  /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "one-to-one" }),
401
625
  /* @__PURE__ */ jsx("dd", { children: "a unique reference, or a global's single one" })
402
626
  ] }),
403
- /* @__PURE__ */ jsxs("div", { children: [
627
+ /* @__PURE__ */ jsxs2("div", { children: [
404
628
  /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "one-to-many" }),
405
629
  /* @__PURE__ */ jsx("dd", { children: "a global's list of references" })
406
630
  ] }),
407
- /* @__PURE__ */ jsxs("div", { children: [
631
+ /* @__PURE__ */ jsxs2("div", { children: [
408
632
  /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "\u21BB" }),
409
633
  /* @__PURE__ */ jsx("dd", { children: "points at its own collection" })
410
634
  ] }),
411
- /* @__PURE__ */ jsxs("div", { children: [
635
+ /* @__PURE__ */ jsxs2("div", { children: [
412
636
  /* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "*" }),
413
637
  /* @__PURE__ */ jsx("dd", { children: "required" })
414
638
  ] })
@@ -422,8 +646,8 @@ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
422
646
  const other = byId.get(otherId);
423
647
  const from = byId.get(line.from);
424
648
  const to = byId.get(line.to);
425
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", { type: "button", className: "rs-lineage__link", onClick: () => onSelect(other.id), children: [
426
- /* @__PURE__ */ jsxs("span", { className: "rs-lineage__link-name", children: [
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: [
427
651
  /* @__PURE__ */ jsx("span", { className: `rs-lineage__swatch rs-lineage__swatch--${other.kind}` }),
428
652
  other.label,
429
653
  /* @__PURE__ */ jsx("span", { className: "rs-erd__cardinality-tag", children: line.cardinality })
@@ -432,15 +656,15 @@ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
432
656
  /* @__PURE__ */ jsx("span", { className: "rs-lineage__via", children: line.relations.map((r) => r.path).join(", ") })
433
657
  ] }) }, line.id);
434
658
  };
435
- return /* @__PURE__ */ jsxs("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
436
- /* @__PURE__ */ jsxs("div", { children: [
437
- /* @__PURE__ */ jsxs("span", { className: `rs-lineage__kind rs-lineage__kind--${entity.kind}`, children: [
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: [
438
662
  entity.kind,
439
663
  entity.auth ? " \xB7 auth" : "",
440
664
  entity.upload ? " \xB7 upload" : ""
441
665
  ] }),
442
666
  /* @__PURE__ */ jsx("h2", { className: "rs-lineage__name", children: entity.label }),
443
- /* @__PURE__ */ jsxs("code", { className: "rs-lineage__id", children: [
667
+ /* @__PURE__ */ jsxs2("code", { className: "rs-lineage__id", children: [
444
668
  entity.slug,
445
669
  " \xB7 ",
446
670
  entity.fields.length,
@@ -448,15 +672,15 @@ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
448
672
  ] })
449
673
  ] }),
450
674
  url && /* @__PURE__ */ jsx("a", { className: "rs-lineage__open", href: url, children: "Open in the admin" }),
451
- /* @__PURE__ */ jsxs("section", { children: [
452
- /* @__PURE__ */ jsxs("h3", { className: "rs-lineage__heading", children: [
675
+ /* @__PURE__ */ jsxs2("section", { children: [
676
+ /* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
453
677
  "References \xB7 ",
454
678
  out.length
455
679
  ] }),
456
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." })
457
681
  ] }),
458
- /* @__PURE__ */ jsxs("section", { children: [
459
- /* @__PURE__ */ jsxs("h3", { className: "rs-lineage__heading", children: [
682
+ /* @__PURE__ */ jsxs2("section", { children: [
683
+ /* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
460
684
  "Referenced by \xB7 ",
461
685
  into.length
462
686
  ] }),
@@ -478,7 +702,7 @@ var TableView = ({
478
702
  onSelect,
479
703
  onTrace
480
704
  }
481
- }) => /* @__PURE__ */ jsxs("div", { className: `rs-erd__table rs-lineage__node--${entity.kind} rs-erd__table--${state}`, children: [
705
+ }) => /* @__PURE__ */ jsxs2("div", { className: `rs-erd__table rs-lineage__node--${entity.kind} rs-erd__table--${state}`, children: [
482
706
  /* @__PURE__ */ jsx(
483
707
  Handle,
484
708
  {
@@ -489,7 +713,7 @@ var TableView = ({
489
713
  isConnectable: false
490
714
  }
491
715
  ),
492
- /* @__PURE__ */ jsxs(
716
+ /* @__PURE__ */ jsxs2(
493
717
  "button",
494
718
  {
495
719
  type: "button",
@@ -505,18 +729,18 @@ var TableView = ({
505
729
  ]
506
730
  }
507
731
  ),
508
- /* @__PURE__ */ jsx("ul", { className: "rs-erd__rows", children: rows.map((f) => /* @__PURE__ */ jsxs(
732
+ /* @__PURE__ */ jsx("ul", { className: "rs-erd__rows", children: rows.map((f) => /* @__PURE__ */ jsxs2(
509
733
  "li",
510
734
  {
511
735
  className: `rs-erd__row${linked.has(f.name) ? " rs-erd__row--link" : ""}`,
512
736
  title: describe(f),
513
737
  children: [
514
- /* @__PURE__ */ jsxs("span", { className: "rs-erd__field", children: [
738
+ /* @__PURE__ */ jsxs2("span", { className: "rs-erd__field", children: [
515
739
  f.name,
516
740
  f.required && /* @__PURE__ */ jsx("span", { className: "rs-erd__required", children: "*" }),
517
741
  selfRefs.has(f.name) && /* @__PURE__ */ jsx("span", { className: "rs-erd__self", title: "Points at its own collection", children: "\u21BB" })
518
742
  ] }),
519
- /* @__PURE__ */ jsxs("span", { className: "rs-erd__type", children: [
743
+ /* @__PURE__ */ jsxs2("span", { className: "rs-erd__type", children: [
520
744
  f.type === "blocks" ? `blocks (${f.blocks?.length ?? 0})` : f.type,
521
745
  f.hasMany ? "[]" : ""
522
746
  ] }),
@@ -573,46 +797,8 @@ import {
573
797
  ReactFlowProvider as ReactFlowProvider2,
574
798
  useReactFlow as useReactFlow2
575
799
  } from "@xyflow/react";
576
-
577
- // ../lineage-core/src/traverse.ts
578
- function adjacency(links) {
579
- const up = /* @__PURE__ */ new Map();
580
- const down = /* @__PURE__ */ new Map();
581
- const add = (map, key, value) => {
582
- const list = map.get(key);
583
- if (list) list.push(value);
584
- else map.set(key, [value]);
585
- };
586
- for (const { source, target } of links) {
587
- add(up, target, source);
588
- add(down, source, target);
589
- }
590
- return { up, down };
591
- }
592
- function walk(start, next, maxDepth = Infinity) {
593
- const seen = /* @__PURE__ */ new Set();
594
- let frontier = [start];
595
- for (let level = 0; level < maxDepth && frontier.length; level++) {
596
- const reached = [];
597
- for (const id of frontier) {
598
- for (const to of next.get(id) ?? []) {
599
- if (to === start || seen.has(to)) continue;
600
- seen.add(to);
601
- reached.push(to);
602
- }
603
- }
604
- frontier = reached;
605
- }
606
- return seen;
607
- }
608
- function trace(id, links) {
609
- const { up, down } = adjacency(links);
610
- return /* @__PURE__ */ new Set([id, ...walk(id, up), ...walk(id, down)]);
611
- }
612
-
613
- // src/view/LineageCanvas.tsx
614
800
  import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
615
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
801
+ import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
616
802
  var KINDS = [
617
803
  { kind: "collection", label: "Collections" },
618
804
  { kind: "block", label: "Blocks" },
@@ -703,8 +889,8 @@ var Canvas2 = ({ graph, adminURLs }) => {
703
889
  const links = visible.edges.length;
704
890
  return (
705
891
  // `data-ready` once the first layout is done, for tests to wait on
706
- /* @__PURE__ */ jsxs2("div", { className: "rs-lineage-layout", ref: layoutRef, children: [
707
- /* @__PURE__ */ jsx2("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */ jsxs2(
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(
708
894
  ReactFlow2,
709
895
  {
710
896
  nodes,
@@ -734,8 +920,8 @@ var Canvas2 = ({ graph, adminURLs }) => {
734
920
  ariaLabel: "Content lineage overview"
735
921
  }
736
922
  ),
737
- /* @__PURE__ */ jsx2(Panel2, { position: "top-left", children: /* @__PURE__ */ jsxs2("div", { className: "rs-lineage__toolbar", children: [
738
- KINDS.map(({ kind, label }) => /* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
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: [
739
925
  /* @__PURE__ */ jsx2(
740
926
  "input",
741
927
  {
@@ -747,7 +933,7 @@ var Canvas2 = ({ graph, adminURLs }) => {
747
933
  /* @__PURE__ */ jsx2("span", { className: `rs-lineage__swatch rs-lineage__swatch--${kind}` }),
748
934
  label
749
935
  ] }, kind)),
750
- /* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
936
+ /* @__PURE__ */ jsxs3("label", { className: "rs-lineage__toggle", children: [
751
937
  /* @__PURE__ */ jsx2(
752
938
  "input",
753
939
  {
@@ -758,13 +944,20 @@ var Canvas2 = ({ graph, adminURLs }) => {
758
944
  ),
759
945
  "Hide unconnected"
760
946
  ] }),
761
- /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__count", children: [
947
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__count", children: [
762
948
  visible.nodes.length,
763
949
  " nodes, ",
764
950
  links,
765
951
  " link",
766
952
  links === 1 ? "" : "s"
767
- ] })
953
+ ] }),
954
+ /* @__PURE__ */ jsx2(
955
+ CopyMermaid,
956
+ {
957
+ text: () => contentGraphToMermaid({ nodes: visible.nodes, edges: visible.edges }),
958
+ size: visible.nodes.length
959
+ }
960
+ )
768
961
  ] }) })
769
962
  ]
770
963
  }
@@ -784,18 +977,18 @@ var Canvas2 = ({ graph, adminURLs }) => {
784
977
  };
785
978
  var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
786
979
  if (!node) {
787
- return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", children: [
980
+ return /* @__PURE__ */ jsxs3("aside", { className: "rs-lineage__details", children: [
788
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." }),
789
- /* @__PURE__ */ jsxs2("dl", { className: "rs-lineage__legend", children: [
790
- /* @__PURE__ */ jsxs2("div", { children: [
982
+ /* @__PURE__ */ jsxs3("dl", { className: "rs-lineage__legend", children: [
983
+ /* @__PURE__ */ jsxs3("div", { children: [
791
984
  /* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--relationship" }),
792
985
  /* @__PURE__ */ jsx2("dd", { children: "relationship" })
793
986
  ] }),
794
- /* @__PURE__ */ jsxs2("div", { children: [
987
+ /* @__PURE__ */ jsxs3("div", { children: [
795
988
  /* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--upload" }),
796
989
  /* @__PURE__ */ jsx2("dd", { children: "upload" })
797
990
  ] }),
798
- /* @__PURE__ */ jsxs2("div", { children: [
991
+ /* @__PURE__ */ jsxs3("div", { children: [
799
992
  /* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--blocks" }),
800
993
  /* @__PURE__ */ jsx2("dd", { children: "blocks field" })
801
994
  ] })
@@ -807,34 +1000,34 @@ var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
807
1000
  const url = adminURLs[node.id];
808
1001
  const item = (edge, otherId) => {
809
1002
  const other = byId.get(otherId);
810
- return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs2("button", { type: "button", className: "rs-lineage__link", onClick: () => onSelect(other.id), children: [
811
- /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__link-name", children: [
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: [
812
1005
  /* @__PURE__ */ jsx2("span", { className: `rs-lineage__swatch rs-lineage__swatch--${other.kind}` }),
813
1006
  edge.source === edge.target ? "Itself" : other.label
814
1007
  ] }),
815
- /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__via", children: [
1008
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__via", children: [
816
1009
  edge.kind,
817
1010
  " \xB7 ",
818
1011
  edge.fields.join(", ")
819
1012
  ] })
820
1013
  ] }) }, `${edge.source}->${edge.target}:${edge.kind}`);
821
1014
  };
822
- return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
823
- /* @__PURE__ */ jsxs2("div", { children: [
1015
+ return /* @__PURE__ */ jsxs3("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
1016
+ /* @__PURE__ */ jsxs3("div", { children: [
824
1017
  /* @__PURE__ */ jsx2("span", { className: `rs-lineage__kind rs-lineage__kind--${node.kind}`, children: node.kind }),
825
1018
  /* @__PURE__ */ jsx2("h2", { className: "rs-lineage__name", children: node.label }),
826
1019
  /* @__PURE__ */ jsx2("code", { className: "rs-lineage__id", children: node.id })
827
1020
  ] }),
828
1021
  url && /* @__PURE__ */ jsx2("a", { className: "rs-lineage__open", href: url, children: "Open in the admin" }),
829
- /* @__PURE__ */ jsxs2("section", { children: [
830
- /* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
1022
+ /* @__PURE__ */ jsxs3("section", { children: [
1023
+ /* @__PURE__ */ jsxs3("h3", { className: "rs-lineage__heading", children: [
831
1024
  "Built from \xB7 ",
832
1025
  builtFrom.length
833
1026
  ] }),
834
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." })
835
1028
  ] }),
836
- /* @__PURE__ */ jsxs2("section", { children: [
837
- /* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
1029
+ /* @__PURE__ */ jsxs3("section", { children: [
1030
+ /* @__PURE__ */ jsxs3("h3", { className: "rs-lineage__heading", children: [
838
1031
  "Used by \xB7 ",
839
1032
  usedBy.length
840
1033
  ] }),
@@ -844,7 +1037,7 @@ var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
844
1037
  };
845
1038
  var ContentNodeView = ({
846
1039
  data: { node, state, selfLink, onSelect, onTrace }
847
- }) => /* @__PURE__ */ jsxs2(
1040
+ }) => /* @__PURE__ */ jsxs3(
848
1041
  "div",
849
1042
  {
850
1043
  className: `rs-lineage__node rs-lineage__node--${node.kind} rs-lineage__node--${state}`,
@@ -859,7 +1052,7 @@ var ContentNodeView = ({
859
1052
  isConnectable: false
860
1053
  }
861
1054
  ),
862
- /* @__PURE__ */ jsxs2(
1055
+ /* @__PURE__ */ jsxs3(
863
1056
  "button",
864
1057
  {
865
1058
  type: "button",
@@ -869,11 +1062,11 @@ var ContentNodeView = ({
869
1062
  onFocus: () => onTrace(node.id),
870
1063
  onBlur: () => onTrace(null),
871
1064
  children: [
872
- /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__label", children: [
1065
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__label", children: [
873
1066
  node.label,
874
1067
  selfLink && /* @__PURE__ */ jsx2("span", { className: "rs-lineage__self", title: "Links to itself", children: "\u21BB" })
875
1068
  ] }),
876
- /* @__PURE__ */ jsxs2("span", { className: "rs-lineage__meta", children: [
1069
+ /* @__PURE__ */ jsxs3("span", { className: "rs-lineage__meta", children: [
877
1070
  node.kind,
878
1071
  " \xB7 ",
879
1072
  node.slug
@@ -899,13 +1092,13 @@ var nodeTypes2 = { content: ContentNodeView };
899
1092
  import { Link, useConfig } from "@payloadcms/ui";
900
1093
  import { usePathname } from "next/navigation";
901
1094
  import { formatAdminURL } from "payload/shared";
902
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1095
+ import { Fragment, jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
903
1096
  var LineageNavLinkClient = () => {
904
1097
  const { config } = useConfig();
905
1098
  const pathname = usePathname();
906
1099
  const href = formatAdminURL({ adminRoute: config.routes.admin, path: "/lineage" });
907
1100
  const active = pathname === href || pathname.startsWith(`${href}/`);
908
- const label = /* @__PURE__ */ jsxs3(Fragment, { children: [
1101
+ const label = /* @__PURE__ */ jsxs4(Fragment, { children: [
909
1102
  active && /* @__PURE__ */ jsx3("div", { className: "nav__link-indicator" }),
910
1103
  /* @__PURE__ */ jsx3("span", { className: "nav__link-label", children: "Lineage" })
911
1104
  ] });
package/dist/index.d.ts CHANGED
@@ -151,6 +151,16 @@ type Schema = {
151
151
  /** Builds the data model of a Payload config: each collection and global with its fields, and the relations between them. */
152
152
  declare function buildSchema(config: PayloadConfigLike, { exclude }?: ContentGraphOptions): Schema;
153
153
 
154
+ /** The lineage graph as a left-to-right flowchart, each link labelled with its fields. */
155
+ declare function contentGraphToMermaid(graph: ContentGraph): string;
156
+ /**
157
+ * The data model as an erDiagram: a table per collection and global with its fields, and a line
158
+ * per row and target, in crow's-foot notation (`}o--o|` is many-to-one, `}o--o{` many-to-many).
159
+ * Fields keep their full path in their comment, and lines are labelled with the full paths of the
160
+ * references they stand for.
161
+ */
162
+ declare function schemaToMermaid(schema: Schema): string;
163
+
154
164
  /**
155
165
  * Adds content lineage to a Payload config: the Lineage and Data model pages in the admin, at
156
166
  * `/admin/lineage` and `/admin/lineage/schema`, and with `outputFile`, the lineage graph as JSON on
@@ -158,4 +168,4 @@ declare function buildSchema(config: PayloadConfigLike, { exclude }?: ContentGra
158
168
  */
159
169
  declare function lineagePlugin({ enabled, adminView, access, outputFile, ...graphOptions }?: LineagePluginOptions): Plugin;
160
170
 
161
- export { type ContentEdge, type ContentEdgeKind, type ContentGraph, type ContentGraphOptions, type ContentKind, type ContentNode, type LineageAccess, type LineageCustomConfig, type LineagePluginOptions, type PayloadConfigLike, type Schema, type SchemaEntity, type SchemaField, type SchemaRelation, buildContentGraph, buildSchema, lineagePlugin };
171
+ export { type ContentEdge, type ContentEdgeKind, type ContentGraph, type ContentGraphOptions, type ContentKind, type ContentNode, type LineageAccess, type LineageCustomConfig, type LineagePluginOptions, type PayloadConfigLike, type Schema, type SchemaEntity, type SchemaField, type SchemaRelation, buildContentGraph, buildSchema, contentGraphToMermaid, lineagePlugin, schemaToMermaid };
package/dist/index.js CHANGED
@@ -3,6 +3,130 @@ import {
3
3
  buildSchema
4
4
  } from "./chunk-GL5UMO2C.js";
5
5
 
6
+ // ../lineage-core/src/layout.ts
7
+ import ELK from "elkjs/lib/elk.bundled.js";
8
+
9
+ // ../lineage-core/src/mermaid.ts
10
+ var ARROW = { solid: "-->", dashed: "-.->", thick: "==>" };
11
+ var MERMAID_COLOR = /^(#[0-9a-f]{3,8}|[a-z]+)$/i;
12
+ function toMermaidFlowchart({
13
+ nodes,
14
+ edges,
15
+ groups = [],
16
+ direction = "LR"
17
+ }) {
18
+ const ids = new Map(nodes.map((n, i) => [n.id, `n${i}`]));
19
+ const lines = [`flowchart ${direction}`];
20
+ for (const n of nodes) lines.push(` ${ids.get(n.id)}["${escapeLabel(n.label)}"]`);
21
+ for (const e of edges) {
22
+ const from = ids.get(e.source);
23
+ const to = ids.get(e.target);
24
+ if (!from || !to) continue;
25
+ const arrow = ARROW[e.style ?? "solid"];
26
+ const label = e.label ? `|"${escapeLabel(e.label)}"|` : "";
27
+ lines.push(` ${from} ${arrow}${label} ${to}`);
28
+ }
29
+ const used = new Set(nodes.map((n) => n.group).filter(Boolean));
30
+ groups.forEach((g, i) => {
31
+ if (!used.has(g.id) || !MERMAID_COLOR.test(g.color)) return;
32
+ const members = nodes.filter((n) => n.group === g.id).map((n) => ids.get(n.id));
33
+ lines.push(` classDef g${i} fill:${g.color},stroke:${g.color},color:#fff`);
34
+ lines.push(` class ${members.join(",")} g${i}`);
35
+ });
36
+ return `${lines.join("\n")}
37
+ `;
38
+ }
39
+ function escapeLabel(text) {
40
+ return text.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;");
41
+ }
42
+
43
+ // src/mermaid.ts
44
+ var KIND_COLORS = {
45
+ collection: "#0f7c74",
46
+ block: "#b4630c",
47
+ global: "#6b4bb8"
48
+ };
49
+ var EDGE_STYLE = { relationship: "solid", upload: "dashed", blocks: "thick" };
50
+ function contentGraphToMermaid(graph) {
51
+ return toMermaidFlowchart({
52
+ nodes: graph.nodes.map((n) => ({ id: n.id, label: n.label, group: n.kind })),
53
+ edges: graph.edges.map((e) => ({
54
+ source: e.source,
55
+ target: e.target,
56
+ label: e.fields.join(", "),
57
+ style: EDGE_STYLE[e.kind]
58
+ })),
59
+ groups: Object.entries(KIND_COLORS).map(([id, color]) => ({ id, color }))
60
+ });
61
+ }
62
+ function schemaToMermaid(schema) {
63
+ const names = entityNames(schema.entities);
64
+ const byId = new Map(schema.entities.map((e) => [e.id, e]));
65
+ const lines = ["erDiagram"];
66
+ for (const entity of schema.entities) {
67
+ const keys = new Set(schema.relations.filter((r) => r.from === entity.id).map((r) => r.field));
68
+ lines.push(` ${names.get(entity.id)}["${escapeLabel(entity.label)}"] {`);
69
+ for (const field of entity.fields) lines.push(` ${attribute(field, keys.has(field.name))}`);
70
+ lines.push(" }");
71
+ }
72
+ const lineGroups = /* @__PURE__ */ new Map();
73
+ for (const r of schema.relations) {
74
+ if (!names.has(r.from) || !names.has(r.to)) continue;
75
+ const key = `${r.from}
76
+ ${r.field}
77
+ ${r.to}`;
78
+ lineGroups.set(key, [...lineGroups.get(key) ?? [], r]);
79
+ }
80
+ for (const group of lineGroups.values()) {
81
+ const { from, to, field } = group[0];
82
+ const row = byId.get(from)?.fields.find((f) => f.name === field);
83
+ const label = [...new Set(group.map((r) => r.path))].join(", ");
84
+ lines.push(
85
+ ` ${names.get(from)} ${crowsFoot(group, from, !!row?.required)} ${names.get(to)} : "${escapeLabel(label)}"`
86
+ );
87
+ }
88
+ return `${lines.join("\n")}
89
+ `;
90
+ }
91
+ function entityNames(entities) {
92
+ const names = /* @__PURE__ */ new Map();
93
+ const taken = /* @__PURE__ */ new Set();
94
+ for (const e of entities) {
95
+ const base = `${e.kind === "global" ? "global_" : ""}${e.slug}`.replace(/[^A-Za-z0-9_-]/g, "_");
96
+ let name = /^[A-Za-z]/.test(base) ? base : `e_${base}`;
97
+ for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
98
+ taken.add(name);
99
+ names.set(e.id, name);
100
+ }
101
+ return names;
102
+ }
103
+ function attribute(field, isReference) {
104
+ const type = `${safeWord(field.type)}${field.hasMany ? "[]" : ""}`;
105
+ const name = safeWord(field.name);
106
+ const key = field.type === "id" || field.name === "id" ? " PK" : isReference ? " FK" : "";
107
+ const notes = [
108
+ ...name !== field.name ? [field.name] : [],
109
+ ...field.required ? ["required"] : [],
110
+ ...field.unique ? ["unique"] : [],
111
+ ...field.relationTo ? [`\u2192 ${field.relationTo.join(" | ")}`] : [],
112
+ ...field.blocks ? [`blocks: ${field.blocks.join(", ")}`] : []
113
+ ];
114
+ const comment = notes.length ? ` "${escapeLabel(notes.join(", "))}"` : "";
115
+ return `${type} ${name}${key}${comment}`;
116
+ }
117
+ function safeWord(text) {
118
+ const word = text.replace(/[^A-Za-z0-9_\-[\]()]/g, "_");
119
+ return /^[A-Za-z]/.test(word) ? word : `f_${word}`;
120
+ }
121
+ function crowsFoot(relations, from, required) {
122
+ const many = relations.some((r) => r.many);
123
+ const atMostOne = from.startsWith("global:") || relations.every((r) => r.unique) && !many;
124
+ const fromSide = atMostOne ? "|o" : "}o";
125
+ const atLeastOne = required && relations.every((r) => !r.polymorphic && r.field === r.path);
126
+ const toSide = many ? atLeastOne ? "|{" : "o{" : atLeastOne ? "||" : "o|";
127
+ return `${fromSide}--${toSide}`;
128
+ }
129
+
6
130
  // src/plugin.ts
7
131
  import { mkdir, writeFile } from "fs/promises";
8
132
  import path from "path";
@@ -77,5 +201,7 @@ function withOutputFile(config, outputFile, graphOptions) {
77
201
  export {
78
202
  buildContentGraph,
79
203
  buildSchema,
80
- lineagePlugin
204
+ contentGraphToMermaid,
205
+ lineagePlugin,
206
+ schemaToMermaid
81
207
  };
package/dist/lineage.css CHANGED
@@ -203,6 +203,24 @@ html[data-theme='dark'] .rs-lineage {
203
203
  .rs-lineage__toggle input {
204
204
  margin: 0;
205
205
  }
206
+ .rs-lineage__copy {
207
+ appearance: none;
208
+ margin: 0;
209
+ padding: 3px 10px;
210
+ border: 1px solid var(--theme-elevation-250);
211
+ border-radius: var(--style-radius-s, 4px);
212
+ background: var(--theme-elevation-50);
213
+ color: var(--theme-text);
214
+ font: inherit;
215
+ cursor: pointer;
216
+ }
217
+ .rs-lineage__copy:hover {
218
+ background: var(--theme-elevation-100);
219
+ }
220
+ .rs-lineage__copy:focus-visible {
221
+ outline: 2px solid var(--theme-elevation-900);
222
+ outline-offset: 2px;
223
+ }
206
224
  .rs-lineage__count {
207
225
  color: var(--theme-elevation-600);
208
226
  font-variant-numeric: tabular-nums;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocksoliddigital/payload-lineage",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lineage and data model (ERD) pages for the Payload admin: the collections, globals and blocks, and the fields that link them.",
5
5
  "keywords": [
6
6
  "payload",
@@ -52,13 +52,13 @@
52
52
  "@types/react": "19.2.14",
53
53
  "@types/react-dom": "19.2.3",
54
54
  "graphql": "16.14.2",
55
- "next": "16.3.3",
55
+ "next": "16.3.8",
56
56
  "payload": "3.90.2",
57
57
  "react": "19.2.6",
58
58
  "react-dom": "19.2.6",
59
59
  "tsup": "8.5.1",
60
60
  "typescript": "5.7.3",
61
- "vitest": "4.0.18"
61
+ "vitest": "4.1.11"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@payloadcms/next": "^3.0.0",