@autono/pinbox-toolbar 0.12.0 → 0.14.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/dist/{index-DK_kVd2Z.d.ts → index-BVd-ZD4Y.d.ts} +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{src-C-gslo8y.js → src-GX3kPe0X.js} +342 -116
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.js +1 -1
- package/dist/toolbar.iife.js +342 -116
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +1 -1
- package/package.json +2 -2
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
//#region src/anchor-watch.ts
|
|
2
|
+
function watchAnchors(win, onChange) {
|
|
3
|
+
let frame = 0;
|
|
4
|
+
const schedule = () => {
|
|
5
|
+
if (frame !== 0) return;
|
|
6
|
+
frame = win.requestAnimationFrame(() => {
|
|
7
|
+
frame = 0;
|
|
8
|
+
onChange();
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
const observer = new (win.MutationObserver ?? MutationObserver)(schedule);
|
|
12
|
+
observer.observe(win.document.body, {
|
|
13
|
+
childList: true,
|
|
14
|
+
subtree: true
|
|
15
|
+
});
|
|
16
|
+
win.addEventListener("popstate", schedule);
|
|
17
|
+
return { destroy() {
|
|
18
|
+
observer.disconnect();
|
|
19
|
+
win.removeEventListener("popstate", schedule);
|
|
20
|
+
if (frame !== 0) win.cancelAnimationFrame(frame);
|
|
21
|
+
frame = 0;
|
|
22
|
+
} };
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
1
25
|
//#region src/targeting/dom.ts
|
|
2
26
|
/**
|
|
3
27
|
* Deepest element under (clientX, clientY) that the caller does not ignore, or null when there is
|
|
@@ -270,9 +294,10 @@ function threadTail(thread) {
|
|
|
270
294
|
function block(pin, thread) {
|
|
271
295
|
const { selector, url, source } = pin.target ?? {};
|
|
272
296
|
return [
|
|
273
|
-
`## Pin ${pin.id} —
|
|
297
|
+
`## Pin ${pin.n === void 0 ? pin.id : `#${pin.n} (${pin.id})`} — ${pin.status.toUpperCase()}`,
|
|
274
298
|
`- label: ${line(label(pin))}`,
|
|
275
299
|
...selector === void 0 ? [] : [`- selector: \`${line(selector)}\``],
|
|
300
|
+
...(pin.target?.targets ?? []).map((t) => t.selector ?? t.anchor ?? t.tag).filter((locus) => locus !== void 0).map((locus) => `- also: \`${line(locus)}\``),
|
|
276
301
|
...source === void 0 ? [] : [`- source: ${line(source.line === void 0 ? source.file : `${source.file}:${source.line}`)}`],
|
|
277
302
|
...url === void 0 ? [] : [`- url: ${line(url)}`],
|
|
278
303
|
"",
|
|
@@ -286,6 +311,11 @@ function pinsToMarkdown(pins, threads) {
|
|
|
286
311
|
if (open.length === 0) return "No open pins.\n";
|
|
287
312
|
return `${open.map((p) => block(p, threads.get(p.id) ?? [])).join("\n\n")}\n`;
|
|
288
313
|
}
|
|
314
|
+
/** One pin's block — the card's per-pin copy (dogfood: "I want to copy an
|
|
315
|
+
* individual pin"); any status, since you copy exactly what you're looking at. */
|
|
316
|
+
function pinToMarkdown(pin, thread) {
|
|
317
|
+
return `${block(pin, thread)}\n`;
|
|
318
|
+
}
|
|
289
319
|
//#endregion
|
|
290
320
|
//#region src/motion/spring.ts
|
|
291
321
|
/** Bar ⇄ puck morphs and the post-drag settle. */
|
|
@@ -837,6 +867,39 @@ async function firstFrame(win, stream) {
|
|
|
837
867
|
});
|
|
838
868
|
return video;
|
|
839
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* The one live capture stream, reused across pins. getDisplayMedia MUST
|
|
872
|
+
* prompt on every call (spec — no persistent grant exists), so the only way
|
|
873
|
+
* to stop asking per pin is to never re-call it: prompt once, keep the track,
|
|
874
|
+
* and read a fresh frame from the still-playing video for every capture.
|
|
875
|
+
* The browser's "sharing this tab" indicator stays on while the track lives —
|
|
876
|
+
* honest, and the user ending it from there simply re-prompts on the next pin.
|
|
877
|
+
*/
|
|
878
|
+
let liveCapture = null;
|
|
879
|
+
async function captureVideo(win, media) {
|
|
880
|
+
const track = liveCapture?.stream.getVideoTracks()[0];
|
|
881
|
+
if (liveCapture && track?.readyState === "live") return liveCapture.video;
|
|
882
|
+
releaseCapture();
|
|
883
|
+
const stream = await media.getDisplayMedia({
|
|
884
|
+
video: true,
|
|
885
|
+
audio: false,
|
|
886
|
+
preferCurrentTab: true
|
|
887
|
+
});
|
|
888
|
+
const video = await firstFrame(win, stream);
|
|
889
|
+
stream.getVideoTracks()[0]?.addEventListener("ended", releaseCapture, { once: true });
|
|
890
|
+
liveCapture = {
|
|
891
|
+
stream,
|
|
892
|
+
video
|
|
893
|
+
};
|
|
894
|
+
return video;
|
|
895
|
+
}
|
|
896
|
+
/** Stop the cached stream (toolbar disconnect; also the track-ended handler). */
|
|
897
|
+
function releaseCapture() {
|
|
898
|
+
if (liveCapture === null) return;
|
|
899
|
+
for (const t of liveCapture.stream.getTracks()) t.stop();
|
|
900
|
+
liveCapture.video.srcObject = null;
|
|
901
|
+
liveCapture = null;
|
|
902
|
+
}
|
|
840
903
|
/** webp-encode a bitmap; also emit the ≤32px placeholder data URL. */
|
|
841
904
|
async function encode(bmp) {
|
|
842
905
|
const canvas = new OffscreenCanvas(bmp.width, bmp.height);
|
|
@@ -876,21 +939,14 @@ async function captureElement(el) {
|
|
|
876
939
|
const crop = visibleCropRect(el);
|
|
877
940
|
if (source === null || crop === null) return null;
|
|
878
941
|
const { win, media } = source;
|
|
879
|
-
let stream = null;
|
|
880
942
|
try {
|
|
881
|
-
|
|
882
|
-
video: true,
|
|
883
|
-
audio: false,
|
|
884
|
-
preferCurrentTab: true
|
|
885
|
-
});
|
|
886
|
-
const video = await firstFrame(win, stream);
|
|
943
|
+
const video = await captureVideo(win, media);
|
|
887
944
|
const sx = video.videoWidth / win.innerWidth;
|
|
888
945
|
const sy = video.videoHeight / win.innerHeight;
|
|
889
946
|
return await encode(await createImageBitmap(video, Math.round(crop.x * sx), Math.round(crop.y * sy), Math.max(1, Math.round(crop.width * sx)), Math.max(1, Math.round(crop.height * sy))));
|
|
890
947
|
} catch {
|
|
948
|
+
releaseCapture();
|
|
891
949
|
return null;
|
|
892
|
-
} finally {
|
|
893
|
-
if (stream) for (const track of stream.getTracks()) track.stop();
|
|
894
950
|
}
|
|
895
951
|
}
|
|
896
952
|
/**
|
|
@@ -926,7 +982,8 @@ function initialState() {
|
|
|
926
982
|
inboxOpen: false,
|
|
927
983
|
connection: "connecting",
|
|
928
984
|
queuedIds: /* @__PURE__ */ new Set(),
|
|
929
|
-
minimized: false
|
|
985
|
+
minimized: false,
|
|
986
|
+
pinsHidden: false
|
|
930
987
|
};
|
|
931
988
|
}
|
|
932
989
|
function createStore() {
|
|
@@ -953,7 +1010,8 @@ function createStore() {
|
|
|
953
1010
|
...state,
|
|
954
1011
|
draft,
|
|
955
1012
|
mode: "idle",
|
|
956
|
-
activePinId: null
|
|
1013
|
+
activePinId: null,
|
|
1014
|
+
pinsHidden: false
|
|
957
1015
|
});
|
|
958
1016
|
},
|
|
959
1017
|
discardDraft() {
|
|
@@ -1618,8 +1676,8 @@ function createAim(doc, handlers) {
|
|
|
1618
1676
|
//#region src/ui/bar.ts
|
|
1619
1677
|
const PIN_ICON = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"3\" y=\"1.5\" width=\"10\" height=\"6.5\" rx=\"1\"/><path d=\"M8 8v6.5\"/></svg>";
|
|
1620
1678
|
const INBOX_ICON = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M1.8 8.5h3.4l1 2h3.6l1-2h3.4\"/><path d=\"M2.6 3.2h10.8l1.2 5.3v4a1 1 0 01-1 1H2.4a1 1 0 01-1-1v-4z\"/></svg>";
|
|
1621
|
-
const THEME_ICON = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M8
|
|
1622
|
-
const COPY_ICON = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8\" rx=\"1\"/><path d=\"M10.5 3.5v-1a1 1 0 00-1-1h-6a1 1 0 00-1 1v6a1 1 0 001 1h1\"/></svg>";
|
|
1679
|
+
const THEME_ICON = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M8 2a4 4 0 0 0 6 6 6 6 0 1 1-6-6z\"/></svg>";
|
|
1680
|
+
const COPY_ICON$1 = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8\" rx=\"1\"/><path d=\"M10.5 3.5v-1a1 1 0 00-1-1h-6a1 1 0 00-1 1v6a1 1 0 001 1h1\"/></svg>";
|
|
1623
1681
|
const IDENT_ICON = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"var(--pb-amber)\" stroke-width=\"1.4\"><rect x=\"2.5\" y=\"1.5\" width=\"11\" height=\"7\" rx=\"1\"/><path d=\"M8 8.5v6\"/><circle cx=\"8\" cy=\"14.6\" r=\".9\" fill=\"var(--pb-amber)\" stroke=\"none\"/></svg>";
|
|
1624
1682
|
const MIN_ICON = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M6.5 2.5v4h-4\"/><path d=\"M9.5 13.5v-4h4\"/></svg>";
|
|
1625
1683
|
const CONNECTION_LABEL = {
|
|
@@ -1631,7 +1689,7 @@ const CONNECTION_LABEL = {
|
|
|
1631
1689
|
function createBar(doc, on) {
|
|
1632
1690
|
const root = doc.createElement("div");
|
|
1633
1691
|
root.className = "pb-bar";
|
|
1634
|
-
root.innerHTML = `<div class="armed-ring"></div><div class="ident">${IDENT_ICON}<span class="bl" data-ref="label">PINBOX</span></div><div class="div"></div><button type="button" class="pb-tb" data-ref="pin" title="Pin (P)">${PIN_ICON}PIN</button><button type="button" class="pb-tb" data-ref="inbox" title="Inbox (I)">${INBOX_ICON}<span data-ref="count">0</span></button><div class="div" style="margin:0 3px"></div><button type="button" class="pb-tb sq" data-ref="copy" title="Copy open pins (C)">${COPY_ICON}</button><button type="button" class="pb-tb sq" data-ref="theme" title="Theme (D)">${THEME_ICON}</button><button type="button" class="pb-tb sq" data-ref="help" title="Shortcuts (?)">?</button><button type="button" class="pb-tb sq" data-ref="min" title="Minimize (M)" aria-label="Minimize toolbar">${MIN_ICON}</button>`;
|
|
1692
|
+
root.innerHTML = `<div class="armed-ring"></div><div class="ident">${IDENT_ICON}<span class="bl" data-ref="label">PINBOX</span></div><div class="div"></div><button type="button" class="pb-tb" data-ref="pin" title="Pin (P)">${PIN_ICON}PIN</button><button type="button" class="pb-tb" data-ref="inbox" title="Inbox (I)">${INBOX_ICON}<span data-ref="count">0</span></button><div class="div" style="margin:0 3px"></div><button type="button" class="pb-tb sq" data-ref="copy" title="Copy open pins (C)">${COPY_ICON$1}</button><button type="button" class="pb-tb sq" data-ref="theme" title="Theme (D)">${THEME_ICON}</button><button type="button" class="pb-tb sq" data-ref="help" title="Shortcuts (?)">?</button><button type="button" class="pb-tb sq" data-ref="min" title="Minimize (M)" aria-label="Minimize toolbar">${MIN_ICON}</button>`;
|
|
1635
1693
|
const ref = (name) => root.querySelector(`[data-ref="${name}"]`);
|
|
1636
1694
|
const label = ref("label");
|
|
1637
1695
|
const pinBtn = ref("pin");
|
|
@@ -1685,6 +1743,144 @@ function pinNumber(n) {
|
|
|
1685
1743
|
return String(n).padStart(2, "0");
|
|
1686
1744
|
}
|
|
1687
1745
|
//#endregion
|
|
1746
|
+
//#region src/ui/pins.ts
|
|
1747
|
+
/** The prototype's `_h` innerHTML memo, kept off the DOM node. */
|
|
1748
|
+
const chipMemo = /* @__PURE__ */ new WeakMap();
|
|
1749
|
+
/** What the hub will number the next pin: max known `n`, else the pin count. */
|
|
1750
|
+
function nextOrdinal(pins) {
|
|
1751
|
+
return Math.max(pins.length, ...pins.map((p) => p.n ?? 0)) + 1;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Does the pin's captured URL still describe the view on screen? Path + search
|
|
1755
|
+
* only — hashes are anchors, not views. An absent or unparseable URL never
|
|
1756
|
+
* gates: old pins (and CLI pins) keep rendering exactly as before.
|
|
1757
|
+
*/
|
|
1758
|
+
function sameView(win, url) {
|
|
1759
|
+
if (url === void 0) return true;
|
|
1760
|
+
try {
|
|
1761
|
+
const target = new URL(url, win.location.href);
|
|
1762
|
+
return target.pathname === win.location.pathname && target.search === win.location.search;
|
|
1763
|
+
} catch {
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Where the pin's anchor is NOW (dogfood #26: markers lingered over unrelated
|
|
1769
|
+
* views after SPA tab switches, because placement trusted the stored rect
|
|
1770
|
+
* forever). Re-resolve the captured selector on every render:
|
|
1771
|
+
* - it resolves with layout → snap to the LIVE rect (also fixes drift);
|
|
1772
|
+
* - it resolves without layout (test DOMs, display:none) → stored rect;
|
|
1773
|
+
* - it does not resolve → no marker; the drawer stays the see-everything list.
|
|
1774
|
+
* A pin with no selector (terminal-adjacent) keeps its stored rect, as before.
|
|
1775
|
+
*/
|
|
1776
|
+
function anchorRect(layer, pin) {
|
|
1777
|
+
const stored = pin.target?.rect;
|
|
1778
|
+
if (stored === void 0) return null;
|
|
1779
|
+
const doc = layer.ownerDocument;
|
|
1780
|
+
const win = doc.defaultView;
|
|
1781
|
+
if (win === null) return stored;
|
|
1782
|
+
if (!sameView(win, pin.target?.url)) return null;
|
|
1783
|
+
const selector = pin.target?.selector;
|
|
1784
|
+
if (selector === void 0) return stored;
|
|
1785
|
+
let el;
|
|
1786
|
+
try {
|
|
1787
|
+
el = doc.querySelector(selector);
|
|
1788
|
+
} catch {
|
|
1789
|
+
return stored;
|
|
1790
|
+
}
|
|
1791
|
+
if (el === null) return null;
|
|
1792
|
+
const r = el.getBoundingClientRect();
|
|
1793
|
+
if (r.width <= 0 && r.height <= 0) return stored;
|
|
1794
|
+
return {
|
|
1795
|
+
x: r.left + win.scrollX,
|
|
1796
|
+
y: r.top + win.scrollY,
|
|
1797
|
+
width: r.width,
|
|
1798
|
+
height: r.height
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Where the needle lands: the point inside the element that was actually clicked, when the pin
|
|
1803
|
+
* recorded one, else the centre of its box.
|
|
1804
|
+
*
|
|
1805
|
+
* `spot` is a fraction of the element, so the pin still tracks the element when it moves or
|
|
1806
|
+
* resizes — it just stops sliding to the middle of a wide block the moment you commit it.
|
|
1807
|
+
*/
|
|
1808
|
+
function pinPoint(r, spot) {
|
|
1809
|
+
const fx = spot?.x ?? .5;
|
|
1810
|
+
const fy = spot?.y ?? .5;
|
|
1811
|
+
return {
|
|
1812
|
+
x: r.x + r.width * fx,
|
|
1813
|
+
y: r.y + r.height * fy
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
/** Chip contents (prototype chipBtnInner, lines 546–550): number + linked-channel tag,
|
|
1817
|
+
* plus the queued badge while the pin waits in the outbox for the reconnect flush. */
|
|
1818
|
+
function chipInner(n, pin, queued = false) {
|
|
1819
|
+
const link = pin?.links?.[0];
|
|
1820
|
+
const badge = link ? `<span class="lk"><span>${esc(link.connector)}</span></span>` : "";
|
|
1821
|
+
const qd = queued ? "<span class=\"qd\">QUEUED</span>" : "";
|
|
1822
|
+
return `<span>${pinNumber(n)}</span>${badge}${qd}`;
|
|
1823
|
+
}
|
|
1824
|
+
function ensureNode(layer, key, fresh) {
|
|
1825
|
+
let node = layer.querySelector(`[data-pin="${key}"]`);
|
|
1826
|
+
if (!node) {
|
|
1827
|
+
node = layer.ownerDocument.createElement("div");
|
|
1828
|
+
node.className = "pb-pin";
|
|
1829
|
+
node.setAttribute("data-pin", key);
|
|
1830
|
+
node.innerHTML = `${fresh ? "<div class=\"ring\"></div>" : ""}<div class="dot"></div><div class="needle"></div><button type="button" class="pb-chipBtn" data-open="${esc(key)}"></button>`;
|
|
1831
|
+
layer.appendChild(node);
|
|
1832
|
+
}
|
|
1833
|
+
return node;
|
|
1834
|
+
}
|
|
1835
|
+
function patchNode(node, at, hot, inner) {
|
|
1836
|
+
node.style.left = `${at.x}px`;
|
|
1837
|
+
node.style.top = `${at.y}px`;
|
|
1838
|
+
node.style.zIndex = hot ? "40" : "20";
|
|
1839
|
+
node.classList.toggle("hot", hot);
|
|
1840
|
+
const chip = node.querySelector(".pb-chipBtn");
|
|
1841
|
+
if (chip && chipMemo.get(chip) !== inner) {
|
|
1842
|
+
chip.innerHTML = inner;
|
|
1843
|
+
chipMemo.set(chip, inner);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Render the pin layer for a state snapshot. Visible pins are open pins, the
|
|
1848
|
+
* active pin regardless of status, and the client-only draft (key "draft").
|
|
1849
|
+
*/
|
|
1850
|
+
function renderPins(layer, state) {
|
|
1851
|
+
layer.hidden = state.pinsHidden;
|
|
1852
|
+
if (state.pinsHidden) return;
|
|
1853
|
+
const visible = state.pins.filter((p) => p.status !== "resolved" || p.id === state.activePinId);
|
|
1854
|
+
const placed = [];
|
|
1855
|
+
visible.forEach((pin, i) => {
|
|
1856
|
+
const rect = anchorRect(layer, pin);
|
|
1857
|
+
if (rect === null) return;
|
|
1858
|
+
const spot = pin.target?.spot;
|
|
1859
|
+
const n = pin.n ?? i + 1;
|
|
1860
|
+
placed.push(spot === void 0 ? {
|
|
1861
|
+
pin,
|
|
1862
|
+
n,
|
|
1863
|
+
rect
|
|
1864
|
+
} : {
|
|
1865
|
+
pin,
|
|
1866
|
+
n,
|
|
1867
|
+
rect,
|
|
1868
|
+
spot
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
const keys = new Set(placed.map((entry) => entry.pin.id));
|
|
1872
|
+
if (state.draft) keys.add("draft");
|
|
1873
|
+
for (const node of [...layer.children]) if (!keys.has(node.getAttribute("data-pin") ?? "")) node.remove();
|
|
1874
|
+
for (const { pin, n, rect, spot } of placed) {
|
|
1875
|
+
const node = ensureNode(layer, pin.id, false);
|
|
1876
|
+
const hot = pin.id === state.activePinId;
|
|
1877
|
+
const queued = state.queuedIds.has(pin.id);
|
|
1878
|
+
node.classList.toggle("queued", queued);
|
|
1879
|
+
patchNode(node, pinPoint(rect, spot), hot, chipInner(n, pin, queued));
|
|
1880
|
+
}
|
|
1881
|
+
if (state.draft) patchNode(ensureNode(layer, "draft", true), state.draft.placedAt, true, chipInner(nextOrdinal(state.pins), null));
|
|
1882
|
+
}
|
|
1883
|
+
//#endregion
|
|
1688
1884
|
//#region src/ui/card.ts
|
|
1689
1885
|
const STATUS_LABEL = {
|
|
1690
1886
|
open: "OPEN",
|
|
@@ -1695,6 +1891,7 @@ const STATUS_LABEL = {
|
|
|
1695
1891
|
};
|
|
1696
1892
|
const CHECK_ICON = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 8.5l3.2 3.2L13 4.8\"/></svg>";
|
|
1697
1893
|
const X_ICON$1 = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>";
|
|
1894
|
+
const COPY_ICON = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8\" rx=\"1\"/><path d=\"M10.5 3.5v-1a1 1 0 00-1-1h-6a1 1 0 00-1 1v6a1 1 0 001 1h1\"/></svg>";
|
|
1698
1895
|
const ctxByCard = /* @__PURE__ */ new WeakMap();
|
|
1699
1896
|
/** The prototype's `_h` innerHTML memo, kept off the DOM node. */
|
|
1700
1897
|
const nodeMemo = /* @__PURE__ */ new WeakMap();
|
|
@@ -1716,8 +1913,15 @@ function attachmentsHtml(m) {
|
|
|
1716
1913
|
if (!m.attachments?.length) return "";
|
|
1717
1914
|
return `<div class="atts">${m.attachments.map((att) => isImage(att) ? `<span class="pb-att"><img src="${esc(safeUrl(att.url ?? att.path ?? ""))}" alt="${esc(fileName(att))}" loading="lazy"></span>` : `<span class="pb-att-chip">${esc(fileName(att))}</span>`).join("")}</div>`;
|
|
1718
1915
|
}
|
|
1916
|
+
/** "claude:lark-mac-agent" → "Claude · lark-mac-agent"; other shapes verbatim. */
|
|
1917
|
+
function agentName(origin) {
|
|
1918
|
+
const idx = origin.indexOf(":");
|
|
1919
|
+
if (idx <= 0) return origin;
|
|
1920
|
+
const agent = origin.slice(0, idx);
|
|
1921
|
+
return `${agent.charAt(0).toUpperCase()}${agent.slice(1)} · ${origin.slice(idx + 1)}`;
|
|
1922
|
+
}
|
|
1719
1923
|
function messageHtml(m) {
|
|
1720
|
-
if (m.role === "agent") return `<div class="pb-msg"><div class="pb-av agent">AI</div><div class="col"><div class="line"><span class="who"
|
|
1924
|
+
if (m.role === "agent") return `<div class="pb-msg"><div class="pb-av agent">AI</div><div class="col"><div class="line"><span class="who">${esc(m.origin === void 0 ? "Agent" : agentName(m.origin))}</span><span class="tm">${esc(timeOf(m.at))}</span></div><div class="txt">${esc(m.text)}</div>${attachmentsHtml(m)}</div></div>`;
|
|
1721
1925
|
const mirror = m.role === "mirror";
|
|
1722
1926
|
const origin = mirror ? m.origin ?? "mirror" : null;
|
|
1723
1927
|
const who = origin ? origin.split(":")[1] ?? origin : "You";
|
|
@@ -1804,7 +2008,14 @@ function onCardClick(card, ctx, e) {
|
|
|
1804
2008
|
else if (action === "close") ctx.actions.close();
|
|
1805
2009
|
else if (ctx.pid !== "draft") {
|
|
1806
2010
|
if (action === "resolve") ctx.actions.resolve(ctx.pid);
|
|
1807
|
-
else if (action === "
|
|
2011
|
+
else if (action === "copy") {
|
|
2012
|
+
ctx.actions.copy(ctx.pid);
|
|
2013
|
+
const btn = e.target.closest?.("[data-action=\"copy\"]");
|
|
2014
|
+
if (btn) {
|
|
2015
|
+
btn.classList.add("ok");
|
|
2016
|
+
card.ownerDocument.defaultView?.setTimeout(() => btn.classList.remove("ok"), 900);
|
|
2017
|
+
}
|
|
2018
|
+
} else if (action === "verify-accept") ctx.actions.verify(ctx.pid, "accepted");
|
|
1808
2019
|
else if (action === "verify-reopen") {
|
|
1809
2020
|
ctx.actions.verify(ctx.pid, "reopened");
|
|
1810
2021
|
card.querySelector("textarea")?.focus();
|
|
@@ -1812,7 +2023,7 @@ function onCardClick(card, ctx, e) {
|
|
|
1812
2023
|
}
|
|
1813
2024
|
}
|
|
1814
2025
|
function buildSkeleton(card, ctx, isDraft, hasThread) {
|
|
1815
|
-
card.innerHTML = "<div class=\"in\"><div class=\"pb-hd\" data-ref=\"hd\"></div><div data-ref=\"link\"></div><div class=\"pb-thread\" data-ref=\"thread\"></div><div data-ref=\"verify\"></div><div class=\"pb-composer\"><textarea rows=\"2\"></textarea><div class=\"row\" data-ref=\"row\"></div></div></div>";
|
|
2026
|
+
card.innerHTML = "<div class=\"in\"><div class=\"pb-hd\" data-ref=\"hd\"></div><div data-ref=\"link\"></div><div data-ref=\"loci\"></div><div class=\"pb-thread\" data-ref=\"thread\"></div><div data-ref=\"verify\"></div><div class=\"pb-composer\"><textarea rows=\"2\"></textarea><div class=\"row\" data-ref=\"row\"></div></div></div>";
|
|
1816
2027
|
const ta = card.querySelector("textarea");
|
|
1817
2028
|
ta.placeholder = hasThread ? "Ask a question or request a change…" : "What should change here?";
|
|
1818
2029
|
ta.addEventListener("keydown", (e) => {
|
|
@@ -1828,8 +2039,20 @@ function buildSkeleton(card, ctx, isDraft, hasThread) {
|
|
|
1828
2039
|
}, true);
|
|
1829
2040
|
if (isDraft) ta.focus();
|
|
1830
2041
|
}
|
|
1831
|
-
function hdHtml(n, targetLabel, status, resolvable) {
|
|
1832
|
-
return `<div class="meta"><span class="num">${pinNumber(n)}</span><span>${esc(targetLabel)}</span><span class="st">${esc(status)}</span></div><div style="display:flex;gap:2px">` + (resolvable ? `<button type="button" class="pb-ico ok" data-action="resolve" title="Resolve (R)">${CHECK_ICON}</button>` : "") + `<button type="button" class="pb-ico" data-action="close" title="Close (Esc)">${X_ICON$1}</button></div>`;
|
|
2042
|
+
function hdHtml(n, targetLabel, status, resolvable, copyable) {
|
|
2043
|
+
return `<div class="meta"><span class="num">${pinNumber(n)}</span><span>${esc(targetLabel)}</span><span class="st">${esc(status)}</span></div><div style="display:flex;gap:2px">` + (copyable ? `<button type="button" class="pb-ico" data-action="copy" title="Copy this pin">${COPY_ICON}</button>` : "") + (resolvable ? `<button type="button" class="pb-ico ok" data-action="resolve" title="Resolve (R)">${CHECK_ICON}</button>` : "") + `<button type="button" class="pb-ico" data-action="close" title="Close (Esc)">${X_ICON$1}</button></div>`;
|
|
2044
|
+
}
|
|
2045
|
+
/** One name per locus: selector first, else anchor, else tag. */
|
|
2046
|
+
function locusName(t) {
|
|
2047
|
+
return t.selector ?? t.anchor ?? t.tag?.toUpperCase();
|
|
2048
|
+
}
|
|
2049
|
+
/** The extra loci of a multi-target pin — the anchor leads, extras follow. */
|
|
2050
|
+
function lociHtml(pin) {
|
|
2051
|
+
const target = pin?.target;
|
|
2052
|
+
const extras = target?.targets;
|
|
2053
|
+
if (target === void 0 || extras === void 0 || extras.length === 0) return "";
|
|
2054
|
+
const names = [target, ...extras].map((t) => esc(locusName(t) ?? "?"));
|
|
2055
|
+
return `<div class="pb-loci">${names.length} targets: ${names.join(" · ")}</div>`;
|
|
1833
2056
|
}
|
|
1834
2057
|
/** Link badge: pin.links[0] read-only — no picker, no unlink yet. */
|
|
1835
2058
|
function linkHtml(pin) {
|
|
@@ -1838,8 +2061,9 @@ function linkHtml(pin) {
|
|
|
1838
2061
|
return `<div class="pb-linkbar"><span class="ch">${esc(link.connector)}</span><span class="mt">${esc(link.ref)}</span><span class="sp"></span><a class="pb-open" href="${esc(safeUrl(link.url))}" target="_blank" rel="noreferrer">OPEN</a></div>`;
|
|
1839
2062
|
}
|
|
1840
2063
|
function verifyHtml(status) {
|
|
1841
|
-
if (status
|
|
1842
|
-
|
|
2064
|
+
if (status === "verify") return "<div class=\"pb-verify\"><button type=\"button\" class=\"pb-bt-ok\" data-action=\"verify-accept\">Looks good</button><button type=\"button\" class=\"pb-bt-ghost\" data-action=\"verify-reopen\">Reopen</button></div>";
|
|
2065
|
+
if (status === "resolved") return "<div class=\"pb-verify\"><button type=\"button\" class=\"pb-bt-ghost\" data-action=\"verify-reopen\">Unresolve</button></div>";
|
|
2066
|
+
return "";
|
|
1843
2067
|
}
|
|
1844
2068
|
function rowHtml(hasThread) {
|
|
1845
2069
|
return `<div class="pb-kbd">⌘ ↵</div><button type="button" class="pb-bt-solid" data-action="send">${hasThread ? "Reply" : "Comment"}</button>`;
|
|
@@ -1876,10 +2100,11 @@ function activePin(state) {
|
|
|
1876
2100
|
if (!state.activePinId) return null;
|
|
1877
2101
|
return state.pins.find((p) => p.id === state.activePinId) ?? null;
|
|
1878
2102
|
}
|
|
1879
|
-
/**
|
|
2103
|
+
/** The pin's hub-born number; visible-index only for pre-`n` pins, drafts next up. */
|
|
1880
2104
|
function ordinalOf(state, pin) {
|
|
1881
|
-
|
|
1882
|
-
|
|
2105
|
+
if (pin === null) return nextOrdinal(state.pins);
|
|
2106
|
+
if (pin.n !== void 0) return pin.n;
|
|
2107
|
+
return state.pins.filter((p) => p.status !== "resolved" || p.id === state.activePinId).indexOf(pin) + 1;
|
|
1883
2108
|
}
|
|
1884
2109
|
function anchorOf(pin, draft) {
|
|
1885
2110
|
const r = pin?.target?.rect;
|
|
@@ -1950,8 +2175,9 @@ function renderCard(root, state, actions) {
|
|
|
1950
2175
|
const queued = view.pin !== null && state.queuedIds.has(view.pin.id);
|
|
1951
2176
|
const statusLabel = queued ? "QUEUED" : view.status ? STATUS_LABEL[view.status] : "NEW";
|
|
1952
2177
|
const resolvable = view.pin?.status === "open" && !queued;
|
|
1953
|
-
setPart(card, ctx, "hd", hdHtml(view.n, view.label, statusLabel, resolvable));
|
|
2178
|
+
setPart(card, ctx, "hd", hdHtml(view.n, view.label, statusLabel, resolvable, view.pin !== null));
|
|
1954
2179
|
setPart(card, ctx, "link", linkHtml(view.pin));
|
|
2180
|
+
setPart(card, ctx, "loci", lociHtml(view.pin));
|
|
1955
2181
|
setPart(card, ctx, "verify", verifyHtml(view.status));
|
|
1956
2182
|
const messages = view.pin === null ? view.thread : [pinAsMessage(view.pin), ...view.thread];
|
|
1957
2183
|
setPart(card, ctx, "row", rowHtml(messages.length > 0));
|
|
@@ -2023,7 +2249,7 @@ function createDrawer(doc, on) {
|
|
|
2023
2249
|
doneTab.classList.toggle("on", tab === "resolved");
|
|
2024
2250
|
}
|
|
2025
2251
|
const list = tab === "open" ? open : resolved;
|
|
2026
|
-
const html = list.length ? list.map((p) => itemHtml(p, state.pins.indexOf(p) + 1, p.id === state.activePinId, state.threads.get(p.id) ?? [], state.queuedIds.has(p.id))).join("") : "<div class=\"pb-empty\">Nothing here yet.</div>";
|
|
2252
|
+
const html = list.length ? list.map((p) => itemHtml(p, p.n ?? state.pins.indexOf(p) + 1, p.id === state.activePinId, state.threads.get(p.id) ?? [], state.queuedIds.has(p.id))).join("") : "<div class=\"pb-empty\">Nothing here yet.</div>";
|
|
2027
2253
|
if (itemsMemo !== html) {
|
|
2028
2254
|
items.innerHTML = html;
|
|
2029
2255
|
itemsMemo = html;
|
|
@@ -2056,87 +2282,23 @@ function createDrawer(doc, on) {
|
|
|
2056
2282
|
};
|
|
2057
2283
|
}
|
|
2058
2284
|
//#endregion
|
|
2059
|
-
//#region src/ui/
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
* `spot` is a fraction of the element, so the pin still tracks the element when it moves or
|
|
2067
|
-
* resizes — it just stops sliding to the middle of a wide block the moment you commit it.
|
|
2068
|
-
*/
|
|
2069
|
-
function pinPoint(r, spot) {
|
|
2070
|
-
const fx = spot?.x ?? .5;
|
|
2071
|
-
const fy = spot?.y ?? .5;
|
|
2072
|
-
return {
|
|
2073
|
-
x: r.x + r.width * fx,
|
|
2074
|
-
y: r.y + r.height * fy
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
2077
|
-
/** Chip contents (prototype chipBtnInner, lines 546–550): number + linked-channel tag,
|
|
2078
|
-
* plus the queued badge while the pin waits in the outbox for the reconnect flush. */
|
|
2079
|
-
function chipInner(n, pin, queued = false) {
|
|
2080
|
-
const link = pin?.links?.[0];
|
|
2081
|
-
const badge = link ? `<span class="lk"><span>${esc(link.connector)}</span></span>` : "";
|
|
2082
|
-
const qd = queued ? "<span class=\"qd\">QUEUED</span>" : "";
|
|
2083
|
-
return `<span>${pinNumber(n)}</span>${badge}${qd}`;
|
|
2084
|
-
}
|
|
2085
|
-
function ensureNode(layer, key, fresh) {
|
|
2086
|
-
let node = layer.querySelector(`[data-pin="${key}"]`);
|
|
2087
|
-
if (!node) {
|
|
2088
|
-
node = layer.ownerDocument.createElement("div");
|
|
2089
|
-
node.className = "pb-pin";
|
|
2090
|
-
node.setAttribute("data-pin", key);
|
|
2091
|
-
node.innerHTML = `${fresh ? "<div class=\"ring\"></div>" : ""}<div class="dot"></div><div class="needle"></div><button type="button" class="pb-chipBtn" data-open="${esc(key)}"></button>`;
|
|
2092
|
-
layer.appendChild(node);
|
|
2093
|
-
}
|
|
2094
|
-
return node;
|
|
2095
|
-
}
|
|
2096
|
-
function patchNode(node, at, hot, inner) {
|
|
2097
|
-
node.style.left = `${at.x}px`;
|
|
2098
|
-
node.style.top = `${at.y}px`;
|
|
2099
|
-
node.style.zIndex = hot ? "40" : "20";
|
|
2100
|
-
node.classList.toggle("hot", hot);
|
|
2101
|
-
const chip = node.querySelector(".pb-chipBtn");
|
|
2102
|
-
if (chip && chipMemo.get(chip) !== inner) {
|
|
2103
|
-
chip.innerHTML = inner;
|
|
2104
|
-
chipMemo.set(chip, inner);
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
/**
|
|
2108
|
-
* Render the pin layer for a state snapshot. Visible pins are open pins, the
|
|
2109
|
-
* active pin regardless of status, and the client-only draft (key "draft").
|
|
2110
|
-
*/
|
|
2111
|
-
function renderPins(layer, state) {
|
|
2112
|
-
const visible = state.pins.filter((p) => p.status !== "resolved" || p.id === state.activePinId);
|
|
2113
|
-
const placed = [];
|
|
2114
|
-
visible.forEach((pin, i) => {
|
|
2115
|
-
const rect = pin.target?.rect;
|
|
2285
|
+
//#region src/ui/multimarks.ts
|
|
2286
|
+
const MARK_CLASS = "pb-multi-mark";
|
|
2287
|
+
/** Replace the mark set to mirror `targets`; entries with no rect draw nothing. */
|
|
2288
|
+
function renderMultiMarks(layer, targets) {
|
|
2289
|
+
for (const node of [...layer.querySelectorAll(`.${MARK_CLASS}`)]) node.remove();
|
|
2290
|
+
targets.forEach((target, i) => {
|
|
2291
|
+
const rect = target.rect;
|
|
2116
2292
|
if (rect === void 0) return;
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
rect,
|
|
2126
|
-
spot
|
|
2127
|
-
});
|
|
2293
|
+
const mark = layer.ownerDocument.createElement("div");
|
|
2294
|
+
mark.className = MARK_CLASS;
|
|
2295
|
+
mark.style.left = `${rect.x}px`;
|
|
2296
|
+
mark.style.top = `${rect.y}px`;
|
|
2297
|
+
mark.style.width = `${rect.width}px`;
|
|
2298
|
+
mark.style.height = `${rect.height}px`;
|
|
2299
|
+
mark.innerHTML = `<span>${i + 1}</span>`;
|
|
2300
|
+
layer.appendChild(mark);
|
|
2128
2301
|
});
|
|
2129
|
-
const keys = new Set(placed.map((entry) => entry.pin.id));
|
|
2130
|
-
if (state.draft) keys.add("draft");
|
|
2131
|
-
for (const node of [...layer.children]) if (!keys.has(node.getAttribute("data-pin") ?? "")) node.remove();
|
|
2132
|
-
for (const { pin, n, rect, spot } of placed) {
|
|
2133
|
-
const node = ensureNode(layer, pin.id, false);
|
|
2134
|
-
const hot = pin.id === state.activePinId;
|
|
2135
|
-
const queued = state.queuedIds.has(pin.id);
|
|
2136
|
-
node.classList.toggle("queued", queued);
|
|
2137
|
-
patchNode(node, pinPoint(rect, spot), hot, chipInner(n, pin, queued));
|
|
2138
|
-
}
|
|
2139
|
-
if (state.draft) patchNode(ensureNode(layer, "draft", true), state.draft.placedAt, true, chipInner(visible.length + 1, null));
|
|
2140
2302
|
}
|
|
2141
2303
|
//#endregion
|
|
2142
2304
|
//#region src/ui/puck.ts
|
|
@@ -2144,7 +2306,9 @@ function renderPins(layer, state) {
|
|
|
2144
2306
|
const PUCK_ICON = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"2.5\" y=\"1.5\" width=\"11\" height=\"7\" rx=\"1\"/><path d=\"M8 8.5v6\"/><circle cx=\"8\" cy=\"14.6\" r=\".9\" fill=\"currentColor\" stroke=\"none\"/></svg>";
|
|
2145
2307
|
const FAN_PIN = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"3\" y=\"1.5\" width=\"10\" height=\"6.5\" rx=\"1\"/><path d=\"M8 8v6.5\"/></svg>";
|
|
2146
2308
|
const FAN_INBOX = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M1.8 8.5h3.4l1 2h3.6l1-2h3.4\"/><path d=\"M2.6 3.2h10.8l1.2 5.3v4a1 1 0 01-1 1H2.4a1 1 0 01-1-1v-4z\"/></svg>";
|
|
2147
|
-
const FAN_THEME = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M8
|
|
2309
|
+
const FAN_THEME = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M8 2a4 4 0 0 0 6 6 6 6 0 1 1-6-6z\"/></svg>";
|
|
2310
|
+
const FAN_EYE = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M1.6 8S4 3.8 8 3.8 14.4 8 14.4 8 12 12.2 8 12.2 1.6 8 1.6 8z\"/><circle cx=\"8\" cy=\"8\" r=\"1.8\"/></svg>";
|
|
2311
|
+
const FAN_EYE_OFF = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M2.3 2.3l11.4 11.4\"/><path d=\"M4.9 4.9C2.7 6.2 1.6 8 1.6 8s2.4 4.2 6.4 4.2c1.2 0 2.3-.3 3.1-.8M6.7 4c.4-.1.9-.2 1.3-.2 4 0 6.4 4.2 6.4 4.2s-.8 1.4-2.2 2.5\"/></svg>";
|
|
2148
2312
|
const FAN_EXPAND = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M9.5 6.5v-4h4\"/><path d=\"M6.5 9.5v4h-4\"/></svg>";
|
|
2149
2313
|
function fanItem(act, index, icon, label, key) {
|
|
2150
2314
|
return `<button type="button" class="pb-fan-item" data-act="${act}" style="--i:${index}" aria-label="${label}">${icon}${act === "inbox" ? "<span class=\"badge\" data-ref=\"count\" hidden>0</span>" : ""}<span class="fl">${label.toUpperCase()}<i>${key}</i></span></button>`;
|
|
@@ -2160,7 +2324,7 @@ function createMinimizeUi(doc) {
|
|
|
2160
2324
|
fan.className = "pb-fan";
|
|
2161
2325
|
fan.hidden = true;
|
|
2162
2326
|
fan.setAttribute("role", "menu");
|
|
2163
|
-
fan.innerHTML = fanItem("pin", 0, FAN_PIN, "Drop a pin", "P") + fanItem("inbox", 1, FAN_INBOX, "Inbox", "I") + fanItem("theme", 2, FAN_THEME, "Theme", "D") + fanItem("
|
|
2327
|
+
fan.innerHTML = fanItem("pin", 0, FAN_PIN, "Drop a pin", "P") + fanItem("inbox", 1, FAN_INBOX, "Inbox", "I") + fanItem("theme", 2, FAN_THEME, "Theme", "D") + fanItem("hide", 3, FAN_EYE_OFF, "Hide pins", "H") + fanItem("expand", 4, FAN_EXPAND, "Expand", "M");
|
|
2164
2328
|
const morphWrap = doc.createElement("div");
|
|
2165
2329
|
morphWrap.className = "pb-morph-wrap";
|
|
2166
2330
|
morphWrap.hidden = true;
|
|
@@ -2176,6 +2340,9 @@ function createMinimizeUi(doc) {
|
|
|
2176
2340
|
carrier.querySelector("[data-ref=\"count\"]"),
|
|
2177
2341
|
fan.querySelector("[data-ref=\"count\"]")
|
|
2178
2342
|
];
|
|
2343
|
+
const hideItem = fan.querySelector("[data-act=\"hide\"]");
|
|
2344
|
+
/** Last-rendered hide state; the item's markup is swapped only on change. */
|
|
2345
|
+
let hideShown = null;
|
|
2179
2346
|
return {
|
|
2180
2347
|
puck,
|
|
2181
2348
|
fan,
|
|
@@ -2191,6 +2358,13 @@ function createMinimizeUi(doc) {
|
|
|
2191
2358
|
const degraded = state.connection === "offline" || state.connection === "incompatible";
|
|
2192
2359
|
puck.classList.toggle("degraded", degraded);
|
|
2193
2360
|
puck.classList.toggle("armed", state.mode === "placing");
|
|
2361
|
+
if (hideShown !== state.pinsHidden) {
|
|
2362
|
+
hideShown = state.pinsHidden;
|
|
2363
|
+
const label = state.pinsHidden ? "Show pins" : "Hide pins";
|
|
2364
|
+
hideItem.innerHTML = `${state.pinsHidden ? FAN_EYE : FAN_EYE_OFF}<span class="fl">${label.toUpperCase()}<i>H</i></span>`;
|
|
2365
|
+
hideItem.setAttribute("aria-label", label);
|
|
2366
|
+
hideItem.classList.toggle("lit", state.pinsHidden);
|
|
2367
|
+
}
|
|
2194
2368
|
}
|
|
2195
2369
|
};
|
|
2196
2370
|
}
|
|
@@ -2250,6 +2424,7 @@ const ROWS = [
|
|
|
2250
2424
|
["Send comment", "⌘ ↵"],
|
|
2251
2425
|
["Mark pin resolved", "R"],
|
|
2252
2426
|
["Copy open pins", "C"],
|
|
2427
|
+
["Hide / show pins", "H"],
|
|
2253
2428
|
["Minimize toolbar", "M"],
|
|
2254
2429
|
["Cancel", "ESC"]
|
|
2255
2430
|
];
|
|
@@ -2493,6 +2668,10 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
2493
2668
|
.pb-fan.down { --fan-from: translateY(-16px); }
|
|
2494
2669
|
.pb-fan.on .pb-fan-item { opacity: 1; transform: none; }
|
|
2495
2670
|
.pb-fan-item:hover { color: var(--pb-amber); border-color: var(--pb-amber); }
|
|
2671
|
+
.pb-fan-item.lit { color: var(--pb-amber); border-color: var(--pb-amber); }
|
|
2672
|
+
.pb-multi-mark { position: absolute; border: 1.5px dashed var(--pb-amber); border-radius: 4px; pointer-events: none; z-index: 15; }
|
|
2673
|
+
.pb-multi-mark span { position: absolute; top: -9px; left: -9px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--pb-amber); color: var(--pb-amber-ink); font-family: var(--pb-font-mono); font-size: 9px; font-weight: 500; display: flex; align-items: center; justify-content: center; }
|
|
2674
|
+
.pb-loci { padding: 6px 14px 0; font-family: var(--pb-font-mono); font-size: 9.5px; letter-spacing: .04em; color: var(--pb-fg3); overflow-wrap: anywhere; }
|
|
2496
2675
|
.pb-fan-item .badge { pointer-events: none; }
|
|
2497
2676
|
.pb-fan-item .fl { position: absolute; top: 50%; display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: var(--pb-elev); border: 1px solid var(--pb-line-2); border-radius: 2px; box-shadow: var(--pb-shadow); font-family: var(--pb-font-mono); font-size: 9px; letter-spacing: .14em; color: var(--pb-fg1); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 140ms linear, transform 200ms var(--pb-ease); }
|
|
2498
2677
|
.pb-fan.labels-right .fl { left: calc(100% + 10px); transform: translateY(-50%) translateX(-6px); }
|
|
@@ -2557,15 +2736,20 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2557
2736
|
#modal = null;
|
|
2558
2737
|
#minUi = null;
|
|
2559
2738
|
#min = null;
|
|
2739
|
+
/** SPA view watcher: DOM/history changes re-run the anchor-gated render. */
|
|
2740
|
+
#anchors = null;
|
|
2560
2741
|
#helpOpen = false;
|
|
2561
2742
|
#pageStyle = null;
|
|
2562
2743
|
#unsubscribe = null;
|
|
2563
2744
|
#hover = null;
|
|
2745
|
+
/** Shift+click accumulation while placing — extra loci for ONE pending pin. */
|
|
2746
|
+
#extraTargets = [];
|
|
2564
2747
|
/** Card → element: send/verify/resolve forward to the transport seam; close dismisses. */
|
|
2565
2748
|
#cardActions = {
|
|
2566
2749
|
send: (pinId, text) => this.actions.send?.(pinId, text),
|
|
2567
2750
|
verify: (pinId, outcome) => this.actions.verify?.(pinId, outcome),
|
|
2568
2751
|
resolve: (pinId) => this.actions.resolve?.(pinId),
|
|
2752
|
+
copy: (pinId) => this.#copyPin(pinId),
|
|
2569
2753
|
close: () => this.#dismiss()
|
|
2570
2754
|
};
|
|
2571
2755
|
/** Programmatic path (Pinbox.init). The snippet path reads hub/token attributes. */
|
|
@@ -2598,6 +2782,7 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2598
2782
|
this.#pageStyle = style;
|
|
2599
2783
|
if (this.#aim === null) this.#mountAim();
|
|
2600
2784
|
if (this.#min === null) this.#mountMinimize();
|
|
2785
|
+
this.#anchors = watchAnchors(window, () => this.#render(this.store.get()));
|
|
2601
2786
|
document.addEventListener("mousemove", this.#onMouseMove);
|
|
2602
2787
|
window.addEventListener("scroll", this.#onViewportChange, { passive: true });
|
|
2603
2788
|
window.addEventListener("resize", this.#onViewportChange);
|
|
@@ -2645,6 +2830,9 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2645
2830
|
this.#aim = null;
|
|
2646
2831
|
this.#min?.destroy();
|
|
2647
2832
|
this.#min = null;
|
|
2833
|
+
releaseCapture();
|
|
2834
|
+
this.#anchors?.destroy();
|
|
2835
|
+
this.#anchors = null;
|
|
2648
2836
|
this.#unsubscribe?.();
|
|
2649
2837
|
this.#unsubscribe = null;
|
|
2650
2838
|
this.#pageStyle?.remove();
|
|
@@ -2734,6 +2922,7 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2734
2922
|
onFanAction: (action) => {
|
|
2735
2923
|
if (action === "pin") this.#togglePlacing();
|
|
2736
2924
|
else if (action === "inbox") this.#toggleInbox();
|
|
2925
|
+
else if (action === "hide") this.#togglePinsHidden();
|
|
2737
2926
|
else this.#toggleTheme();
|
|
2738
2927
|
}
|
|
2739
2928
|
});
|
|
@@ -2861,6 +3050,18 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2861
3050
|
navigator.clipboard.writeText(pinsToMarkdown(state.pins, state.threads));
|
|
2862
3051
|
} catch {}
|
|
2863
3052
|
}
|
|
3053
|
+
/** The card's copy: exactly the pin you are looking at, thread included. */
|
|
3054
|
+
#copyPin(pinId) {
|
|
3055
|
+
const state = this.store.get();
|
|
3056
|
+
const pin = state.pins.find((p) => p.id === pinId);
|
|
3057
|
+
if (pin === void 0) return;
|
|
3058
|
+
try {
|
|
3059
|
+
navigator.clipboard.writeText(pinToMarkdown(pin, state.threads.get(pinId) ?? []));
|
|
3060
|
+
} catch {}
|
|
3061
|
+
}
|
|
3062
|
+
#togglePinsHidden() {
|
|
3063
|
+
this.store.update({ pinsHidden: !this.store.get().pinsHidden });
|
|
3064
|
+
}
|
|
2864
3065
|
#setHelp(open) {
|
|
2865
3066
|
this.#helpOpen = open;
|
|
2866
3067
|
this.#modal?.set(open);
|
|
@@ -2878,9 +3079,13 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2878
3079
|
}
|
|
2879
3080
|
#togglePlacing() {
|
|
2880
3081
|
const placing = this.store.get().mode === "placing";
|
|
2881
|
-
this.store.update({
|
|
2882
|
-
mode:
|
|
3082
|
+
this.store.update(placing ? {
|
|
3083
|
+
mode: "idle",
|
|
2883
3084
|
activePinId: null
|
|
3085
|
+
} : {
|
|
3086
|
+
mode: "placing",
|
|
3087
|
+
activePinId: null,
|
|
3088
|
+
pinsHidden: false
|
|
2884
3089
|
});
|
|
2885
3090
|
}
|
|
2886
3091
|
#toggleInbox() {
|
|
@@ -2897,6 +3102,7 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2897
3102
|
/** esc / click-away: leave placing, discard the draft (client-only), deactivate. */
|
|
2898
3103
|
#dismiss() {
|
|
2899
3104
|
this.#setHelp(false);
|
|
3105
|
+
this.#clearExtraTargets();
|
|
2900
3106
|
this.store.update({
|
|
2901
3107
|
mode: "idle",
|
|
2902
3108
|
activePinId: null
|
|
@@ -2962,12 +3168,14 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2962
3168
|
#placeDraft(e) {
|
|
2963
3169
|
e.preventDefault();
|
|
2964
3170
|
e.stopPropagation();
|
|
2965
|
-
const
|
|
3171
|
+
const capture = captureTarget(this.#hover ?? document.body, { at: {
|
|
3172
|
+
x: e.pageX,
|
|
3173
|
+
y: e.pageY
|
|
3174
|
+
} });
|
|
3175
|
+
if (this.#extraTargets.length > 0) capture.target.targets = this.#extraTargets;
|
|
3176
|
+
this.#clearExtraTargets();
|
|
2966
3177
|
this.store.place({
|
|
2967
|
-
target:
|
|
2968
|
-
x: e.pageX,
|
|
2969
|
-
y: e.pageY
|
|
2970
|
-
} }),
|
|
3178
|
+
target: capture,
|
|
2971
3179
|
placedAt: {
|
|
2972
3180
|
x: e.pageX,
|
|
2973
3181
|
y: e.pageY
|
|
@@ -2975,11 +3183,27 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2975
3183
|
});
|
|
2976
3184
|
this.#reticle?.release();
|
|
2977
3185
|
}
|
|
3186
|
+
/** Shift+click while placing: capture WITHOUT committing; a numbered dashed
|
|
3187
|
+
* outline is the receipt. Plain click still commits (with these attached). */
|
|
3188
|
+
#accumulateTarget(e) {
|
|
3189
|
+
e.preventDefault();
|
|
3190
|
+
e.stopPropagation();
|
|
3191
|
+
const el = this.#hover ?? document.body;
|
|
3192
|
+
this.#extraTargets = [...this.#extraTargets, captureTarget(el).target];
|
|
3193
|
+
if (this.#pinsLayer) renderMultiMarks(this.#pinsLayer, this.#extraTargets);
|
|
3194
|
+
}
|
|
3195
|
+
#clearExtraTargets() {
|
|
3196
|
+
if (this.#extraTargets.length === 0) return;
|
|
3197
|
+
this.#extraTargets = [];
|
|
3198
|
+
if (this.#pinsLayer) renderMultiMarks(this.#pinsLayer, []);
|
|
3199
|
+
}
|
|
2978
3200
|
#onClickCapture = (e) => {
|
|
2979
3201
|
if (e.composedPath().includes(this)) return;
|
|
2980
3202
|
const state = this.store.get();
|
|
2981
3203
|
if (state.mode === "placing") {
|
|
2982
|
-
if (
|
|
3204
|
+
if (needsDragAim(window)) return;
|
|
3205
|
+
if (e.shiftKey) this.#accumulateTarget(e);
|
|
3206
|
+
else this.#placeDraft(e);
|
|
2983
3207
|
return;
|
|
2984
3208
|
}
|
|
2985
3209
|
if (state.inboxOpen) this.store.update({ inboxOpen: false });
|
|
@@ -2996,6 +3220,7 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
2996
3220
|
d: () => this.#toggleTheme(),
|
|
2997
3221
|
r: () => this.#resolveActive(),
|
|
2998
3222
|
c: () => this.#copyOpenPins(),
|
|
3223
|
+
h: () => this.#togglePinsHidden(),
|
|
2999
3224
|
m: () => this.#min?.minimized() === true ? this.restore(true) : this.minimize(true),
|
|
3000
3225
|
"?": () => this.#toggleHelp()
|
|
3001
3226
|
};
|
|
@@ -3028,6 +3253,7 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
3028
3253
|
const placing = state.mode === "placing";
|
|
3029
3254
|
this.toggleAttribute("data-placing", placing);
|
|
3030
3255
|
document.body.classList.toggle(PAGE_PLACING_CLASS, placing);
|
|
3256
|
+
if (!placing) this.#clearExtraTargets();
|
|
3031
3257
|
if (!placing) this.#reticle?.release();
|
|
3032
3258
|
this.#syncAim(placing);
|
|
3033
3259
|
if (this.#pinsLayer) renderPins(this.#pinsLayer, state);
|