vite-plugin-devtools-vue2 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/panel.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // on every Vue scheduler flush.
4
4
 
5
5
  import { LitElement, css, html } from 'lit';
6
- import { DRAG_THRESHOLD, EDGE_MARGIN, PANEL_EDGE, PANEL_H, PANEL_W, SECTION_FILTER_THRESHOLD, STORE_KEY } from './config.js';
6
+ import { DragThreshold, EdgeMargin, PanelEdge, PanelH, PanelW, SectionFilterThreshold, StoreKey } from './config.js';
7
7
  import hook from './hook.js';
8
8
  import { hide, highlight } from './inspector.js';
9
9
  import { isPicking, startPicking, stopPicking } from './picker.js';
@@ -22,6 +22,7 @@ export class VueDevToolsPanel extends LitElement {
22
22
  tab: { state: true },
23
23
  vuexSelected: { state: true },
24
24
  timelineSelected: { state: true },
25
+ theme: { state: true },
25
26
  renderCodeText: { state: true }
26
27
  };
27
28
 
@@ -38,9 +39,12 @@ export class VueDevToolsPanel extends LitElement {
38
39
  this.tab = ui.tab || 'components';
39
40
  this.vuexSelected = 0;
40
41
  this.timelineSelected = null;
42
+ this.theme = ui.theme || 'light';
41
43
  this.renderCodeText = null;
42
44
  this.valueExpanded = new Set();
43
45
  this.sectionCollapsed = new Set();
46
+ // Nodes collapsed within search results (default: all shown expanded).
47
+ this._searchCollapsed = new Set();
44
48
  // Per-section (props/data/…) key filter text, keyed by section title.
45
49
  this._sectionFilter = {};
46
50
  // Docked position of the entry/panel. Defaults to the bottom edge near
@@ -59,14 +63,16 @@ export class VueDevToolsPanel extends LitElement {
59
63
  // derived from `el.__vue__` anyway.
60
64
  this._domObserver = null;
61
65
  this._onFlush = () => this._scheduleRefresh();
62
- this._onKeydown = e => this._handleKeydown(e);
66
+ this._onKeydown = event => this._handleKeydown(event);
63
67
  // Keep the entry/panel on-screen when the viewport shrinks. resize can
64
68
  // fire many times per second while dragging the window edge, and
65
69
  // _applyPos reads layout then writes styles — so coalesce to at most one
66
70
  // call per frame via rAF to avoid layout thrashing.
67
71
  this._resizeRaf = 0;
68
72
  this._onResize = () => {
69
- if (this._resizeRaf) return;
73
+ if (this._resizeRaf) {
74
+ return;
75
+ }
70
76
  this._resizeRaf = requestAnimationFrame(() => {
71
77
  this._resizeRaf = 0;
72
78
  this._applyPos();
@@ -76,8 +82,8 @@ export class VueDevToolsPanel extends LitElement {
76
82
 
77
83
  static _loadUiState() {
78
84
  try {
79
- return JSON.parse(localStorage.getItem(STORE_KEY)) || {};
80
- } catch (e) {
85
+ return JSON.parse(localStorage.getItem(StoreKey)) || {};
86
+ } catch (err) {
81
87
  return {};
82
88
  }
83
89
  }
@@ -85,8 +91,8 @@ export class VueDevToolsPanel extends LitElement {
85
91
  _persistUiState() {
86
92
  try {
87
93
  const pos = this._pos && Number.isFinite(this._pos.along) ? this._pos : undefined;
88
- localStorage.setItem(STORE_KEY, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, pos }));
89
- } catch (e) {
94
+ localStorage.setItem(StoreKey, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, theme: this.theme, pos }));
95
+ } catch (err) {
90
96
  /* storage unavailable — ignore */
91
97
  }
92
98
  }
@@ -96,97 +102,107 @@ export class VueDevToolsPanel extends LitElement {
96
102
  // the viewport; clamped to stay fully on-screen.
97
103
  _applyPos() {
98
104
  const entry = this.renderRoot && this.renderRoot.querySelector('.entry');
99
- if (!entry) return;
100
- const p = this._pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
101
- const M = EDGE_MARGIN;
105
+ if (!entry) {
106
+ return;
107
+ }
108
+ const pos = this._pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
109
+ const margin = EdgeMargin;
102
110
  const vw = window.innerWidth;
103
111
  const vh = window.innerHeight;
104
112
  const er = entry.getBoundingClientRect();
105
113
  const ew = er.width || 40;
106
114
  const eh = er.height || 40;
107
- const clamp = (v, max) => Math.min(Math.max(M, v), Math.max(M, max));
115
+ const clamp = (value, max) => Math.min(Math.max(margin, value), Math.max(margin, max));
108
116
 
109
117
  // Clamped offset of the entry along its docked edge. We derive the panel
110
118
  // position from these numbers directly rather than re-reading the entry's
111
119
  // live rect — on a fresh open/refresh that rect can still be stale (reads
112
120
  // ~0), which left the entry bottom-right but the panel bottom-left.
113
- const alongX = clamp(p.along, vw - ew - M);
114
- const alongY = clamp(p.along, vh - eh - M);
121
+ const alongX = clamp(pos.along, vw - ew - margin);
122
+ const alongY = clamp(pos.along, vh - eh - margin);
115
123
 
116
124
  const es = entry.style;
117
125
  es.insetInlineStart = es.insetBlockStart = es.insetInlineEnd = es.insetBlockEnd = 'auto';
118
- if (p.edge === 'right' || p.edge === 'left') {
119
- es['inset' + (p.edge === 'right' ? 'InlineEnd' : 'InlineStart')] = M + 'px';
126
+ if (pos.edge === 'right' || pos.edge === 'left') {
127
+ es['inset' + (pos.edge === 'right' ? 'InlineEnd' : 'InlineStart')] = margin + 'px';
120
128
  es.insetBlockStart = alongY + 'px';
121
129
  } else {
122
- es['inset' + (p.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = M + 'px';
130
+ es['inset' + (pos.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = margin + 'px';
123
131
  es.insetInlineStart = alongX + 'px';
124
132
  }
125
- this.setAttribute('dock', p.edge);
133
+ this.setAttribute('dock', pos.edge);
126
134
 
127
135
  const panel = this.renderRoot.querySelector('.panel');
128
- if (!panel) return;
136
+ if (!panel) {
137
+ return;
138
+ }
129
139
  const ps = panel.style;
130
140
  ps.insetInlineStart = ps.insetBlockStart = ps.insetInlineEnd = ps.insetBlockEnd = 'auto';
131
141
  // Entry center along its edge, computed from the clamped offsets above.
132
142
  const cx = alongX + ew / 2;
133
143
  const cy = alongY + eh / 2;
134
- if (p.edge === 'right') {
135
- ps.insetInlineEnd = PANEL_EDGE + 'px';
136
- ps.insetBlockStart = clamp(cy - PANEL_H / 2, vh - PANEL_H - M) + 'px';
137
- } else if (p.edge === 'left') {
138
- ps.insetInlineStart = PANEL_EDGE + 'px';
139
- ps.insetBlockStart = clamp(cy - PANEL_H / 2, vh - PANEL_H - M) + 'px';
140
- } else if (p.edge === 'top') {
141
- ps.insetBlockStart = PANEL_EDGE + 'px';
142
- ps.insetInlineStart = clamp(cx - PANEL_W / 2, vw - PANEL_W - M) + 'px';
144
+ if (pos.edge === 'right') {
145
+ ps.insetInlineEnd = PanelEdge + 'px';
146
+ ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
147
+ } else if (pos.edge === 'left') {
148
+ ps.insetInlineStart = PanelEdge + 'px';
149
+ ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
150
+ } else if (pos.edge === 'top') {
151
+ ps.insetBlockStart = PanelEdge + 'px';
152
+ ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
143
153
  } else {
144
- ps.insetBlockEnd = PANEL_EDGE + 'px';
145
- ps.insetInlineStart = clamp(cx - PANEL_W / 2, vw - PANEL_W - M) + 'px';
154
+ ps.insetBlockEnd = PanelEdge + 'px';
155
+ ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
146
156
  }
147
157
  }
148
158
 
149
159
  // Drag the always-visible entry. Movement over a threshold = drag (live snap
150
160
  // to nearest edge, panel follows); a plain click toggles the panel.
151
- _startDrag(e) {
152
- if (e.button !== 0) return;
153
- e.preventDefault();
161
+ _startDrag(event) {
162
+ if (event.button !== 0) {
163
+ return;
164
+ }
165
+ event.preventDefault();
154
166
  hide(); // clear any hover highlight before dragging
155
167
  const entry = this.renderRoot.querySelector('.entry');
156
168
  const rect = entry.getBoundingClientRect();
157
169
  this._drag = {
158
- startX: e.clientX,
159
- startY: e.clientY,
160
- offX: e.clientX - rect.left,
161
- offY: e.clientY - rect.top,
170
+ startX: event.clientX,
171
+ startY: event.clientY,
172
+ offX: event.clientX - rect.left,
173
+ offY: event.clientY - rect.top,
162
174
  moved: false
163
175
  };
164
- this._onDragMove = ev => this._dragMove(ev);
165
- this._onDragUp = ev => this._dragUp(ev);
176
+ this._onDragMove = event => this._dragMove(event);
177
+ this._onDragUp = event => this._dragUp(event);
166
178
  window.addEventListener('pointermove', this._onDragMove, true);
167
179
  window.addEventListener('pointerup', this._onDragUp, true);
168
180
  }
169
181
 
170
- _onFabPointerDown(e) {
171
- this._startDrag(e);
182
+ _onFabPointerDown(event) {
183
+ this._startDrag(event);
172
184
  }
173
185
 
174
- _dragMove(e) {
175
- const d = this._drag;
176
- if (!d) return;
177
- if (!d.moved && Math.abs(e.clientX - d.startX) + Math.abs(e.clientY - d.startY) < DRAG_THRESHOLD) return;
178
- d.moved = true;
186
+ _dragMove(event) {
187
+ const drag = this._drag;
188
+ if (!drag) {
189
+ return;
190
+ }
191
+ if (!drag.moved && Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < DragThreshold) {
192
+ return;
193
+ }
194
+ drag.moved = true;
179
195
  // Snap to the nearest edge live during the drag (not on release).
180
196
  const vw = window.innerWidth;
181
197
  const vh = window.innerHeight;
182
198
  const dist = {
183
- left: e.clientX,
184
- right: vw - e.clientX,
185
- top: e.clientY,
186
- bottom: vh - e.clientY
199
+ left: event.clientX,
200
+ right: vw - event.clientX,
201
+ top: event.clientY,
202
+ bottom: vh - event.clientY
187
203
  };
188
- const edge = Object.keys(dist).reduce((a, b) => (dist[b] < dist[a] ? b : a));
189
- const along = edge === 'left' || edge === 'right' ? e.clientY - d.offY : e.clientX - d.offX;
204
+ const edge = Object.keys(dist).reduce((best, current) => (dist[current] < dist[best] ? current : best));
205
+ const along = edge === 'left' || edge === 'right' ? event.clientY - drag.offY : event.clientX - drag.offX;
190
206
  this._pos = { edge, along };
191
207
  this._applyPos();
192
208
  }
@@ -194,10 +210,12 @@ export class VueDevToolsPanel extends LitElement {
194
210
  _dragUp() {
195
211
  window.removeEventListener('pointermove', this._onDragMove, true);
196
212
  window.removeEventListener('pointerup', this._onDragUp, true);
197
- const d = this._drag;
213
+ const drag = this._drag;
198
214
  this._drag = null;
199
- if (!d) return;
200
- if (!d.moved) {
215
+ if (!drag) {
216
+ return;
217
+ }
218
+ if (!drag.moved) {
201
219
  // plain click → toggle the panel
202
220
  this.collapsed = !this.collapsed;
203
221
  return;
@@ -236,13 +254,22 @@ export class VueDevToolsPanel extends LitElement {
236
254
  this._domObserver.disconnect();
237
255
  this._domObserver = null;
238
256
  }
239
- if (this._vuexUnsub) this._vuexUnsub();
240
- if (this._eventsUnsub) this._eventsUnsub();
257
+ if (this._vuexUnsub) {
258
+ this._vuexUnsub();
259
+ }
260
+ if (this._eventsUnsub) {
261
+ this._eventsUnsub();
262
+ }
241
263
  stopPicking();
242
264
  }
243
265
 
244
266
  firstUpdated() {
245
267
  this._applyPos();
268
+ this.setAttribute('theme', this.theme);
269
+ }
270
+
271
+ _toggleTheme() {
272
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
246
273
  }
247
274
 
248
275
  _scheduleRefresh() {
@@ -265,16 +292,23 @@ export class VueDevToolsPanel extends LitElement {
265
292
  }
266
293
 
267
294
  _toggle(id) {
268
- if (this.expanded.has(id)) this.expanded.delete(id);
269
- else this.expanded.add(id);
295
+ if (this.expanded.has(id)) {
296
+ this.expanded.delete(id);
297
+ } else {
298
+ this.expanded.add(id);
299
+ }
270
300
  this.requestUpdate();
271
301
  }
272
302
 
273
303
  // Highlight the page DOM only while hovering a tree row (vue-devtools style).
274
304
  _hoverEnter(id) {
275
- if (this._drag) return; // don't highlight while dragging the entry/panel
305
+ if (this._drag) {
306
+ return; // don't highlight while dragging the entry/panel
307
+ }
276
308
  const vm = getInstance(id);
277
- if (vm) highlight(vm);
309
+ if (vm) {
310
+ highlight(vm);
311
+ }
278
312
  }
279
313
 
280
314
  _hoverLeave() {
@@ -299,7 +333,9 @@ export class VueDevToolsPanel extends LitElement {
299
333
  _selectVm(vm) {
300
334
  this.refresh();
301
335
  const path = this._pathToVm(vm);
302
- if (!path.length) return;
336
+ if (!path.length) {
337
+ return;
338
+ }
303
339
  for (let i = 0; i < path.length - 1; i++) this.expanded.add(path[i]);
304
340
  this._select(path[path.length - 1]);
305
341
  this._scrollToSelected = true;
@@ -314,10 +350,18 @@ export class VueDevToolsPanel extends LitElement {
314
350
  found = next;
315
351
  return true;
316
352
  }
317
- for (const c of node.children || []) if (dfs(c, next)) return true;
353
+ for (const child of node.children || []) {
354
+ if (dfs(child, next)) {
355
+ return true;
356
+ }
357
+ }
318
358
  return false;
319
359
  };
320
- for (const r of this.tree) if (dfs(r, [])) break;
360
+ for (const root of this.tree) {
361
+ if (dfs(root, [])) {
362
+ break;
363
+ }
364
+ }
321
365
  return found || [];
322
366
  }
323
367
 
@@ -327,19 +371,23 @@ export class VueDevToolsPanel extends LitElement {
327
371
  // match nested inside another match is not promoted (it shows in the subtree).
328
372
  _computeFilter() {
329
373
  const q = this.query.trim().toLowerCase();
330
- if (!q) return null;
374
+ if (!q) {
375
+ return null;
376
+ }
331
377
  const matched = new Set();
332
378
  const mark = node => {
333
- if (node.name.toLowerCase().includes(q)) matched.add(node.id);
334
- for (const c of node.children || []) mark(c);
379
+ if (node.name.toLowerCase().includes(q)) {
380
+ matched.add(node.id);
381
+ }
382
+ for (const child of node.children || []) mark(child);
335
383
  };
336
- for (const r of this.tree) mark(r);
384
+ for (const root of this.tree) mark(root);
337
385
 
338
386
  const roots = [];
339
387
  const show = new Set();
340
388
  const collect = node => {
341
389
  show.add(node.id);
342
- for (const c of node.children || []) collect(c);
390
+ for (const child of node.children || []) collect(child);
343
391
  };
344
392
  const walk = (node, hasMatchedAncestor) => {
345
393
  const isMatch = matched.has(node.id);
@@ -347,9 +395,9 @@ export class VueDevToolsPanel extends LitElement {
347
395
  roots.push(node);
348
396
  collect(node);
349
397
  }
350
- for (const c of node.children || []) walk(c, hasMatchedAncestor || isMatch);
398
+ for (const child of node.children || []) walk(child, hasMatchedAncestor || isMatch);
351
399
  };
352
- for (const r of this.tree) walk(r, false);
400
+ for (const root of this.tree) walk(root, false);
353
401
  return { roots, show, q };
354
402
  }
355
403
 
@@ -360,81 +408,130 @@ export class VueDevToolsPanel extends LitElement {
360
408
  const parent = new Map();
361
409
  const node = new Map();
362
410
  if (filter) {
363
- const walk = (n, p) => {
364
- node.set(n.id, n);
365
- parent.set(n.id, p);
366
- order.push(n.id);
367
- for (const c of n.children || []) {
368
- if (filter.show.has(c.id)) walk(c, n.id);
411
+ const walk = (treeNode, parentId) => {
412
+ node.set(treeNode.id, treeNode);
413
+ parent.set(treeNode.id, parentId);
414
+ order.push(treeNode.id);
415
+ if (this._searchCollapsed.has(treeNode.id)) {
416
+ return; // collapsed in search
417
+ }
418
+ for (const child of treeNode.children || []) {
419
+ if (filter.show.has(child.id)) {
420
+ walk(child, treeNode.id);
421
+ }
369
422
  }
370
423
  };
371
- for (const r of filter.roots) walk(r, null);
424
+ for (const root of filter.roots) walk(root, null);
372
425
  return { order, parent, node };
373
426
  }
374
- const walk = (n, p) => {
375
- node.set(n.id, n);
376
- parent.set(n.id, p);
377
- order.push(n.id);
378
- if (n.children && n.children.length && this.expanded.has(n.id)) {
379
- for (const c of n.children) walk(c, n.id);
427
+ const walk = (treeNode, parentId) => {
428
+ node.set(treeNode.id, treeNode);
429
+ parent.set(treeNode.id, parentId);
430
+ order.push(treeNode.id);
431
+ if (treeNode.children && treeNode.children.length && this.expanded.has(treeNode.id)) {
432
+ for (const child of treeNode.children) walk(child, treeNode.id);
380
433
  }
381
434
  };
382
- for (const r of this.tree) walk(r, null);
435
+ for (const root of this.tree) walk(root, null);
383
436
  return { order, parent, node };
384
437
  }
385
438
 
386
439
  // Arrow-key navigation once a component is selected (VS Code / devtools style):
387
440
  // ↑/↓ move through visible rows, → step into / expand, ← step out / collapse.
388
441
  // During search the subtree is always shown, so →/← just navigate in/out.
389
- _handleKeydown(e) {
390
- if (this.collapsed || this.selectedId == null) return;
391
- if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return;
442
+ _handleKeydown(event) {
443
+ if (this.collapsed) {
444
+ return;
445
+ }
446
+ if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
447
+ return;
448
+ }
392
449
  // Don't hijack arrow keys while typing in a field. composedPath sees into
393
450
  // our shadow root (the search box) as well as the app's own inputs.
394
- const path = e.composedPath ? e.composedPath() : [];
451
+ const path = event.composedPath ? event.composedPath() : [];
395
452
  const inEditable = path.some(el => el && el.tagName && (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable));
396
- if (inEditable) return;
453
+ if (inEditable) {
454
+ return;
455
+ }
456
+
457
+ // Timeline tab: ↑/↓ move between entries.
458
+ if (this.tab === 'timeline') {
459
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
460
+ return;
461
+ }
462
+ const entries = this._timelineEntries();
463
+ if (!entries.length) {
464
+ return;
465
+ }
466
+ const curIdx = entries.findIndex(item => item.id === this.timelineSelected);
467
+ const base = curIdx < 0 ? entries.length - 1 : curIdx;
468
+ const nextIdx = event.key === 'ArrowDown' ? Math.min(entries.length - 1, base + 1) : Math.max(0, base - 1);
469
+ this.timelineSelected = entries[nextIdx].id;
470
+ this._scrollToSelected = true;
471
+ event.preventDefault();
472
+ return;
473
+ }
397
474
 
475
+ // Components tab: tree navigation (needs a selected node).
476
+ if (this.selectedId == null) {
477
+ return;
478
+ }
398
479
  const filter = this._computeFilter();
399
480
  const searching = !!filter;
400
481
  const { order, parent, node } = this._index(filter);
401
482
  const id = this.selectedId;
402
483
  const cur = node.get(id);
403
- if (!cur) return;
404
- const i = order.indexOf(id);
405
- if (i < 0) return;
406
- const shownChildren = (cur.children || []).filter(c => !filter || filter.show.has(c.id));
484
+ if (!cur) {
485
+ return;
486
+ }
487
+ const index = order.indexOf(id);
488
+ if (index < 0) {
489
+ return;
490
+ }
491
+ const shownChildren = (cur.children || []).filter(item => !filter || filter.show.has(item.id));
407
492
  const hasChildren = shownChildren.length > 0;
408
- // A node is "open" if its children are currently shown. In search mode the
409
- // matched subtree is always shown.
410
- const isOpen = searching || this.expanded.has(id);
493
+ // "open" respects the active mode: normal tree uses `expanded`, search
494
+ // uses `_searchCollapsed` (default open).
495
+ const isOpen = searching ? !this._searchCollapsed.has(id) : this.expanded.has(id);
411
496
  let next = null;
412
497
 
413
- if (e.key === 'ArrowDown') {
414
- next = order[Math.min(order.length - 1, i + 1)];
415
- } else if (e.key === 'ArrowUp') {
416
- next = order[Math.max(0, i - 1)];
417
- } else if (e.key === 'ArrowRight') {
498
+ if (event.key === 'ArrowDown') {
499
+ next = order[Math.min(order.length - 1, index + 1)];
500
+ } else if (event.key === 'ArrowUp') {
501
+ next = order[Math.max(0, index - 1)];
502
+ } else if (event.key === 'ArrowRight') {
418
503
  // Closed with children -> open it; already open -> step into first child.
419
- if (hasChildren && !isOpen) this.expanded.add(id);
420
- else if (hasChildren && isOpen) next = shownChildren[0].id;
421
- } else if (e.key === 'ArrowLeft') {
504
+ if (hasChildren && !isOpen) {
505
+ searching ? this._searchCollapsed.delete(id) : this.expanded.add(id);
506
+ } else if (hasChildren && isOpen) {
507
+ next = shownChildren[0].id;
508
+ }
509
+ } else if (event.key === 'ArrowLeft') {
422
510
  // Check open/closed FIRST: open -> collapse; closed (or leaf) -> parent.
423
- if (isOpen && hasChildren && !searching) this.expanded.delete(id);
424
- else next = parent.get(id);
511
+ if (isOpen && hasChildren) {
512
+ searching ? this._searchCollapsed.add(id) : this.expanded.delete(id);
513
+ } else {
514
+ next = parent.get(id);
515
+ }
425
516
  }
426
517
 
427
- e.preventDefault();
428
- if (next != null) this._select(next);
518
+ event.preventDefault();
519
+ if (next != null) {
520
+ this._select(next);
521
+ }
429
522
  this._scrollToSelected = true;
430
523
  this.requestUpdate();
431
524
  }
432
525
 
433
526
  updated(changed) {
434
527
  // Persist remembered UI bits across reloads.
435
- if (changed && (changed.has('collapsed') || changed.has('tab'))) {
528
+ if (changed && (changed.has('collapsed') || changed.has('tab') || changed.has('theme'))) {
436
529
  this._persistUiState();
437
530
  }
531
+ // Reflect the theme onto the host so the CSS variable overrides apply.
532
+ if (changed && changed.has('theme')) {
533
+ this.setAttribute('theme', this.theme);
534
+ }
438
535
  // Re-clamp the docked position when switching fab <-> panel (sizes differ).
439
536
  if (changed && changed.has('collapsed')) {
440
537
  this._applyPos();
@@ -450,16 +547,20 @@ export class VueDevToolsPanel extends LitElement {
450
547
  }
451
548
  // When the query changes, auto-select the first match (like vue-devtools).
452
549
  if (changed && changed.has('query') && this.query.trim()) {
453
- const f = this._computeFilter();
454
- if (f && f.roots.length && !f.show.has(this.selectedId)) {
455
- this._select(f.roots[0].id);
550
+ const filter = this._computeFilter();
551
+ if (filter && filter.roots.length && !filter.show.has(this.selectedId)) {
552
+ this._select(filter.roots[0].id);
456
553
  this._scrollToSelected = true;
457
554
  }
458
555
  }
459
- if (!this._scrollToSelected) return;
556
+ if (!this._scrollToSelected) {
557
+ return;
558
+ }
460
559
  this._scrollToSelected = false;
461
560
  const el = this.renderRoot.querySelector('.node.selected');
462
- if (el) el.scrollIntoView({ block: 'nearest' });
561
+ if (el) {
562
+ el.scrollIntoView({ block: 'nearest' });
563
+ }
463
564
  }
464
565
 
465
566
  _renderNode(node, depth) {
@@ -477,8 +578,8 @@ export class VueDevToolsPanel extends LitElement {
477
578
  >
478
579
  <span
479
580
  class="caret-btn"
480
- @click=${e => {
481
- e.stopPropagation();
581
+ @click=${event => {
582
+ event.stopPropagation();
482
583
  this._toggle(node.id);
483
584
  }}
484
585
  >
@@ -486,7 +587,7 @@ export class VueDevToolsPanel extends LitElement {
486
587
  </span>
487
588
  <span class="tag">&lt;${node.name}&gt;</span>
488
589
  </div>
489
- ${hasChildren && isOpen ? node.children.map(c => this._renderNode(c, depth + 1)) : null}
590
+ ${hasChildren && isOpen ? node.children.map(item => this._renderNode(item, depth + 1)) : null}
490
591
  </div>
491
592
  `;
492
593
  }
@@ -494,8 +595,10 @@ export class VueDevToolsPanel extends LitElement {
494
595
  // Search result row: matched node as a subtree root, with all descendants
495
596
  // shown (always open). Ancestors are omitted. Arrow is non-interactive here.
496
597
  _renderSearchNode(node, depth, show) {
497
- const kids = (node.children || []).filter(c => show.has(c.id));
598
+ const kids = (node.children || []).filter(item => show.has(item.id));
599
+ const hasKids = kids.length > 0;
498
600
  const isSelected = node.id === this.selectedId;
601
+ const open = !this._searchCollapsed.has(node.id); // default open in search
499
602
  return html`
500
603
  <div>
501
604
  <div
@@ -505,24 +608,45 @@ export class VueDevToolsPanel extends LitElement {
505
608
  @mouseenter=${() => this._hoverEnter(node.id)}
506
609
  @mouseleave=${() => this._hoverLeave()}
507
610
  >
508
- <span class="caret-btn static">${this._caret(kids.length > 0, kids.length === 0)}</span>
611
+ <span
612
+ class="caret-btn"
613
+ @click=${event => {
614
+ event.stopPropagation();
615
+ if (hasKids) {
616
+ this._toggleSearchNode(node.id);
617
+ }
618
+ }}
619
+ >
620
+ ${this._caret(open && hasKids, !hasKids)}
621
+ </span>
509
622
  <span class="tag">&lt;${node.name}&gt;</span>
510
623
  </div>
511
- ${kids.map(c => this._renderSearchNode(c, depth + 1, show))}
624
+ ${hasKids && open ? kids.map(item => this._renderSearchNode(item, depth + 1, show)) : null}
512
625
  </div>
513
626
  `;
514
627
  }
515
628
 
629
+ _toggleSearchNode(id) {
630
+ if (this._searchCollapsed.has(id)) {
631
+ this._searchCollapsed.delete(id);
632
+ } else {
633
+ this._searchCollapsed.add(id);
634
+ }
635
+ this.requestUpdate();
636
+ }
637
+
516
638
  // Render an object as a titled, collapsible section of expandable value rows.
517
639
  // `editable` enables inline editing of primitive leaves (writes back into obj).
518
640
  // Sections with many keys get a live filter input in the header.
519
641
  _renderKvSection(title, obj, editable) {
520
642
  const allKeys = obj ? Object.keys(obj) : [];
521
- if (!allKeys.length) return null;
643
+ if (!allKeys.length) {
644
+ return null;
645
+ }
522
646
  const collapsed = this.sectionCollapsed.has(title);
523
- const showFilter = allKeys.length > SECTION_FILTER_THRESHOLD;
647
+ const showFilter = !collapsed && allKeys.length > SectionFilterThreshold;
524
648
  const q = (this._sectionFilter[title] || '').trim().toLowerCase();
525
- const keys = q ? allKeys.filter(k => k.toLowerCase().includes(q)) : allKeys;
649
+ const keys = q ? allKeys.filter(item => item.toLowerCase().includes(q)) : allKeys;
526
650
  return html`
527
651
  <div class="section-title" @click=${() => this._toggleSection(title)}>
528
652
  ${this._caret(!collapsed, false)}
@@ -534,16 +658,16 @@ export class VueDevToolsPanel extends LitElement {
534
658
  type="search"
535
659
  placeholder="filter…"
536
660
  .value=${this._sectionFilter[title] || ''}
537
- @click=${e => e.stopPropagation()}
538
- @keydown=${e => {
539
- if (e.key === 'Escape') {
661
+ @click=${event => event.stopPropagation()}
662
+ @keydown=${event => {
663
+ if (event.key === 'Escape') {
540
664
  this._sectionFilter[title] = '';
541
665
  this.requestUpdate();
542
666
  }
543
- e.stopPropagation();
667
+ event.stopPropagation();
544
668
  }}
545
- @input=${e => {
546
- this._sectionFilter[title] = e.target.value;
669
+ @input=${event => {
670
+ this._sectionFilter[title] = event.target.value;
547
671
  this.requestUpdate();
548
672
  }}
549
673
  />
@@ -553,7 +677,7 @@ export class VueDevToolsPanel extends LitElement {
553
677
  ${collapsed
554
678
  ? null
555
679
  : keys.length
556
- ? keys.map(k => this._renderValueRow(k, obj[k], `${title}.${k}`, 0, obj, editable))
680
+ ? keys.map(item => this._renderValueRow(item, obj[item], `${title}.${item}`, 0, obj, editable))
557
681
  : html`
558
682
  <div class="empty">No match</div>
559
683
  `}
@@ -561,14 +685,20 @@ export class VueDevToolsPanel extends LitElement {
561
685
  }
562
686
 
563
687
  _toggleSection(title) {
564
- if (this.sectionCollapsed.has(title)) this.sectionCollapsed.delete(title);
565
- else this.sectionCollapsed.add(title);
688
+ if (this.sectionCollapsed.has(title)) {
689
+ this.sectionCollapsed.delete(title);
690
+ } else {
691
+ this.sectionCollapsed.add(title);
692
+ }
566
693
  this.requestUpdate();
567
694
  }
568
695
 
569
696
  _toggleValue(path) {
570
- if (this.valueExpanded.has(path)) this.valueExpanded.delete(path);
571
- else this.valueExpanded.add(path);
697
+ if (this.valueExpanded.has(path)) {
698
+ this.valueExpanded.delete(path);
699
+ } else {
700
+ this.valueExpanded.add(path);
701
+ }
572
702
  this.requestUpdate();
573
703
  }
574
704
 
@@ -576,7 +706,7 @@ export class VueDevToolsPanel extends LitElement {
576
706
  // `editable` is set, primitive leaves can be clicked to edit in place.
577
707
  _renderValueRow(keyLabel, value, path, depth, parent, editable) {
578
708
  const isObj = value !== null && typeof value === 'object';
579
- const keys = isObj ? (Array.isArray(value) ? value.map((_, i) => i) : Object.keys(value)) : [];
709
+ const keys = isObj ? (Array.isArray(value) ? value.map((_, index) => index) : Object.keys(value)) : [];
580
710
  const expandable = isObj && keys.length > 0;
581
711
  const open = this.valueExpanded.has(path);
582
712
  const canEdit = editable && !isObj && typeof value !== 'function';
@@ -594,18 +724,20 @@ export class VueDevToolsPanel extends LitElement {
594
724
  <input
595
725
  class="edit-input"
596
726
  .value=${String(value)}
597
- @click=${e => e.stopPropagation()}
598
- @keydown=${e => this._onEditKeydown(e, parent, keyLabel, value)}
599
- @blur=${e => this._commitEdit(parent, keyLabel, e.target.value, value)}
727
+ @click=${event => event.stopPropagation()}
728
+ @keydown=${event => this._onEditKeydown(event, parent, keyLabel, value)}
729
+ @blur=${event => this._commitEdit(parent, keyLabel, event.target.value, value)}
600
730
  />
601
731
  `
602
732
  : html`
603
733
  <span
604
734
  class="val ${this._valClass(value)} ${canEdit ? 'editable' : ''}"
605
735
  title=${preview}
606
- @click=${e => {
607
- if (!canEdit) return;
608
- e.stopPropagation();
736
+ @click=${event => {
737
+ if (!canEdit) {
738
+ return;
739
+ }
740
+ event.stopPropagation();
609
741
  this._editingPath = path;
610
742
  this._focusEdit = true;
611
743
  this.requestUpdate();
@@ -619,8 +751,8 @@ export class VueDevToolsPanel extends LitElement {
619
751
  <button
620
752
  class="copy-btn"
621
753
  title="Copy value"
622
- @click=${e => {
623
- e.stopPropagation();
754
+ @click=${event => {
755
+ event.stopPropagation();
624
756
  this._copyValue(value);
625
757
  }}
626
758
  >
@@ -629,7 +761,7 @@ export class VueDevToolsPanel extends LitElement {
629
761
  `
630
762
  : null}
631
763
  </div>
632
- ${expandable && open ? keys.map(k => this._renderValueRow(k, value[k], `${path}.${k}`, depth + 1, value, editable)) : null}
764
+ ${expandable && open ? keys.map(item => this._renderValueRow(item, value[item], `${path}.${item}`, depth + 1, value, editable)) : null}
633
765
  `;
634
766
  }
635
767
 
@@ -638,7 +770,7 @@ export class VueDevToolsPanel extends LitElement {
638
770
  if (value !== null && typeof value === 'object') {
639
771
  try {
640
772
  text = JSON.stringify(value, null, 2);
641
- } catch (e) {
773
+ } catch (err) {
642
774
  text = String(value);
643
775
  }
644
776
  } else {
@@ -666,17 +798,17 @@ export class VueDevToolsPanel extends LitElement {
666
798
  ta.select();
667
799
  try {
668
800
  document.execCommand('copy');
669
- } catch (e) {
801
+ } catch (err) {
670
802
  /* ignore */
671
803
  }
672
804
  document.body.removeChild(ta);
673
805
  }
674
806
 
675
- _onEditKeydown(e, parent, key, oldValue) {
676
- e.stopPropagation();
677
- if (e.key === 'Enter') {
678
- this._commitEdit(parent, key, e.target.value, oldValue);
679
- } else if (e.key === 'Escape') {
807
+ _onEditKeydown(event, parent, key, oldValue) {
808
+ event.stopPropagation();
809
+ if (event.key === 'Enter') {
810
+ this._commitEdit(parent, key, event.target.value, oldValue);
811
+ } else if (event.key === 'Escape') {
680
812
  this._editingPath = null;
681
813
  this.requestUpdate();
682
814
  }
@@ -687,34 +819,47 @@ export class VueDevToolsPanel extends LitElement {
687
819
  _commitEdit(parent, key, rawStr, oldValue) {
688
820
  this._editingPath = null;
689
821
  let parsed = rawStr;
690
- const t = typeof oldValue;
691
- if (t === 'number') {
692
- const n = Number(rawStr);
693
- parsed = Number.isNaN(n) ? oldValue : n;
694
- } else if (t === 'boolean') {
822
+ const type = typeof oldValue;
823
+ if (type === 'number') {
824
+ const num = Number(rawStr);
825
+ parsed = Number.isNaN(num) ? oldValue : num;
826
+ } else if (type === 'boolean') {
695
827
  parsed = rawStr === 'true' || rawStr === '1';
696
828
  } else if (oldValue === null || oldValue === undefined) {
697
829
  try {
698
830
  parsed = JSON.parse(rawStr);
699
- } catch (e) {
831
+ } catch (err) {
700
832
  parsed = rawStr;
701
833
  }
702
834
  }
703
835
  const Vue = hook.Vue;
704
836
  if (parent) {
705
- if (Vue && Vue.set) Vue.set(parent, key, parsed);
706
- else parent[key] = parsed;
837
+ if (Vue && Vue.set) {
838
+ Vue.set(parent, key, parsed);
839
+ } else {
840
+ parent[key] = parsed;
841
+ }
707
842
  }
708
843
  this.requestUpdate();
709
844
  }
710
845
 
711
846
  _valClass(value) {
712
- if (value === null || value === undefined) return 'v-null';
713
- const t = typeof value;
714
- if (t === 'number') return 'v-num';
715
- if (t === 'boolean') return 'v-bool';
716
- if (t === 'string') return 'v-str';
717
- if (t === 'function') return 'v-fn';
847
+ if (value === null || value === undefined) {
848
+ return 'v-null';
849
+ }
850
+ const type = typeof value;
851
+ if (type === 'number') {
852
+ return 'v-num';
853
+ }
854
+ if (type === 'boolean') {
855
+ return 'v-bool';
856
+ }
857
+ if (type === 'string') {
858
+ return 'v-str';
859
+ }
860
+ if (type === 'function') {
861
+ return 'v-fn';
862
+ }
718
863
  return 'v-obj';
719
864
  }
720
865
 
@@ -734,20 +879,21 @@ export class VueDevToolsPanel extends LitElement {
734
879
  `;
735
880
  }
736
881
  const vm = getInstance(this.selectedId);
737
- if (!vm)
882
+ if (!vm) {
738
883
  return html`
739
884
  <div class="empty">Component unmounted</div>
740
885
  `;
886
+ }
741
887
 
742
888
  const propsObj = vm._props || {};
743
889
  const dataObj = vm._data || vm.$data || {};
744
890
  const compDefs = (vm.$options && vm.$options.computed) || {};
745
891
  const compObj = {};
746
- for (const k of Object.keys(compDefs)) {
892
+ for (const key of Object.keys(compDefs)) {
747
893
  try {
748
- compObj[k] = vm[k];
894
+ compObj[key] = vm[key];
749
895
  } catch (err) {
750
- compObj[k] = `⚠ ${err && err.message}`;
896
+ compObj[key] = `⚠ ${err && err.message}`;
751
897
  }
752
898
  }
753
899
  const attrsObj = vm.$attrs || {};
@@ -790,27 +936,34 @@ export class VueDevToolsPanel extends LitElement {
790
936
  }
791
937
 
792
938
  _vmName(vm) {
793
- const o = vm.$options || {};
794
- let n = o.name || o._componentTag;
795
- if (!n && o.__file)
796
- n = String(o.__file)
939
+ const options = vm.$options || {};
940
+ let name = options.name || options._componentTag;
941
+ if (!name && options.__file) {
942
+ name = String(options.__file)
797
943
  .split(/[\\/]/)
798
944
  .pop()
799
945
  .replace(/\.vue$/, '');
800
- if (!n && vm.$root === vm) n = 'Root';
801
- return n || 'Anonymous';
946
+ }
947
+ if (!name && vm.$root === vm) {
948
+ name = 'Root';
949
+ }
950
+ return name || 'Anonymous';
802
951
  }
803
952
 
804
953
  // Ask the Vite dev server to open the component's source file in the editor.
805
954
  _openInEditor(file) {
806
- if (!file) return;
955
+ if (!file) {
956
+ return;
957
+ }
807
958
  fetch('/__open-in-editor?file=' + encodeURIComponent(file)).catch(() => {});
808
959
  }
809
960
 
810
961
  // Scroll the component's root DOM element into view and flash the highlight.
811
962
  _scrollToComponent(vm) {
812
963
  const el = vm && vm.$el;
813
- if (!el || !el.scrollIntoView) return;
964
+ if (!el || !el.scrollIntoView) {
965
+ return;
966
+ }
814
967
  el.scrollIntoView({
815
968
  behavior: 'smooth',
816
969
  block: 'center',
@@ -833,17 +986,23 @@ export class VueDevToolsPanel extends LitElement {
833
986
  const lines = code.split('\n');
834
987
  let min = Infinity;
835
988
  for (let i = 1; i < lines.length; i++) {
836
- if (!lines[i].trim()) continue;
989
+ if (!lines[i].trim()) {
990
+ continue;
991
+ }
837
992
  const indent = lines[i].match(/^[ \t]*/)[0].length;
838
- if (indent < min) min = indent;
993
+ if (indent < min) {
994
+ min = indent;
995
+ }
839
996
  }
840
- if (!isFinite(min) || min === 0) return code;
841
- return lines.map((l, i) => (i === 0 ? l : l.slice(min))).join('\n');
997
+ if (!isFinite(min) || min === 0) {
998
+ return code;
999
+ }
1000
+ return lines.map((item, index) => (index === 0 ? item : item.slice(min))).join('\n');
842
1001
  }
843
1002
 
844
1003
  render() {
845
1004
  return html`
846
- <div class="entry" @pointerdown=${e => this._onFabPointerDown(e)}>
1005
+ <div class="entry" @pointerdown=${event => this._onFabPointerDown(event)}>
847
1006
  <span class="fab-icon">${this._vueLogo()}</span>
848
1007
  </div>
849
1008
  ${this.collapsed ? null : this._renderPanel()}
@@ -876,6 +1035,10 @@ export class VueDevToolsPanel extends LitElement {
876
1035
  </button>
877
1036
  `
878
1037
  : null}
1038
+ <button class="side-tab" @click=${() => this._toggleTheme()}>
1039
+ ${this._icon(this.theme === 'dark' ? 'sun' : 'moon')}
1040
+ <span class="tip">${this.theme === 'dark' ? 'Light theme' : 'Dark theme'}</span>
1041
+ </button>
879
1042
  <button class="side-tab side-tab--min" @click=${() => (this.collapsed = true)}>
880
1043
  ${this._icon('min')}
881
1044
  <span class="tip">Minimize</span>
@@ -909,11 +1072,11 @@ export class VueDevToolsPanel extends LitElement {
909
1072
  type="search"
910
1073
  placeholder="Search components…"
911
1074
  .value=${this.query}
912
- @input=${e => (this.query = e.target.value)}
913
- @keydown=${e => {
914
- if (e.key === 'Escape') {
1075
+ @input=${event => (this.query = event.target.value)}
1076
+ @keydown=${event => {
1077
+ if (event.key === 'Escape') {
915
1078
  this.query = '';
916
- e.stopPropagation();
1079
+ event.stopPropagation();
917
1080
  }
918
1081
  }}
919
1082
  />
@@ -929,8 +1092,8 @@ export class VueDevToolsPanel extends LitElement {
929
1092
  <div class="empty">No component matches</div>
930
1093
  `
931
1094
  : filter
932
- ? filter.roots.map(n => this._renderSearchNode(n, 0, filter.show))
933
- : this.tree.map(n => this._renderNode(n, 0))}
1095
+ ? filter.roots.map(item => this._renderSearchNode(item, 0, filter.show))
1096
+ : this.tree.map(item => this._renderNode(item, 0))}
934
1097
  </div>
935
1098
  <div class="detail">${this._renderDetail()}</div>
936
1099
  </div>
@@ -964,10 +1127,10 @@ export class VueDevToolsPanel extends LitElement {
964
1127
  </button>
965
1128
  </div>
966
1129
  ${snaps.map(
967
- (s, i) => html`
968
- <div class="node ${i === sel ? 'selected' : ''}" @click=${() => (this.vuexSelected = i)}>
969
- <span class="mut-index">${s.base ? '' : i}</span>
970
- <span class="tag">${s.base ? 'Base State' : s.type}</span>
1130
+ (item, index) => html`
1131
+ <div class="node ${index === sel ? 'selected' : ''}" @click=${() => (this.vuexSelected = index)}>
1132
+ <span class="mut-index">${item.base ? '' : index}</span>
1133
+ <span class="tag">${item.base ? 'Base State' : item.type}</span>
971
1134
  </div>
972
1135
  `
973
1136
  )}
@@ -995,14 +1158,16 @@ export class VueDevToolsPanel extends LitElement {
995
1158
  // (like the vue-devtools v7 Timeline).
996
1159
  _timelineEntries() {
997
1160
  const entries = [];
998
- for (const e of getEvents()) {
999
- entries.push({ id: `e${e.id}`, kind: 'event', time: e.time, title: e.name, sub: `<${e.component}>`, event: e });
1161
+ for (const event of getEvents()) {
1162
+ entries.push({ id: `e${event.id}`, kind: 'event', time: event.time, title: event.name, sub: `<${event.component}>`, event: event });
1000
1163
  }
1001
- getSnapshots().forEach((s, i) => {
1002
- if (s.base) return;
1003
- entries.push({ id: `m${i}`, kind: 'mutation', time: s.time || 0, title: s.type, sub: 'vuex', snap: s, index: i });
1164
+ getSnapshots().forEach((item, index) => {
1165
+ if (item.base) {
1166
+ return;
1167
+ }
1168
+ entries.push({ id: `m${index}`, kind: 'mutation', time: item.time || 0, title: item.type, sub: 'vuex', snap: item, index });
1004
1169
  });
1005
- entries.sort((a, b) => a.time - b.time);
1170
+ entries.sort((first, second) => first.time - second.time);
1006
1171
  return entries;
1007
1172
  }
1008
1173
 
@@ -1013,7 +1178,7 @@ export class VueDevToolsPanel extends LitElement {
1013
1178
  <div class="body"><div class="empty">No timeline activity yet</div></div>
1014
1179
  `;
1015
1180
  }
1016
- const sel = entries.find(e => e.id === this.timelineSelected) || entries[entries.length - 1];
1181
+ const sel = entries.find(item => item.id === this.timelineSelected) || entries[entries.length - 1];
1017
1182
  return html`
1018
1183
  <div class="body">
1019
1184
  <div class="tree">
@@ -1030,12 +1195,11 @@ export class VueDevToolsPanel extends LitElement {
1030
1195
  </button>
1031
1196
  </div>
1032
1197
  ${entries.map(
1033
- e => html`
1034
- <div class="node ${e.id === sel.id ? 'selected' : ''}" @click=${() => (this.timelineSelected = e.id)}>
1035
- <span class="tl-badge tl-${e.kind}">${e.kind === 'event' ? 'evt' : 'mut'}</span>
1036
- <span class="ev-time">${this._formatTime(e.time)}</span>
1037
- <span class="tag">${e.title}</span>
1038
- <span class="ev-comp">${e.sub}</span>
1198
+ item => html`
1199
+ <div class="node ${item.id === sel.id ? 'selected' : ''}" @click=${() => (this.timelineSelected = item.id)}>
1200
+ <span class="ev-time">${this._formatTime(item.time)}</span>
1201
+ <span class="tag">${item.title}</span>
1202
+ <span class="ev-comp">${item.sub}</span>
1039
1203
  </div>
1040
1204
  `
1041
1205
  )}
@@ -1046,44 +1210,46 @@ export class VueDevToolsPanel extends LitElement {
1046
1210
  }
1047
1211
 
1048
1212
  _renderTimelineDetail(entry) {
1049
- if (!entry) return null;
1213
+ if (!entry) {
1214
+ return null;
1215
+ }
1050
1216
  if (entry.kind === 'event') {
1051
- const e = entry.event;
1217
+ const event = entry.event;
1052
1218
  const argsObj = {};
1053
- (e.args || []).forEach((a, i) => {
1054
- argsObj[i] = a;
1219
+ (event.args || []).forEach((item, index) => {
1220
+ argsObj[index] = item;
1055
1221
  });
1056
1222
  return html`
1057
- ${this._renderKvSection('event', { name: e.name, from: e.component, time: new Date(e.time).toLocaleTimeString() }, false)}
1058
- ${e.args && e.args.length
1223
+ ${this._renderKvSection('event', { name: event.name, from: event.component, time: new Date(event.time).toLocaleTimeString() }, false)}
1224
+ ${event.args && event.args.length
1059
1225
  ? this._renderKvSection('payload', argsObj, false)
1060
1226
  : html`
1061
1227
  <div class="empty">No payload</div>
1062
1228
  `}
1063
1229
  `;
1064
1230
  }
1065
- const s = entry.snap;
1231
+ const snap = entry.snap;
1066
1232
  const store = getStore();
1067
- const payloadObj = s.payload === undefined ? null : { payload: s.payload };
1233
+ const payloadObj = snap.payload === undefined ? null : { payload: snap.payload };
1068
1234
  return html`
1069
1235
  <button class="btn on time-travel" @click=${() => travelTo(entry.index)}>⏱ Time Travel</button>
1070
- ${this._renderKvSection('mutation', { type: s.type, time: new Date(s.time).toLocaleTimeString() }, false)}
1236
+ ${this._renderKvSection('mutation', { type: snap.type, time: new Date(snap.time).toLocaleTimeString() }, false)}
1071
1237
  ${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null}
1072
- ${this._renderKvSection('state', s.state || {}, false)}
1238
+ ${this._renderKvSection('state', snap.state || {}, false)}
1073
1239
  ${store ? this._renderKvSection('getters (live)', store.getters || {}, false) : null}
1074
1240
  `;
1075
1241
  }
1076
1242
 
1077
- _formatTime(t) {
1078
- const d = new Date(t);
1079
- const p = (n, l = 2) => String(n).padStart(l, '0');
1080
- return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
1243
+ _formatTime(time) {
1244
+ const date = new Date(time);
1245
+ const pad = (num, length = 2) => String(num).padStart(length, '0');
1246
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
1081
1247
  }
1082
1248
 
1083
1249
  // Plugin logo
1084
1250
  _vueLogo() {
1085
1251
  return html`
1086
- <svg fill-rule="evenodd" viewBox="64 64 896 896" fill="#2932E1" aria-hidden="true">
1252
+ <svg fill-rule="evenodd" viewBox="64 64 896 896" fill="var(--logo-fill)" aria-hidden="true">
1087
1253
  <path
1088
1254
  d="M250.02 547.04c92.37-19.8 79.77-130.07 76.95-154.18-4.56-37.2-48.26-102.16-107.63-97.02-74.7 6.7-85.65 114.58-85.65 114.58-10.04 49.88 24.2 156.43 116.33 136.62m84.7 214.14c10.28 38.7 43.95 40.43 43.95 40.43H427V683.55h-51.74c-23.22 6.96-34.5 25.1-36.98 32.8-2.74 7.8-8.71 27.6-3.57 44.83m169.07-531.1c0-72.42-41.13-131.08-92.2-131.08-50.92 0-92.21 58.66-92.21 131.07 0 72.5 41.3 131.16 92.2 131.16 51.08 0 92.21-58.66 92.21-131.16m248.1 9.1c8.86-54.92-35.08-118.88-83.34-129.82-48.34-11.1-108.7 66.28-114.18 116.74-6.55 61.72 8.79 123.28 76.86 132.06 68.16 8.87 112.03-63.87 120.65-118.97m46.35 433.02s-105.47-81.53-167-169.6c-83.4-129.91-201.98-77.05-241.62-11.02-39.47 66.03-101 107.87-109.7 118.9-8.87 10.93-127.36 74.8-101.07 191.55 26.28 116.65 118.73 114.5 118.73 114.5s68.08 6.7 147.1-10.94C523.7 888.03 591.7 910 591.7 910s184.57 61.72 235.07-57.18c50.41-118.97-28.53-180.61-28.53-180.61M362.42 849.17c-51.83-10.36-72.47-45.65-75.13-51.7-2.57-6.13-17.24-34.55-9.45-82.85 22.39-72.41 86.23-77.63 86.23-77.63h63.85v-78.46l54.4.82.08 289.82zm205.38-.83c-53.56-13.75-56.05-51.78-56.05-51.78V643.95l56.05-.92v137.12c3.4 14.59 21.65 17.32 21.65 17.32h56.88V643.95h59.62v204.39zm323.84-397.72c0-26.35-21.89-105.72-103.15-105.72-81.43 0-92.29 74.9-92.29 127.84 0 50.54 4.31 121.13 105.4 118.8 101.15-2.15 90.04-114.41 90.04-140.92"
1089
1255
  />
@@ -1094,6 +1260,19 @@ export class VueDevToolsPanel extends LitElement {
1094
1260
  // Inline stroked icons (currentColor) for the sidebar / toolbar.
1095
1261
  _icon(name) {
1096
1262
  switch (name) {
1263
+ case 'moon':
1264
+ return html`
1265
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1266
+ <path d="M21 12.8A9 9 0 1111.2 3 7 7 0 0021 12.8z" />
1267
+ </svg>
1268
+ `;
1269
+ case 'sun':
1270
+ return html`
1271
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1272
+ <circle cx="12" cy="12" r="4" />
1273
+ <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
1274
+ </svg>
1275
+ `;
1097
1276
  case 'components':
1098
1277
  return html`
1099
1278
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
@@ -1199,6 +1378,7 @@ export class VueDevToolsPanel extends LitElement {
1199
1378
  --c-null: #b45309;
1200
1379
  --c-fn: #2563eb;
1201
1380
  --c-obj: #475467;
1381
+ --logo-fill: #2932e1;
1202
1382
 
1203
1383
  position: fixed;
1204
1384
  inset: 0;
@@ -1208,6 +1388,25 @@ export class VueDevToolsPanel extends LitElement {
1208
1388
  font-size: 12px;
1209
1389
  color: var(--text);
1210
1390
  }
1391
+ :host([theme='dark']) {
1392
+ --bg: #1e1e20;
1393
+ --surface: #26262a;
1394
+ --border: #34343a;
1395
+ --border-strong: #3a3a42;
1396
+ --field-border: #3a3a42;
1397
+ --text: #e4e4e7;
1398
+ --text-strong: #f4f4f5;
1399
+ --muted: #8b8b93;
1400
+ --muted-2: #a1a1aa;
1401
+ --c-key: #80cbc4;
1402
+ --c-num: #ffcb6b;
1403
+ --c-bool: #c792ea;
1404
+ --c-str: #c3e88d;
1405
+ --c-null: #f78c6c;
1406
+ --c-fn: #82aaff;
1407
+ --c-obj: #b0bec5;
1408
+ --logo-fill: #ffffff;
1409
+ }
1211
1410
  .entry {
1212
1411
  position: fixed;
1213
1412
  pointer-events: auto;
@@ -1571,7 +1770,7 @@ export class VueDevToolsPanel extends LitElement {
1571
1770
  cursor: pointer;
1572
1771
 
1573
1772
  &:hover {
1574
- color: #475467;
1773
+ color: var(--text);
1575
1774
  }
1576
1775
  & .section-filter {
1577
1776
  margin-inline-start: 6px;