@matter-server/dashboard 0.3.2 → 0.3.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.
Files changed (124) hide show
  1. package/README.md +76 -0
  2. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +2 -2
  3. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
  4. package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
  5. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts +36 -0
  6. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts.map +1 -0
  7. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js +159 -0
  8. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js.map +6 -0
  9. package/dist/esm/pages/cluster-commands/index.d.ts +1 -0
  10. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  11. package/dist/esm/pages/cluster-commands/index.js +1 -0
  12. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  13. package/dist/esm/pages/components/footer.d.ts.map +1 -1
  14. package/dist/esm/pages/components/footer.js +4 -7
  15. package/dist/esm/pages/components/footer.js.map +1 -1
  16. package/dist/esm/pages/components/header.d.ts +5 -0
  17. package/dist/esm/pages/components/header.d.ts.map +1 -1
  18. package/dist/esm/pages/components/header.js +75 -0
  19. package/dist/esm/pages/components/header.js.map +1 -1
  20. package/dist/esm/pages/components/node-details.js +2 -2
  21. package/dist/esm/pages/components/node-details.js.map +1 -1
  22. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  23. package/dist/esm/pages/components/server-details.js +0 -1
  24. package/dist/esm/pages/components/server-details.js.map +1 -1
  25. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
  26. package/dist/esm/pages/matter-cluster-view.js +9 -4
  27. package/dist/esm/pages/matter-cluster-view.js.map +1 -1
  28. package/dist/esm/pages/matter-dashboard-app.d.ts +12 -0
  29. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
  30. package/dist/esm/pages/matter-dashboard-app.js +84 -4
  31. package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
  32. package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
  33. package/dist/esm/pages/matter-endpoint-view.js +8 -2
  34. package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
  35. package/dist/esm/pages/matter-network-view.d.ts +52 -0
  36. package/dist/esm/pages/matter-network-view.d.ts.map +1 -0
  37. package/dist/esm/pages/matter-network-view.js +309 -0
  38. package/dist/esm/pages/matter-network-view.js.map +6 -0
  39. package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
  40. package/dist/esm/pages/matter-node-view.js +86 -3
  41. package/dist/esm/pages/matter-node-view.js.map +1 -1
  42. package/dist/esm/pages/matter-server-view.d.ts +4 -0
  43. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  44. package/dist/esm/pages/matter-server-view.js +16 -1
  45. package/dist/esm/pages/matter-server-view.js.map +1 -1
  46. package/dist/esm/pages/network/base-network-graph.d.ts +74 -0
  47. package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -0
  48. package/dist/esm/pages/network/base-network-graph.js +411 -0
  49. package/dist/esm/pages/network/base-network-graph.js.map +6 -0
  50. package/dist/esm/pages/network/device-icons.d.ts +52 -0
  51. package/dist/esm/pages/network/device-icons.d.ts.map +1 -0
  52. package/dist/esm/pages/network/device-icons.js +197 -0
  53. package/dist/esm/pages/network/device-icons.js.map +6 -0
  54. package/dist/esm/pages/network/device-panel.d.ts +31 -0
  55. package/dist/esm/pages/network/device-panel.d.ts.map +1 -0
  56. package/dist/esm/pages/network/device-panel.js +183 -0
  57. package/dist/esm/pages/network/device-panel.js.map +6 -0
  58. package/dist/esm/pages/network/network-details.d.ts +77 -0
  59. package/dist/esm/pages/network/network-details.d.ts.map +1 -0
  60. package/dist/esm/pages/network/network-details.js +904 -0
  61. package/dist/esm/pages/network/network-details.js.map +6 -0
  62. package/dist/esm/pages/network/network-types.d.ts +159 -0
  63. package/dist/esm/pages/network/network-types.d.ts.map +1 -0
  64. package/dist/esm/pages/network/network-types.js +19 -0
  65. package/dist/esm/pages/network/network-types.js.map +6 -0
  66. package/dist/esm/pages/network/network-utils.d.ts +196 -0
  67. package/dist/esm/pages/network/network-utils.d.ts.map +1 -0
  68. package/dist/esm/pages/network/network-utils.js +540 -0
  69. package/dist/esm/pages/network/network-utils.js.map +6 -0
  70. package/dist/esm/pages/network/thread-graph.d.ts +27 -0
  71. package/dist/esm/pages/network/thread-graph.d.ts.map +1 -0
  72. package/dist/esm/pages/network/thread-graph.js +137 -0
  73. package/dist/esm/pages/network/thread-graph.js.map +6 -0
  74. package/dist/esm/pages/network/update-connections-dialog.d.ts +55 -0
  75. package/dist/esm/pages/network/update-connections-dialog.d.ts.map +1 -0
  76. package/dist/esm/pages/network/update-connections-dialog.js +284 -0
  77. package/dist/esm/pages/network/update-connections-dialog.js.map +6 -0
  78. package/dist/esm/pages/network/wifi-graph.d.ts +27 -0
  79. package/dist/esm/pages/network/wifi-graph.d.ts.map +1 -0
  80. package/dist/esm/pages/network/wifi-graph.js +169 -0
  81. package/dist/esm/pages/network/wifi-graph.js.map +6 -0
  82. package/dist/esm/util/format_hex.d.ts +18 -0
  83. package/dist/esm/util/format_hex.d.ts.map +1 -1
  84. package/dist/esm/util/format_hex.js +21 -1
  85. package/dist/esm/util/format_hex.js.map +1 -1
  86. package/dist/web/js/{commission-node-dialog-CBSDiqRW.js → commission-node-dialog-CcMuttYO.js} +5 -5
  87. package/dist/web/js/{commission-node-existing-TP6s8Tez.js → commission-node-existing-CqTRDMAr.js} +2 -5
  88. package/dist/web/js/{commission-node-thread-DOB8pu6x.js → commission-node-thread-DgwtTVwK.js} +2 -5
  89. package/dist/web/js/{commission-node-wifi-tzavmk1j.js → commission-node-wifi-XaN2SEnE.js} +2 -5
  90. package/dist/web/js/{dialog-box-Dknil_Be.js → dialog-box-COpDD8i7.js} +2 -2
  91. package/dist/web/js/{fire_event-DRpOSjJR.js → fire_event-mDYWi2sw.js} +1 -1
  92. package/dist/web/js/{log-level-dialog-TXkma-7Z.js → log-level-dialog-Bc32kZVw.js} +2 -3
  93. package/dist/web/js/main.js +1 -1
  94. package/dist/web/js/matter-dashboard-app-CrBHT4fT.js +31606 -0
  95. package/dist/web/js/{node-binding-dialog-D52FCBFP.js → node-binding-dialog-C8fqOJiB.js} +2 -4
  96. package/dist/web/js/prevent_default-D-ohDGsN.js +8 -0
  97. package/package.json +6 -5
  98. package/src/pages/cluster-commands/base-cluster-commands.ts +2 -2
  99. package/src/pages/cluster-commands/clusters/basic-information-commands.ts +171 -0
  100. package/src/pages/cluster-commands/index.ts +1 -0
  101. package/src/pages/components/footer.ts +4 -7
  102. package/src/pages/components/header.ts +81 -0
  103. package/src/pages/components/node-details.ts +3 -3
  104. package/src/pages/components/server-details.ts +0 -1
  105. package/src/pages/matter-cluster-view.ts +11 -4
  106. package/src/pages/matter-dashboard-app.ts +105 -5
  107. package/src/pages/matter-endpoint-view.ts +10 -3
  108. package/src/pages/matter-network-view.ts +325 -0
  109. package/src/pages/matter-node-view.ts +93 -4
  110. package/src/pages/matter-server-view.ts +17 -1
  111. package/src/pages/network/base-network-graph.ts +477 -0
  112. package/src/pages/network/device-icons.ts +283 -0
  113. package/src/pages/network/device-panel.ts +180 -0
  114. package/src/pages/network/network-details.ts +1015 -0
  115. package/src/pages/network/network-types.ts +167 -0
  116. package/src/pages/network/network-utils.ts +861 -0
  117. package/src/pages/network/thread-graph.ts +170 -0
  118. package/src/pages/network/update-connections-dialog.ts +327 -0
  119. package/src/pages/network/wifi-graph.ts +193 -0
  120. package/src/util/format_hex.ts +39 -0
  121. package/dist/web/js/matter-dashboard-app-B7GUghkC.js +0 -17254
  122. package/dist/web/js/outlined-text-field-D1DyKQY-.js +0 -968
  123. package/dist/web/js/prevent_default-BPgSQsuY.js +0 -814
  124. package/dist/web/js/validator-C735j770.js +0 -1122
@@ -0,0 +1,477 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type { MatterNode } from "@matter-server/ws-client";
8
+ import { LitElement, css } from "lit";
9
+ import { property, state } from "lit/decorators.js";
10
+ // @ts-expect-error vis-network doesn't have proper type declarations for standalone export
11
+ import { DataSet, Network } from "vis-network/standalone";
12
+ import { ThemeService } from "../../util/theme-service.js";
13
+ import type { NetworkGraphEdge, NetworkGraphNode } from "./network-types.js";
14
+
15
+ /**
16
+ * Base class for network graph components (Thread and WiFi).
17
+ * Provides shared vis.js network initialization, highlighting, and theme support.
18
+ */
19
+ export abstract class BaseNetworkGraph extends LitElement {
20
+ @property({ type: Object })
21
+ public nodes: Record<string, MatterNode> = {};
22
+
23
+ @state()
24
+ protected _selectedNodeId: number | string | null = null;
25
+
26
+ protected _network?: Network;
27
+ protected _nodesDataSet?: DataSet<NetworkGraphNode>;
28
+ protected _edgesDataSet?: DataSet<NetworkGraphEdge>;
29
+ protected _container?: HTMLDivElement;
30
+ protected _resizeObserver?: ResizeObserver;
31
+ protected _themeUnsubscribe?: () => void;
32
+ protected _updateDebounceTimer?: ReturnType<typeof setTimeout>;
33
+
34
+ /** Store original edge colors for restoration after highlighting */
35
+ private _originalEdgeColors: Map<string, { color: string; highlight: string }> = new Map();
36
+
37
+ protected _getFontColor(): string {
38
+ return ThemeService.effectiveTheme === "dark" ? "#e0e0e0" : "#333333";
39
+ }
40
+
41
+ protected _getDimmedEdgeColor(): string {
42
+ return ThemeService.effectiveTheme === "dark" ? "#555555" : "#cccccc";
43
+ }
44
+
45
+ /**
46
+ * Returns physics options for the network. Override in subclasses for different behavior.
47
+ */
48
+ protected _getPhysicsOptions(): any {
49
+ return {
50
+ enabled: true,
51
+ solver: "forceAtlas2Based",
52
+ forceAtlas2Based: {
53
+ gravitationalConstant: -70,
54
+ centralGravity: 0.005,
55
+ springLength: 130,
56
+ springConstant: 0.08,
57
+ damping: 0.4,
58
+ avoidOverlap: 0.6,
59
+ },
60
+ stabilization: {
61
+ enabled: true,
62
+ iterations: 250,
63
+ updateInterval: 25,
64
+ },
65
+ };
66
+ }
67
+
68
+ override updated(changedProperties: Map<string, unknown>): void {
69
+ super.updated(changedProperties);
70
+
71
+ // If container wasn't found in firstUpdated (empty state was rendered),
72
+ // try to find and observe it now that it might have appeared
73
+ if (!this._container && !this._resizeObserver) {
74
+ this._tryAttachContainer();
75
+ }
76
+
77
+ if (changedProperties.has("nodes")) {
78
+ this._debouncedUpdateGraph();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Try to find and attach the graph container if it wasn't available before.
84
+ * This handles the case where empty state was rendered initially.
85
+ */
86
+ private _tryAttachContainer(): void {
87
+ const container = this.shadowRoot?.querySelector(".graph-container") as HTMLDivElement;
88
+ if (container) {
89
+ this._container = container;
90
+ this._resizeObserver = new ResizeObserver(entries => {
91
+ const entry = entries[0];
92
+ if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
93
+ if (!this._network) {
94
+ this._initializeNetwork();
95
+ } else {
96
+ this._network.setSize(`${entry.contentRect.width}px`, `${entry.contentRect.height}px`);
97
+ this._network.redraw();
98
+ }
99
+ }
100
+ });
101
+ this._resizeObserver.observe(this._container);
102
+ }
103
+ }
104
+
105
+ /** Debounced graph update to avoid excessive redraws */
106
+ protected _debouncedUpdateGraph(): void {
107
+ if (this._updateDebounceTimer) {
108
+ clearTimeout(this._updateDebounceTimer);
109
+ }
110
+ this._updateDebounceTimer = setTimeout(() => {
111
+ this._updateGraph();
112
+ }, 100);
113
+ }
114
+
115
+ override firstUpdated(): void {
116
+ this._container = this.shadowRoot?.querySelector(".graph-container") as HTMLDivElement;
117
+ if (this._container) {
118
+ // Wait for container to have proper dimensions before initializing
119
+ this._resizeObserver = new ResizeObserver(entries => {
120
+ const entry = entries[0];
121
+ if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
122
+ if (!this._network) {
123
+ this._initializeNetwork();
124
+ } else {
125
+ // Resize existing network
126
+ this._network.setSize(`${entry.contentRect.width}px`, `${entry.contentRect.height}px`);
127
+ this._network.redraw();
128
+ }
129
+ }
130
+ });
131
+ this._resizeObserver.observe(this._container);
132
+ }
133
+
134
+ // Subscribe to theme changes - refresh entire graph to update colors
135
+ this._themeUnsubscribe = ThemeService.subscribe(() => {
136
+ if (this._network && this._nodesDataSet) {
137
+ const fontColor = this._getFontColor();
138
+
139
+ // Update default font colors for new nodes
140
+ this._network.setOptions({
141
+ nodes: {
142
+ font: {
143
+ color: fontColor,
144
+ },
145
+ },
146
+ });
147
+
148
+ // Regenerate node icons and edges with new theme colors
149
+ this._updateGraph();
150
+
151
+ // Update font color on all existing nodes in the dataset
152
+ const allNodes = this._nodesDataSet.get();
153
+ const nodeUpdates = allNodes.map((node: NetworkGraphNode) => ({
154
+ id: node.id,
155
+ font: { color: fontColor },
156
+ }));
157
+ this._nodesDataSet.update(nodeUpdates);
158
+
159
+ // Force redraw
160
+ this._network.redraw();
161
+ }
162
+ });
163
+ }
164
+
165
+ override disconnectedCallback(): void {
166
+ super.disconnectedCallback();
167
+ this._resizeObserver?.disconnect();
168
+ this._themeUnsubscribe?.();
169
+ if (this._updateDebounceTimer) {
170
+ clearTimeout(this._updateDebounceTimer);
171
+ }
172
+ this._nodesDataSet?.clear();
173
+ this._edgesDataSet?.clear();
174
+ this._network?.destroy();
175
+ this._network = undefined;
176
+ this._nodesDataSet = undefined;
177
+ this._edgesDataSet = undefined;
178
+ this._originalEdgeColors.clear();
179
+ }
180
+
181
+ protected _initializeNetwork(): void {
182
+ if (!this._container) return;
183
+
184
+ // Get actual dimensions
185
+ const rect = this._container.getBoundingClientRect();
186
+ if (rect.width === 0 || rect.height === 0) {
187
+ return; // Wait for proper dimensions
188
+ }
189
+
190
+ this._nodesDataSet = new DataSet<NetworkGraphNode>();
191
+ this._edgesDataSet = new DataSet<NetworkGraphEdge>();
192
+
193
+ const options = {
194
+ width: `${rect.width}px`,
195
+ height: `${rect.height}px`,
196
+ autoResize: false, // We handle resize manually
197
+ nodes: {
198
+ shape: "image",
199
+ size: 30,
200
+ font: {
201
+ size: 12,
202
+ color: this._getFontColor(),
203
+ },
204
+ borderWidth: 2,
205
+ borderWidthSelected: 3,
206
+ },
207
+ edges: {
208
+ width: 2,
209
+ smooth: {
210
+ type: "continuous",
211
+ roundness: 0.5,
212
+ },
213
+ },
214
+ physics: this._getPhysicsOptions(),
215
+ interaction: {
216
+ hover: true,
217
+ tooltipDelay: 200,
218
+ hideEdgesOnDrag: true,
219
+ },
220
+ };
221
+
222
+ this._network = new Network(
223
+ this._container,
224
+ {
225
+ nodes: this._nodesDataSet,
226
+ edges: this._edgesDataSet,
227
+ },
228
+ options,
229
+ );
230
+
231
+ // Handle node selection
232
+ this._network.on("selectNode", (params: { nodes: (number | string)[] }) => {
233
+ if (params.nodes.length > 0) {
234
+ this._selectedNodeId = params.nodes[0];
235
+ this._highlightConnections(this._selectedNodeId);
236
+ this._dispatchNodeSelected(this._selectedNodeId);
237
+ }
238
+ });
239
+
240
+ this._network.on("deselectNode", () => {
241
+ this._selectedNodeId = null;
242
+ this._clearHighlights();
243
+ this._dispatchNodeSelected(null);
244
+ });
245
+
246
+ // Auto-fit after stabilization completes
247
+ this._network.on("stabilizationIterationsDone", () => {
248
+ // Fit with padding to keep nodes away from edges
249
+ this._network?.fit({
250
+ animation: {
251
+ duration: 500,
252
+ easingFunction: "easeInOutQuad",
253
+ },
254
+ });
255
+ });
256
+
257
+ // Also fit when physics fully stops (catches any drift after initial stabilization)
258
+ this._network.on("stabilized", () => {
259
+ this._network?.fit({
260
+ animation: {
261
+ duration: 300,
262
+ easingFunction: "easeInOutQuad",
263
+ },
264
+ });
265
+ });
266
+
267
+ this._updateGraph();
268
+ }
269
+
270
+ /**
271
+ * Returns true if the graph is initialized and ready for interaction.
272
+ */
273
+ public isReady(): boolean {
274
+ return this._network !== undefined && this._nodesDataSet !== undefined;
275
+ }
276
+
277
+ /**
278
+ * Fits all nodes into view.
279
+ */
280
+ public fit(): void {
281
+ this._network?.fit({
282
+ animation: {
283
+ duration: 300,
284
+ easingFunction: "easeInOutQuad",
285
+ },
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Selects a node by ID and focuses on it.
291
+ */
292
+ public selectNode(nodeId: number | string): void {
293
+ if (!this._network) return;
294
+
295
+ this._selectedNodeId = nodeId;
296
+ this._network.selectNodes([nodeId]);
297
+ this._highlightConnections(nodeId);
298
+ this._dispatchNodeSelected(nodeId);
299
+
300
+ // Focus on the selected node
301
+ this._network.focus(nodeId, {
302
+ scale: 1.2,
303
+ animation: {
304
+ duration: 500,
305
+ easingFunction: "easeInOutQuad",
306
+ },
307
+ });
308
+ }
309
+
310
+ protected _dispatchNodeSelected(nodeId: number | string | null): void {
311
+ this.dispatchEvent(
312
+ new CustomEvent("node-selected", {
313
+ detail: { nodeId },
314
+ bubbles: true,
315
+ composed: true,
316
+ }),
317
+ );
318
+ }
319
+
320
+ /**
321
+ * Highlights edges connected to the selected node and makes neighbor nodes more prominent.
322
+ */
323
+ protected _highlightConnections(nodeId: number | string): void {
324
+ if (!this._edgesDataSet || !this._nodesDataSet) return;
325
+
326
+ // Store original colors before highlighting
327
+ const allEdges = this._edgesDataSet.get();
328
+ for (const edge of allEdges) {
329
+ const edgeId = String(edge.id);
330
+ if (!this._originalEdgeColors.has(edgeId)) {
331
+ const colorObj = edge.color as { color: string; highlight: string };
332
+ this._originalEdgeColors.set(edgeId, {
333
+ color: colorObj?.color ?? "#999999",
334
+ highlight: colorObj?.highlight ?? "#999999",
335
+ });
336
+ }
337
+ }
338
+
339
+ // Find all edges connected to this node
340
+ const connectedEdges = this._edgesDataSet.get({
341
+ filter: (edge: NetworkGraphEdge) => edge.from === nodeId || edge.to === nodeId,
342
+ });
343
+
344
+ // Get neighbor node IDs
345
+ const neighborIds = new Set<number | string>();
346
+ for (const edge of connectedEdges) {
347
+ if (edge.from === nodeId) {
348
+ neighborIds.add(edge.to);
349
+ } else {
350
+ neighborIds.add(edge.from);
351
+ }
352
+ }
353
+
354
+ // Update edges - make connected ones thicker
355
+ const dimmedColor = this._getDimmedEdgeColor();
356
+ const edgeUpdates = allEdges.map((edge: NetworkGraphEdge) => {
357
+ const isConnected = edge.from === nodeId || edge.to === nodeId;
358
+ const originalColor = this._originalEdgeColors.get(String(edge.id));
359
+ return {
360
+ id: edge.id,
361
+ width: isConnected ? 3 : 1,
362
+ // Dim non-connected edges
363
+ color: isConnected
364
+ ? { color: originalColor?.color, highlight: originalColor?.highlight }
365
+ : {
366
+ color: dimmedColor,
367
+ highlight: dimmedColor,
368
+ },
369
+ };
370
+ });
371
+ this._edgesDataSet.update(edgeUpdates);
372
+
373
+ // Update nodes - make neighbors more prominent
374
+ const allNodes = this._nodesDataSet.get();
375
+ const nodeUpdates = allNodes.map((node: NetworkGraphNode) => {
376
+ const isNeighbor = neighborIds.has(node.id);
377
+ const isSelected = node.id === nodeId;
378
+ return {
379
+ id: node.id,
380
+ size: isSelected ? 40 : isNeighbor ? 35 : 25,
381
+ font: {
382
+ size: isSelected ? 14 : isNeighbor ? 13 : 11,
383
+ color: this._getFontColor(),
384
+ bold: isSelected || isNeighbor ? { color: this._getFontColor() } : undefined,
385
+ },
386
+ opacity: isSelected || isNeighbor ? 1 : 0.5,
387
+ };
388
+ });
389
+ this._nodesDataSet.update(nodeUpdates);
390
+ }
391
+
392
+ /**
393
+ * Clears all highlights and restores default styling.
394
+ */
395
+ protected _clearHighlights(): void {
396
+ if (!this._edgesDataSet || !this._nodesDataSet) return;
397
+
398
+ // Restore edge widths and original colors
399
+ const allEdges = this._edgesDataSet.get();
400
+ const edgeUpdates = allEdges.map((edge: NetworkGraphEdge) => {
401
+ const originalColor = this._originalEdgeColors.get(String(edge.id));
402
+ return {
403
+ id: edge.id,
404
+ width: 2,
405
+ color: originalColor ?? { color: "#999999", highlight: "#999999" },
406
+ };
407
+ });
408
+ this._edgesDataSet.update(edgeUpdates);
409
+
410
+ // Restore node sizes
411
+ const allNodes = this._nodesDataSet.get();
412
+ const nodeUpdates = allNodes.map((node: NetworkGraphNode) => ({
413
+ id: node.id,
414
+ size: 30,
415
+ font: {
416
+ size: 12,
417
+ color: this._getFontColor(),
418
+ bold: undefined,
419
+ },
420
+ opacity: 1,
421
+ }));
422
+ this._nodesDataSet.update(nodeUpdates);
423
+ }
424
+
425
+ /**
426
+ * Clear stored edge colors when graph is updated (edges are recreated).
427
+ */
428
+ protected _clearOriginalEdgeColors(): void {
429
+ this._originalEdgeColors.clear();
430
+ }
431
+
432
+ /**
433
+ * Abstract method to be implemented by subclasses for graph-specific updates.
434
+ */
435
+ protected abstract _updateGraph(): void;
436
+
437
+ static override styles = css`
438
+ :host {
439
+ display: block;
440
+ width: 100%;
441
+ height: 100%;
442
+ min-height: 0;
443
+ }
444
+
445
+ .graph-container {
446
+ width: 100%;
447
+ height: 100%;
448
+ background-color: var(--md-sys-color-surface, #fff);
449
+ border-radius: 8px;
450
+ border: 1px solid var(--md-sys-color-outline-variant, #ccc);
451
+ }
452
+
453
+ .empty-state {
454
+ display: flex;
455
+ flex-direction: column;
456
+ align-items: center;
457
+ justify-content: center;
458
+ height: 100%;
459
+ color: var(--md-sys-color-on-surface-variant, #666);
460
+ text-align: center;
461
+ padding: 24px;
462
+ box-sizing: border-box;
463
+ background-color: var(--md-sys-color-surface, #fff);
464
+ border-radius: 8px;
465
+ border: 1px solid var(--md-sys-color-outline-variant, #ccc);
466
+ }
467
+
468
+ .empty-state p {
469
+ margin: 8px 0;
470
+ }
471
+
472
+ .empty-state .hint {
473
+ font-size: 0.875rem;
474
+ opacity: 0.7;
475
+ }
476
+ `;
477
+ }