@rokkit/graph 1.7.0 → 1.8.1

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 (120) hide show
  1. package/README.md +67 -1
  2. package/dist/Graph.svelte +234 -259
  3. package/dist/Graph.svelte.d.ts +1 -1
  4. package/dist/GraphLegend.svelte +81 -10
  5. package/dist/GraphLegend.svelte.d.ts +15 -0
  6. package/dist/GraphState.svelte.d.ts +168 -51
  7. package/dist/GraphState.svelte.js +406 -305
  8. package/dist/actions/canvas.d.ts +18 -0
  9. package/dist/actions/canvas.js +59 -0
  10. package/dist/actions/choices.d.ts +12 -0
  11. package/dist/actions/choices.js +23 -0
  12. package/dist/actions/interactions.d.ts +20 -0
  13. package/dist/actions/interactions.js +57 -0
  14. package/dist/actions/listen.d.ts +2 -0
  15. package/dist/actions/listen.js +10 -0
  16. package/dist/canvas/frame.d.ts +41 -0
  17. package/dist/canvas/frame.js +34 -0
  18. package/dist/controls/BundleControl.svelte +11 -10
  19. package/dist/controls/BundleControl.svelte.d.ts +0 -7
  20. package/dist/controls/DensityControl.svelte +17 -9
  21. package/dist/controls/DensityControl.svelte.d.ts +0 -8
  22. package/dist/controls/DepthControl.svelte +11 -4
  23. package/dist/controls/DepthControl.svelte.d.ts +0 -9
  24. package/dist/controls/DrillBar.svelte +63 -0
  25. package/dist/controls/DrillBar.svelte.d.ts +9 -0
  26. package/dist/controls/EdgeStyleControl.svelte +10 -10
  27. package/dist/controls/EdgeStyleControl.svelte.d.ts +0 -6
  28. package/dist/controls/HiddenControl.svelte +27 -0
  29. package/dist/controls/HiddenControl.svelte.d.ts +7 -0
  30. package/dist/controls/MeasureControl.svelte +39 -0
  31. package/dist/controls/MeasureControl.svelte.d.ts +12 -0
  32. package/dist/controls/ToggleControl.svelte +33 -0
  33. package/dist/controls/ToggleControl.svelte.d.ts +13 -0
  34. package/dist/controls/ViolationsControl.svelte +27 -0
  35. package/dist/controls/ViolationsControl.svelte.d.ts +7 -0
  36. package/dist/controls/ZoomControl.svelte +21 -18
  37. package/dist/controls/zoom.d.ts +3 -0
  38. package/dist/controls/zoom.js +6 -0
  39. package/dist/diagrams/ArcDiagram.svelte +116 -0
  40. package/dist/diagrams/ArcDiagram.svelte.d.ts +31 -0
  41. package/dist/diagrams/CallTree.svelte +17 -5
  42. package/dist/diagrams/CallTree.svelte.d.ts +5 -1
  43. package/dist/diagrams/DependencyDiagram.svelte +18 -6
  44. package/dist/diagrams/DependencyDiagram.svelte.d.ts +5 -1
  45. package/dist/diagrams/DependencyMatrix.svelte +156 -0
  46. package/dist/diagrams/DependencyMatrix.svelte.d.ts +34 -0
  47. package/dist/diagrams/ErDiagram.svelte +18 -6
  48. package/dist/diagrams/ErDiagram.svelte.d.ts +5 -1
  49. package/dist/diagrams/LayersDiagram.svelte +124 -0
  50. package/dist/diagrams/LayersDiagram.svelte.d.ts +30 -0
  51. package/dist/diagrams/Neighborhood.svelte +26 -6
  52. package/dist/diagrams/Neighborhood.svelte.d.ts +11 -1
  53. package/dist/diagrams/PolymetricTree.svelte +116 -0
  54. package/dist/diagrams/PolymetricTree.svelte.d.ts +28 -0
  55. package/dist/diagrams/StructureDiagram.svelte +38 -8
  56. package/dist/diagrams/StructureDiagram.svelte.d.ts +9 -1
  57. package/dist/diagrams/Sunburst.svelte +44 -8
  58. package/dist/diagrams/Sunburst.svelte.d.ts +12 -2
  59. package/dist/diagrams/Treemap.svelte +44 -8
  60. package/dist/diagrams/Treemap.svelte.d.ts +12 -2
  61. package/dist/index.d.ts +13 -3
  62. package/dist/index.js +11 -2
  63. package/dist/layout/arcs.d.ts +6 -0
  64. package/dist/layout/arcs.js +141 -0
  65. package/dist/layout/cards.js +9 -2
  66. package/dist/layout/edges.d.ts +13 -1
  67. package/dist/layout/edges.js +35 -0
  68. package/dist/layout/extent.d.ts +17 -0
  69. package/dist/layout/extent.js +27 -0
  70. package/dist/layout/flow.js +3 -1
  71. package/dist/layout/index.d.ts +4 -1
  72. package/dist/layout/index.js +17 -2
  73. package/dist/layout/layers.d.ts +6 -0
  74. package/dist/layout/layers.js +180 -0
  75. package/dist/layout/matrix.d.ts +50 -0
  76. package/dist/layout/matrix.js +110 -0
  77. package/dist/layout/neighborhood.js +16 -7
  78. package/dist/layout/options.d.ts +1 -1
  79. package/dist/layout/options.js +16 -3
  80. package/dist/layout/percentile.d.ts +8 -0
  81. package/dist/layout/percentile.js +13 -0
  82. package/dist/layout/polymetric.d.ts +3 -0
  83. package/dist/layout/polymetric.js +221 -0
  84. package/dist/layout/radial.js +3 -1
  85. package/dist/layout/structure.js +7 -1
  86. package/dist/layout/sunburst.js +11 -4
  87. package/dist/layout/types.d.ts +83 -0
  88. package/dist/layout/world.js +11 -4
  89. package/dist/messages.d.ts +14 -0
  90. package/dist/messages.js +17 -0
  91. package/dist/model/condense.d.ts +8 -0
  92. package/dist/model/condense.js +114 -0
  93. package/dist/model/entities.d.ts +12 -0
  94. package/dist/model/entities.js +12 -0
  95. package/dist/model/normalize.d.ts +2 -6
  96. package/dist/model/normalize.js +83 -5
  97. package/dist/model/relationships.d.ts +30 -0
  98. package/dist/model/relationships.js +51 -0
  99. package/dist/model/share.d.ts +9 -0
  100. package/dist/model/share.js +43 -0
  101. package/dist/model/tree.d.ts +7 -1
  102. package/dist/model/tree.js +11 -4
  103. package/dist/preset.d.ts +18 -0
  104. package/dist/preset.js +34 -3
  105. package/dist/schema/EntitiesView.svelte +8 -6
  106. package/dist/schema/EntityView.svelte +13 -10
  107. package/dist/state/GraphConfig.svelte.d.ts +124 -0
  108. package/dist/state/GraphConfig.svelte.js +253 -0
  109. package/dist/state/GraphDrill.svelte.d.ts +52 -0
  110. package/dist/state/GraphDrill.svelte.js +115 -0
  111. package/dist/state/GraphGroups.svelte.d.ts +35 -0
  112. package/dist/state/GraphGroups.svelte.js +73 -0
  113. package/dist/state/GraphSelection.svelte.d.ts +53 -0
  114. package/dist/state/GraphSelection.svelte.js +94 -0
  115. package/dist/state/intents.d.ts +33 -0
  116. package/dist/state/intents.js +30 -0
  117. package/dist/state/text.d.ts +28 -0
  118. package/dist/state/text.js +56 -0
  119. package/dist/types.d.ts +92 -1
  120. package/package.json +5 -4
@@ -0,0 +1,124 @@
1
+ import type { GraphPreset } from '../preset.js';
2
+ import type { Arrange, Density, EdgeStyle, LayoutFn, NodeAxis, ShowEdges } from '../layout/types.js';
3
+ import type { GraphFields } from '../types.js';
4
+ import type { GraphStateConfig } from '../GraphState.svelte.js';
5
+ export type GraphConfigValues = {
6
+ nodes: unknown[];
7
+ edges: unknown[];
8
+ fields: GraphFields;
9
+ layout: string | LayoutFn;
10
+ focus: string | null;
11
+ density: Density;
12
+ arrange: Arrange;
13
+ groupBy: NodeAxis;
14
+ nestBy: NodeAxis | null;
15
+ groupTint: boolean;
16
+ sizeBy: string;
17
+ sizeScale: 'linear' | 'log';
18
+ depth: number;
19
+ focusPath: string[];
20
+ levels: number | undefined;
21
+ root: string | null;
22
+ bundleTension: number | undefined;
23
+ radialMode: 'tree' | 'dendrogram';
24
+ edgeStyle: EdgeStyle;
25
+ preset: GraphPreset;
26
+ mode: 'light' | 'dark';
27
+ label: string | undefined;
28
+ onselect: ((id: string | null) => void) | undefined;
29
+ ondrill: GraphStateConfig['ondrill'];
30
+ ondrillup: GraphStateConfig['ondrillup'];
31
+ onfocuspath: GraphStateConfig['onfocuspath'];
32
+ onexpand: GraphStateConfig['onexpand'];
33
+ oncollapse: GraphStateConfig['oncollapse'];
34
+ showEdges: ShowEdges;
35
+ layerLabels: string[] | undefined;
36
+ widthBy: string | undefined;
37
+ heightBy: string | undefined;
38
+ colorBy: string | undefined;
39
+ above: string | undefined;
40
+ shadeBy: string | undefined;
41
+ };
42
+ type Key = keyof GraphConfigValues;
43
+ type Field = {
44
+ key: Key;
45
+ /** Shapes the supplied-or-fallback value; sees the whole config (nestBy reads groupBy). */
46
+ normalise?: (value: unknown, config: GraphStateConfig) => unknown;
47
+ } & ({
48
+ fallback: () => unknown;
49
+ raw?: never;
50
+ }
51
+ /** Taken as given — a `null` stays `null`, an omitted key is `undefined` — no fallback. */
52
+ | {
53
+ raw: true;
54
+ fallback?: never;
55
+ });
56
+ /**
57
+ * Every input, its default, and how it is shaped. `update()` assigns every field on every call —
58
+ * an omitted key reverts to its fallback, which is the contract the GraphState specs pin
59
+ * field by field.
60
+ */
61
+ export declare const CONFIG_FIELDS: readonly Field[];
62
+ /**
63
+ * A graph's inputs — the one job `update()` has. The layout pipeline and the selection read
64
+ * from here.
65
+ *
66
+ * `update()` FULLY re-applies config rather than merging deltas, matching `SparkState.update` —
67
+ * so a prop reverting to undefined actually reverts. `apply()` is the merging verb, for a
68
+ * component writing the few options its own controls drive into a state someone else owns.
69
+ */
70
+ export declare class GraphConfig {
71
+ #private;
72
+ constructor(config?: GraphStateConfig);
73
+ update(config?: GraphStateConfig): void;
74
+ /** Merge a partial config over the last full one, leaving every key it does not name alone. */
75
+ apply(config: Partial<GraphStateConfig>): GraphStateConfig;
76
+ /**
77
+ * Change the detail level from inside the component. `Graph`'s `density` PROP only reaches
78
+ * the state it owns; with a caller-supplied state — how all three views share one — a
79
+ * control that set the prop would render, click, and change nothing.
80
+ */
81
+ setDensity(density: Density): void;
82
+ /** Every edge or only the climbing ones (#167) — what `ViolationsControl` drives. */
83
+ setShowEdges(value: ShowEdges): void;
84
+ /** Move the drill path from inside the state — drilling. See `GraphDrill`. */
85
+ setFocusPath(path: string[]): void;
86
+ /** Change the grouping axes from inside the component — same reason as `setDensity`. */
87
+ setGrouping(outer: NodeAxis, inner?: NodeAxis | null): void;
88
+ get nodes(): unknown[];
89
+ get edges(): unknown[];
90
+ get fields(): GraphFields;
91
+ get layout(): string | LayoutFn;
92
+ get focus(): string | null;
93
+ get density(): Density;
94
+ get arrange(): Arrange;
95
+ get groupBy(): NodeAxis;
96
+ get nestBy(): NodeAxis | null;
97
+ get groupTint(): boolean;
98
+ get sizeBy(): string;
99
+ get sizeScale(): "linear" | "log";
100
+ get depth(): number;
101
+ get focusPath(): string[];
102
+ get levels(): number | undefined;
103
+ get root(): string | null;
104
+ get bundleTension(): number | undefined;
105
+ get radialMode(): "tree" | "dendrogram";
106
+ get edgeStyle(): EdgeStyle;
107
+ get preset(): GraphPreset;
108
+ get mode(): "light" | "dark";
109
+ get label(): string | undefined;
110
+ get onselect(): ((id: string | null) => void) | undefined;
111
+ get ondrill(): ((path: string[], node: import("../types.js").GraphNode | null) => void | Promise<void>) | undefined;
112
+ get ondrillup(): ((path: string[]) => void | Promise<void>) | undefined;
113
+ get onfocuspath(): ((path: string[]) => void) | undefined;
114
+ get onexpand(): ((id: string, node: import("../types.js").GraphNode) => void) | undefined;
115
+ get oncollapse(): ((id: string, node: import("../types.js").GraphNode) => void) | undefined;
116
+ get showEdges(): ShowEdges;
117
+ get layerLabels(): string[] | undefined;
118
+ get widthBy(): string | undefined;
119
+ get heightBy(): string | undefined;
120
+ get colorBy(): string | undefined;
121
+ get above(): string | undefined;
122
+ get shadeBy(): string | undefined;
123
+ }
124
+ export {};
@@ -0,0 +1,253 @@
1
+ import { defaultGraphPreset } from '../preset.js';
2
+ /**
3
+ * A level cap, or nothing.
4
+ *
5
+ * Left UNDEFINED when the caller said nothing: a state-level default of 2 is indistinguishable
6
+ * from an explicit 2, and `radial` needs "no cap" to mean the whole tree — inventing a number
7
+ * here silently pruned a codebase dendrogram to two rings.
8
+ */
9
+ const floorLevels = (levels) => levels === undefined ? undefined : Math.max(1, Math.floor(levels));
10
+ /**
11
+ * Rejected rather than rendered: a box subdivided by its OWN axis yields exactly one child
12
+ * containing everything, which reads as a rendering fault, not a no-op.
13
+ */
14
+ const distinctNest = (inner, outer) => (inner === outer ? null : inner);
15
+ /**
16
+ * Every input, its default, and how it is shaped. `update()` assigns every field on every call —
17
+ * an omitted key reverts to its fallback, which is the contract the GraphState specs pin
18
+ * field by field.
19
+ */
20
+ export const CONFIG_FIELDS = Object.freeze([
21
+ { key: 'nodes', fallback: () => [] },
22
+ { key: 'edges', fallback: () => [] },
23
+ { key: 'fields', fallback: () => ({}) },
24
+ { key: 'layout', fallback: () => 'flow' },
25
+ { key: 'focus', fallback: () => null },
26
+ { key: 'density', fallback: () => 'keys' },
27
+ { key: 'arrange', fallback: () => 'untangle' },
28
+ { key: 'groupBy', fallback: () => 'group' },
29
+ {
30
+ key: 'nestBy',
31
+ fallback: () => null,
32
+ normalise: (v, config) => distinctNest(v, config.groupBy ?? 'group')
33
+ },
34
+ { key: 'groupTint', fallback: () => false },
35
+ { key: 'sizeBy', fallback: () => 'degree' },
36
+ { key: 'sizeScale', fallback: () => 'linear' },
37
+ // Floored at 1: a depth of 0 or -1 is a request for nothing, and returning an empty canvas
38
+ // for it looks identical to a broken focus.
39
+ { key: 'depth', fallback: () => 1, normalise: (v) => Math.max(1, Math.floor(v)) },
40
+ { key: 'focusPath', fallback: () => [] },
41
+ { key: 'levels', normalise: floorLevels, raw: true },
42
+ { key: 'root', fallback: () => null },
43
+ { key: 'bundleTension', raw: true },
44
+ { key: 'radialMode', fallback: () => 'tree' },
45
+ { key: 'edgeStyle', fallback: () => 'curved' },
46
+ { key: 'preset', fallback: () => defaultGraphPreset },
47
+ { key: 'mode', fallback: () => 'light' },
48
+ { key: 'label', raw: true },
49
+ { key: 'onselect', raw: true },
50
+ { key: 'ondrill', raw: true },
51
+ { key: 'ondrillup', raw: true },
52
+ { key: 'onfocuspath', raw: true },
53
+ { key: 'onexpand', raw: true },
54
+ { key: 'oncollapse', raw: true },
55
+ { key: 'showEdges', fallback: () => 'all' },
56
+ { key: 'layerLabels', raw: true },
57
+ { key: 'widthBy', raw: true },
58
+ { key: 'heightBy', raw: true },
59
+ { key: 'colorBy', raw: true },
60
+ { key: 'above', raw: true },
61
+ { key: 'shadeBy', raw: true }
62
+ ]);
63
+ function resolve(config) {
64
+ const values = {};
65
+ for (const field of CONFIG_FIELDS) {
66
+ const given = config[field.key];
67
+ const value = field.raw ? given : (given ?? field.fallback());
68
+ const { key, normalise } = field;
69
+ values[key] = normalise ? normalise(value, config) : value;
70
+ }
71
+ return values;
72
+ }
73
+ /**
74
+ * A graph's inputs — the one job `update()` has. The layout pipeline and the selection read
75
+ * from here.
76
+ *
77
+ * `update()` FULLY re-applies config rather than merging deltas, matching `SparkState.update` —
78
+ * so a prop reverting to undefined actually reverts. `apply()` is the merging verb, for a
79
+ * component writing the few options its own controls drive into a state someone else owns.
80
+ */
81
+ export class GraphConfig {
82
+ #v = $state(resolve({}));
83
+ /** The last FULL config, so `apply` can merge over it rather than reset around it. */
84
+ #last = {};
85
+ constructor(config = {}) {
86
+ this.update(config);
87
+ }
88
+ update(config = {}) {
89
+ const previous = this.#last;
90
+ this.#last = config;
91
+ // Field by field onto the one proxy, not a new object: each field stays its own signal,
92
+ // so a re-render that leaves `nodes` alone does not re-trigger what reads only `nodes`.
93
+ //
94
+ // And only the fields whose INPUT changed. Re-assigning the same array still fires its
95
+ // signal — the proxy wraps it afresh — and an absent `fields` resolves to a fresh `{}`, so
96
+ // every update would re-normalise the whole model even when only a layout option moved.
97
+ // A consumer's own `$state` array mutated in place is unaffected: it is tracked through
98
+ // its own proxy, not through this assignment.
99
+ const resolved = resolve(config);
100
+ const target = this.#v;
101
+ for (const key of Object.keys(resolved)) {
102
+ if (key in config && config[key] === previous[key])
103
+ continue;
104
+ if (!(key in config) && !(key in previous))
105
+ continue;
106
+ target[key] = resolved[key];
107
+ }
108
+ }
109
+ /** Merge a partial config over the last full one, leaving every key it does not name alone. */
110
+ apply(config) {
111
+ const merged = { ...this.#last, ...config };
112
+ this.update(merged);
113
+ return merged;
114
+ }
115
+ /**
116
+ * Change the detail level from inside the component. `Graph`'s `density` PROP only reaches
117
+ * the state it owns; with a caller-supplied state — how all three views share one — a
118
+ * control that set the prop would render, click, and change nothing.
119
+ */
120
+ setDensity(density) {
121
+ this.#v.density = density;
122
+ this.#remember({ density });
123
+ }
124
+ /** Every edge or only the climbing ones (#167) — what `ViolationsControl` drives. */
125
+ setShowEdges(value) {
126
+ this.#v.showEdges = value;
127
+ this.#remember({ showEdges: value });
128
+ }
129
+ /** Move the drill path from inside the state — drilling. See `GraphDrill`. */
130
+ setFocusPath(path) {
131
+ this.#v.focusPath = path;
132
+ this.#remember({ focusPath: path });
133
+ }
134
+ /** Change the grouping axes from inside the component — same reason as `setDensity`. */
135
+ setGrouping(outer, inner = null) {
136
+ this.#v.groupBy = outer;
137
+ this.#v.nestBy = distinctNest(inner, outer);
138
+ this.#remember({ groupBy: outer, nestBy: this.#v.nestBy });
139
+ }
140
+ /**
141
+ * Fold a change made from INSIDE into the last full config. `apply()` merges over that, so a
142
+ * setter that changed only the live value was quietly undone by the next merge — any
143
+ * component writing its own options put the old drill path, density or grouping back.
144
+ */
145
+ #remember(change) {
146
+ this.#last = { ...this.#last, ...change };
147
+ }
148
+ get nodes() {
149
+ return this.#v.nodes;
150
+ }
151
+ get edges() {
152
+ return this.#v.edges;
153
+ }
154
+ get fields() {
155
+ return this.#v.fields;
156
+ }
157
+ get layout() {
158
+ return this.#v.layout;
159
+ }
160
+ get focus() {
161
+ return this.#v.focus;
162
+ }
163
+ get density() {
164
+ return this.#v.density;
165
+ }
166
+ get arrange() {
167
+ return this.#v.arrange;
168
+ }
169
+ get groupBy() {
170
+ return this.#v.groupBy;
171
+ }
172
+ get nestBy() {
173
+ return this.#v.nestBy;
174
+ }
175
+ get groupTint() {
176
+ return this.#v.groupTint;
177
+ }
178
+ get sizeBy() {
179
+ return this.#v.sizeBy;
180
+ }
181
+ get sizeScale() {
182
+ return this.#v.sizeScale;
183
+ }
184
+ get depth() {
185
+ return this.#v.depth;
186
+ }
187
+ get focusPath() {
188
+ return this.#v.focusPath;
189
+ }
190
+ get levels() {
191
+ return this.#v.levels;
192
+ }
193
+ get root() {
194
+ return this.#v.root;
195
+ }
196
+ get bundleTension() {
197
+ return this.#v.bundleTension;
198
+ }
199
+ get radialMode() {
200
+ return this.#v.radialMode;
201
+ }
202
+ get edgeStyle() {
203
+ return this.#v.edgeStyle;
204
+ }
205
+ get preset() {
206
+ return this.#v.preset;
207
+ }
208
+ get mode() {
209
+ return this.#v.mode;
210
+ }
211
+ get label() {
212
+ return this.#v.label;
213
+ }
214
+ get onselect() {
215
+ return this.#v.onselect;
216
+ }
217
+ get ondrill() {
218
+ return this.#v.ondrill;
219
+ }
220
+ get ondrillup() {
221
+ return this.#v.ondrillup;
222
+ }
223
+ get onfocuspath() {
224
+ return this.#v.onfocuspath;
225
+ }
226
+ get onexpand() {
227
+ return this.#v.onexpand;
228
+ }
229
+ get oncollapse() {
230
+ return this.#v.oncollapse;
231
+ }
232
+ get showEdges() {
233
+ return this.#v.showEdges;
234
+ }
235
+ get layerLabels() {
236
+ return this.#v.layerLabels;
237
+ }
238
+ get widthBy() {
239
+ return this.#v.widthBy;
240
+ }
241
+ get heightBy() {
242
+ return this.#v.heightBy;
243
+ }
244
+ get colorBy() {
245
+ return this.#v.colorBy;
246
+ }
247
+ get above() {
248
+ return this.#v.above;
249
+ }
250
+ get shadeBy() {
251
+ return this.#v.shadeBy;
252
+ }
253
+ }
@@ -0,0 +1,52 @@
1
+ import type { Cluster } from '../layout/types.js';
2
+ import type { GraphModel } from '../types.js';
3
+ import type { GraphConfig } from './GraphConfig.svelte.js';
4
+ export type Breadcrumb = {
5
+ /** The drill path this crumb returns to. `[]` is the root. */
6
+ path: string[];
7
+ /** The segment's name; null for the root, which the view names (`All`, a title…). */
8
+ label: string | null;
9
+ /** Where the reader is now — the last crumb, which is shown but not a way back. */
10
+ current: boolean;
11
+ };
12
+ type Sources = {
13
+ config: GraphConfig;
14
+ model: () => GraphModel;
15
+ /** The layout's registry key, or 'custom'. */
16
+ layoutName: () => string;
17
+ };
18
+ /**
19
+ * Drilling: moving `focusPath` — which containment subtree is the whole canvas — and telling the
20
+ * host, who may have to FETCH that level (#165). docs/design/24-world-view.md, Decision 3.
21
+ *
22
+ * Drilling changes scope; it never selects. Only a layout that declares `focusPath` drills — a
23
+ * custom LayoutFn might ignore it, and a no-op gesture is worse than none.
24
+ *
25
+ * A handler may return a promise: the state is `pending` until it settles, only the LATEST
26
+ * drill's settlement counts (an older load finishing late changes nothing), and a rejection —
27
+ * or a handler that throws — restores the previous path and exposes the error, because an
28
+ * empty canvas would read as a broken level rather than a failed load.
29
+ */
30
+ export declare class GraphDrill {
31
+ #private;
32
+ constructor(sources: Sources);
33
+ get path(): string[];
34
+ get breadcrumbs(): Breadcrumb[];
35
+ get pending(): boolean;
36
+ /** Why the last drill failed, until the next drill. */
37
+ get error(): unknown;
38
+ /** Whether the active layout drills at all. */
39
+ get enabled(): boolean;
40
+ /**
41
+ * Whether a box can be opened: in a drilling layout, with an address (a pathless box has
42
+ * none), not already the canvas, and with something below it — in the data, or behind the
43
+ * host's loader.
44
+ */
45
+ canDrill(box: Cluster): boolean;
46
+ drillInto(box: Cluster): boolean;
47
+ /** Climb `levels` toward the root. False (and silent) when already there. */
48
+ drillOut(levels?: number): boolean;
49
+ /** Return to an ancestor — what a breadcrumb does. False for anything that is not one. */
50
+ drillTo(path: string[]): boolean;
51
+ }
52
+ export {};
@@ -0,0 +1,115 @@
1
+ import { LAYOUT_OPTIONS } from '../layout/options.js';
2
+ const isPrefix = (prefix, path) => prefix.length <= path.length && prefix.every((segment, i) => path[i] === segment);
3
+ const isThenable = (value) => value !== null && typeof value === 'object' && typeof value.then === 'function';
4
+ /**
5
+ * Drilling: moving `focusPath` — which containment subtree is the whole canvas — and telling the
6
+ * host, who may have to FETCH that level (#165). docs/design/24-world-view.md, Decision 3.
7
+ *
8
+ * Drilling changes scope; it never selects. Only a layout that declares `focusPath` drills — a
9
+ * custom LayoutFn might ignore it, and a no-op gesture is worse than none.
10
+ *
11
+ * A handler may return a promise: the state is `pending` until it settles, only the LATEST
12
+ * drill's settlement counts (an older load finishing late changes nothing), and a rejection —
13
+ * or a handler that throws — restores the previous path and exposes the error, because an
14
+ * empty canvas would read as a broken level rather than a failed load.
15
+ */
16
+ export class GraphDrill {
17
+ #sources;
18
+ #pending = $state(false);
19
+ #error = $state(null);
20
+ /** Bumped by every drill; a settlement for an older token is ignored. */
21
+ #token = 0;
22
+ constructor(sources) {
23
+ this.#sources = sources;
24
+ }
25
+ get path() {
26
+ return this.#sources.config.focusPath;
27
+ }
28
+ get breadcrumbs() {
29
+ const path = this.path;
30
+ return [
31
+ { path: [], label: null, current: path.length === 0 },
32
+ ...path.map((segment, i) => ({
33
+ path: path.slice(0, i + 1),
34
+ label: segment,
35
+ current: i === path.length - 1
36
+ }))
37
+ ];
38
+ }
39
+ get pending() {
40
+ return this.#pending;
41
+ }
42
+ /** Why the last drill failed, until the next drill. */
43
+ get error() {
44
+ return this.#error;
45
+ }
46
+ /** Whether the active layout drills at all. */
47
+ get enabled() {
48
+ return LAYOUT_OPTIONS[this.#sources.layoutName()]?.includes('focusPath') === true;
49
+ }
50
+ /**
51
+ * Whether a box can be opened: in a drilling layout, with an address (a pathless box has
52
+ * none), not already the canvas, and with something below it — in the data, or behind the
53
+ * host's loader.
54
+ */
55
+ canDrill(box) {
56
+ const path = box.path ?? [];
57
+ if (!this.enabled || path.length === 0 || isPrefix(path, this.path))
58
+ return false;
59
+ return !box.leaf || this.#sources.config.ondrill !== undefined;
60
+ }
61
+ drillInto(box) {
62
+ if (!this.canDrill(box))
63
+ return false;
64
+ const path = box.path;
65
+ const node = box.declared ? (this.#sources.model().byId.get(box.declared) ?? null) : null;
66
+ this.#move(path, () => this.#sources.config.ondrill?.(path, node));
67
+ return true;
68
+ }
69
+ /** Climb `levels` toward the root. False (and silent) when already there. */
70
+ drillOut(levels = 1) {
71
+ if (this.path.length === 0)
72
+ return false;
73
+ return this.drillTo(this.path.slice(0, Math.max(0, this.path.length - levels)));
74
+ }
75
+ /** Return to an ancestor — what a breadcrumb does. False for anything that is not one. */
76
+ drillTo(path) {
77
+ if (path.length >= this.path.length || !isPrefix(path, this.path))
78
+ return false;
79
+ this.#move(path, () => this.#sources.config.ondrillup?.(path));
80
+ return true;
81
+ }
82
+ #move(path, fire) {
83
+ const previous = this.path;
84
+ const token = ++this.#token;
85
+ this.#pending = false;
86
+ this.#error = null;
87
+ this.#setPath(path);
88
+ const fail = (error) => {
89
+ if (token !== this.#token)
90
+ return;
91
+ this.#pending = false;
92
+ this.#error = error;
93
+ this.#setPath(previous);
94
+ };
95
+ let result;
96
+ try {
97
+ result = fire();
98
+ }
99
+ catch (error) {
100
+ fail(error);
101
+ return;
102
+ }
103
+ if (!isThenable(result))
104
+ return;
105
+ this.#pending = true;
106
+ result.then(() => {
107
+ if (token === this.#token)
108
+ this.#pending = false;
109
+ }, fail);
110
+ }
111
+ #setPath(path) {
112
+ this.#sources.config.setFocusPath(path);
113
+ this.#sources.config.onfocuspath?.(path);
114
+ }
115
+ }
@@ -0,0 +1,35 @@
1
+ import type { GraphModel } from '../types.js';
2
+ import type { GraphConfig } from './GraphConfig.svelte.js';
3
+ /** What a drill bar can offer for the selection: open its group, or fold its group back. */
4
+ export type GroupAction = {
5
+ kind: 'expand' | 'collapse';
6
+ group: string;
7
+ label: string;
8
+ };
9
+ type Sources = {
10
+ /** The CANONICAL model — before condensation, so every group and member is visible here. */
11
+ model: () => GraphModel;
12
+ config: GraphConfig;
13
+ };
14
+ /**
15
+ * Which groups are collapsed (#166). A group starts as its node says (`collapsed`, default
16
+ * true — a node with members is a group to fold); the reader toggles from there. The state
17
+ * condenses the model through `collapsed` before any layout sees it.
18
+ */
19
+ export declare class GraphGroups {
20
+ #private;
21
+ constructor(sources: Sources);
22
+ /** The collapsed group ids — what condensation reads. */
23
+ get collapsed(): Set<string>;
24
+ isGroup(id: string): boolean;
25
+ isCollapsed(id: string): boolean;
26
+ /** How many members the group names — loaded or not, since the host may send them later. */
27
+ memberCount(id: string): number;
28
+ /** The EXPANDED group a node belongs to, or null — what a member collapses back into. */
29
+ groupOf(id: string): string | null;
30
+ /** Flip a group. False for a node that is not one. Tells the host either way it went. */
31
+ toggle(id: string): boolean;
32
+ /** The selection's group action, or null when it is neither a collapsed group nor a member. */
33
+ actionFor(selected: string | null): GroupAction | null;
34
+ }
35
+ export {};
@@ -0,0 +1,73 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ import { groupsOf } from '../model/condense.js';
3
+ /**
4
+ * Which groups are collapsed (#166). A group starts as its node says (`collapsed`, default
5
+ * true — a node with members is a group to fold); the reader toggles from there. The state
6
+ * condenses the model through `collapsed` before any layout sees it.
7
+ */
8
+ export class GraphGroups {
9
+ #sources;
10
+ /** Groups the reader flipped from their starting state. */
11
+ #toggled = new SvelteSet();
12
+ #groups;
13
+ #collapsed;
14
+ constructor(sources) {
15
+ this.#sources = sources;
16
+ this.#groups = $derived(groupsOf(sources.model()));
17
+ // A SvelteSet, as GraphSelection's `related` is: the house shape for a derived id set.
18
+ this.#collapsed = $derived(new SvelteSet([...this.#groups.keys()].filter((id) => this.#startsCollapsed(id) !== this.#toggled.has(id))));
19
+ }
20
+ #node(id) {
21
+ return this.#sources.model().byId.get(id);
22
+ }
23
+ #startsCollapsed(id) {
24
+ return this.#node(id)?.collapsed ?? true;
25
+ }
26
+ /** The collapsed group ids — what condensation reads. */
27
+ get collapsed() {
28
+ return this.#collapsed;
29
+ }
30
+ isGroup(id) {
31
+ return this.#groups.has(id);
32
+ }
33
+ isCollapsed(id) {
34
+ return this.#collapsed.has(id);
35
+ }
36
+ /** How many members the group names — loaded or not, since the host may send them later. */
37
+ memberCount(id) {
38
+ return this.#node(id)?.members?.length ?? 0;
39
+ }
40
+ /** The EXPANDED group a node belongs to, or null — what a member collapses back into. */
41
+ groupOf(id) {
42
+ for (const [group, members] of this.#groups) {
43
+ if (!this.#collapsed.has(group) && members.includes(id))
44
+ return group;
45
+ }
46
+ return null;
47
+ }
48
+ /** Flip a group. False for a node that is not one. Tells the host either way it went. */
49
+ toggle(id) {
50
+ const node = this.#node(id);
51
+ if (!node || !this.isGroup(id))
52
+ return false;
53
+ const expanding = this.isCollapsed(id);
54
+ if (this.#toggled.has(id))
55
+ this.#toggled.delete(id);
56
+ else
57
+ this.#toggled.add(id);
58
+ const { onexpand, oncollapse } = this.#sources.config;
59
+ (expanding ? onexpand : oncollapse)?.(id, node);
60
+ return true;
61
+ }
62
+ /** The selection's group action, or null when it is neither a collapsed group nor a member. */
63
+ actionFor(selected) {
64
+ if (!selected)
65
+ return null;
66
+ const label = (group) => this.#node(group)?.label ?? group;
67
+ if (this.isGroup(selected) && this.isCollapsed(selected)) {
68
+ return { kind: 'expand', group: selected, label: label(selected) };
69
+ }
70
+ const group = this.groupOf(selected);
71
+ return group ? { kind: 'collapse', group, label: label(group) } : null;
72
+ }
73
+ }