@zenfg/inspector 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +182 -0
- package/README.md +60 -26
- package/THEMING.md +124 -0
- package/dist/FrameGraphInspector.d.ts +24 -8
- package/dist/FrameGraphInspector.d.ts.map +1 -1
- package/dist/FrameGraphInspector.js +151 -65
- package/dist/FrameGraphInspector.js.map +1 -1
- package/dist/capture.d.ts +5 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +2 -0
- package/dist/capture.js.map +1 -0
- package/dist/debugCaptureModel.d.ts +28 -4
- package/dist/debugCaptureModel.d.ts.map +1 -1
- package/dist/debugCaptureModel.js +72 -16
- package/dist/debugCaptureModel.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/panelAliasAnalysis.d.ts +1 -0
- package/dist/panelAliasAnalysis.d.ts.map +1 -1
- package/dist/panelAliasAnalysis.js +23 -54
- package/dist/panelAliasAnalysis.js.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.d.ts +18 -3
- package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.js +183 -41
- package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
- package/dist/panelDetailLayout.d.ts +27 -0
- package/dist/panelDetailLayout.d.ts.map +1 -0
- package/dist/panelDetailLayout.js +174 -0
- package/dist/panelDetailLayout.js.map +1 -0
- package/dist/panelDetailStyles.d.ts +2 -0
- package/dist/panelDetailStyles.d.ts.map +1 -0
- package/dist/panelDetailStyles.js +57 -0
- package/dist/panelDetailStyles.js.map +1 -0
- package/dist/panelDiagnosticsView.d.ts +10 -1
- package/dist/panelDiagnosticsView.d.ts.map +1 -1
- package/dist/panelDiagnosticsView.js +167 -58
- package/dist/panelDiagnosticsView.js.map +1 -1
- package/dist/panelDomHelpers.d.ts.map +1 -1
- package/dist/panelDomHelpers.js +6 -5
- package/dist/panelDomHelpers.js.map +1 -1
- package/dist/panelGraphLayout.d.ts +3 -2
- package/dist/panelGraphLayout.d.ts.map +1 -1
- package/dist/panelGraphLayout.js +20 -5
- package/dist/panelGraphLayout.js.map +1 -1
- package/dist/panelGraphRenderer.d.ts +10 -0
- package/dist/panelGraphRenderer.d.ts.map +1 -1
- package/dist/panelGraphRenderer.js.map +1 -1
- package/dist/panelGraphScene.d.ts +15 -26
- package/dist/panelGraphScene.d.ts.map +1 -1
- package/dist/panelGraphScene.js +191 -208
- package/dist/panelGraphScene.js.map +1 -1
- package/dist/panelGraphSearch.d.ts +16 -0
- package/dist/panelGraphSearch.d.ts.map +1 -0
- package/dist/panelGraphSearch.js +93 -0
- package/dist/panelGraphSearch.js.map +1 -0
- package/dist/panelGraphView.d.ts +82 -0
- package/dist/panelGraphView.d.ts.map +1 -1
- package/dist/panelGraphView.js +42 -9
- package/dist/panelGraphView.js.map +1 -1
- package/dist/panelGraphVisuals.d.ts +18 -6
- package/dist/panelGraphVisuals.d.ts.map +1 -1
- package/dist/panelGraphVisuals.js +156 -116
- package/dist/panelGraphVisuals.js.map +1 -1
- package/dist/panelInspectorView.d.ts +13 -0
- package/dist/panelInspectorView.d.ts.map +1 -1
- package/dist/panelInspectorView.js +326 -46
- package/dist/panelInspectorView.js.map +1 -1
- package/dist/panelListStyles.d.ts +3 -0
- package/dist/panelListStyles.d.ts.map +1 -0
- package/dist/panelListStyles.js +155 -0
- package/dist/panelListStyles.js.map +1 -0
- package/dist/panelMemoryStyles.d.ts +3 -0
- package/dist/panelMemoryStyles.d.ts.map +1 -0
- package/dist/panelMemoryStyles.js +42 -0
- package/dist/panelMemoryStyles.js.map +1 -0
- package/dist/panelMemoryView.d.ts +13 -1
- package/dist/panelMemoryView.d.ts.map +1 -1
- package/dist/panelMemoryView.js +161 -41
- package/dist/panelMemoryView.js.map +1 -1
- package/dist/panelPassesView.d.ts +15 -0
- package/dist/panelPassesView.d.ts.map +1 -1
- package/dist/panelPassesView.js +202 -72
- package/dist/panelPassesView.js.map +1 -1
- package/dist/panelRawView.d.ts +15 -0
- package/dist/panelRawView.d.ts.map +1 -0
- package/dist/panelRawView.js +105 -0
- package/dist/panelRawView.js.map +1 -0
- package/dist/panelResourcesView.d.ts +8 -0
- package/dist/panelResourcesView.d.ts.map +1 -1
- package/dist/panelResourcesView.js +62 -31
- package/dist/panelResourcesView.js.map +1 -1
- package/dist/panelSelection.d.ts +9 -1
- package/dist/panelSelection.d.ts.map +1 -1
- package/dist/panelSelection.js +36 -6
- package/dist/panelSelection.js.map +1 -1
- package/dist/panelTheme.d.ts +13 -0
- package/dist/panelTheme.d.ts.map +1 -0
- package/dist/panelTheme.js +104 -0
- package/dist/panelTheme.js.map +1 -0
- package/dist/panelTypes.d.ts +18 -4
- package/dist/panelTypes.d.ts.map +1 -1
- package/dist/panelTypes.js.map +1 -1
- package/dist/panelVisualTheme.d.ts +176 -71
- package/dist/panelVisualTheme.d.ts.map +1 -1
- package/dist/panelVisualTheme.js +34 -36
- package/dist/panelVisualTheme.js.map +1 -1
- package/dist/panelWorkbenchHelpers.d.ts +12 -1
- package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
- package/dist/panelWorkbenchHelpers.js +63 -2
- package/dist/panelWorkbenchHelpers.js.map +1 -1
- package/dist/panelWorkbenchStyles.d.ts +2 -0
- package/dist/panelWorkbenchStyles.d.ts.map +1 -0
- package/dist/panelWorkbenchStyles.js +101 -0
- package/dist/panelWorkbenchStyles.js.map +1 -0
- package/dist/panelWorkbenchView.d.ts +16 -0
- package/dist/panelWorkbenchView.d.ts.map +1 -1
- package/dist/panelWorkbenchView.js +228 -50
- package/dist/panelWorkbenchView.js.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +161 -205
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +22 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +10 -0
- package/dist/theme.js.map +1 -0
- package/dist/themeDefinitions.d.ts +251 -0
- package/dist/themeDefinitions.d.ts.map +1 -0
- package/dist/themeDefinitions.js +96 -0
- package/dist/themeDefinitions.js.map +1 -0
- package/dist/themePalette.d.ts +66 -0
- package/dist/themePalette.d.ts.map +1 -0
- package/dist/themePalette.js +37 -0
- package/dist/themePalette.js.map +1 -0
- package/dist/themes.css +165 -0
- package/package.json +44 -10
- package/src/FrameGraphInspector.ts +145 -68
- package/src/capture.ts +4 -0
- package/src/debugCaptureModel.ts +97 -21
- package/src/index.ts +6 -2
- package/src/panelAliasAnalysis.ts +24 -70
- package/src/panelCytoscapeGraphRenderer.ts +177 -40
- package/src/panelDetailLayout.ts +155 -0
- package/src/panelDetailStyles.ts +56 -0
- package/src/panelDiagnosticsView.ts +167 -86
- package/src/panelDomHelpers.ts +4 -5
- package/src/panelGraphLayout.ts +20 -5
- package/src/panelGraphRenderer.ts +7 -0
- package/src/panelGraphScene.ts +204 -292
- package/src/panelGraphSearch.ts +96 -0
- package/src/panelGraphView.ts +41 -9
- package/src/panelGraphVisuals.ts +152 -118
- package/src/panelInspectorView.ts +315 -48
- package/src/panelListStyles.ts +154 -0
- package/src/panelMemoryStyles.ts +41 -0
- package/src/panelMemoryView.ts +178 -53
- package/src/panelPassesView.ts +190 -137
- package/src/panelRawView.ts +99 -0
- package/src/panelResourcesView.ts +69 -56
- package/src/panelSelection.ts +39 -7
- package/src/panelTheme.ts +85 -0
- package/src/panelTypes.ts +16 -4
- package/src/panelVisualTheme.ts +38 -38
- package/src/panelWorkbenchHelpers.ts +62 -4
- package/src/panelWorkbenchStyles.ts +100 -0
- package/src/panelWorkbenchView.ts +201 -52
- package/src/query.ts +11 -11
- package/src/styles.ts +162 -205
- package/src/theme.ts +24 -0
- package/src/themeDefinitions.ts +103 -0
- package/src/themePalette.ts +44 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GRAPH_VISUAL_THEME, type GraphVisualTheme } from './panelVisualTheme.ts';
|
|
1
2
|
import type cytoscape from 'cytoscape';
|
|
2
3
|
import type { ELK } from 'elkjs/lib/elk-api';
|
|
3
4
|
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
} from './panelGraphScene.ts';
|
|
19
20
|
import {
|
|
20
21
|
createGraphStyles,
|
|
22
|
+
fitGraphLabel,
|
|
23
|
+
fitCollapsedGroupLabel,
|
|
24
|
+
graphLabelFontSize,
|
|
21
25
|
expandedGroupLabelMaxWidth,
|
|
22
26
|
GRAPH_GEOMETRY,
|
|
23
27
|
graphEdgeDisplayLabel,
|
|
@@ -38,7 +42,7 @@ export type GraphResizeSubscription = { disconnect(): void };
|
|
|
38
42
|
export type GraphRendererEnvironment = {
|
|
39
43
|
readonly createElement: (tagName: string) => HTMLElement;
|
|
40
44
|
readonly loadRuntime: () => Promise<GraphRendererRuntime>;
|
|
41
|
-
readonly layoutScene: (elk: ELK, scene: GraphScene) => Promise<GraphLayoutResult>;
|
|
45
|
+
readonly layoutScene: (elk: ELK, scene: GraphScene, theme?: GraphVisualTheme) => Promise<GraphLayoutResult>;
|
|
42
46
|
readonly observeResize?: (element: HTMLElement, onResize: () => void) => GraphResizeSubscription;
|
|
43
47
|
};
|
|
44
48
|
|
|
@@ -49,6 +53,9 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
49
53
|
private readonly statusMessage: HTMLElement;
|
|
50
54
|
private readonly retryButton: HTMLButtonElement;
|
|
51
55
|
private readonly resizeSubscription: GraphResizeSubscription | undefined;
|
|
56
|
+
private theme: GraphVisualTheme = GRAPH_VISUAL_THEME;
|
|
57
|
+
private geometryRevision = 0;
|
|
58
|
+
private wasVisible = false;
|
|
52
59
|
private core: cytoscape.Core | undefined;
|
|
53
60
|
private elk: ELK | undefined;
|
|
54
61
|
private loading: Promise<void> | undefined;
|
|
@@ -64,7 +71,16 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
64
71
|
private anchorTargetContentKey: string | undefined;
|
|
65
72
|
private failedSceneContentKey: string | undefined;
|
|
66
73
|
private overview = false;
|
|
67
|
-
private
|
|
74
|
+
private dragPanning = false;
|
|
75
|
+
private pointerInside = false;
|
|
76
|
+
private pointerOverElement = false;
|
|
77
|
+
private readonly cancelPointerInteraction = () => {
|
|
78
|
+
this.dragPanning = false;
|
|
79
|
+
this.cancelGroupTap();
|
|
80
|
+
this.clearHover();
|
|
81
|
+
};
|
|
82
|
+
private pendingReveal: { readonly request: NonNullable<GraphRenderRequest['reveal']>; readonly contentKey: string; readonly captureRevision: number | undefined } | undefined;
|
|
83
|
+
private pendingGroupTap: { readonly id: string; readonly timer: ReturnType<typeof setTimeout> } | undefined;
|
|
68
84
|
|
|
69
85
|
constructor(
|
|
70
86
|
private readonly host: HTMLElement,
|
|
@@ -72,9 +88,15 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
72
88
|
) {
|
|
73
89
|
this.canvasHost = environment.createElement('div');
|
|
74
90
|
this.canvasHost.className = 'zenfg-inspector-graph-canvas';
|
|
91
|
+
this.updateCursor();
|
|
75
92
|
this.tooltip = environment.createElement('div');
|
|
76
93
|
this.tooltip.className = 'zenfg-inspector-graph-tooltip';
|
|
77
94
|
this.tooltip.hidden = true;
|
|
95
|
+
// Cytoscape's node mouseout is not guaranteed when leaving its canvas directly.
|
|
96
|
+
this.canvasHost.addEventListener('pointerenter', () => { this.pointerInside = true; });
|
|
97
|
+
this.canvasHost.addEventListener('pointerleave', () => { this.pointerInside = false; this.clearHover(); });
|
|
98
|
+
this.canvasHost.addEventListener('pointercancel', this.cancelPointerInteraction);
|
|
99
|
+
this.canvasHost.ownerDocument?.defaultView?.addEventListener('blur', this.cancelPointerInteraction);
|
|
78
100
|
this.status = environment.createElement('div');
|
|
79
101
|
this.status.className = 'zenfg-inspector-graph-status';
|
|
80
102
|
this.status.hidden = true;
|
|
@@ -88,12 +110,33 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
88
110
|
this.retryButton.addEventListener('click', () => this.relayout());
|
|
89
111
|
this.status.append(this.statusMessage, this.retryButton);
|
|
90
112
|
this.host.replaceChildren(this.canvasHost, this.status, this.tooltip);
|
|
91
|
-
this.resizeSubscription = environment.observeResize?.(host, () =>
|
|
113
|
+
this.resizeSubscription = environment.observeResize?.(host, () => {
|
|
114
|
+
const visible = host.clientWidth > 0 && host.clientHeight > 0;
|
|
115
|
+
if (visible && !this.wasVisible) this.latestRequest?.onVisible?.();
|
|
116
|
+
this.wasVisible = visible;
|
|
117
|
+
this.core?.resize();
|
|
118
|
+
});
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
render(request: GraphRenderRequest): void {
|
|
95
122
|
if (this.destroyed) return;
|
|
123
|
+
if (request.theme) this.setTheme(request.theme);
|
|
124
|
+
if (this.latestRequest?.scene !== request.scene
|
|
125
|
+
|| (this.latestRequest?.selected && selectionKey(this.latestRequest.selected)) !== (request.selected && selectionKey(request.selected))) {
|
|
126
|
+
this.cancelGroupTap();
|
|
127
|
+
}
|
|
128
|
+
if ((!request.hovered && !this.tooltip.hidden) || this.latestRequest?.scene.contentKey !== request.scene.contentKey) {
|
|
129
|
+
this.hideTooltip();
|
|
130
|
+
this.core?.elements().removeClass('semantic-hover');
|
|
131
|
+
}
|
|
96
132
|
this.latestRequest = request;
|
|
133
|
+
if (request.reveal) {
|
|
134
|
+
this.pendingReveal = { request: request.reveal, contentKey: request.scene.contentKey, captureRevision: request.captureRevision };
|
|
135
|
+
} else if (this.pendingReveal && (this.pendingReveal.captureRevision !== request.captureRevision
|
|
136
|
+
|| this.pendingReveal.contentKey !== request.scene.contentKey
|
|
137
|
+
|| !request.selected || selectionKey(request.selected) !== selectionKey(this.pendingReveal.request.selection))) {
|
|
138
|
+
this.pendingReveal = undefined;
|
|
139
|
+
}
|
|
97
140
|
if (request.fit) {
|
|
98
141
|
this.fitTargetContentKey = request.scene.contentKey;
|
|
99
142
|
} else if (this.fitTargetContentKey !== request.scene.contentKey) {
|
|
@@ -122,16 +165,48 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
122
165
|
this.syncInteraction(request);
|
|
123
166
|
this.consumeFitIfCurrentTarget();
|
|
124
167
|
this.consumeAnchorIfCurrentTarget();
|
|
168
|
+
this.consumeReveal();
|
|
125
169
|
this.processedVersion = version;
|
|
126
170
|
return;
|
|
127
171
|
}
|
|
128
172
|
this.scheduleProcessing();
|
|
129
173
|
}
|
|
130
174
|
|
|
175
|
+
setTheme(theme: GraphVisualTheme): void {
|
|
176
|
+
if (this.destroyed || JSON.stringify(theme) === JSON.stringify(this.theme)) return;
|
|
177
|
+
const typographyChanged = theme.fontFamily !== this.theme.fontFamily || theme.fontSize !== this.theme.fontSize;
|
|
178
|
+
this.theme = theme;
|
|
179
|
+
this.core?.style().fromJson(createGraphStyles(theme)).update();
|
|
180
|
+
if (!typographyChanged) return;
|
|
181
|
+
this.geometryRevision++;
|
|
182
|
+
if (this.core && this.latestRequest && this.appliedScene) {
|
|
183
|
+
const selected = this.core.nodes('.semantic-selected').first();
|
|
184
|
+
const center = this.core.extent();
|
|
185
|
+
const target = selected.nonempty() ? selected : this.core.nodes().filter(node => !node.isParent()).sort((a, b) => {
|
|
186
|
+
const distance = (node: cytoscape.NodeSingular) => Math.hypot(node.position('x') - center.x1 - center.w / 2, node.position('y') - center.y1 - center.h / 2);
|
|
187
|
+
return distance(a as cytoscape.NodeSingular) - distance(b as cytoscape.NodeSingular);
|
|
188
|
+
}).first();
|
|
189
|
+
if (target.nonempty()) {
|
|
190
|
+
this.anchorElementId = target.id();
|
|
191
|
+
this.anchorTargetContentKey = this.latestRequest.scene.contentKey;
|
|
192
|
+
}
|
|
193
|
+
this.forceRelayout = true;
|
|
194
|
+
++this.requestVersion;
|
|
195
|
+
this.scheduleProcessing();
|
|
196
|
+
}
|
|
197
|
+
this.updateSemanticZoom(true);
|
|
198
|
+
}
|
|
199
|
+
|
|
131
200
|
resize(): void {
|
|
132
201
|
this.core?.resize();
|
|
133
202
|
}
|
|
134
203
|
|
|
204
|
+
cancelReveal(): void {
|
|
205
|
+
this.pendingReveal = undefined;
|
|
206
|
+
this.cancelGroupTap();
|
|
207
|
+
this.clearHover();
|
|
208
|
+
}
|
|
209
|
+
|
|
135
210
|
fit(): void {
|
|
136
211
|
const targetContentKey = this.latestRequest?.scene.contentKey;
|
|
137
212
|
if (targetContentKey) this.fitTargetContentKey = targetContentKey;
|
|
@@ -144,6 +219,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
144
219
|
|
|
145
220
|
relayout(): void {
|
|
146
221
|
if (!this.latestRequest || this.destroyed) return;
|
|
222
|
+
this.cancelGroupTap();
|
|
147
223
|
this.failedSceneContentKey = undefined;
|
|
148
224
|
this.forceRelayout = true;
|
|
149
225
|
this.fitTargetContentKey = this.latestRequest.scene.contentKey;
|
|
@@ -157,8 +233,10 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
157
233
|
destroy(): void {
|
|
158
234
|
if (this.destroyed) return;
|
|
159
235
|
this.destroyed = true;
|
|
236
|
+
this.cancelGroupTap();
|
|
160
237
|
this.requestVersion++;
|
|
161
238
|
this.resizeSubscription?.disconnect();
|
|
239
|
+
this.canvasHost.ownerDocument?.defaultView?.removeEventListener('blur', this.cancelPointerInteraction);
|
|
162
240
|
this.hideTooltip();
|
|
163
241
|
this.core?.destroy();
|
|
164
242
|
this.core = undefined;
|
|
@@ -192,7 +270,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
192
270
|
|
|
193
271
|
if (request.scene.nodes.length === 0) {
|
|
194
272
|
this.hideTooltip();
|
|
195
|
-
if (this.core) replaceElements(this.core, request.scene);
|
|
273
|
+
if (this.core) replaceElements(this.core, request.scene, this.theme);
|
|
196
274
|
this.appliedScene = request.scene;
|
|
197
275
|
this.forceRelayout = false;
|
|
198
276
|
if (this.fitTargetContentKey === request.scene.contentKey) this.fitTargetContentKey = undefined;
|
|
@@ -215,6 +293,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
215
293
|
this.syncInteraction(request);
|
|
216
294
|
this.consumeFitIfCurrentTarget();
|
|
217
295
|
this.consumeAnchorIfCurrentTarget();
|
|
296
|
+
this.consumeReveal();
|
|
218
297
|
this.hideStatus();
|
|
219
298
|
this.processedVersion = version;
|
|
220
299
|
continue;
|
|
@@ -235,16 +314,18 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
235
314
|
|
|
236
315
|
if (geometryChanged || this.forceRelayout) {
|
|
237
316
|
this.showStatus('layout', 'Laying out graph…');
|
|
238
|
-
const
|
|
317
|
+
const geometryRevision = this.geometryRevision;
|
|
318
|
+
const result = await this.environment.layoutScene(this.elk!, request.scene, this.theme);
|
|
319
|
+
if (geometryRevision !== this.geometryRevision) continue;
|
|
239
320
|
const layoutRequest = this.resolveCurrentRequest(version, request.scene.contentKey);
|
|
240
321
|
if (!layoutRequest) continue;
|
|
241
322
|
({ version, request } = layoutRequest);
|
|
242
|
-
if (topologyChanged || !this.appliedScene) replaceElements(core, request.scene);
|
|
243
|
-
else updateElementData(core, request.scene);
|
|
323
|
+
if (topologyChanged || !this.appliedScene) replaceElements(core, request.scene, this.theme);
|
|
324
|
+
else updateElementData(core, request.scene, this.theme);
|
|
244
325
|
applyGraphLayout(core, request.scene, result);
|
|
245
326
|
laidOut = true;
|
|
246
327
|
} else {
|
|
247
|
-
updateElementData(core, request.scene);
|
|
328
|
+
updateElementData(core, request.scene, this.theme);
|
|
248
329
|
}
|
|
249
330
|
|
|
250
331
|
syncExpandedGroupLabelGeometry(core);
|
|
@@ -264,6 +345,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
264
345
|
restoreViewport(core, previousViewport);
|
|
265
346
|
}
|
|
266
347
|
this.consumeAnchorIfCurrentTarget();
|
|
348
|
+
this.consumeReveal();
|
|
267
349
|
this.updateSemanticZoom(true);
|
|
268
350
|
this.hideStatus();
|
|
269
351
|
this.processedVersion = version;
|
|
@@ -301,7 +383,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
301
383
|
this.core = runtime.createCore({
|
|
302
384
|
container: this.canvasHost,
|
|
303
385
|
elements: [],
|
|
304
|
-
style: createGraphStyles(),
|
|
386
|
+
style: createGraphStyles(this.theme),
|
|
305
387
|
layout: { name: 'preset' },
|
|
306
388
|
minZoom: 0.03,
|
|
307
389
|
maxZoom: 4,
|
|
@@ -319,25 +401,33 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
319
401
|
if (!request) return;
|
|
320
402
|
const id = event.target.id() as GraphSceneElementId;
|
|
321
403
|
const selection = request.scene.interaction.selectionByElementId.get(id);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
404
|
+
const doubleTap = this.pendingGroupTap?.id === id;
|
|
405
|
+
this.cancelGroupTap();
|
|
406
|
+
if (selection?.kind !== 'group') {
|
|
407
|
+
if (selection) request.onSelect(selection);
|
|
408
|
+
} else if (doubleTap) {
|
|
325
409
|
request.onToggleGroup(selection.pathKey);
|
|
326
|
-
this.lastTap = undefined;
|
|
327
410
|
} else {
|
|
328
|
-
|
|
411
|
+
// Defer group selection so expansion does not replace the inspected resource.
|
|
412
|
+
this.pendingGroupTap = { id, timer: setTimeout(() => {
|
|
413
|
+
this.pendingGroupTap = undefined;
|
|
414
|
+
this.interactiveRequest()?.onSelect(selection);
|
|
415
|
+
}, 350) };
|
|
329
416
|
}
|
|
330
417
|
};
|
|
331
418
|
core.on('tap', 'node', selectElement);
|
|
332
419
|
core.on('tap', 'edge', selectElement);
|
|
333
420
|
core.on('tap', (event) => {
|
|
334
|
-
if (event.target === core) this.
|
|
421
|
+
if (event.target === core) this.cancelGroupTap();
|
|
335
422
|
});
|
|
336
423
|
const hoverElement = (event: cytoscape.EventObject) => {
|
|
337
424
|
const request = this.interactiveRequest();
|
|
338
|
-
if (!request) return;
|
|
425
|
+
if (!request || this.dragPanning) return;
|
|
339
426
|
const id = event.target.id() as GraphSceneElementId;
|
|
340
427
|
const selection = request.scene.interaction.selectionByElementId.get(id);
|
|
428
|
+
this.pointerInside = true;
|
|
429
|
+
this.pointerOverElement = !!selection;
|
|
430
|
+
this.updateCursor();
|
|
341
431
|
if (selection) request.onHover(selection);
|
|
342
432
|
const title = event.target.data('tooltip') as string | undefined;
|
|
343
433
|
if (title) {
|
|
@@ -351,12 +441,21 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
351
441
|
const moveTooltip = (event: cytoscape.EventObject) => this.positionTooltip(event.renderedPosition);
|
|
352
442
|
core.on('mousemove', 'node', moveTooltip);
|
|
353
443
|
core.on('mousemove', 'edge', moveTooltip);
|
|
354
|
-
const clearHover = () =>
|
|
355
|
-
this.hideTooltip();
|
|
356
|
-
this.latestRequest?.onHover(undefined);
|
|
357
|
-
};
|
|
444
|
+
const clearHover = () => this.clearHover();
|
|
358
445
|
core.on('mouseout', 'node', clearHover);
|
|
359
446
|
core.on('mouseout', 'edge', clearHover);
|
|
447
|
+
core.on('dragpan', () => {
|
|
448
|
+
if (this.dragPanning) return;
|
|
449
|
+
this.dragPanning = true;
|
|
450
|
+
this.cancelGroupTap();
|
|
451
|
+
this.clearHover();
|
|
452
|
+
});
|
|
453
|
+
core.on('tapend', (event) => {
|
|
454
|
+
this.dragPanning = false;
|
|
455
|
+
this.pointerOverElement = this.pointerInside && event.target !== core
|
|
456
|
+
&& !!this.interactiveRequest()?.scene.interaction.selectionByElementId.get(event.target.id());
|
|
457
|
+
this.updateCursor();
|
|
458
|
+
});
|
|
360
459
|
core.on('zoom', () => this.updateSemanticZoom());
|
|
361
460
|
}
|
|
362
461
|
|
|
@@ -371,7 +470,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
371
470
|
core.batch(() => {
|
|
372
471
|
core.elements().removeClass('semantic-selected semantic-hover');
|
|
373
472
|
if (request.hovered) {
|
|
374
|
-
for (const id of request.scene.interaction.
|
|
473
|
+
for (const id of request.scene.interaction.hoverElementIdsBySelection.get(selectionKey(request.hovered)) ?? []) {
|
|
375
474
|
core.getElementById(id).addClass('semantic-hover');
|
|
376
475
|
}
|
|
377
476
|
}
|
|
@@ -402,6 +501,23 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
402
501
|
this.anchorTargetContentKey = undefined;
|
|
403
502
|
}
|
|
404
503
|
|
|
504
|
+
private consumeReveal(): void {
|
|
505
|
+
const pending = this.pendingReveal;
|
|
506
|
+
const request = this.latestRequest;
|
|
507
|
+
if (!pending || !request || !this.core || this.targetNeedsApply()) return;
|
|
508
|
+
this.pendingReveal = undefined;
|
|
509
|
+
if (pending.captureRevision !== request.captureRevision || pending.contentKey !== request.scene.contentKey) return;
|
|
510
|
+
const ids = request.scene.interaction.primaryElementIdsBySelection.get(selectionKey(pending.request.selection)) ?? [];
|
|
511
|
+
const targets = this.core.collection();
|
|
512
|
+
for (const id of ids) targets.merge(this.core.getElementById(id));
|
|
513
|
+
if (!targets.length) return;
|
|
514
|
+
this.core.resize();
|
|
515
|
+
this.core.fit(targets, 64);
|
|
516
|
+
if (this.core.zoom() > 1.2) this.core.zoom(1.2);
|
|
517
|
+
this.core.center(targets);
|
|
518
|
+
this.updateSemanticZoom();
|
|
519
|
+
}
|
|
520
|
+
|
|
405
521
|
private targetNeedsApply(): boolean {
|
|
406
522
|
const target = this.latestRequest?.scene;
|
|
407
523
|
if (!target || !this.appliedScene) return target !== undefined;
|
|
@@ -411,7 +527,7 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
411
527
|
private updateSemanticZoom(force = false): void {
|
|
412
528
|
const core = this.core;
|
|
413
529
|
if (!core) return;
|
|
414
|
-
const overview = isOverviewGraphScale(core.zoom());
|
|
530
|
+
const overview = isOverviewGraphScale(core.zoom(), this.theme);
|
|
415
531
|
if (!force && overview === this.overview) return;
|
|
416
532
|
this.overview = overview;
|
|
417
533
|
core.batch(() => {
|
|
@@ -434,7 +550,22 @@ export class CytoscapeGraphRenderer implements GraphRenderer {
|
|
|
434
550
|
private hideTooltip(): void {
|
|
435
551
|
this.tooltip.hidden = true;
|
|
436
552
|
this.tooltip.textContent = '';
|
|
437
|
-
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private cancelGroupTap(): void {
|
|
556
|
+
if (this.pendingGroupTap) clearTimeout(this.pendingGroupTap.timer);
|
|
557
|
+
this.pendingGroupTap = undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private clearHover(): void {
|
|
561
|
+
this.pointerOverElement = false;
|
|
562
|
+
this.updateCursor();
|
|
563
|
+
this.hideTooltip();
|
|
564
|
+
this.latestRequest?.onHover(undefined);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private updateCursor(): void {
|
|
568
|
+
this.canvasHost.style.cursor = this.dragPanning ? 'grabbing' : this.pointerOverElement ? 'pointer' : 'grab';
|
|
438
569
|
}
|
|
439
570
|
|
|
440
571
|
private showStatus(kind: 'loading' | 'layout' | 'empty' | 'error', message: string, retry = false): void {
|
|
@@ -502,9 +633,9 @@ const browserGraphRendererEnvironment: GraphRendererEnvironment = {
|
|
|
502
633
|
},
|
|
503
634
|
};
|
|
504
635
|
|
|
505
|
-
function replaceElements(core: cytoscape.Core, scene: GraphScene): void {
|
|
636
|
+
function replaceElements(core: cytoscape.Core, scene: GraphScene, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): void {
|
|
506
637
|
const definitions: cytoscape.ElementDefinition[] = [
|
|
507
|
-
...scene.nodes.map(nodeDefinition),
|
|
638
|
+
...scene.nodes.map(node => nodeDefinition(node, theme)),
|
|
508
639
|
...scene.edges.map(edgeDefinition),
|
|
509
640
|
];
|
|
510
641
|
core.startBatch();
|
|
@@ -513,11 +644,11 @@ function replaceElements(core: cytoscape.Core, scene: GraphScene): void {
|
|
|
513
644
|
core.endBatch();
|
|
514
645
|
}
|
|
515
646
|
|
|
516
|
-
export function updateElementData(core: cytoscape.Core, scene: GraphScene): void {
|
|
647
|
+
export function updateElementData(core: cytoscape.Core, scene: GraphScene, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): void {
|
|
517
648
|
core.batch(() => {
|
|
518
649
|
for (const node of scene.nodes) {
|
|
519
650
|
const element = core.getElementById(node.id);
|
|
520
|
-
if (element.nonempty()) element.data(nodeRenderableData(node));
|
|
651
|
+
if (element.nonempty()) element.data(nodeRenderableData(node, theme));
|
|
521
652
|
}
|
|
522
653
|
for (const edge of scene.edges) {
|
|
523
654
|
const element = core.getElementById(edge.id);
|
|
@@ -526,32 +657,41 @@ export function updateElementData(core: cytoscape.Core, scene: GraphScene): void
|
|
|
526
657
|
});
|
|
527
658
|
}
|
|
528
659
|
|
|
529
|
-
function nodeDefinition(node: GraphSceneNode): cytoscape.ElementDefinition {
|
|
660
|
+
function nodeDefinition(node: GraphSceneNode, theme: GraphVisualTheme): cytoscape.ElementDefinition {
|
|
530
661
|
return {
|
|
531
662
|
group: 'nodes',
|
|
663
|
+
pannable: true,
|
|
532
664
|
data: {
|
|
533
665
|
id: node.id,
|
|
534
666
|
parent: node.parentId,
|
|
535
|
-
...nodeRenderableData(node),
|
|
667
|
+
...nodeRenderableData(node, theme),
|
|
536
668
|
},
|
|
537
669
|
};
|
|
538
670
|
}
|
|
539
671
|
|
|
540
|
-
function nodeRenderableData(node: GraphSceneNode): Record<string, unknown> {
|
|
541
|
-
const dimensions = nodeDimensions(node);
|
|
672
|
+
function nodeRenderableData(node: GraphSceneNode, theme: GraphVisualTheme): Record<string, unknown> {
|
|
673
|
+
const dimensions = nodeDimensions(node, theme);
|
|
542
674
|
const labelMaxWidth = node.kind === 'group' && node.collapsed
|
|
543
675
|
? expandedGroupLabelMaxWidth(dimensions.width)
|
|
544
|
-
: 160;
|
|
676
|
+
: 160 * theme.fontSize / GRAPH_GEOMETRY.baseFontSize;
|
|
677
|
+
const fitLabel = (label: string): string => node.kind !== 'group'
|
|
678
|
+
? fitGraphLabel(label, labelMaxWidth, theme)
|
|
679
|
+
: node.collapsed ? fitCollapsedGroupLabel(label, labelMaxWidth, theme) : label;
|
|
680
|
+
const detailLabel = fitLabel(node.label);
|
|
681
|
+
const labelFontSize = node.kind === 'group' && !node.collapsed
|
|
682
|
+
? theme.fontSize * 1.2
|
|
683
|
+
: graphLabelFontSize(detailLabel, labelMaxWidth, dimensions.height - 12 * theme.fontSize / GRAPH_GEOMETRY.baseFontSize, theme);
|
|
545
684
|
return {
|
|
546
685
|
kind: node.kind,
|
|
547
|
-
passKind: node.kind === 'pass'
|
|
548
|
-
resourceKind: node.kind === 'resource' ? node.resourceKind : undefined,
|
|
686
|
+
passKind: node.kind === 'pass' ? node.passKind : undefined,
|
|
687
|
+
resourceKind: (node.kind === 'resource' || node.kind === 'root') ? node.resourceKind : undefined,
|
|
549
688
|
collapsed: node.kind === 'group' && node.collapsed ? 1 : 0,
|
|
550
689
|
hasCulled: node.kind === 'group' && node.culledNodeCount > 0 ? 1 : 0,
|
|
551
690
|
depthBand: node.kind === 'group' ? node.depthBand : undefined,
|
|
552
|
-
detailLabel
|
|
553
|
-
|
|
554
|
-
|
|
691
|
+
detailLabel,
|
|
692
|
+
labelFontSize,
|
|
693
|
+
overviewLabel: fitLabel(node.overviewLabel),
|
|
694
|
+
displayLabel: detailLabel,
|
|
555
695
|
tooltip: node.title,
|
|
556
696
|
labelMaxWidth,
|
|
557
697
|
width: dimensions.width,
|
|
@@ -575,9 +715,6 @@ function edgeRenderableData(edge: GraphSceneEdge): Record<string, unknown> {
|
|
|
575
715
|
const detailLabel = graphEdgeDisplayLabel(edge);
|
|
576
716
|
return {
|
|
577
717
|
kind: edge.kind,
|
|
578
|
-
dependencyKind: edge.kind === 'dependency' ? edge.dependencyKind : undefined,
|
|
579
|
-
accessMode: edge.kind === 'access' ? edge.accessMode : undefined,
|
|
580
|
-
dashed: edge.kind === 'access' && edge.dashed ? 1 : 0,
|
|
581
718
|
detailLabel,
|
|
582
719
|
displayLabel: detailLabel,
|
|
583
720
|
tooltip: edge.title,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const MIN_DETAIL_WIDTH = 300;
|
|
2
|
+
const MAX_DETAIL_WIDTH = 480;
|
|
3
|
+
const MIN_MAIN_WIDTH = 520;
|
|
4
|
+
const DIVIDER_WIDTH = 8;
|
|
5
|
+
|
|
6
|
+
/** Owns the dock/drawer geometry and modal keyboard boundary for one workbench. */
|
|
7
|
+
export class DetailLayout {
|
|
8
|
+
private readonly backdrop = document.createElement('button');
|
|
9
|
+
private readonly divider = document.createElement('div');
|
|
10
|
+
private readonly resizeObserver: ResizeObserver | undefined;
|
|
11
|
+
private readonly abort = new AbortController();
|
|
12
|
+
private open = false;
|
|
13
|
+
private preferredWidth = MIN_DETAIL_WIDTH;
|
|
14
|
+
private previousFocus: HTMLElement | undefined;
|
|
15
|
+
private dragging = false;
|
|
16
|
+
private modal = false;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly workspace: HTMLElement,
|
|
20
|
+
private readonly main: HTMLElement,
|
|
21
|
+
private readonly aside: HTMLElement,
|
|
22
|
+
private readonly chrome: HTMLElement,
|
|
23
|
+
private readonly onClose: () => void,
|
|
24
|
+
) {
|
|
25
|
+
this.backdrop.type = 'button';
|
|
26
|
+
this.backdrop.className = 'zenfg-inspector-detail-backdrop';
|
|
27
|
+
this.backdrop.setAttribute('aria-label', 'Close selection inspector');
|
|
28
|
+
this.backdrop.tabIndex = -1;
|
|
29
|
+
this.backdrop.hidden = true;
|
|
30
|
+
this.backdrop.addEventListener('click', onClose);
|
|
31
|
+
this.divider.className = 'zenfg-inspector-detail-divider';
|
|
32
|
+
this.divider.setAttribute('role', 'separator');
|
|
33
|
+
this.divider.setAttribute('aria-label', 'Resize selection inspector');
|
|
34
|
+
this.divider.setAttribute('aria-orientation', 'vertical');
|
|
35
|
+
this.divider.tabIndex = 0;
|
|
36
|
+
this.divider.hidden = true;
|
|
37
|
+
workspace.insertBefore(this.divider, aside);
|
|
38
|
+
workspace.insertBefore(this.backdrop, aside);
|
|
39
|
+
this.divider.addEventListener('pointerdown', (event) => {
|
|
40
|
+
this.dragging = true;
|
|
41
|
+
this.divider.setPointerCapture(event.pointerId);
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
});
|
|
44
|
+
this.divider.addEventListener('pointermove', (event) => {
|
|
45
|
+
if (!this.dragging) return;
|
|
46
|
+
this.preferredWidth = this.clampWidth(workspace.getBoundingClientRect().right - event.clientX);
|
|
47
|
+
this.applyGeometry();
|
|
48
|
+
});
|
|
49
|
+
this.divider.addEventListener('pointerup', () => { this.dragging = false; });
|
|
50
|
+
this.divider.addEventListener('lostpointercapture', () => { this.dragging = false; });
|
|
51
|
+
this.divider.addEventListener('keydown', (event) => {
|
|
52
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
event.stopPropagation();
|
|
55
|
+
this.preferredWidth = this.clampWidth(event.key === 'Home' ? MIN_DETAIL_WIDTH : event.key === 'End' ? MAX_DETAIL_WIDTH
|
|
56
|
+
: this.preferredWidth + (event.key === 'ArrowLeft' ? 20 : -20));
|
|
57
|
+
this.applyGeometry();
|
|
58
|
+
});
|
|
59
|
+
workspace.parentElement?.addEventListener('keydown', this.handleKey, { signal: this.abort.signal });
|
|
60
|
+
this.resizeObserver = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(() => this.applyGeometry());
|
|
61
|
+
this.resizeObserver?.observe(workspace);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get isDrawer(): boolean { return this.modal; }
|
|
65
|
+
|
|
66
|
+
update(open: boolean): void {
|
|
67
|
+
const changed = open !== this.open;
|
|
68
|
+
if (changed && open) this.previousFocus = this.workspace.ownerDocument.activeElement as HTMLElement | undefined;
|
|
69
|
+
this.open = open;
|
|
70
|
+
this.applyGeometry();
|
|
71
|
+
if (changed && !open) this.restoreFocus();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
destroy(): void {
|
|
75
|
+
this.abort.abort();
|
|
76
|
+
this.resizeObserver?.disconnect();
|
|
77
|
+
this.main.inert = false;
|
|
78
|
+
this.chrome.inert = false;
|
|
79
|
+
for (const sibling of this.chrome.parentElement?.children ?? []) if (sibling !== this.workspace) (sibling as HTMLElement).inert = false;
|
|
80
|
+
if (this.open && this.modal) this.restoreFocus();
|
|
81
|
+
this.open = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private restoreFocus(): void {
|
|
85
|
+
const isAvailable = (element: HTMLElement | undefined): element is HTMLElement => {
|
|
86
|
+
if (!element?.isConnected || element === element.ownerDocument.body || element.closest('[hidden], [inert]') || element.matches(':disabled')) return false;
|
|
87
|
+
for (let parent: HTMLElement | null = element; parent; parent = parent.parentElement) if (parent.inert) return false;
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
const target = isAvailable(this.previousFocus) ? this.previousFocus
|
|
91
|
+
: this.chrome.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]') ?? undefined;
|
|
92
|
+
if (isAvailable(target)) target.focus({ preventScroll: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private clampWidth(width: number): number {
|
|
96
|
+
const available = this.workspace.getBoundingClientRect().width || 1024;
|
|
97
|
+
return Math.max(MIN_DETAIL_WIDTH, Math.min(MAX_DETAIL_WIDTH, available - MIN_MAIN_WIDTH - DIVIDER_WIDTH, width));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private applyGeometry(): void {
|
|
101
|
+
const wasModal = this.modal;
|
|
102
|
+
const available = this.workspace.getBoundingClientRect().width || 1024;
|
|
103
|
+
this.modal = available < MIN_MAIN_WIDTH + DIVIDER_WIDTH + MIN_DETAIL_WIDTH;
|
|
104
|
+
if (this.modal && !wasModal && this.open && !this.aside.contains(document.activeElement)) this.previousFocus = this.workspace.ownerDocument.activeElement as HTMLElement | undefined;
|
|
105
|
+
const width = this.clampWidth(this.preferredWidth);
|
|
106
|
+
this.workspace.style.setProperty('--fgd-detail-width', `${width}px`);
|
|
107
|
+
this.workspace.classList.toggle('detail-drawer', this.modal);
|
|
108
|
+
this.workspace.classList.toggle('inspector-open', this.open);
|
|
109
|
+
this.divider.hidden = !this.open || this.modal;
|
|
110
|
+
this.divider.setAttribute('aria-valuemin', String(MIN_DETAIL_WIDTH));
|
|
111
|
+
this.divider.setAttribute('aria-valuemax', String(Math.max(MIN_DETAIL_WIDTH, Math.min(MAX_DETAIL_WIDTH, available - MIN_MAIN_WIDTH - DIVIDER_WIDTH))));
|
|
112
|
+
this.divider.setAttribute('aria-valuenow', String(Math.round(width)));
|
|
113
|
+
this.backdrop.hidden = !this.open || !this.modal;
|
|
114
|
+
this.main.inert = this.open && this.modal;
|
|
115
|
+
this.chrome.inert = this.open && this.modal;
|
|
116
|
+
for (const sibling of this.chrome.parentElement?.children ?? []) if (sibling !== this.workspace) (sibling as HTMLElement).inert = this.open && this.modal;
|
|
117
|
+
if (this.open && this.modal) {
|
|
118
|
+
this.aside.setAttribute('role', 'dialog');
|
|
119
|
+
this.aside.setAttribute('aria-modal', 'true');
|
|
120
|
+
if (!this.aside.contains(document.activeElement)) this.focusable()[0]?.focus();
|
|
121
|
+
} else {
|
|
122
|
+
this.aside.removeAttribute('role');
|
|
123
|
+
this.aside.removeAttribute('aria-modal');
|
|
124
|
+
}
|
|
125
|
+
if (wasModal !== this.modal && this.open) this.workspace.dispatchEvent(new Event('detail-layout-change'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private focusable(): HTMLElement[] {
|
|
129
|
+
return Array.from(this.aside.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), select:not(:disabled), summary, [tabindex="0"]'))
|
|
130
|
+
.filter((element) => {
|
|
131
|
+
if (element.closest('[hidden]') || element.tabIndex < 0 && element.tagName !== 'SUMMARY') return false;
|
|
132
|
+
for (let parent = element.parentElement; parent && parent !== this.aside; parent = parent.parentElement) {
|
|
133
|
+
if (parent.tagName === 'DETAILS' && !parent.hasAttribute('open') && !parent.querySelector(':scope > summary')?.contains(element)) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private readonly handleKey = (event: KeyboardEvent): void => {
|
|
140
|
+
if (event.defaultPrevented || !this.open) return;
|
|
141
|
+
if (event.key === 'Escape') {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
this.onClose();
|
|
145
|
+
} else if (this.modal && event.key === 'Tab') {
|
|
146
|
+
const items = this.focusable();
|
|
147
|
+
if (!items.length) { event.preventDefault(); return; }
|
|
148
|
+
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
149
|
+
if (event.shiftKey && index <= 0 || !event.shiftKey && (index === items.length - 1 || index < 0)) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
(event.shiftKey ? items.at(-1) : items[0])?.focus();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const PANEL_DETAIL_CSS = `
|
|
2
|
+
.zenfg-inspector-detail-actions,
|
|
3
|
+
.zenfg-inspector-relation-links,
|
|
4
|
+
.zenfg-inspector-copy-id,
|
|
5
|
+
.zenfg-inspector-raw-toolbar {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
gap: 6px 10px;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
}
|
|
12
|
+
.zenfg-inspector-detail-actions { margin-bottom: var(--fgd-space-3); }
|
|
13
|
+
.zenfg-inspector-detail-actions:empty { display: none; }
|
|
14
|
+
.zenfg-inspector-inline-action,
|
|
15
|
+
.zenfg-inspector-copy-id button,
|
|
16
|
+
.zenfg-inspector-raw-toolbar button,
|
|
17
|
+
.zenfg-inspector-capture-summary section > button,
|
|
18
|
+
.zenfg-inspector-graph-search-results > button {
|
|
19
|
+
padding: 5px var(--fgd-space-2); border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm);
|
|
20
|
+
color: var(--fgd-text-secondary); background: var(--fgd-surface);
|
|
21
|
+
font: var(--fgd-font-size)/1.4 var(--fgd-font-ui); cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
.zenfg-inspector-inline-action:hover,
|
|
24
|
+
.zenfg-inspector-copy-id button:hover,
|
|
25
|
+
.zenfg-inspector-raw-toolbar button:hover,
|
|
26
|
+
.zenfg-inspector-capture-summary section > button:hover,
|
|
27
|
+
.zenfg-inspector-graph-search-results > button:hover { color: var(--fgd-text); background: var(--fgd-surface-hover); }
|
|
28
|
+
.zenfg-inspector-raw-toolbar input {
|
|
29
|
+
padding: 6px var(--fgd-space-2); border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm);
|
|
30
|
+
color: var(--fgd-text); background: var(--fgd-panel); font: var(--fgd-font-size) var(--fgd-font-ui);
|
|
31
|
+
}
|
|
32
|
+
.zenfg-inspector-inspector-summary dd,
|
|
33
|
+
.zenfg-inspector-relation-entry,
|
|
34
|
+
.zenfg-inspector-detail-diagnostics { min-width: 0; overflow-wrap: anywhere; }
|
|
35
|
+
.zenfg-inspector-inspector-summary { grid-template-columns: 108px minmax(0, 1fr); font-size: var(--fgd-font-size-small); }
|
|
36
|
+
.zenfg-inspector-inspector-summary dd { font-family: var(--fgd-font-ui); }
|
|
37
|
+
.zenfg-inspector-inspector-relations .zenfg-inspector-relation-button,
|
|
38
|
+
.zenfg-inspector-relation-entry > span { font: var(--fgd-font-size)/1.5 var(--fgd-font-ui); }
|
|
39
|
+
.zenfg-inspector-relation-links .zenfg-inspector-relation-button { flex: 1 1 130px; min-width: 0; }
|
|
40
|
+
.zenfg-inspector-relation-links .zenfg-inspector-inline-action { flex: 0 0 auto; }
|
|
41
|
+
.zenfg-inspector-copy-id code { overflow-wrap: anywhere; }
|
|
42
|
+
.zenfg-inspector-inspector-content > details { margin-top: 14px; }
|
|
43
|
+
.zenfg-inspector-inspector-content summary { cursor: pointer; }
|
|
44
|
+
.zenfg-inspector-inspector-content h3 { font-size: var(--fgd-font-size-small); margin: 14px 0 6px; }
|
|
45
|
+
.zenfg-inspector-detail-diagnostics article { padding: var(--fgd-space-2) 0; border-bottom: 1px solid var(--fgd-border); }
|
|
46
|
+
.zenfg-inspector-detail-diagnostics p { margin: 6px 0; white-space: pre-wrap; }
|
|
47
|
+
.zenfg-inspector-detail-diagnostics article[data-severity="error"] > strong { color: var(--fgd-danger); }
|
|
48
|
+
.zenfg-inspector-detail-diagnostics article[data-severity="warning"] > strong { color: var(--fgd-warning); }
|
|
49
|
+
.zenfg-inspector-raw-view { min-width: 0; display: grid; gap: 10px; }
|
|
50
|
+
.zenfg-inspector-raw-path { overflow-wrap: anywhere; font-size: var(--fgd-font-size-small); }
|
|
51
|
+
.zenfg-inspector-raw-toolbar input { flex: 1 1 170px; min-width: 0; }
|
|
52
|
+
.zenfg-inspector-inspector .zenfg-inspector-raw-detail { white-space: normal; overflow: visible; overflow-wrap: anywhere; font: var(--fgd-font-size-small)/1.55 var(--fgd-font-mono); }
|
|
53
|
+
.zenfg-inspector-raw-children { padding-left: var(--fgd-space-3); border-left: 1px solid var(--fgd-border); }
|
|
54
|
+
.zenfg-inspector-raw-detail summary, .zenfg-inspector-raw-leaf { padding: 3px 0; }
|
|
55
|
+
.zenfg-inspector-inspector-content :is(button, input, summary):focus-visible { outline: 2px solid var(--fgd-accent); outline-offset: 2px; }
|
|
56
|
+
`;
|