@steffolino/diagram-kit-core 0.1.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -0
  3. package/dist/cssPalette.d.ts +15 -0
  4. package/dist/cssPalette.d.ts.map +1 -0
  5. package/dist/cssPalette.js +37 -0
  6. package/dist/cssPalette.js.map +1 -0
  7. package/dist/cycleLayout.d.ts +13 -0
  8. package/dist/cycleLayout.d.ts.map +1 -0
  9. package/dist/cycleLayout.js +174 -0
  10. package/dist/cycleLayout.js.map +1 -0
  11. package/dist/detailText.d.ts +5 -0
  12. package/dist/detailText.d.ts.map +1 -0
  13. package/dist/detailText.js +34 -0
  14. package/dist/detailText.js.map +1 -0
  15. package/dist/graph.d.ts +52 -0
  16. package/dist/graph.d.ts.map +1 -0
  17. package/dist/graph.js +30 -0
  18. package/dist/graph.js.map +1 -0
  19. package/dist/graph.test.d.ts +2 -0
  20. package/dist/graph.test.d.ts.map +1 -0
  21. package/dist/graph.test.js +33 -0
  22. package/dist/graph.test.js.map +1 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +12 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/layout.d.ts +46 -0
  28. package/dist/layout.d.ts.map +1 -0
  29. package/dist/layout.js +57 -0
  30. package/dist/layout.js.map +1 -0
  31. package/dist/layout.test.d.ts +2 -0
  32. package/dist/layout.test.d.ts.map +1 -0
  33. package/dist/layout.test.js +72 -0
  34. package/dist/layout.test.js.map +1 -0
  35. package/dist/path.d.ts +25 -0
  36. package/dist/path.d.ts.map +1 -0
  37. package/dist/path.js +60 -0
  38. package/dist/path.js.map +1 -0
  39. package/dist/presets.d.ts +15 -0
  40. package/dist/presets.d.ts.map +1 -0
  41. package/dist/presets.js +211 -0
  42. package/dist/presets.js.map +1 -0
  43. package/dist/stackLayout.d.ts +22 -0
  44. package/dist/stackLayout.d.ts.map +1 -0
  45. package/dist/stackLayout.js +47 -0
  46. package/dist/stackLayout.js.map +1 -0
  47. package/dist/stackLayout.test.d.ts +2 -0
  48. package/dist/stackLayout.test.d.ts.map +1 -0
  49. package/dist/stackLayout.test.js +48 -0
  50. package/dist/stackLayout.test.js.map +1 -0
  51. package/dist/structuralLayout.d.ts +30 -0
  52. package/dist/structuralLayout.d.ts.map +1 -0
  53. package/dist/structuralLayout.js +117 -0
  54. package/dist/structuralLayout.js.map +1 -0
  55. package/dist/structuralLayout.test.d.ts +2 -0
  56. package/dist/structuralLayout.test.d.ts.map +1 -0
  57. package/dist/structuralLayout.test.js +74 -0
  58. package/dist/structuralLayout.test.js.map +1 -0
  59. package/dist/textWidth.d.ts +18 -0
  60. package/dist/textWidth.d.ts.map +1 -0
  61. package/dist/textWidth.js +50 -0
  62. package/dist/textWidth.js.map +1 -0
  63. package/dist/theme.d.ts +68 -0
  64. package/dist/theme.d.ts.map +1 -0
  65. package/dist/theme.js +94 -0
  66. package/dist/theme.js.map +1 -0
  67. package/package.json +51 -0
@@ -0,0 +1,30 @@
1
+ import type { DiagramGraph } from './graph.js';
2
+ import type { PositionedGraph } from './layout.js';
3
+ export interface StructuralLayoutOptions {
4
+ showDetails?: boolean;
5
+ /** Inner padding between a container's border and its children. Default 16. */
6
+ padding?: number;
7
+ /** Gap between sibling boxes, both within a row and between rows. Default 10. */
8
+ gap?: number;
9
+ /** Minimum leaf width — leaves with longer labels grow wider to fit, up to `maxLeafWidth`, when `nodeSizing` is "responsive". Default 140. */
10
+ leafWidth?: number;
11
+ /** Cap on how wide a single leaf can grow to fit its label. Default 420. */
12
+ maxLeafWidth?: number;
13
+ /** "responsive" (default): leaves grow to fit their label, up to `maxLeafWidth`. "fixed": every leaf is exactly `leafWidth` wide, and a label too long to fit gets truncated with an ellipsis instead. */
14
+ nodeSizing?: 'responsive' | 'fixed';
15
+ leafHeight?: number;
16
+ /** Space reserved at the top of a container for its own label. Default 30. */
17
+ headerHeight?: number;
18
+ /** Children wrap onto a new row once a row would exceed this width. Default 560. */
19
+ maxRowWidth?: number;
20
+ }
21
+ /**
22
+ * Nested-box layout from `DiagramNode.parentId` containment — a container
23
+ * is sized to fit its children (wrapped into rows), no connector lines are
24
+ * produced, and containment is read from nesting instead. This is the
25
+ * "structural" counterpart to `layoutGraph`'s node-and-edge DAG layout, for
26
+ * data that's naturally a tree (a directory walk, a context's internal
27
+ * layers) rather than an arbitrary graph.
28
+ */
29
+ export declare function layoutStructural(graph: DiagramGraph, options?: StructuralLayoutOptions): PositionedGraph;
30
+ //# sourceMappingURL=structuralLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuralLayout.d.ts","sourceRoot":"","sources":["../src/structuralLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,aAAa,CAAA;AAIlE,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8IAA8I;IAC9I,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0MAA0M;IAC1M,UAAU,CAAC,EAAE,YAAY,GAAG,OAAO,CAAA;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAqCD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,GAAE,uBAA4B,GAAG,eAAe,CAoF5G"}
@@ -0,0 +1,117 @@
1
+ import { estimateNodeWidth, estimateTextWidth } from './textWidth.js';
2
+ import { detailHeight } from './detailText.js';
3
+ const DEFAULTS = {
4
+ showDetails: false,
5
+ padding: 16,
6
+ gap: 10,
7
+ leafWidth: 140,
8
+ maxLeafWidth: 420,
9
+ nodeSizing: 'responsive',
10
+ leafHeight: 40,
11
+ headerHeight: 30,
12
+ maxRowWidth: 560,
13
+ };
14
+ function wrapIntoRows(items, gap, maxRowWidth) {
15
+ const rows = [];
16
+ let row = [];
17
+ let rowWidth = 0;
18
+ for (const item of items) {
19
+ const nextWidth = rowWidth + (row.length > 0 ? gap : 0) + item.width;
20
+ if (row.length > 0 && nextWidth > maxRowWidth) {
21
+ rows.push(row);
22
+ row = [];
23
+ rowWidth = 0;
24
+ }
25
+ row.push(item);
26
+ rowWidth += (row.length > 1 ? gap : 0) + item.width;
27
+ }
28
+ if (row.length > 0)
29
+ rows.push(row);
30
+ return rows;
31
+ }
32
+ /**
33
+ * Nested-box layout from `DiagramNode.parentId` containment — a container
34
+ * is sized to fit its children (wrapped into rows), no connector lines are
35
+ * produced, and containment is read from nesting instead. This is the
36
+ * "structural" counterpart to `layoutGraph`'s node-and-edge DAG layout, for
37
+ * data that's naturally a tree (a directory walk, a context's internal
38
+ * layers) rather than an arbitrary graph.
39
+ */
40
+ export function layoutStructural(graph, options = {}) {
41
+ const opts = { ...DEFAULTS, ...options };
42
+ const childrenByParent = new Map();
43
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
44
+ for (const node of graph.nodes) {
45
+ const list = childrenByParent.get(node.parentId) ?? [];
46
+ list.push(node.id);
47
+ childrenByParent.set(node.parentId, list);
48
+ }
49
+ const sizeCache = new Map();
50
+ function measure(id) {
51
+ const cached = sizeCache.get(id);
52
+ if (cached)
53
+ return cached;
54
+ const node = nodeById.get(id);
55
+ const childIds = childrenByParent.get(id) ?? [];
56
+ let size;
57
+ if (childIds.length === 0) {
58
+ const width = estimateNodeWidth(node.label, node.badge, {
59
+ minWidth: opts.leafWidth,
60
+ maxWidth: opts.nodeSizing === 'fixed' ? opts.leafWidth : opts.maxLeafWidth,
61
+ });
62
+ size = { width, height: opts.leafHeight + detailHeight(node, width, opts.showDetails) };
63
+ }
64
+ else {
65
+ const childSizes = childIds.map(measure);
66
+ const rows = wrapIntoRows(childSizes, opts.gap, opts.maxRowWidth);
67
+ const contentWidth = Math.max(...rows.map((row) => row.reduce((sum, s, i) => sum + s.width + (i > 0 ? opts.gap : 0), 0)));
68
+ const contentHeight = rows.reduce((sum, row, i) => sum + Math.max(...row.map((s) => s.height)) + (i > 0 ? opts.gap : 0), 0);
69
+ // A container must also be wide enough for its own header label —
70
+ // otherwise a container with few/narrow children but a long name
71
+ // (e.g. a deeply-nested directory) overflows its own header text.
72
+ // In "fixed" mode the header is capped instead (and truncated when
73
+ // rendered), matching leaves not growing either.
74
+ const rawHeaderWidth = estimateTextWidth(node.label, 12) + 40;
75
+ const headerWidth = opts.nodeSizing === 'fixed' ? Math.min(rawHeaderWidth, opts.leafWidth) : rawHeaderWidth;
76
+ size = {
77
+ width: Math.max(contentWidth + opts.padding * 2, headerWidth),
78
+ height: contentHeight + opts.padding * 2 + opts.headerHeight,
79
+ };
80
+ size.height += detailHeight(node, size.width, opts.showDetails);
81
+ }
82
+ sizeCache.set(id, size);
83
+ return size;
84
+ }
85
+ for (const node of graph.nodes)
86
+ measure(node.id);
87
+ const positioned = [];
88
+ function place(id, x, y) {
89
+ const size = sizeCache.get(id);
90
+ positioned.push({ id, x: x + size.width / 2, y: y + size.height / 2, width: size.width, height: size.height });
91
+ const childIds = childrenByParent.get(id) ?? [];
92
+ if (childIds.length === 0)
93
+ return;
94
+ const children = childIds.map((childId) => ({ id: childId, ...sizeCache.get(childId) }));
95
+ const rows = wrapIntoRows(children, opts.gap, opts.maxRowWidth);
96
+ let cursorY = y + opts.padding + opts.headerHeight + detailHeight(nodeById.get(id), size.width, opts.showDetails);
97
+ for (const row of rows) {
98
+ let cursorX = x + opts.padding;
99
+ const rowHeight = Math.max(...row.map((c) => c.height));
100
+ for (const child of row) {
101
+ place(child.id, cursorX, cursorY);
102
+ cursorX += child.width + opts.gap;
103
+ }
104
+ cursorY += rowHeight + opts.gap;
105
+ }
106
+ }
107
+ const roots = childrenByParent.get(undefined) ?? [];
108
+ let cursorX = 0;
109
+ for (const rootId of roots) {
110
+ place(rootId, cursorX, 0);
111
+ cursorX += sizeCache.get(rootId).width + opts.gap;
112
+ }
113
+ const width = Math.max(0, ...positioned.map((p) => p.x + p.width / 2));
114
+ const height = Math.max(0, ...positioned.map((p) => p.y + p.height / 2));
115
+ return { nodes: positioned, edges: [], width, height };
116
+ }
117
+ //# sourceMappingURL=structuralLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuralLayout.js","sourceRoot":"","sources":["../src/structuralLayout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=structuralLayout.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuralLayout.test.d.ts","sourceRoot":"","sources":["../src/structuralLayout.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createGraph } from './graph.js';
3
+ import { layoutStructural } from './structuralLayout.js';
4
+ describe('layoutStructural', () => {
5
+ it('produces no edges regardless of the input graph having any', () => {
6
+ const graph = createGraph([
7
+ { id: 'parent', label: 'Parent' },
8
+ { id: 'child', label: 'Child', parentId: 'parent' },
9
+ ], [{ id: 'x', source: 'parent', target: 'child' }]);
10
+ const positioned = layoutStructural(graph);
11
+ expect(positioned.edges).toEqual([]);
12
+ });
13
+ it('sizes a container to enclose its children', () => {
14
+ const graph = createGraph([
15
+ { id: 'parent', label: 'Parent' },
16
+ { id: 'a', label: 'A', parentId: 'parent' },
17
+ { id: 'b', label: 'B', parentId: 'parent' },
18
+ ]);
19
+ const positioned = layoutStructural(graph);
20
+ const parent = positioned.nodes.find((n) => n.id === 'parent');
21
+ const a = positioned.nodes.find((n) => n.id === 'a');
22
+ const b = positioned.nodes.find((n) => n.id === 'b');
23
+ const parentLeft = parent.x - parent.width / 2;
24
+ const parentRight = parent.x + parent.width / 2;
25
+ const parentTop = parent.y - parent.height / 2;
26
+ const parentBottom = parent.y + parent.height / 2;
27
+ for (const child of [a, b]) {
28
+ expect(child.x - child.width / 2).toBeGreaterThanOrEqual(parentLeft);
29
+ expect(child.x + child.width / 2).toBeLessThanOrEqual(parentRight);
30
+ expect(child.y - child.height / 2).toBeGreaterThanOrEqual(parentTop);
31
+ expect(child.y + child.height / 2).toBeLessThanOrEqual(parentBottom);
32
+ }
33
+ });
34
+ it('wraps children onto a new row once maxRowWidth is exceeded', () => {
35
+ const graph = createGraph([
36
+ { id: 'parent', label: 'Parent' },
37
+ { id: 'a', label: 'A', parentId: 'parent' },
38
+ { id: 'b', label: 'B', parentId: 'parent' },
39
+ { id: 'c', label: 'C', parentId: 'parent' },
40
+ ]);
41
+ const positioned = layoutStructural(graph, { leafWidth: 100, maxRowWidth: 250, gap: 10 });
42
+ const a = positioned.nodes.find((n) => n.id === 'a');
43
+ const c = positioned.nodes.find((n) => n.id === 'c');
44
+ // 3 leaves at 100 + 2 gaps of 10 = 320 > maxRowWidth 250, so the third
45
+ // leaf should wrap onto a second row (different y than the first).
46
+ expect(c.y).toBeGreaterThan(a.y);
47
+ });
48
+ it('sizes a leaf wide enough for a long label', () => {
49
+ const graph = createGraph([
50
+ { id: 'short', label: 'A' },
51
+ { id: 'long', label: 'A Much Much Longer Node Label' },
52
+ ]);
53
+ const positioned = layoutStructural(graph);
54
+ const short = positioned.nodes.find((n) => n.id === 'short');
55
+ const long = positioned.nodes.find((n) => n.id === 'long');
56
+ expect(long.width).toBeGreaterThan(short.width);
57
+ });
58
+ it('clamps leaf width in "fixed" sizing mode', () => {
59
+ const graph = createGraph([{ id: 'long', label: 'A Much Much Longer Node Label Than Default' }]);
60
+ const positioned = layoutStructural(graph, { nodeSizing: 'fixed', leafWidth: 140 });
61
+ expect(positioned.nodes[0].width).toBe(140);
62
+ });
63
+ it('places multiple root nodes side by side without overlapping', () => {
64
+ const graph = createGraph([
65
+ { id: 'root1', label: 'Root 1' },
66
+ { id: 'root2', label: 'Root 2' },
67
+ ]);
68
+ const positioned = layoutStructural(graph);
69
+ const root1 = positioned.nodes.find((n) => n.id === 'root1');
70
+ const root2 = positioned.nodes.find((n) => n.id === 'root2');
71
+ expect(root2.x - root2.width / 2).toBeGreaterThanOrEqual(root1.x + root1.width / 2);
72
+ });
73
+ });
74
+ //# sourceMappingURL=structuralLayout.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuralLayout.test.js","sourceRoot":"","sources":["../src/structuralLayout.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export declare function estimateTextWidth(text: string, fontSize?: number): number;
2
+ export interface NodeWidthOptions {
3
+ minWidth?: number;
4
+ maxWidth?: number;
5
+ fontSize?: number;
6
+ /** Fixed space consumed by the accent dot, gaps, and left/right padding. Default 46. */
7
+ horizontalPadding?: number;
8
+ }
9
+ /** Estimates the box width a node's chip/rect needs to fit its label (and badge) without the text overflowing, clamped to [minWidth, maxWidth]. */
10
+ export declare function estimateNodeWidth(label: string, badge: string | undefined, options?: NodeWidthOptions): number;
11
+ /**
12
+ * Shortens `label` with a trailing "…" until it fits `maxWidth`, for
13
+ * renderers (like static SVG) with no native text-truncation of their
14
+ * own — the React renderer gets this for free from CSS `text-overflow:
15
+ * ellipsis` instead.
16
+ */
17
+ export declare function truncateLabel(label: string, maxWidth: number, fontSize?: number): string;
18
+ //# sourceMappingURL=textWidth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textWidth.d.ts","sourceRoot":"","sources":["../src/textWidth.ts"],"names":[],"mappings":"AAaA,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAK,GAAG,MAAM,CASrE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,mJAAmJ;AACnJ,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAKlH;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAK,GAAG,MAAM,CAOpF"}
@@ -0,0 +1,50 @@
1
+ // Per-character width ratios (fraction of font-size), approximating a
2
+ // typical system-ui/sans-serif proportional font. There's no canvas or DOM
3
+ // available in a headless/static rendering context, so this is a portable
4
+ // estimate rather than a real text measurement — good enough to size a box
5
+ // to its label instead of using one fixed width for every node regardless
6
+ // of content.
7
+ const NARROW_CHARS = new Set(['i', 'l', 'j', '.', ',', "'", '!', ':', ';', '|', ' ', '"']);
8
+ const WIDE_CHARS = new Set(['m', 'w', 'M', 'W']);
9
+ const NARROW_WIDTH = 0.3;
10
+ const WIDE_WIDTH = 0.92;
11
+ const UPPERCASE_WIDTH = 0.66;
12
+ const DEFAULT_WIDTH = 0.56;
13
+ export function estimateTextWidth(text, fontSize = 12) {
14
+ let total = 0;
15
+ for (const ch of text) {
16
+ if (NARROW_CHARS.has(ch))
17
+ total += NARROW_WIDTH;
18
+ else if (WIDE_CHARS.has(ch))
19
+ total += WIDE_WIDTH;
20
+ else if (ch >= 'A' && ch <= 'Z')
21
+ total += UPPERCASE_WIDTH;
22
+ else
23
+ total += DEFAULT_WIDTH;
24
+ }
25
+ return total * fontSize;
26
+ }
27
+ /** Estimates the box width a node's chip/rect needs to fit its label (and badge) without the text overflowing, clamped to [minWidth, maxWidth]. */
28
+ export function estimateNodeWidth(label, badge, options = {}) {
29
+ const { minWidth = 140, maxWidth = 420, fontSize = 12, horizontalPadding = 46 } = options;
30
+ let width = estimateTextWidth(label, fontSize) + horizontalPadding;
31
+ if (badge)
32
+ width += estimateTextWidth(badge, 10) + 10;
33
+ return Math.min(maxWidth, Math.max(minWidth, Math.ceil(width)));
34
+ }
35
+ /**
36
+ * Shortens `label` with a trailing "…" until it fits `maxWidth`, for
37
+ * renderers (like static SVG) with no native text-truncation of their
38
+ * own — the React renderer gets this for free from CSS `text-overflow:
39
+ * ellipsis` instead.
40
+ */
41
+ export function truncateLabel(label, maxWidth, fontSize = 12) {
42
+ if (estimateTextWidth(label, fontSize) <= maxWidth)
43
+ return label;
44
+ let end = label.length - 1;
45
+ while (end > 0 && estimateTextWidth(label.slice(0, end) + '…', fontSize) > maxWidth) {
46
+ end--;
47
+ }
48
+ return label.slice(0, end) + '…';
49
+ }
50
+ //# sourceMappingURL=textWidth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textWidth.js","sourceRoot":"","sources":["../src/textWidth.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,0EAA0E;AAC1E,cAAc;AACd,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;AAC1F,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;AAChD,MAAM,YAAY,GAAG,GAAG,CAAA;AACxB,MAAM,UAAU,GAAG,IAAI,CAAA;AACvB,MAAM,eAAe,GAAG,IAAI,CAAA;AAC5B,MAAM,aAAa,GAAG,IAAI,CAAA;AAE1B,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAQ,GAAG,EAAE;IAC3D,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACtB,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,KAAK,IAAI,YAAY,CAAA;aAC1C,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,KAAK,IAAI,UAAU,CAAA;aAC3C,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG;YAAE,KAAK,IAAI,eAAe,CAAA;;YACpD,KAAK,IAAI,aAAa,CAAA;IAC7B,CAAC;IACD,OAAO,KAAK,GAAG,QAAQ,CAAA;AACzB,CAAC;AAUD,mJAAmJ;AACnJ,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,KAAyB,EAAE,UAA4B,EAAE;IACxG,MAAM,EAAE,QAAQ,GAAG,GAAG,EAAE,QAAQ,GAAG,GAAG,EAAE,QAAQ,GAAG,EAAE,EAAE,iBAAiB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAA;IACzF,IAAI,KAAK,GAAG,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,iBAAiB,CAAA;IAClE,IAAI,KAAK;QAAE,KAAK,IAAI,iBAAiB,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,EAAE,CAAA;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,QAAgB,EAAE,QAAQ,GAAG,EAAE;IAC1E,IAAI,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,QAAQ;QAAE,OAAO,KAAK,CAAA;IAChE,IAAI,GAAG,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC1B,OAAO,GAAG,GAAG,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAAE,CAAC;QACpF,GAAG,EAAE,CAAA;IACP,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAA;AAClC,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Design tokens, not the fixed CSS "accent" class names used in earlier
3
+ * hand-built diagrams. A category (an adapter-defined string) is hashed
4
+ * onto a palette slot, so new categories degrade gracefully instead of
5
+ * falling back to an "unstyled" state.
6
+ */
7
+ export interface ThemeColors {
8
+ background: string;
9
+ surface: string;
10
+ border: string;
11
+ text: string;
12
+ textMuted: string;
13
+ edge: string;
14
+ edgeProjected: string;
15
+ /** Fallback palette for categories with no explicit entry in `categoryColors` — hashed onto a slot so unregistered categories still degrade gracefully instead of going unstyled. */
16
+ categories: string[];
17
+ /** Named category -> color. Takes precedence over `categories`. This is what a color picker keyed by category name (e.g. "application", "external") should write to. */
18
+ categoryColors?: Record<string, string>;
19
+ }
20
+ export interface DiagramTheme {
21
+ light: ThemeColors;
22
+ dark: ThemeColors;
23
+ }
24
+ export interface GenerateThemeOptions {
25
+ /**
26
+ * Explicit categorical palette (hex or any CSS color string), used as-is
27
+ * for both light and dark mode. Set this when you already have brand
28
+ * colors — it skips generation entirely.
29
+ */
30
+ categories?: string[];
31
+ /**
32
+ * Named category -> color, e.g. `{ application: '#5F8D6B', external: '#3F8A8C' }`.
33
+ * Takes precedence over `categories`/generation for any category listed here;
34
+ * categories not listed still fall back to the generated/explicit palette.
35
+ */
36
+ categoryColors?: Record<string, string>;
37
+ /** Starting hue in degrees (0-360) for a generated palette. Default 250 (blue). */
38
+ seedHue?: number;
39
+ /** Degrees between each generated category's hue. Default 47 — avoids evenly-divided wheels landing two categories on near-identical hues. */
40
+ hueStep?: number;
41
+ /** How many categories to pre-generate when the graph's category list isn't known yet. Default 8. */
42
+ categoryCount?: number;
43
+ /** OKLCH chroma (colorfulness) for generated categories, roughly 0-0.4. Default 0.11 — restrained, not neon. */
44
+ chroma?: number;
45
+ /** Tints background/surface/border/edge with `seedHue` at very low chroma instead of pure gray, for a cohesive brand-matched neutral scale. Default false. */
46
+ tintNeutrals?: boolean;
47
+ }
48
+ /**
49
+ * Generates a full light/dark theme from a handful of parameters instead of
50
+ * hand-picked hex codes — pass an explicit `categories` palette if you have
51
+ * brand colors, or let it derive an evenly-spaced OKLCH palette from a seed
52
+ * hue. OKLCH (rather than HSL) keeps generated colors at consistent
53
+ * perceived lightness/vividness across hues, so e.g. yellow and blue
54
+ * categories read as equally prominent instead of the yellow blowing out.
55
+ */
56
+ export declare function generateTheme(options?: GenerateThemeOptions): DiagramTheme;
57
+ export declare const defaultTheme: DiagramTheme;
58
+ /** Appends an alpha channel to a hex color, e.g. `withAlpha('#3B6EA5', 0.5)` -> `#3B6EA580`. */
59
+ export declare function withAlpha(hex: string, alpha: number): string;
60
+ /**
61
+ * Resolves a category string to a color for the given mode: an explicit
62
+ * `categoryColors[category]` entry wins if present, otherwise the category
63
+ * is hashed onto a slot in the fallback `categories` palette so unregistered
64
+ * categories still get a stable (if arbitrary) color instead of going
65
+ * unstyled.
66
+ */
67
+ export declare function categoryColor(category: string | undefined, theme: DiagramTheme, mode?: 'light' | 'dark'): string;
68
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,qLAAqL;IACrL,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,wKAAwK;IACxK,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,CAAA;IAClB,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8IAA8I;IAC9I,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gHAAgH;IAChH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8JAA8J;IAC9J,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AA4CD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,YAAY,CAa9E;AAED,eAAO,MAAM,YAAY,EAAE,YAA8B,CAAA;AAEzD,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK5D;AAWD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,YAAY,EACnB,IAAI,GAAE,OAAO,GAAG,MAAgB,GAC/B,MAAM,CAOR"}
package/dist/theme.js ADDED
@@ -0,0 +1,94 @@
1
+ import { formatHex } from 'culori';
2
+ function oklchHex(l, c, h) {
3
+ return formatHex({ mode: 'oklch', l, c, h });
4
+ }
5
+ function generateCategories(options, mode) {
6
+ if (options.categories)
7
+ return options.categories;
8
+ const seedHue = options.seedHue ?? 250;
9
+ const hueStep = options.hueStep ?? 47;
10
+ const count = options.categoryCount ?? 8;
11
+ const chroma = options.chroma ?? 0.11;
12
+ const lightness = mode === 'light' ? 0.52 : 0.78;
13
+ return Array.from({ length: count }, (_, i) => oklchHex(lightness, chroma, (seedHue + i * hueStep) % 360));
14
+ }
15
+ function neutralScale(options, mode) {
16
+ const hue = options.seedHue ?? 250;
17
+ const c = options.tintNeutrals ? 0.02 : 0;
18
+ if (mode === 'light') {
19
+ return {
20
+ background: oklchHex(0.995, c, hue),
21
+ surface: oklchHex(0.97, c, hue),
22
+ border: oklchHex(0.87, c, hue),
23
+ text: oklchHex(0.22, c * 1.5, hue),
24
+ textMuted: oklchHex(0.48, c, hue),
25
+ edge: oklchHex(0.68, c, hue),
26
+ edgeProjected: oklchHex(0.83, c, hue),
27
+ };
28
+ }
29
+ return {
30
+ background: oklchHex(0.14, c, hue),
31
+ surface: oklchHex(0.19, c, hue),
32
+ border: oklchHex(0.28, c, hue),
33
+ text: oklchHex(0.92, c, hue),
34
+ textMuted: oklchHex(0.66, c, hue),
35
+ edge: oklchHex(0.42, c, hue),
36
+ edgeProjected: oklchHex(0.28, c, hue),
37
+ };
38
+ }
39
+ /**
40
+ * Generates a full light/dark theme from a handful of parameters instead of
41
+ * hand-picked hex codes — pass an explicit `categories` palette if you have
42
+ * brand colors, or let it derive an evenly-spaced OKLCH palette from a seed
43
+ * hue. OKLCH (rather than HSL) keeps generated colors at consistent
44
+ * perceived lightness/vividness across hues, so e.g. yellow and blue
45
+ * categories read as equally prominent instead of the yellow blowing out.
46
+ */
47
+ export function generateTheme(options = {}) {
48
+ return {
49
+ light: {
50
+ ...neutralScale(options, 'light'),
51
+ categories: generateCategories(options, 'light'),
52
+ categoryColors: options.categoryColors,
53
+ },
54
+ dark: {
55
+ ...neutralScale(options, 'dark'),
56
+ categories: generateCategories(options, 'dark'),
57
+ categoryColors: options.categoryColors,
58
+ },
59
+ };
60
+ }
61
+ export const defaultTheme = generateTheme();
62
+ /** Appends an alpha channel to a hex color, e.g. `withAlpha('#3B6EA5', 0.5)` -> `#3B6EA580`. */
63
+ export function withAlpha(hex, alpha) {
64
+ const alphaHex = Math.round(alpha * 255)
65
+ .toString(16)
66
+ .padStart(2, '0');
67
+ return `${hex}${alphaHex}`;
68
+ }
69
+ function hashString(value) {
70
+ let hash = 0;
71
+ for (let i = 0; i < value.length; i++) {
72
+ hash = (hash << 5) - hash + value.charCodeAt(i);
73
+ hash |= 0;
74
+ }
75
+ return Math.abs(hash);
76
+ }
77
+ /**
78
+ * Resolves a category string to a color for the given mode: an explicit
79
+ * `categoryColors[category]` entry wins if present, otherwise the category
80
+ * is hashed onto a slot in the fallback `categories` palette so unregistered
81
+ * categories still get a stable (if arbitrary) color instead of going
82
+ * unstyled.
83
+ */
84
+ export function categoryColor(category, theme, mode = 'light') {
85
+ const colors = theme[mode];
86
+ if (!category)
87
+ return colors.textMuted;
88
+ const named = colors.categoryColors?.[category];
89
+ if (named)
90
+ return named;
91
+ const index = hashString(category) % colors.categories.length;
92
+ return colors.categories[index];
93
+ }
94
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@steffolino/diagram-kit-core",
3
+ "version": "0.1.0",
4
+ "description": "Normalized graph model, auto-layout, and theme tokens shared by every diagram-kit renderer and adapter.",
5
+ "license": "MIT",
6
+ "author": "Stefan Stretz",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/steffolino/diagram-kit.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://github.com/steffolino/diagram-kit/tree/master/packages/core#readme",
13
+ "bugs": "https://github.com/steffolino/diagram-kit/issues",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "keywords": [
18
+ "diagram",
19
+ "graph",
20
+ "layout",
21
+ "dagre",
22
+ "visualization"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "dependencies": {
39
+ "@dagrejs/dagre": "^1.1.4",
40
+ "culori": "^4.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/culori": "^4.0.1",
44
+ "typescript": "^5.7.2"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json",
48
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit"
50
+ }
51
+ }