@metricinsights/pp-dev 1.0.0-beta.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +60 -0
  3. package/dist/CHANGELOG.md +50 -0
  4. package/dist/README.md +60 -0
  5. package/dist/cjs/cli.js +1 -1
  6. package/dist/cjs/cli.js.map +1 -1
  7. package/dist/cjs/index-DeUfAH6g.js +2 -0
  8. package/dist/cjs/index-DeUfAH6g.js.map +1 -0
  9. package/dist/cjs/index.js +1 -1
  10. package/dist/cjs/package.json +2 -2
  11. package/dist/cjs/{plugin-34c8_AjI.js → plugin-Cm9vRLCu.js} +2 -2
  12. package/dist/cjs/plugin-Cm9vRLCu.js.map +1 -0
  13. package/dist/cjs/plugin.js +1 -1
  14. package/dist/cjs/{version-plugin-BOFuJ-Rp.js → version-plugin-DsC5NLoX.js} +2 -2
  15. package/dist/cjs/{version-plugin-BOFuJ-Rp.js.map → version-plugin-DsC5NLoX.js.map} +1 -1
  16. package/dist/client/client.css +1 -1
  17. package/dist/client/client.css.map +1 -1
  18. package/dist/client/client.js +481 -30
  19. package/dist/client/client.js.map +1 -1
  20. package/dist/client/index.html +38 -2
  21. package/dist/esm/cli.js +1 -1
  22. package/dist/esm/cli.js.map +1 -1
  23. package/dist/esm/index-DOjGFYJ7.js +2 -0
  24. package/dist/esm/index-DOjGFYJ7.js.map +1 -0
  25. package/dist/esm/index.js +1 -1
  26. package/dist/esm/package.json +2 -2
  27. package/dist/esm/plugin-B2ocw-wb.js +2 -0
  28. package/dist/esm/plugin-B2ocw-wb.js.map +1 -0
  29. package/dist/esm/plugin.js +1 -1
  30. package/dist/esm/{version-plugin-vu8VWkws.js → version-plugin-BGWVTSsN.js} +2 -2
  31. package/dist/esm/{version-plugin-vu8VWkws.js.map → version-plugin-BGWVTSsN.js.map} +1 -1
  32. package/dist/types/index.d.ts +14 -1
  33. package/package.json +6 -6
  34. package/dist/cjs/index-MzGAxNTB.js +0 -2
  35. package/dist/cjs/index-MzGAxNTB.js.map +0 -1
  36. package/dist/cjs/plugin-34c8_AjI.js.map +0 -1
  37. package/dist/esm/index-C7dYPskY.js +0 -2
  38. package/dist/esm/index-C7dYPskY.js.map +0 -1
  39. package/dist/esm/plugin-Dvva-sxE.js +0 -2
  40. package/dist/esm/plugin-Dvva-sxE.js.map +0 -1
@@ -130,7 +130,14 @@ function createPPDevHotContext() {
130
130
  return context;
131
131
  }
132
132
 
133
- /// <reference types="vite/client" />
133
+ // localStorage helpers guarded against environments where storage is unavailable
134
+ // (e.g. sandboxed iframes). Keys follow the original `pp-dev-info-closed` naming.
135
+ const STORAGE_KEYS = {
136
+ closed: 'pp-dev-info-closed',
137
+ position: 'pp-dev-info-position',
138
+ autoHide: 'pp-dev-info-auto-hide',
139
+ hidden: 'pp-dev-info-hidden',
140
+ };
134
141
  function checkLocalStorage() {
135
142
  try {
136
143
  localStorage.setItem('test', 'test');
@@ -152,6 +159,412 @@ function getStorageItem(key) {
152
159
  }
153
160
  return null;
154
161
  }
162
+ function removeStorageItem(key) {
163
+ if (checkLocalStorage()) {
164
+ localStorage.removeItem(key);
165
+ }
166
+ }
167
+
168
+ const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
169
+ const POSITION_CLASS_PREFIX = 'pp-dev-info--';
170
+ const AUTO_HIDE_CLASS = 'pp-dev-info--auto-hide';
171
+ const HIDDEN_CLASS = 'pp-dev-info--hidden';
172
+ const CLOSED_CLASS$1 = 'closed';
173
+ const NO_TRANSITION_CLASS = 'is-dragging';
174
+ function isCorner(value) {
175
+ return typeof value === 'string' && CORNERS.includes(value);
176
+ }
177
+ function stateFromDataAttrs($panel) {
178
+ return {
179
+ position: isCorner($panel.dataset.position) ? $panel.dataset.position : 'bottom-right',
180
+ autoHide: $panel.dataset.autoHide === 'true',
181
+ hidden: $panel.dataset.hidden === 'true',
182
+ };
183
+ }
184
+ /**
185
+ * Resolve the effective panel state. Precedence per setting:
186
+ * localStorage (runtime user choice) → data-* attribute (config) → built-in default.
187
+ * The `?pp-dev-panel=show|hide` URL param writes a persistent localStorage override
188
+ * before resolution, so it both restores a hidden panel and survives reloads.
189
+ */
190
+ function resolvePanelState($panel) {
191
+ const param = new URLSearchParams(window.location.search).get('pp-dev-panel');
192
+ if (param === 'show') {
193
+ setStorageItem(STORAGE_KEYS.hidden, 'false');
194
+ }
195
+ else if (param === 'hide') {
196
+ setStorageItem(STORAGE_KEYS.hidden, 'true');
197
+ }
198
+ const defaults = stateFromDataAttrs($panel);
199
+ const storedPosition = getStorageItem(STORAGE_KEYS.position);
200
+ const storedAutoHide = getStorageItem(STORAGE_KEYS.autoHide);
201
+ const storedHidden = getStorageItem(STORAGE_KEYS.hidden);
202
+ return {
203
+ position: isCorner(storedPosition) ? storedPosition : defaults.position,
204
+ autoHide: storedAutoHide !== null ? storedAutoHide === 'true' : defaults.autoHide,
205
+ hidden: storedHidden !== null ? storedHidden === 'true' : defaults.hidden,
206
+ };
207
+ }
208
+ function applyPanelState($panel, state, opts) {
209
+ const apply = () => {
210
+ for (const corner of CORNERS) {
211
+ $panel.classList.toggle(POSITION_CLASS_PREFIX + corner, corner === state.position);
212
+ }
213
+ $panel.classList.toggle(AUTO_HIDE_CLASS, state.autoHide);
214
+ $panel.classList.toggle(HIDDEN_CLASS, state.hidden);
215
+ };
216
+ if (opts?.instant) {
217
+ // No-transition guard: when localStorage differs from the server-rendered corner,
218
+ // the panel must not visibly slide across the screen on load. `is-dragging`
219
+ // disables transitions; a reflow makes the class swap land before it is removed.
220
+ $panel.classList.add(NO_TRANSITION_CLASS);
221
+ apply();
222
+ const unlock = () => $panel.classList.remove(NO_TRANSITION_CLASS);
223
+ if (typeof requestAnimationFrame === 'function') {
224
+ requestAnimationFrame(unlock);
225
+ }
226
+ else {
227
+ setTimeout(unlock, 0);
228
+ }
229
+ }
230
+ else {
231
+ apply();
232
+ }
233
+ }
234
+ function clearClosed($panel) {
235
+ $panel.classList.remove(CLOSED_CLASS$1);
236
+ $panel.querySelector('.pp-dev-info__wrap-btn svg')?.classList.remove(CLOSED_CLASS$1);
237
+ removeStorageItem(STORAGE_KEYS.closed);
238
+ }
239
+ function createPanelStateController($panel) {
240
+ let state = resolvePanelState($panel);
241
+ const listeners = [];
242
+ if (state.autoHide) {
243
+ // Auto-hide and minimize are mutually exclusive; a stale persisted `closed`
244
+ // state must not combine with the auto-hide transform.
245
+ clearClosed($panel);
246
+ }
247
+ applyPanelState($panel, state, { instant: true });
248
+ const commit = () => {
249
+ applyPanelState($panel, state);
250
+ for (const cb of listeners) {
251
+ cb(state);
252
+ }
253
+ };
254
+ return {
255
+ getState: () => state,
256
+ setPosition(corner) {
257
+ state = { ...state, position: corner };
258
+ setStorageItem(STORAGE_KEYS.position, corner);
259
+ commit();
260
+ },
261
+ setAutoHide(on) {
262
+ state = { ...state, autoHide: on };
263
+ setStorageItem(STORAGE_KEYS.autoHide, String(on));
264
+ if (on) {
265
+ clearClosed($panel);
266
+ }
267
+ commit();
268
+ },
269
+ setHidden(on) {
270
+ state = { ...state, hidden: on };
271
+ setStorageItem(STORAGE_KEYS.hidden, String(on));
272
+ commit();
273
+ },
274
+ reset() {
275
+ removeStorageItem(STORAGE_KEYS.position);
276
+ removeStorageItem(STORAGE_KEYS.autoHide);
277
+ removeStorageItem(STORAGE_KEYS.hidden);
278
+ clearClosed($panel);
279
+ // Re-resolve purely from the server-rendered config (bypass the URL param,
280
+ // which would immediately re-write its localStorage override).
281
+ state = stateFromDataAttrs($panel);
282
+ commit();
283
+ },
284
+ onChange(cb) {
285
+ listeners.push(cb);
286
+ },
287
+ };
288
+ }
289
+
290
+ const AUTO_HIDE_SHOW_DELAY = 300;
291
+ const AUTO_HIDE_HIDE_DELAY = 500;
292
+ const PEEKING_CLASS = 'is-peeking';
293
+ const DRAGGING_CLASS = 'is-dragging';
294
+ /** Nearest screen corner for a viewport point; ties resolve toward bottom-right. */
295
+ function nearestCorner(x, y, viewportWidth, viewportHeight) {
296
+ const left = x < viewportWidth / 2;
297
+ const top = y < viewportHeight / 2;
298
+ if (top) {
299
+ return left ? 'top-left' : 'top-right';
300
+ }
301
+ return left ? 'bottom-left' : 'bottom-right';
302
+ }
303
+ function clamp(value, min, max) {
304
+ return Math.min(Math.max(value, min), max);
305
+ }
306
+ /**
307
+ * Drag the panel by its handle and snap to the nearest corner on release.
308
+ * Pointer capture keeps the drag tracking over iframes and outside the window.
309
+ */
310
+ function initDrag($panel, $handle, onSnap) {
311
+ let dragging = false;
312
+ let pointerId = -1;
313
+ let offsetX = 0;
314
+ let offsetY = 0;
315
+ let panelWidth = 0;
316
+ let panelHeight = 0;
317
+ const onKeyDown = (ev) => {
318
+ if (ev.key === 'Escape') {
319
+ stop(false);
320
+ }
321
+ };
322
+ const stop = (snap, ev) => {
323
+ if (!dragging) {
324
+ return;
325
+ }
326
+ dragging = false;
327
+ try {
328
+ $handle.releasePointerCapture(pointerId);
329
+ }
330
+ catch {
331
+ // pointer already released
332
+ }
333
+ document.body.style.userSelect = '';
334
+ document.removeEventListener('keydown', onKeyDown, true);
335
+ // Re-enable transitions before clearing the inline placement so the snap animates.
336
+ $panel.classList.remove(DRAGGING_CLASS);
337
+ $panel.style.left = '';
338
+ $panel.style.top = '';
339
+ $panel.style.right = '';
340
+ $panel.style.bottom = '';
341
+ $panel.style.transform = '';
342
+ if (snap && ev) {
343
+ onSnap(nearestCorner(ev.clientX, ev.clientY, window.innerWidth, window.innerHeight));
344
+ }
345
+ };
346
+ const moveTo = (ev) => {
347
+ const x = clamp(ev.clientX - offsetX, 0, Math.max(0, window.innerWidth - panelWidth));
348
+ const y = clamp(ev.clientY - offsetY, 0, Math.max(0, window.innerHeight - panelHeight));
349
+ $panel.style.transform = `translate3d(${x}px, ${y}px, 0)`;
350
+ };
351
+ $handle.addEventListener('pointerdown', (ev) => {
352
+ if (ev.pointerType === 'mouse' && ev.button !== 0) {
353
+ return;
354
+ }
355
+ const rect = $panel.getBoundingClientRect();
356
+ dragging = true;
357
+ pointerId = ev.pointerId;
358
+ offsetX = ev.clientX - rect.left;
359
+ offsetY = ev.clientY - rect.top;
360
+ panelWidth = rect.width;
361
+ panelHeight = rect.height;
362
+ $handle.setPointerCapture(ev.pointerId);
363
+ $panel.classList.add(DRAGGING_CLASS);
364
+ // Anchor to the top-left origin so translate3d coordinates are viewport-absolute;
365
+ // inline styles override the corner-class placement for the duration of the drag.
366
+ $panel.style.left = '0';
367
+ $panel.style.top = '0';
368
+ $panel.style.right = 'auto';
369
+ $panel.style.bottom = 'auto';
370
+ moveTo(ev);
371
+ document.body.style.userSelect = 'none';
372
+ document.addEventListener('keydown', onKeyDown, true);
373
+ ev.preventDefault();
374
+ });
375
+ $handle.addEventListener('pointermove', (ev) => {
376
+ if (dragging) {
377
+ moveTo(ev);
378
+ }
379
+ });
380
+ $handle.addEventListener('pointerup', (ev) => stop(true, ev));
381
+ $handle.addEventListener('pointercancel', () => stop(false));
382
+ }
383
+ /**
384
+ * Hover-reveal behavior for the auto-hide mode: pointer over the exposed strip for
385
+ * AUTO_HIDE_SHOW_DELAY slides the panel out; leaving re-hides it after a grace period.
386
+ * Keyboard focus inside the panel also keeps it revealed.
387
+ */
388
+ function initAutoHide($panel, isActive) {
389
+ let showTimer;
390
+ let hideTimer;
391
+ let popoverOpen = false;
392
+ const shouldStayPeeked = () => popoverOpen || $panel.matches(':focus-within') || $panel.classList.contains(DRAGGING_CLASS);
393
+ const scheduleHide = () => {
394
+ window.clearTimeout(hideTimer);
395
+ hideTimer = window.setTimeout(() => {
396
+ if (!shouldStayPeeked() && !$panel.matches(':hover')) {
397
+ $panel.classList.remove(PEEKING_CLASS);
398
+ }
399
+ }, AUTO_HIDE_HIDE_DELAY);
400
+ };
401
+ $panel.addEventListener('pointerenter', () => {
402
+ if (!isActive()) {
403
+ return;
404
+ }
405
+ window.clearTimeout(hideTimer);
406
+ showTimer = window.setTimeout(() => $panel.classList.add(PEEKING_CLASS), AUTO_HIDE_SHOW_DELAY);
407
+ });
408
+ $panel.addEventListener('pointerleave', () => {
409
+ if (!isActive()) {
410
+ return;
411
+ }
412
+ window.clearTimeout(showTimer);
413
+ scheduleHide();
414
+ });
415
+ $panel.addEventListener('focusin', () => {
416
+ if (isActive()) {
417
+ window.clearTimeout(hideTimer);
418
+ $panel.classList.add(PEEKING_CLASS);
419
+ }
420
+ });
421
+ $panel.addEventListener('focusout', () => {
422
+ if (isActive()) {
423
+ scheduleHide();
424
+ }
425
+ });
426
+ return {
427
+ keepPeeked(on) {
428
+ popoverOpen = on;
429
+ if (!isActive()) {
430
+ return;
431
+ }
432
+ if (on) {
433
+ window.clearTimeout(showTimer);
434
+ window.clearTimeout(hideTimer);
435
+ $panel.classList.add(PEEKING_CLASS);
436
+ }
437
+ else {
438
+ scheduleHide();
439
+ }
440
+ },
441
+ };
442
+ }
443
+
444
+ const CORNER_TITLES = {
445
+ 'top-left': 'Top left',
446
+ 'top-right': 'Top right',
447
+ 'bottom-left': 'Bottom left',
448
+ 'bottom-right': 'Bottom right',
449
+ };
450
+ function buildPopover(controller) {
451
+ const state = controller.getState();
452
+ const $popover = document.createElement('div');
453
+ $popover.classList.add('pp-dev-info__settings');
454
+ const cornerButtons = CORNERS.map((corner) => {
455
+ const active = corner === state.position ? ' active' : '';
456
+ return `<button type="button" class="pp-dev-info__corner-btn pp-dev-info__corner-btn--${corner}${active}" data-corner="${corner}" title="${CORNER_TITLES[corner]}" aria-pressed="${corner === state.position}"></button>`;
457
+ }).join('');
458
+ $popover.innerHTML = `
459
+ <div class="pp-dev-info__settings-title">Panel settings</div>
460
+ <div class="pp-dev-info__settings-row">
461
+ <span class="pp-dev-info__settings-label">Position</span>
462
+ <div class="pp-dev-info__corner-grid">${cornerButtons}</div>
463
+ </div>
464
+ <div class="pp-dev-info__settings-row">
465
+ <label class="pp-dev-info__settings-label" for="pp-dev-auto-hide-toggle">Auto-hide</label>
466
+ <input
467
+ type="checkbox"
468
+ id="pp-dev-auto-hide-toggle"
469
+ class="pp-dev-info__settings-toggle"
470
+ ${state.autoHide ? 'checked' : ''}
471
+ />
472
+ </div>
473
+ <button type="button" class="pp-dev-info__settings-hide-btn">Hide panel</button>
474
+ <div class="pp-dev-info__settings-hint">Restore with <code>?pp-dev-panel=show</code> in the URL</div>
475
+ <span class="pp-dev-info__settings-reset" role="button" tabindex="0">Reset to config defaults</span>
476
+ `;
477
+ return $popover;
478
+ }
479
+ function syncPopover($popover, controller) {
480
+ const state = controller.getState();
481
+ $popover.querySelectorAll('.pp-dev-info__corner-btn').forEach(($btn) => {
482
+ const active = $btn.dataset.corner === state.position;
483
+ $btn.classList.toggle('active', active);
484
+ $btn.setAttribute('aria-pressed', String(active));
485
+ });
486
+ const $toggle = $popover.querySelector('.pp-dev-info__settings-toggle');
487
+ if ($toggle) {
488
+ $toggle.checked = state.autoHide;
489
+ }
490
+ }
491
+ /** Settings popover: corner picker, auto-hide toggle, hide button, reset. */
492
+ function initPanelSettings($panel, controller, hooks) {
493
+ const $btn = $panel.querySelector('.pp-dev-info__settings-btn');
494
+ if (!$btn) {
495
+ return;
496
+ }
497
+ let $popover = null;
498
+ const onDocClick = (ev) => {
499
+ const target = ev.target;
500
+ if ($popover && !$popover.contains(target) && !$btn.contains(target)) {
501
+ close();
502
+ }
503
+ };
504
+ const onKeyDown = (ev) => {
505
+ if (ev.key === 'Escape') {
506
+ close();
507
+ }
508
+ };
509
+ const close = () => {
510
+ if (!$popover) {
511
+ return;
512
+ }
513
+ $popover.remove();
514
+ $popover = null;
515
+ document.removeEventListener('click', onDocClick, true);
516
+ document.removeEventListener('keydown', onKeyDown, true);
517
+ hooks?.onOpenChange?.(false);
518
+ };
519
+ const open = () => {
520
+ $popover = buildPopover(controller);
521
+ $popover.querySelectorAll('.pp-dev-info__corner-btn').forEach(($cornerBtn) => {
522
+ $cornerBtn.addEventListener('click', (ev) => {
523
+ ev.preventDefault();
524
+ controller.setPosition($cornerBtn.dataset.corner);
525
+ });
526
+ });
527
+ $popover.querySelector('.pp-dev-info__settings-toggle')?.addEventListener('change', (ev) => {
528
+ controller.setAutoHide(ev.target.checked);
529
+ });
530
+ $popover.querySelector('.pp-dev-info__settings-hide-btn')?.addEventListener('click', (ev) => {
531
+ ev.preventDefault();
532
+ close();
533
+ controller.setHidden(true);
534
+ });
535
+ const $reset = $popover.querySelector('.pp-dev-info__settings-reset');
536
+ $reset?.addEventListener('click', (ev) => {
537
+ ev.preventDefault();
538
+ controller.reset();
539
+ });
540
+ $reset?.addEventListener('keydown', (ev) => {
541
+ if (ev.key === 'Enter' || ev.key === ' ') {
542
+ ev.preventDefault();
543
+ controller.reset();
544
+ }
545
+ });
546
+ $panel.appendChild($popover);
547
+ document.addEventListener('click', onDocClick, true);
548
+ document.addEventListener('keydown', onKeyDown, true);
549
+ hooks?.onOpenChange?.(true);
550
+ };
551
+ $btn.addEventListener('click', (ev) => {
552
+ ev.preventDefault();
553
+ if ($popover) {
554
+ close();
555
+ }
556
+ else {
557
+ open();
558
+ }
559
+ });
560
+ controller.onChange(() => {
561
+ if ($popover) {
562
+ syncPopover($popover, controller);
563
+ }
564
+ });
565
+ }
566
+
567
+ /// <reference types="vite/client" />
155
568
  const POPUP_OFFSET = 10;
156
569
  const POPUP_HEIGHT = 100;
157
570
  const ANIMATION_DURATION = 300;
@@ -207,22 +620,30 @@ function createPopupElement(opts) {
207
620
  $popup.innerHTML = template;
208
621
  return $popup;
209
622
  }
623
+ let panelController = null;
210
624
  function updatePopupPositions() {
211
625
  const popups = document.querySelectorAll('.pp-dev-info-namespace:not(.pp-dev-info)');
212
- const $devPanel = document.querySelector('.pp-dev-info');
213
- // Update popup positions
626
+ const position = panelController?.getState().position ?? 'bottom-right';
627
+ const isLeft = position === 'top-left' || position === 'bottom-left';
628
+ const isTop = position === 'top-left' || position === 'top-right';
629
+ // Anchor popups to the panel's horizontal side and stack them from the vertically
630
+ // opposite edge so they never cover the panel.
214
631
  popups.forEach((popup, index) => {
215
- const top = POPUP_OFFSET + index * (POPUP_HEIGHT + POPUP_OFFSET);
632
+ const offset = POPUP_OFFSET + index * (POPUP_HEIGHT + POPUP_OFFSET);
216
633
  const $popupContent = popup.querySelector('.pp-dev-info__popup');
217
- if ($popupContent) {
218
- $popupContent.style.top = `${top}px`;
634
+ if (!$popupContent) {
635
+ return;
636
+ }
637
+ $popupContent.classList.toggle('pp-dev-info__popup--left', isLeft);
638
+ if (isTop) {
639
+ $popupContent.style.top = 'auto';
640
+ $popupContent.style.bottom = `${offset}px`;
641
+ }
642
+ else {
643
+ $popupContent.style.bottom = 'auto';
644
+ $popupContent.style.top = `${offset}px`;
219
645
  }
220
646
  });
221
- // Ensure dev panel stays at the bottom
222
- if ($devPanel) {
223
- $devPanel.style.top = 'auto';
224
- $devPanel.style.bottom = '0';
225
- }
226
647
  }
227
648
  function animatePopup($popup, type) {
228
649
  return new Promise((resolve) => {
@@ -349,12 +770,59 @@ function confirmModal(opts) {
349
770
  const url = window.location.origin + '/@pp-dev/inspector';
350
771
  console.log('%cpp-dev%c 🔍 Request Inspector → %c%s', 'background:#6e8efb;color:#fff;padding:2px 8px;border-radius:4px;font-weight:700;font-size:11px', 'color:#a0a0b8;font-size:11px', 'color:#a78bfa;font-size:11px;text-decoration:underline', url);
351
772
  })();
773
+ // ── Panel UI: position, auto-hide, hide, minimize ─────────────────────────────
774
+ // Initialized outside the hot-context guard — panel placement and visibility must
775
+ // not depend on the WebSocket transport being available.
776
+ const CLOSED_CLASS = 'closed';
777
+ const $infoPanel = document.querySelector('.pp-dev-info');
778
+ if ($infoPanel) {
779
+ panelController = createPanelStateController($infoPanel);
780
+ const $dragHandle = $infoPanel.querySelector('.pp-dev-info__drag-handle');
781
+ if ($dragHandle) {
782
+ initDrag($infoPanel, $dragHandle, (corner) => panelController.setPosition(corner));
783
+ }
784
+ const autoHide = initAutoHide($infoPanel, () => panelController.getState().autoHide);
785
+ initPanelSettings($infoPanel, panelController, {
786
+ onOpenChange: (open) => autoHide.keepPeeked(open),
787
+ });
788
+ panelController.onChange((state) => {
789
+ if (!state.autoHide) {
790
+ $infoPanel.classList.remove('is-peeking');
791
+ }
792
+ updatePopupPositions();
793
+ });
794
+ // Minimize button; in auto-hide mode it acts as "pin" (disables auto-hide).
795
+ const $minimizeButtonWrap = $infoPanel.querySelector('.pp-dev-info__wrap-btn');
796
+ const $minimizeButtonSVG = $minimizeButtonWrap?.querySelector('svg');
797
+ if ($minimizeButtonWrap && $minimizeButtonSVG) {
798
+ let isClosed = getStorageItem(STORAGE_KEYS.closed) === 'true' && !panelController.getState().autoHide;
799
+ const updateTitle = () => {
800
+ $minimizeButtonWrap.title = panelController.getState().autoHide ? 'Pin panel' : 'Minimize';
801
+ };
802
+ updateTitle();
803
+ panelController.onChange(updateTitle);
804
+ if (isClosed) {
805
+ $infoPanel.classList.add(CLOSED_CLASS);
806
+ $minimizeButtonSVG.classList.add(CLOSED_CLASS);
807
+ }
808
+ $minimizeButtonWrap.addEventListener('click', (e) => {
809
+ e.preventDefault();
810
+ if (panelController.getState().autoHide) {
811
+ isClosed = false;
812
+ panelController.setAutoHide(false);
813
+ return;
814
+ }
815
+ $infoPanel.classList.toggle(CLOSED_CLASS);
816
+ $minimizeButtonSVG.classList.toggle(CLOSED_CLASS);
817
+ isClosed = !isClosed;
818
+ setStorageItem(STORAGE_KEYS.closed, isClosed ? 'true' : 'false');
819
+ });
820
+ }
821
+ }
352
822
  // Use Vite's HMR context when available; otherwise fall back to a raw-WebSocket
353
823
  // shim so the dev panel also works under the `pp-dev next` server (no Vite HMR).
354
824
  const hot = import.meta.hot ?? createPPDevHotContext();
355
825
  if (hot) {
356
- const CLOSED_CLASS = 'closed';
357
- const CLOSED_CLASS_STORAGE_KEY = 'pp-dev-info-closed';
358
826
  hot.on('redirect', (data) => {
359
827
  window.location.href = data.url;
360
828
  });
@@ -378,23 +846,6 @@ if (hot) {
378
846
  }
379
847
  }
380
848
  });
381
- let isClosed = getStorageItem(CLOSED_CLASS_STORAGE_KEY) === 'true' || false;
382
- const $infoPanel = document.querySelector('.pp-dev-info');
383
- const $minimizeButtonWrap = document.querySelector('.pp-dev-info__wrap-btn');
384
- const $minimizeButtonSVG = $minimizeButtonWrap?.querySelector('svg');
385
- if ($infoPanel && $minimizeButtonWrap && $minimizeButtonSVG) {
386
- if (isClosed) {
387
- $infoPanel.classList.add(CLOSED_CLASS);
388
- $minimizeButtonSVG.classList.add(CLOSED_CLASS);
389
- }
390
- $minimizeButtonWrap.addEventListener('click', (e) => {
391
- e.preventDefault();
392
- $infoPanel.classList.toggle(CLOSED_CLASS);
393
- $minimizeButtonSVG.classList.toggle(CLOSED_CLASS);
394
- isClosed = !isClosed;
395
- setStorageItem(CLOSED_CLASS_STORAGE_KEY, isClosed ? 'true' : 'false');
396
- });
397
- }
398
849
  const $syncButton = document.getElementById('sync-template');
399
850
  if ($syncButton) {
400
851
  hot.on('template:sync:action-required', async (payload) => {