vite-plugin-devtools-vue2 0.1.1 → 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,25 +3,14 @@
3
3
  // on every Vue scheduler flush.
4
4
 
5
5
  import { LitElement, css, html } from 'lit';
6
+ import { DragThreshold, EdgeMargin, PanelEdge, PanelH, PanelW, SectionFilterThreshold, StoreKey } from './config.js';
6
7
  import hook from './hook.js';
7
8
  import { hide, highlight } from './inspector.js';
8
9
  import { isPicking, startPicking, stopPicking } from './picker.js';
9
10
  import { commitAll, getSnapshots, getStore, hasStore, travelTo, subscribe as vuexSubscribe } from './vuex.js';
11
+ import { clearEvents, getEvents, subscribe as eventsSubscribe } from './events.js';
10
12
  import { buildTree, formatValue, getInstance } from './walker.js';
11
13
 
12
- // Persisted UI state (survives reloads). Only stable, cheap bits — not the
13
- // component tree / expanded set (ids are regenerated each load).
14
- const STORE_KEY = 'vue-devtools:ui';
15
-
16
- // Panel size (keep in sync with .panel CSS) — used to keep it on-screen.
17
- const PANEL_W = 620;
18
- const PANEL_H = 420;
19
- const EDGE_MARGIN = 12;
20
- // Distance from the viewport edge to the panel when open (leaves room for the
21
- // floating entry to sit in the gutter, matching the official devtools).
22
- const PANEL_EDGE = EDGE_MARGIN + 30 / 2;
23
- const DRAG_THRESHOLD = 4;
24
-
25
14
  export class VueDevToolsPanel extends LitElement {
26
15
  static properties = {
27
16
  tree: { state: true },
@@ -32,6 +21,8 @@ export class VueDevToolsPanel extends LitElement {
32
21
  query: { state: true },
33
22
  tab: { state: true },
34
23
  vuexSelected: { state: true },
24
+ timelineSelected: { state: true },
25
+ theme: { state: true },
35
26
  renderCodeText: { state: true }
36
27
  };
37
28
 
@@ -47,9 +38,15 @@ export class VueDevToolsPanel extends LitElement {
47
38
  this.query = '';
48
39
  this.tab = ui.tab || 'components';
49
40
  this.vuexSelected = 0;
41
+ this.timelineSelected = null;
42
+ this.theme = ui.theme || 'light';
50
43
  this.renderCodeText = null;
51
44
  this.valueExpanded = new Set();
52
45
  this.sectionCollapsed = new Set();
46
+ // Nodes collapsed within search results (default: all shown expanded).
47
+ this._searchCollapsed = new Set();
48
+ // Per-section (props/data/…) key filter text, keyed by section title.
49
+ this._sectionFilter = {};
53
50
  // Docked position of the entry/panel. Defaults to the bottom edge near
54
51
  // the right; { edge: 'left'|'right'|'top'|'bottom', along: number }.
55
52
  this._pos = ui.pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
@@ -66,14 +63,16 @@ export class VueDevToolsPanel extends LitElement {
66
63
  // derived from `el.__vue__` anyway.
67
64
  this._domObserver = null;
68
65
  this._onFlush = () => this._scheduleRefresh();
69
- this._onKeydown = e => this._handleKeydown(e);
66
+ this._onKeydown = event => this._handleKeydown(event);
70
67
  // Keep the entry/panel on-screen when the viewport shrinks. resize can
71
68
  // fire many times per second while dragging the window edge, and
72
69
  // _applyPos reads layout then writes styles — so coalesce to at most one
73
70
  // call per frame via rAF to avoid layout thrashing.
74
71
  this._resizeRaf = 0;
75
72
  this._onResize = () => {
76
- if (this._resizeRaf) return;
73
+ if (this._resizeRaf) {
74
+ return;
75
+ }
77
76
  this._resizeRaf = requestAnimationFrame(() => {
78
77
  this._resizeRaf = 0;
79
78
  this._applyPos();
@@ -83,8 +82,8 @@ export class VueDevToolsPanel extends LitElement {
83
82
 
84
83
  static _loadUiState() {
85
84
  try {
86
- return JSON.parse(localStorage.getItem(STORE_KEY)) || {};
87
- } catch (e) {
85
+ return JSON.parse(localStorage.getItem(StoreKey)) || {};
86
+ } catch (err) {
88
87
  return {};
89
88
  }
90
89
  }
@@ -92,8 +91,8 @@ export class VueDevToolsPanel extends LitElement {
92
91
  _persistUiState() {
93
92
  try {
94
93
  const pos = this._pos && Number.isFinite(this._pos.along) ? this._pos : undefined;
95
- localStorage.setItem(STORE_KEY, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, pos }));
96
- } catch (e) {
94
+ localStorage.setItem(StoreKey, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, theme: this.theme, pos }));
95
+ } catch (err) {
97
96
  /* storage unavailable — ignore */
98
97
  }
99
98
  }
@@ -103,97 +102,107 @@ export class VueDevToolsPanel extends LitElement {
103
102
  // the viewport; clamped to stay fully on-screen.
104
103
  _applyPos() {
105
104
  const entry = this.renderRoot && this.renderRoot.querySelector('.entry');
106
- if (!entry) return;
107
- const p = this._pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
108
- 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;
109
110
  const vw = window.innerWidth;
110
111
  const vh = window.innerHeight;
111
112
  const er = entry.getBoundingClientRect();
112
113
  const ew = er.width || 40;
113
114
  const eh = er.height || 40;
114
- 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));
115
116
 
116
117
  // Clamped offset of the entry along its docked edge. We derive the panel
117
118
  // position from these numbers directly rather than re-reading the entry's
118
119
  // live rect — on a fresh open/refresh that rect can still be stale (reads
119
120
  // ~0), which left the entry bottom-right but the panel bottom-left.
120
- const alongX = clamp(p.along, vw - ew - M);
121
- 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);
122
123
 
123
124
  const es = entry.style;
124
125
  es.insetInlineStart = es.insetBlockStart = es.insetInlineEnd = es.insetBlockEnd = 'auto';
125
- if (p.edge === 'right' || p.edge === 'left') {
126
- 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';
127
128
  es.insetBlockStart = alongY + 'px';
128
129
  } else {
129
- es['inset' + (p.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = M + 'px';
130
+ es['inset' + (pos.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = margin + 'px';
130
131
  es.insetInlineStart = alongX + 'px';
131
132
  }
132
- this.setAttribute('dock', p.edge);
133
+ this.setAttribute('dock', pos.edge);
133
134
 
134
135
  const panel = this.renderRoot.querySelector('.panel');
135
- if (!panel) return;
136
+ if (!panel) {
137
+ return;
138
+ }
136
139
  const ps = panel.style;
137
140
  ps.insetInlineStart = ps.insetBlockStart = ps.insetInlineEnd = ps.insetBlockEnd = 'auto';
138
141
  // Entry center along its edge, computed from the clamped offsets above.
139
142
  const cx = alongX + ew / 2;
140
143
  const cy = alongY + eh / 2;
141
- if (p.edge === 'right') {
142
- ps.insetInlineEnd = PANEL_EDGE + 'px';
143
- ps.insetBlockStart = clamp(cy - PANEL_H / 2, vh - PANEL_H - M) + 'px';
144
- } else if (p.edge === 'left') {
145
- ps.insetInlineStart = PANEL_EDGE + 'px';
146
- ps.insetBlockStart = clamp(cy - PANEL_H / 2, vh - PANEL_H - M) + 'px';
147
- } else if (p.edge === 'top') {
148
- ps.insetBlockStart = PANEL_EDGE + 'px';
149
- 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';
150
153
  } else {
151
- ps.insetBlockEnd = PANEL_EDGE + 'px';
152
- 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';
153
156
  }
154
157
  }
155
158
 
156
159
  // Drag the always-visible entry. Movement over a threshold = drag (live snap
157
160
  // to nearest edge, panel follows); a plain click toggles the panel.
158
- _startDrag(e) {
159
- if (e.button !== 0) return;
160
- e.preventDefault();
161
+ _startDrag(event) {
162
+ if (event.button !== 0) {
163
+ return;
164
+ }
165
+ event.preventDefault();
161
166
  hide(); // clear any hover highlight before dragging
162
167
  const entry = this.renderRoot.querySelector('.entry');
163
168
  const rect = entry.getBoundingClientRect();
164
169
  this._drag = {
165
- startX: e.clientX,
166
- startY: e.clientY,
167
- offX: e.clientX - rect.left,
168
- 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,
169
174
  moved: false
170
175
  };
171
- this._onDragMove = ev => this._dragMove(ev);
172
- this._onDragUp = ev => this._dragUp(ev);
176
+ this._onDragMove = event => this._dragMove(event);
177
+ this._onDragUp = event => this._dragUp(event);
173
178
  window.addEventListener('pointermove', this._onDragMove, true);
174
179
  window.addEventListener('pointerup', this._onDragUp, true);
175
180
  }
176
181
 
177
- _onFabPointerDown(e) {
178
- this._startDrag(e);
182
+ _onFabPointerDown(event) {
183
+ this._startDrag(event);
179
184
  }
180
185
 
181
- _dragMove(e) {
182
- const d = this._drag;
183
- if (!d) return;
184
- if (!d.moved && Math.abs(e.clientX - d.startX) + Math.abs(e.clientY - d.startY) < DRAG_THRESHOLD) return;
185
- 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;
186
195
  // Snap to the nearest edge live during the drag (not on release).
187
196
  const vw = window.innerWidth;
188
197
  const vh = window.innerHeight;
189
198
  const dist = {
190
- left: e.clientX,
191
- right: vw - e.clientX,
192
- top: e.clientY,
193
- bottom: vh - e.clientY
199
+ left: event.clientX,
200
+ right: vw - event.clientX,
201
+ top: event.clientY,
202
+ bottom: vh - event.clientY
194
203
  };
195
- const edge = Object.keys(dist).reduce((a, b) => (dist[b] < dist[a] ? b : a));
196
- 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;
197
206
  this._pos = { edge, along };
198
207
  this._applyPos();
199
208
  }
@@ -201,10 +210,12 @@ export class VueDevToolsPanel extends LitElement {
201
210
  _dragUp() {
202
211
  window.removeEventListener('pointermove', this._onDragMove, true);
203
212
  window.removeEventListener('pointerup', this._onDragUp, true);
204
- const d = this._drag;
213
+ const drag = this._drag;
205
214
  this._drag = null;
206
- if (!d) return;
207
- if (!d.moved) {
215
+ if (!drag) {
216
+ return;
217
+ }
218
+ if (!drag.moved) {
208
219
  // plain click → toggle the panel
209
220
  this.collapsed = !this.collapsed;
210
221
  return;
@@ -219,6 +230,7 @@ export class VueDevToolsPanel extends LitElement {
219
230
  window.addEventListener('keydown', this._onKeydown, true);
220
231
  window.addEventListener('resize', this._onResize);
221
232
  this._vuexUnsub = vuexSubscribe(() => this.requestUpdate());
233
+ this._eventsUnsub = eventsSubscribe(() => this.requestUpdate());
222
234
  // DOM-based fallback refresh (see constructor). Only childList/subtree —
223
235
  // our own panel renders inside a shadow root (not observed), and the
224
236
  // inspector highlight box only mutates via style, so this won't loop.
@@ -242,12 +254,22 @@ export class VueDevToolsPanel extends LitElement {
242
254
  this._domObserver.disconnect();
243
255
  this._domObserver = null;
244
256
  }
245
- if (this._vuexUnsub) this._vuexUnsub();
257
+ if (this._vuexUnsub) {
258
+ this._vuexUnsub();
259
+ }
260
+ if (this._eventsUnsub) {
261
+ this._eventsUnsub();
262
+ }
246
263
  stopPicking();
247
264
  }
248
265
 
249
266
  firstUpdated() {
250
267
  this._applyPos();
268
+ this.setAttribute('theme', this.theme);
269
+ }
270
+
271
+ _toggleTheme() {
272
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
251
273
  }
252
274
 
253
275
  _scheduleRefresh() {
@@ -270,16 +292,23 @@ export class VueDevToolsPanel extends LitElement {
270
292
  }
271
293
 
272
294
  _toggle(id) {
273
- if (this.expanded.has(id)) this.expanded.delete(id);
274
- else this.expanded.add(id);
295
+ if (this.expanded.has(id)) {
296
+ this.expanded.delete(id);
297
+ } else {
298
+ this.expanded.add(id);
299
+ }
275
300
  this.requestUpdate();
276
301
  }
277
302
 
278
303
  // Highlight the page DOM only while hovering a tree row (vue-devtools style).
279
304
  _hoverEnter(id) {
280
- 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
+ }
281
308
  const vm = getInstance(id);
282
- if (vm) highlight(vm);
309
+ if (vm) {
310
+ highlight(vm);
311
+ }
283
312
  }
284
313
 
285
314
  _hoverLeave() {
@@ -304,7 +333,9 @@ export class VueDevToolsPanel extends LitElement {
304
333
  _selectVm(vm) {
305
334
  this.refresh();
306
335
  const path = this._pathToVm(vm);
307
- if (!path.length) return;
336
+ if (!path.length) {
337
+ return;
338
+ }
308
339
  for (let i = 0; i < path.length - 1; i++) this.expanded.add(path[i]);
309
340
  this._select(path[path.length - 1]);
310
341
  this._scrollToSelected = true;
@@ -319,10 +350,18 @@ export class VueDevToolsPanel extends LitElement {
319
350
  found = next;
320
351
  return true;
321
352
  }
322
- 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
+ }
323
358
  return false;
324
359
  };
325
- 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
+ }
326
365
  return found || [];
327
366
  }
328
367
 
@@ -332,19 +371,23 @@ export class VueDevToolsPanel extends LitElement {
332
371
  // match nested inside another match is not promoted (it shows in the subtree).
333
372
  _computeFilter() {
334
373
  const q = this.query.trim().toLowerCase();
335
- if (!q) return null;
374
+ if (!q) {
375
+ return null;
376
+ }
336
377
  const matched = new Set();
337
378
  const mark = node => {
338
- if (node.name.toLowerCase().includes(q)) matched.add(node.id);
339
- 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);
340
383
  };
341
- for (const r of this.tree) mark(r);
384
+ for (const root of this.tree) mark(root);
342
385
 
343
386
  const roots = [];
344
387
  const show = new Set();
345
388
  const collect = node => {
346
389
  show.add(node.id);
347
- for (const c of node.children || []) collect(c);
390
+ for (const child of node.children || []) collect(child);
348
391
  };
349
392
  const walk = (node, hasMatchedAncestor) => {
350
393
  const isMatch = matched.has(node.id);
@@ -352,9 +395,9 @@ export class VueDevToolsPanel extends LitElement {
352
395
  roots.push(node);
353
396
  collect(node);
354
397
  }
355
- for (const c of node.children || []) walk(c, hasMatchedAncestor || isMatch);
398
+ for (const child of node.children || []) walk(child, hasMatchedAncestor || isMatch);
356
399
  };
357
- for (const r of this.tree) walk(r, false);
400
+ for (const root of this.tree) walk(root, false);
358
401
  return { roots, show, q };
359
402
  }
360
403
 
@@ -365,81 +408,130 @@ export class VueDevToolsPanel extends LitElement {
365
408
  const parent = new Map();
366
409
  const node = new Map();
367
410
  if (filter) {
368
- const walk = (n, p) => {
369
- node.set(n.id, n);
370
- parent.set(n.id, p);
371
- order.push(n.id);
372
- for (const c of n.children || []) {
373
- 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
+ }
374
422
  }
375
423
  };
376
- for (const r of filter.roots) walk(r, null);
424
+ for (const root of filter.roots) walk(root, null);
377
425
  return { order, parent, node };
378
426
  }
379
- const walk = (n, p) => {
380
- node.set(n.id, n);
381
- parent.set(n.id, p);
382
- order.push(n.id);
383
- if (n.children && n.children.length && this.expanded.has(n.id)) {
384
- 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);
385
433
  }
386
434
  };
387
- for (const r of this.tree) walk(r, null);
435
+ for (const root of this.tree) walk(root, null);
388
436
  return { order, parent, node };
389
437
  }
390
438
 
391
439
  // Arrow-key navigation once a component is selected (VS Code / devtools style):
392
440
  // ↑/↓ move through visible rows, → step into / expand, ← step out / collapse.
393
441
  // During search the subtree is always shown, so →/← just navigate in/out.
394
- _handleKeydown(e) {
395
- if (this.collapsed || this.selectedId == null) return;
396
- 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
+ }
397
449
  // Don't hijack arrow keys while typing in a field. composedPath sees into
398
450
  // our shadow root (the search box) as well as the app's own inputs.
399
- const path = e.composedPath ? e.composedPath() : [];
451
+ const path = event.composedPath ? event.composedPath() : [];
400
452
  const inEditable = path.some(el => el && el.tagName && (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable));
401
- if (inEditable) return;
453
+ if (inEditable) {
454
+ return;
455
+ }
402
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
+ }
474
+
475
+ // Components tab: tree navigation (needs a selected node).
476
+ if (this.selectedId == null) {
477
+ return;
478
+ }
403
479
  const filter = this._computeFilter();
404
480
  const searching = !!filter;
405
481
  const { order, parent, node } = this._index(filter);
406
482
  const id = this.selectedId;
407
483
  const cur = node.get(id);
408
- if (!cur) return;
409
- const i = order.indexOf(id);
410
- if (i < 0) return;
411
- 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));
412
492
  const hasChildren = shownChildren.length > 0;
413
- // A node is "open" if its children are currently shown. In search mode the
414
- // matched subtree is always shown.
415
- 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);
416
496
  let next = null;
417
497
 
418
- if (e.key === 'ArrowDown') {
419
- next = order[Math.min(order.length - 1, i + 1)];
420
- } else if (e.key === 'ArrowUp') {
421
- next = order[Math.max(0, i - 1)];
422
- } 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') {
423
503
  // Closed with children -> open it; already open -> step into first child.
424
- if (hasChildren && !isOpen) this.expanded.add(id);
425
- else if (hasChildren && isOpen) next = shownChildren[0].id;
426
- } 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') {
427
510
  // Check open/closed FIRST: open -> collapse; closed (or leaf) -> parent.
428
- if (isOpen && hasChildren && !searching) this.expanded.delete(id);
429
- 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
+ }
430
516
  }
431
517
 
432
- e.preventDefault();
433
- if (next != null) this._select(next);
518
+ event.preventDefault();
519
+ if (next != null) {
520
+ this._select(next);
521
+ }
434
522
  this._scrollToSelected = true;
435
523
  this.requestUpdate();
436
524
  }
437
525
 
438
526
  updated(changed) {
439
527
  // Persist remembered UI bits across reloads.
440
- if (changed && (changed.has('collapsed') || changed.has('tab'))) {
528
+ if (changed && (changed.has('collapsed') || changed.has('tab') || changed.has('theme'))) {
441
529
  this._persistUiState();
442
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
+ }
443
535
  // Re-clamp the docked position when switching fab <-> panel (sizes differ).
444
536
  if (changed && changed.has('collapsed')) {
445
537
  this._applyPos();
@@ -455,16 +547,20 @@ export class VueDevToolsPanel extends LitElement {
455
547
  }
456
548
  // When the query changes, auto-select the first match (like vue-devtools).
457
549
  if (changed && changed.has('query') && this.query.trim()) {
458
- const f = this._computeFilter();
459
- if (f && f.roots.length && !f.show.has(this.selectedId)) {
460
- 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);
461
553
  this._scrollToSelected = true;
462
554
  }
463
555
  }
464
- if (!this._scrollToSelected) return;
556
+ if (!this._scrollToSelected) {
557
+ return;
558
+ }
465
559
  this._scrollToSelected = false;
466
560
  const el = this.renderRoot.querySelector('.node.selected');
467
- if (el) el.scrollIntoView({ block: 'nearest' });
561
+ if (el) {
562
+ el.scrollIntoView({ block: 'nearest' });
563
+ }
468
564
  }
469
565
 
470
566
  _renderNode(node, depth) {
@@ -482,8 +578,8 @@ export class VueDevToolsPanel extends LitElement {
482
578
  >
483
579
  <span
484
580
  class="caret-btn"
485
- @click=${e => {
486
- e.stopPropagation();
581
+ @click=${event => {
582
+ event.stopPropagation();
487
583
  this._toggle(node.id);
488
584
  }}
489
585
  >
@@ -491,7 +587,7 @@ export class VueDevToolsPanel extends LitElement {
491
587
  </span>
492
588
  <span class="tag">&lt;${node.name}&gt;</span>
493
589
  </div>
494
- ${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}
495
591
  </div>
496
592
  `;
497
593
  }
@@ -499,8 +595,10 @@ export class VueDevToolsPanel extends LitElement {
499
595
  // Search result row: matched node as a subtree root, with all descendants
500
596
  // shown (always open). Ancestors are omitted. Arrow is non-interactive here.
501
597
  _renderSearchNode(node, depth, show) {
502
- 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;
503
600
  const isSelected = node.id === this.selectedId;
601
+ const open = !this._searchCollapsed.has(node.id); // default open in search
504
602
  return html`
505
603
  <div>
506
604
  <div
@@ -510,35 +608,97 @@ export class VueDevToolsPanel extends LitElement {
510
608
  @mouseenter=${() => this._hoverEnter(node.id)}
511
609
  @mouseleave=${() => this._hoverLeave()}
512
610
  >
513
- <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>
514
622
  <span class="tag">&lt;${node.name}&gt;</span>
515
623
  </div>
516
- ${kids.map(c => this._renderSearchNode(c, depth + 1, show))}
624
+ ${hasKids && open ? kids.map(item => this._renderSearchNode(item, depth + 1, show)) : null}
517
625
  </div>
518
626
  `;
519
627
  }
520
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
+
521
638
  // Render an object as a titled, collapsible section of expandable value rows.
522
639
  // `editable` enables inline editing of primitive leaves (writes back into obj).
640
+ // Sections with many keys get a live filter input in the header.
523
641
  _renderKvSection(title, obj, editable) {
524
- const keys = obj ? Object.keys(obj) : [];
525
- if (!keys.length) return null;
642
+ const allKeys = obj ? Object.keys(obj) : [];
643
+ if (!allKeys.length) {
644
+ return null;
645
+ }
526
646
  const collapsed = this.sectionCollapsed.has(title);
647
+ const showFilter = !collapsed && allKeys.length > SectionFilterThreshold;
648
+ const q = (this._sectionFilter[title] || '').trim().toLowerCase();
649
+ const keys = q ? allKeys.filter(item => item.toLowerCase().includes(q)) : allKeys;
527
650
  return html`
528
- <div class="section-title" @click=${() => this._toggleSection(title)}>${this._caret(!collapsed, false)} ${title}</div>
529
- ${collapsed ? null : keys.map(k => this._renderValueRow(k, obj[k], `${title}.${k}`, 0, obj, editable))}
651
+ <div class="section-title" @click=${() => this._toggleSection(title)}>
652
+ ${this._caret(!collapsed, false)}
653
+ <span class="section-name">${title}</span>
654
+ ${showFilter
655
+ ? html`
656
+ <input
657
+ class="section-filter"
658
+ type="search"
659
+ placeholder="filter…"
660
+ .value=${this._sectionFilter[title] || ''}
661
+ @click=${event => event.stopPropagation()}
662
+ @keydown=${event => {
663
+ if (event.key === 'Escape') {
664
+ this._sectionFilter[title] = '';
665
+ this.requestUpdate();
666
+ }
667
+ event.stopPropagation();
668
+ }}
669
+ @input=${event => {
670
+ this._sectionFilter[title] = event.target.value;
671
+ this.requestUpdate();
672
+ }}
673
+ />
674
+ `
675
+ : null}
676
+ </div>
677
+ ${collapsed
678
+ ? null
679
+ : keys.length
680
+ ? keys.map(item => this._renderValueRow(item, obj[item], `${title}.${item}`, 0, obj, editable))
681
+ : html`
682
+ <div class="empty">No match</div>
683
+ `}
530
684
  `;
531
685
  }
532
686
 
533
687
  _toggleSection(title) {
534
- if (this.sectionCollapsed.has(title)) this.sectionCollapsed.delete(title);
535
- else this.sectionCollapsed.add(title);
688
+ if (this.sectionCollapsed.has(title)) {
689
+ this.sectionCollapsed.delete(title);
690
+ } else {
691
+ this.sectionCollapsed.add(title);
692
+ }
536
693
  this.requestUpdate();
537
694
  }
538
695
 
539
696
  _toggleValue(path) {
540
- if (this.valueExpanded.has(path)) this.valueExpanded.delete(path);
541
- else this.valueExpanded.add(path);
697
+ if (this.valueExpanded.has(path)) {
698
+ this.valueExpanded.delete(path);
699
+ } else {
700
+ this.valueExpanded.add(path);
701
+ }
542
702
  this.requestUpdate();
543
703
  }
544
704
 
@@ -546,7 +706,7 @@ export class VueDevToolsPanel extends LitElement {
546
706
  // `editable` is set, primitive leaves can be clicked to edit in place.
547
707
  _renderValueRow(keyLabel, value, path, depth, parent, editable) {
548
708
  const isObj = value !== null && typeof value === 'object';
549
- 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)) : [];
550
710
  const expandable = isObj && keys.length > 0;
551
711
  const open = this.valueExpanded.has(path);
552
712
  const canEdit = editable && !isObj && typeof value !== 'function';
@@ -564,18 +724,20 @@ export class VueDevToolsPanel extends LitElement {
564
724
  <input
565
725
  class="edit-input"
566
726
  .value=${String(value)}
567
- @click=${e => e.stopPropagation()}
568
- @keydown=${e => this._onEditKeydown(e, parent, keyLabel, value)}
569
- @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)}
570
730
  />
571
731
  `
572
732
  : html`
573
733
  <span
574
734
  class="val ${this._valClass(value)} ${canEdit ? 'editable' : ''}"
575
735
  title=${preview}
576
- @click=${e => {
577
- if (!canEdit) return;
578
- e.stopPropagation();
736
+ @click=${event => {
737
+ if (!canEdit) {
738
+ return;
739
+ }
740
+ event.stopPropagation();
579
741
  this._editingPath = path;
580
742
  this._focusEdit = true;
581
743
  this.requestUpdate();
@@ -589,8 +751,8 @@ export class VueDevToolsPanel extends LitElement {
589
751
  <button
590
752
  class="copy-btn"
591
753
  title="Copy value"
592
- @click=${e => {
593
- e.stopPropagation();
754
+ @click=${event => {
755
+ event.stopPropagation();
594
756
  this._copyValue(value);
595
757
  }}
596
758
  >
@@ -599,7 +761,7 @@ export class VueDevToolsPanel extends LitElement {
599
761
  `
600
762
  : null}
601
763
  </div>
602
- ${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}
603
765
  `;
604
766
  }
605
767
 
@@ -608,7 +770,7 @@ export class VueDevToolsPanel extends LitElement {
608
770
  if (value !== null && typeof value === 'object') {
609
771
  try {
610
772
  text = JSON.stringify(value, null, 2);
611
- } catch (e) {
773
+ } catch (err) {
612
774
  text = String(value);
613
775
  }
614
776
  } else {
@@ -636,17 +798,17 @@ export class VueDevToolsPanel extends LitElement {
636
798
  ta.select();
637
799
  try {
638
800
  document.execCommand('copy');
639
- } catch (e) {
801
+ } catch (err) {
640
802
  /* ignore */
641
803
  }
642
804
  document.body.removeChild(ta);
643
805
  }
644
806
 
645
- _onEditKeydown(e, parent, key, oldValue) {
646
- e.stopPropagation();
647
- if (e.key === 'Enter') {
648
- this._commitEdit(parent, key, e.target.value, oldValue);
649
- } 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') {
650
812
  this._editingPath = null;
651
813
  this.requestUpdate();
652
814
  }
@@ -657,34 +819,47 @@ export class VueDevToolsPanel extends LitElement {
657
819
  _commitEdit(parent, key, rawStr, oldValue) {
658
820
  this._editingPath = null;
659
821
  let parsed = rawStr;
660
- const t = typeof oldValue;
661
- if (t === 'number') {
662
- const n = Number(rawStr);
663
- parsed = Number.isNaN(n) ? oldValue : n;
664
- } 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') {
665
827
  parsed = rawStr === 'true' || rawStr === '1';
666
828
  } else if (oldValue === null || oldValue === undefined) {
667
829
  try {
668
830
  parsed = JSON.parse(rawStr);
669
- } catch (e) {
831
+ } catch (err) {
670
832
  parsed = rawStr;
671
833
  }
672
834
  }
673
835
  const Vue = hook.Vue;
674
836
  if (parent) {
675
- if (Vue && Vue.set) Vue.set(parent, key, parsed);
676
- else parent[key] = parsed;
837
+ if (Vue && Vue.set) {
838
+ Vue.set(parent, key, parsed);
839
+ } else {
840
+ parent[key] = parsed;
841
+ }
677
842
  }
678
843
  this.requestUpdate();
679
844
  }
680
845
 
681
846
  _valClass(value) {
682
- if (value === null || value === undefined) return 'v-null';
683
- const t = typeof value;
684
- if (t === 'number') return 'v-num';
685
- if (t === 'boolean') return 'v-bool';
686
- if (t === 'string') return 'v-str';
687
- 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
+ }
688
863
  return 'v-obj';
689
864
  }
690
865
 
@@ -704,20 +879,21 @@ export class VueDevToolsPanel extends LitElement {
704
879
  `;
705
880
  }
706
881
  const vm = getInstance(this.selectedId);
707
- if (!vm)
882
+ if (!vm) {
708
883
  return html`
709
884
  <div class="empty">Component unmounted</div>
710
885
  `;
886
+ }
711
887
 
712
888
  const propsObj = vm._props || {};
713
889
  const dataObj = vm._data || vm.$data || {};
714
890
  const compDefs = (vm.$options && vm.$options.computed) || {};
715
891
  const compObj = {};
716
- for (const k of Object.keys(compDefs)) {
892
+ for (const key of Object.keys(compDefs)) {
717
893
  try {
718
- compObj[k] = vm[k];
894
+ compObj[key] = vm[key];
719
895
  } catch (err) {
720
- compObj[k] = `⚠ ${err && err.message}`;
896
+ compObj[key] = `⚠ ${err && err.message}`;
721
897
  }
722
898
  }
723
899
  const attrsObj = vm.$attrs || {};
@@ -760,27 +936,34 @@ export class VueDevToolsPanel extends LitElement {
760
936
  }
761
937
 
762
938
  _vmName(vm) {
763
- const o = vm.$options || {};
764
- let n = o.name || o._componentTag;
765
- if (!n && o.__file)
766
- 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)
767
943
  .split(/[\\/]/)
768
944
  .pop()
769
945
  .replace(/\.vue$/, '');
770
- if (!n && vm.$root === vm) n = 'Root';
771
- return n || 'Anonymous';
946
+ }
947
+ if (!name && vm.$root === vm) {
948
+ name = 'Root';
949
+ }
950
+ return name || 'Anonymous';
772
951
  }
773
952
 
774
953
  // Ask the Vite dev server to open the component's source file in the editor.
775
954
  _openInEditor(file) {
776
- if (!file) return;
955
+ if (!file) {
956
+ return;
957
+ }
777
958
  fetch('/__open-in-editor?file=' + encodeURIComponent(file)).catch(() => {});
778
959
  }
779
960
 
780
961
  // Scroll the component's root DOM element into view and flash the highlight.
781
962
  _scrollToComponent(vm) {
782
963
  const el = vm && vm.$el;
783
- if (!el || !el.scrollIntoView) return;
964
+ if (!el || !el.scrollIntoView) {
965
+ return;
966
+ }
784
967
  el.scrollIntoView({
785
968
  behavior: 'smooth',
786
969
  block: 'center',
@@ -803,17 +986,23 @@ export class VueDevToolsPanel extends LitElement {
803
986
  const lines = code.split('\n');
804
987
  let min = Infinity;
805
988
  for (let i = 1; i < lines.length; i++) {
806
- if (!lines[i].trim()) continue;
989
+ if (!lines[i].trim()) {
990
+ continue;
991
+ }
807
992
  const indent = lines[i].match(/^[ \t]*/)[0].length;
808
- if (indent < min) min = indent;
993
+ if (indent < min) {
994
+ min = indent;
995
+ }
809
996
  }
810
- if (!isFinite(min) || min === 0) return code;
811
- 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');
812
1001
  }
813
1002
 
814
1003
  render() {
815
1004
  return html`
816
- <div class="entry" @pointerdown=${e => this._onFabPointerDown(e)}>
1005
+ <div class="entry" @pointerdown=${event => this._onFabPointerDown(event)}>
817
1006
  <span class="fab-icon">${this._vueLogo()}</span>
818
1007
  </div>
819
1008
  ${this.collapsed ? null : this._renderPanel()}
@@ -833,6 +1022,10 @@ export class VueDevToolsPanel extends LitElement {
833
1022
  ${this._icon('vuex')}
834
1023
  <span class="tip">Vuex</span>
835
1024
  </button>
1025
+ <button class="side-tab ${this.tab === 'timeline' ? 'active' : ''}" @click=${() => (this.tab = 'timeline')}>
1026
+ ${this._icon('timeline')}
1027
+ <span class="tip">Timeline</span>
1028
+ </button>
836
1029
  <span class="side-spacer"></span>
837
1030
  ${this.tab === 'components'
838
1031
  ? html`
@@ -842,12 +1035,18 @@ export class VueDevToolsPanel extends LitElement {
842
1035
  </button>
843
1036
  `
844
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>
845
1042
  <button class="side-tab side-tab--min" @click=${() => (this.collapsed = true)}>
846
1043
  ${this._icon('min')}
847
1044
  <span class="tip">Minimize</span>
848
1045
  </button>
849
1046
  </nav>
850
- <div class="main">${this.tab === 'components' ? this._renderComponents() : this._renderVuex()}</div>
1047
+ <div class="main">
1048
+ ${this.tab === 'components' ? this._renderComponents() : this.tab === 'vuex' ? this._renderVuex() : this._renderTimeline()}
1049
+ </div>
851
1050
  ${this.renderCodeText != null
852
1051
  ? html`
853
1052
  <div class="code-overlay">
@@ -873,11 +1072,11 @@ export class VueDevToolsPanel extends LitElement {
873
1072
  type="search"
874
1073
  placeholder="Search components…"
875
1074
  .value=${this.query}
876
- @input=${e => (this.query = e.target.value)}
877
- @keydown=${e => {
878
- if (e.key === 'Escape') {
1075
+ @input=${event => (this.query = event.target.value)}
1076
+ @keydown=${event => {
1077
+ if (event.key === 'Escape') {
879
1078
  this.query = '';
880
- e.stopPropagation();
1079
+ event.stopPropagation();
881
1080
  }
882
1081
  }}
883
1082
  />
@@ -893,8 +1092,8 @@ export class VueDevToolsPanel extends LitElement {
893
1092
  <div class="empty">No component matches</div>
894
1093
  `
895
1094
  : filter
896
- ? filter.roots.map(n => this._renderSearchNode(n, 0, filter.show))
897
- : 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))}
898
1097
  </div>
899
1098
  <div class="detail">${this._renderDetail()}</div>
900
1099
  </div>
@@ -928,10 +1127,10 @@ export class VueDevToolsPanel extends LitElement {
928
1127
  </button>
929
1128
  </div>
930
1129
  ${snaps.map(
931
- (s, i) => html`
932
- <div class="node ${i === sel ? 'selected' : ''}" @click=${() => (this.vuexSelected = i)}>
933
- <span class="mut-index">${s.base ? '' : i}</span>
934
- <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>
935
1134
  </div>
936
1135
  `
937
1136
  )}
@@ -955,10 +1154,102 @@ export class VueDevToolsPanel extends LitElement {
955
1154
  `;
956
1155
  }
957
1156
 
958
- // Plugin logo (Baidu mark).
1157
+ // Merge component events + Vuex mutations into one chronological timeline
1158
+ // (like the vue-devtools v7 Timeline).
1159
+ _timelineEntries() {
1160
+ const entries = [];
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 });
1163
+ }
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 });
1169
+ });
1170
+ entries.sort((first, second) => first.time - second.time);
1171
+ return entries;
1172
+ }
1173
+
1174
+ _renderTimeline() {
1175
+ const entries = this._timelineEntries();
1176
+ if (!entries.length) {
1177
+ return html`
1178
+ <div class="body"><div class="empty">No timeline activity yet</div></div>
1179
+ `;
1180
+ }
1181
+ const sel = entries.find(item => item.id === this.timelineSelected) || entries[entries.length - 1];
1182
+ return html`
1183
+ <div class="body">
1184
+ <div class="tree">
1185
+ <div class="vuex-bar">
1186
+ <button
1187
+ class="btn"
1188
+ title="Clear recorded events"
1189
+ @click=${() => {
1190
+ clearEvents();
1191
+ this.timelineSelected = null;
1192
+ }}
1193
+ >
1194
+ ✕ Clear events
1195
+ </button>
1196
+ </div>
1197
+ ${entries.map(
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>
1203
+ </div>
1204
+ `
1205
+ )}
1206
+ </div>
1207
+ <div class="detail">${this._renderTimelineDetail(sel)}</div>
1208
+ </div>
1209
+ `;
1210
+ }
1211
+
1212
+ _renderTimelineDetail(entry) {
1213
+ if (!entry) {
1214
+ return null;
1215
+ }
1216
+ if (entry.kind === 'event') {
1217
+ const event = entry.event;
1218
+ const argsObj = {};
1219
+ (event.args || []).forEach((item, index) => {
1220
+ argsObj[index] = item;
1221
+ });
1222
+ return html`
1223
+ ${this._renderKvSection('event', { name: event.name, from: event.component, time: new Date(event.time).toLocaleTimeString() }, false)}
1224
+ ${event.args && event.args.length
1225
+ ? this._renderKvSection('payload', argsObj, false)
1226
+ : html`
1227
+ <div class="empty">No payload</div>
1228
+ `}
1229
+ `;
1230
+ }
1231
+ const snap = entry.snap;
1232
+ const store = getStore();
1233
+ const payloadObj = snap.payload === undefined ? null : { payload: snap.payload };
1234
+ return html`
1235
+ <button class="btn on time-travel" @click=${() => travelTo(entry.index)}>⏱ Time Travel</button>
1236
+ ${this._renderKvSection('mutation', { type: snap.type, time: new Date(snap.time).toLocaleTimeString() }, false)}
1237
+ ${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null}
1238
+ ${this._renderKvSection('state', snap.state || {}, false)}
1239
+ ${store ? this._renderKvSection('getters (live)', store.getters || {}, false) : null}
1240
+ `;
1241
+ }
1242
+
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)}`;
1247
+ }
1248
+
1249
+ // Plugin logo
959
1250
  _vueLogo() {
960
1251
  return html`
961
- <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">
962
1253
  <path
963
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"
964
1255
  />
@@ -969,6 +1260,19 @@ export class VueDevToolsPanel extends LitElement {
969
1260
  // Inline stroked icons (currentColor) for the sidebar / toolbar.
970
1261
  _icon(name) {
971
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
+ `;
972
1276
  case 'components':
973
1277
  return html`
974
1278
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
@@ -986,6 +1290,12 @@ export class VueDevToolsPanel extends LitElement {
986
1290
  <path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
987
1291
  </svg>
988
1292
  `;
1293
+ case 'timeline':
1294
+ return html`
1295
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1296
+ <path d="M3 12h4l3 8 4-16 3 8h4" />
1297
+ </svg>
1298
+ `;
989
1299
  case 'pick':
990
1300
  return html`
991
1301
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
@@ -1068,6 +1378,7 @@ export class VueDevToolsPanel extends LitElement {
1068
1378
  --c-null: #b45309;
1069
1379
  --c-fn: #2563eb;
1070
1380
  --c-obj: #475467;
1381
+ --logo-fill: #2932e1;
1071
1382
 
1072
1383
  position: fixed;
1073
1384
  inset: 0;
@@ -1077,6 +1388,25 @@ export class VueDevToolsPanel extends LitElement {
1077
1388
  font-size: 12px;
1078
1389
  color: var(--text);
1079
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
+ }
1080
1410
  .entry {
1081
1411
  position: fixed;
1082
1412
  pointer-events: auto;
@@ -1440,7 +1770,23 @@ export class VueDevToolsPanel extends LitElement {
1440
1770
  cursor: pointer;
1441
1771
 
1442
1772
  &:hover {
1443
- color: #475467;
1773
+ color: var(--text);
1774
+ }
1775
+ & .section-filter {
1776
+ margin-inline-start: 6px;
1777
+ inline-size: 96px;
1778
+ padding: 1px 6px;
1779
+ border: 1px solid var(--field-border);
1780
+ border-radius: 5px;
1781
+ background: var(--bg);
1782
+ color: var(--text-strong);
1783
+ font-size: 11px;
1784
+ text-transform: none;
1785
+ outline: none;
1786
+ cursor: text;
1787
+ }
1788
+ & .section-filter:focus {
1789
+ border-color: var(--accent);
1444
1790
  }
1445
1791
  }
1446
1792
  .vrow {
@@ -1544,6 +1890,21 @@ export class VueDevToolsPanel extends LitElement {
1544
1890
  display: inline-block;
1545
1891
  margin-block: 4px 8px;
1546
1892
  }
1893
+ .ev-time {
1894
+ flex: none;
1895
+ font-size: 10px;
1896
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1897
+ color: var(--muted);
1898
+ }
1899
+ .ev-comp {
1900
+ margin-inline-start: auto;
1901
+ padding-inline-start: 8px;
1902
+ color: var(--muted);
1903
+ }
1904
+ .node.selected .ev-time,
1905
+ .node.selected .ev-comp {
1906
+ color: rgb(255 255 255 / 0.85);
1907
+ }
1547
1908
  `;
1548
1909
  }
1549
1910