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.cjs
CHANGED
|
@@ -57,7 +57,17 @@ function getClientScript(options) {
|
|
|
57
57
|
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>';
|
|
58
58
|
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>';
|
|
59
59
|
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>';
|
|
60
|
-
|
|
60
|
+
// Toggle chord: the Vite-config value (or the built-in default) is the baseline; a
|
|
61
|
+
// per-origin localStorage override \u2014 set from the panel's rebind UI or window.__specter \u2014
|
|
62
|
+
// wins, so anyone can change it with no config edit and no dev-server restart.
|
|
63
|
+
var ACTIVATE_DEFAULT = ${JSON.stringify(activateShortcut)};
|
|
64
|
+
var ACTIVATE_KEY = '__specter_activate';
|
|
65
|
+
var ACTIVATE = ACTIVATE_DEFAULT;
|
|
66
|
+
try { var _actOv = localStorage.getItem(ACTIVATE_KEY); if (_actOv) ACTIVATE = _actOv; } catch (e) {}
|
|
67
|
+
function shortcutLabel(s) { return String(s || '').split('+').map(function (p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join('+'); }
|
|
68
|
+
function setActivate(combo) { ACTIVATE = combo; try { localStorage.setItem(ACTIVATE_KEY, combo); } catch (e) {} }
|
|
69
|
+
function resetActivate() { ACTIVATE = ACTIVATE_DEFAULT; try { localStorage.removeItem(ACTIVATE_KEY); } catch (e) {} }
|
|
70
|
+
function activateOverridden() { try { return !!localStorage.getItem(ACTIVATE_KEY); } catch (e) { return false; } }
|
|
61
71
|
var BRIDGE = ${JSON.stringify(bridgeUrl)}; // Claude MCP bridge URL, or '' if disabled
|
|
62
72
|
|
|
63
73
|
// \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
|
|
@@ -715,6 +725,7 @@ function getClientScript(options) {
|
|
|
715
725
|
var groups = [];
|
|
716
726
|
for (var i = 0; i < specs.length; i++) {
|
|
717
727
|
var s = specs[i], g = null;
|
|
728
|
+
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
|
|
718
729
|
if (s.kind === 'element' && s.el) {
|
|
719
730
|
for (var j = 0; j < groups.length; j++) { if (groups[j].kind === 'element' && groups[j].el === s.el) { g = groups[j]; break; } }
|
|
720
731
|
}
|
|
@@ -955,10 +966,17 @@ function getClientScript(options) {
|
|
|
955
966
|
function startLoop() { if (rafId == null) (function loop() { reflowSpecs(); rafId = requestAnimationFrame(loop); })(); }
|
|
956
967
|
function stopLoop() { if (rafId != null) { cancelAnimationFrame(rafId); rafId = null; } }
|
|
957
968
|
|
|
958
|
-
|
|
969
|
+
// Paint a badge's number/\u2713 + done tint. Called by renumber and updateBadgeContent so
|
|
970
|
+
// the on-page pin reflects resolved state everywhere (reflowSpecs never touches these).
|
|
971
|
+
function paintBadgeState(spec, i) {
|
|
972
|
+
spec.num.textContent = String(i + 1); // always the number (for wayfinding) \u2014 green tint carries the "done" signal
|
|
973
|
+
if (spec.cap) { spec.cap.style.background = spec.resolved ? GREEN : PURPLE; spec.cap.style.opacity = spec.resolved ? '0.65' : '1'; }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function renumber() { for (var i = 0; i < specs.length; i++) paintBadgeState(specs[i], i); }
|
|
959
977
|
|
|
960
978
|
function updateBadgeContent(spec) {
|
|
961
|
-
spec
|
|
979
|
+
paintBadgeState(spec, specs.indexOf(spec));
|
|
962
980
|
if (spec.note) spec.noteSpan.textContent = spec.note;
|
|
963
981
|
else spec.noteSpan.textContent = spec.kind === 'measure' ? '\u2B21 measure' : '';
|
|
964
982
|
}
|
|
@@ -1080,6 +1098,33 @@ function getClientScript(options) {
|
|
|
1080
1098
|
saveSpecs();
|
|
1081
1099
|
}
|
|
1082
1100
|
|
|
1101
|
+
// \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
|
|
1102
|
+
// A resolved Spec is NOT deleted \u2014 it stays as a record of what changed, greyed with
|
|
1103
|
+
// a \u2713, and drops out of the copy + bridge sync (see groupSpecs) so it's never re-applied.
|
|
1104
|
+
function resolvedCount() { var n = 0; for (var i = 0; i < specs.length; i++) if (specs[i].resolved) n++; return n; }
|
|
1105
|
+
|
|
1106
|
+
function toggleResolved(spec) {
|
|
1107
|
+
spec.resolved = !spec.resolved;
|
|
1108
|
+
if (spec.resolved && panelEditSpec === spec) panelEditSpec = null; // close an open editor on the one being marked done
|
|
1109
|
+
updateBadgeContent(spec);
|
|
1110
|
+
updatePill(); // re-renders the panel when open
|
|
1111
|
+
saveSpecs(); // persists resolved + re-syncs the bridge (now excluding it)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Sweep every resolved Spec once the user is satisfied \u2014 leaves open Specs untouched.
|
|
1115
|
+
function clearResolved() {
|
|
1116
|
+
for (var i = specs.length - 1; i >= 0; i--) {
|
|
1117
|
+
if (!specs[i].resolved) continue;
|
|
1118
|
+
if (highlightSpec === specs[i]) highlightSpec = null;
|
|
1119
|
+
if (panelEditSpec === specs[i]) panelEditSpec = null;
|
|
1120
|
+
if (specs[i].wrap) specs[i].wrap.remove();
|
|
1121
|
+
specs.splice(i, 1);
|
|
1122
|
+
}
|
|
1123
|
+
renumber();
|
|
1124
|
+
updatePill();
|
|
1125
|
+
saveSpecs();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1083
1128
|
// Drop every imported (shared) spec, leaving the user's own local ones. Used by
|
|
1084
1129
|
// importComments so a received share replaces the prior set instead of stacking.
|
|
1085
1130
|
function removeSharedSpecs() {
|
|
@@ -1096,7 +1141,7 @@ function getClientScript(options) {
|
|
|
1096
1141
|
function saveSpecs() {
|
|
1097
1142
|
try {
|
|
1098
1143
|
if (!specs.length) localStorage.removeItem(STORAGE_KEY);
|
|
1099
|
-
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 }; })));
|
|
1144
|
+
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 }; })));
|
|
1100
1145
|
} catch (e) {}
|
|
1101
1146
|
scheduleSync(); // keep the Claude bridge mirrored to the current Specs
|
|
1102
1147
|
}
|
|
@@ -1106,7 +1151,7 @@ function getClientScript(options) {
|
|
|
1106
1151
|
try { data = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); } catch (e) { return; }
|
|
1107
1152
|
if (!Array.isArray(data) || !data.length) return;
|
|
1108
1153
|
data.forEach(function (d) {
|
|
1109
|
-
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 || '' };
|
|
1154
|
+
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 };
|
|
1110
1155
|
specs.push(spec);
|
|
1111
1156
|
createBadge(spec);
|
|
1112
1157
|
});
|
|
@@ -1204,16 +1249,34 @@ function getClientScript(options) {
|
|
|
1204
1249
|
return hit;
|
|
1205
1250
|
}
|
|
1206
1251
|
|
|
1252
|
+
function fpOk(el, item) { return !item.fp || fingerprint(el) === item.fp; }
|
|
1253
|
+
|
|
1254
|
+
// Rank the signals rather than pooling them, so a specific locator always beats a
|
|
1255
|
+
// vague one:
|
|
1256
|
+
// 1. a find anchor that resolves to exactly ONE element (id / unique text / class)
|
|
1257
|
+
// \u2014 the strongest, most semantic signal;
|
|
1258
|
+
// 2. the rooted nth-of-type path, if unique \u2014 this is what disambiguates repeated
|
|
1259
|
+
// structure (a class-path find matches every paragraph; the path pins the exact
|
|
1260
|
+
// one). Pooling let an ambiguous find shadow the unique path \u2014 the mis-anchor bug;
|
|
1261
|
+
// 3. the structural fingerprint across the pooled candidates, then a DOM-wide scan;
|
|
1262
|
+
// 4. any lone resolution.
|
|
1263
|
+
// fp guards 1 and 2 so a drifted anchor can't win blindly, but a unique anchor with a
|
|
1264
|
+
// changed signature is still trusted at step 4 (placement over false-MISSING).
|
|
1207
1265
|
function reFindShared(item) {
|
|
1208
|
-
var
|
|
1209
|
-
|
|
1210
|
-
|
|
1266
|
+
var byFind = resolveFindCandidates(item.find);
|
|
1267
|
+
if (byFind.length === 1 && fpOk(byFind[0], item)) return byFind[0];
|
|
1268
|
+
var byPath = item.path ? queryAll(item.path) : [];
|
|
1269
|
+
if (byPath.length === 1 && fpOk(byPath[0], item)) return byPath[0];
|
|
1270
|
+
var cands = [], i, all = byFind.concat(byPath);
|
|
1271
|
+
for (i = 0; i < all.length; i++) if (all[i] && cands.indexOf(all[i]) < 0) cands.push(all[i]);
|
|
1211
1272
|
if (item.fp) {
|
|
1212
|
-
for (i = 0; i <
|
|
1273
|
+
for (i = 0; i < cands.length; i++) if (fingerprint(cands[i]) === item.fp) return cands[i];
|
|
1213
1274
|
var scan = findByFingerprint(item.fp); // selectors failed/ambiguous \u2192 find it by signature
|
|
1214
1275
|
if (scan) return scan;
|
|
1215
1276
|
}
|
|
1216
|
-
|
|
1277
|
+
if (byFind.length === 1) return byFind[0]; // unique anchor, fp drifted \u2014 trust the anchor
|
|
1278
|
+
if (byPath.length === 1) return byPath[0];
|
|
1279
|
+
return cands.length === 1 ? cands[0] : null;
|
|
1217
1280
|
}
|
|
1218
1281
|
|
|
1219
1282
|
// URL gate: two people must be on the SAME page for a shared comment to place.
|
|
@@ -1617,7 +1680,7 @@ function getClientScript(options) {
|
|
|
1617
1680
|
var rows = [
|
|
1618
1681
|
['P', 'mark / comment the hovered element'],
|
|
1619
1682
|
['C', 'toggle Comment mode (hide properties)'],
|
|
1620
|
-
['\\u2325 Option', 'toggle Measure mode'],
|
|
1683
|
+
['\\u2325 Option / Alt', 'toggle Measure mode'],
|
|
1621
1684
|
['M', 'pin an element to measure from'],
|
|
1622
1685
|
['Cmd/Ctrl+C', 'copy all Specs'],
|
|
1623
1686
|
['L', 'toggle this panel'],
|
|
@@ -1633,6 +1696,45 @@ function getClientScript(options) {
|
|
|
1633
1696
|
head.appendChild(headText);
|
|
1634
1697
|
var body = document.createElement('div');
|
|
1635
1698
|
Object.assign(body.style, { display: 'none', padding: '0 16px 12px' }); // collapsed by default
|
|
1699
|
+
|
|
1700
|
+
// Rebindable toggle shortcut \u2014 the one chord you might have to change if it collides
|
|
1701
|
+
// with a browser/extension binding. Click Change, press a new combo. Saved per origin,
|
|
1702
|
+
// no restart. Works identically in the Vite plugin overlay and the browser extension.
|
|
1703
|
+
(function () {
|
|
1704
|
+
var rrow = document.createElement('div');
|
|
1705
|
+
Object.assign(rrow.style, { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px', paddingBottom: '10px', borderBottom: '1px solid rgba(255,255,255,0.06)' });
|
|
1706
|
+
var chip = document.createElement('span');
|
|
1707
|
+
Object.assign(chip.style, { color: '#E0A3F5', fontWeight: '700', minWidth: '78px', flexShrink: '0' });
|
|
1708
|
+
var lbl = document.createElement('span');
|
|
1709
|
+
Object.assign(lbl.style, { flex: '1', minWidth: '0' });
|
|
1710
|
+
lbl.textContent = 'toggle Specter on/off';
|
|
1711
|
+
var change = document.createElement('span');
|
|
1712
|
+
Object.assign(change.style, { color: '#8AB4F8', cursor: 'pointer', flexShrink: '0', textDecoration: 'underline' });
|
|
1713
|
+
change.textContent = 'Change';
|
|
1714
|
+
var reset = document.createElement('span');
|
|
1715
|
+
Object.assign(reset.style, { color: '#B9BBC2', cursor: 'pointer', flexShrink: '0', textDecoration: 'underline', display: 'none' });
|
|
1716
|
+
reset.textContent = 'Reset';
|
|
1717
|
+
function refresh() {
|
|
1718
|
+
chip.textContent = shortcutLabel(ACTIVATE);
|
|
1719
|
+
chip.style.color = '#E0A3F5';
|
|
1720
|
+
change.style.display = 'inline';
|
|
1721
|
+
reset.style.display = activateOverridden() ? 'inline' : 'none';
|
|
1722
|
+
}
|
|
1723
|
+
change.addEventListener('click', function (e) {
|
|
1724
|
+
e.stopPropagation();
|
|
1725
|
+
change.style.display = 'none';
|
|
1726
|
+
reset.style.display = 'none';
|
|
1727
|
+
startRebind(
|
|
1728
|
+
function (text, color) { chip.textContent = text; chip.style.color = color; },
|
|
1729
|
+
function () { refresh(); }
|
|
1730
|
+
);
|
|
1731
|
+
});
|
|
1732
|
+
reset.addEventListener('click', function (e) { e.stopPropagation(); resetActivate(); refresh(); });
|
|
1733
|
+
refresh();
|
|
1734
|
+
rrow.appendChild(chip); rrow.appendChild(lbl); rrow.appendChild(change); rrow.appendChild(reset);
|
|
1735
|
+
body.appendChild(rrow);
|
|
1736
|
+
})();
|
|
1737
|
+
|
|
1636
1738
|
rows.forEach(function (r) {
|
|
1637
1739
|
var row = document.createElement('div');
|
|
1638
1740
|
Object.assign(row.style, { display: 'flex', gap: '8px', marginBottom: '8px' });
|
|
@@ -1655,20 +1757,48 @@ function getClientScript(options) {
|
|
|
1655
1757
|
panelKeys.appendChild(body);
|
|
1656
1758
|
})();
|
|
1657
1759
|
|
|
1760
|
+
// Footer \u2014 a quiet link out to the GitHub README so first-timers can find the docs.
|
|
1761
|
+
var panelFoot = document.createElement('div');
|
|
1762
|
+
Object.assign(panelFoot.style, {
|
|
1763
|
+
flexShrink: '0', padding: '10px 16px', borderTop: '1px solid rgba(255,255,255,0.08)',
|
|
1764
|
+
display: 'flex', alignItems: 'center',
|
|
1765
|
+
});
|
|
1766
|
+
var docsLink = document.createElement('a');
|
|
1767
|
+
docsLink.href = 'https://github.com/setugk/vite-plugin-specter#readme';
|
|
1768
|
+
docsLink.target = '_blank';
|
|
1769
|
+
docsLink.rel = 'noopener noreferrer';
|
|
1770
|
+
docsLink.textContent = 'Documentation \\u2197'; // \u2197
|
|
1771
|
+
Object.assign(docsLink.style, { fontSize: '11px', fontWeight: '600', color: '#8AB4F8', textDecoration: 'none', cursor: 'pointer' });
|
|
1772
|
+
hoverFx(docsLink, { color: '#fff' }, { color: '#8AB4F8' });
|
|
1773
|
+
docsLink.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
1774
|
+
panelFoot.appendChild(docsLink);
|
|
1775
|
+
|
|
1658
1776
|
panelWrap.appendChild(panelHead);
|
|
1659
1777
|
panelWrap.appendChild(panelTools);
|
|
1660
1778
|
panelWrap.appendChild(panelHint);
|
|
1661
1779
|
panelWrap.appendChild(panelList);
|
|
1662
1780
|
panelWrap.appendChild(panelKeys);
|
|
1781
|
+
panelWrap.appendChild(panelFoot);
|
|
1663
1782
|
mount(panelWrap);
|
|
1664
1783
|
|
|
1665
|
-
//
|
|
1666
|
-
//
|
|
1667
|
-
//
|
|
1784
|
+
// A wheel anywhere over the panel must NEVER scroll the page behind it. Relying on
|
|
1785
|
+
// overscroll-behavior alone still lets trackpad momentum chain into the page at the
|
|
1786
|
+
// list bounds, so we take over: always swallow the event, and drive the list's own
|
|
1787
|
+
// scroll ourselves. (The inline note editor auto-grows instead of scrolling, so the
|
|
1788
|
+
// list is the only scrollable region inside the panel \u2014 nothing else needs the wheel.)
|
|
1789
|
+
var listScrolling = false, listScrollTimer = null;
|
|
1668
1790
|
panelWrap.addEventListener('wheel', function (e) {
|
|
1669
|
-
var canScroll = panelList.scrollHeight > panelList.clientHeight;
|
|
1670
|
-
if (panelList.contains(e.target) && canScroll) return; // let the list scroll natively
|
|
1671
1791
|
e.preventDefault();
|
|
1792
|
+
if (panelList.scrollHeight > panelList.clientHeight && panelList.contains(e.target)) {
|
|
1793
|
+
var dy = e.deltaMode === 1 ? e.deltaY * 16 : (e.deltaMode === 2 ? e.deltaY * panelList.clientHeight : e.deltaY);
|
|
1794
|
+
panelList.scrollTop += dy;
|
|
1795
|
+
// Rows slide under a stationary cursor as the list scrolls, firing mouseenter \u2192
|
|
1796
|
+
// revealSpec \u2192 scrollIntoView, which would drag the PAGE around. Mark the list as
|
|
1797
|
+
// actively scrolling so those hover-reveals are suppressed until scrolling settles.
|
|
1798
|
+
listScrolling = true;
|
|
1799
|
+
clearTimeout(listScrollTimer);
|
|
1800
|
+
listScrollTimer = setTimeout(function () { listScrolling = false; }, 200);
|
|
1801
|
+
}
|
|
1672
1802
|
}, { passive: false });
|
|
1673
1803
|
|
|
1674
1804
|
// \u2500\u2500 Auto-sync: mirror the browser's current Specs to the local bridge \u2500\u2500
|
|
@@ -1681,7 +1811,7 @@ function getClientScript(options) {
|
|
|
1681
1811
|
setPillSync(s); // control-bar dot
|
|
1682
1812
|
applyDotState(dot, s); // side-panel dot \u2014 same spinner/green/red
|
|
1683
1813
|
if (s === 'syncing') { dotLabel.textContent = 'Syncing\u2026'; }
|
|
1684
|
-
else if (s === 'synced') {
|
|
1814
|
+
else if (s === 'synced') { var open = specs.length - resolvedCount(); dotLabel.textContent = open > 0 ? (open + (open === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
|
|
1685
1815
|
else { dotLabel.textContent = 'Bridge offline'; }
|
|
1686
1816
|
}
|
|
1687
1817
|
function doSync() {
|
|
@@ -1728,11 +1858,28 @@ function getClientScript(options) {
|
|
|
1728
1858
|
panelList.appendChild(empty);
|
|
1729
1859
|
return;
|
|
1730
1860
|
}
|
|
1861
|
+
// Summary of resolved Specs + a one-click sweep, pinned above the list.
|
|
1862
|
+
var rc = resolvedCount();
|
|
1863
|
+
if (rc > 0) {
|
|
1864
|
+
var doneBar = document.createElement('div');
|
|
1865
|
+
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)' });
|
|
1866
|
+
var dlbl = document.createElement('span');
|
|
1867
|
+
dlbl.textContent = '\u2713 ' + rc + (rc === 1 ? ' done' : ' done');
|
|
1868
|
+
Object.assign(dlbl.style, { fontSize: '11px', color: GREEN, fontWeight: '700', letterSpacing: '0.04em', textTransform: 'uppercase' });
|
|
1869
|
+
var clr = document.createElement('span');
|
|
1870
|
+
clr.textContent = 'Clear done';
|
|
1871
|
+
Object.assign(clr.style, { fontSize: '11px', color: '#B9BBC2', cursor: 'pointer', textDecoration: 'underline' });
|
|
1872
|
+
clr.addEventListener('click', function (e) { e.stopPropagation(); clearResolved(); });
|
|
1873
|
+
hoverFx(clr, { color: '#fff' }, { color: '#B9BBC2' });
|
|
1874
|
+
doneBar.appendChild(dlbl); doneBar.appendChild(clr);
|
|
1875
|
+
panelList.appendChild(doneBar);
|
|
1876
|
+
}
|
|
1731
1877
|
var focusEditor = null, measures = [];
|
|
1732
1878
|
specs.forEach(function (spec, i) {
|
|
1733
1879
|
var missing = !!spec.missing;
|
|
1734
1880
|
var visible = missing ? false : specVisible(spec); // don't specVisible() a missing spec \u2014 it would re-anchor via path
|
|
1735
1881
|
var editing = (panelEditSpec === spec);
|
|
1882
|
+
var resolved = !!spec.resolved;
|
|
1736
1883
|
var row = document.createElement('div');
|
|
1737
1884
|
Object.assign(row.style, {
|
|
1738
1885
|
position: 'relative', display: 'flex', alignItems: 'flex-start', gap: '12px', boxSizing: 'border-box',
|
|
@@ -1743,7 +1890,7 @@ function getClientScript(options) {
|
|
|
1743
1890
|
badge.textContent = String(i + 1);
|
|
1744
1891
|
Object.assign(badge.style, {
|
|
1745
1892
|
flexShrink: '0', width: '20px', height: '20px', borderRadius: '999px',
|
|
1746
|
-
background: BADGE_IDLE, color: '#fff', fontSize: '11px', fontWeight: '700',
|
|
1893
|
+
background: resolved ? GREEN : BADGE_IDLE, color: '#fff', fontSize: '11px', fontWeight: '700',
|
|
1747
1894
|
border: '1px solid #fff', boxSizing: 'border-box',
|
|
1748
1895
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1749
1896
|
transition: 'background 0.12s ease',
|
|
@@ -1815,6 +1962,7 @@ function getClientScript(options) {
|
|
|
1815
1962
|
cursor: spec.note ? 'pointer' : 'default',
|
|
1816
1963
|
});
|
|
1817
1964
|
note.textContent = spec.note || (spec.kind === 'measure' ? '\u2B21 measurement' : '\u2014 no note \u2014');
|
|
1965
|
+
if (resolved) { note.style.textDecoration = 'line-through'; note.style.color = LABEL; note.style.cursor = spec.note ? 'pointer' : 'default'; }
|
|
1818
1966
|
|
|
1819
1967
|
// Floated into the top-right corner so they don't reserve note width. On hover
|
|
1820
1968
|
// they get a background that fades in from the left, masking the note text behind
|
|
@@ -1833,11 +1981,14 @@ function getClientScript(options) {
|
|
|
1833
1981
|
// Show an expand toggle only when the collapsed note actually overflows.
|
|
1834
1982
|
var chev = mkIcon(CHEV, expanded ? 'Collapse' : 'Expand', '#B9BBC2', function () { spec._expanded = !spec._expanded; renderPanel(); });
|
|
1835
1983
|
chev.firstChild.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
1984
|
+
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' });
|
|
1836
1985
|
var editBtn = mkIcon(PENCIL, 'Edit note', '#B9BBC2', function () { panelEditSpec = spec; spec._expanded = true; renderPanel(); });
|
|
1837
1986
|
var delBtn = mkIcon(TRASH, 'Delete Spec', '#ED8FA6', function () { removeSpec(spec); }, { background: 'rgba(237,62,97,0.30)', color: '#fff' });
|
|
1838
|
-
//
|
|
1839
|
-
// and fixed
|
|
1840
|
-
|
|
1987
|
+
// \u2713 / edit / delete all reveal only on row hover (the expand chevron stays rightmost
|
|
1988
|
+
// and fixed). The done state reads from the green badge + strikethrough + "\u2713 DONE",
|
|
1989
|
+
// so the row needs no persistent icon \u2014 it looks like any other list item.
|
|
1990
|
+
editBtn.style.display = delBtn.style.display = resolveBtn.style.display = 'none';
|
|
1991
|
+
actions.appendChild(resolveBtn);
|
|
1841
1992
|
actions.appendChild(editBtn);
|
|
1842
1993
|
actions.appendChild(delBtn);
|
|
1843
1994
|
actions.appendChild(chev);
|
|
@@ -1845,9 +1996,27 @@ function getClientScript(options) {
|
|
|
1845
1996
|
content.appendChild(note);
|
|
1846
1997
|
content.appendChild(meta);
|
|
1847
1998
|
|
|
1999
|
+
// Done: a green DONE tag + dimmed row. Supersedes HIDDEN/MISSING \u2014 once a Spec is
|
|
2000
|
+
// resolved you don't care whether its element is currently on screen.
|
|
2001
|
+
if (resolved) {
|
|
2002
|
+
row.style.opacity = '0.6';
|
|
2003
|
+
var rbar = document.createElement('div');
|
|
2004
|
+
Object.assign(rbar.style, { display: 'flex', alignItems: 'center', gap: '5px', marginTop: '2px', flexWrap: 'wrap' });
|
|
2005
|
+
var rcheck = document.createElement('span');
|
|
2006
|
+
rcheck.innerHTML = CHECK;
|
|
2007
|
+
Object.assign(rcheck.style, { display: 'inline-flex', alignItems: 'center', color: GREEN, flexShrink: '0' });
|
|
2008
|
+
if (rcheck.firstChild) { rcheck.firstChild.setAttribute('width', '12'); rcheck.firstChild.setAttribute('height', '12'); }
|
|
2009
|
+
var rtag = document.createElement('span');
|
|
2010
|
+
rtag.textContent = 'DONE';
|
|
2011
|
+
// Text-only status label (no fill/padding/radius) so it doesn't read as a clickable chip.
|
|
2012
|
+
Object.assign(rtag.style, { fontSize: '10px', fontWeight: '700', letterSpacing: '0.08em', textTransform: 'uppercase', color: GREEN, flexShrink: '0' });
|
|
2013
|
+
rbar.appendChild(rcheck);
|
|
2014
|
+
rbar.appendChild(rtag);
|
|
2015
|
+
content.appendChild(rbar);
|
|
2016
|
+
}
|
|
1848
2017
|
// Shared comment whose target is gone/changed: MISSING (greyed, reason shown,
|
|
1849
2018
|
// never drawn on the page \u2014 design: show missing, don't guess a spot).
|
|
1850
|
-
if (missing) {
|
|
2019
|
+
else if (missing) {
|
|
1851
2020
|
row.style.opacity = '0.7';
|
|
1852
2021
|
var mbar = document.createElement('div');
|
|
1853
2022
|
Object.assign(mbar.style, { display: 'flex', alignItems: 'center', gap: '7px', marginTop: '2px', flexWrap: 'wrap' });
|
|
@@ -1884,15 +2053,15 @@ function getClientScript(options) {
|
|
|
1884
2053
|
note.addEventListener('click', function (e) { if (spec.note) { e.stopPropagation(); spec._expanded = !spec._expanded; renderPanel(); } });
|
|
1885
2054
|
row.addEventListener('mouseenter', function () {
|
|
1886
2055
|
row.style.background = 'rgba(255,255,255,0.05)';
|
|
1887
|
-
badge.style.background = PURPLE;
|
|
1888
|
-
editBtn.style.display = delBtn.style.display = 'flex';
|
|
2056
|
+
if (!resolved) badge.style.background = PURPLE;
|
|
2057
|
+
editBtn.style.display = delBtn.style.display = resolveBtn.style.display = 'flex';
|
|
1889
2058
|
actions.style.background = 'linear-gradient(to right, rgba(52,54,60,0) 0, rgba(52,54,60,1) 22px)';
|
|
1890
|
-
if (visible) { highlightSpec = spec; revealSpec(spec); }
|
|
2059
|
+
if (visible && !listScrolling) { highlightSpec = spec; revealSpec(spec); } // don't reveal on rows that merely slid under the cursor while scrolling
|
|
1891
2060
|
});
|
|
1892
2061
|
row.addEventListener('mouseleave', function () {
|
|
1893
2062
|
row.style.background = 'transparent';
|
|
1894
|
-
badge.style.background = BADGE_IDLE;
|
|
1895
|
-
editBtn.style.display = delBtn.style.display = 'none';
|
|
2063
|
+
badge.style.background = resolved ? GREEN : BADGE_IDLE;
|
|
2064
|
+
editBtn.style.display = delBtn.style.display = resolveBtn.style.display = 'none';
|
|
1896
2065
|
actions.style.background = 'transparent';
|
|
1897
2066
|
if (highlightSpec === spec) highlightSpec = null;
|
|
1898
2067
|
});
|
|
@@ -2320,6 +2489,41 @@ function getClientScript(options) {
|
|
|
2320
2489
|
return eKey === key || eCode === 'key' + key || (key === 'period' && (eKey === '.' || eCode === 'period'));
|
|
2321
2490
|
}
|
|
2322
2491
|
|
|
2492
|
+
// Capture the next chord the user presses and make it the new toggle. Runs on the
|
|
2493
|
+
// capture phase + stops propagation so a rebind keystroke (e.g. "L") can't leak into
|
|
2494
|
+
// Specter's own shortcuts. Requires at least one modifier so the toggle can't be a bare
|
|
2495
|
+
// key that fires while you type. onState(text, color) drives the prompt; onDone(committed).
|
|
2496
|
+
var rebinding = false;
|
|
2497
|
+
function startRebind(onState, onDone) {
|
|
2498
|
+
if (rebinding) return;
|
|
2499
|
+
rebinding = true;
|
|
2500
|
+
onState('Press a key combo\u2026', GREEN);
|
|
2501
|
+
function cap(e) {
|
|
2502
|
+
e.preventDefault(); e.stopPropagation();
|
|
2503
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2504
|
+
var k = e.key;
|
|
2505
|
+
if (k === 'Escape') { finish(false); return; }
|
|
2506
|
+
if (k === 'Control' || k === 'Alt' || k === 'Shift' || k === 'Meta') return; // wait for the real key
|
|
2507
|
+
var parts = [];
|
|
2508
|
+
if (e.ctrlKey) parts.push('ctrl');
|
|
2509
|
+
if (e.altKey) parts.push('alt');
|
|
2510
|
+
if (e.shiftKey) parts.push('shift');
|
|
2511
|
+
if (e.metaKey) parts.push('meta');
|
|
2512
|
+
if (!parts.length) { onState('Hold Ctrl / Alt / \\u2318 too\u2026', '#F59E0B'); return; }
|
|
2513
|
+
var key = (e.key.length === 1 ? e.key : e.code.replace(/^Key/, '')).toLowerCase();
|
|
2514
|
+
if (!key) return;
|
|
2515
|
+
parts.push(key);
|
|
2516
|
+
setActivate(parts.join('+'));
|
|
2517
|
+
finish(true);
|
|
2518
|
+
}
|
|
2519
|
+
function finish(committed) {
|
|
2520
|
+
document.removeEventListener('keydown', cap, true);
|
|
2521
|
+
rebinding = false;
|
|
2522
|
+
onDone(committed);
|
|
2523
|
+
}
|
|
2524
|
+
document.addEventListener('keydown', cap, true);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2323
2527
|
// \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
|
|
2324
2528
|
function onMouseMove(e) {
|
|
2325
2529
|
if (!fiActive) return;
|
|
@@ -2501,7 +2705,21 @@ function getClientScript(options) {
|
|
|
2501
2705
|
importFromHash(); // if arrived via a #spx= share link, import + reveal the shared comments
|
|
2502
2706
|
scheduleSync(); // mirror restored Specs to the Claude bridge on load
|
|
2503
2707
|
|
|
2504
|
-
console
|
|
2708
|
+
// Dev-only console API \u2014 the escape hatch when the toggle chord collides with a browser
|
|
2709
|
+
// shortcut so you can't even open the overlay. Specter is stripped from prod, so this
|
|
2710
|
+
// global never ships. Vite devs can run these straight from DevTools.
|
|
2711
|
+
try {
|
|
2712
|
+
window.__specter = {
|
|
2713
|
+
toggle: function () { if (fiActive) deactivate(); else activate(); },
|
|
2714
|
+
activate: activate,
|
|
2715
|
+
deactivate: deactivate,
|
|
2716
|
+
get shortcut() { return ACTIVATE; },
|
|
2717
|
+
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; },
|
|
2718
|
+
resetShortcut: function () { resetActivate(); if (panelOpen) renderPanel(); console.log('%c\u{1F47B} Specter \u2014 toggle reset to ' + shortcutLabel(ACTIVATE), 'color:#aaa'); return ACTIVATE; },
|
|
2719
|
+
};
|
|
2720
|
+
} catch (e) {}
|
|
2721
|
+
|
|
2722
|
+
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;');
|
|
2505
2723
|
})();`;
|
|
2506
2724
|
}
|
|
2507
2725
|
|