@prnt/dagr-explorer 0.1.3

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 (135) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +21 -0
  3. package/README.md +441 -0
  4. package/dist/base.d.ts +63 -0
  5. package/dist/base.d.ts.map +1 -0
  6. package/dist/base.js +20 -0
  7. package/dist/base.js.map +1 -0
  8. package/dist/camera.d.ts +61 -0
  9. package/dist/camera.d.ts.map +1 -0
  10. package/dist/camera.js +142 -0
  11. package/dist/camera.js.map +1 -0
  12. package/dist/context.d.ts +110 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/context.js +71 -0
  15. package/dist/context.js.map +1 -0
  16. package/dist/dagr-explorer.d.ts +41 -0
  17. package/dist/dagr-explorer.d.ts.map +1 -0
  18. package/dist/dagr-explorer.js +13 -0
  19. package/dist/dagr-explorer.js.map +1 -0
  20. package/dist/errors.d.ts +55 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +57 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/explorer-details.d.ts +44 -0
  25. package/dist/explorer-details.d.ts.map +1 -0
  26. package/dist/explorer-details.js +76 -0
  27. package/dist/explorer-details.js.map +1 -0
  28. package/dist/explorer-search.d.ts +31 -0
  29. package/dist/explorer-search.d.ts.map +1 -0
  30. package/dist/explorer-search.js +86 -0
  31. package/dist/explorer-search.js.map +1 -0
  32. package/dist/explorer-toolbar.d.ts +17 -0
  33. package/dist/explorer-toolbar.d.ts.map +1 -0
  34. package/dist/explorer-toolbar.js +44 -0
  35. package/dist/explorer-toolbar.js.map +1 -0
  36. package/dist/explorer-trace-toggle.d.ts +16 -0
  37. package/dist/explorer-trace-toggle.d.ts.map +1 -0
  38. package/dist/explorer-trace-toggle.js +8 -0
  39. package/dist/explorer-trace-toggle.js.map +1 -0
  40. package/dist/explorer-viewport.d.ts +69 -0
  41. package/dist/explorer-viewport.d.ts.map +1 -0
  42. package/dist/explorer-viewport.js +69 -0
  43. package/dist/explorer-viewport.js.map +1 -0
  44. package/dist/explorer-views.d.ts +24 -0
  45. package/dist/explorer-views.d.ts.map +1 -0
  46. package/dist/explorer-views.js +16 -0
  47. package/dist/explorer-views.js.map +1 -0
  48. package/dist/index.d.ts +48 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +33 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/isomorphic-layout-effect.d.ts +7 -0
  53. package/dist/isomorphic-layout-effect.d.ts.map +1 -0
  54. package/dist/isomorphic-layout-effect.js +7 -0
  55. package/dist/isomorphic-layout-effect.js.map +1 -0
  56. package/dist/labels.d.ts +67 -0
  57. package/dist/labels.d.ts.map +1 -0
  58. package/dist/labels.js +50 -0
  59. package/dist/labels.js.map +1 -0
  60. package/dist/layout.d.ts +82 -0
  61. package/dist/layout.d.ts.map +1 -0
  62. package/dist/layout.js +244 -0
  63. package/dist/layout.js.map +1 -0
  64. package/dist/navigation.d.ts +20 -0
  65. package/dist/navigation.d.ts.map +1 -0
  66. package/dist/navigation.js +44 -0
  67. package/dist/navigation.js.map +1 -0
  68. package/dist/root.d.ts +89 -0
  69. package/dist/root.d.ts.map +1 -0
  70. package/dist/root.js +401 -0
  71. package/dist/root.js.map +1 -0
  72. package/dist/search.d.ts +19 -0
  73. package/dist/search.d.ts.map +1 -0
  74. package/dist/search.js +31 -0
  75. package/dist/search.js.map +1 -0
  76. package/dist/size.d.ts +12 -0
  77. package/dist/size.d.ts.map +1 -0
  78. package/dist/size.js +20 -0
  79. package/dist/size.js.map +1 -0
  80. package/dist/svg-base.d.ts +33 -0
  81. package/dist/svg-base.d.ts.map +1 -0
  82. package/dist/svg-base.js +89 -0
  83. package/dist/svg-base.js.map +1 -0
  84. package/dist/types.d.ts +61 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/dist/types.js +2 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/use-explorer-camera.d.ts +56 -0
  89. package/dist/use-explorer-camera.d.ts.map +1 -0
  90. package/dist/use-explorer-camera.js +641 -0
  91. package/dist/use-explorer-camera.js.map +1 -0
  92. package/dist/use-explorer.d.ts +23 -0
  93. package/dist/use-explorer.d.ts.map +1 -0
  94. package/dist/use-explorer.js +26 -0
  95. package/dist/use-explorer.js.map +1 -0
  96. package/dist/validate.d.ts +20 -0
  97. package/dist/validate.d.ts.map +1 -0
  98. package/dist/validate.js +99 -0
  99. package/dist/validate.js.map +1 -0
  100. package/dist/viewport-surface.d.ts +90 -0
  101. package/dist/viewport-surface.d.ts.map +1 -0
  102. package/dist/viewport-surface.js +485 -0
  103. package/dist/viewport-surface.js.map +1 -0
  104. package/dist/visible-set.d.ts +65 -0
  105. package/dist/visible-set.d.ts.map +1 -0
  106. package/dist/visible-set.js +168 -0
  107. package/dist/visible-set.js.map +1 -0
  108. package/package.json +62 -0
  109. package/src/base.ts +67 -0
  110. package/src/camera.ts +206 -0
  111. package/src/context.ts +168 -0
  112. package/src/dagr-explorer.tsx +85 -0
  113. package/src/errors.ts +85 -0
  114. package/src/explorer-details.tsx +156 -0
  115. package/src/explorer-search.tsx +140 -0
  116. package/src/explorer-toolbar.tsx +77 -0
  117. package/src/explorer-trace-toggle.tsx +34 -0
  118. package/src/explorer-viewport.tsx +181 -0
  119. package/src/explorer-views.tsx +61 -0
  120. package/src/index.ts +76 -0
  121. package/src/isomorphic-layout-effect.ts +7 -0
  122. package/src/labels.ts +109 -0
  123. package/src/layout.ts +298 -0
  124. package/src/navigation.ts +51 -0
  125. package/src/root.tsx +544 -0
  126. package/src/search.ts +36 -0
  127. package/src/size.ts +23 -0
  128. package/src/svg-base.tsx +173 -0
  129. package/src/types.ts +69 -0
  130. package/src/use-explorer-camera.ts +675 -0
  131. package/src/use-explorer.ts +32 -0
  132. package/src/validate.ts +173 -0
  133. package/src/viewport-surface.tsx +650 -0
  134. package/src/visible-set.ts +225 -0
  135. package/styles.css +253 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Which nodes get a DOM element, which are left to the base layer, and which
3
+ * edges are in view: the explorer's virtualization, as one pure function.
4
+ *
5
+ * A node's tier comes from its width on screen in CSS pixels, with half-open
6
+ * gates, the rule `@prnt/dagr-render`'s rich nodes use. Below the `summary`
7
+ * gate it is a mark and has NO element: the base layer draws it.
8
+ *
9
+ * **The scan is linear on purpose.** Testing 10,000 boxes costs about half a
10
+ * millisecond per call (0.4 to 0.6 ms measured in `packages/explorer/bench`,
11
+ * the output lists included), well inside a 16 ms frame. The renderer's overlay scans the same way and names a spatial
12
+ * index as the fix if a measurement ever asks for one.
13
+ *
14
+ * Internal to the package. Only the tier types and `ExplorerVisibleSet` are
15
+ * exported from the entry, as types: a base layer is given a visible set.
16
+ */
17
+
18
+ import type { ExplorerBox, ExplorerLayout } from './layout.js';
19
+ import type { Vec2 } from '@prnt/dagr-render/core';
20
+ import { visibleWorld } from './camera.js';
21
+ import type { ExplorerCamera, ExplorerViewportSize } from './camera.js';
22
+
23
+ export type ExplorerTier = 'summary' | 'rich';
24
+ export interface ExplorerTiers {
25
+ readonly summary: number;
26
+ readonly rich: number;
27
+ }
28
+ export const DEFAULT_TIERS: ExplorerTiers = Object.freeze({ summary: 56, rich: 200 });
29
+ export const DEFAULT_MAX_OVERLAY_NODES = 200;
30
+ export const OVERSCAN = 0.25;
31
+
32
+ export interface LayoutIndex {
33
+ readonly nodeIds: readonly string[];
34
+ readonly nodeBoxes: readonly ExplorerBox[];
35
+ readonly edgeIds: readonly string[];
36
+ /** `null` for an edge with no route, which is never in view. */
37
+ readonly edgeBounds: readonly (ExplorerBox | null)[];
38
+ }
39
+
40
+ export function indexLayout(layout: ExplorerLayout): LayoutIndex {
41
+ const edgeIds: string[] = [];
42
+ const edgeBounds: (ExplorerBox | null)[] = [];
43
+ for (const [id, points] of layout.routes) {
44
+ edgeIds.push(id);
45
+ if (points.length === 0) {
46
+ edgeBounds.push(null);
47
+ continue;
48
+ }
49
+ let minX = Infinity;
50
+ let minY = Infinity;
51
+ let maxX = -Infinity;
52
+ let maxY = -Infinity;
53
+ for (const p of points) {
54
+ minX = Math.min(minX, p.x);
55
+ minY = Math.min(minY, p.y);
56
+ maxX = Math.max(maxX, p.x);
57
+ maxY = Math.max(maxY, p.y);
58
+ }
59
+ edgeBounds.push({ x: minX, y: minY, width: maxX - minX, height: maxY - minY });
60
+ }
61
+ return {
62
+ nodeIds: [...layout.boxes.keys()],
63
+ nodeBoxes: [...layout.boxes.values()],
64
+ edgeIds,
65
+ edgeBounds,
66
+ };
67
+ }
68
+
69
+ export interface VisibleSetOptions {
70
+ readonly tiers?: ExplorerTiers | undefined;
71
+ readonly maxOverlayNodes?: number | undefined;
72
+ /** Node ids that are always overlay nodes, exempt from the cap. */
73
+ readonly pinned?: readonly string[] | undefined;
74
+ }
75
+
76
+ /**
77
+ * What is in view: the nodes with an element, the nodes and edges a base
78
+ * layer draws. Exported as a type, and experimental like the base-layer seam
79
+ * that is given it: it may change until a native base confirms it.
80
+ */
81
+ export interface ExplorerVisibleSet {
82
+ /** Overlay nodes and their tier, in data order. */
83
+ readonly overlay: ReadonlyMap<string, ExplorerTier>;
84
+ /** Nodes in view that the base layer draws as marks, in data order. */
85
+ readonly baseNodes: readonly string[];
86
+ /** Edges whose route is in view, in data order. */
87
+ readonly edges: readonly string[];
88
+ }
89
+
90
+ /** Inclusive: boxes that only touch count, so a hairline route is not culled. */
91
+ function intersects(a: ExplorerBox, b: ExplorerBox): boolean {
92
+ return (
93
+ a.x <= b.x + b.width &&
94
+ a.x + a.width >= b.x &&
95
+ a.y <= b.y + b.height &&
96
+ a.y + a.height >= b.y
97
+ );
98
+ }
99
+
100
+ export function computeVisibleSet(
101
+ index: LayoutIndex,
102
+ camera: ExplorerCamera,
103
+ viewport: ExplorerViewportSize,
104
+ options: VisibleSetOptions = {},
105
+ ): ExplorerVisibleSet {
106
+ const tiers = options.tiers ?? DEFAULT_TIERS;
107
+ const cap = options.maxOverlayNodes ?? DEFAULT_MAX_OVERLAY_NODES;
108
+ const pinned = new Set(options.pinned ?? []);
109
+ const world = visibleWorld(camera, viewport);
110
+ const view: ExplorerBox = {
111
+ x: world.x - world.width * OVERSCAN,
112
+ y: world.y - world.height * OVERSCAN,
113
+ width: world.width * (1 + OVERSCAN * 2),
114
+ height: world.height * (1 + OVERSCAN * 2),
115
+ };
116
+ const centerX = world.x + world.width / 2;
117
+ const centerY = world.y + world.height / 2;
118
+
119
+ const tierOf = new Map<number, ExplorerTier>();
120
+ const candidates: { readonly i: number; readonly distance: number }[] = [];
121
+ const inView: number[] = [];
122
+ index.nodeBoxes.forEach((box, i) => {
123
+ const id = index.nodeIds[i] as string;
124
+ const screenWidth = box.width * camera.scale;
125
+ const natural: ExplorerTier | null =
126
+ screenWidth >= tiers.rich ? 'rich' : screenWidth >= tiers.summary ? 'summary' : null;
127
+ if (pinned.has(id)) {
128
+ tierOf.set(i, natural ?? 'summary');
129
+ return;
130
+ }
131
+ if (!intersects(box, view)) return;
132
+ inView.push(i);
133
+ if (natural === null) return;
134
+ tierOf.set(i, natural);
135
+ const dx = box.x + box.width / 2 - centerX;
136
+ const dy = box.y + box.height / 2 - centerY;
137
+ candidates.push({ i, distance: dx * dx + dy * dy });
138
+ });
139
+
140
+ if (candidates.length > cap) {
141
+ candidates.sort(
142
+ (a, b) =>
143
+ a.distance - b.distance ||
144
+ ((index.nodeIds[a.i] as string) < (index.nodeIds[b.i] as string) ? -1 : 1),
145
+ );
146
+ for (const dropped of candidates.slice(Math.max(0, cap))) tierOf.delete(dropped.i);
147
+ }
148
+
149
+ const overlay = new Map<string, ExplorerTier>();
150
+ index.nodeIds.forEach((id, i) => {
151
+ const tier = tierOf.get(i);
152
+ if (tier !== undefined) overlay.set(id, tier);
153
+ });
154
+ const baseNodes = inView.filter((i) => !tierOf.has(i)).map((i) => index.nodeIds[i] as string);
155
+ const edges: string[] = [];
156
+ index.edgeBounds.forEach((bounds, i) => {
157
+ if (bounds !== null && intersects(bounds, view)) edges.push(index.edgeIds[i] as string);
158
+ });
159
+ return { overlay, baseNodes, edges };
160
+ }
161
+
162
+ /**
163
+ * Whether two sets hold the same nodes at the same tiers, and the same marks
164
+ * and edges in the same order. Overlay order is not compared:
165
+ * `computeVisibleSet` always emits the overlay in the index's data order, so
166
+ * two sets over one index cannot differ in order alone.
167
+ */
168
+ export function sameVisibleSet(a: ExplorerVisibleSet, b: ExplorerVisibleSet): boolean {
169
+ if (a === b) return true;
170
+ if (
171
+ a.overlay.size !== b.overlay.size ||
172
+ a.baseNodes.length !== b.baseNodes.length ||
173
+ a.edges.length !== b.edges.length
174
+ ) {
175
+ return false;
176
+ }
177
+ for (const [id, tier] of a.overlay) {
178
+ if (b.overlay.get(id) !== tier) return false;
179
+ }
180
+ for (let i = 0; i < a.baseNodes.length; i += 1) {
181
+ if (a.baseNodes[i] !== b.baseNodes[i]) return false;
182
+ }
183
+ for (let i = 0; i < a.edges.length; i += 1) {
184
+ if (a.edges[i] !== b.edges[i]) return false;
185
+ }
186
+ return true;
187
+ }
188
+
189
+ export function nearestToCenter(
190
+ index: LayoutIndex,
191
+ camera: ExplorerCamera,
192
+ viewport: ExplorerViewportSize,
193
+ ): string | null {
194
+ const world = visibleWorld(camera, viewport);
195
+ const centerX = world.x + world.width / 2;
196
+ const centerY = world.y + world.height / 2;
197
+ let best: string | null = null;
198
+ let bestDistance = Infinity;
199
+ index.nodeBoxes.forEach((box, i) => {
200
+ const dx = box.x + box.width / 2 - centerX;
201
+ const dy = box.y + box.height / 2 - centerY;
202
+ const distance = dx * dx + dy * dy;
203
+ const id = index.nodeIds[i] as string;
204
+ if (distance < bestDistance || (distance === bestDistance && best !== null && id < best)) {
205
+ best = id;
206
+ bestDistance = distance;
207
+ }
208
+ });
209
+ return best;
210
+ }
211
+
212
+ export function nodeAtPoint(index: LayoutIndex, point: Vec2): string | null {
213
+ for (let i = index.nodeBoxes.length - 1; i >= 0; i -= 1) {
214
+ const box = index.nodeBoxes[i] as ExplorerBox;
215
+ if (
216
+ point.x >= box.x &&
217
+ point.x <= box.x + box.width &&
218
+ point.y >= box.y &&
219
+ point.y <= box.y + box.height
220
+ ) {
221
+ return index.nodeIds[i] as string;
222
+ }
223
+ }
224
+ return null;
225
+ }
package/styles.css ADDED
@@ -0,0 +1,253 @@
1
+ /*
2
+ * @prnt/dagr-explorer/styles.css: the explorer's optional default look.
3
+ *
4
+ * The parts work without it: everything they need to function is inline.
5
+ * This file is the look on top, written only against the parts'
6
+ * `data-dagr-explorer` hooks and their state hooks (`data-tier`,
7
+ * `data-selected`, `data-dimmed`, `data-dragging`, `data-active`).
8
+ *
9
+ * Theme it with these variables, set anywhere above the explorer. Each has
10
+ * a fallback here for a light page, and none is defined by this file, so a
11
+ * value a host sets on `:root` is the one used:
12
+ *
13
+ * --dagr-explorer-accent selection and pressed controls
14
+ * --dagr-explorer-fg text
15
+ * --dagr-explorer-fg-muted secondary text, marks and edges
16
+ * --dagr-explorer-border borders
17
+ * --dagr-explorer-bg surfaces: nodes, controls, the drawer
18
+ * --dagr-explorer-bg-subtle the graph's background, hover
19
+ * --dagr-explorer-focus focus rings
20
+ * --dagr-explorer-font-mono the zoom readout
21
+ *
22
+ * `--dagr-explorer-height` sets the graph's height (default 480px).
23
+ *
24
+ * Every selector is wrapped in `:where()`, so it has no specificity and any
25
+ * rule of the host's wins without a fight. The exceptions are the `:hover`,
26
+ * `:focus` and `:focus-visible` rules: the pseudo-class sits outside the
27
+ * `:where()`, so each of those weighs as one class. A host rule overrides
28
+ * them with a class and a later position in the cascade, or with more
29
+ * specificity.
30
+ */
31
+
32
+ :where([data-dagr-explorer='root']) {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 8px;
36
+ color: var(--dagr-explorer-fg, #1d2430);
37
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
38
+ font-size: 14px;
39
+ line-height: 1.4;
40
+ }
41
+
42
+ /* Controls */
43
+
44
+ :where([data-dagr-explorer='views'] button, [data-dagr-explorer='trace'], [data-dagr-explorer='toolbar'] button, [data-dagr-explorer='details'] button) {
45
+ font: inherit;
46
+ color: inherit;
47
+ background: var(--dagr-explorer-bg, #ffffff);
48
+ border: 1px solid var(--dagr-explorer-border, #d3d9e3);
49
+ border-radius: 6px;
50
+ padding: 4px 10px;
51
+ cursor: pointer;
52
+ }
53
+
54
+ :where([data-dagr-explorer='views'] button, [data-dagr-explorer='trace'], [data-dagr-explorer='toolbar'] button, [data-dagr-explorer='details'] button, [data-dagr-explorer='search'] button):hover {
55
+ background: var(--dagr-explorer-bg-subtle, #f3f5f8);
56
+ }
57
+
58
+ :where([data-dagr-explorer='root'] button, [data-dagr-explorer='root'] input, [data-dagr-explorer='details-body']):focus-visible {
59
+ outline: 2px solid var(--dagr-explorer-focus, #2f6fdf);
60
+ outline-offset: 2px;
61
+ }
62
+
63
+ :where([data-dagr-explorer='views'] button[aria-pressed='true'], [data-dagr-explorer='trace'][data-active='true']) {
64
+ border-color: var(--dagr-explorer-accent, #2f6fdf);
65
+ color: var(--dagr-explorer-accent, #2f6fdf);
66
+ }
67
+
68
+ :where([data-dagr-explorer='toolbar'] button:disabled) {
69
+ opacity: 0.5;
70
+ cursor: default;
71
+ }
72
+
73
+ :where([data-dagr-explorer='views'], [data-dagr-explorer='toolbar']) {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ align-items: center;
77
+ gap: 4px;
78
+ }
79
+
80
+ :where([data-dagr-explorer='zoom-level']) {
81
+ min-width: 5ch;
82
+ text-align: center;
83
+ font-family: var(--dagr-explorer-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
84
+ font-size: 12px;
85
+ color: var(--dagr-explorer-fg-muted, #5c6778);
86
+ }
87
+
88
+ :where([data-dagr-explorer='trace']) {
89
+ align-self: flex-start;
90
+ }
91
+
92
+ /* Search */
93
+
94
+ :where([data-dagr-explorer='search']) {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 4px;
98
+ }
99
+
100
+ :where([data-dagr-explorer='search'] label) {
101
+ font-weight: 600;
102
+ }
103
+
104
+ :where([data-dagr-explorer='search'] input) {
105
+ font: inherit;
106
+ color: inherit;
107
+ background: var(--dagr-explorer-bg, #ffffff);
108
+ border: 1px solid var(--dagr-explorer-border, #d3d9e3);
109
+ border-radius: 6px;
110
+ padding: 6px 10px;
111
+ }
112
+
113
+ :where([data-dagr-explorer='search-count']) {
114
+ margin: 0;
115
+ min-height: 1.4em;
116
+ font-size: 12px;
117
+ color: var(--dagr-explorer-fg-muted, #5c6778);
118
+ }
119
+
120
+ :where([data-dagr-explorer='search-results']) {
121
+ max-height: 12em;
122
+ border: 1px solid var(--dagr-explorer-border, #d3d9e3);
123
+ border-radius: 6px;
124
+ }
125
+
126
+ :where([data-dagr-explorer='search-results'] button) {
127
+ display: block;
128
+ width: 100%;
129
+ font: inherit;
130
+ color: inherit;
131
+ text-align: left;
132
+ background: transparent;
133
+ border: 0;
134
+ padding: 4px 10px;
135
+ cursor: pointer;
136
+ }
137
+
138
+ :where([data-dagr-explorer='search-results'] button[data-selected='true']) {
139
+ color: var(--dagr-explorer-accent, #2f6fdf);
140
+ font-weight: 600;
141
+ }
142
+
143
+ /* Graph */
144
+
145
+ :where([data-dagr-explorer='viewport']) {
146
+ background: var(--dagr-explorer-bg-subtle, #f3f5f8);
147
+ border: 1px solid var(--dagr-explorer-border, #d3d9e3);
148
+ border-radius: 8px;
149
+ cursor: grab;
150
+ color: var(--dagr-explorer-fg-muted, #5c6778);
151
+ }
152
+
153
+ :where([data-dagr-explorer='viewport'][data-dragging='true']) {
154
+ cursor: grabbing;
155
+ }
156
+
157
+ /* Graph focus is what turns wheel zoom on, so it always shows. */
158
+ :where([data-dagr-explorer='viewport']):focus {
159
+ outline: 2px solid var(--dagr-explorer-focus, #2f6fdf);
160
+ outline-offset: 2px;
161
+ }
162
+
163
+ :where([data-dagr-explorer='plane'] svg [data-dimmed='true']) {
164
+ opacity: 0.25;
165
+ }
166
+
167
+ :where([data-dagr-explorer='node']) {
168
+ box-sizing: border-box;
169
+ overflow: hidden;
170
+ padding: 8px 10px;
171
+ font: inherit;
172
+ font-size: 13px;
173
+ text-align: left;
174
+ color: var(--dagr-explorer-fg, #1d2430);
175
+ background: var(--dagr-explorer-bg, #ffffff);
176
+ border: 1px solid var(--dagr-explorer-border, #d3d9e3);
177
+ border-radius: 6px;
178
+ cursor: pointer;
179
+ }
180
+
181
+ /* A summary is small on screen: its text is counter-scaled to stay legible, up to a point. */
182
+ :where([data-dagr-explorer='node'][data-tier='summary']) {
183
+ font-size: calc(12px * min(var(--dagr-explorer-inv-zoom, 1), 2.5));
184
+ white-space: nowrap;
185
+ text-overflow: ellipsis;
186
+ }
187
+
188
+ :where([data-dagr-explorer='node'][data-selected='true']) {
189
+ border-color: var(--dagr-explorer-accent, #2f6fdf);
190
+ box-shadow: 0 0 0 2px var(--dagr-explorer-accent, #2f6fdf);
191
+ }
192
+
193
+ :where([data-dagr-explorer='node']):focus-visible {
194
+ outline: 2px solid var(--dagr-explorer-focus, #2f6fdf);
195
+ outline-offset: 2px;
196
+ }
197
+
198
+ :where([data-dagr-explorer='node'][data-dimmed='true']) {
199
+ opacity: 0.3;
200
+ }
201
+
202
+ :where([data-dagr-explorer='hint']) {
203
+ margin: 0;
204
+ font-size: 12px;
205
+ color: var(--dagr-explorer-fg-muted, #5c6778);
206
+ }
207
+
208
+ /* Drawer */
209
+
210
+ :where([data-dagr-explorer='details']) {
211
+ box-sizing: border-box;
212
+ background: var(--dagr-explorer-bg, #ffffff);
213
+ border-left: 1px solid var(--dagr-explorer-border, #d3d9e3);
214
+ border-radius: 0 8px 8px 0;
215
+ box-shadow: -8px 0 24px rgb(20 30 50 / 0.12);
216
+ }
217
+
218
+ :where([data-dagr-explorer='details-header']) {
219
+ padding: 8px 12px;
220
+ border-bottom: 1px solid var(--dagr-explorer-border, #d3d9e3);
221
+ font-size: 12px;
222
+ font-weight: 600;
223
+ text-transform: uppercase;
224
+ letter-spacing: 0.04em;
225
+ color: var(--dagr-explorer-fg-muted, #5c6778);
226
+ }
227
+
228
+ :where([data-dagr-explorer='details-header'] button) {
229
+ text-transform: none;
230
+ letter-spacing: normal;
231
+ }
232
+
233
+ :where([data-dagr-explorer='details-body']) {
234
+ padding: 12px;
235
+ }
236
+
237
+ :where([data-dagr-explorer='details-label']) {
238
+ margin: 0 0 12px;
239
+ font-size: 16px;
240
+ font-weight: 600;
241
+ color: var(--dagr-explorer-fg, #1d2430);
242
+ }
243
+
244
+ :where([data-dagr-explorer='connections']) {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: 4px;
248
+ }
249
+
250
+ :where([data-dagr-explorer='connections'] button) {
251
+ width: 100%;
252
+ text-align: left;
253
+ }