@rokkit/graph 1.6.0 → 1.7.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 (77) hide show
  1. package/README.md +59 -2
  2. package/dist/Graph.svelte +121 -72
  3. package/dist/GraphLegend.svelte +112 -0
  4. package/dist/GraphLegend.svelte.d.ts +33 -0
  5. package/dist/GraphState.svelte.d.ts +107 -15
  6. package/dist/GraphState.svelte.js +186 -3
  7. package/dist/controls/BundleControl.svelte +30 -0
  8. package/dist/controls/BundleControl.svelte.d.ts +16 -0
  9. package/dist/controls/DensityControl.svelte +39 -0
  10. package/dist/controls/DensityControl.svelte.d.ts +16 -0
  11. package/dist/controls/DepthControl.svelte +34 -0
  12. package/dist/controls/DepthControl.svelte.d.ts +17 -0
  13. package/dist/controls/EdgeStyleControl.svelte +31 -0
  14. package/dist/controls/EdgeStyleControl.svelte.d.ts +14 -0
  15. package/dist/controls/ZoomControl.svelte +47 -0
  16. package/dist/controls/ZoomControl.svelte.d.ts +7 -0
  17. package/dist/controls/zoom.d.ts +6 -0
  18. package/dist/controls/zoom.js +12 -0
  19. package/dist/diagrams/CallTree.svelte +135 -0
  20. package/dist/diagrams/CallTree.svelte.d.ts +37 -0
  21. package/dist/diagrams/DependencyDiagram.svelte +124 -0
  22. package/dist/diagrams/DependencyDiagram.svelte.d.ts +30 -0
  23. package/dist/diagrams/DiagramFrame.svelte +36 -0
  24. package/dist/diagrams/DiagramFrame.svelte.d.ts +20 -0
  25. package/dist/diagrams/ErDiagram.svelte +128 -0
  26. package/dist/diagrams/ErDiagram.svelte.d.ts +33 -0
  27. package/dist/diagrams/Neighborhood.svelte +127 -0
  28. package/dist/diagrams/Neighborhood.svelte.d.ts +33 -0
  29. package/dist/diagrams/StructureDiagram.svelte +131 -0
  30. package/dist/diagrams/StructureDiagram.svelte.d.ts +31 -0
  31. package/dist/diagrams/Sunburst.svelte +124 -0
  32. package/dist/diagrams/Sunburst.svelte.d.ts +32 -0
  33. package/dist/diagrams/Treemap.svelte +124 -0
  34. package/dist/diagrams/Treemap.svelte.d.ts +32 -0
  35. package/dist/icons.d.ts +5 -0
  36. package/dist/icons.js +5 -0
  37. package/dist/index.d.ts +18 -1
  38. package/dist/index.js +23 -1
  39. package/dist/layout/arc.d.ts +13 -0
  40. package/dist/layout/arc.js +58 -0
  41. package/dist/layout/bundle.d.ts +30 -0
  42. package/dist/layout/bundle.js +80 -0
  43. package/dist/layout/cluster.js +2 -0
  44. package/dist/layout/flow.d.ts +8 -0
  45. package/dist/layout/flow.js +141 -0
  46. package/dist/layout/hierarchy.d.ts +42 -0
  47. package/dist/layout/hierarchy.js +175 -0
  48. package/dist/layout/index.d.ts +7 -1
  49. package/dist/layout/index.js +8 -2
  50. package/dist/layout/neighborhood.js +164 -28
  51. package/dist/layout/options.d.ts +44 -0
  52. package/dist/layout/options.js +106 -0
  53. package/dist/layout/order.d.ts +11 -0
  54. package/dist/layout/order.js +153 -0
  55. package/dist/layout/points.js +27 -35
  56. package/dist/layout/radial.d.ts +16 -0
  57. package/dist/layout/radial.js +214 -0
  58. package/dist/layout/rank.d.ts +15 -0
  59. package/dist/layout/rank.js +105 -0
  60. package/dist/layout/rings.d.ts +20 -0
  61. package/dist/layout/rings.js +92 -0
  62. package/dist/layout/sizing.d.ts +37 -0
  63. package/dist/layout/sizing.js +79 -0
  64. package/dist/layout/squarify.d.ts +14 -0
  65. package/dist/layout/squarify.js +114 -0
  66. package/dist/layout/structure.d.ts +9 -0
  67. package/dist/layout/structure.js +367 -0
  68. package/dist/layout/sunburst.d.ts +8 -0
  69. package/dist/layout/sunburst.js +126 -0
  70. package/dist/layout/types.d.ts +141 -0
  71. package/dist/layout/world.d.ts +9 -0
  72. package/dist/layout/world.js +144 -0
  73. package/dist/model/normalize.js +47 -1
  74. package/dist/model/tree.d.ts +27 -0
  75. package/dist/model/tree.js +182 -0
  76. package/dist/types.d.ts +55 -3
  77. package/package.json +9 -4
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A call graph as a tidy tree or dendrogram around a circle.
4
+ *
5
+ * Angle separates the subtrees, radius carries the depth — which is the question a call
6
+ * graph asks. Packing it into boxes instead answers "how much is there" and, at any real
7
+ * size, reads as a stack of colliding labels.
8
+ *
9
+ * `levels` is the control that makes it legible over a codebase: a dendrogram of a whole
10
+ * repo puts every leaf on one rim, each a fraction of a degree wide. Show the crates, then
11
+ * set `root` to drill into one — a dendrogram of dendrograms.
12
+ */
13
+ import Graph from '../Graph.svelte'
14
+ import GraphLegend from '../GraphLegend.svelte'
15
+ import DiagramFrame from './DiagramFrame.svelte'
16
+ import DepthControl from '../controls/DepthControl.svelte'
17
+ import ZoomControl from '../controls/ZoomControl.svelte'
18
+ import { GraphState } from '../GraphState.svelte.js'
19
+ import type { GraphFields } from '../types.js'
20
+ import type { GraphPreset } from '../preset.js'
21
+
22
+ type Props = {
23
+ /**
24
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
25
+ * the caller owns it: this component reads it and never calls `update`, exactly as
26
+ * `Graph` does — two owners writing one state fight on every render.
27
+ */
28
+ state?: GraphState
29
+ nodes?: unknown[]
30
+ edges?: unknown[]
31
+ fields?: GraphFields
32
+ /** `tree` reads radius as depth; `dendrogram` pins every leaf to the rim. */
33
+ radialMode?: 'tree' | 'dendrogram'
34
+ /** How many levels out from the root to draw. Unset draws the whole tree. */
35
+ levels?: number
36
+ /** The node to centre on — drilling. Unset roots at whatever nothing calls. */
37
+ root?: string | null
38
+ /** What a node's area encodes. `degree` counts edges. */
39
+ sizeBy?: string
40
+ sizeScale?: 'linear' | 'log'
41
+ controls?: boolean
42
+ /** Deepest level the depth control offers. */
43
+ maxLevels?: number
44
+ legend?: boolean
45
+ value?: string | null
46
+ preset?: GraphPreset
47
+ mode?: 'light' | 'dark'
48
+ arrows?: boolean
49
+ label?: string
50
+ onselect?: (id: string | null) => void
51
+ class?: string
52
+ }
53
+
54
+ let {
55
+ state: provided,
56
+ nodes = [],
57
+ edges = [],
58
+ fields = {},
59
+ radialMode = 'tree',
60
+ levels = $bindable(undefined),
61
+ root = $bindable(null),
62
+ sizeBy = 'degree',
63
+ sizeScale = 'linear',
64
+ controls = false,
65
+ maxLevels = 4,
66
+ legend = false,
67
+ value = $bindable(undefined),
68
+ preset = undefined,
69
+ mode = 'light',
70
+ arrows = true,
71
+ label = undefined,
72
+ onselect = undefined,
73
+ class: className = ''
74
+ }: Props = $props()
75
+
76
+ let zoom = $state(1)
77
+
78
+ const config = () => ({
79
+ nodes,
80
+ edges,
81
+ fields,
82
+ layout: 'radial',
83
+ radialMode,
84
+ levels,
85
+ root,
86
+ sizeBy,
87
+ sizeScale,
88
+ value,
89
+ preset,
90
+ mode,
91
+ label,
92
+ onselect
93
+ })
94
+
95
+ // svelte-ignore state_referenced_locally
96
+ const graph = provided ?? new GraphState(config())
97
+
98
+ $effect(() => {
99
+ if (!provided) {
100
+ graph.update(config())
101
+
102
+ return
103
+ }
104
+
105
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
106
+ // and are never reset here. What this component DOES own is the options its own
107
+ // controls drive — the layout above all, since choosing this component is choosing it.
108
+ // `apply` merges; `update` would revert everything it does not name.
109
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
110
+ config()
111
+ graph.apply(mine)
112
+ })
113
+ </script>
114
+
115
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
116
+ inside is always defined, so the frame would render an empty control bar over
117
+ every diagram that asked for none. -->
118
+ {#snippet controlBar()}
119
+ <DepthControl levels={levels ?? maxLevels} max={maxLevels} onchange={(v) => (levels = v)} />
120
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
121
+ {/snippet}
122
+
123
+ {#snippet legendBar()}
124
+ <GraphLegend state={graph} kinds />
125
+ {/snippet}
126
+
127
+ <DiagramFrame
128
+ class={className}
129
+ overlay={controls ? controlBar : undefined}
130
+ footer={legend ? legendBar : undefined}
131
+ >
132
+ {#snippet canvas()}
133
+ <Graph state={graph} bind:zoom {arrows} />
134
+ {/snippet}
135
+ </DiagramFrame>
@@ -0,0 +1,37 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { GraphFields } from '../types.js';
3
+ import type { GraphPreset } from '../preset.js';
4
+ type Props = {
5
+ /**
6
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
7
+ * the caller owns it: this component reads it and never calls `update`, exactly as
8
+ * `Graph` does — two owners writing one state fight on every render.
9
+ */
10
+ state?: GraphState;
11
+ nodes?: unknown[];
12
+ edges?: unknown[];
13
+ fields?: GraphFields;
14
+ /** `tree` reads radius as depth; `dendrogram` pins every leaf to the rim. */
15
+ radialMode?: 'tree' | 'dendrogram';
16
+ /** How many levels out from the root to draw. Unset draws the whole tree. */
17
+ levels?: number;
18
+ /** The node to centre on — drilling. Unset roots at whatever nothing calls. */
19
+ root?: string | null;
20
+ /** What a node's area encodes. `degree` counts edges. */
21
+ sizeBy?: string;
22
+ sizeScale?: 'linear' | 'log';
23
+ controls?: boolean;
24
+ /** Deepest level the depth control offers. */
25
+ maxLevels?: number;
26
+ legend?: boolean;
27
+ value?: string | null;
28
+ preset?: GraphPreset;
29
+ mode?: 'light' | 'dark';
30
+ arrows?: boolean;
31
+ label?: string;
32
+ onselect?: (id: string | null) => void;
33
+ class?: string;
34
+ };
35
+ declare const CallTree: import("svelte").Component<Props, {}, "value" | "levels" | "root">;
36
+ type CallTree = ReturnType<typeof CallTree>;
37
+ export default CallTree;
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ /**
3
+ * What reads, writes, calls and contains what — the other half of a schema.
4
+ *
5
+ * dbd's v2 model splits `tables`/`refs` from `entities`/`deps` because an ER renderer
6
+ * wants one and a call-graph renderer wants the other. Pass
7
+ * `toGraphInput(model, 'dependencies')` and this draws the second.
8
+ *
9
+ * Keyed by RELATION rather than by kind: on this canvas the verb is the information, and
10
+ * `reads` versus `writes` is the distinction a reader is here for.
11
+ */
12
+ import Graph from '../Graph.svelte'
13
+ import GraphLegend from '../GraphLegend.svelte'
14
+ import DiagramFrame from './DiagramFrame.svelte'
15
+ import DensityControl from '../controls/DensityControl.svelte'
16
+ import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
17
+ import ZoomControl from '../controls/ZoomControl.svelte'
18
+ import { GraphState } from '../GraphState.svelte.js'
19
+ import type { Density, EdgeStyle } from '../layout/types.js'
20
+ import type { GraphFields } from '../types.js'
21
+ import type { GraphPreset } from '../preset.js'
22
+
23
+ type Props = {
24
+ /**
25
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
26
+ * the caller owns it: this component reads it and never calls `update`, exactly as
27
+ * `Graph` does — two owners writing one state fight on every render.
28
+ */
29
+ state?: GraphState
30
+ nodes?: unknown[]
31
+ edges?: unknown[]
32
+ fields?: GraphFields
33
+ density?: Density
34
+ edgeStyle?: EdgeStyle
35
+ groupTint?: boolean
36
+ controls?: boolean
37
+ legend?: boolean
38
+ value?: string | null
39
+ preset?: GraphPreset
40
+ mode?: 'light' | 'dark'
41
+ arrows?: boolean
42
+ label?: string
43
+ onselect?: (id: string | null) => void
44
+ class?: string
45
+ }
46
+
47
+ let {
48
+ state: provided,
49
+ nodes = [],
50
+ edges = [],
51
+ fields = {},
52
+ density = $bindable('names'),
53
+ edgeStyle = $bindable('curved'),
54
+ groupTint = true,
55
+ controls = false,
56
+ legend = false,
57
+ value = $bindable(undefined),
58
+ preset = undefined,
59
+ mode = 'light',
60
+ arrows = true,
61
+ label = undefined,
62
+ onselect = undefined,
63
+ class: className = ''
64
+ }: Props = $props()
65
+
66
+ let zoom = $state(1)
67
+
68
+ const config = () => ({
69
+ nodes,
70
+ edges,
71
+ fields,
72
+ layout: 'flow',
73
+ density,
74
+ edgeStyle,
75
+ groupTint,
76
+ value,
77
+ preset,
78
+ mode,
79
+ label,
80
+ onselect
81
+ })
82
+
83
+ // svelte-ignore state_referenced_locally
84
+ const graph = provided ?? new GraphState(config())
85
+
86
+ $effect(() => {
87
+ if (!provided) {
88
+ graph.update(config())
89
+
90
+ return
91
+ }
92
+
93
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
94
+ // and are never reset here. What this component DOES own is the options its own
95
+ // controls drive — the layout above all, since choosing this component is choosing it.
96
+ // `apply` merges; `update` would revert everything it does not name.
97
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
98
+ config()
99
+ graph.apply(mine)
100
+ })
101
+ </script>
102
+
103
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
104
+ inside is always defined, so the frame would render an empty control bar over
105
+ every diagram that asked for none. -->
106
+ {#snippet controlBar()}
107
+ <DensityControl {density} onchange={(v) => (density = v)} />
108
+ <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
109
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
110
+ {/snippet}
111
+
112
+ {#snippet legendBar()}
113
+ <GraphLegend state={graph} relations kinds />
114
+ {/snippet}
115
+
116
+ <DiagramFrame
117
+ class={className}
118
+ overlay={controls ? controlBar : undefined}
119
+ footer={legend ? legendBar : undefined}
120
+ >
121
+ {#snippet canvas()}
122
+ <Graph state={graph} bind:zoom {arrows} />
123
+ {/snippet}
124
+ </DiagramFrame>
@@ -0,0 +1,30 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { Density, EdgeStyle } from '../layout/types.js';
3
+ import type { GraphFields } from '../types.js';
4
+ import type { GraphPreset } from '../preset.js';
5
+ type Props = {
6
+ /**
7
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
8
+ * the caller owns it: this component reads it and never calls `update`, exactly as
9
+ * `Graph` does — two owners writing one state fight on every render.
10
+ */
11
+ state?: GraphState;
12
+ nodes?: unknown[];
13
+ edges?: unknown[];
14
+ fields?: GraphFields;
15
+ density?: Density;
16
+ edgeStyle?: EdgeStyle;
17
+ groupTint?: boolean;
18
+ controls?: boolean;
19
+ legend?: boolean;
20
+ value?: string | null;
21
+ preset?: GraphPreset;
22
+ mode?: 'light' | 'dark';
23
+ arrows?: boolean;
24
+ label?: string;
25
+ onselect?: (id: string | null) => void;
26
+ class?: string;
27
+ };
28
+ declare const DependencyDiagram: import("svelte").Component<Props, {}, "value" | "density" | "edgeStyle">;
29
+ type DependencyDiagram = ReturnType<typeof DependencyDiagram>;
30
+ export default DependencyDiagram;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The shell every named diagram shares: a canvas, controls over it, a legend under it.
4
+ *
5
+ * Internal on purpose. A consumer composes by picking a diagram — `ErDiagram`, `CallTree`
6
+ * — not by assembling a frame, exactly as they pick `BarChart` rather than assembling a
7
+ * `Plot` with a geom. This exists so the seven diagrams do not each re-declare the same
8
+ * positioned box.
9
+ */
10
+ import type { Snippet } from 'svelte'
11
+
12
+ type Props = {
13
+ canvas: Snippet
14
+ /** Controls drawn OVER the canvas, anchored to its corners. */
15
+ overlay?: Snippet
16
+ /** A legend or caption under the canvas, in the layout flow rather than over it. */
17
+ footer?: Snippet
18
+ class?: string
19
+ }
20
+
21
+ let { canvas, overlay, footer, class: className = '' }: Props = $props()
22
+ </script>
23
+
24
+ <div data-graph-diagram class={className}>
25
+ <!-- The canvas fills what is left after the footer, so a legend never overlaps the
26
+ picture and never gets clipped by it. -->
27
+ <div data-graph-diagram-canvas>
28
+ {@render canvas()}
29
+ {#if overlay}
30
+ <div data-graph-diagram-overlay>{@render overlay()}</div>
31
+ {/if}
32
+ </div>
33
+ {#if footer}
34
+ <div data-graph-diagram-footer>{@render footer()}</div>
35
+ {/if}
36
+ </div>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The shell every named diagram shares: a canvas, controls over it, a legend under it.
3
+ *
4
+ * Internal on purpose. A consumer composes by picking a diagram — `ErDiagram`, `CallTree`
5
+ * — not by assembling a frame, exactly as they pick `BarChart` rather than assembling a
6
+ * `Plot` with a geom. This exists so the seven diagrams do not each re-declare the same
7
+ * positioned box.
8
+ */
9
+ import type { Snippet } from 'svelte';
10
+ type Props = {
11
+ canvas: Snippet;
12
+ /** Controls drawn OVER the canvas, anchored to its corners. */
13
+ overlay?: Snippet;
14
+ /** A legend or caption under the canvas, in the layout flow rather than over it. */
15
+ footer?: Snippet;
16
+ class?: string;
17
+ };
18
+ declare const DiagramFrame: import("svelte").Component<Props, {}, "">;
19
+ type DiagramFrame = ReturnType<typeof DiagramFrame>;
20
+ export default DiagramFrame;
@@ -0,0 +1,128 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Table entities and their foreign keys, ranked so direction reads off the geometry.
4
+ *
5
+ * An ER diagram is table entities and their relationships — a view is a derived projection
6
+ * and a routine is behaviour, so neither belongs here. Pass `toGraphInput(model, 'er')`
7
+ * from `@rokkit/graph/schema` and the split is made for you.
8
+ *
9
+ * `flow` gives up group boxes to rank by reference direction, so `groupTint` is on by
10
+ * default: without it the schema a table belongs to becomes invisible, which is the one
11
+ * thing the older cluster arrangement did show.
12
+ */
13
+ import Graph from '../Graph.svelte'
14
+ import GraphLegend from '../GraphLegend.svelte'
15
+ import DiagramFrame from './DiagramFrame.svelte'
16
+ import DensityControl from '../controls/DensityControl.svelte'
17
+ import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
18
+ import ZoomControl from '../controls/ZoomControl.svelte'
19
+ import { GraphState } from '../GraphState.svelte.js'
20
+ import type { Density, EdgeStyle } from '../layout/types.js'
21
+ import type { GraphFields } from '../types.js'
22
+ import type { GraphPreset } from '../preset.js'
23
+
24
+ type Props = {
25
+ /**
26
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
27
+ * the caller owns it: this component reads it and never calls `update`, exactly as
28
+ * `Graph` does — two owners writing one state fight on every render.
29
+ */
30
+ state?: GraphState
31
+ nodes?: unknown[]
32
+ edges?: unknown[]
33
+ fields?: GraphFields
34
+ density?: Density
35
+ edgeStyle?: EdgeStyle
36
+ /** Paint each card with its schema colour. On by default — `flow` has no group boxes. */
37
+ groupTint?: boolean
38
+ /** Show the on-canvas density, edge-style and zoom controls. */
39
+ controls?: boolean
40
+ /** Show a key under the canvas. Keyed by GROUP, which is what the tint encodes. */
41
+ legend?: boolean
42
+ value?: string | null
43
+ preset?: GraphPreset
44
+ mode?: 'light' | 'dark'
45
+ arrows?: boolean
46
+ label?: string
47
+ onselect?: (id: string | null) => void
48
+ class?: string
49
+ }
50
+
51
+ let {
52
+ state: provided,
53
+ nodes = [],
54
+ edges = [],
55
+ fields = {},
56
+ density = $bindable('keys'),
57
+ edgeStyle = $bindable('curved'),
58
+ groupTint = true,
59
+ controls = false,
60
+ legend = false,
61
+ value = $bindable(undefined),
62
+ preset = undefined,
63
+ mode = 'light',
64
+ arrows = true,
65
+ label = undefined,
66
+ onselect = undefined,
67
+ class: className = ''
68
+ }: Props = $props()
69
+
70
+ let zoom = $state(1)
71
+
72
+ const config = () => ({
73
+ nodes,
74
+ edges,
75
+ fields,
76
+ layout: 'flow',
77
+ density,
78
+ edgeStyle,
79
+ groupTint,
80
+ value,
81
+ preset,
82
+ mode,
83
+ label,
84
+ onselect
85
+ })
86
+
87
+ // svelte-ignore state_referenced_locally
88
+ const graph = provided ?? new GraphState(config())
89
+
90
+ $effect(() => {
91
+ if (!provided) {
92
+ graph.update(config())
93
+
94
+ return
95
+ }
96
+
97
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
98
+ // and are never reset here. What this component DOES own is the options its own
99
+ // controls drive — the layout above all, since choosing this component is choosing it.
100
+ // `apply` merges; `update` would revert everything it does not name.
101
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
102
+ config()
103
+ graph.apply(mine)
104
+ })
105
+ </script>
106
+
107
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
108
+ inside is always defined, so the frame would render an empty control bar over
109
+ every diagram that asked for none. -->
110
+ {#snippet controlBar()}
111
+ <DensityControl {density} onchange={(v) => (density = v)} />
112
+ <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
113
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
114
+ {/snippet}
115
+
116
+ {#snippet legendBar()}
117
+ <GraphLegend state={graph} groups />
118
+ {/snippet}
119
+
120
+ <DiagramFrame
121
+ class={className}
122
+ overlay={controls ? controlBar : undefined}
123
+ footer={legend ? legendBar : undefined}
124
+ >
125
+ {#snippet canvas()}
126
+ <Graph state={graph} bind:zoom {arrows} />
127
+ {/snippet}
128
+ </DiagramFrame>
@@ -0,0 +1,33 @@
1
+ import { GraphState } from '../GraphState.svelte.js';
2
+ import type { Density, EdgeStyle } from '../layout/types.js';
3
+ import type { GraphFields } from '../types.js';
4
+ import type { GraphPreset } from '../preset.js';
5
+ type Props = {
6
+ /**
7
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
8
+ * the caller owns it: this component reads it and never calls `update`, exactly as
9
+ * `Graph` does — two owners writing one state fight on every render.
10
+ */
11
+ state?: GraphState;
12
+ nodes?: unknown[];
13
+ edges?: unknown[];
14
+ fields?: GraphFields;
15
+ density?: Density;
16
+ edgeStyle?: EdgeStyle;
17
+ /** Paint each card with its schema colour. On by default — `flow` has no group boxes. */
18
+ groupTint?: boolean;
19
+ /** Show the on-canvas density, edge-style and zoom controls. */
20
+ controls?: boolean;
21
+ /** Show a key under the canvas. Keyed by GROUP, which is what the tint encodes. */
22
+ legend?: boolean;
23
+ value?: string | null;
24
+ preset?: GraphPreset;
25
+ mode?: 'light' | 'dark';
26
+ arrows?: boolean;
27
+ label?: string;
28
+ onselect?: (id: string | null) => void;
29
+ class?: string;
30
+ };
31
+ declare const ErDiagram: import("svelte").Component<Props, {}, "value" | "density" | "edgeStyle">;
32
+ type ErDiagram = ReturnType<typeof ErDiagram>;
33
+ export default ErDiagram;
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ /**
3
+ * One node and what reaches it, walked a hop at a time.
4
+ *
5
+ * The answer to "what touches this" — and at `depth: 2`, to "what does changing this
6
+ * reach", which is the question a reader has before editing something. Cards are built at
7
+ * full detail unconditionally: a portrait of one node's surroundings that hides the
8
+ * columns its edges land on defeats its own purpose, which is why there is no density
9
+ * control here.
10
+ */
11
+ import Graph from '../Graph.svelte'
12
+ import GraphLegend from '../GraphLegend.svelte'
13
+ import DiagramFrame from './DiagramFrame.svelte'
14
+ import DepthControl from '../controls/DepthControl.svelte'
15
+ import EdgeStyleControl from '../controls/EdgeStyleControl.svelte'
16
+ import ZoomControl from '../controls/ZoomControl.svelte'
17
+ import { GraphState } from '../GraphState.svelte.js'
18
+ import type { EdgeStyle } from '../layout/types.js'
19
+ import type { GraphFields } from '../types.js'
20
+ import type { GraphPreset } from '../preset.js'
21
+
22
+ type Props = {
23
+ /**
24
+ * Share one state across this diagram and, say, an entity table beside it. Given one,
25
+ * the caller owns it: this component reads it and never calls `update`, exactly as
26
+ * `Graph` does — two owners writing one state fight on every render.
27
+ */
28
+ state?: GraphState
29
+ nodes?: unknown[]
30
+ edges?: unknown[]
31
+ fields?: GraphFields
32
+ /** The node to centre. Defaults to the selection. */
33
+ focus?: string | null
34
+ /** How many hops out. One answers "what touches this"; two, "what does this reach". */
35
+ depth?: number
36
+ edgeStyle?: EdgeStyle
37
+ controls?: boolean
38
+ maxDepth?: number
39
+ legend?: boolean
40
+ value?: string | null
41
+ preset?: GraphPreset
42
+ mode?: 'light' | 'dark'
43
+ arrows?: boolean
44
+ label?: string
45
+ onselect?: (id: string | null) => void
46
+ class?: string
47
+ }
48
+
49
+ let {
50
+ state: provided,
51
+ nodes = [],
52
+ edges = [],
53
+ fields = {},
54
+ focus = $bindable(null),
55
+ depth = $bindable(1),
56
+ edgeStyle = $bindable('curved'),
57
+ controls = false,
58
+ maxDepth = 3,
59
+ legend = false,
60
+ value = $bindable(undefined),
61
+ preset = undefined,
62
+ mode = 'light',
63
+ arrows = true,
64
+ label = undefined,
65
+ onselect = undefined,
66
+ class: className = ''
67
+ }: Props = $props()
68
+
69
+ let zoom = $state(1)
70
+
71
+ const config = () => ({
72
+ nodes,
73
+ edges,
74
+ fields,
75
+ layout: 'neighborhood',
76
+ focus,
77
+ depth,
78
+ edgeStyle,
79
+ value,
80
+ preset,
81
+ mode,
82
+ label,
83
+ onselect
84
+ })
85
+
86
+ // svelte-ignore state_referenced_locally
87
+ const graph = provided ?? new GraphState(config())
88
+
89
+ $effect(() => {
90
+ if (!provided) {
91
+ graph.update(config())
92
+
93
+ return
94
+ }
95
+
96
+ // A supplied state is the caller's: their data, preset, mode and selection stay theirs
97
+ // and are never reset here. What this component DOES own is the options its own
98
+ // controls drive — the layout above all, since choosing this component is choosing it.
99
+ // `apply` merges; `update` would revert everything it does not name.
100
+ const { nodes: _n, edges: _e, fields: _f, preset: _p, mode: _m, value: _v, ...mine } =
101
+ config()
102
+ graph.apply(mine)
103
+ })
104
+ </script>
105
+
106
+ <!-- Snippets are declared OUTSIDE the frame and passed conditionally: one declared
107
+ inside is always defined, so the frame would render an empty control bar over
108
+ every diagram that asked for none. -->
109
+ {#snippet controlBar()}
110
+ <DepthControl levels={depth} max={maxDepth} onchange={(v) => (depth = v)} />
111
+ <EdgeStyleControl {edgeStyle} onchange={(v) => (edgeStyle = v)} />
112
+ <ZoomControl {zoom} onchange={(v) => (zoom = v)} />
113
+ {/snippet}
114
+
115
+ {#snippet legendBar()}
116
+ <GraphLegend state={graph} kinds relations />
117
+ {/snippet}
118
+
119
+ <DiagramFrame
120
+ class={className}
121
+ overlay={controls ? controlBar : undefined}
122
+ footer={legend ? legendBar : undefined}
123
+ >
124
+ {#snippet canvas()}
125
+ <Graph state={graph} bind:zoom {arrows} />
126
+ {/snippet}
127
+ </DiagramFrame>