vite-plugin-specter 0.7.1 → 0.7.5
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 +42 -9
- package/dist/client.js +246 -28
- package/dist/extension-chrome/content.js +246 -28
- package/dist/extension-chrome/manifest.json +1 -1
- package/dist/extension-firefox/content.js +246 -28
- package/dist/extension-firefox/manifest.json +1 -1
- package/dist/index.cjs +246 -28
- package/dist/index.js +246 -28
- package/extension/content.js +246 -28
- package/mcp-bridge/README.md +73 -0
- package/mcp-bridge/package.json +10 -0
- package/mcp-bridge/server.mjs +161 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -30,7 +30,17 @@ function getClientScript(options) {
|
|
|
30
30
|
var CHECK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
|
31
31
|
var SHARE = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg>';
|
|
32
32
|
var IMPORT = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v13"></path><polyline points="8 12 12 16 16 12"></polyline><path d="M4 21h16"></path></svg>';
|
|
33
|
-
|
|
33
|
+
// Toggle chord: the Vite-config value (or the built-in default) is the baseline; a
|
|
34
|
+
// per-origin localStorage override \u2014 set from the panel's rebind UI or window.__specter \u2014
|
|
35
|
+
// wins, so anyone can change it with no config edit and no dev-server restart.
|
|
36
|
+
var ACTIVATE_DEFAULT = ${JSON.stringify(activateShortcut)};
|
|
37
|
+
var ACTIVATE_KEY = '__specter_activate';
|
|
38
|
+
var ACTIVATE = ACTIVATE_DEFAULT;
|
|
39
|
+
try { var _actOv = localStorage.getItem(ACTIVATE_KEY); if (_actOv) ACTIVATE = _actOv; } catch (e) {}
|
|
40
|
+
function shortcutLabel(s) { return String(s || '').split('+').map(function (p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join('+'); }
|
|
41
|
+
function setActivate(combo) { ACTIVATE = combo; try { localStorage.setItem(ACTIVATE_KEY, combo); } catch (e) {} }
|
|
42
|
+
function resetActivate() { ACTIVATE = ACTIVATE_DEFAULT; try { localStorage.removeItem(ACTIVATE_KEY); } catch (e) {} }
|
|
43
|
+
function activateOverridden() { try { return !!localStorage.getItem(ACTIVATE_KEY); } catch (e) { return false; } }
|
|
34
44
|
var BRIDGE = ${JSON.stringify(bridgeUrl)}; // Claude MCP bridge URL, or '' if disabled
|
|
35
45
|
|
|
36
46
|
// \u2500\u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -688,6 +698,7 @@ function getClientScript(options) {
|
|
|
688
698
|
var groups = [];
|
|
689
699
|
for (var i = 0; i < specs.length; i++) {
|
|
690
700
|
var s = specs[i], g = null;
|
|
701
|
+
if (s.resolved) continue; // done Specs drop out of both the Cmd+C copy and the bridge sync \u2014 so /spectify never re-applies them
|
|
691
702
|
if (s.kind === 'element' && s.el) {
|
|
692
703
|
for (var j = 0; j < groups.length; j++) { if (groups[j].kind === 'element' && groups[j].el === s.el) { g = groups[j]; break; } }
|
|
693
704
|
}
|
|
@@ -928,10 +939,17 @@ function getClientScript(options) {
|
|
|
928
939
|
function startLoop() { if (rafId == null) (function loop() { reflowSpecs(); rafId = requestAnimationFrame(loop); })(); }
|
|
929
940
|
function stopLoop() { if (rafId != null) { cancelAnimationFrame(rafId); rafId = null; } }
|
|
930
941
|
|
|
931
|
-
|
|
942
|
+
// Paint a badge's number/\u2713 + done tint. Called by renumber and updateBadgeContent so
|
|
943
|
+
// the on-page pin reflects resolved state everywhere (reflowSpecs never touches these).
|
|
944
|
+
function paintBadgeState(spec, i) {
|
|
945
|
+
spec.num.textContent = String(i + 1); // always the number (for wayfinding) \u2014 green tint carries the "done" signal
|
|
946
|
+
if (spec.cap) { spec.cap.style.background = spec.resolved ? GREEN : PURPLE; spec.cap.style.opacity = spec.resolved ? '0.65' : '1'; }
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renumber() { for (var i = 0; i < specs.length; i++) paintBadgeState(specs[i], i); }
|
|
932
950
|
|
|
933
951
|
function updateBadgeContent(spec) {
|
|
934
|
-
spec
|
|
952
|
+
paintBadgeState(spec, specs.indexOf(spec));
|
|
935
953
|
if (spec.note) spec.noteSpan.textContent = spec.note;
|
|
936
954
|
else spec.noteSpan.textContent = spec.kind === 'measure' ? '\u2B21 measure' : '';
|
|
937
955
|
}
|
|
@@ -1053,6 +1071,33 @@ function getClientScript(options) {
|
|
|
1053
1071
|
saveSpecs();
|
|
1054
1072
|
}
|
|
1055
1073
|
|
|
1074
|
+
// \u2500\u2500 Resolved (done) lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1075
|
+
// A resolved Spec is NOT deleted \u2014 it stays as a record of what changed, greyed with
|
|
1076
|
+
// a \u2713, and drops out of the copy + bridge sync (see groupSpecs) so it's never re-applied.
|
|
1077
|
+
function resolvedCount() { var n = 0; for (var i = 0; i < specs.length; i++) if (specs[i].resolved) n++; return n; }
|
|
1078
|
+
|
|
1079
|
+
function toggleResolved(spec) {
|
|
1080
|
+
spec.resolved = !spec.resolved;
|
|
1081
|
+
if (spec.resolved && panelEditSpec === spec) panelEditSpec = null; // close an open editor on the one being marked done
|
|
1082
|
+
updateBadgeContent(spec);
|
|
1083
|
+
updatePill(); // re-renders the panel when open
|
|
1084
|
+
saveSpecs(); // persists resolved + re-syncs the bridge (now excluding it)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Sweep every resolved Spec once the user is satisfied \u2014 leaves open Specs untouched.
|
|
1088
|
+
function clearResolved() {
|
|
1089
|
+
for (var i = specs.length - 1; i >= 0; i--) {
|
|
1090
|
+
if (!specs[i].resolved) continue;
|
|
1091
|
+
if (highlightSpec === specs[i]) highlightSpec = null;
|
|
1092
|
+
if (panelEditSpec === specs[i]) panelEditSpec = null;
|
|
1093
|
+
if (specs[i].wrap) specs[i].wrap.remove();
|
|
1094
|
+
specs.splice(i, 1);
|
|
1095
|
+
}
|
|
1096
|
+
renumber();
|
|
1097
|
+
updatePill();
|
|
1098
|
+
saveSpecs();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1056
1101
|
// Drop every imported (shared) spec, leaving the user's own local ones. Used by
|
|
1057
1102
|
// importComments so a received share replaces the prior set instead of stacking.
|
|
1058
1103
|
function removeSharedSpecs() {
|
|
@@ -1069,7 +1114,7 @@ function getClientScript(options) {
|
|
|
1069
1114
|
function saveSpecs() {
|
|
1070
1115
|
try {
|
|
1071
1116
|
if (!specs.length) localStorage.removeItem(STORAGE_KEY);
|
|
1072
|
-
else localStorage.setItem(STORAGE_KEY, JSON.stringify(specs.map(function (s) { return { path: s.path, note: s.note, body: s.body, kind: s.kind, locate: s.locate, shared: s.shared, fp: s.fp, missing: s.missing, missReason: s.missReason }; })));
|
|
1117
|
+
else localStorage.setItem(STORAGE_KEY, JSON.stringify(specs.map(function (s) { return { path: s.path, note: s.note, body: s.body, kind: s.kind, locate: s.locate, shared: s.shared, fp: s.fp, missing: s.missing, missReason: s.missReason, resolved: s.resolved }; })));
|
|
1073
1118
|
} catch (e) {}
|
|
1074
1119
|
scheduleSync(); // keep the Claude bridge mirrored to the current Specs
|
|
1075
1120
|
}
|
|
@@ -1079,7 +1124,7 @@ function getClientScript(options) {
|
|
|
1079
1124
|
try { data = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); } catch (e) { return; }
|
|
1080
1125
|
if (!Array.isArray(data) || !data.length) return;
|
|
1081
1126
|
data.forEach(function (d) {
|
|
1082
|
-
var spec = { el: d.missing ? null : safeQuery(d.path), path: d.path, note: d.note || '', body: d.body || '', kind: d.kind || 'element', locate: d.locate || '', shared: !!d.shared, fp: d.fp || '', missing: !!d.missing, missReason: d.missReason || '' };
|
|
1127
|
+
var spec = { el: d.missing ? null : safeQuery(d.path), path: d.path, note: d.note || '', body: d.body || '', kind: d.kind || 'element', locate: d.locate || '', shared: !!d.shared, fp: d.fp || '', missing: !!d.missing, missReason: d.missReason || '', resolved: !!d.resolved };
|
|
1083
1128
|
specs.push(spec);
|
|
1084
1129
|
createBadge(spec);
|
|
1085
1130
|
});
|
|
@@ -1177,16 +1222,34 @@ function getClientScript(options) {
|
|
|
1177
1222
|
return hit;
|
|
1178
1223
|
}
|
|
1179
1224
|
|
|
1225
|
+
function fpOk(el, item) { return !item.fp || fingerprint(el) === item.fp; }
|
|
1226
|
+
|
|
1227
|
+
// Rank the signals rather than pooling them, so a specific locator always beats a
|
|
1228
|
+
// vague one:
|
|
1229
|
+
// 1. a find anchor that resolves to exactly ONE element (id / unique text / class)
|
|
1230
|
+
// \u2014 the strongest, most semantic signal;
|
|
1231
|
+
// 2. the rooted nth-of-type path, if unique \u2014 this is what disambiguates repeated
|
|
1232
|
+
// structure (a class-path find matches every paragraph; the path pins the exact
|
|
1233
|
+
// one). Pooling let an ambiguous find shadow the unique path \u2014 the mis-anchor bug;
|
|
1234
|
+
// 3. the structural fingerprint across the pooled candidates, then a DOM-wide scan;
|
|
1235
|
+
// 4. any lone resolution.
|
|
1236
|
+
// fp guards 1 and 2 so a drifted anchor can't win blindly, but a unique anchor with a
|
|
1237
|
+
// changed signature is still trusted at step 4 (placement over false-MISSING).
|
|
1180
1238
|
function reFindShared(item) {
|
|
1181
|
-
var
|
|
1182
|
-
|
|
1183
|
-
|
|
1239
|
+
var byFind = resolveFindCandidates(item.find);
|
|
1240
|
+
if (byFind.length === 1 && fpOk(byFind[0], item)) return byFind[0];
|
|
1241
|
+
var byPath = item.path ? queryAll(item.path) : [];
|
|
1242
|
+
if (byPath.length === 1 && fpOk(byPath[0], item)) return byPath[0];
|
|
1243
|
+
var cands = [], i, all = byFind.concat(byPath);
|
|
1244
|
+
for (i = 0; i < all.length; i++) if (all[i] && cands.indexOf(all[i]) < 0) cands.push(all[i]);
|
|
1184
1245
|
if (item.fp) {
|
|
1185
|
-
for (i = 0; i <
|
|
1246
|
+
for (i = 0; i < cands.length; i++) if (fingerprint(cands[i]) === item.fp) return cands[i];
|
|
1186
1247
|
var scan = findByFingerprint(item.fp); // selectors failed/ambiguous \u2192 find it by signature
|
|
1187
1248
|
if (scan) return scan;
|
|
1188
1249
|
}
|
|
1189
|
-
|
|
1250
|
+
if (byFind.length === 1) return byFind[0]; // unique anchor, fp drifted \u2014 trust the anchor
|
|
1251
|
+
if (byPath.length === 1) return byPath[0];
|
|
1252
|
+
return cands.length === 1 ? cands[0] : null;
|
|
1190
1253
|
}
|
|
1191
1254
|
|
|
1192
1255
|
// URL gate: two people must be on the SAME page for a shared comment to place.
|
|
@@ -1590,7 +1653,7 @@ function getClientScript(options) {
|
|
|
1590
1653
|
var rows = [
|
|
1591
1654
|
['P', 'mark / comment the hovered element'],
|
|
1592
1655
|
['C', 'toggle Comment mode (hide properties)'],
|
|
1593
|
-
['\\u2325 Option', 'toggle Measure mode'],
|
|
1656
|
+
['\\u2325 Option / Alt', 'toggle Measure mode'],
|
|
1594
1657
|
['M', 'pin an element to measure from'],
|
|
1595
1658
|
['Cmd/Ctrl+C', 'copy all Specs'],
|
|
1596
1659
|
['L', 'toggle this panel'],
|
|
@@ -1606,6 +1669,45 @@ function getClientScript(options) {
|
|
|
1606
1669
|
head.appendChild(headText);
|
|
1607
1670
|
var body = document.createElement('div');
|
|
1608
1671
|
Object.assign(body.style, { display: 'none', padding: '0 16px 12px' }); // collapsed by default
|
|
1672
|
+
|
|
1673
|
+
// Rebindable toggle shortcut \u2014 the one chord you might have to change if it collides
|
|
1674
|
+
// with a browser/extension binding. Click Change, press a new combo. Saved per origin,
|
|
1675
|
+
// no restart. Works identically in the Vite plugin overlay and the browser extension.
|
|
1676
|
+
(function () {
|
|
1677
|
+
var rrow = document.createElement('div');
|
|
1678
|
+
Object.assign(rrow.style, { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px', paddingBottom: '10px', borderBottom: '1px solid rgba(255,255,255,0.06)' });
|
|
1679
|
+
var chip = document.createElement('span');
|
|
1680
|
+
Object.assign(chip.style, { color: '#E0A3F5', fontWeight: '700', minWidth: '78px', flexShrink: '0' });
|
|
1681
|
+
var lbl = document.createElement('span');
|
|
1682
|
+
Object.assign(lbl.style, { flex: '1', minWidth: '0' });
|
|
1683
|
+
lbl.textContent = 'toggle Specter on/off';
|
|
1684
|
+
var change = document.createElement('span');
|
|
1685
|
+
Object.assign(change.style, { color: '#8AB4F8', cursor: 'pointer', flexShrink: '0', textDecoration: 'underline' });
|
|
1686
|
+
change.textContent = 'Change';
|
|
1687
|
+
var reset = document.createElement('span');
|
|
1688
|
+
Object.assign(reset.style, { color: '#B9BBC2', cursor: 'pointer', flexShrink: '0', textDecoration: 'underline', display: 'none' });
|
|
1689
|
+
reset.textContent = 'Reset';
|
|
1690
|
+
function refresh() {
|
|
1691
|
+
chip.textContent = shortcutLabel(ACTIVATE);
|
|
1692
|
+
chip.style.color = '#E0A3F5';
|
|
1693
|
+
change.style.display = 'inline';
|
|
1694
|
+
reset.style.display = activateOverridden() ? 'inline' : 'none';
|
|
1695
|
+
}
|
|
1696
|
+
change.addEventListener('click', function (e) {
|
|
1697
|
+
e.stopPropagation();
|
|
1698
|
+
change.style.display = 'none';
|
|
1699
|
+
reset.style.display = 'none';
|
|
1700
|
+
startRebind(
|
|
1701
|
+
function (text, color) { chip.textContent = text; chip.style.color = color; },
|
|
1702
|
+
function () { refresh(); }
|
|
1703
|
+
);
|
|
1704
|
+
});
|
|
1705
|
+
reset.addEventListener('click', function (e) { e.stopPropagation(); resetActivate(); refresh(); });
|
|
1706
|
+
refresh();
|
|
1707
|
+
rrow.appendChild(chip); rrow.appendChild(lbl); rrow.appendChild(change); rrow.appendChild(reset);
|
|
1708
|
+
body.appendChild(rrow);
|
|
1709
|
+
})();
|
|
1710
|
+
|
|
1609
1711
|
rows.forEach(function (r) {
|
|
1610
1712
|
var row = document.createElement('div');
|
|
1611
1713
|
Object.assign(row.style, { display: 'flex', gap: '8px', marginBottom: '8px' });
|
|
@@ -1628,20 +1730,48 @@ function getClientScript(options) {
|
|
|
1628
1730
|
panelKeys.appendChild(body);
|
|
1629
1731
|
})();
|
|
1630
1732
|
|
|
1733
|
+
// Footer \u2014 a quiet link out to the GitHub README so first-timers can find the docs.
|
|
1734
|
+
var panelFoot = document.createElement('div');
|
|
1735
|
+
Object.assign(panelFoot.style, {
|
|
1736
|
+
flexShrink: '0', padding: '10px 16px', borderTop: '1px solid rgba(255,255,255,0.08)',
|
|
1737
|
+
display: 'flex', alignItems: 'center',
|
|
1738
|
+
});
|
|
1739
|
+
var docsLink = document.createElement('a');
|
|
1740
|
+
docsLink.href = 'https://github.com/setugk/vite-plugin-specter#readme';
|
|
1741
|
+
docsLink.target = '_blank';
|
|
1742
|
+
docsLink.rel = 'noopener noreferrer';
|
|
1743
|
+
docsLink.textContent = 'Documentation \\u2197'; // \u2197
|
|
1744
|
+
Object.assign(docsLink.style, { fontSize: '11px', fontWeight: '600', color: '#8AB4F8', textDecoration: 'none', cursor: 'pointer' });
|
|
1745
|
+
hoverFx(docsLink, { color: '#fff' }, { color: '#8AB4F8' });
|
|
1746
|
+
docsLink.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
1747
|
+
panelFoot.appendChild(docsLink);
|
|
1748
|
+
|
|
1631
1749
|
panelWrap.appendChild(panelHead);
|
|
1632
1750
|
panelWrap.appendChild(panelTools);
|
|
1633
1751
|
panelWrap.appendChild(panelHint);
|
|
1634
1752
|
panelWrap.appendChild(panelList);
|
|
1635
1753
|
panelWrap.appendChild(panelKeys);
|
|
1754
|
+
panelWrap.appendChild(panelFoot);
|
|
1636
1755
|
mount(panelWrap);
|
|
1637
1756
|
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1757
|
+
// A wheel anywhere over the panel must NEVER scroll the page behind it. Relying on
|
|
1758
|
+
// overscroll-behavior alone still lets trackpad momentum chain into the page at the
|
|
1759
|
+
// list bounds, so we take over: always swallow the event, and drive the list's own
|
|
1760
|
+
// scroll ourselves. (The inline note editor auto-grows instead of scrolling, so the
|
|
1761
|
+
// list is the only scrollable region inside the panel \u2014 nothing else needs the wheel.)
|
|
1762
|
+
var listScrolling = false, listScrollTimer = null;
|
|
1641
1763
|
panelWrap.addEventListener('wheel', function (e) {
|
|
1642
|
-
var canScroll = panelList.scrollHeight > panelList.clientHeight;
|
|
1643
|
-
if (panelList.contains(e.target) && canScroll) return; // let the list scroll natively
|
|
1644
1764
|
e.preventDefault();
|
|
1765
|
+
if (panelList.scrollHeight > panelList.clientHeight && panelList.contains(e.target)) {
|
|
1766
|
+
var dy = e.deltaMode === 1 ? e.deltaY * 16 : (e.deltaMode === 2 ? e.deltaY * panelList.clientHeight : e.deltaY);
|
|
1767
|
+
panelList.scrollTop += dy;
|
|
1768
|
+
// Rows slide under a stationary cursor as the list scrolls, firing mouseenter \u2192
|
|
1769
|
+
// revealSpec \u2192 scrollIntoView, which would drag the PAGE around. Mark the list as
|
|
1770
|
+
// actively scrolling so those hover-reveals are suppressed until scrolling settles.
|
|
1771
|
+
listScrolling = true;
|
|
1772
|
+
clearTimeout(listScrollTimer);
|
|
1773
|
+
listScrollTimer = setTimeout(function () { listScrolling = false; }, 200);
|
|
1774
|
+
}
|
|
1645
1775
|
}, { passive: false });
|
|
1646
1776
|
|
|
1647
1777
|
// \u2500\u2500 Auto-sync: mirror the browser's current Specs to the local bridge \u2500\u2500
|
|
@@ -1654,7 +1784,7 @@ function getClientScript(options) {
|
|
|
1654
1784
|
setPillSync(s); // control-bar dot
|
|
1655
1785
|
applyDotState(dot, s); // side-panel dot \u2014 same spinner/green/red
|
|
1656
1786
|
if (s === 'syncing') { dotLabel.textContent = 'Syncing\u2026'; }
|
|
1657
|
-
else if (s === 'synced') {
|
|
1787
|
+
else if (s === 'synced') { var open = specs.length - resolvedCount(); dotLabel.textContent = open > 0 ? (open + (open === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
|
|
1658
1788
|
else { dotLabel.textContent = 'Bridge offline'; }
|
|
1659
1789
|
}
|
|
1660
1790
|
function doSync() {
|
|
@@ -1701,11 +1831,28 @@ function getClientScript(options) {
|
|
|
1701
1831
|
panelList.appendChild(empty);
|
|
1702
1832
|
return;
|
|
1703
1833
|
}
|
|
1834
|
+
// Summary of resolved Specs + a one-click sweep, pinned above the list.
|
|
1835
|
+
var rc = resolvedCount();
|
|
1836
|
+
if (rc > 0) {
|
|
1837
|
+
var doneBar = document.createElement('div');
|
|
1838
|
+
Object.assign(doneBar.style, { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', padding: '8px 16px', borderBottom: '1px solid rgba(255,255,255,0.06)' });
|
|
1839
|
+
var dlbl = document.createElement('span');
|
|
1840
|
+
dlbl.textContent = '\u2713 ' + rc + (rc === 1 ? ' done' : ' done');
|
|
1841
|
+
Object.assign(dlbl.style, { fontSize: '11px', color: GREEN, fontWeight: '700', letterSpacing: '0.04em', textTransform: 'uppercase' });
|
|
1842
|
+
var clr = document.createElement('span');
|
|
1843
|
+
clr.textContent = 'Clear done';
|
|
1844
|
+
Object.assign(clr.style, { fontSize: '11px', color: '#B9BBC2', cursor: 'pointer', textDecoration: 'underline' });
|
|
1845
|
+
clr.addEventListener('click', function (e) { e.stopPropagation(); clearResolved(); });
|
|
1846
|
+
hoverFx(clr, { color: '#fff' }, { color: '#B9BBC2' });
|
|
1847
|
+
doneBar.appendChild(dlbl); doneBar.appendChild(clr);
|
|
1848
|
+
panelList.appendChild(doneBar);
|
|
1849
|
+
}
|
|
1704
1850
|
var focusEditor = null, measures = [];
|
|
1705
1851
|
specs.forEach(function (spec, i) {
|
|
1706
1852
|
var missing = !!spec.missing;
|
|
1707
1853
|
var visible = missing ? false : specVisible(spec); // don't specVisible() a missing spec \u2014 it would re-anchor via path
|
|
1708
1854
|
var editing = (panelEditSpec === spec);
|
|
1855
|
+
var resolved = !!spec.resolved;
|
|
1709
1856
|
var row = document.createElement('div');
|
|
1710
1857
|
Object.assign(row.style, {
|
|
1711
1858
|
position: 'relative', display: 'flex', alignItems: 'flex-start', gap: '12px', boxSizing: 'border-box',
|
|
@@ -1716,7 +1863,7 @@ function getClientScript(options) {
|
|
|
1716
1863
|
badge.textContent = String(i + 1);
|
|
1717
1864
|
Object.assign(badge.style, {
|
|
1718
1865
|
flexShrink: '0', width: '20px', height: '20px', borderRadius: '999px',
|
|
1719
|
-
background: BADGE_IDLE, color: '#fff', fontSize: '11px', fontWeight: '700',
|
|
1866
|
+
background: resolved ? GREEN : BADGE_IDLE, color: '#fff', fontSize: '11px', fontWeight: '700',
|
|
1720
1867
|
border: '1px solid #fff', boxSizing: 'border-box',
|
|
1721
1868
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1722
1869
|
transition: 'background 0.12s ease',
|
|
@@ -1788,6 +1935,7 @@ function getClientScript(options) {
|
|
|
1788
1935
|
cursor: spec.note ? 'pointer' : 'default',
|
|
1789
1936
|
});
|
|
1790
1937
|
note.textContent = spec.note || (spec.kind === 'measure' ? '\u2B21 measurement' : '\u2014 no note \u2014');
|
|
1938
|
+
if (resolved) { note.style.textDecoration = 'line-through'; note.style.color = LABEL; note.style.cursor = spec.note ? 'pointer' : 'default'; }
|
|
1791
1939
|
|
|
1792
1940
|
// Floated into the top-right corner so they don't reserve note width. On hover
|
|
1793
1941
|
// they get a background that fades in from the left, masking the note text behind
|
|
@@ -1806,11 +1954,14 @@ function getClientScript(options) {
|
|
|
1806
1954
|
// Show an expand toggle only when the collapsed note actually overflows.
|
|
1807
1955
|
var chev = mkIcon(CHEV, expanded ? 'Collapse' : 'Expand', '#B9BBC2', function () { spec._expanded = !spec._expanded; renderPanel(); });
|
|
1808
1956
|
chev.firstChild.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
1957
|
+
var resolveBtn = mkIcon(CHECK, resolved ? 'Mark as not done' : 'Mark as done', resolved ? GREEN : '#9BE6B0', function () { toggleResolved(spec); }, { background: 'rgba(34,197,94,0.28)', color: '#fff' });
|
|
1809
1958
|
var editBtn = mkIcon(PENCIL, 'Edit note', '#B9BBC2', function () { panelEditSpec = spec; spec._expanded = true; renderPanel(); });
|
|
1810
1959
|
var delBtn = mkIcon(TRASH, 'Delete Spec', '#ED8FA6', function () { removeSpec(spec); }, { background: 'rgba(237,62,97,0.30)', color: '#fff' });
|
|
1811
|
-
//
|
|
1812
|
-
// and fixed
|
|
1813
|
-
|
|
1960
|
+
// \u2713 / edit / delete all reveal only on row hover (the expand chevron stays rightmost
|
|
1961
|
+
// and fixed). The done state reads from the green badge + strikethrough + "\u2713 DONE",
|
|
1962
|
+
// so the row needs no persistent icon \u2014 it looks like any other list item.
|
|
1963
|
+
editBtn.style.display = delBtn.style.display = resolveBtn.style.display = 'none';
|
|
1964
|
+
actions.appendChild(resolveBtn);
|
|
1814
1965
|
actions.appendChild(editBtn);
|
|
1815
1966
|
actions.appendChild(delBtn);
|
|
1816
1967
|
actions.appendChild(chev);
|
|
@@ -1818,9 +1969,27 @@ function getClientScript(options) {
|
|
|
1818
1969
|
content.appendChild(note);
|
|
1819
1970
|
content.appendChild(meta);
|
|
1820
1971
|
|
|
1972
|
+
// Done: a green DONE tag + dimmed row. Supersedes HIDDEN/MISSING \u2014 once a Spec is
|
|
1973
|
+
// resolved you don't care whether its element is currently on screen.
|
|
1974
|
+
if (resolved) {
|
|
1975
|
+
row.style.opacity = '0.6';
|
|
1976
|
+
var rbar = document.createElement('div');
|
|
1977
|
+
Object.assign(rbar.style, { display: 'flex', alignItems: 'center', gap: '5px', marginTop: '2px', flexWrap: 'wrap' });
|
|
1978
|
+
var rcheck = document.createElement('span');
|
|
1979
|
+
rcheck.innerHTML = CHECK;
|
|
1980
|
+
Object.assign(rcheck.style, { display: 'inline-flex', alignItems: 'center', color: GREEN, flexShrink: '0' });
|
|
1981
|
+
if (rcheck.firstChild) { rcheck.firstChild.setAttribute('width', '12'); rcheck.firstChild.setAttribute('height', '12'); }
|
|
1982
|
+
var rtag = document.createElement('span');
|
|
1983
|
+
rtag.textContent = 'DONE';
|
|
1984
|
+
// Text-only status label (no fill/padding/radius) so it doesn't read as a clickable chip.
|
|
1985
|
+
Object.assign(rtag.style, { fontSize: '10px', fontWeight: '700', letterSpacing: '0.08em', textTransform: 'uppercase', color: GREEN, flexShrink: '0' });
|
|
1986
|
+
rbar.appendChild(rcheck);
|
|
1987
|
+
rbar.appendChild(rtag);
|
|
1988
|
+
content.appendChild(rbar);
|
|
1989
|
+
}
|
|
1821
1990
|
// Shared comment whose target is gone/changed: MISSING (greyed, reason shown,
|
|
1822
1991
|
// never drawn on the page \u2014 design: show missing, don't guess a spot).
|
|
1823
|
-
if (missing) {
|
|
1992
|
+
else if (missing) {
|
|
1824
1993
|
row.style.opacity = '0.7';
|
|
1825
1994
|
var mbar = document.createElement('div');
|
|
1826
1995
|
Object.assign(mbar.style, { display: 'flex', alignItems: 'center', gap: '7px', marginTop: '2px', flexWrap: 'wrap' });
|
|
@@ -1857,15 +2026,15 @@ function getClientScript(options) {
|
|
|
1857
2026
|
note.addEventListener('click', function (e) { if (spec.note) { e.stopPropagation(); spec._expanded = !spec._expanded; renderPanel(); } });
|
|
1858
2027
|
row.addEventListener('mouseenter', function () {
|
|
1859
2028
|
row.style.background = 'rgba(255,255,255,0.05)';
|
|
1860
|
-
badge.style.background = PURPLE;
|
|
1861
|
-
editBtn.style.display = delBtn.style.display = 'flex';
|
|
2029
|
+
if (!resolved) badge.style.background = PURPLE;
|
|
2030
|
+
editBtn.style.display = delBtn.style.display = resolveBtn.style.display = 'flex';
|
|
1862
2031
|
actions.style.background = 'linear-gradient(to right, rgba(52,54,60,0) 0, rgba(52,54,60,1) 22px)';
|
|
1863
|
-
if (visible) { highlightSpec = spec; revealSpec(spec); }
|
|
2032
|
+
if (visible && !listScrolling) { highlightSpec = spec; revealSpec(spec); } // don't reveal on rows that merely slid under the cursor while scrolling
|
|
1864
2033
|
});
|
|
1865
2034
|
row.addEventListener('mouseleave', function () {
|
|
1866
2035
|
row.style.background = 'transparent';
|
|
1867
|
-
badge.style.background = BADGE_IDLE;
|
|
1868
|
-
editBtn.style.display = delBtn.style.display = 'none';
|
|
2036
|
+
badge.style.background = resolved ? GREEN : BADGE_IDLE;
|
|
2037
|
+
editBtn.style.display = delBtn.style.display = resolveBtn.style.display = 'none';
|
|
1869
2038
|
actions.style.background = 'transparent';
|
|
1870
2039
|
if (highlightSpec === spec) highlightSpec = null;
|
|
1871
2040
|
});
|
|
@@ -2293,6 +2462,41 @@ function getClientScript(options) {
|
|
|
2293
2462
|
return eKey === key || eCode === 'key' + key || (key === 'period' && (eKey === '.' || eCode === 'period'));
|
|
2294
2463
|
}
|
|
2295
2464
|
|
|
2465
|
+
// Capture the next chord the user presses and make it the new toggle. Runs on the
|
|
2466
|
+
// capture phase + stops propagation so a rebind keystroke (e.g. "L") can't leak into
|
|
2467
|
+
// Specter's own shortcuts. Requires at least one modifier so the toggle can't be a bare
|
|
2468
|
+
// key that fires while you type. onState(text, color) drives the prompt; onDone(committed).
|
|
2469
|
+
var rebinding = false;
|
|
2470
|
+
function startRebind(onState, onDone) {
|
|
2471
|
+
if (rebinding) return;
|
|
2472
|
+
rebinding = true;
|
|
2473
|
+
onState('Press a key combo\u2026', GREEN);
|
|
2474
|
+
function cap(e) {
|
|
2475
|
+
e.preventDefault(); e.stopPropagation();
|
|
2476
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2477
|
+
var k = e.key;
|
|
2478
|
+
if (k === 'Escape') { finish(false); return; }
|
|
2479
|
+
if (k === 'Control' || k === 'Alt' || k === 'Shift' || k === 'Meta') return; // wait for the real key
|
|
2480
|
+
var parts = [];
|
|
2481
|
+
if (e.ctrlKey) parts.push('ctrl');
|
|
2482
|
+
if (e.altKey) parts.push('alt');
|
|
2483
|
+
if (e.shiftKey) parts.push('shift');
|
|
2484
|
+
if (e.metaKey) parts.push('meta');
|
|
2485
|
+
if (!parts.length) { onState('Hold Ctrl / Alt / \\u2318 too\u2026', '#F59E0B'); return; }
|
|
2486
|
+
var key = (e.key.length === 1 ? e.key : e.code.replace(/^Key/, '')).toLowerCase();
|
|
2487
|
+
if (!key) return;
|
|
2488
|
+
parts.push(key);
|
|
2489
|
+
setActivate(parts.join('+'));
|
|
2490
|
+
finish(true);
|
|
2491
|
+
}
|
|
2492
|
+
function finish(committed) {
|
|
2493
|
+
document.removeEventListener('keydown', cap, true);
|
|
2494
|
+
rebinding = false;
|
|
2495
|
+
onDone(committed);
|
|
2496
|
+
}
|
|
2497
|
+
document.addEventListener('keydown', cap, true);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2296
2500
|
// \u2500\u2500\u2500 Mouse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2297
2501
|
function onMouseMove(e) {
|
|
2298
2502
|
if (!fiActive) return;
|
|
@@ -2474,7 +2678,21 @@ function getClientScript(options) {
|
|
|
2474
2678
|
importFromHash(); // if arrived via a #spx= share link, import + reveal the shared comments
|
|
2475
2679
|
scheduleSync(); // mirror restored Specs to the Claude bridge on load
|
|
2476
2680
|
|
|
2477
|
-
console
|
|
2681
|
+
// Dev-only console API \u2014 the escape hatch when the toggle chord collides with a browser
|
|
2682
|
+
// shortcut so you can't even open the overlay. Specter is stripped from prod, so this
|
|
2683
|
+
// global never ships. Vite devs can run these straight from DevTools.
|
|
2684
|
+
try {
|
|
2685
|
+
window.__specter = {
|
|
2686
|
+
toggle: function () { if (fiActive) deactivate(); else activate(); },
|
|
2687
|
+
activate: activate,
|
|
2688
|
+
deactivate: deactivate,
|
|
2689
|
+
get shortcut() { return ACTIVATE; },
|
|
2690
|
+
setShortcut: function (combo) { if (combo) { setActivate(String(combo).toLowerCase()); if (panelOpen) renderPanel(); console.log('%c\u{1F47B} Specter \u2014 toggle is now ' + shortcutLabel(ACTIVATE), 'color:#aaa'); } return ACTIVATE; },
|
|
2691
|
+
resetShortcut: function () { resetActivate(); if (panelOpen) renderPanel(); console.log('%c\u{1F47B} Specter \u2014 toggle reset to ' + shortcutLabel(ACTIVATE), 'color:#aaa'); return ACTIVATE; },
|
|
2692
|
+
};
|
|
2693
|
+
} catch (e) {}
|
|
2694
|
+
|
|
2695
|
+
console.log('%c\u{1F47B} Specter \u2014 ' + shortcutLabel(ACTIVATE) + ' to toggle \xB7 change it: window.__specter.setShortcut("ctrl+alt+p")', 'color:#aaa;font-size:11px;');
|
|
2478
2696
|
})();`;
|
|
2479
2697
|
}
|
|
2480
2698
|
|