vite-plugin-devtools-vue2 0.1.2 → 0.1.5

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,12 +3,12 @@
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, IdleTuckDelay, IdleTuckMargin, PanelEdge, PanelH, PanelW, SectionFilterThreshold, StoreKey } from './config.js';
7
+ import { clearEvents, subscribe as eventsSubscribe, getEvents } from './events.js';
7
8
  import hook from './hook.js';
8
9
  import { hide, highlight } from './inspector.js';
9
10
  import { isPicking, startPicking, stopPicking } from './picker.js';
10
11
  import { commitAll, getSnapshots, getStore, hasStore, travelTo, subscribe as vuexSubscribe } from './vuex.js';
11
- import { clearEvents, getEvents, subscribe as eventsSubscribe } from './events.js';
12
12
  import { buildTree, formatValue, getInstance } from './walker.js';
13
13
 
14
14
  export class VueDevToolsPanel extends LitElement {
@@ -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,15 +39,21 @@ 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
47
51
  // the right; { edge: 'left'|'right'|'top'|'bottom', along: number }.
48
52
  this._pos = ui.pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
49
53
  this._drag = null;
54
+ // When collapsed + idle, the entry tucks toward the edge (see IdleTuck*).
55
+ this._idleTucked = false;
56
+ this._idleTimer = 0;
50
57
  this._editingPath = null;
51
58
  this._focusEdit = false;
52
59
  this._flushTimer = null;
@@ -59,14 +66,16 @@ export class VueDevToolsPanel extends LitElement {
59
66
  // derived from `el.__vue__` anyway.
60
67
  this._domObserver = null;
61
68
  this._onFlush = () => this._scheduleRefresh();
62
- this._onKeydown = e => this._handleKeydown(e);
69
+ this._onKeydown = event => this._handleKeydown(event);
63
70
  // Keep the entry/panel on-screen when the viewport shrinks. resize can
64
71
  // fire many times per second while dragging the window edge, and
65
72
  // _applyPos reads layout then writes styles — so coalesce to at most one
66
73
  // call per frame via rAF to avoid layout thrashing.
67
74
  this._resizeRaf = 0;
68
75
  this._onResize = () => {
69
- if (this._resizeRaf) return;
76
+ if (this._resizeRaf) {
77
+ return;
78
+ }
70
79
  this._resizeRaf = requestAnimationFrame(() => {
71
80
  this._resizeRaf = 0;
72
81
  this._applyPos();
@@ -76,8 +85,8 @@ export class VueDevToolsPanel extends LitElement {
76
85
 
77
86
  static _loadUiState() {
78
87
  try {
79
- return JSON.parse(localStorage.getItem(STORE_KEY)) || {};
80
- } catch (e) {
88
+ return JSON.parse(localStorage.getItem(StoreKey)) || {};
89
+ } catch (err) {
81
90
  return {};
82
91
  }
83
92
  }
@@ -85,8 +94,8 @@ export class VueDevToolsPanel extends LitElement {
85
94
  _persistUiState() {
86
95
  try {
87
96
  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) {
97
+ localStorage.setItem(StoreKey, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, theme: this.theme, pos }));
98
+ } catch (err) {
90
99
  /* storage unavailable — ignore */
91
100
  }
92
101
  }
@@ -96,97 +105,111 @@ export class VueDevToolsPanel extends LitElement {
96
105
  // the viewport; clamped to stay fully on-screen.
97
106
  _applyPos() {
98
107
  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;
108
+ if (!entry) {
109
+ return;
110
+ }
111
+ const pos = this._pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
112
+ const margin = EdgeMargin;
102
113
  const vw = window.innerWidth;
103
114
  const vh = window.innerHeight;
104
115
  const er = entry.getBoundingClientRect();
105
116
  const ew = er.width || 40;
106
117
  const eh = er.height || 40;
107
- const clamp = (v, max) => Math.min(Math.max(M, v), Math.max(M, max));
118
+ const clamp = (value, max) => Math.min(Math.max(margin, value), Math.max(margin, max));
108
119
 
109
120
  // Clamped offset of the entry along its docked edge. We derive the panel
110
121
  // position from these numbers directly rather than re-reading the entry's
111
122
  // live rect — on a fresh open/refresh that rect can still be stale (reads
112
123
  // ~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);
124
+ const alongX = clamp(pos.along, vw - ew - margin);
125
+ const alongY = clamp(pos.along, vh - eh - margin);
115
126
 
116
127
  const es = entry.style;
128
+ // When collapsed and idle, tuck the entry toward the edge (smaller /
129
+ // negative margin) so it peeks; hovering/interacting restores it.
130
+ const dockMargin = this.collapsed && this._idleTucked ? IdleTuckMargin : EdgeMargin;
117
131
  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';
132
+ if (pos.edge === 'right' || pos.edge === 'left') {
133
+ es['inset' + (pos.edge === 'right' ? 'InlineEnd' : 'InlineStart')] = dockMargin + 'px';
120
134
  es.insetBlockStart = alongY + 'px';
121
135
  } else {
122
- es['inset' + (p.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = M + 'px';
136
+ es['inset' + (pos.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = dockMargin + 'px';
123
137
  es.insetInlineStart = alongX + 'px';
124
138
  }
125
- this.setAttribute('dock', p.edge);
139
+ this.setAttribute('dock', pos.edge);
126
140
 
127
141
  const panel = this.renderRoot.querySelector('.panel');
128
- if (!panel) return;
142
+ if (!panel) {
143
+ return;
144
+ }
129
145
  const ps = panel.style;
130
146
  ps.insetInlineStart = ps.insetBlockStart = ps.insetInlineEnd = ps.insetBlockEnd = 'auto';
131
147
  // Entry center along its edge, computed from the clamped offsets above.
132
148
  const cx = alongX + ew / 2;
133
149
  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';
150
+ if (pos.edge === 'right') {
151
+ ps.insetInlineEnd = PanelEdge + 'px';
152
+ ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
153
+ } else if (pos.edge === 'left') {
154
+ ps.insetInlineStart = PanelEdge + 'px';
155
+ ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
156
+ } else if (pos.edge === 'top') {
157
+ ps.insetBlockStart = PanelEdge + 'px';
158
+ ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
143
159
  } else {
144
- ps.insetBlockEnd = PANEL_EDGE + 'px';
145
- ps.insetInlineStart = clamp(cx - PANEL_W / 2, vw - PANEL_W - M) + 'px';
160
+ ps.insetBlockEnd = PanelEdge + 'px';
161
+ ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
146
162
  }
147
163
  }
148
164
 
149
165
  // Drag the always-visible entry. Movement over a threshold = drag (live snap
150
166
  // to nearest edge, panel follows); a plain click toggles the panel.
151
- _startDrag(e) {
152
- if (e.button !== 0) return;
153
- e.preventDefault();
167
+ _startDrag(event) {
168
+ if (event.button !== 0) {
169
+ return;
170
+ }
171
+ event.preventDefault();
154
172
  hide(); // clear any hover highlight before dragging
173
+ this._wakeEntry();
155
174
  const entry = this.renderRoot.querySelector('.entry');
156
175
  const rect = entry.getBoundingClientRect();
157
176
  this._drag = {
158
- startX: e.clientX,
159
- startY: e.clientY,
160
- offX: e.clientX - rect.left,
161
- offY: e.clientY - rect.top,
177
+ startX: event.clientX,
178
+ startY: event.clientY,
179
+ offX: event.clientX - rect.left,
180
+ offY: event.clientY - rect.top,
162
181
  moved: false
163
182
  };
164
- this._onDragMove = ev => this._dragMove(ev);
165
- this._onDragUp = ev => this._dragUp(ev);
183
+ this._onDragMove = event => this._dragMove(event);
184
+ this._onDragUp = event => this._dragUp(event);
166
185
  window.addEventListener('pointermove', this._onDragMove, true);
167
186
  window.addEventListener('pointerup', this._onDragUp, true);
168
187
  }
169
188
 
170
- _onFabPointerDown(e) {
171
- this._startDrag(e);
189
+ _onFabPointerDown(event) {
190
+ this._startDrag(event);
172
191
  }
173
192
 
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;
193
+ _dragMove(event) {
194
+ const drag = this._drag;
195
+ if (!drag) {
196
+ return;
197
+ }
198
+ if (!drag.moved && Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < DragThreshold) {
199
+ return;
200
+ }
201
+ drag.moved = true;
179
202
  // Snap to the nearest edge live during the drag (not on release).
180
203
  const vw = window.innerWidth;
181
204
  const vh = window.innerHeight;
182
205
  const dist = {
183
- left: e.clientX,
184
- right: vw - e.clientX,
185
- top: e.clientY,
186
- bottom: vh - e.clientY
206
+ left: event.clientX,
207
+ right: vw - event.clientX,
208
+ top: event.clientY,
209
+ bottom: vh - event.clientY
187
210
  };
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;
211
+ const edge = Object.keys(dist).reduce((best, current) => (dist[current] < dist[best] ? current : best));
212
+ const along = edge === 'left' || edge === 'right' ? event.clientY - drag.offY : event.clientX - drag.offX;
190
213
  this._pos = { edge, along };
191
214
  this._applyPos();
192
215
  }
@@ -194,16 +217,40 @@ export class VueDevToolsPanel extends LitElement {
194
217
  _dragUp() {
195
218
  window.removeEventListener('pointermove', this._onDragMove, true);
196
219
  window.removeEventListener('pointerup', this._onDragUp, true);
197
- const d = this._drag;
220
+ const drag = this._drag;
198
221
  this._drag = null;
199
- if (!d) return;
200
- if (!d.moved) {
222
+ if (!drag) {
223
+ return;
224
+ }
225
+ if (!drag.moved) {
201
226
  // plain click → toggle the panel
202
227
  this.collapsed = !this.collapsed;
203
228
  return;
204
229
  }
205
230
  // Position was already decided live in _dragMove; just remember it.
206
231
  this._persistUiState();
232
+ this._scheduleIdleTuck();
233
+ }
234
+
235
+ // Idle-tuck: when collapsed with no interaction, the entry slides toward the
236
+ // edge; hovering/dragging/opening wakes it back to the normal margin.
237
+ _scheduleIdleTuck() {
238
+ clearTimeout(this._idleTimer);
239
+ if (!this.collapsed) {
240
+ return;
241
+ }
242
+ this._idleTimer = setTimeout(() => {
243
+ this._idleTucked = true;
244
+ this._applyPos();
245
+ }, IdleTuckDelay);
246
+ }
247
+
248
+ _wakeEntry() {
249
+ clearTimeout(this._idleTimer);
250
+ if (this._idleTucked) {
251
+ this._idleTucked = false;
252
+ this._applyPos();
253
+ }
207
254
  }
208
255
 
209
256
  connectedCallback() {
@@ -228,6 +275,7 @@ export class VueDevToolsPanel extends LitElement {
228
275
  hook.off('flush', this._onFlush);
229
276
  window.removeEventListener('keydown', this._onKeydown, true);
230
277
  window.removeEventListener('resize', this._onResize);
278
+ clearTimeout(this._idleTimer);
231
279
  if (this._resizeRaf) {
232
280
  cancelAnimationFrame(this._resizeRaf);
233
281
  this._resizeRaf = 0;
@@ -236,13 +284,23 @@ export class VueDevToolsPanel extends LitElement {
236
284
  this._domObserver.disconnect();
237
285
  this._domObserver = null;
238
286
  }
239
- if (this._vuexUnsub) this._vuexUnsub();
240
- if (this._eventsUnsub) this._eventsUnsub();
287
+ if (this._vuexUnsub) {
288
+ this._vuexUnsub();
289
+ }
290
+ if (this._eventsUnsub) {
291
+ this._eventsUnsub();
292
+ }
241
293
  stopPicking();
242
294
  }
243
295
 
244
296
  firstUpdated() {
245
297
  this._applyPos();
298
+ this.setAttribute('theme', this.theme);
299
+ this._scheduleIdleTuck();
300
+ }
301
+
302
+ _toggleTheme() {
303
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
246
304
  }
247
305
 
248
306
  _scheduleRefresh() {
@@ -265,16 +323,23 @@ export class VueDevToolsPanel extends LitElement {
265
323
  }
266
324
 
267
325
  _toggle(id) {
268
- if (this.expanded.has(id)) this.expanded.delete(id);
269
- else this.expanded.add(id);
326
+ if (this.expanded.has(id)) {
327
+ this.expanded.delete(id);
328
+ } else {
329
+ this.expanded.add(id);
330
+ }
270
331
  this.requestUpdate();
271
332
  }
272
333
 
273
334
  // Highlight the page DOM only while hovering a tree row (vue-devtools style).
274
335
  _hoverEnter(id) {
275
- if (this._drag) return; // don't highlight while dragging the entry/panel
336
+ if (this._drag) {
337
+ return; // don't highlight while dragging the entry/panel
338
+ }
276
339
  const vm = getInstance(id);
277
- if (vm) highlight(vm);
340
+ if (vm) {
341
+ highlight(vm);
342
+ }
278
343
  }
279
344
 
280
345
  _hoverLeave() {
@@ -299,7 +364,9 @@ export class VueDevToolsPanel extends LitElement {
299
364
  _selectVm(vm) {
300
365
  this.refresh();
301
366
  const path = this._pathToVm(vm);
302
- if (!path.length) return;
367
+ if (!path.length) {
368
+ return;
369
+ }
303
370
  for (let i = 0; i < path.length - 1; i++) this.expanded.add(path[i]);
304
371
  this._select(path[path.length - 1]);
305
372
  this._scrollToSelected = true;
@@ -314,10 +381,18 @@ export class VueDevToolsPanel extends LitElement {
314
381
  found = next;
315
382
  return true;
316
383
  }
317
- for (const c of node.children || []) if (dfs(c, next)) return true;
384
+ for (const child of node.children || []) {
385
+ if (dfs(child, next)) {
386
+ return true;
387
+ }
388
+ }
318
389
  return false;
319
390
  };
320
- for (const r of this.tree) if (dfs(r, [])) break;
391
+ for (const root of this.tree) {
392
+ if (dfs(root, [])) {
393
+ break;
394
+ }
395
+ }
321
396
  return found || [];
322
397
  }
323
398
 
@@ -327,19 +402,23 @@ export class VueDevToolsPanel extends LitElement {
327
402
  // match nested inside another match is not promoted (it shows in the subtree).
328
403
  _computeFilter() {
329
404
  const q = this.query.trim().toLowerCase();
330
- if (!q) return null;
405
+ if (!q) {
406
+ return null;
407
+ }
331
408
  const matched = new Set();
332
409
  const mark = node => {
333
- if (node.name.toLowerCase().includes(q)) matched.add(node.id);
334
- for (const c of node.children || []) mark(c);
410
+ if (node.name.toLowerCase().includes(q)) {
411
+ matched.add(node.id);
412
+ }
413
+ for (const child of node.children || []) mark(child);
335
414
  };
336
- for (const r of this.tree) mark(r);
415
+ for (const root of this.tree) mark(root);
337
416
 
338
417
  const roots = [];
339
418
  const show = new Set();
340
419
  const collect = node => {
341
420
  show.add(node.id);
342
- for (const c of node.children || []) collect(c);
421
+ for (const child of node.children || []) collect(child);
343
422
  };
344
423
  const walk = (node, hasMatchedAncestor) => {
345
424
  const isMatch = matched.has(node.id);
@@ -347,9 +426,9 @@ export class VueDevToolsPanel extends LitElement {
347
426
  roots.push(node);
348
427
  collect(node);
349
428
  }
350
- for (const c of node.children || []) walk(c, hasMatchedAncestor || isMatch);
429
+ for (const child of node.children || []) walk(child, hasMatchedAncestor || isMatch);
351
430
  };
352
- for (const r of this.tree) walk(r, false);
431
+ for (const root of this.tree) walk(root, false);
353
432
  return { roots, show, q };
354
433
  }
355
434
 
@@ -360,84 +439,139 @@ export class VueDevToolsPanel extends LitElement {
360
439
  const parent = new Map();
361
440
  const node = new Map();
362
441
  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);
442
+ const walk = (treeNode, parentId) => {
443
+ node.set(treeNode.id, treeNode);
444
+ parent.set(treeNode.id, parentId);
445
+ order.push(treeNode.id);
446
+ if (this._searchCollapsed.has(treeNode.id)) {
447
+ return; // collapsed in search
448
+ }
449
+ for (const child of treeNode.children || []) {
450
+ if (filter.show.has(child.id)) {
451
+ walk(child, treeNode.id);
452
+ }
369
453
  }
370
454
  };
371
- for (const r of filter.roots) walk(r, null);
455
+ for (const root of filter.roots) walk(root, null);
372
456
  return { order, parent, node };
373
457
  }
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);
458
+ const walk = (treeNode, parentId) => {
459
+ node.set(treeNode.id, treeNode);
460
+ parent.set(treeNode.id, parentId);
461
+ order.push(treeNode.id);
462
+ if (treeNode.children && treeNode.children.length && this.expanded.has(treeNode.id)) {
463
+ for (const child of treeNode.children) walk(child, treeNode.id);
380
464
  }
381
465
  };
382
- for (const r of this.tree) walk(r, null);
466
+ for (const root of this.tree) walk(root, null);
383
467
  return { order, parent, node };
384
468
  }
385
469
 
386
470
  // Arrow-key navigation once a component is selected (VS Code / devtools style):
387
471
  // ↑/↓ move through visible rows, → step into / expand, ← step out / collapse.
388
472
  // 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;
473
+ _handleKeydown(event) {
474
+ if (this.collapsed) {
475
+ return;
476
+ }
477
+ if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
478
+ return;
479
+ }
392
480
  // Don't hijack arrow keys while typing in a field. composedPath sees into
393
481
  // our shadow root (the search box) as well as the app's own inputs.
394
- const path = e.composedPath ? e.composedPath() : [];
482
+ const path = event.composedPath ? event.composedPath() : [];
395
483
  const inEditable = path.some(el => el && el.tagName && (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable));
396
- if (inEditable) return;
484
+ if (inEditable) {
485
+ return;
486
+ }
397
487
 
488
+ // Timeline tab: ↑/↓ move between entries.
489
+ if (this.tab === 'timeline') {
490
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
491
+ return;
492
+ }
493
+ const entries = this._timelineEntries();
494
+ if (!entries.length) {
495
+ return;
496
+ }
497
+ const curIdx = entries.findIndex(item => item.id === this.timelineSelected);
498
+ const base = curIdx < 0 ? entries.length - 1 : curIdx;
499
+ const nextIdx = event.key === 'ArrowDown' ? Math.min(entries.length - 1, base + 1) : Math.max(0, base - 1);
500
+ this.timelineSelected = entries[nextIdx].id;
501
+ this._scrollToSelected = true;
502
+ event.preventDefault();
503
+ return;
504
+ }
505
+
506
+ // Components tab: tree navigation (needs a selected node).
507
+ if (this.selectedId == null) {
508
+ return;
509
+ }
398
510
  const filter = this._computeFilter();
399
511
  const searching = !!filter;
400
512
  const { order, parent, node } = this._index(filter);
401
513
  const id = this.selectedId;
402
514
  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));
515
+ if (!cur) {
516
+ return;
517
+ }
518
+ const index = order.indexOf(id);
519
+ if (index < 0) {
520
+ return;
521
+ }
522
+ const shownChildren = (cur.children || []).filter(item => !filter || filter.show.has(item.id));
407
523
  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);
524
+ // "open" respects the active mode: normal tree uses `expanded`, search
525
+ // uses `_searchCollapsed` (default open).
526
+ const isOpen = searching ? !this._searchCollapsed.has(id) : this.expanded.has(id);
411
527
  let next = null;
412
528
 
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') {
529
+ if (event.key === 'ArrowDown') {
530
+ next = order[Math.min(order.length - 1, index + 1)];
531
+ } else if (event.key === 'ArrowUp') {
532
+ next = order[Math.max(0, index - 1)];
533
+ } else if (event.key === 'ArrowRight') {
418
534
  // 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') {
535
+ if (hasChildren && !isOpen) {
536
+ searching ? this._searchCollapsed.delete(id) : this.expanded.add(id);
537
+ } else if (hasChildren && isOpen) {
538
+ next = shownChildren[0].id;
539
+ }
540
+ } else if (event.key === 'ArrowLeft') {
422
541
  // 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);
542
+ if (isOpen && hasChildren) {
543
+ searching ? this._searchCollapsed.add(id) : this.expanded.delete(id);
544
+ } else {
545
+ next = parent.get(id);
546
+ }
425
547
  }
426
548
 
427
- e.preventDefault();
428
- if (next != null) this._select(next);
549
+ event.preventDefault();
550
+ if (next != null) {
551
+ this._select(next);
552
+ }
429
553
  this._scrollToSelected = true;
430
554
  this.requestUpdate();
431
555
  }
432
556
 
433
557
  updated(changed) {
434
558
  // Persist remembered UI bits across reloads.
435
- if (changed && (changed.has('collapsed') || changed.has('tab'))) {
559
+ if (changed && (changed.has('collapsed') || changed.has('tab') || changed.has('theme'))) {
436
560
  this._persistUiState();
437
561
  }
562
+ // Reflect the theme onto the host so the CSS variable overrides apply.
563
+ if (changed && changed.has('theme')) {
564
+ this.setAttribute('theme', this.theme);
565
+ }
438
566
  // Re-clamp the docked position when switching fab <-> panel (sizes differ).
439
567
  if (changed && changed.has('collapsed')) {
440
568
  this._applyPos();
569
+ // Opening wakes the entry; collapsing restarts the idle-tuck timer.
570
+ if (this.collapsed) {
571
+ this._scheduleIdleTuck();
572
+ } else {
573
+ this._wakeEntry();
574
+ }
441
575
  }
442
576
  // Focus a freshly opened inline editor.
443
577
  if (this._focusEdit) {
@@ -450,16 +584,20 @@ export class VueDevToolsPanel extends LitElement {
450
584
  }
451
585
  // When the query changes, auto-select the first match (like vue-devtools).
452
586
  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);
587
+ const filter = this._computeFilter();
588
+ if (filter && filter.roots.length && !filter.show.has(this.selectedId)) {
589
+ this._select(filter.roots[0].id);
456
590
  this._scrollToSelected = true;
457
591
  }
458
592
  }
459
- if (!this._scrollToSelected) return;
593
+ if (!this._scrollToSelected) {
594
+ return;
595
+ }
460
596
  this._scrollToSelected = false;
461
597
  const el = this.renderRoot.querySelector('.node.selected');
462
- if (el) el.scrollIntoView({ block: 'nearest' });
598
+ if (el) {
599
+ el.scrollIntoView({ block: 'nearest' });
600
+ }
463
601
  }
464
602
 
465
603
  _renderNode(node, depth) {
@@ -477,8 +615,8 @@ export class VueDevToolsPanel extends LitElement {
477
615
  >
478
616
  <span
479
617
  class="caret-btn"
480
- @click=${e => {
481
- e.stopPropagation();
618
+ @click=${event => {
619
+ event.stopPropagation();
482
620
  this._toggle(node.id);
483
621
  }}
484
622
  >
@@ -486,7 +624,7 @@ export class VueDevToolsPanel extends LitElement {
486
624
  </span>
487
625
  <span class="tag">&lt;${node.name}&gt;</span>
488
626
  </div>
489
- ${hasChildren && isOpen ? node.children.map(c => this._renderNode(c, depth + 1)) : null}
627
+ ${hasChildren && isOpen ? node.children.map(item => this._renderNode(item, depth + 1)) : null}
490
628
  </div>
491
629
  `;
492
630
  }
@@ -494,8 +632,10 @@ export class VueDevToolsPanel extends LitElement {
494
632
  // Search result row: matched node as a subtree root, with all descendants
495
633
  // shown (always open). Ancestors are omitted. Arrow is non-interactive here.
496
634
  _renderSearchNode(node, depth, show) {
497
- const kids = (node.children || []).filter(c => show.has(c.id));
635
+ const kids = (node.children || []).filter(item => show.has(item.id));
636
+ const hasKids = kids.length > 0;
498
637
  const isSelected = node.id === this.selectedId;
638
+ const open = !this._searchCollapsed.has(node.id); // default open in search
499
639
  return html`
500
640
  <div>
501
641
  <div
@@ -505,24 +645,45 @@ export class VueDevToolsPanel extends LitElement {
505
645
  @mouseenter=${() => this._hoverEnter(node.id)}
506
646
  @mouseleave=${() => this._hoverLeave()}
507
647
  >
508
- <span class="caret-btn static">${this._caret(kids.length > 0, kids.length === 0)}</span>
648
+ <span
649
+ class="caret-btn"
650
+ @click=${event => {
651
+ event.stopPropagation();
652
+ if (hasKids) {
653
+ this._toggleSearchNode(node.id);
654
+ }
655
+ }}
656
+ >
657
+ ${this._caret(open && hasKids, !hasKids)}
658
+ </span>
509
659
  <span class="tag">&lt;${node.name}&gt;</span>
510
660
  </div>
511
- ${kids.map(c => this._renderSearchNode(c, depth + 1, show))}
661
+ ${hasKids && open ? kids.map(item => this._renderSearchNode(item, depth + 1, show)) : null}
512
662
  </div>
513
663
  `;
514
664
  }
515
665
 
666
+ _toggleSearchNode(id) {
667
+ if (this._searchCollapsed.has(id)) {
668
+ this._searchCollapsed.delete(id);
669
+ } else {
670
+ this._searchCollapsed.add(id);
671
+ }
672
+ this.requestUpdate();
673
+ }
674
+
516
675
  // Render an object as a titled, collapsible section of expandable value rows.
517
676
  // `editable` enables inline editing of primitive leaves (writes back into obj).
518
677
  // Sections with many keys get a live filter input in the header.
519
678
  _renderKvSection(title, obj, editable) {
520
679
  const allKeys = obj ? Object.keys(obj) : [];
521
- if (!allKeys.length) return null;
680
+ if (!allKeys.length) {
681
+ return null;
682
+ }
522
683
  const collapsed = this.sectionCollapsed.has(title);
523
- const showFilter = allKeys.length > SECTION_FILTER_THRESHOLD;
684
+ const showFilter = !collapsed && allKeys.length > SectionFilterThreshold;
524
685
  const q = (this._sectionFilter[title] || '').trim().toLowerCase();
525
- const keys = q ? allKeys.filter(k => k.toLowerCase().includes(q)) : allKeys;
686
+ const keys = q ? allKeys.filter(item => item.toLowerCase().includes(q)) : allKeys;
526
687
  return html`
527
688
  <div class="section-title" @click=${() => this._toggleSection(title)}>
528
689
  ${this._caret(!collapsed, false)}
@@ -534,16 +695,16 @@ export class VueDevToolsPanel extends LitElement {
534
695
  type="search"
535
696
  placeholder="filter…"
536
697
  .value=${this._sectionFilter[title] || ''}
537
- @click=${e => e.stopPropagation()}
538
- @keydown=${e => {
539
- if (e.key === 'Escape') {
698
+ @click=${event => event.stopPropagation()}
699
+ @keydown=${event => {
700
+ if (event.key === 'Escape') {
540
701
  this._sectionFilter[title] = '';
541
702
  this.requestUpdate();
542
703
  }
543
- e.stopPropagation();
704
+ event.stopPropagation();
544
705
  }}
545
- @input=${e => {
546
- this._sectionFilter[title] = e.target.value;
706
+ @input=${event => {
707
+ this._sectionFilter[title] = event.target.value;
547
708
  this.requestUpdate();
548
709
  }}
549
710
  />
@@ -553,7 +714,7 @@ export class VueDevToolsPanel extends LitElement {
553
714
  ${collapsed
554
715
  ? null
555
716
  : keys.length
556
- ? keys.map(k => this._renderValueRow(k, obj[k], `${title}.${k}`, 0, obj, editable))
717
+ ? keys.map(item => this._renderValueRow(item, obj[item], `${title}.${item}`, 0, obj, editable))
557
718
  : html`
558
719
  <div class="empty">No match</div>
559
720
  `}
@@ -561,14 +722,20 @@ export class VueDevToolsPanel extends LitElement {
561
722
  }
562
723
 
563
724
  _toggleSection(title) {
564
- if (this.sectionCollapsed.has(title)) this.sectionCollapsed.delete(title);
565
- else this.sectionCollapsed.add(title);
725
+ if (this.sectionCollapsed.has(title)) {
726
+ this.sectionCollapsed.delete(title);
727
+ } else {
728
+ this.sectionCollapsed.add(title);
729
+ }
566
730
  this.requestUpdate();
567
731
  }
568
732
 
569
733
  _toggleValue(path) {
570
- if (this.valueExpanded.has(path)) this.valueExpanded.delete(path);
571
- else this.valueExpanded.add(path);
734
+ if (this.valueExpanded.has(path)) {
735
+ this.valueExpanded.delete(path);
736
+ } else {
737
+ this.valueExpanded.add(path);
738
+ }
572
739
  this.requestUpdate();
573
740
  }
574
741
 
@@ -576,7 +743,7 @@ export class VueDevToolsPanel extends LitElement {
576
743
  // `editable` is set, primitive leaves can be clicked to edit in place.
577
744
  _renderValueRow(keyLabel, value, path, depth, parent, editable) {
578
745
  const isObj = value !== null && typeof value === 'object';
579
- const keys = isObj ? (Array.isArray(value) ? value.map((_, i) => i) : Object.keys(value)) : [];
746
+ const keys = isObj ? (Array.isArray(value) ? value.map((_, index) => index) : Object.keys(value)) : [];
580
747
  const expandable = isObj && keys.length > 0;
581
748
  const open = this.valueExpanded.has(path);
582
749
  const canEdit = editable && !isObj && typeof value !== 'function';
@@ -594,18 +761,20 @@ export class VueDevToolsPanel extends LitElement {
594
761
  <input
595
762
  class="edit-input"
596
763
  .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)}
764
+ @click=${event => event.stopPropagation()}
765
+ @keydown=${event => this._onEditKeydown(event, parent, keyLabel, value)}
766
+ @blur=${event => this._commitEdit(parent, keyLabel, event.target.value, value)}
600
767
  />
601
768
  `
602
769
  : html`
603
770
  <span
604
771
  class="val ${this._valClass(value)} ${canEdit ? 'editable' : ''}"
605
772
  title=${preview}
606
- @click=${e => {
607
- if (!canEdit) return;
608
- e.stopPropagation();
773
+ @click=${event => {
774
+ if (!canEdit) {
775
+ return;
776
+ }
777
+ event.stopPropagation();
609
778
  this._editingPath = path;
610
779
  this._focusEdit = true;
611
780
  this.requestUpdate();
@@ -619,8 +788,8 @@ export class VueDevToolsPanel extends LitElement {
619
788
  <button
620
789
  class="copy-btn"
621
790
  title="Copy value"
622
- @click=${e => {
623
- e.stopPropagation();
791
+ @click=${event => {
792
+ event.stopPropagation();
624
793
  this._copyValue(value);
625
794
  }}
626
795
  >
@@ -629,7 +798,7 @@ export class VueDevToolsPanel extends LitElement {
629
798
  `
630
799
  : null}
631
800
  </div>
632
- ${expandable && open ? keys.map(k => this._renderValueRow(k, value[k], `${path}.${k}`, depth + 1, value, editable)) : null}
801
+ ${expandable && open ? keys.map(item => this._renderValueRow(item, value[item], `${path}.${item}`, depth + 1, value, editable)) : null}
633
802
  `;
634
803
  }
635
804
 
@@ -638,7 +807,7 @@ export class VueDevToolsPanel extends LitElement {
638
807
  if (value !== null && typeof value === 'object') {
639
808
  try {
640
809
  text = JSON.stringify(value, null, 2);
641
- } catch (e) {
810
+ } catch (err) {
642
811
  text = String(value);
643
812
  }
644
813
  } else {
@@ -666,17 +835,17 @@ export class VueDevToolsPanel extends LitElement {
666
835
  ta.select();
667
836
  try {
668
837
  document.execCommand('copy');
669
- } catch (e) {
838
+ } catch (err) {
670
839
  /* ignore */
671
840
  }
672
841
  document.body.removeChild(ta);
673
842
  }
674
843
 
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') {
844
+ _onEditKeydown(event, parent, key, oldValue) {
845
+ event.stopPropagation();
846
+ if (event.key === 'Enter') {
847
+ this._commitEdit(parent, key, event.target.value, oldValue);
848
+ } else if (event.key === 'Escape') {
680
849
  this._editingPath = null;
681
850
  this.requestUpdate();
682
851
  }
@@ -687,34 +856,47 @@ export class VueDevToolsPanel extends LitElement {
687
856
  _commitEdit(parent, key, rawStr, oldValue) {
688
857
  this._editingPath = null;
689
858
  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') {
859
+ const type = typeof oldValue;
860
+ if (type === 'number') {
861
+ const num = Number(rawStr);
862
+ parsed = Number.isNaN(num) ? oldValue : num;
863
+ } else if (type === 'boolean') {
695
864
  parsed = rawStr === 'true' || rawStr === '1';
696
865
  } else if (oldValue === null || oldValue === undefined) {
697
866
  try {
698
867
  parsed = JSON.parse(rawStr);
699
- } catch (e) {
868
+ } catch (err) {
700
869
  parsed = rawStr;
701
870
  }
702
871
  }
703
872
  const Vue = hook.Vue;
704
873
  if (parent) {
705
- if (Vue && Vue.set) Vue.set(parent, key, parsed);
706
- else parent[key] = parsed;
874
+ if (Vue && Vue.set) {
875
+ Vue.set(parent, key, parsed);
876
+ } else {
877
+ parent[key] = parsed;
878
+ }
707
879
  }
708
880
  this.requestUpdate();
709
881
  }
710
882
 
711
883
  _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';
884
+ if (value === null || value === undefined) {
885
+ return 'v-null';
886
+ }
887
+ const type = typeof value;
888
+ if (type === 'number') {
889
+ return 'v-num';
890
+ }
891
+ if (type === 'boolean') {
892
+ return 'v-bool';
893
+ }
894
+ if (type === 'string') {
895
+ return 'v-str';
896
+ }
897
+ if (type === 'function') {
898
+ return 'v-fn';
899
+ }
718
900
  return 'v-obj';
719
901
  }
720
902
 
@@ -734,20 +916,21 @@ export class VueDevToolsPanel extends LitElement {
734
916
  `;
735
917
  }
736
918
  const vm = getInstance(this.selectedId);
737
- if (!vm)
919
+ if (!vm) {
738
920
  return html`
739
921
  <div class="empty">Component unmounted</div>
740
922
  `;
923
+ }
741
924
 
742
925
  const propsObj = vm._props || {};
743
926
  const dataObj = vm._data || vm.$data || {};
744
927
  const compDefs = (vm.$options && vm.$options.computed) || {};
745
928
  const compObj = {};
746
- for (const k of Object.keys(compDefs)) {
929
+ for (const key of Object.keys(compDefs)) {
747
930
  try {
748
- compObj[k] = vm[k];
931
+ compObj[key] = vm[key];
749
932
  } catch (err) {
750
- compObj[k] = `⚠ ${err && err.message}`;
933
+ compObj[key] = `⚠ ${err && err.message}`;
751
934
  }
752
935
  }
753
936
  const attrsObj = vm.$attrs || {};
@@ -790,27 +973,34 @@ export class VueDevToolsPanel extends LitElement {
790
973
  }
791
974
 
792
975
  _vmName(vm) {
793
- const o = vm.$options || {};
794
- let n = o.name || o._componentTag;
795
- if (!n && o.__file)
796
- n = String(o.__file)
976
+ const options = vm.$options || {};
977
+ let name = options.name || options._componentTag;
978
+ if (!name && options.__file) {
979
+ name = String(options.__file)
797
980
  .split(/[\\/]/)
798
981
  .pop()
799
982
  .replace(/\.vue$/, '');
800
- if (!n && vm.$root === vm) n = 'Root';
801
- return n || 'Anonymous';
983
+ }
984
+ if (!name && vm.$root === vm) {
985
+ name = 'Root';
986
+ }
987
+ return name || 'Anonymous';
802
988
  }
803
989
 
804
990
  // Ask the Vite dev server to open the component's source file in the editor.
805
991
  _openInEditor(file) {
806
- if (!file) return;
992
+ if (!file) {
993
+ return;
994
+ }
807
995
  fetch('/__open-in-editor?file=' + encodeURIComponent(file)).catch(() => {});
808
996
  }
809
997
 
810
998
  // Scroll the component's root DOM element into view and flash the highlight.
811
999
  _scrollToComponent(vm) {
812
1000
  const el = vm && vm.$el;
813
- if (!el || !el.scrollIntoView) return;
1001
+ if (!el || !el.scrollIntoView) {
1002
+ return;
1003
+ }
814
1004
  el.scrollIntoView({
815
1005
  behavior: 'smooth',
816
1006
  block: 'center',
@@ -833,17 +1023,28 @@ export class VueDevToolsPanel extends LitElement {
833
1023
  const lines = code.split('\n');
834
1024
  let min = Infinity;
835
1025
  for (let i = 1; i < lines.length; i++) {
836
- if (!lines[i].trim()) continue;
1026
+ if (!lines[i].trim()) {
1027
+ continue;
1028
+ }
837
1029
  const indent = lines[i].match(/^[ \t]*/)[0].length;
838
- if (indent < min) min = indent;
1030
+ if (indent < min) {
1031
+ min = indent;
1032
+ }
1033
+ }
1034
+ if (!isFinite(min) || min === 0) {
1035
+ return code;
839
1036
  }
840
- if (!isFinite(min) || min === 0) return code;
841
- return lines.map((l, i) => (i === 0 ? l : l.slice(min))).join('\n');
1037
+ return lines.map((item, index) => (index === 0 ? item : item.slice(min))).join('\n');
842
1038
  }
843
1039
 
844
1040
  render() {
845
1041
  return html`
846
- <div class="entry" @pointerdown=${e => this._onFabPointerDown(e)}>
1042
+ <div
1043
+ class="entry"
1044
+ @pointerdown=${event => this._onFabPointerDown(event)}
1045
+ @pointerenter=${() => this._wakeEntry()}
1046
+ @pointerleave=${() => this._scheduleIdleTuck()}
1047
+ >
847
1048
  <span class="fab-icon">${this._vueLogo()}</span>
848
1049
  </div>
849
1050
  ${this.collapsed ? null : this._renderPanel()}
@@ -854,7 +1055,6 @@ export class VueDevToolsPanel extends LitElement {
854
1055
  return html`
855
1056
  <div class="panel">
856
1057
  <nav class="sidebar">
857
- <div class="logo">${this._vueLogo()}</div>
858
1058
  <button class="side-tab ${this.tab === 'components' ? 'active' : ''}" @click=${() => (this.tab = 'components')}>
859
1059
  ${this._icon('components')}
860
1060
  <span class="tip">Components</span>
@@ -876,6 +1076,10 @@ export class VueDevToolsPanel extends LitElement {
876
1076
  </button>
877
1077
  `
878
1078
  : null}
1079
+ <button class="side-tab" @click=${() => this._toggleTheme()}>
1080
+ ${this._icon(this.theme === 'dark' ? 'sun' : 'moon')}
1081
+ <span class="tip">${this.theme === 'dark' ? 'Light theme' : 'Dark theme'}</span>
1082
+ </button>
879
1083
  <button class="side-tab side-tab--min" @click=${() => (this.collapsed = true)}>
880
1084
  ${this._icon('min')}
881
1085
  <span class="tip">Minimize</span>
@@ -909,11 +1113,11 @@ export class VueDevToolsPanel extends LitElement {
909
1113
  type="search"
910
1114
  placeholder="Search components…"
911
1115
  .value=${this.query}
912
- @input=${e => (this.query = e.target.value)}
913
- @keydown=${e => {
914
- if (e.key === 'Escape') {
1116
+ @input=${event => (this.query = event.target.value)}
1117
+ @keydown=${event => {
1118
+ if (event.key === 'Escape') {
915
1119
  this.query = '';
916
- e.stopPropagation();
1120
+ event.stopPropagation();
917
1121
  }
918
1122
  }}
919
1123
  />
@@ -929,8 +1133,8 @@ export class VueDevToolsPanel extends LitElement {
929
1133
  <div class="empty">No component matches</div>
930
1134
  `
931
1135
  : filter
932
- ? filter.roots.map(n => this._renderSearchNode(n, 0, filter.show))
933
- : this.tree.map(n => this._renderNode(n, 0))}
1136
+ ? filter.roots.map(item => this._renderSearchNode(item, 0, filter.show))
1137
+ : this.tree.map(item => this._renderNode(item, 0))}
934
1138
  </div>
935
1139
  <div class="detail">${this._renderDetail()}</div>
936
1140
  </div>
@@ -964,10 +1168,10 @@ export class VueDevToolsPanel extends LitElement {
964
1168
  </button>
965
1169
  </div>
966
1170
  ${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>
1171
+ (item, index) => html`
1172
+ <div class="node ${index === sel ? 'selected' : ''}" @click=${() => (this.vuexSelected = index)}>
1173
+ <span class="mut-index">${item.base ? '' : index}</span>
1174
+ <span class="tag">${item.base ? 'Base State' : item.type}</span>
971
1175
  </div>
972
1176
  `
973
1177
  )}
@@ -995,14 +1199,16 @@ export class VueDevToolsPanel extends LitElement {
995
1199
  // (like the vue-devtools v7 Timeline).
996
1200
  _timelineEntries() {
997
1201
  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 });
1202
+ for (const event of getEvents()) {
1203
+ entries.push({ id: `e${event.id}`, kind: 'event', time: event.time, title: event.name, sub: `<${event.component}>`, event: event });
1000
1204
  }
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 });
1205
+ getSnapshots().forEach((item, index) => {
1206
+ if (item.base) {
1207
+ return;
1208
+ }
1209
+ entries.push({ id: `m${index}`, kind: 'mutation', time: item.time || 0, title: item.type, sub: 'vuex', snap: item, index });
1004
1210
  });
1005
- entries.sort((a, b) => a.time - b.time);
1211
+ entries.sort((first, second) => first.time - second.time);
1006
1212
  return entries;
1007
1213
  }
1008
1214
 
@@ -1013,7 +1219,7 @@ export class VueDevToolsPanel extends LitElement {
1013
1219
  <div class="body"><div class="empty">No timeline activity yet</div></div>
1014
1220
  `;
1015
1221
  }
1016
- const sel = entries.find(e => e.id === this.timelineSelected) || entries[entries.length - 1];
1222
+ const sel = entries.find(item => item.id === this.timelineSelected) || entries[entries.length - 1];
1017
1223
  return html`
1018
1224
  <div class="body">
1019
1225
  <div class="tree">
@@ -1030,12 +1236,11 @@ export class VueDevToolsPanel extends LitElement {
1030
1236
  </button>
1031
1237
  </div>
1032
1238
  ${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>
1239
+ item => html`
1240
+ <div class="node ${item.id === sel.id ? 'selected' : ''}" @click=${() => (this.timelineSelected = item.id)}>
1241
+ <span class="ev-time">${this._formatTime(item.time)}</span>
1242
+ <span class="tag">${item.title}</span>
1243
+ <span class="ev-comp">${item.sub}</span>
1039
1244
  </div>
1040
1245
  `
1041
1246
  )}
@@ -1046,46 +1251,48 @@ export class VueDevToolsPanel extends LitElement {
1046
1251
  }
1047
1252
 
1048
1253
  _renderTimelineDetail(entry) {
1049
- if (!entry) return null;
1254
+ if (!entry) {
1255
+ return null;
1256
+ }
1050
1257
  if (entry.kind === 'event') {
1051
- const e = entry.event;
1258
+ const event = entry.event;
1052
1259
  const argsObj = {};
1053
- (e.args || []).forEach((a, i) => {
1054
- argsObj[i] = a;
1260
+ (event.args || []).forEach((item, index) => {
1261
+ argsObj[index] = item;
1055
1262
  });
1056
1263
  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
1264
+ ${this._renderKvSection('event', { name: event.name, from: event.component, time: new Date(event.time).toLocaleTimeString() }, false)}
1265
+ ${event.args && event.args.length
1059
1266
  ? this._renderKvSection('payload', argsObj, false)
1060
1267
  : html`
1061
1268
  <div class="empty">No payload</div>
1062
1269
  `}
1063
1270
  `;
1064
1271
  }
1065
- const s = entry.snap;
1272
+ const snap = entry.snap;
1066
1273
  const store = getStore();
1067
- const payloadObj = s.payload === undefined ? null : { payload: s.payload };
1274
+ const payloadObj = snap.payload === undefined ? null : { payload: snap.payload };
1068
1275
  return html`
1069
1276
  <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)}
1071
- ${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null}
1072
- ${this._renderKvSection('state', s.state || {}, false)}
1277
+ ${this._renderKvSection('mutation', { type: snap.type, time: new Date(snap.time).toLocaleTimeString() }, false)}
1278
+ ${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null} ${this._renderKvSection('state', snap.state || {}, false)}
1073
1279
  ${store ? this._renderKvSection('getters (live)', store.getters || {}, false) : null}
1074
1280
  `;
1075
1281
  }
1076
1282
 
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)}`;
1283
+ _formatTime(time) {
1284
+ const date = new Date(time);
1285
+ const pad = (num, length = 2) => String(num).padStart(length, '0');
1286
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
1081
1287
  }
1082
1288
 
1083
- // Plugin logo
1289
+ // Plugin logo (Vite mark).
1084
1290
  _vueLogo() {
1085
1291
  return html`
1086
- <svg fill-rule="evenodd" viewBox="64 64 896 896" fill="#2932E1" aria-hidden="true">
1292
+ <svg class="vlogo" viewBox="0 0 48 46" fill="none" aria-hidden="true">
1087
1293
  <path
1088
- 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"
1294
+ d="M25.9456 44.9383C25.2821 45.7827 23.925 45.3131 23.925 44.2403V33.9369C23.925 32.6875 22.9126 31.6751 21.6631 31.6751H10.287C9.36714 31.6751 8.83075 30.6346 9.36713 29.8871L16.8464 19.4157C17.917 17.9185 16.8464 15.8376 15.0046 15.8376H1.23731C0.317479 15.8376 -0.218913 14.7972 0.317475 14.0497L10.0134 0.4741C10.2266 0.176825 10.5692 0.000183105 10.9332 0.000183105H39.8271C40.7469 0.000183105 41.2833 1.04065 40.7469 1.78814L33.2676 12.2595C32.197 13.7567 33.2676 15.8376 35.1094 15.8376H46.4856C47.4291 15.8376 47.959 16.9255 47.3753 17.6687L25.9478 44.9404L25.9456 44.9383Z"
1295
+ fill="#863BFF"
1089
1296
  />
1090
1297
  </svg>
1091
1298
  `;
@@ -1094,6 +1301,19 @@ export class VueDevToolsPanel extends LitElement {
1094
1301
  // Inline stroked icons (currentColor) for the sidebar / toolbar.
1095
1302
  _icon(name) {
1096
1303
  switch (name) {
1304
+ case 'moon':
1305
+ return html`
1306
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1307
+ <path d="M21 12.8A9 9 0 1111.2 3 7 7 0 0021 12.8z" />
1308
+ </svg>
1309
+ `;
1310
+ case 'sun':
1311
+ return html`
1312
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1313
+ <circle cx="12" cy="12" r="4" />
1314
+ <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" />
1315
+ </svg>
1316
+ `;
1097
1317
  case 'components':
1098
1318
  return html`
1099
1319
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
@@ -1199,6 +1419,7 @@ export class VueDevToolsPanel extends LitElement {
1199
1419
  --c-null: #b45309;
1200
1420
  --c-fn: #2563eb;
1201
1421
  --c-obj: #475467;
1422
+ --logo-fill: #2932e1;
1202
1423
 
1203
1424
  position: fixed;
1204
1425
  inset: 0;
@@ -1208,6 +1429,25 @@ export class VueDevToolsPanel extends LitElement {
1208
1429
  font-size: 12px;
1209
1430
  color: var(--text);
1210
1431
  }
1432
+ :host([theme='dark']) {
1433
+ --bg: #1e1e20;
1434
+ --surface: #26262a;
1435
+ --border: #34343a;
1436
+ --border-strong: #3a3a42;
1437
+ --field-border: #3a3a42;
1438
+ --text: #e4e4e7;
1439
+ --text-strong: #f4f4f5;
1440
+ --muted: #8b8b93;
1441
+ --muted-2: #a1a1aa;
1442
+ --c-key: #80cbc4;
1443
+ --c-num: #ffcb6b;
1444
+ --c-bool: #c792ea;
1445
+ --c-str: #c3e88d;
1446
+ --c-null: #f78c6c;
1447
+ --c-fn: #82aaff;
1448
+ --c-obj: #b0bec5;
1449
+ --logo-fill: #ffffff;
1450
+ }
1211
1451
  .entry {
1212
1452
  position: fixed;
1213
1453
  pointer-events: auto;
@@ -1223,6 +1463,11 @@ export class VueDevToolsPanel extends LitElement {
1223
1463
  background: var(--bg);
1224
1464
  border: 1px solid var(--border-strong);
1225
1465
  box-shadow: 0 6px 20px rgb(16 24 40 / 0.18);
1466
+ transition:
1467
+ inset-block-start 0.2s ease,
1468
+ inset-block-end 0.2s ease,
1469
+ inset-inline-start 0.2s ease,
1470
+ inset-inline-end 0.2s ease;
1226
1471
  }
1227
1472
  .entry:active {
1228
1473
  cursor: grabbing;
@@ -1237,6 +1482,11 @@ export class VueDevToolsPanel extends LitElement {
1237
1482
  inline-size: 16px;
1238
1483
  block-size: 16px;
1239
1484
  }
1485
+ .fab-icon .vlogo {
1486
+ inline-size: 18px;
1487
+ block-size: 18px;
1488
+ display: block;
1489
+ }
1240
1490
  /* On the left/right edges, stack the entry's icons vertically. */
1241
1491
  :host([dock='left']) .entry,
1242
1492
  :host([dock='right']) .entry {
@@ -1571,7 +1821,7 @@ export class VueDevToolsPanel extends LitElement {
1571
1821
  cursor: pointer;
1572
1822
 
1573
1823
  &:hover {
1574
- color: #475467;
1824
+ color: var(--text);
1575
1825
  }
1576
1826
  & .section-filter {
1577
1827
  margin-inline-start: 6px;