pi-ele-picker 0.2.1 → 0.3.0
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/README.md +1 -0
- package/client/picker.js +116 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,7 @@ The launcher starts in the **bottom-right** corner and stays out of the way:
|
|
|
95
95
|
- **Fades when idle** (opacity `0.5`) and returns to full opacity on hover.
|
|
96
96
|
- **Shows the live pi session status** as a coloured dot **when enabled** — green while pi is running, amber while follow-ups are queued, grey while awaiting input, red when the bridge is gone (see [Session status](#session-status)).
|
|
97
97
|
- The panel always opens **next to the launcher**, flipping sides and clamping to stay on screen.
|
|
98
|
+
- **The panel is draggable too** — grab its title bar (or any empty spot) to move it; the offset from the launcher is remembered per browser (`localStorage`), and the panel still follows the launcher while clamping itself to stay on screen.
|
|
98
99
|
|
|
99
100
|
1. Click **Pi Picker** and pick an element on the page.
|
|
100
101
|
2. Describe the change (paste or attach screenshots if you like).
|
package/client/picker.js
CHANGED
|
@@ -65,9 +65,11 @@
|
|
|
65
65
|
'.sw .sw-label { font-size: 11px; }',
|
|
66
66
|
'.panel { position: fixed; width: 380px; max-width: calc(100vw - 36px); z-index: 2147483647; pointer-events: auto; background: #1b1c22; color: #e7e7ea; border: 1px solid #34353d; border-radius: 12px; box-shadow: 0 18px 48px rgba(0,0,0,.5); font-size: 13px; display: none; }',
|
|
67
67
|
'.panel.open { display: block; }',
|
|
68
|
-
'.hd { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #2b2c33; }',
|
|
68
|
+
'.hd { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #2b2c33; cursor: grab; user-select: none; -webkit-user-select: none; touch-action: none; }',
|
|
69
69
|
'.hd .title { font-weight: 700; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
70
70
|
'.hd .close { background: transparent; border: none; color: #9a9aa5; font-size: 16px; cursor: pointer; line-height: 1; }',
|
|
71
|
+
'.panel.dragging { user-select: none; -webkit-user-select: none; }',
|
|
72
|
+
'.panel.dragging .hd { cursor: grabbing; }',
|
|
71
73
|
'.meta { padding: 8px 12px 0; color: #9a9aa5; font-size: 11px; line-height: 1.5; word-break: break-all; }',
|
|
72
74
|
'.meta code { color: #a5b4fc; }',
|
|
73
75
|
'textarea { display: block; width: calc(100% - 24px); margin: 8px 12px 0; min-height: 72px; max-height: 200px; resize: vertical; background: #111218; color: #e7e7ea; border: 1px solid #34353d; border-radius: 8px; padding: 8px 10px; font-size: 13px; outline: none; }',
|
|
@@ -117,7 +119,7 @@
|
|
|
117
119
|
triggerDot.className = 'dot st-off';
|
|
118
120
|
trigger.appendChild(triggerDot);
|
|
119
121
|
trigger.appendChild(document.createTextNode('Pi Picker'));
|
|
120
|
-
trigger.title = '拖拽可移动 · 点击开始选择元素';
|
|
122
|
+
trigger.title = '拖拽可移动 · 面板标题栏也可拖拽 · 点击开始选择元素';
|
|
121
123
|
root.appendChild(trigger);
|
|
122
124
|
|
|
123
125
|
var panel = document.createElement('div');
|
|
@@ -161,6 +163,29 @@
|
|
|
161
163
|
var suppressClick = false;
|
|
162
164
|
var drag = null;
|
|
163
165
|
|
|
166
|
+
/* --- panel: independently draggable, remembered as an offset from the anchor --- */
|
|
167
|
+
var PANEL_OFS_KEY = 'pi-ele-picker:panel-ofs:v1';
|
|
168
|
+
var panelOffset = { dx: 0, dy: 0 };
|
|
169
|
+
var panelDrag = null;
|
|
170
|
+
|
|
171
|
+
function loadPanelOffset() {
|
|
172
|
+
try {
|
|
173
|
+
var o = JSON.parse(localStorage.getItem(PANEL_OFS_KEY) || 'null');
|
|
174
|
+
if (o && typeof o.dx === 'number' && typeof o.dy === 'number') return { dx: o.dx, dy: o.dy };
|
|
175
|
+
} catch (e) {
|
|
176
|
+
/* storage unavailable — fall back to the anchored position */
|
|
177
|
+
}
|
|
178
|
+
return { dx: 0, dy: 0 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function savePanelOffset() {
|
|
182
|
+
try {
|
|
183
|
+
localStorage.setItem(PANEL_OFS_KEY, JSON.stringify(panelOffset));
|
|
184
|
+
} catch (e) {
|
|
185
|
+
/* storage unavailable — ignore */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
// Hide until positioned to avoid a flash at the origin.
|
|
165
190
|
trigger.style.visibility = 'hidden';
|
|
166
191
|
|
|
@@ -215,9 +240,8 @@
|
|
|
215
240
|
trigger.style.visibility = '';
|
|
216
241
|
}
|
|
217
242
|
|
|
218
|
-
/**
|
|
219
|
-
function
|
|
220
|
-
if (!panel.classList.contains('open')) return;
|
|
243
|
+
/** Where the panel sits until it is dragged: next to the launcher, flipping sides to stay in view. */
|
|
244
|
+
function panelBasePos() {
|
|
221
245
|
var gap = 10;
|
|
222
246
|
var m = 10;
|
|
223
247
|
var t = trigger.getBoundingClientRect();
|
|
@@ -227,8 +251,32 @@
|
|
|
227
251
|
var vh = window.innerHeight;
|
|
228
252
|
var top = t.top + t.height / 2 > vh / 2 ? t.top - gap - ph : t.bottom + gap;
|
|
229
253
|
var left = t.left + t.width / 2 > vw / 2 ? t.right - pw : t.left;
|
|
230
|
-
|
|
231
|
-
|
|
254
|
+
return { left: Math.max(m, Math.min(left, vw - pw - m)), top: Math.max(m, Math.min(top, vh - ph - m)) };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function clampPanelPos(left, top) {
|
|
258
|
+
var m = 10;
|
|
259
|
+
var pw = panel.offsetWidth || 380;
|
|
260
|
+
var ph = panel.offsetHeight || 320;
|
|
261
|
+
var vw = window.innerWidth;
|
|
262
|
+
var vh = window.innerHeight;
|
|
263
|
+
return {
|
|
264
|
+
left: Math.max(m, Math.min(left, vw - pw - m)),
|
|
265
|
+
top: Math.max(m, Math.min(top, vh - ph - m)),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function setPanelPos(left, top) {
|
|
270
|
+
panel.style.left = left + 'px';
|
|
271
|
+
panel.style.top = top + 'px';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Anchor the panel next to the launcher, plus whatever offset the user dragged it by. */
|
|
275
|
+
function positionPanel() {
|
|
276
|
+
if (!panel.classList.contains('open')) return;
|
|
277
|
+
var b = panelBasePos();
|
|
278
|
+
var p = clampPanelPos(b.left + panelOffset.dx, b.top + panelOffset.dy);
|
|
279
|
+
setPanelPos(p.left, p.top);
|
|
232
280
|
}
|
|
233
281
|
|
|
234
282
|
trigger.addEventListener('pointerdown', function (e) {
|
|
@@ -269,6 +317,65 @@
|
|
|
269
317
|
trigger.addEventListener('pointerup', endDrag);
|
|
270
318
|
trigger.addEventListener('pointercancel', endDrag);
|
|
271
319
|
|
|
320
|
+
/* --- panel drag: grab the title bar (or any area that is not interactive) --- */
|
|
321
|
+
/**
|
|
322
|
+
* Children that own the pointer keep their native behaviour: typing, clicking,
|
|
323
|
+
* selecting the selector text, dropping images. Everything else is a drag handle.
|
|
324
|
+
*/
|
|
325
|
+
function isInteractiveNode(node) {
|
|
326
|
+
if (!node || typeof node.closest !== 'function') return false;
|
|
327
|
+
return !!node.closest('textarea, input, button, label, select, a, img, .meta, .shot, [contenteditable="true"]');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
panel.addEventListener('pointerdown', function (e) {
|
|
331
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
332
|
+
if (isInteractiveNode(e.target)) return;
|
|
333
|
+
var r = panel.getBoundingClientRect();
|
|
334
|
+
panelDrag = {
|
|
335
|
+
id: e.pointerId,
|
|
336
|
+
sx: e.clientX,
|
|
337
|
+
sy: e.clientY,
|
|
338
|
+
ox: r.left,
|
|
339
|
+
oy: r.top,
|
|
340
|
+
base: panelBasePos(),
|
|
341
|
+
moved: false,
|
|
342
|
+
};
|
|
343
|
+
try {
|
|
344
|
+
panel.setPointerCapture(e.pointerId);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
/* ignore */
|
|
347
|
+
}
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
panel.addEventListener('pointermove', function (e) {
|
|
352
|
+
if (!panelDrag || e.pointerId !== panelDrag.id) return;
|
|
353
|
+
var dx = e.clientX - panelDrag.sx;
|
|
354
|
+
var dy = e.clientY - panelDrag.sy;
|
|
355
|
+
if (!panelDrag.moved && Math.abs(dx) + Math.abs(dy) < 4) return;
|
|
356
|
+
panelDrag.moved = true;
|
|
357
|
+
panel.classList.add('dragging');
|
|
358
|
+
var p = clampPanelPos(panelDrag.ox + dx, panelDrag.oy + dy);
|
|
359
|
+
setPanelPos(p.left, p.top);
|
|
360
|
+
// Store the drag as an offset from the anchor so the panel keeps following the launcher.
|
|
361
|
+
panelOffset = { dx: p.left - panelDrag.base.left, dy: p.top - panelDrag.base.top };
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
function endPanelDrag(e) {
|
|
365
|
+
if (!panelDrag || e.pointerId !== panelDrag.id) return;
|
|
366
|
+
var moved = panelDrag.moved;
|
|
367
|
+
panelDrag = null;
|
|
368
|
+
panel.classList.remove('dragging');
|
|
369
|
+
try {
|
|
370
|
+
panel.releasePointerCapture(e.pointerId);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
/* ignore */
|
|
373
|
+
}
|
|
374
|
+
if (moved) savePanelOffset();
|
|
375
|
+
}
|
|
376
|
+
panel.addEventListener('pointerup', endPanelDrag);
|
|
377
|
+
panel.addEventListener('pointercancel', endPanelDrag);
|
|
378
|
+
|
|
272
379
|
window.addEventListener('resize', function () {
|
|
273
380
|
var p = launcherPos();
|
|
274
381
|
setLauncherPos(p.x, p.y);
|
|
@@ -276,6 +383,7 @@
|
|
|
276
383
|
});
|
|
277
384
|
|
|
278
385
|
initLauncherPos();
|
|
386
|
+
panelOffset = loadPanelOffset();
|
|
279
387
|
|
|
280
388
|
function setStatus(msg, kind) {
|
|
281
389
|
statusEl.textContent = msg || '';
|
|
@@ -694,7 +802,7 @@
|
|
|
694
802
|
if (info && info.sessionId) detail.push('会话 ' + String(info.sessionId).slice(0, 8));
|
|
695
803
|
var line = STATUS_LABEL[st] + (detail.length ? ' · ' + detail.join(' · ') : '');
|
|
696
804
|
sessionText.textContent = line;
|
|
697
|
-
var title = line + ' · 拖拽可移动 · 点击开始选择元素';
|
|
805
|
+
var title = line + ' · 拖拽可移动 · 面板标题栏也可拖拽 · 点击开始选择元素';
|
|
698
806
|
if (title !== statusTitle) {
|
|
699
807
|
trigger.title = title;
|
|
700
808
|
statusTitle = title;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ele-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pick an element in your running page, describe the change, and send it to a pi session. Ships a Vite dev plugin (browser picker + discovery endpoint) and the matching pi bridge extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|