@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.
- package/CHANGELOG.md +198 -0
- package/README.md +298 -266
- package/dist/CHANGELOG.md +198 -0
- package/dist/README.md +298 -266
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/helpers.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/migrate-r2mXdGLZ.js +2 -0
- package/dist/cjs/migrate-r2mXdGLZ.js.map +1 -0
- package/dist/cjs/package.json +16 -16
- package/dist/cjs/plugin-Cm9vRLCu.js +2 -0
- package/dist/cjs/plugin-Cm9vRLCu.js.map +1 -0
- package/dist/cjs/plugin.js +1 -1
- package/dist/cjs/version-plugin-DsC5NLoX.js +2 -0
- package/dist/cjs/{version-plugin-DUKsGBsW.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 +509 -47
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.html +71 -18
- package/dist/esm/cli.js +1 -1
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/helpers.js +1 -1
- package/dist/esm/helpers.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/migrate-BYuCtRbI.js +2 -0
- package/dist/esm/migrate-BYuCtRbI.js.map +1 -0
- package/dist/esm/package.json +16 -16
- 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-BGWVTSsN.js +2 -0
- package/dist/esm/{version-plugin-C7j2jDQX.js.map → version-plugin-BGWVTSsN.js.map} +1 -1
- package/dist/types/index.d.ts +116 -139
- package/package.json +34 -28
- package/dist/cjs/index-Bb3yIW5d.js +0 -2
- package/dist/cjs/index-Bb3yIW5d.js.map +0 -1
- package/dist/cjs/plugin-CqahMAGe.js +0 -2
- package/dist/cjs/plugin-CqahMAGe.js.map +0 -1
- package/dist/cjs/version-plugin-DUKsGBsW.js +0 -2
- package/dist/esm/index-Cd-DmbPQ.js +0 -2
- package/dist/esm/index-Cd-DmbPQ.js.map +0 -1
- package/dist/esm/plugin-BeehFkJq.js +0 -2
- package/dist/esm/plugin-BeehFkJq.js.map +0 -1
- package/dist/esm/version-plugin-C7j2jDQX.js +0 -2
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,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="
|
|
182
|
-
height="
|
|
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
|
|
204
|
-
|
|
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
|
|
207
|
-
popup.
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
708
|
+
requestAnimationFrame(scheduleDismiss);
|
|
282
709
|
}
|
|
283
710
|
});
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
896
|
+
type: 'success',
|
|
435
897
|
});
|
|
436
898
|
}
|
|
437
899
|
});
|