cmdzero 0.5.0 → 0.6.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/overlay/overlay.js +264 -136
- package/package.json +1 -1
package/overlay/overlay.js
CHANGED
|
@@ -34,13 +34,22 @@
|
|
|
34
34
|
.cz-dragghost{position:fixed;pointer-events:none;z-index:2147483350;opacity:.5;outline:2px dashed #6366f1;border-radius:4px;background:rgba(99,102,241,.12)}
|
|
35
35
|
.cz-delete-btn:hover{background:#ef4444}
|
|
36
36
|
.cz-delete-btn svg{width:12px;height:12px;pointer-events:none}
|
|
37
|
-
.cz-pop{position:fixed;background:#111827;color:#f9fafb;border:1.5px solid #10b981;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.35);padding:8px;pointer-events:auto;display:flex;flex-direction:column;gap:6px;min-width:
|
|
37
|
+
.cz-pop{position:fixed;background:#111827;color:#f9fafb;border:1.5px solid #10b981;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.35);padding:8px;pointer-events:auto;display:flex;flex-direction:column;gap:6px;min-width:340px;max-width:400px;font-size:12px}
|
|
38
|
+
.cz-pop textarea{flex:1;width:100%;min-height:66px;resize:vertical;background:#1f2937;border:1px solid #374151;border-radius:6px;color:#f9fafb;padding:7px 9px;font-size:12.5px;line-height:1.45;outline:none;font-family:inherit}
|
|
39
|
+
.cz-pop textarea:focus{border-color:#6366f1}
|
|
38
40
|
.cz-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
|
39
41
|
.cz-pop button{background:#374151;color:#f9fafb;border:none;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer}
|
|
40
42
|
.cz-pop button:hover{background:#4b5563}
|
|
41
43
|
.cz-pop button.cz-primary{background:#6366f1}
|
|
42
44
|
.cz-pop input{flex:1;background:#1f2937;border:1px solid #374151;border-radius:6px;color:#f9fafb;padding:5px 8px;font-size:12px;outline:none}
|
|
43
45
|
.cz-pop select{background:#1f2937;border:1px solid #374151;border-radius:6px;color:#f9fafb;padding:3px 4px;font-size:11.5px;outline:none}
|
|
46
|
+
.cz-panel{position:fixed;left:12px;top:42px;width:238px;max-height:calc(100vh - 58px);overflow-y:auto;background:#111827;color:#f9fafb;border:1.5px solid #10b981;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.4);padding:9px;pointer-events:auto;display:flex;flex-direction:column;gap:6px;font-size:12px;z-index:2147483100}
|
|
47
|
+
.cz-panel .cz-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
|
48
|
+
.cz-panel button{background:#374151;color:#f9fafb;border:none;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer}
|
|
49
|
+
.cz-panel button:hover{background:#4b5563}
|
|
50
|
+
.cz-panel select{background:#1f2937;border:1px solid #374151;border-radius:6px;color:#f9fafb;padding:3px 4px;font-size:11.5px;outline:none}
|
|
51
|
+
.cz-ptitle{font-family:ui-monospace,monospace;font-size:11px;color:#6ee7b7;padding-bottom:5px;border-bottom:1px solid #1f2937}
|
|
52
|
+
.cz-sec{color:#94a3b8;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;margin:5px 0 1px}
|
|
44
53
|
.cz-label{color:#9ca3af;min-width:50px}
|
|
45
54
|
.cz-note{color:#fbbf24;font-size:11px;line-height:1.35;background:rgba(245,158,11,.1);border-radius:6px;padding:4px 7px}
|
|
46
55
|
.cz-note.cz-info{color:#93c5fd;background:rgba(59,130,246,.12)}
|
|
@@ -49,16 +58,24 @@
|
|
|
49
58
|
.cz-swatch{width:18px;height:18px;border-radius:4px;border:1px solid rgba(255,255,255,.25);cursor:pointer;padding:0}
|
|
50
59
|
.cz-swatch:hover{transform:scale(1.15)}
|
|
51
60
|
.cz-chip{font-size:10.5px !important;padding:2px 6px !important}
|
|
52
|
-
.cz-tray{position:fixed;right:14px;bottom:14px;display:flex;flex-direction:column;align-items:flex-end;gap:6px;pointer-events:auto}
|
|
61
|
+
.cz-tray{position:fixed;right:14px;bottom:14px;display:flex;flex-direction:column;align-items:flex-end;gap:6px;pointer-events:auto;transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .22s ease}
|
|
62
|
+
.cz-tray.cz-dodged{transform:translateX(calc(100% + 28px));opacity:0;pointer-events:none}
|
|
63
|
+
.cz-banner{position:fixed;top:0;left:0;right:0;height:30px;display:flex;align-items:center;justify-content:space-between;gap:14px;padding:0 14px;background:rgba(9,13,20,.94);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border-bottom:1px solid #1f2937;color:#cbd5e1;font-size:12px;line-height:1;pointer-events:auto;z-index:2147483200;box-sizing:border-box}
|
|
64
|
+
.cz-banner .cz-brand{display:flex;align-items:center;gap:6px;font-weight:600;color:#e2e8f0;flex:none}
|
|
65
|
+
.cz-banner .cz-brand kbd{background:#10b981;color:#052e1b;border-radius:5px;padding:2px 6px;font-size:10.5px;font-family:ui-monospace,monospace;font-weight:700}
|
|
66
|
+
.cz-banner .cz-stats{display:flex;gap:18px;align-items:center;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;flex:1;justify-content:center}
|
|
67
|
+
.cz-banner .cz-stat span{color:#64748b}
|
|
68
|
+
.cz-banner .cz-stat b{color:#f1f5f9;font-weight:600;margin-left:5px}
|
|
69
|
+
.cz-banner .cz-stat.cz-accent b{color:#6ee7b7}
|
|
70
|
+
.cz-banner a{color:#6ee7b7;text-decoration:none;flex:none}
|
|
71
|
+
.cz-banner a:hover{text-decoration:underline}
|
|
72
|
+
.cz-banner .cz-idle{color:#64748b}
|
|
53
73
|
.cz-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
|
54
74
|
.cz-wrap.cz-expanded{max-height:calc(100vh - 90px);overflow-y:auto;overflow-x:hidden;padding:2px}
|
|
55
75
|
.cz-wrap:not(.cz-expanded) > .cz-tweak:nth-child(n+6){display:none}
|
|
56
76
|
.cz-fade{height:16px;width:160px;background:linear-gradient(to bottom,rgba(17,24,39,0),rgba(17,24,39,.55));pointer-events:none;margin-top:-10px}
|
|
57
77
|
.cz-history{background:#0b1220;color:#93c5fd;border:1px solid #263041;border-radius:8px;padding:4px 10px;font-size:11.5px;cursor:pointer;pointer-events:auto;box-shadow:0 4px 14px rgba(0,0,0,.3)}
|
|
58
78
|
.cz-history:hover{border-color:#6366f1}
|
|
59
|
-
.cz-total{background:#064e3b;color:#a7f3d0;border-radius:8px;padding:6px 11px;font-size:13px;box-shadow:0 4px 14px rgba(0,0,0,.3);white-space:nowrap;width:max-content;max-width:720px}
|
|
60
|
-
.cz-total a{color:#6ee7b7;margin-left:10px;text-decoration:underline;cursor:pointer}
|
|
61
|
-
.cz-total a:hover{color:#a7f3d0}
|
|
62
79
|
.cz-tweak{background:#111827;color:#e5e7eb;border-radius:8px;padding:7px 11px;font-size:13px;display:flex;gap:8px;align-items:center;box-shadow:0 4px 14px rgba(0,0,0,.3);white-space:nowrap;width:max-content;max-width:720px;overflow:hidden;text-overflow:ellipsis}
|
|
63
80
|
.cz-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
64
81
|
.cz-dot.done{background:#10b981}.cz-dot.queued,.cz-dot.running{background:#f59e0b;animation:cz-pulse 1s infinite}.cz-dot.error{background:#ef4444}.cz-dot.reverted,.cz-dot.cancelled{background:#6b7280}
|
|
@@ -213,6 +230,11 @@
|
|
|
213
230
|
const pop = el('div', 'cz-pop');
|
|
214
231
|
pop.style.display = 'none';
|
|
215
232
|
root.appendChild(pop);
|
|
233
|
+
// Left-side style inspector — every style attribute broken out into its own
|
|
234
|
+
// control, populated by renderPanel() when an element is selected.
|
|
235
|
+
const panel = el('div', 'cz-panel');
|
|
236
|
+
panel.style.display = 'none';
|
|
237
|
+
root.appendChild(panel);
|
|
216
238
|
const popLabel = el('div', 'cz-pop-label');
|
|
217
239
|
popLabel.style.display = 'none';
|
|
218
240
|
root.appendChild(popLabel);
|
|
@@ -305,6 +327,7 @@
|
|
|
305
327
|
const dropLine = el('div', 'cz-drop');
|
|
306
328
|
dropLine.style.display = 'none';
|
|
307
329
|
root.appendChild(dropLine);
|
|
330
|
+
let suppressClick = false; // swallow the click the browser fires right after a drag
|
|
308
331
|
|
|
309
332
|
grip.addEventListener('mousedown', (e) => {
|
|
310
333
|
e.preventDefault();
|
|
@@ -313,20 +336,36 @@
|
|
|
313
336
|
if (!s || !document.contains(s.el)) return;
|
|
314
337
|
const axis = siblingAxis(s.el);
|
|
315
338
|
const { peers } = dragPeers(s.el, s.loc);
|
|
316
|
-
if (peers.length < 2)
|
|
339
|
+
if (peers.length < 2) {
|
|
340
|
+
addTweak({ id: 'x' + Date.now(), status: 'error', label: 'nothing to reorder here — select a card in a row, or the section container' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
317
343
|
const from = peers.indexOf(s.el);
|
|
318
|
-
|
|
344
|
+
if (from < 0) return;
|
|
345
|
+
const startX = e.clientX, startY = e.clientY;
|
|
346
|
+
let engaged = false, insertIndex = from;
|
|
347
|
+
|
|
348
|
+
const engage = () => {
|
|
349
|
+
engaged = true;
|
|
350
|
+
document.body.style.cursor = 'grabbing';
|
|
351
|
+
grip.style.cursor = 'grabbing';
|
|
352
|
+
s.el.style.opacity = '0.45'; // show what's moving
|
|
353
|
+
// hide the panels/popover so they don't block the drop targets
|
|
354
|
+
pop.style.display = 'none'; panel.style.display = 'none'; popLabel.style.display = 'none';
|
|
355
|
+
};
|
|
319
356
|
|
|
320
357
|
const onMove = (ev) => {
|
|
358
|
+
if (!engaged) {
|
|
359
|
+
if (Math.abs(ev.clientX - startX) < 4 && Math.abs(ev.clientY - startY) < 4) return; // threshold
|
|
360
|
+
engage();
|
|
361
|
+
}
|
|
321
362
|
const pos = axis === 'horizontal' ? ev.clientX : ev.clientY;
|
|
322
|
-
// find insertion slot: before the first peer whose midpoint is past pointer
|
|
323
363
|
insertIndex = peers.length;
|
|
324
364
|
for (let i = 0; i < peers.length; i++) {
|
|
325
365
|
const rr = peers[i].getBoundingClientRect();
|
|
326
366
|
const mid = axis === 'horizontal' ? rr.left + rr.width / 2 : rr.top + rr.height / 2;
|
|
327
367
|
if (pos < mid) { insertIndex = i; break; }
|
|
328
368
|
}
|
|
329
|
-
// draw the drop indicator at that gap
|
|
330
369
|
const ref = peers[Math.min(insertIndex, peers.length - 1)].getBoundingClientRect();
|
|
331
370
|
dropLine.style.display = 'block';
|
|
332
371
|
if (axis === 'horizontal') {
|
|
@@ -341,11 +380,20 @@
|
|
|
341
380
|
removeEventListener('mousemove', onMove, true);
|
|
342
381
|
removeEventListener('mouseup', onUp, true);
|
|
343
382
|
dropLine.style.display = 'none';
|
|
383
|
+
document.body.style.cursor = '';
|
|
384
|
+
grip.style.cursor = 'grab';
|
|
385
|
+
if (document.contains(s.el)) s.el.style.opacity = '';
|
|
386
|
+
if (!engaged) return; // was a click, not a drag
|
|
387
|
+
suppressClick = true; // swallow the synthetic click that follows a drag
|
|
388
|
+
setTimeout(() => { suppressClick = false; }, 0);
|
|
389
|
+
const restore = () => { if (state.selected === s && document.contains(s.el)) { renderPopover(); renderPanel(); reposition(); } };
|
|
344
390
|
const finalIndex = insertIndex > from ? insertIndex - 1 : insertIndex;
|
|
345
|
-
if (finalIndex === from || finalIndex < 0) return;
|
|
391
|
+
if (finalIndex === from || finalIndex < 0) { restore(); return; } // dropped where it started
|
|
346
392
|
try {
|
|
347
393
|
await api('move', { loc: s.loc, index: instanceIndex(s.el, s.loc), toIndex: finalIndex });
|
|
394
|
+
restore(); // reorder shows via auto-reload; restore panels if it doesn't
|
|
348
395
|
} catch (err) {
|
|
396
|
+
restore();
|
|
349
397
|
addTweak({ id: 'x' + Date.now(), status: 'error', label: `move: ${err.message.slice(0, 140)}` });
|
|
350
398
|
}
|
|
351
399
|
};
|
|
@@ -354,7 +402,7 @@
|
|
|
354
402
|
});
|
|
355
403
|
|
|
356
404
|
function select(target) {
|
|
357
|
-
finishTextEdit(
|
|
405
|
+
finishTextEdit(true); // moving to another element saves the current text edit
|
|
358
406
|
const loc = target.getAttribute('data-cz');
|
|
359
407
|
// How many rendered elements map to this same source line — i.e. how many
|
|
360
408
|
// instances a shared-source edit (style/functionality) will change.
|
|
@@ -362,6 +410,7 @@
|
|
|
362
410
|
state.selected = { el: target, loc, meta: null, instances };
|
|
363
411
|
selBox.style.display = 'block';
|
|
364
412
|
renderPopover();
|
|
413
|
+
renderPanel();
|
|
365
414
|
reposition();
|
|
366
415
|
loadMeta();
|
|
367
416
|
}
|
|
@@ -377,15 +426,20 @@
|
|
|
377
426
|
if (state.selected !== s) return; // selection changed meanwhile
|
|
378
427
|
s.meta = meta;
|
|
379
428
|
renderPopover();
|
|
429
|
+
renderPanel();
|
|
380
430
|
reposition();
|
|
431
|
+
// Text editing is active the moment an editable element is highlighted —
|
|
432
|
+
// no button click needed. Caret goes to the end so it's non-destructive.
|
|
433
|
+
if (canEditWholeText(s) && !state.editing) startTextEdit(false);
|
|
381
434
|
} catch { /* element not resolvable — leave popover as is */ }
|
|
382
435
|
}
|
|
383
436
|
|
|
384
437
|
function deselect() {
|
|
385
|
-
finishTextEdit(
|
|
438
|
+
finishTextEdit(true); // exiting/⌘0 saves the current text edit
|
|
386
439
|
state.selected = null;
|
|
387
440
|
selBox.style.display = 'none';
|
|
388
441
|
pop.style.display = 'none';
|
|
442
|
+
panel.style.display = 'none';
|
|
389
443
|
popLabel.style.display = 'none';
|
|
390
444
|
deleteBtn.style.display = 'none';
|
|
391
445
|
moveBar.style.display = 'none';
|
|
@@ -494,8 +548,12 @@
|
|
|
494
548
|
const r = positionBox(selBox, s.el);
|
|
495
549
|
pop.style.display = 'flex';
|
|
496
550
|
const labelH = 22; // room for the file:line tab above the panel
|
|
497
|
-
|
|
498
|
-
|
|
551
|
+
// Keep a generous gap so the panel never sits on top of the text you're
|
|
552
|
+
// editing — below the element by default, above only if there's no room.
|
|
553
|
+
const GAP = 26;
|
|
554
|
+
const below = r.bottom + GAP + labelH;
|
|
555
|
+
const top = below + pop.offsetHeight > innerHeight ? r.top - pop.offsetHeight - GAP : below;
|
|
556
|
+
const left = Math.min(Math.max(r.left, 8), innerWidth - Math.max(pop.offsetWidth + 16, 356));
|
|
499
557
|
Object.assign(pop.style, { left: left + 'px', top: Math.max(top, 8 + labelH) + 'px' });
|
|
500
558
|
popLabel.style.display = 'block';
|
|
501
559
|
popLabel.textContent = s.instances > 1 ? `${shortLoc(s.loc)} · ${s.instances}×` : shortLoc(s.loc);
|
|
@@ -529,7 +587,7 @@
|
|
|
529
587
|
} catch (e) {
|
|
530
588
|
addTweak({ id: 'x' + Date.now(), status: 'error', label: `${label}: ${e.message}` });
|
|
531
589
|
}
|
|
532
|
-
setTimeout(() => { reposition(); renderPopover(); }, 350); // after HMR
|
|
590
|
+
setTimeout(() => { reposition(); renderPopover(); renderPanel(); }, 350); // after HMR
|
|
533
591
|
}
|
|
534
592
|
|
|
535
593
|
// ---------- popover ----------
|
|
@@ -613,7 +671,7 @@
|
|
|
613
671
|
} catch (e) {
|
|
614
672
|
addTweak({ id: 'x' + Date.now(), status: 'error', label: `${label}: ${e.message}` });
|
|
615
673
|
}
|
|
616
|
-
setTimeout(() => { reposition(); renderPopover(); }, 350);
|
|
674
|
+
setTimeout(() => { reposition(); renderPopover(); renderPanel(); }, 350);
|
|
617
675
|
}
|
|
618
676
|
|
|
619
677
|
function pxStep(current, scale, dir) {
|
|
@@ -726,6 +784,80 @@
|
|
|
726
784
|
return row;
|
|
727
785
|
}
|
|
728
786
|
|
|
787
|
+
// Build the inline swatches/chips control for one style property (used by the
|
|
788
|
+
// left panel — every attribute broken out, no dropdown).
|
|
789
|
+
function buildPropSwatches(name, tw) {
|
|
790
|
+
const s = state.selected;
|
|
791
|
+
const wrap = el('div', 'cz-swatches');
|
|
792
|
+
const p = (tw ? PROPS : STYLE_PROPS)[name];
|
|
793
|
+
if (!p) return wrap;
|
|
794
|
+
if (p.type === 'color') {
|
|
795
|
+
if (tw) {
|
|
796
|
+
const { colors, fromDS } = readTheme();
|
|
797
|
+
if (!fromDS) { wrap.append(el('span', 'cz-meta', 'no design-system colors in CSS')); return wrap; }
|
|
798
|
+
for (const c of colors) {
|
|
799
|
+
const b = el('button', 'cz-swatch');
|
|
800
|
+
b.style.background = c.value;
|
|
801
|
+
b.title = `${p.prefix}-${c.name}`;
|
|
802
|
+
b.onclick = () => applyClassTweak(propChange(s.el, name, `${p.prefix}-${c.name}`), name);
|
|
803
|
+
wrap.appendChild(b);
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
const { varColors } = readTheme();
|
|
807
|
+
const palette = varColors.length ? varColors.slice(0, 48) : harvestPageColors();
|
|
808
|
+
for (const c of palette) {
|
|
809
|
+
const b = el('button', 'cz-swatch');
|
|
810
|
+
b.style.background = c.value;
|
|
811
|
+
b.title = c.name;
|
|
812
|
+
b.onclick = () => applyStyleProp(name, c.apply);
|
|
813
|
+
wrap.appendChild(b);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
p.options.forEach((opt, i) => {
|
|
818
|
+
const b = el('button', 'cz-chip', p.labels ? p.labels[i] : opt);
|
|
819
|
+
b.onclick = () => tw ? applyClassTweak(propChange(s.el, name, opt), name) : applyStyleProp(name, opt);
|
|
820
|
+
wrap.appendChild(b);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
return wrap;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Left inspector panel: every style attribute broken out into its own control.
|
|
827
|
+
function renderPanel() {
|
|
828
|
+
const s = state.selected;
|
|
829
|
+
if (!s) { panel.style.display = 'none'; return; }
|
|
830
|
+
panel.style.display = 'flex';
|
|
831
|
+
panel.textContent = '';
|
|
832
|
+
panel.appendChild(el('div', 'cz-ptitle', `<${s.el.tagName.toLowerCase()}> ${shortLoc(s.loc)}${s.instances > 1 ? ` · ${s.instances}×` : ''}`));
|
|
833
|
+
const tw = state.tailwind;
|
|
834
|
+
|
|
835
|
+
panel.appendChild(el('div', 'cz-sec', 'Spacing'));
|
|
836
|
+
if (tw) { panel.appendChild(spacingRow('Padding', 'p')); panel.appendChild(spacingRow('Margin', 'm')); }
|
|
837
|
+
else { panel.appendChild(spacingRowPx('Padding', 'padding')); panel.appendChild(spacingRowPx('Margin', 'margin')); }
|
|
838
|
+
|
|
839
|
+
panel.appendChild(el('div', 'cz-sec', 'Typography'));
|
|
840
|
+
if (tw) {
|
|
841
|
+
const fontRow = el('div', 'cz-row');
|
|
842
|
+
fontRow.append(el('span', 'cz-label', 'Font'));
|
|
843
|
+
const fMinus = el('button', null, 'A−');
|
|
844
|
+
const fPlus = el('button', null, 'A+');
|
|
845
|
+
fMinus.onclick = () => applyClassTweak(fontStep(s.el, -1), 'font');
|
|
846
|
+
fPlus.onclick = () => applyClassTweak(fontStep(s.el, +1), 'font');
|
|
847
|
+
const curFont = classList(s.el).find((c) => readTheme().textSizes.includes(c));
|
|
848
|
+
fontRow.append(fMinus, fPlus, el('span', 'cz-cur', curFont || 'inherited'));
|
|
849
|
+
panel.appendChild(fontRow);
|
|
850
|
+
} else {
|
|
851
|
+
panel.appendChild(fontRowPx());
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
panel.appendChild(el('div', 'cz-sec', 'Color & border'));
|
|
855
|
+
for (const name of Object.keys(tw ? PROPS : STYLE_PROPS)) {
|
|
856
|
+
panel.appendChild(el('span', 'cz-label', name));
|
|
857
|
+
panel.appendChild(buildPropSwatches(name, tw));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
729
861
|
function renderPopover() {
|
|
730
862
|
const s = state.selected;
|
|
731
863
|
if (!s) return;
|
|
@@ -758,12 +890,7 @@
|
|
|
758
890
|
const wholeText =
|
|
759
891
|
literals.length === 1 && literals[0].value.trim() === s.el.textContent.trim();
|
|
760
892
|
if (wholeText) {
|
|
761
|
-
|
|
762
|
-
row.append(el('span', 'cz-label', 'Copy'));
|
|
763
|
-
const b = el('button', null, '✎ Edit text in place');
|
|
764
|
-
b.onclick = () => startTextEdit();
|
|
765
|
-
row.appendChild(b);
|
|
766
|
-
pop.appendChild(row);
|
|
893
|
+
pop.appendChild(el('div', 'cz-note cz-info', '✎ Editing text inline — just type. Saves on ⌘0 or when you move to another element; Esc cancels.'));
|
|
767
894
|
} else {
|
|
768
895
|
// DOM is transformed (or several literals) → edit the source text here
|
|
769
896
|
for (const t of literals) {
|
|
@@ -790,91 +917,13 @@
|
|
|
790
917
|
}
|
|
791
918
|
}
|
|
792
919
|
|
|
793
|
-
//
|
|
794
|
-
// else gets inline-style edits — same zero-token lane, any styling system.
|
|
795
|
-
const tw = state.tailwind;
|
|
796
|
-
|
|
797
|
-
if (tw) {
|
|
798
|
-
pop.appendChild(spacingRow('Padding', 'p'));
|
|
799
|
-
pop.appendChild(spacingRow('Margin', 'm'));
|
|
800
|
-
const fontRow = el('div', 'cz-row');
|
|
801
|
-
fontRow.append(el('span', 'cz-label', 'Font'));
|
|
802
|
-
const fMinus = el('button', null, 'A−');
|
|
803
|
-
const fPlus = el('button', null, 'A+');
|
|
804
|
-
fMinus.onclick = () => applyClassTweak(fontStep(s.el, -1), 'font');
|
|
805
|
-
fPlus.onclick = () => applyClassTweak(fontStep(s.el, +1), 'font');
|
|
806
|
-
fontRow.append(fMinus, fPlus);
|
|
807
|
-
const curFont = classList(s.el).find((c) => readTheme().textSizes.includes(c));
|
|
808
|
-
fontRow.append(el('span', 'cz-cur', curFont || 'inherited'));
|
|
809
|
-
pop.appendChild(fontRow);
|
|
810
|
-
} else {
|
|
811
|
-
pop.appendChild(spacingRowPx('Padding', 'padding'));
|
|
812
|
-
pop.appendChild(spacingRowPx('Margin', 'margin'));
|
|
813
|
-
pop.appendChild(fontRowPx());
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// property editor
|
|
817
|
-
const propRow = el('div', 'cz-row');
|
|
818
|
-
propRow.append(el('span', 'cz-label', 'Style'));
|
|
819
|
-
const propSel = el('select');
|
|
820
|
-
propSel.appendChild(el('option', null, 'Choose property…'));
|
|
821
|
-
for (const name of Object.keys(tw ? PROPS : STYLE_PROPS)) {
|
|
822
|
-
const o = el('option', null, name);
|
|
823
|
-
o.value = name;
|
|
824
|
-
propSel.appendChild(o);
|
|
825
|
-
}
|
|
826
|
-
propRow.appendChild(propSel);
|
|
827
|
-
pop.appendChild(propRow);
|
|
828
|
-
const swatches = el('div', 'cz-swatches');
|
|
829
|
-
pop.appendChild(swatches);
|
|
830
|
-
propSel.onchange = () => {
|
|
831
|
-
swatches.textContent = '';
|
|
832
|
-
const p = (tw ? PROPS : STYLE_PROPS)[propSel.value];
|
|
833
|
-
if (!p) return;
|
|
834
|
-
if (p.type === 'color') {
|
|
835
|
-
if (tw) {
|
|
836
|
-
const { colors, fromDS } = readTheme();
|
|
837
|
-
if (!fromDS) {
|
|
838
|
-
swatches.append(el('span', 'cz-meta', 'no design-system colors found in page CSS'));
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
for (const c of colors) {
|
|
842
|
-
const b = el('button', 'cz-swatch');
|
|
843
|
-
b.style.background = c.value;
|
|
844
|
-
b.title = `${p.prefix}-${c.name}`;
|
|
845
|
-
b.onclick = () => applyClassTweak(propChange(s.el, propSel.value, `${p.prefix}-${c.name}`), propSel.value);
|
|
846
|
-
swatches.appendChild(b);
|
|
847
|
-
}
|
|
848
|
-
} else {
|
|
849
|
-
// design tokens if the app defines color custom properties,
|
|
850
|
-
// otherwise the palette actually rendered on the page
|
|
851
|
-
const { varColors } = readTheme();
|
|
852
|
-
const palette = varColors.length ? varColors.slice(0, 48) : harvestPageColors();
|
|
853
|
-
for (const c of palette) {
|
|
854
|
-
const b = el('button', 'cz-swatch');
|
|
855
|
-
b.style.background = c.value;
|
|
856
|
-
b.title = c.name;
|
|
857
|
-
b.onclick = () => applyStyleProp(propSel.value, c.apply);
|
|
858
|
-
swatches.appendChild(b);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
} else {
|
|
862
|
-
p.options.forEach((opt, i) => {
|
|
863
|
-
const b = el('button', 'cz-chip', p.labels ? p.labels[i] : opt);
|
|
864
|
-
b.onclick = () =>
|
|
865
|
-
tw
|
|
866
|
-
? applyClassTweak(propChange(s.el, propSel.value, opt), propSel.value)
|
|
867
|
-
: applyStyleProp(propSel.value, opt);
|
|
868
|
-
swatches.appendChild(b);
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
reposition();
|
|
872
|
-
};
|
|
920
|
+
// Style controls now live in the left inspector panel (renderPanel).
|
|
873
921
|
|
|
874
|
-
const
|
|
875
|
-
const input = el('
|
|
876
|
-
input.placeholder = 'Describe a change…';
|
|
877
|
-
|
|
922
|
+
const nlWrap = el('div', 'cz-nlwrap');
|
|
923
|
+
const input = el('textarea');
|
|
924
|
+
input.placeholder = 'Describe a change… (⌘↵ to send)';
|
|
925
|
+
input.rows = 3;
|
|
926
|
+
const go = el('button', 'cz-primary', 'Go →');
|
|
878
927
|
const send = async () => {
|
|
879
928
|
const instruction = input.value.trim();
|
|
880
929
|
if (!instruction) return;
|
|
@@ -888,9 +937,13 @@
|
|
|
888
937
|
}
|
|
889
938
|
};
|
|
890
939
|
go.onclick = send;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
940
|
+
// ⌘↵ / Ctrl↵ sends; plain Enter inserts a newline (multi-line prompts).
|
|
941
|
+
input.onkeydown = (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); send(); } e.stopPropagation(); };
|
|
942
|
+
const goRow = el('div', 'cz-row');
|
|
943
|
+
goRow.style.justifyContent = 'flex-end';
|
|
944
|
+
goRow.appendChild(go);
|
|
945
|
+
nlWrap.append(input, goRow);
|
|
946
|
+
pop.appendChild(nlWrap);
|
|
894
947
|
|
|
895
948
|
// Model picker: Auto (router) or a forced tier.
|
|
896
949
|
const modelRow = el('div', 'cz-row');
|
|
@@ -913,11 +966,21 @@
|
|
|
913
966
|
// descriptions, buttons, links — even when animations have split the DOM
|
|
914
967
|
// into spans: we swap in the source text for editing and keep the original
|
|
915
968
|
// DOM aside so Esc restores it untouched.
|
|
916
|
-
|
|
969
|
+
// Whether the selected element's whole visible text is a single source
|
|
970
|
+
// literal we can edit in place (headings, buttons, links, paragraphs — not
|
|
971
|
+
// containers, and not text shared across multiple instances).
|
|
972
|
+
function canEditWholeText(s) {
|
|
973
|
+
const lits = s?.meta?.texts;
|
|
974
|
+
if (!lits || lits.length !== 1 || s.instances > 1) return false;
|
|
975
|
+
return document.contains(s.el) && lits[0].value.trim() === s.el.textContent.trim();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function startTextEdit(selectAll = true) {
|
|
917
979
|
const s = state.selected;
|
|
918
980
|
// the popover can outlive its selection (HMR, reloads) — never throw
|
|
919
981
|
const literal = s?.meta?.texts?.[0]?.value;
|
|
920
982
|
if (literal == null || !document.contains(s.el)) return;
|
|
983
|
+
if (state.editing && state.editing.el === s.el) return; // already editing this one
|
|
921
984
|
const frag = document.createDocumentFragment();
|
|
922
985
|
while (s.el.firstChild) frag.appendChild(s.el.firstChild);
|
|
923
986
|
state.editing = {
|
|
@@ -930,9 +993,11 @@
|
|
|
930
993
|
s.el.textContent = literal;
|
|
931
994
|
s.el.style.userSelect = 'text'; // buttons often have user-select: none
|
|
932
995
|
try { s.el.contentEditable = 'plaintext-only'; } catch { s.el.contentEditable = 'true'; }
|
|
933
|
-
s.el.focus();
|
|
934
|
-
document.getSelection()
|
|
935
|
-
|
|
996
|
+
s.el.focus({ preventScroll: true });
|
|
997
|
+
const sel = document.getSelection();
|
|
998
|
+
if (selectAll) sel?.selectAllChildren(s.el);
|
|
999
|
+
else { const rng = document.createRange(); rng.selectNodeContents(s.el); rng.collapse(false); sel?.removeAllRanges(); sel?.addRange(rng); }
|
|
1000
|
+
hint.textContent = 'editing text — type to change · ⌘0 or click another element saves · Esc cancels';
|
|
936
1001
|
}
|
|
937
1002
|
|
|
938
1003
|
async function finishTextEdit(commit) {
|
|
@@ -961,20 +1026,28 @@
|
|
|
961
1026
|
setTimeout(() => { reposition(); loadMeta(); }, 350);
|
|
962
1027
|
}
|
|
963
1028
|
|
|
964
|
-
// ----------
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const
|
|
970
|
-
const reportLink = el('a', null, 'monthly report →');
|
|
1029
|
+
// ---------- stats banner (permanent, above the site) ----------
|
|
1030
|
+
const banner = el('div', 'cz-banner');
|
|
1031
|
+
const brand = el('div', 'cz-brand');
|
|
1032
|
+
brand.innerHTML = '<kbd>⌘0</kbd> CmdZero';
|
|
1033
|
+
const bannerStats = el('div', 'cz-stats');
|
|
1034
|
+
const reportLink = el('a', null, 'report →');
|
|
971
1035
|
reportLink.href = 'https://cmdzero.dev/report';
|
|
972
1036
|
reportLink.target = '_blank';
|
|
973
1037
|
reportLink.rel = 'noopener';
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
//
|
|
977
|
-
|
|
1038
|
+
banner.append(brand, bannerStats, reportLink);
|
|
1039
|
+
root.appendChild(banner);
|
|
1040
|
+
// Push the page down so the banner sits ABOVE the site rather than over it.
|
|
1041
|
+
try {
|
|
1042
|
+
const prevPad = getComputedStyle(document.body).paddingTop;
|
|
1043
|
+
document.body.style.paddingTop = `calc(${prevPad} + 30px)`;
|
|
1044
|
+
} catch { /* no body yet */ }
|
|
1045
|
+
|
|
1046
|
+
// ---------- tray ----------
|
|
1047
|
+
const tray = el('div', 'cz-tray');
|
|
1048
|
+
root.appendChild(tray);
|
|
1049
|
+
// Alerts live in their own wrap so history can collapse independently.
|
|
1050
|
+
// Newest is inserted at the top; the 5 newest stay visible and
|
|
978
1051
|
// older ones fold behind an expander (fades at the bottom).
|
|
979
1052
|
const tweaksWrap = el('div', 'cz-wrap');
|
|
980
1053
|
tray.appendChild(tweaksWrap);
|
|
@@ -1010,11 +1083,35 @@
|
|
|
1010
1083
|
} catch { /* no storage */ }
|
|
1011
1084
|
}
|
|
1012
1085
|
|
|
1086
|
+
const stat = (label, value, accent) => {
|
|
1087
|
+
const s = el('div', 'cz-stat' + (accent ? ' cz-accent' : ''));
|
|
1088
|
+
s.append(el('span', null, label), el('b', null, value));
|
|
1089
|
+
return s;
|
|
1090
|
+
};
|
|
1091
|
+
// How many of the session's tweaks cost zero tokens (copy/style/move/delete).
|
|
1092
|
+
function zeroTokenCount() {
|
|
1093
|
+
let z = 0;
|
|
1094
|
+
for (const r of tweakData.values()) if (!r.model && r.tokens === 0 && !String(r.id).startsWith('x')) z++;
|
|
1095
|
+
return z;
|
|
1096
|
+
}
|
|
1013
1097
|
function showTotals(t) {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1098
|
+
bannerStats.textContent = '';
|
|
1099
|
+
if (!t || !t.count) {
|
|
1100
|
+
bannerStats.append(el('span', 'cz-idle', 'Press ⌘0, then click any element to tweak it — edits land in your source.'));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const secs = Math.round(t.ms / 1000);
|
|
1104
|
+
const time = secs >= 60 ? `${Math.round(secs / 60)}m` : `${secs}s`;
|
|
1105
|
+
const z = zeroTokenCount();
|
|
1106
|
+
bannerStats.append(
|
|
1107
|
+
stat('tweaks', String(t.count)),
|
|
1108
|
+
stat('saved', `$${t.usd.toFixed(2)}`, true),
|
|
1109
|
+
stat('faster', time, true),
|
|
1110
|
+
);
|
|
1111
|
+
if (z > 0) bannerStats.append(stat('zero-token', String(z)));
|
|
1112
|
+
bannerStats.append(stat('vs', 'unscoped agent'));
|
|
1017
1113
|
}
|
|
1114
|
+
showTotals(null); // idle banner on load until /api/health arrives
|
|
1018
1115
|
|
|
1019
1116
|
function addTweak(t, hydrate) {
|
|
1020
1117
|
const key = String(t.id);
|
|
@@ -1244,29 +1341,37 @@
|
|
|
1244
1341
|
selectNext(e.shiftKey ? -1 : 1);
|
|
1245
1342
|
return;
|
|
1246
1343
|
}
|
|
1247
|
-
|
|
1344
|
+
// Enter saves ONLY when the caret is in the edited element — not when
|
|
1345
|
+
// typing in the popover's textarea (which uses Enter for newlines).
|
|
1346
|
+
if (e.key === 'Enter' && state.editing && e.target === state.editing.el) {
|
|
1248
1347
|
e.preventDefault();
|
|
1249
1348
|
finishTextEdit(true);
|
|
1250
1349
|
}
|
|
1251
1350
|
}, true);
|
|
1252
1351
|
|
|
1253
1352
|
addEventListener('mousemove', (e) => {
|
|
1254
|
-
if (!state.selectMode
|
|
1353
|
+
if (!state.selectMode) return;
|
|
1354
|
+
// keep hover previews live even while editing text, so you can see the
|
|
1355
|
+
// next element you'll click (clicking saves + moves there)
|
|
1255
1356
|
if (inOverlay(e.target)) return setHover(null);
|
|
1357
|
+
if (state.editing && state.editing.el.contains(e.target)) return setHover(null);
|
|
1256
1358
|
setHover(e.target instanceof Element ? e.target.closest('[data-cz]') : null);
|
|
1257
1359
|
}, true);
|
|
1258
1360
|
|
|
1259
1361
|
addEventListener('click', (e) => {
|
|
1260
1362
|
if (!state.selectMode) return;
|
|
1363
|
+
if (suppressClick) { e.preventDefault(); e.stopPropagation(); return; } // just finished a drag
|
|
1261
1364
|
if (inOverlay(e.target)) return;
|
|
1262
1365
|
if (state.editing) {
|
|
1263
|
-
// clicks inside the editable text place the caret;
|
|
1264
|
-
//
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1366
|
+
// clicks inside the editable text just place the caret; clicking another
|
|
1367
|
+
// element saves the current edit AND moves the selection there.
|
|
1368
|
+
if (state.editing.el.contains(e.target)) return;
|
|
1369
|
+
e.preventDefault();
|
|
1370
|
+
e.stopPropagation();
|
|
1371
|
+
finishTextEdit(true);
|
|
1372
|
+
const next = e.target instanceof Element ? e.target.closest('[data-cz]') : null;
|
|
1373
|
+
if (next && next !== state.selected?.el) select(next);
|
|
1374
|
+
else if (!next) deselect();
|
|
1270
1375
|
return;
|
|
1271
1376
|
}
|
|
1272
1377
|
e.preventDefault();
|
|
@@ -1279,6 +1384,29 @@
|
|
|
1279
1384
|
else deselect();
|
|
1280
1385
|
}, true);
|
|
1281
1386
|
|
|
1387
|
+
// Alert tray dodges the cursor: when the pointer enters its region, slide it
|
|
1388
|
+
// off to the right so you can click the elements underneath; slide back once
|
|
1389
|
+
// the pointer leaves the original region (hysteresis avoids flicker).
|
|
1390
|
+
let trayDodged = false, trayZone = null;
|
|
1391
|
+
addEventListener('mousemove', (e) => {
|
|
1392
|
+
const x = e.clientX, y = e.clientY;
|
|
1393
|
+
if (!trayDodged) {
|
|
1394
|
+
if (!tweaksWrap.children.length) return; // nothing to dodge
|
|
1395
|
+
const r = tray.getBoundingClientRect();
|
|
1396
|
+
if (r.width && x >= r.left - 8 && x <= r.right + 8 && y >= r.top - 8 && y <= r.bottom + 8) {
|
|
1397
|
+
trayZone = r;
|
|
1398
|
+
trayDodged = true;
|
|
1399
|
+
tray.classList.add('cz-dodged');
|
|
1400
|
+
}
|
|
1401
|
+
} else {
|
|
1402
|
+
const r = trayZone, m = 34;
|
|
1403
|
+
if (!r || x < r.left - m || x > r.right + m || y < r.top - m || y > r.bottom + m) {
|
|
1404
|
+
trayDodged = false;
|
|
1405
|
+
tray.classList.remove('cz-dodged');
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}, true);
|
|
1409
|
+
|
|
1282
1410
|
addEventListener('scroll', () => { reposition(); positionMulti(); setHover(state.hoverEl); }, true);
|
|
1283
1411
|
addEventListener('resize', () => { reposition(); positionMulti(); });
|
|
1284
1412
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cmdzero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Tweak your UI live in the browser and write the changes straight to source. Copy and Tailwind edits cost zero tokens; everything else routes to a right-sized model via headless claude.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|