@metricinsights/pp-dev 0.19.0 → 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 (51) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +298 -266
  3. package/dist/CHANGELOG.md +198 -0
  4. package/dist/README.md +298 -266
  5. package/dist/cjs/cli.js +1 -1
  6. package/dist/cjs/cli.js.map +1 -1
  7. package/dist/cjs/helpers.js +1 -1
  8. package/dist/cjs/helpers.js.map +1 -1
  9. package/dist/cjs/index-DeUfAH6g.js +2 -0
  10. package/dist/cjs/index-DeUfAH6g.js.map +1 -0
  11. package/dist/cjs/index.js +1 -1
  12. package/dist/cjs/migrate-r2mXdGLZ.js +2 -0
  13. package/dist/cjs/migrate-r2mXdGLZ.js.map +1 -0
  14. package/dist/cjs/package.json +16 -16
  15. package/dist/cjs/plugin-Cm9vRLCu.js +2 -0
  16. package/dist/cjs/plugin-Cm9vRLCu.js.map +1 -0
  17. package/dist/cjs/plugin.js +1 -1
  18. package/dist/cjs/version-plugin-DsC5NLoX.js +2 -0
  19. package/dist/cjs/{version-plugin-DUKsGBsW.js.map → version-plugin-DsC5NLoX.js.map} +1 -1
  20. package/dist/client/client.css +1 -1
  21. package/dist/client/client.css.map +1 -1
  22. package/dist/client/client.js +509 -47
  23. package/dist/client/client.js.map +1 -1
  24. package/dist/client/index.html +71 -18
  25. package/dist/esm/cli.js +1 -1
  26. package/dist/esm/cli.js.map +1 -1
  27. package/dist/esm/helpers.js +1 -1
  28. package/dist/esm/helpers.js.map +1 -1
  29. package/dist/esm/index-DOjGFYJ7.js +2 -0
  30. package/dist/esm/index-DOjGFYJ7.js.map +1 -0
  31. package/dist/esm/index.js +1 -1
  32. package/dist/esm/migrate-BYuCtRbI.js +2 -0
  33. package/dist/esm/migrate-BYuCtRbI.js.map +1 -0
  34. package/dist/esm/package.json +16 -16
  35. package/dist/esm/plugin-B2ocw-wb.js +2 -0
  36. package/dist/esm/plugin-B2ocw-wb.js.map +1 -0
  37. package/dist/esm/plugin.js +1 -1
  38. package/dist/esm/version-plugin-BGWVTSsN.js +2 -0
  39. package/dist/esm/{version-plugin-C7j2jDQX.js.map → version-plugin-BGWVTSsN.js.map} +1 -1
  40. package/dist/types/index.d.ts +116 -139
  41. package/package.json +34 -28
  42. package/dist/cjs/index-Bb3yIW5d.js +0 -2
  43. package/dist/cjs/index-Bb3yIW5d.js.map +0 -1
  44. package/dist/cjs/plugin-CqahMAGe.js +0 -2
  45. package/dist/cjs/plugin-CqahMAGe.js.map +0 -1
  46. package/dist/cjs/version-plugin-DUKsGBsW.js +0 -2
  47. package/dist/esm/index-Cd-DmbPQ.js +0 -2
  48. package/dist/esm/index-Cd-DmbPQ.js.map +0 -1
  49. package/dist/esm/plugin-BeehFkJq.js +0 -2
  50. package/dist/esm/plugin-BeehFkJq.js.map +0 -1
  51. package/dist/esm/version-plugin-C7j2jDQX.js +0 -2
@@ -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,11 +159,419 @@ 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;
158
571
  const CONFIRM_MODAL_OVERLAY_CLASS = 'pp-dev-info__confirm-overlay';
159
572
  const activeConfirmModals = new Map();
573
+ const ICON_SIZE = 16;
574
+ const CLOSE_ICON_SIZE = 12;
160
575
  function teardownConfirmModal(overlay, result) {
161
576
  const entry = activeConfirmModals.get(overlay);
162
577
  if (!entry) {
@@ -167,19 +582,27 @@ function teardownConfirmModal(overlay, result) {
167
582
  overlay.remove();
168
583
  entry.resolve(result);
169
584
  }
585
+ const TYPE_ICONS = {
586
+ success: `<svg viewBox="0 0 24 24" width="${ICON_SIZE}" height="${ICON_SIZE}" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`,
587
+ danger: `<svg viewBox="0 0 24 24" width="${ICON_SIZE}" height="${ICON_SIZE}" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>`,
588
+ warning: `<svg viewBox="0 0 24 24" width="${ICON_SIZE}" height="${ICON_SIZE}" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,
589
+ info: `<svg viewBox="0 0 24 24" width="${ICON_SIZE}" height="${ICON_SIZE}" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`,
590
+ };
170
591
  function createPopupElement(opts) {
171
592
  const $popup = document.createElement('div');
172
593
  $popup.classList.add('pp-dev-info-namespace');
173
594
  const typeClass = opts.type ? `pp-dev-info__popup--${opts.type}` : '';
595
+ const iconHtml = opts.type ? `<div class="pp-dev-info__popup-title-icon">${TYPE_ICONS[opts.type]}</div>` : '';
174
596
  const template = `
175
597
  <div class="pp-dev-info__popup ${typeClass} ${opts.className || ''}" style="${opts.style || ''}">
176
598
  <div class="pp-dev-info__popup-title">
599
+ ${iconHtml}
177
600
  <div class="pp-dev-info__popup-title-text">${opts.title}</div>
178
601
  <div class="pp-dev-info__popup-title-close">
179
602
  <svg
180
603
  viewBox="0 0 24 24"
181
- width="16"
182
- height="16"
604
+ width="${CLOSE_ICON_SIZE}"
605
+ height="${CLOSE_ICON_SIZE}"
183
606
  stroke="currentColor"
184
607
  stroke-width="1.5"
185
608
  fill="none"
@@ -192,29 +615,39 @@ function createPopupElement(opts) {
192
615
  </div>
193
616
  </div>
194
617
  <div class="pp-dev-info__popup-content">${opts.content}</div>
195
- <div class="pp-dev-info__popup-progress"></div>
196
618
  </div>
197
619
  `;
198
620
  $popup.innerHTML = template;
199
621
  return $popup;
200
622
  }
623
+ let panelController = null;
201
624
  function updatePopupPositions() {
202
625
  const popups = document.querySelectorAll('.pp-dev-info-namespace:not(.pp-dev-info)');
203
- const $devPanel = document.querySelector('.pp-dev-info');
204
- // 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.
205
631
  popups.forEach((popup, index) => {
206
- const top = POPUP_OFFSET + index * (POPUP_HEIGHT + POPUP_OFFSET);
207
- popup.style.top = `${top}px`;
632
+ const offset = POPUP_OFFSET + index * (POPUP_HEIGHT + POPUP_OFFSET);
633
+ const $popupContent = popup.querySelector('.pp-dev-info__popup');
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`;
645
+ }
208
646
  });
209
- // Ensure dev panel stays at the bottom
210
- if ($devPanel) {
211
- $devPanel.style.top = 'auto';
212
- $devPanel.style.bottom = '0';
213
- }
214
647
  }
215
648
  function animatePopup($popup, type) {
216
649
  return new Promise((resolve) => {
217
- const $popupContent = $popup.querySelector('.pp-dev-info-namespace');
650
+ const $popupContent = $popup.querySelector('.pp-dev-info__popup');
218
651
  if (!$popupContent) {
219
652
  return resolve();
220
653
  }
@@ -237,7 +670,6 @@ function animatePopup($popup, type) {
237
670
  function infoPopup(opts) {
238
671
  const $popup = createPopupElement(opts);
239
672
  const $closeButton = $popup.querySelector('.pp-dev-info__popup-title-close');
240
- const $progressBar = $popup.querySelector('.pp-dev-info__popup-progress');
241
673
  const removePopup = async () => {
242
674
  await animatePopup($popup, 'exit');
243
675
  $popup.remove();
@@ -254,8 +686,7 @@ function infoPopup(opts) {
254
686
  let remainingTime = duration;
255
687
  let lastUpdate = Date.now();
256
688
  let isVisible = true;
257
- // Update progress bar
258
- const updateProgress = () => {
689
+ const scheduleDismiss = () => {
259
690
  if (!isVisible) {
260
691
  return;
261
692
  }
@@ -267,22 +698,17 @@ function infoPopup(opts) {
267
698
  removePopup();
268
699
  return;
269
700
  }
270
- const progress = (remainingTime / duration) * 100;
271
- if ($progressBar) {
272
- $progressBar.style.width = `${progress}%`;
273
- }
274
- requestAnimationFrame(updateProgress);
701
+ requestAnimationFrame(scheduleDismiss);
275
702
  };
276
703
  // Handle visibility change
277
704
  document.addEventListener('visibilitychange', () => {
278
705
  isVisible = !document.hidden;
279
706
  if (isVisible) {
280
707
  lastUpdate = Date.now();
281
- requestAnimationFrame(updateProgress);
708
+ requestAnimationFrame(scheduleDismiss);
282
709
  }
283
710
  });
284
- // Start progress bar animation
285
- requestAnimationFrame(updateProgress);
711
+ requestAnimationFrame(scheduleDismiss);
286
712
  }
287
713
  }
288
714
  function closeAllConfirmModals() {
@@ -337,12 +763,66 @@ function confirmModal(opts) {
337
763
  document.body.appendChild($overlay);
338
764
  });
339
765
  }
766
+ // ── Inspector console banner ──────────────────────────────────────────────────
767
+ // Logged once on page load so it is visible in DevTools history when the console
768
+ // is opened. The message is harmless if the inspector is disabled.
769
+ (function printInspectorBanner() {
770
+ const url = window.location.origin + '/@pp-dev/inspector';
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);
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
+ }
340
822
  // Use Vite's HMR context when available; otherwise fall back to a raw-WebSocket
341
823
  // shim so the dev panel also works under the `pp-dev next` server (no Vite HMR).
342
824
  const hot = import.meta.hot ?? createPPDevHotContext();
343
825
  if (hot) {
344
- const CLOSED_CLASS = 'closed';
345
- const CLOSED_CLASS_STORAGE_KEY = 'pp-dev-info-closed';
346
826
  hot.on('redirect', (data) => {
347
827
  window.location.href = data.url;
348
828
  });
@@ -366,23 +846,6 @@ if (hot) {
366
846
  }
367
847
  }
368
848
  });
369
- let isClosed = getStorageItem(CLOSED_CLASS_STORAGE_KEY) === 'true' || false;
370
- const $infoPanel = document.querySelector('.pp-dev-info');
371
- const $minimizeButtonWrap = document.querySelector('.pp-dev-info__wrap-btn');
372
- const $minimizeButtonSVG = $minimizeButtonWrap?.querySelector('svg');
373
- if ($infoPanel && $minimizeButtonWrap && $minimizeButtonSVG) {
374
- if (isClosed) {
375
- $infoPanel.classList.add(CLOSED_CLASS);
376
- $minimizeButtonSVG.classList.add(CLOSED_CLASS);
377
- }
378
- $minimizeButtonWrap.addEventListener('click', (e) => {
379
- e.preventDefault();
380
- $infoPanel.classList.toggle(CLOSED_CLASS);
381
- $minimizeButtonSVG.classList.toggle(CLOSED_CLASS);
382
- isClosed = !isClosed;
383
- setStorageItem(CLOSED_CLASS_STORAGE_KEY, isClosed ? 'true' : 'false');
384
- });
385
- }
386
849
  const $syncButton = document.getElementById('sync-template');
387
850
  if ($syncButton) {
388
851
  hot.on('template:sync:action-required', async (payload) => {
@@ -406,7 +869,6 @@ if (hot) {
406
869
  infoPopup({
407
870
  title: 'Sync cancelled',
408
871
  content: payload.message,
409
- className: 'pp-dev-info__popup--warning',
410
872
  type: 'warning',
411
873
  });
412
874
  }
@@ -414,7 +876,7 @@ if (hot) {
414
876
  infoPopup({
415
877
  title: 'Sync error',
416
878
  content: payload.error,
417
- className: 'pp-dev-info__popup--danger',
879
+ type: 'danger',
418
880
  });
419
881
  if (payload.refresh) {
420
882
  setTimeout(() => {
@@ -431,7 +893,7 @@ if (hot) {
431
893
  infoPopup({
432
894
  title: 'Sync success',
433
895
  content: `Synced at ${new Date(payload.syncedAt).toLocaleString()}.<br />Backup filename: ${payload.backupFilename}`,
434
- className: 'pp-dev-info__popup--success',
896
+ type: 'success',
435
897
  });
436
898
  }
437
899
  });