@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.
- package/CHANGELOG.md +50 -0
- package/README.md +60 -0
- package/dist/CHANGELOG.md +50 -0
- package/dist/README.md +60 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/index-DeUfAH6g.js +2 -0
- package/dist/cjs/index-DeUfAH6g.js.map +1 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/{plugin-34c8_AjI.js → plugin-Cm9vRLCu.js} +2 -2
- package/dist/cjs/plugin-Cm9vRLCu.js.map +1 -0
- package/dist/cjs/plugin.js +1 -1
- package/dist/cjs/{version-plugin-BOFuJ-Rp.js → version-plugin-DsC5NLoX.js} +2 -2
- package/dist/cjs/{version-plugin-BOFuJ-Rp.js.map → version-plugin-DsC5NLoX.js.map} +1 -1
- package/dist/client/client.css +1 -1
- package/dist/client/client.css.map +1 -1
- package/dist/client/client.js +481 -30
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.html +38 -2
- package/dist/esm/cli.js +1 -1
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index-DOjGFYJ7.js +2 -0
- package/dist/esm/index-DOjGFYJ7.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/package.json +2 -2
- package/dist/esm/plugin-B2ocw-wb.js +2 -0
- package/dist/esm/plugin-B2ocw-wb.js.map +1 -0
- package/dist/esm/plugin.js +1 -1
- package/dist/esm/{version-plugin-vu8VWkws.js → version-plugin-BGWVTSsN.js} +2 -2
- package/dist/esm/{version-plugin-vu8VWkws.js.map → version-plugin-BGWVTSsN.js.map} +1 -1
- package/dist/types/index.d.ts +14 -1
- package/package.json +6 -6
- package/dist/cjs/index-MzGAxNTB.js +0 -2
- package/dist/cjs/index-MzGAxNTB.js.map +0 -1
- package/dist/cjs/plugin-34c8_AjI.js.map +0 -1
- package/dist/esm/index-C7dYPskY.js +0 -2
- package/dist/esm/index-C7dYPskY.js.map +0 -1
- package/dist/esm/plugin-Dvva-sxE.js +0 -2
- package/dist/esm/plugin-Dvva-sxE.js.map +0 -1
package/dist/client/client.js
CHANGED
|
@@ -130,7 +130,14 @@ function createPPDevHotContext() {
|
|
|
130
130
|
return context;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
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
|
|
213
|
-
|
|
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
|
|
632
|
+
const offset = POPUP_OFFSET + index * (POPUP_HEIGHT + POPUP_OFFSET);
|
|
216
633
|
const $popupContent = popup.querySelector('.pp-dev-info__popup');
|
|
217
|
-
if (
|
|
218
|
-
|
|
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) => {
|