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 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
- /** Place the panel next to the launcher, flipping sides to stay in view. */
219
- function positionPanel() {
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
- panel.style.left = Math.max(m, Math.min(left, vw - pw - m)) + 'px';
231
- panel.style.top = Math.max(m, Math.min(top, vh - ph - m)) + 'px';
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.2.1",
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",