@zenfg/inspector 0.1.0-beta.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/GUIDE.md +182 -0
  2. package/README.md +61 -89
  3. package/THEMING.md +124 -0
  4. package/dist/FrameGraphInspector.d.ts +23 -4
  5. package/dist/FrameGraphInspector.d.ts.map +1 -1
  6. package/dist/FrameGraphInspector.js +137 -29
  7. package/dist/FrameGraphInspector.js.map +1 -1
  8. package/dist/capture.d.ts +5 -0
  9. package/dist/capture.d.ts.map +1 -0
  10. package/dist/capture.js +2 -0
  11. package/dist/capture.js.map +1 -0
  12. package/dist/debugCaptureModel.d.ts +19 -0
  13. package/dist/debugCaptureModel.d.ts.map +1 -1
  14. package/dist/debugCaptureModel.js +36 -2
  15. package/dist/debugCaptureModel.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/panelAliasAnalysis.d.ts +1 -0
  20. package/dist/panelAliasAnalysis.d.ts.map +1 -1
  21. package/dist/panelAliasAnalysis.js +23 -54
  22. package/dist/panelAliasAnalysis.js.map +1 -1
  23. package/dist/panelCytoscapeGraphRenderer.d.ts +15 -2
  24. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  25. package/dist/panelCytoscapeGraphRenderer.js +145 -23
  26. package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
  27. package/dist/panelDetailLayout.d.ts +27 -0
  28. package/dist/panelDetailLayout.d.ts.map +1 -0
  29. package/dist/panelDetailLayout.js +174 -0
  30. package/dist/panelDetailLayout.js.map +1 -0
  31. package/dist/panelDetailStyles.d.ts +2 -0
  32. package/dist/panelDetailStyles.d.ts.map +1 -0
  33. package/dist/panelDetailStyles.js +57 -0
  34. package/dist/panelDetailStyles.js.map +1 -0
  35. package/dist/panelDiagnosticsView.d.ts +10 -1
  36. package/dist/panelDiagnosticsView.d.ts.map +1 -1
  37. package/dist/panelDiagnosticsView.js +166 -57
  38. package/dist/panelDiagnosticsView.js.map +1 -1
  39. package/dist/panelDomHelpers.d.ts.map +1 -1
  40. package/dist/panelDomHelpers.js +6 -5
  41. package/dist/panelDomHelpers.js.map +1 -1
  42. package/dist/panelGraphLayout.d.ts +3 -2
  43. package/dist/panelGraphLayout.d.ts.map +1 -1
  44. package/dist/panelGraphLayout.js +6 -5
  45. package/dist/panelGraphLayout.js.map +1 -1
  46. package/dist/panelGraphRenderer.d.ts +10 -0
  47. package/dist/panelGraphRenderer.d.ts.map +1 -1
  48. package/dist/panelGraphRenderer.js.map +1 -1
  49. package/dist/panelGraphScene.d.ts +1 -0
  50. package/dist/panelGraphScene.d.ts.map +1 -1
  51. package/dist/panelGraphScene.js +69 -32
  52. package/dist/panelGraphScene.js.map +1 -1
  53. package/dist/panelGraphSearch.d.ts +16 -0
  54. package/dist/panelGraphSearch.d.ts.map +1 -0
  55. package/dist/panelGraphSearch.js +93 -0
  56. package/dist/panelGraphSearch.js.map +1 -0
  57. package/dist/panelGraphView.d.ts +82 -0
  58. package/dist/panelGraphView.d.ts.map +1 -1
  59. package/dist/panelGraphView.js +15 -4
  60. package/dist/panelGraphView.js.map +1 -1
  61. package/dist/panelGraphVisuals.d.ts +14 -6
  62. package/dist/panelGraphVisuals.d.ts.map +1 -1
  63. package/dist/panelGraphVisuals.js +126 -51
  64. package/dist/panelGraphVisuals.js.map +1 -1
  65. package/dist/panelInspectorView.d.ts +8 -0
  66. package/dist/panelInspectorView.d.ts.map +1 -1
  67. package/dist/panelInspectorView.js +242 -48
  68. package/dist/panelInspectorView.js.map +1 -1
  69. package/dist/panelListStyles.d.ts +3 -0
  70. package/dist/panelListStyles.d.ts.map +1 -0
  71. package/dist/panelListStyles.js +155 -0
  72. package/dist/panelListStyles.js.map +1 -0
  73. package/dist/panelMemoryStyles.d.ts +3 -0
  74. package/dist/panelMemoryStyles.d.ts.map +1 -0
  75. package/dist/panelMemoryStyles.js +42 -0
  76. package/dist/panelMemoryStyles.js.map +1 -0
  77. package/dist/panelMemoryView.d.ts +13 -1
  78. package/dist/panelMemoryView.d.ts.map +1 -1
  79. package/dist/panelMemoryView.js +161 -41
  80. package/dist/panelMemoryView.js.map +1 -1
  81. package/dist/panelPassesView.d.ts +15 -0
  82. package/dist/panelPassesView.d.ts.map +1 -1
  83. package/dist/panelPassesView.js +202 -72
  84. package/dist/panelPassesView.js.map +1 -1
  85. package/dist/panelRawView.d.ts +15 -0
  86. package/dist/panelRawView.d.ts.map +1 -0
  87. package/dist/panelRawView.js +105 -0
  88. package/dist/panelRawView.js.map +1 -0
  89. package/dist/panelResourcesView.d.ts +8 -0
  90. package/dist/panelResourcesView.d.ts.map +1 -1
  91. package/dist/panelResourcesView.js +62 -31
  92. package/dist/panelResourcesView.js.map +1 -1
  93. package/dist/panelSelection.d.ts +9 -1
  94. package/dist/panelSelection.d.ts.map +1 -1
  95. package/dist/panelSelection.js +34 -4
  96. package/dist/panelSelection.js.map +1 -1
  97. package/dist/panelTheme.d.ts +13 -0
  98. package/dist/panelTheme.d.ts.map +1 -0
  99. package/dist/panelTheme.js +104 -0
  100. package/dist/panelTheme.js.map +1 -0
  101. package/dist/panelTypes.d.ts +10 -1
  102. package/dist/panelTypes.d.ts.map +1 -1
  103. package/dist/panelTypes.js.map +1 -1
  104. package/dist/panelVisualTheme.d.ts +147 -50
  105. package/dist/panelVisualTheme.d.ts.map +1 -1
  106. package/dist/panelVisualTheme.js +34 -44
  107. package/dist/panelVisualTheme.js.map +1 -1
  108. package/dist/panelWorkbenchHelpers.d.ts +12 -1
  109. package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
  110. package/dist/panelWorkbenchHelpers.js +62 -1
  111. package/dist/panelWorkbenchHelpers.js.map +1 -1
  112. package/dist/panelWorkbenchStyles.d.ts +2 -0
  113. package/dist/panelWorkbenchStyles.d.ts.map +1 -0
  114. package/dist/panelWorkbenchStyles.js +101 -0
  115. package/dist/panelWorkbenchStyles.js.map +1 -0
  116. package/dist/panelWorkbenchView.d.ts +16 -0
  117. package/dist/panelWorkbenchView.d.ts.map +1 -1
  118. package/dist/panelWorkbenchView.js +228 -50
  119. package/dist/panelWorkbenchView.js.map +1 -1
  120. package/dist/query.js.map +1 -1
  121. package/dist/styles.d.ts.map +1 -1
  122. package/dist/styles.js +147 -195
  123. package/dist/styles.js.map +1 -1
  124. package/dist/theme.d.ts +22 -0
  125. package/dist/theme.d.ts.map +1 -0
  126. package/dist/theme.js +10 -0
  127. package/dist/theme.js.map +1 -0
  128. package/dist/themeDefinitions.d.ts +251 -0
  129. package/dist/themeDefinitions.d.ts.map +1 -0
  130. package/dist/themeDefinitions.js +96 -0
  131. package/dist/themeDefinitions.js.map +1 -0
  132. package/dist/themePalette.d.ts +66 -0
  133. package/dist/themePalette.d.ts.map +1 -0
  134. package/dist/themePalette.js +37 -0
  135. package/dist/themePalette.js.map +1 -0
  136. package/dist/themes.css +165 -0
  137. package/package.json +44 -10
  138. package/src/FrameGraphInspector.ts +130 -31
  139. package/src/capture.ts +4 -0
  140. package/src/debugCaptureModel.ts +53 -2
  141. package/src/index.ts +6 -2
  142. package/src/panelAliasAnalysis.ts +24 -70
  143. package/src/panelCytoscapeGraphRenderer.ts +140 -23
  144. package/src/panelDetailLayout.ts +155 -0
  145. package/src/panelDetailStyles.ts +56 -0
  146. package/src/panelDiagnosticsView.ts +166 -85
  147. package/src/panelDomHelpers.ts +4 -5
  148. package/src/panelGraphLayout.ts +6 -5
  149. package/src/panelGraphRenderer.ts +7 -0
  150. package/src/panelGraphScene.ts +68 -30
  151. package/src/panelGraphSearch.ts +96 -0
  152. package/src/panelGraphView.ts +14 -4
  153. package/src/panelGraphVisuals.ts +122 -52
  154. package/src/panelInspectorView.ts +227 -48
  155. package/src/panelListStyles.ts +154 -0
  156. package/src/panelMemoryStyles.ts +41 -0
  157. package/src/panelMemoryView.ts +178 -53
  158. package/src/panelPassesView.ts +190 -137
  159. package/src/panelRawView.ts +99 -0
  160. package/src/panelResourcesView.ts +69 -56
  161. package/src/panelSelection.ts +37 -5
  162. package/src/panelTheme.ts +85 -0
  163. package/src/panelTypes.ts +7 -1
  164. package/src/panelVisualTheme.ts +38 -49
  165. package/src/panelWorkbenchHelpers.ts +60 -2
  166. package/src/panelWorkbenchStyles.ts +100 -0
  167. package/src/panelWorkbenchView.ts +201 -52
  168. package/src/query.ts +11 -11
  169. package/src/styles.ts +148 -195
  170. package/src/theme.ts +24 -0
  171. package/src/themeDefinitions.ts +103 -0
  172. package/src/themePalette.ts +44 -0
@@ -1,69 +1,60 @@
1
- import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
1
+ import type { FrameGraphDebugResource, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
2
  import { createCell, labelResource } from './panelDomHelpers.ts';
3
3
  import type { Selection } from './panelTypes.ts';
4
4
  import {
5
- createEmptyTableRow,
6
- createKindCell,
7
- createFilterSelect,
8
- createSearchInput,
9
- createSelectionCell,
10
- createTableScroller,
11
- createViewToolbar,
12
- formatEstimatedBytes,
13
- formatResourceDescriptor,
14
- groupPath,
15
- registerSelectable,
16
- resourceAccessCounts,
17
- type WorkbenchCallbacks,
18
- updateSelectedRows,
5
+ createEmptyTableRow, createKindCell, createFilterSelect, createSearchInput, createSelectionCell,
6
+ createTableScroller, createViewToolbar, formatEstimatedBytes, groupPath, registerSelectable,
7
+ resourceAccessCounts, selectionKey, type WorkbenchCallbacks, updateSelectedRows,
19
8
  } from './panelWorkbenchHelpers.ts';
20
9
 
10
+ type ResourceSort = 'name' | 'size' | 'lifetime';
11
+
21
12
  export class ResourcesView {
22
13
  readonly root = document.createElement('section');
23
14
  private readonly table = createTableScroller([
24
- { label: 'Resource / Group' },
15
+ { label: 'Name' },
25
16
  { label: 'Type / Origin', column: 'kind' },
26
- { label: 'Descriptor', column: 'code' },
27
17
  { label: 'Estimated', column: 'numeric' },
28
18
  { label: 'Lifetime', column: 'code' },
29
- { label: 'Physical', column: 'numeric' },
30
19
  { label: 'R / W', column: 'numeric' },
31
20
  ]);
32
21
  private readonly rows = new Map<string, HTMLElement[]>();
22
+ private readonly count = document.createElement('span');
23
+ private readonly searchInput: HTMLInputElement;
24
+ private readonly kindSelect: HTMLSelectElement;
25
+ private readonly originSelect: HTMLSelectElement;
33
26
  private snapshot: FrameGraphDebugViewModel | undefined;
34
27
  private selected: Selection | undefined;
35
28
  private search = '';
36
29
  private kind = 'all';
37
30
  private origin = 'all';
31
+ private sort: ResourceSort = 'name';
38
32
 
39
33
  constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
40
34
  this.root.className = 'zenfg-inspector-view zenfg-inspector-resources-view';
41
35
  this.root.id = `${idPrefix}-view-resources`;
42
36
  this.root.setAttribute('role', 'tabpanel');
43
- const toolbar = createViewToolbar('Resource filters');
44
- toolbar.append(
45
- createSearchInput('Search resource or group', this.search, (value) => {
46
- this.search = value.trim().toLocaleLowerCase();
47
- this.renderRows();
48
- }),
49
- createFilterSelect('Resource kind', this.kind, [
50
- ['all', 'All kinds'],
51
- ['texture', 'Texture'],
52
- ['buffer', 'Buffer'],
53
- ], (value) => {
54
- this.kind = value;
55
- this.renderRows();
56
- }),
57
- createFilterSelect('Resource origin', this.origin, [
58
- ['all', 'All origins'],
59
- ['transient', 'Transient'],
60
- ['imported', 'Imported'],
61
- ['swapchain', 'Swapchain'],
62
- ], (value) => {
63
- this.origin = value;
64
- this.renderRows();
65
- }),
66
- );
37
+ const toolbar = createViewToolbar('Resource filters and sorting');
38
+ this.searchInput = createSearchInput('Search resource, ID or group', this.search, (value) => {
39
+ this.search = value.trim().toLocaleLowerCase();
40
+ this.renderRows();
41
+ });
42
+ this.kindSelect = createFilterSelect('Resource kind', this.kind, [
43
+ ['all', 'All kinds'], ['texture', 'Texture'], ['buffer', 'Buffer'],
44
+ ], (value) => { this.kind = value; this.renderRows(); });
45
+ this.originSelect = createFilterSelect('Resource origin', this.origin, [
46
+ ['all', 'All origins'], ['transient', 'Transient'], ['imported', 'Imported'], ['surface', 'Surface'],
47
+ ], (value) => { this.origin = value; this.renderRows(); });
48
+ const sort = createFilterSelect('Sort resources', this.sort, [
49
+ ['name', 'Name'], ['size', 'Size: largest first'], ['lifetime', 'Lifetime: first use'],
50
+ ], (value) => { this.sort = value as ResourceSort; this.renderRows(); });
51
+ const clear = document.createElement('button');
52
+ clear.type = 'button';
53
+ clear.textContent = 'Clear filters';
54
+ clear.addEventListener('click', () => { this.clearFilters(); this.renderRows(); });
55
+ this.count.className = 'zenfg-inspector-result-count';
56
+ this.count.setAttribute('role', 'status');
57
+ toolbar.append(this.searchInput, this.kindSelect, this.originSelect, sort, clear, this.count);
67
58
  this.root.append(toolbar, this.table.scroller);
68
59
  }
69
60
 
@@ -77,6 +68,18 @@ export class ResourcesView {
77
68
  updateSelectedRows(this.rows, selected);
78
69
  }
79
70
 
71
+ reveal(selection: Selection): void {
72
+ if (selection.kind !== 'resource') return;
73
+ this.clearFilters();
74
+ this.renderRows();
75
+ this.rows.get(selectionKey(selection))?.[0]?.scrollIntoView?.({ block: 'nearest' });
76
+ }
77
+
78
+ private clearFilters(): void {
79
+ this.search = ''; this.kind = 'all'; this.origin = 'all';
80
+ this.searchInput.value = ''; this.kindSelect.value = 'all'; this.originSelect.value = 'all';
81
+ }
82
+
80
83
  private renderRows(): void {
81
84
  const snapshot = this.snapshot;
82
85
  if (!snapshot) return;
@@ -85,31 +88,41 @@ export class ResourcesView {
85
88
  const resources = snapshot.resources.filter((resource) => {
86
89
  if (this.kind !== 'all' && resource.kind !== this.kind) return false;
87
90
  if (this.origin !== 'all' && resource.origin !== this.origin) return false;
88
- if (this.search.length === 0) return true;
89
- return `${labelResource(resource)} ${groupPath(snapshot, resource.debugGroupId)}`
91
+ return !this.search || `${labelResource(resource)} ${resource.id} ${groupPath(snapshot, resource.debugGroupId)}`
90
92
  .toLocaleLowerCase().includes(this.search);
91
- });
93
+ }).sort((a, b) => this.compareResources(a, b));
94
+ this.count.textContent = `${resources.length} / ${snapshot.resources.length} resources`;
92
95
  for (const resource of resources) {
93
96
  const selection: Selection = { kind: 'resource', id: resource.id };
94
97
  const row = document.createElement('tr');
95
98
  registerSelectable(this.rows, row, selection, this.callbacks);
96
99
  const [reads, writes] = resourceAccessCounts(snapshot, resource.id);
97
100
  const resourceCell = createSelectionCell(labelResource(resource), selection, this.callbacks);
98
- const group = document.createElement('small');
99
- group.textContent = groupPath(snapshot, resource.debugGroupId);
100
- resourceCell.appendChild(group);
101
- row.append(
102
- resourceCell,
101
+ resourceCell.title = resource.id;
102
+ if (resource.debugGroupId !== undefined) {
103
+ const group = document.createElement('small');
104
+ group.textContent = groupPath(snapshot, resource.debugGroupId);
105
+ resourceCell.appendChild(group);
106
+ }
107
+ row.append(resourceCell,
103
108
  createKindCell(resource.kind, `${resource.kind} · ${resource.origin}`),
104
- createCell(formatResourceDescriptor(resource), { column: 'code' }),
105
109
  createCell(formatEstimatedBytes(resource.estimatedByteSize), { column: 'numeric' }),
106
- createCell(resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : '-', { column: 'code' }),
107
- createCell(resource.physicalResourceId === undefined ? '-' : `#${resource.physicalResourceId}`, { column: 'numeric' }),
108
- createCell(`${reads} / ${writes}`, { column: 'numeric' }),
109
- );
110
+ createCell(resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : 'Unknown', { column: 'code' }),
111
+ createCell(`${reads} / ${writes}`, { column: 'numeric' }));
110
112
  this.table.body.appendChild(row);
111
113
  }
112
- if (resources.length === 0) this.table.body.appendChild(createEmptyTableRow(7, 'No resources match the current filters.'));
114
+ if (resources.length === 0) this.table.body.appendChild(createEmptyTableRow(5, 'No resources match the current filters.'));
113
115
  updateSelectedRows(this.rows, this.selected);
114
116
  }
117
+
118
+ private compareResources(a: FrameGraphDebugResource, b: FrameGraphDebugResource): number {
119
+ const byName = () => labelResource(a).localeCompare(labelResource(b)) || a.id.localeCompare(b.id);
120
+ const knownFirst = (left: number | undefined, right: number | undefined, descending: boolean): number => (
121
+ left === undefined ? right === undefined ? 0 : 1
122
+ : right === undefined ? -1 : descending ? right - left : left - right
123
+ );
124
+ if (this.sort === 'size') return knownFirst(a.estimatedByteSize, b.estimatedByteSize, true) || byName();
125
+ if (this.sort === 'lifetime') return knownFirst(a.lifetime?.firstUse, b.lifetime?.firstUse, false) || byName();
126
+ return byName();
127
+ }
115
128
  }
@@ -1,4 +1,4 @@
1
- import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
1
+ import { rootKey, type FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
2
  import type { Selection } from './panelTypes.ts';
3
3
 
4
4
  export function resolveSelectedDetail(
@@ -28,7 +28,7 @@ export function resolveSelectedDetail(
28
28
  case 'root':
29
29
  return snapshot.roots.find((root) => root.key === selected.key);
30
30
  case 'culled': {
31
- const culled = snapshot.culledNodes[selected.index];
31
+ const culled = snapshot.culledById.get(selected.id);
32
32
  return culled ? {
33
33
  ...culled,
34
34
  node: {
@@ -56,7 +56,7 @@ export function selectionExists(snapshot: FrameGraphDebugViewModel, selected: Se
56
56
  case 'root':
57
57
  return snapshot.roots.filter((root) => root.key === selected.key).length === 1;
58
58
  case 'culled':
59
- return isValidIndex(selected.index, snapshot.culledNodes.length);
59
+ return snapshot.culledById.has(selected.id);
60
60
  case 'allocation':
61
61
  return snapshot.allocationById.has(selected.id);
62
62
  case 'segment':
@@ -94,6 +94,38 @@ function debugGroupPath(snapshot: FrameGraphDebugViewModel, groupId: string | un
94
94
  return snapshot.groupById.get(groupId)?.path.join(' / ') ?? `#${groupId}`;
95
95
  }
96
96
 
97
- function isValidIndex(index: number, length: number): boolean {
98
- return Number.isInteger(index) && index >= 0 && index < length;
97
+ /** Resolve identity again after a capture, including retained/culled transitions. */
98
+ export function resolveNodeSelection(snapshot: FrameGraphDebugViewModel, id: string): Selection | undefined {
99
+ if (snapshot.nodeById.has(id)) return { kind: 'node', id };
100
+ if (snapshot.culledById.has(id)) return { kind: 'culled', id };
101
+ return undefined;
102
+ }
103
+
104
+ export type SelectedCanonicalDetail = { readonly path: string; readonly value: unknown };
105
+
106
+ /** Raw shows the canonical source object, never the presentation model. */
107
+ export function resolveSelectedCanonicalDetail(
108
+ snapshot: FrameGraphDebugViewModel,
109
+ selected: Selection | undefined,
110
+ ): SelectedCanonicalDetail | undefined {
111
+ if (!selected) return undefined;
112
+ const protocol = snapshot.protocol;
113
+ function at<T>(values: readonly T[], path: string, predicate: (value: T) => boolean): SelectedCanonicalDetail | undefined {
114
+ const index = values.findIndex(predicate);
115
+ return index < 0 ? undefined : { path: `${path}[${index}]`, value: values[index] };
116
+ }
117
+ switch (selected.kind) {
118
+ case 'node':
119
+ case 'culled': return at(protocol.graph.nodes, '$.graph.nodes', (node) => node.id === selected.id);
120
+ case 'resource': return at(protocol.graph.resources, '$.graph.resources', (resource) => resource.id === selected.id);
121
+ case 'group': {
122
+ const id = snapshot.groupByPathKey.get(selected.pathKey)?.id;
123
+ return at(protocol.graph.groups, '$.graph.groups', (group) => group.id === id);
124
+ }
125
+ case 'root': return at(protocol.graph.roots, '$.graph.roots', (root) => rootKey(root) === selected.key);
126
+ case 'allocation': return protocol.memory.allocationReport.status === 'available'
127
+ ? at(protocol.memory.allocationReport.allocations, '$.memory.allocationReport.allocations', (allocation) => allocation.id === selected.id)
128
+ : undefined;
129
+ case 'segment': return at(protocol.graph.segments, '$.graph.segments', (segment) => segment.order === selected.index);
130
+ }
99
131
  }
@@ -0,0 +1,85 @@
1
+ import type { InspectorTheme, InspectorThemeVariables } from './theme.ts';
2
+ import { internalThemeProperty, stormVariables } from './themeDefinitions.ts';
3
+ import { createGraphVisualTheme, defaultThemeValue, type GraphVisualTheme } from './panelVisualTheme.ts';
4
+
5
+ /** Own only declarations written by this API, restoring pre-existing inline styles on reset. */
6
+ export class InspectorThemeController {
7
+ private readonly owned = new Map<string, { previous: string; priority: string; applied: string }>();
8
+ constructor(private readonly root: HTMLElement) {}
9
+
10
+ apply(theme: InspectorTheme | null): void {
11
+ const style = this.root.style;
12
+ for (const [key, entry] of this.owned) {
13
+ if (style.getPropertyValue(key) !== entry.applied || style.getPropertyPriority(key)) continue;
14
+ if (entry.previous) style.setProperty(key, entry.previous, entry.priority);
15
+ else style.removeProperty(key);
16
+ }
17
+ this.owned.clear();
18
+ const entries = Object.entries(theme?.variables ?? {}).filter(([key]) => Object.hasOwn(stormVariables, key));
19
+ if (theme?.colorScheme) entries.push(['--zfgi-color-scheme', theme.colorScheme]);
20
+ for (const [key, value] of entries) {
21
+ if (typeof value !== 'string') continue;
22
+ const previous = style.getPropertyValue(key);
23
+ const priority = style.getPropertyPriority(key);
24
+ style.setProperty(key, value);
25
+ this.owned.set(key, { previous, priority, applied: style.getPropertyValue(key) });
26
+ }
27
+ }
28
+
29
+ destroy(): void { this.owned.clear(); }
30
+ }
31
+
32
+ /** Resolve through the DOM once per refresh, including var(), color-mix(), and CSS lengths. */
33
+ export function resolveGraphTheme(root: HTMLElement): GraphVisualTheme {
34
+ const document = root.ownerDocument;
35
+ const view = document?.defaultView;
36
+ if (!view || !root.isConnected) return createGraphVisualTheme();
37
+ const styles = view.getComputedStyle(root);
38
+ const probe = document.createElement('span');
39
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;width:0;height:0;overflow:hidden;';
40
+ probe.setAttribute('aria-hidden', 'true');
41
+ root.appendChild(probe);
42
+ const cache = new Map<string, string>();
43
+ let context: CanvasRenderingContext2D | null | undefined;
44
+ const color = (value: string): string | undefined => {
45
+ probe.style.color = '';
46
+ probe.style.color = value;
47
+ if (!probe.style.color) return undefined;
48
+ const resolved = view.getComputedStyle(probe).color;
49
+ if (/^(#[\da-f]{3,8}|rgba?\([\d.,\s]+\))$/i.test(resolved)) return resolved;
50
+ // Normalize newer CSS color spaces into sRGB supported by Cytoscape.
51
+ if (context === undefined) {
52
+ const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
53
+ context = canvas.getContext('2d', { willReadFrequently: true });
54
+ }
55
+ if (!context) return undefined;
56
+ context.clearRect(0, 0, 1, 1);
57
+ context.fillStyle = resolved;
58
+ context.fillRect(0, 0, 1, 1);
59
+ const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
60
+ return `rgba(${r}, ${g}, ${b}, ${a! / 255})`;
61
+ };
62
+ const read = (key: keyof InspectorThemeVariables): string => {
63
+ const cached = cache.get(key);
64
+ if (cached !== undefined) return cached;
65
+ const value = styles.getPropertyValue(internalThemeProperty(key)).trim() || defaultThemeValue(key);
66
+ let result = value;
67
+ if (key.endsWith('font-family')) {
68
+ probe.style.fontFamily = value;
69
+ result = view.getComputedStyle(probe).fontFamily;
70
+ } else if (key.endsWith('size') || key.endsWith('width')) {
71
+ probe.style.fontSize = ''; probe.style.fontSize = value;
72
+ result = probe.style.fontSize ? view.getComputedStyle(probe).fontSize : defaultThemeValue(key);
73
+ if (!(parseFloat(result) > 0)) result = defaultThemeValue(key);
74
+ } else if (key.endsWith('opacity') || key.endsWith('scale')) {
75
+ const n = Number(value);
76
+ result = Number.isFinite(n) && n >= 0 && (!key.endsWith('opacity') || n <= 1) ? value : defaultThemeValue(key);
77
+ } else {
78
+ result = color(value) ?? defaultThemeValue(key);
79
+ }
80
+ cache.set(key, result);
81
+ return result;
82
+ };
83
+ try { return createGraphVisualTheme(read); }
84
+ finally { probe.remove(); }
85
+ }
package/src/panelTypes.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { GraphVisualTheme } from './panelVisualTheme.ts';
1
2
  import type { GraphRenderer } from './panelGraphRenderer.ts';
2
3
  import type { FrameGraphDebugEdge } from './debugCaptureModel.ts';
3
4
 
@@ -13,7 +14,7 @@ export type Selection =
13
14
  | { kind: 'group'; pathKey: string }
14
15
  | { kind: 'resource'; id: string }
15
16
  | { kind: 'root'; key: string }
16
- | { kind: 'culled'; index: number }
17
+ | { kind: 'culled'; id: string }
17
18
  | { kind: 'allocation'; id: string }
18
19
  | { kind: 'segment'; index: number };
19
20
 
@@ -22,13 +23,18 @@ export type PassesSubview = 'list' | 'groups';
22
23
  export type InspectorTab = 'summary' | 'relations' | 'raw';
23
24
 
24
25
  export type GraphViewState = {
26
+ theme?: GraphVisualTheme;
27
+ readonly refreshTheme?: () => void;
25
28
  readonly host: HTMLElement;
26
29
  readonly toolbar: HTMLElement;
27
30
  readonly legend?: HTMLElement;
28
31
  readonly layoutElementBudget?: number;
29
32
  groupsEnabled: boolean;
33
+ showResourceDeclarations?: boolean;
30
34
  readonly expandedGroupPaths: Set<string>;
31
35
  renderer?: GraphRenderer;
32
36
  fitOnNextRender: boolean;
33
37
  anchorElementIdOnNextRender?: string;
38
+ revealOnNextRender?: { readonly selection: Selection; readonly revision: number };
39
+ captureRevision?: number;
34
40
  };
@@ -1,50 +1,39 @@
1
- export const FRAME_GRAPH_DEBUG_VISUAL_THEME = {
2
- canvas: '#0b0f14',
3
- panel: '#0f151d',
4
- surface: '#131b24',
5
- surfaceRaised: '#18222d',
6
- surfaceHover: '#1d2935',
7
- border: '#263341',
8
- text: '#e6edf3',
9
- textSecondary: '#b4beca',
10
- muted: '#8b98a5',
11
- accent: '#38bdf8',
12
- success: '#34d399',
13
- warning: '#fbbf24',
14
- danger: '#fb7185',
15
- fontUi: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
16
- fontMono: 'ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace',
17
- } as const;
18
-
19
- function category(stroke: string) {
20
- const canvas = FRAME_GRAPH_DEBUG_VISUAL_THEME.canvas;
21
- const fill = '#' + [1, 3, 5].map((offset) => Math.round(
22
- parseInt(stroke.slice(offset, offset + 2), 16) * 0.18
23
- + parseInt(canvas.slice(offset, offset + 2), 16) * 0.82,
24
- ).toString(16).padStart(2, '0')).join('');
25
- return { stroke, fill };
1
+ import { stormVariables } from './themeDefinitions.ts';
2
+ import type { InspectorThemeVariables } from './theme.ts';
3
+ type Variable = keyof InspectorThemeVariables;
4
+ export function mixHex(front: string, back: string, amount: number): string {
5
+ return '#' + [1, 3, 5].map(offset => Math.round(parseInt(front.slice(offset, offset + 2), 16) * amount + parseInt(back.slice(offset, offset + 2), 16) * (1 - amount)).toString(16).padStart(2, '0')).join('');
6
+ }
7
+ export function defaultThemeValue(key: Variable, variables: InspectorThemeVariables = stormVariables): string {
8
+ let value: string = variables[key];
9
+ value = value.replace(/var\((--zfgi-[\w-]+), ([^()]+)\)/g, (_match, name: Variable) => defaultThemeValue(name, variables));
10
+ const mix = /^color-mix\(in srgb, (#[\da-f]+) ([\d.]+)%, (#[\da-f]+)\)$/i.exec(value);
11
+ return mix ? mixHex(mix[1]!, mix[3]!, Number(mix[2]) / 100) : value;
26
12
  }
27
-
28
- export const GRAPH_VISUAL_THEME = {
29
- canvas: FRAME_GRAPH_DEBUG_VISUAL_THEME.canvas,
30
- surface: FRAME_GRAPH_DEBUG_VISUAL_THEME.surface,
31
- surfaceRaised: FRAME_GRAPH_DEBUG_VISUAL_THEME.surfaceRaised,
32
- text: FRAME_GRAPH_DEBUG_VISUAL_THEME.text,
33
- muted: FRAME_GRAPH_DEBUG_VISUAL_THEME.muted,
34
- render: category('#57C785'),
35
- compute: category('#9AA5FF'),
36
- copy: category('#F2CD60'),
37
- clear: category('#C4CF89'),
38
- command: category('#CF91E8'),
39
- external: category('#F29A67'),
40
- declaration: category('#B9AB94'),
41
- output: category('#EC91AE'),
42
- texture: { stroke: '#f472b6', fill: '#3c1f32' },
43
- buffer: { stroke: '#2dd4bf', fill: '#123632' },
44
- group: { stroke: '#64748b', fill: '#141c29', alternateFill: '#172033' },
45
- dependency: { value: '#94a3b8', ordering: '#8593a6' },
46
- access: { read: '#2dd4bf', write: '#f59e0b' },
47
- selected: FRAME_GRAPH_DEBUG_VISUAL_THEME.accent,
48
- hover: '#cbd5e1',
49
- culled: { stroke: '#64748b', fill: '#171d24' },
50
- } as const;
13
+ export function createGraphVisualTheme(read: (key: Variable) => string = defaultThemeValue) {
14
+ const category = (kind: 'render' | 'compute' | 'copy' | 'clear' | 'command' | 'external' | 'declaration' | 'output' | 'texture' | 'buffer') => ({
15
+ stroke: read(('--zfgi-graph-' + kind + '-stroke') as Variable), fill: read(('--zfgi-graph-' + kind + '-fill') as Variable),
16
+ });
17
+ const number = (key: Variable) => parseFloat(read(key));
18
+ return {
19
+ canvas: read('--zfgi-canvas'), surface: read('--zfgi-surface'), surfaceRaised: read('--zfgi-surface-raised'),
20
+ text: read('--zfgi-graph-text'), muted: read('--zfgi-graph-muted'),
21
+ render: category('render'), compute: category('compute'), copy: category('copy'), clear: category('clear'),
22
+ command: category('command'), external: category('external'), declaration: category('declaration'), output: category('output'), texture: category('texture'), buffer: category('buffer'),
23
+ group: { stroke: read('--zfgi-graph-group-stroke'), fill: read('--zfgi-graph-group-fill'), alternateFill: read('--zfgi-graph-group-alternate-fill') },
24
+ dependency: { value: read('--zfgi-graph-value'), ordering: read('--zfgi-graph-ordering') },
25
+ access: { read: read('--zfgi-graph-read'), write: read('--zfgi-graph-write') },
26
+ selected: read('--zfgi-graph-selected'), hover: read('--zfgi-graph-hover'),
27
+ activeBackground: { color: read('--zfgi-graph-active-bg'), opacity: number('--zfgi-graph-active-bg-opacity'), size: number('--zfgi-graph-active-bg-size') },
28
+ culled: { stroke: read('--zfgi-graph-culled-stroke'), fill: read('--zfgi-graph-culled-fill') },
29
+ fontFamily: read('--zfgi-graph-font-family'), fontSize: number('--zfgi-graph-font-size'),
30
+ nodeBorderWidth: number('--zfgi-graph-node-border-width'), groupBorderWidth: number('--zfgi-graph-group-border-width'),
31
+ edgeWidth: number('--zfgi-graph-edge-width'), edgeOpacity: number('--zfgi-graph-edge-opacity'), orderingOpacity: number('--zfgi-graph-ordering-opacity'), groupOpacity: number('--zfgi-graph-group-opacity'),
32
+ arrowScale: number('--zfgi-graph-arrow-scale'), hoverWidth: number('--zfgi-graph-hover-width'), selectedWidth: number('--zfgi-graph-selected-width'),
33
+ };
34
+ }
35
+ export type GraphVisualTheme = ReturnType<typeof createGraphVisualTheme>;
36
+ export const GRAPH_VISUAL_THEME = createGraphVisualTheme();
37
+ export const FRAME_GRAPH_DEBUG_VISUAL_THEME = {
38
+ canvas: defaultThemeValue('--zfgi-canvas'), panel: defaultThemeValue('--zfgi-background'), surface: defaultThemeValue('--zfgi-surface'), surfaceRaised: defaultThemeValue('--zfgi-surface-raised'), surfaceHover: defaultThemeValue('--zfgi-surface-hover'), text: defaultThemeValue('--zfgi-text'), textSecondary: defaultThemeValue('--zfgi-text-secondary'), muted: defaultThemeValue('--zfgi-muted'),
39
+ };
@@ -3,13 +3,15 @@ import type {
3
3
  FrameGraphDebugViewModel,
4
4
  } from './debugCaptureModel.ts';
5
5
  import { createCell, formatBytes, labelResource } from './panelDomHelpers.ts';
6
- import type { Selection } from './panelTypes.ts';
6
+ import type { Selection, WorkbenchTab } from './panelTypes.ts';
7
7
 
8
8
  export type WorkbenchCallbacks = {
9
9
  readonly onSelect: (selection: Selection) => void;
10
10
  readonly onHover: (selection: Selection | undefined) => void;
11
11
  readonly onGroupToggle: (pathKey: string) => void;
12
12
  readonly isGroupExpanded: (pathKey: string) => boolean;
13
+ readonly onReveal?: (selection: Selection, tab: WorkbenchTab) => void;
14
+ readonly onNavigate?: (tab: WorkbenchTab, filter?: 'culled') => void;
13
15
  };
14
16
  export type WorkbenchTableColumn = {
15
17
  readonly label: string;
@@ -23,7 +25,7 @@ export function selectionKey(selection: Selection): string {
23
25
  case 'group': return `group:${selection.pathKey}`;
24
26
  case 'resource': return `resource:${selection.id}`;
25
27
  case 'root': return `root:${selection.key}`;
26
- case 'culled': return `culled:${selection.index}`;
28
+ case 'culled': return `culled:${selection.id}`;
27
29
  case 'allocation': return `allocation:${selection.id}`;
28
30
  case 'segment': return `segment:${selection.index}`;
29
31
  }
@@ -214,3 +216,59 @@ export function resourceLabel(snapshot: FrameGraphDebugViewModel, resourceId: st
214
216
  const resource = snapshot.resourceById.get(resourceId);
215
217
  return resource ? labelResource(resource) : `resource-${resourceId}`;
216
218
  }
219
+
220
+ /** All tablists use a single tab stop and automatic keyboard activation. */
221
+ export function enableTabKeyboard(tabList: HTMLElement): void {
222
+ const buttons = () => Array.from(tabList.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
223
+ const sync = () => {
224
+ for (const button of buttons()) button.tabIndex = button.getAttribute('aria-selected') === 'true' && !button.disabled ? 0 : -1;
225
+ };
226
+ tabList.addEventListener('click', sync);
227
+ tabList.addEventListener('keydown', (event) => {
228
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
229
+ const available = buttons().filter((button) => !button.disabled && !button.hidden);
230
+ if (!available.length) return;
231
+ const index = Math.max(0, available.indexOf(document.activeElement as HTMLButtonElement));
232
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? available.length - 1
233
+ : (index + (event.key === 'ArrowRight' ? 1 : -1) + available.length) % available.length;
234
+ event.preventDefault();
235
+ event.stopPropagation();
236
+ available[next]!.click();
237
+ available[next]!.focus();
238
+ sync();
239
+ });
240
+ sync();
241
+ }
242
+
243
+ export function formatTimingCoverage(timed: number, eligible: number): string {
244
+ if (eligible === 0) return 'Not applicable · no eligible passes';
245
+ if (timed === 0) return `Not collected · 0/${eligible}`;
246
+ return `${timed === eligible ? 'Complete' : 'Partial'} · ${timed}/${eligible} timed`;
247
+ }
248
+
249
+ export function formatMeasuredGpuWork(micros: number, timed: number, eligible: number): string {
250
+ if (eligible === 0) return 'Not applicable';
251
+ if (timed === 0) return 'Not collected';
252
+ return `${(micros / 1000).toFixed(3)} ms · ${formatTimingCoverage(timed, eligible)}`;
253
+ }
254
+
255
+ export function formatEstimateCoverage(bytes: number | undefined, coverage: { known: number; total: number }): string {
256
+ return bytes === undefined ? `Unknown · ${coverage.known}/${coverage.total} sizes known` : formatBytes(bytes);
257
+ }
258
+
259
+ export async function writeClipboardText(text: string): Promise<void> {
260
+ if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text);
261
+ const input = document.createElement('textarea');
262
+ input.value = text;
263
+ input.readOnly = true;
264
+ input.style.cssText = 'position:fixed;left:-9999px;top:0';
265
+ document.body.append(input);
266
+ const previous = document.activeElement as HTMLElement | null;
267
+ input.select();
268
+ try {
269
+ if (!document.execCommand('copy')) throw new Error('Clipboard is unavailable.');
270
+ } finally {
271
+ input.remove();
272
+ previous?.focus();
273
+ }
274
+ }
@@ -0,0 +1,100 @@
1
+ export const PANEL_WORKBENCH_CSS = `
2
+ .zenfg-inspector [hidden] { display: none !important; }
3
+ .zenfg-inspector { font-size: var(--fgd-font-size); }
4
+ .zenfg-inspector-workbench { display: flex; flex-direction: column; gap: 6px; }
5
+ .zenfg-inspector-workbench-command-bar,
6
+ .zenfg-inspector-feedback { flex: 0 0 auto; }
7
+ .zenfg-inspector-workspace { flex: 1 1 0; gap: 0; }
8
+ .zenfg-inspector-main { container-name: zenfg-inspector-main; container-type: inline-size; }
9
+ .zenfg-inspector-workspace.inspector-open { grid-template-columns: minmax(0, 1fr) 8px var(--fgd-detail-width, 300px); }
10
+ .zenfg-inspector-workspace.inspector-open::after { display: none; }
11
+ .zenfg-inspector-workspace:not(.detail-drawer) .zenfg-inspector-inspector {
12
+ position: static; width: auto; grid-column: 3; box-shadow: none;
13
+ }
14
+ .zenfg-inspector-workspace.detail-drawer { grid-template-columns: minmax(0, 1fr); }
15
+ .zenfg-inspector-workspace.detail-drawer .zenfg-inspector-inspector {
16
+ position: absolute; z-index: 20; top: 0; right: 0; bottom: 0;
17
+ width: min(340px, calc(100% - 24px)); box-shadow: var(--fgd-shadow);
18
+ }
19
+ .zenfg-inspector-detail-divider {
20
+ grid-column: 2; cursor: col-resize; touch-action: none; align-self: stretch; border-radius: var(--fgd-radius-sm);
21
+ }
22
+ .zenfg-inspector-detail-divider:hover,
23
+ .zenfg-inspector-detail-divider:focus-visible { background: var(--fgd-accent-soft); outline: 1px solid var(--fgd-accent); }
24
+ .zenfg-inspector-detail-backdrop {
25
+ position: absolute; z-index: 10; inset: 0; border: 0; padding: 0; background: var(--fgd-backdrop); cursor: default;
26
+ }
27
+ .zenfg-inspector-diagnostic-badge { margin-left: 5px; padding: 1px var(--fgd-space-1); border-radius: var(--fgd-radius-sm); color: var(--fgd-warning); background: var(--fgd-surface-raised); font: var(--fgd-font-size-small)/1.2 var(--fgd-font-mono); }
28
+ .zenfg-inspector-feedback { border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm); padding: 6px 10px; }
29
+ .zenfg-inspector-feedback summary { color: var(--fgd-text-secondary); cursor: pointer; }
30
+ .zenfg-inspector-feedback[data-tone='error'] { border-color: var(--fgd-danger); }
31
+ .zenfg-inspector-feedback[data-tone='error'] summary { color: var(--fgd-danger); }
32
+ .zenfg-inspector-feedback .zenfg-inspector-command-status {
33
+ display: block; max-width: none; max-height: 140px; margin-top: 5px; overflow: auto;
34
+ white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fgd-font-size-small);
35
+ }
36
+ .zenfg-inspector-capture-summary { gap: var(--fgd-space-3); grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr)); }
37
+ .zenfg-inspector-capture-summary section > div { font-size: var(--fgd-font-size-small); margin-top: var(--fgd-space-1); grid-template-columns: minmax(90px, auto) minmax(0, 1fr); }
38
+ .zenfg-inspector-capture-summary strong { white-space: normal; overflow-wrap: anywhere; font-size: var(--fgd-font-size-small); }
39
+ .zenfg-inspector-capture-summary > section { background: var(--fgd-surface); padding: 14px; }
40
+ .zenfg-inspector-capture-summary h2 { font-size: var(--fgd-font-size-small); letter-spacing: 0; text-transform: none; }
41
+ .zenfg-inspector-capture-summary section > button { margin-top: var(--fgd-space-3); font-size: var(--fgd-font-size); }
42
+ .zenfg-inspector-capture-details { grid-column: 1 / -1; padding: 10px var(--fgd-space-3); border-top: 1px solid var(--fgd-border); }
43
+ .zenfg-inspector-capture-details > summary { cursor: pointer; color: var(--fgd-text-secondary); }
44
+ .zenfg-inspector-capture-details > section { margin-top: var(--fgd-space-3); max-width: 760px; }
45
+ .zenfg-inspector-capture-details h2 { display: none; }
46
+ .zenfg-inspector-view-toolbar input,
47
+ .zenfg-inspector-view-toolbar select { height: var(--fgd-control-height); }
48
+ .zenfg-inspector-workbench-table { min-width: 620px; font: var(--fgd-font-size-small)/1.5 var(--fgd-font-ui); }
49
+ .zenfg-inspector-workbench-table th { font-size: var(--fgd-font-size-small); }
50
+ .zenfg-inspector-workbench-table td { height: var(--fgd-row-height); }
51
+ .zenfg-inspector-workbench-table [data-column='numeric'] { font-family: var(--fgd-font-mono); }
52
+ .zenfg-inspector-relation-button { overflow: hidden; }
53
+ .zenfg-inspector-muted { font-size: var(--fgd-font-size-small); }
54
+ .zenfg-inspector-graph-toolbar {
55
+ position: absolute; z-index: 5; top: var(--fgd-space-2); left: var(--fgd-space-2); right: var(--fgd-space-2);
56
+ display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; pointer-events: none;
57
+ }
58
+ .zenfg-inspector-graph-toolbar > * { pointer-events: auto; }
59
+ .zenfg-inspector-graph-toolbar > .zenfg-inspector-graph-action-controls,
60
+ .zenfg-inspector-graph-search > button { box-shadow: var(--fgd-shadow); }
61
+ .zenfg-inspector-graph-toolbar [hidden] { display: none !important; }
62
+ .zenfg-inspector-graph-search { min-width: 0; }
63
+ .zenfg-inspector-graph-search-popover {
64
+ position: absolute; top: calc(100% + var(--fgd-space-2)); left: 0; width: min(360px, 100%);
65
+ padding: var(--fgd-space-2); border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm);
66
+ background: var(--fgd-surface-raised); box-shadow: var(--fgd-shadow);
67
+ }
68
+ .zenfg-inspector-graph-search input { width: 100%; min-width: 0; height: var(--fgd-control-height); padding: var(--fgd-space-1) var(--fgd-space-2); border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm); color: var(--fgd-text); background: var(--fgd-panel); font: var(--fgd-font-size) var(--fgd-font-ui); }
69
+ .zenfg-inspector-graph-search-results {
70
+ max-height: min(240px, 40vh); margin-top: var(--fgd-space-1); overflow: auto;
71
+ }
72
+ .zenfg-inspector-graph-search-results > p { margin: 0 var(--fgd-space-1) 5px; color: var(--fgd-muted); font-size: var(--fgd-font-size-small); }
73
+ .zenfg-inspector-graph-search-results > button { display: block; width: 100%; height: auto; min-height: var(--fgd-control-height); text-align: left; padding: 6px; margin: 2px 0; white-space: normal; overflow-wrap: anywhere; font-size: var(--fgd-font-size); }
74
+ .zenfg-inspector-graph-viewport { position: relative; isolation: isolate; display: flex; flex: 1 1 0; min-width: 0; min-height: 0; }
75
+ .zenfg-inspector-legend-details {
76
+ position: absolute; z-index: 4; right: var(--fgd-space-2); bottom: var(--fgd-space-2);
77
+ max-width: calc(100% - 2 * var(--fgd-space-2)); max-height: calc(100% - 2 * var(--fgd-space-2)); overflow: auto;
78
+ border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm);
79
+ background: var(--fgd-surface-raised); box-shadow: var(--fgd-shadow); font-size: var(--fgd-font-size-small);
80
+ }
81
+ .zenfg-inspector-legend-details > summary { min-height: var(--fgd-control-height); padding: var(--fgd-space-2); cursor: pointer; color: var(--fgd-text-secondary); }
82
+ .zenfg-inspector-legend-details[open] { width: 360px; }
83
+ .zenfg-inspector-legend-details .zenfg-inspector-graph-legend {
84
+ flex-direction: column; align-items: stretch; gap: var(--fgd-space-2); padding: 0 var(--fgd-space-2) var(--fgd-space-2);
85
+ border: 0; font-size: var(--fgd-font-size-small); line-height: 1.5; white-space: normal;
86
+ }
87
+ .zenfg-inspector-legend-details .zenfg-inspector-legend-group + .zenfg-inspector-legend-group {
88
+ border-left: 0; border-top: 1px solid var(--fgd-border-subtle); padding: var(--fgd-space-2) 0 0;
89
+ }
90
+ .zenfg-inspector-legend-group { flex-wrap: wrap; }
91
+ .zenfg-inspector button:focus-visible,
92
+ .zenfg-inspector input:focus-visible,
93
+ .zenfg-inspector select:focus-visible,
94
+ .zenfg-inspector summary:focus-visible { outline: 2px solid var(--fgd-accent); outline-offset: 1px; }
95
+ @container zenfg-inspector (max-width: 560px) {
96
+ .zenfg-inspector-workbench-command-bar.branding-hidden { grid-template-columns: minmax(0, 1fr); }
97
+ .zenfg-inspector-workbench-command-bar.branding-hidden .zenfg-inspector-workbench-tabs { grid-column: 1; grid-row: 2; }
98
+ .zenfg-inspector-workbench-command-bar.branding-hidden .zenfg-inspector-workbench-actions { grid-column: 1; grid-row: 1; justify-content: end; }
99
+ }
100
+ `;