@autono/pinbox-toolbar 0.19.0 → 0.21.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-DENhglDF.d.ts → index-CMSOhoVs.d.ts} +6 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/model-target-BykKL8H5.js +350 -0
- package/dist/model.d.ts +52 -0
- package/dist/model.js +182 -0
- package/dist/{options-CNqXqkp9.d.ts → options-DPsmmgra.d.ts} +4 -0
- package/dist/plugins/next.d.ts +1 -1
- package/dist/plugins/vite.d.ts +1 -1
- package/dist/plugins/vite.js +61 -5
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{src-BsWnyGwB.js → src-B134WXZ8.js} +311 -284
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +2 -2
- package/dist/toolbar.iife.js +193 -36
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +1 -1
- package/package.json +7 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as targetLabel, c as captureKey, i as hitTest, l as loadCaptureMode, r as captureTarget, s as toggleToolbarCapture, t as projectModelTarget } from "./model-target-BykKL8H5.js";
|
|
1
2
|
//#region src/anchor-watch.ts
|
|
2
3
|
function watchAnchors(win, onChange) {
|
|
3
4
|
let frame = 0;
|
|
@@ -22,23 +23,6 @@ function watchAnchors(win, onChange) {
|
|
|
22
23
|
} };
|
|
23
24
|
}
|
|
24
25
|
//#endregion
|
|
25
|
-
//#region src/capture-mode.ts
|
|
26
|
-
function captureKey(prefix) {
|
|
27
|
-
return `${prefix}:capture`;
|
|
28
|
-
}
|
|
29
|
-
function loadCaptureMode(storage, key, fallback) {
|
|
30
|
-
try {
|
|
31
|
-
const raw = storage?.getItem(key);
|
|
32
|
-
if (raw === "dom" || raw === "tab") return raw;
|
|
33
|
-
} catch {}
|
|
34
|
-
return fallback;
|
|
35
|
-
}
|
|
36
|
-
function saveCaptureMode(storage, key, mode) {
|
|
37
|
-
try {
|
|
38
|
-
storage?.setItem(key, mode);
|
|
39
|
-
} catch {}
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
26
|
//#region src/ui/actions.ts
|
|
43
27
|
const PIN_GLYPH = "<rect x=\"3\" y=\"1.5\" width=\"10\" height=\"6.5\" rx=\"1\"/><path d=\"M8 8v6.5\"/>";
|
|
44
28
|
const INBOX_GLYPH = "<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\"/>";
|
|
@@ -208,10 +192,11 @@ function threadTail(thread) {
|
|
|
208
192
|
];
|
|
209
193
|
}
|
|
210
194
|
function block(pin, thread) {
|
|
211
|
-
const { selector, url, source } = pin.target ?? {};
|
|
195
|
+
const { selector, url, source, model } = pin.target ?? {};
|
|
212
196
|
return [
|
|
213
197
|
`## Pin ${pin.n === void 0 ? pin.id : `#${pin.n} (${pin.id})`} — ${pin.status.toUpperCase()}`,
|
|
214
198
|
`- label: ${line(label(pin))}`,
|
|
199
|
+
...model ? [`- model: ${line(model.modelId)} / ${line(model.partId)} @ ${line(model.revision)}`, `- position: ${model.position.join(", ")} ${model.units}`] : [],
|
|
215
200
|
...selector === void 0 ? [] : [`- selector: \`${line(selector)}\``],
|
|
216
201
|
...(pin.target?.targets ?? []).map((t) => t.selector ?? t.anchor ?? t.tag).filter((locus) => locus !== void 0).map((locus) => `- also: \`${line(locus)}\``),
|
|
217
202
|
...source === void 0 ? [] : [`- source: ${line(source.line === void 0 ? source.file : `${source.file}:${source.line}`)}`],
|
|
@@ -808,254 +793,123 @@ function createMinimize(host) {
|
|
|
808
793
|
};
|
|
809
794
|
}
|
|
810
795
|
//#endregion
|
|
811
|
-
//#region src/
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
*
|
|
816
|
-
* Looks THROUGH our own overlay rather than giving up at it. The single-element form could not:
|
|
817
|
-
* the drag-aim grip sits exactly on the point being aimed at, so it is always the topmost thing
|
|
818
|
-
* under the crosshair, and every probe came back "nothing" the moment touch aiming existed.
|
|
819
|
-
*/
|
|
820
|
-
function hitTest(doc, x, y, ignore) {
|
|
821
|
-
const stack = doc.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
|
|
822
|
-
for (const el of stack) {
|
|
823
|
-
if (!el || el === doc.body || el === doc.documentElement) return null;
|
|
824
|
-
if (!ignore(el)) return el;
|
|
825
|
-
}
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
/** CLASS-or-TAG display name with a sibling index when needed (prototype nodeName). */
|
|
829
|
-
function nodeName(el) {
|
|
830
|
-
const key = el.classList[0];
|
|
831
|
-
let name = (key ?? el.tagName).toUpperCase();
|
|
832
|
-
const parent = el.parentElement;
|
|
833
|
-
if (parent) {
|
|
834
|
-
const sibs = [...parent.children].filter((c) => c.classList[0] === key && c.tagName === el.tagName);
|
|
835
|
-
if (sibs.length > 1) name += ` ${sibs.indexOf(el) + 1}`;
|
|
836
|
-
}
|
|
837
|
-
return name;
|
|
796
|
+
//#region src/reveal-settle.ts
|
|
797
|
+
const pending = /* @__PURE__ */ new WeakMap();
|
|
798
|
+
function cancelRevealSettle(doc) {
|
|
799
|
+
pending.get(doc)?.();
|
|
838
800
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
];
|
|
868
|
-
function attrSegment(el, doc) {
|
|
869
|
-
for (const attr of STABLE_DATA_ATTRS) {
|
|
870
|
-
const value = el.getAttribute(attr);
|
|
871
|
-
if (value === null || value.includes("\"") || value.includes("\\")) continue;
|
|
872
|
-
const selector = `${el.tagName.toLowerCase()}[${attr}="${value}"]`;
|
|
873
|
-
if (doc.querySelectorAll(selector).length === 1) return selector;
|
|
874
|
-
}
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
function nthSegment(el) {
|
|
878
|
-
const tag = el.tagName.toLowerCase();
|
|
879
|
-
const parent = el.parentElement;
|
|
880
|
-
if (!parent) return tag;
|
|
881
|
-
const sameTag = [...parent.children].filter((c) => c.tagName === el.tagName);
|
|
882
|
-
return sameTag.length > 1 ? `${tag}:nth-of-type(${sameTag.indexOf(el) + 1})` : tag;
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Stable CSS path for an element: ids > stable data attributes > an
|
|
886
|
-
* nth-of-type chain. Guaranteed round-trip: querySelector(buildSelector(el)) === el.
|
|
887
|
-
*/
|
|
888
|
-
function buildSelector(el) {
|
|
889
|
-
const doc = el.ownerDocument;
|
|
890
|
-
const segments = [];
|
|
891
|
-
let node = el;
|
|
892
|
-
while (node && node !== doc.documentElement) {
|
|
893
|
-
const id = node.getAttribute("id");
|
|
894
|
-
if (id && SAFE_ID.test(id) && doc.querySelectorAll(`#${id}`).length === 1) {
|
|
895
|
-
segments.unshift(`#${id}`);
|
|
896
|
-
return segments.join(" > ");
|
|
897
|
-
}
|
|
898
|
-
const byAttr = attrSegment(node, doc);
|
|
899
|
-
if (byAttr) {
|
|
900
|
-
segments.unshift(byAttr);
|
|
901
|
-
return segments.join(" > ");
|
|
902
|
-
}
|
|
903
|
-
segments.unshift(nthSegment(node));
|
|
904
|
-
node = node.parentElement;
|
|
905
|
-
}
|
|
906
|
-
return segments.join(" > ");
|
|
801
|
+
function settlePinReveal(doc, reveal) {
|
|
802
|
+
cancelRevealSettle(doc);
|
|
803
|
+
const win = doc.defaultView;
|
|
804
|
+
if (!win) return;
|
|
805
|
+
let attempts = 0;
|
|
806
|
+
let timer = 0;
|
|
807
|
+
const events = [
|
|
808
|
+
"wheel",
|
|
809
|
+
"touchstart",
|
|
810
|
+
"pointerdown",
|
|
811
|
+
"keydown"
|
|
812
|
+
];
|
|
813
|
+
const stop = () => {
|
|
814
|
+
win.clearTimeout(timer);
|
|
815
|
+
for (const event of events) doc.removeEventListener(event, stop, true);
|
|
816
|
+
pending.delete(doc);
|
|
817
|
+
};
|
|
818
|
+
for (const event of events) doc.addEventListener(event, stop, {
|
|
819
|
+
capture: true,
|
|
820
|
+
passive: true
|
|
821
|
+
});
|
|
822
|
+
const tick = () => {
|
|
823
|
+
reveal();
|
|
824
|
+
if (++attempts >= 10) stop();
|
|
825
|
+
else timer = win.setTimeout(tick, 150);
|
|
826
|
+
};
|
|
827
|
+
pending.set(doc, stop);
|
|
828
|
+
timer = win.setTimeout(tick, 150);
|
|
907
829
|
}
|
|
908
830
|
//#endregion
|
|
909
|
-
//#region src/
|
|
910
|
-
/**
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
"position",
|
|
914
|
-
"font-size",
|
|
915
|
-
"color",
|
|
916
|
-
"background-color",
|
|
917
|
-
"margin",
|
|
918
|
-
"padding",
|
|
919
|
-
"overflow"
|
|
920
|
-
];
|
|
921
|
-
const NEARBY_TEXT_MAX = 160;
|
|
922
|
-
function styleSubset(win, el) {
|
|
923
|
-
let cs;
|
|
831
|
+
//#region src/pin-reveal.ts
|
|
832
|
+
/** Query ordering is not a view change; repeated parameter ordering remains intact. */
|
|
833
|
+
function samePinView(win, value) {
|
|
834
|
+
if (!value) return true;
|
|
924
835
|
try {
|
|
925
|
-
|
|
836
|
+
const target = new URL(value, win.location.href);
|
|
837
|
+
const current = new URL(win.location.href);
|
|
838
|
+
target.searchParams.sort();
|
|
839
|
+
current.searchParams.sort();
|
|
840
|
+
return target.pathname === current.pathname && target.search === current.search;
|
|
926
841
|
} catch {
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
const out = {};
|
|
930
|
-
for (const key of STYLE_KEYS) {
|
|
931
|
-
const value = cs.getPropertyValue(key);
|
|
932
|
-
if (value !== "") out[key] = value;
|
|
842
|
+
return true;
|
|
933
843
|
}
|
|
934
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
935
844
|
}
|
|
936
|
-
function
|
|
937
|
-
|
|
938
|
-
for (const name of el.getAttributeNames()) if (name.startsWith("aria-")) out[name] = el.getAttribute(name) ?? "";
|
|
939
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
845
|
+
function pagePin(pin) {
|
|
846
|
+
return pin.target?.selector === "html" || pin.target?.selector === "body";
|
|
940
847
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
function selectedText(win, el) {
|
|
848
|
+
/** Resolve before clipping: an offscreen row is precisely the target we need to scroll. */
|
|
849
|
+
function scrollToPin(doc, pin) {
|
|
850
|
+
if (pin.target?.model || pagePin(pin)) return false;
|
|
851
|
+
const win = doc.defaultView;
|
|
852
|
+
if (!win || !samePinView(win, pin.target?.url) || !pin.target?.selector) return false;
|
|
947
853
|
try {
|
|
948
|
-
const
|
|
949
|
-
if (!
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
854
|
+
const element = doc.querySelector(pin.target.selector);
|
|
855
|
+
if (!element?.getClientRects().length) return false;
|
|
856
|
+
element.scrollIntoView({
|
|
857
|
+
behavior: "instant",
|
|
858
|
+
block: "center",
|
|
859
|
+
inline: "center"
|
|
860
|
+
});
|
|
861
|
+
return true;
|
|
953
862
|
} catch {
|
|
954
|
-
return;
|
|
863
|
+
return false;
|
|
955
864
|
}
|
|
956
865
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
866
|
+
const key = (endpoint) => `pinbox:${endpoint}:reveal`;
|
|
867
|
+
/** Returns true when leaving this document. Never navigates to a different origin. */
|
|
868
|
+
function revealPin(doc, pin, endpoint) {
|
|
869
|
+
cancelRevealSettle(doc);
|
|
870
|
+
const win = doc.defaultView;
|
|
871
|
+
if (!win) return false;
|
|
872
|
+
try {
|
|
873
|
+
win.sessionStorage.removeItem(key(endpoint));
|
|
874
|
+
} catch {}
|
|
875
|
+
if (pagePin(pin) || pin.target?.model) return false;
|
|
876
|
+
if (pin.target?.url && !samePinView(win, pin.target.url)) try {
|
|
877
|
+
const target = new URL(pin.target.url, win.location.href);
|
|
878
|
+
if (target.origin !== win.location.origin || !["http:", "https:"].includes(target.protocol)) return false;
|
|
879
|
+
win.sessionStorage.setItem(key(endpoint), JSON.stringify({
|
|
880
|
+
id: pin.id,
|
|
881
|
+
expires: Date.now() + 3e4
|
|
882
|
+
}));
|
|
883
|
+
win.location.assign(target.href);
|
|
884
|
+
return true;
|
|
961
885
|
} catch {
|
|
962
886
|
return false;
|
|
963
887
|
}
|
|
888
|
+
scrollToPin(doc, pin);
|
|
964
889
|
return false;
|
|
965
890
|
}
|
|
966
|
-
/**
|
|
967
|
-
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
* The element's text, split the way the browser stores it: one entry per run of characters.
|
|
978
|
-
*
|
|
979
|
-
* This walks TEXT NODES, not elements, and that distinction is the whole point — it makes no
|
|
980
|
-
* assumption about how a site is built. A heading is one run. A nav bar is one per link. A
|
|
981
|
-
* paragraph with a bold word in the middle is three, in reading order, including the halves either
|
|
982
|
-
* side of the bold. An earlier version keyed off "elements with no element children", which
|
|
983
|
-
* quietly lost the "Hello " in `<p>Hello <b>world</b></p>` — text a person can obviously see and
|
|
984
|
-
* would obviously expect to be able to change.
|
|
985
|
-
*
|
|
986
|
-
* `nearbyText` runs them all together, which is fine to read and useless to edit: it cannot tell
|
|
987
|
-
* an agent that "work approach people contact" is four separate places. This can.
|
|
988
|
-
*/
|
|
989
|
-
function textRuns(el) {
|
|
990
|
-
const runs = [];
|
|
991
|
-
const walk = (node) => {
|
|
992
|
-
if (node.nodeType === 3) {
|
|
993
|
-
const text = (node.nodeValue ?? "").trim();
|
|
994
|
-
if (text.length > 0) runs.push(text.slice(0, MAX_RUN_LENGTH));
|
|
995
|
-
return runs.length <= MAX_RUNS;
|
|
891
|
+
/** Called on pin updates and DOM changes: SPA content may arrive after the toolbar. */
|
|
892
|
+
function pendingPinReveal(doc, pins, endpoint) {
|
|
893
|
+
const win = doc.defaultView;
|
|
894
|
+
if (!win) return null;
|
|
895
|
+
try {
|
|
896
|
+
const raw = win.sessionStorage.getItem(key(endpoint));
|
|
897
|
+
if (!raw) return null;
|
|
898
|
+
const pending = JSON.parse(raw);
|
|
899
|
+
if (typeof pending.id !== "string" || !Number.isFinite(pending.expires) || pending.expires < Date.now()) {
|
|
900
|
+
win.sessionStorage.removeItem(key(endpoint));
|
|
901
|
+
return null;
|
|
996
902
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (el.classList.length > 0) context.classes = [...el.classList];
|
|
1007
|
-
const styles = styleSubset(win, el);
|
|
1008
|
-
if (styles !== void 0) context.styles = styles;
|
|
1009
|
-
const aria = ariaMap(el);
|
|
1010
|
-
if (aria !== void 0) context.aria = aria;
|
|
1011
|
-
const nearby = nearbyText(el);
|
|
1012
|
-
if (nearby !== void 0) context.nearbyText = nearby;
|
|
1013
|
-
const selected = selectedText(win, el);
|
|
1014
|
-
if (selected !== void 0) context.selectedText = selected;
|
|
1015
|
-
const runs = textRuns(el);
|
|
1016
|
-
if (runs !== void 0) context.textRuns = runs;
|
|
1017
|
-
return Object.keys(context).length > 0 ? context : void 0;
|
|
1018
|
-
}
|
|
1019
|
-
/** Fills PinInput.target/env from a chosen element (shapes come from the pin schema). */
|
|
1020
|
-
function captureTarget(el, opts) {
|
|
1021
|
-
const win = el.ownerDocument.defaultView;
|
|
1022
|
-
const r = el.getBoundingClientRect();
|
|
1023
|
-
const target = {
|
|
1024
|
-
url: win.location.href,
|
|
1025
|
-
selector: buildSelector(el),
|
|
1026
|
-
tag: el.tagName.toLowerCase(),
|
|
1027
|
-
rect: {
|
|
1028
|
-
x: r.left + win.scrollX,
|
|
1029
|
-
y: r.top + win.scrollY,
|
|
1030
|
-
width: r.width,
|
|
1031
|
-
height: r.height
|
|
1032
|
-
},
|
|
1033
|
-
fixed: isFixed(win, el)
|
|
1034
|
-
};
|
|
1035
|
-
if (opts?.anchor !== void 0) target.anchor = opts.anchor;
|
|
1036
|
-
if (opts?.at !== void 0 && r.width > 0 && r.height > 0) {
|
|
1037
|
-
const fx = (opts.at.x - (r.left + win.scrollX)) / r.width;
|
|
1038
|
-
const fy = (opts.at.y - (r.top + win.scrollY)) / r.height;
|
|
1039
|
-
if (fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1) target.spot = {
|
|
1040
|
-
x: fx,
|
|
1041
|
-
y: fy
|
|
1042
|
-
};
|
|
903
|
+
const pin = pins.find((p) => p.id === pending.id);
|
|
904
|
+
if (!pin || !scrollToPin(doc, pin)) return null;
|
|
905
|
+
win.sessionStorage.removeItem(key(endpoint));
|
|
906
|
+
settlePinReveal(doc, () => {
|
|
907
|
+
scrollToPin(doc, pin);
|
|
908
|
+
});
|
|
909
|
+
return pin.id;
|
|
910
|
+
} catch {
|
|
911
|
+
return null;
|
|
1043
912
|
}
|
|
1044
|
-
const context = buildContext(win, el);
|
|
1045
|
-
if (context !== void 0) target.context = context;
|
|
1046
|
-
return {
|
|
1047
|
-
target,
|
|
1048
|
-
env: {
|
|
1049
|
-
viewport: {
|
|
1050
|
-
w: win.innerWidth,
|
|
1051
|
-
h: win.innerHeight,
|
|
1052
|
-
dpr: win.devicePixelRatio
|
|
1053
|
-
},
|
|
1054
|
-
browser: win.navigator.userAgent,
|
|
1055
|
-
os: win.navigator.platform || "unknown",
|
|
1056
|
-
colorScheme: win.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
1057
|
-
}
|
|
1058
|
-
};
|
|
1059
913
|
}
|
|
1060
914
|
//#endregion
|
|
1061
915
|
//#region src/ui/aim.ts
|
|
@@ -1311,7 +1165,8 @@ function createPlacement(deps) {
|
|
|
1311
1165
|
function placeDraft(e) {
|
|
1312
1166
|
e.preventDefault();
|
|
1313
1167
|
e.stopPropagation();
|
|
1314
|
-
const
|
|
1168
|
+
const el = hover ?? doc.body;
|
|
1169
|
+
const capture = captureTarget(el, { at: {
|
|
1315
1170
|
x: e.pageX,
|
|
1316
1171
|
y: e.pageY
|
|
1317
1172
|
} });
|
|
@@ -2368,8 +2223,8 @@ function createBar(doc, on) {
|
|
|
2368
2223
|
inboxBtn.classList.toggle("lit", state.inboxOpen);
|
|
2369
2224
|
const open = String(openTaskCount(state.pins));
|
|
2370
2225
|
if (count.textContent !== open) count.textContent = open;
|
|
2371
|
-
captureBtn.classList.toggle("lit", state.captureMode === "tab");
|
|
2372
|
-
captureBtn.title = state.captureMode === "tab" ? "Tab capture on — real pixels, Chrome asks once per page load (S)" : "Screenshots: DOM snapshot, no prompt — press for tab capture (S)";
|
|
2226
|
+
captureBtn.classList.toggle("lit", !state.captureLabel && state.captureMode === "tab");
|
|
2227
|
+
captureBtn.title = state.captureLabel ?? (state.captureMode === "tab" ? "Tab capture on — real pixels, Chrome asks once per page load (S)" : "Screenshots: DOM snapshot, no prompt — press for tab capture (S)");
|
|
2373
2228
|
if (hideShown !== state.pinsHidden) {
|
|
2374
2229
|
hideShown = state.pinsHidden;
|
|
2375
2230
|
hideBtn.innerHTML = icon(state.pinsHidden ? EYE_GLYPH : EYE_OFF_GLYPH, 14);
|
|
@@ -2649,20 +2504,6 @@ const chipMemo = /* @__PURE__ */ new WeakMap();
|
|
|
2649
2504
|
function nextOrdinal(pins) {
|
|
2650
2505
|
return Math.max(pins.length, ...pins.map((p) => p.n ?? 0)) + 1;
|
|
2651
2506
|
}
|
|
2652
|
-
/**
|
|
2653
|
-
* Does the pin's captured URL still describe the view on screen? Path + search
|
|
2654
|
-
* only — hashes are anchors, not views. An absent or unparseable URL never
|
|
2655
|
-
* gates: old pins (and CLI pins) keep rendering exactly as before.
|
|
2656
|
-
*/
|
|
2657
|
-
function sameView(win, url) {
|
|
2658
|
-
if (url === void 0) return true;
|
|
2659
|
-
try {
|
|
2660
|
-
const target = new URL(url, win.location.href);
|
|
2661
|
-
return target.pathname === win.location.pathname && target.search === win.location.search;
|
|
2662
|
-
} catch {
|
|
2663
|
-
return true;
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
2507
|
/** A stored (document-space) rect or point, in today's viewport. */
|
|
2667
2508
|
function toViewport(win, p) {
|
|
2668
2509
|
return {
|
|
@@ -2687,22 +2528,23 @@ function anchorRect(doc, pin) {
|
|
|
2687
2528
|
}
|
|
2688
2529
|
/** The same resolution for any captured target — a pin's, or the draft's before it commits. */
|
|
2689
2530
|
function targetRect(doc, target) {
|
|
2531
|
+
if (target?.model) return projectModelTarget(doc, target.model);
|
|
2690
2532
|
const stored = target?.rect;
|
|
2691
|
-
if (stored === void 0) return null;
|
|
2692
2533
|
const win = doc.defaultView;
|
|
2693
|
-
if (win === null) return stored;
|
|
2694
|
-
if (!
|
|
2534
|
+
if (win === null) return stored ?? null;
|
|
2535
|
+
if (!samePinView(win, target?.url)) return null;
|
|
2695
2536
|
const selector = target?.selector;
|
|
2696
|
-
if (selector === void 0) return toViewport(win, stored);
|
|
2537
|
+
if (selector === void 0) return stored ? toViewport(win, stored) : null;
|
|
2538
|
+
if (!stored && (selector === "html" || selector === "body")) return null;
|
|
2697
2539
|
let el;
|
|
2698
2540
|
try {
|
|
2699
2541
|
el = doc.querySelector(selector);
|
|
2700
2542
|
} catch {
|
|
2701
|
-
return toViewport(win, stored);
|
|
2543
|
+
return stored ? toViewport(win, stored) : null;
|
|
2702
2544
|
}
|
|
2703
2545
|
if (el === null) return null;
|
|
2704
2546
|
const r = el.getBoundingClientRect();
|
|
2705
|
-
if (r.width <= 0 && r.height <= 0) return toViewport(win, stored);
|
|
2547
|
+
if (r.width <= 0 && r.height <= 0) return stored ? toViewport(win, stored) : null;
|
|
2706
2548
|
return clipToScrollAncestors(win, el, {
|
|
2707
2549
|
x: r.left,
|
|
2708
2550
|
y: r.top,
|
|
@@ -3016,6 +2858,8 @@ function anchorOf(root, pin, draft) {
|
|
|
3016
2858
|
* labels the card without claiming an element that was never captured.
|
|
3017
2859
|
*/
|
|
3018
2860
|
function labelOf(target) {
|
|
2861
|
+
if (target?.selector === "html" || target?.selector === "body") return "PAGE";
|
|
2862
|
+
if (target?.model) return `3D · ${target.model.partId}`;
|
|
3019
2863
|
return target?.anchor ?? target?.tag?.toUpperCase() ?? "PIN";
|
|
3020
2864
|
}
|
|
3021
2865
|
function viewOf(root, state) {
|
|
@@ -3753,11 +3597,7 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
3753
3597
|
return loadCaptureMode(globalThis.localStorage, key, this.config?.capture ?? "dom");
|
|
3754
3598
|
}
|
|
3755
3599
|
#toggleCapture() {
|
|
3756
|
-
|
|
3757
|
-
this.store.update({ captureMode: next });
|
|
3758
|
-
if (next === "dom") releaseCapture();
|
|
3759
|
-
saveCaptureMode(globalThis.localStorage, captureKey(`pinbox:${this.config?.endpoint ?? ""}`), next);
|
|
3760
|
-
return true;
|
|
3600
|
+
return toggleToolbarCapture(this, this.config?.endpoint ?? "", releaseCapture);
|
|
3761
3601
|
}
|
|
3762
3602
|
/** Theme from the OS when the host set none; the page-level CSS (placing cursor) into <head>. */
|
|
3763
3603
|
#applyPageDefaults() {
|
|
@@ -4028,16 +3868,15 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
4028
3868
|
/** Inbox item click: activate the pin and scroll it into view (prototype line 700). */
|
|
4029
3869
|
#activateFromInbox(pinId) {
|
|
4030
3870
|
const pin = this.store.get().pins.find((p) => p.id === pinId);
|
|
3871
|
+
if (!pin) return;
|
|
3872
|
+
this.store.update({
|
|
3873
|
+
inboxOpen: false,
|
|
3874
|
+
activePinId: null,
|
|
3875
|
+
pinsHidden: false
|
|
3876
|
+
});
|
|
3877
|
+
if (revealPin(document, pin, this.config?.endpoint ?? "")) return;
|
|
4031
3878
|
this.#ensureThread(pinId);
|
|
4032
3879
|
this.store.update({ activePinId: pinId });
|
|
4033
|
-
const rect = pin === void 0 ? null : anchorRect(document, pin);
|
|
4034
|
-
if (rect) {
|
|
4035
|
-
const y = window.scrollY + rect.y + rect.height / 2;
|
|
4036
|
-
window.scrollTo({
|
|
4037
|
-
top: Math.max(0, y - window.innerHeight / 2),
|
|
4038
|
-
behavior: "smooth"
|
|
4039
|
-
});
|
|
4040
|
-
}
|
|
4041
3880
|
}
|
|
4042
3881
|
/**
|
|
4043
3882
|
* Nudge a stale pin: re-post the last human message as a new thread message. A watcher that
|
|
@@ -4186,6 +4025,16 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
4186
4025
|
});
|
|
4187
4026
|
};
|
|
4188
4027
|
#render(state) {
|
|
4028
|
+
const restored = pendingPinReveal(document, state.pins, this.config?.endpoint ?? "");
|
|
4029
|
+
if (restored) {
|
|
4030
|
+
this.#ensureThread(restored);
|
|
4031
|
+
this.store.update({
|
|
4032
|
+
activePinId: restored,
|
|
4033
|
+
inboxOpen: false,
|
|
4034
|
+
pinsHidden: false
|
|
4035
|
+
});
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4189
4038
|
const placing = state.mode === "placing";
|
|
4190
4039
|
this.toggleAttribute("data-placing", placing);
|
|
4191
4040
|
document.body.classList.toggle(PAGE_PLACING_CLASS, placing);
|
|
@@ -4198,6 +4047,184 @@ var PinboxToolbarElement = class extends BaseElement {
|
|
|
4198
4047
|
}
|
|
4199
4048
|
};
|
|
4200
4049
|
//#endregion
|
|
4050
|
+
//#region src/ui/preview-styles.ts
|
|
4051
|
+
const PREVIEW_STYLES = `
|
|
4052
|
+
.pb-preview-wrap { display:flex; align-items:center; border-left:1px solid var(--pb-line); margin-left:4px; padding-left:4px; }
|
|
4053
|
+
.pb-preview-trigger { max-width:160px; gap:7px; }
|
|
4054
|
+
.pb-preview-trigger .name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:110px; }
|
|
4055
|
+
.pb-preview-trigger svg { flex-shrink:0; }
|
|
4056
|
+
.pb-preview-menu { position:fixed; margin:0; padding:0; width:304px; max-width:calc(100vw - 24px); max-height:70vh; overflow:auto; background:var(--pb-surface); color:var(--pb-fg1); border:1px solid var(--pb-line-2); border-radius:4px; box-shadow:var(--pb-shadow); font-family:var(--pb-font-body); }
|
|
4057
|
+
.pb-preview-menu::backdrop { background:transparent; }
|
|
4058
|
+
.pb-preview-head { display:flex; align-items:center; justify-content:space-between; padding:10px 12px 8px 16px; border-bottom:1px solid var(--pb-line); }
|
|
4059
|
+
.pb-preview-head span { font:10px var(--pb-font-mono); letter-spacing:.18em; color:var(--pb-fg3); }
|
|
4060
|
+
.pb-preview-list { padding:5px; }
|
|
4061
|
+
.pb-preview-option { display:flex; align-items:center; gap:10px; width:100%; text-align:left; padding:11px 10px; border-radius:2px; color:var(--pb-fg2); }
|
|
4062
|
+
.pb-preview-option:hover, .pb-preview-option:focus-visible { background:var(--pb-hover); outline:1px solid var(--pb-line-2); }
|
|
4063
|
+
.pb-preview-option[aria-checked="true"] { background:var(--pb-amber-soft); color:var(--pb-fg1); }
|
|
4064
|
+
.pb-preview-option:disabled { opacity:.45; cursor:not-allowed; }
|
|
4065
|
+
.pb-preview-option .check { width:14px; flex-shrink:0; color:var(--pb-amber); }
|
|
4066
|
+
.pb-preview-option .copy { display:flex; flex-direction:column; gap:4px; min-width:0; }
|
|
4067
|
+
.pb-preview-option .title { font-size:12px; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
4068
|
+
.pb-preview-option .branch { font:10px var(--pb-font-mono); color:var(--pb-fg3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
4069
|
+
.pb-preview-foot { border-top:1px solid var(--pb-line); padding:11px 16px; display:flex; flex-direction:column; gap:8px; }
|
|
4070
|
+
.pb-preview-status { font-size:11px; line-height:1.5; color:var(--pb-fg3); }
|
|
4071
|
+
.pb-preview-foot a { font:10px var(--pb-font-mono); letter-spacing:.08em; color:var(--pb-amber); text-decoration:none; }
|
|
4072
|
+
`;
|
|
4073
|
+
//#endregion
|
|
4074
|
+
//#region src/ui/preview-switcher.ts
|
|
4075
|
+
const CHEVRON = icon("<path d=\"m4 6 4 4 4-4\"/>", 14);
|
|
4076
|
+
const REFRESH = icon("<path d=\"M2 8a6 6 0 0 1 10.5-4L14 6M14 2v4h-4M14 8a6 6 0 0 1-10.5 4L2 10M2 14v-4h4\"/>", 14);
|
|
4077
|
+
const BRANCH = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"><circle cx=\"4\" cy=\"3\" r=\"1.5\"/><circle cx=\"4\" cy=\"13\" r=\"1.5\"/><circle cx=\"12\" cy=\"4\" r=\"1.5\"/><path d=\"M4 4.5v7M12 5.5v1a3 3 0 0 1-3 3H7a3 3 0 0 0-3 3\"/></svg>";
|
|
4078
|
+
function createPreviewView(doc, root, bar) {
|
|
4079
|
+
const style = doc.createElement("style");
|
|
4080
|
+
style.textContent = PREVIEW_STYLES;
|
|
4081
|
+
root.append(style);
|
|
4082
|
+
const wrap = doc.createElement("div");
|
|
4083
|
+
wrap.dataset["previewSwitcher"] = "";
|
|
4084
|
+
wrap.className = "pb-preview-wrap";
|
|
4085
|
+
wrap.innerHTML = `<button type="button" class="pb-tb pb-preview-trigger" aria-label="Switch preview" aria-haspopup="menu" aria-controls="pb-preview-menu" aria-expanded="false">${BRANCH}<span class="name">PREVIEW</span>${CHEVRON}</button>
|
|
4086
|
+
<div id="pb-preview-menu" class="pb-preview-menu" popover="auto" aria-label="Local previews">
|
|
4087
|
+
<div class="pb-preview-head"><span>LOCAL PREVIEWS</span><button type="button" class="pb-tb sq" aria-label="Refresh previews">${REFRESH}</button></div>
|
|
4088
|
+
<div class="pb-preview-list" role="menu" aria-label="Preview branch or worktree"></div>
|
|
4089
|
+
<div class="pb-preview-foot"><span class="pb-preview-status" role="status">Loading previews…</span><a target="_blank" rel="noopener noreferrer" hidden>OPEN PR ↗</a></div>
|
|
4090
|
+
</div>`;
|
|
4091
|
+
bar.append(wrap);
|
|
4092
|
+
const trigger = wrap.querySelector(".pb-preview-trigger");
|
|
4093
|
+
const panel = wrap.querySelector(".pb-preview-menu");
|
|
4094
|
+
const list = wrap.querySelector(".pb-preview-list");
|
|
4095
|
+
const reload = wrap.querySelector("[aria-label=\"Refresh previews\"]");
|
|
4096
|
+
const status = wrap.querySelector("[role=\"status\"]");
|
|
4097
|
+
const pr = wrap.querySelector("a");
|
|
4098
|
+
wirePopover(trigger, panel, list, doc);
|
|
4099
|
+
return {
|
|
4100
|
+
trigger,
|
|
4101
|
+
list,
|
|
4102
|
+
status,
|
|
4103
|
+
reload,
|
|
4104
|
+
render(choices) {
|
|
4105
|
+
renderOptions(doc, list, choices);
|
|
4106
|
+
const current = choices.find((p) => p.current);
|
|
4107
|
+
trigger.querySelector(".name").textContent = compactLabel(current);
|
|
4108
|
+
trigger.setAttribute("aria-label", `Switch preview: ${current?.label ?? "local previews"}`);
|
|
4109
|
+
trigger.title = current ? `${current.branch} · ${current.commit.slice(0, 8)}` : "Choose a preview";
|
|
4110
|
+
status.textContent = current?.backend || "Local frontend preview";
|
|
4111
|
+
pr.hidden = !current?.pr;
|
|
4112
|
+
if (current?.pr) pr.href = current.pr;
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
}
|
|
4116
|
+
function compactLabel(current) {
|
|
4117
|
+
if (!current) return "PREVIEW";
|
|
4118
|
+
return current.label.match(/^PR #\d+/)?.[0] ?? (current.branch === "main" ? "MAIN" : current.branch.replace(/^[^/]+\//, ""));
|
|
4119
|
+
}
|
|
4120
|
+
function renderOptions(doc, list, choices) {
|
|
4121
|
+
list.replaceChildren();
|
|
4122
|
+
for (const item of choices) {
|
|
4123
|
+
const button = doc.createElement("button");
|
|
4124
|
+
button.type = "button";
|
|
4125
|
+
button.className = "pb-preview-option";
|
|
4126
|
+
button.dataset["previewId"] = item.id;
|
|
4127
|
+
button.setAttribute("role", "menuitemradio");
|
|
4128
|
+
button.setAttribute("aria-checked", String(item.current));
|
|
4129
|
+
button.disabled = !item.ready;
|
|
4130
|
+
button.innerHTML = "<span class=\"check\" aria-hidden=\"true\"></span><span class=\"copy\"><span class=\"title\"></span><span class=\"branch\"></span></span>";
|
|
4131
|
+
button.querySelector(".check").textContent = item.current ? "✓" : "";
|
|
4132
|
+
button.querySelector(".title").textContent = item.label;
|
|
4133
|
+
button.querySelector(".branch").textContent = `${item.branch}${item.ready ? "" : " · unavailable"}`;
|
|
4134
|
+
list.append(button);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
function wirePopover(trigger, panel, list, doc) {
|
|
4138
|
+
trigger.addEventListener("click", () => {
|
|
4139
|
+
panel.togglePopover();
|
|
4140
|
+
const rect = trigger.getBoundingClientRect();
|
|
4141
|
+
const width = doc.documentElement.clientWidth;
|
|
4142
|
+
panel.style.left = `${Math.max(12, Math.min(rect.right - 304, width - 316))}px`;
|
|
4143
|
+
panel.style.top = `${rect.top > panel.offsetHeight + 20 ? rect.top - panel.offsetHeight - 10 : rect.bottom + 10}px`;
|
|
4144
|
+
(list.querySelector("button[aria-checked=\"true\"]:not(:disabled)") ?? list.querySelector("button:not(:disabled)"))?.focus();
|
|
4145
|
+
});
|
|
4146
|
+
panel.addEventListener("toggle", () => trigger.setAttribute("aria-expanded", String(panel.matches(":popover-open"))));
|
|
4147
|
+
panel.addEventListener("keydown", (e) => {
|
|
4148
|
+
const buttons = [...list.querySelectorAll("button:not(:disabled)")];
|
|
4149
|
+
const index = buttons.indexOf(panel.getRootNode().activeElement);
|
|
4150
|
+
if ([
|
|
4151
|
+
"ArrowDown",
|
|
4152
|
+
"ArrowUp",
|
|
4153
|
+
"Home",
|
|
4154
|
+
"End"
|
|
4155
|
+
].includes(e.key)) {
|
|
4156
|
+
e.preventDefault();
|
|
4157
|
+
buttons[e.key === "Home" ? 0 : e.key === "End" ? buttons.length - 1 : (index + (e.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length]?.focus();
|
|
4158
|
+
}
|
|
4159
|
+
if (e.key === "Escape") {
|
|
4160
|
+
panel.hidePopover();
|
|
4161
|
+
trigger.focus();
|
|
4162
|
+
}
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
//#endregion
|
|
4166
|
+
//#region src/previews.ts
|
|
4167
|
+
function previewDestination(origin, current) {
|
|
4168
|
+
const target = new URL(origin);
|
|
4169
|
+
const source = new URL(current);
|
|
4170
|
+
if (!["http:", "https:"].includes(target.protocol) || target.username || target.password || target.pathname !== "/" || target.search || target.hash) throw new Error("Invalid preview origin");
|
|
4171
|
+
target.pathname = source.pathname;
|
|
4172
|
+
target.search = source.search;
|
|
4173
|
+
target.hash = source.hash;
|
|
4174
|
+
return target.href;
|
|
4175
|
+
}
|
|
4176
|
+
function mountPreviewSwitcher(host, endpoint) {
|
|
4177
|
+
const root = host.shadowRoot;
|
|
4178
|
+
const bar = root?.querySelector(".pb-bar");
|
|
4179
|
+
if (!root || !bar || root.querySelector("[data-preview-switcher]")) return;
|
|
4180
|
+
const doc = host.ownerDocument;
|
|
4181
|
+
const { trigger, list, status, reload, render } = createPreviewView(doc, root, bar);
|
|
4182
|
+
let choices = [];
|
|
4183
|
+
let busy = false;
|
|
4184
|
+
const load = async () => {
|
|
4185
|
+
const response = await fetch(endpoint, {
|
|
4186
|
+
credentials: "same-origin",
|
|
4187
|
+
cache: "no-store"
|
|
4188
|
+
});
|
|
4189
|
+
const envelope = await response.json();
|
|
4190
|
+
if (!response.ok || !envelope.ok || !Array.isArray(envelope.data)) throw new Error("Preview discovery unavailable");
|
|
4191
|
+
return envelope.data;
|
|
4192
|
+
};
|
|
4193
|
+
async function refresh() {
|
|
4194
|
+
if (busy) return;
|
|
4195
|
+
busy = true;
|
|
4196
|
+
try {
|
|
4197
|
+
choices = await load();
|
|
4198
|
+
render(choices);
|
|
4199
|
+
trigger.disabled = false;
|
|
4200
|
+
} catch {
|
|
4201
|
+
status.textContent = "Preview discovery unavailable";
|
|
4202
|
+
trigger.disabled = false;
|
|
4203
|
+
} finally {
|
|
4204
|
+
busy = false;
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
reload.addEventListener("click", () => {
|
|
4208
|
+
refresh();
|
|
4209
|
+
});
|
|
4210
|
+
list.addEventListener("click", async (event) => {
|
|
4211
|
+
const target = event.target.closest("[data-preview-id]");
|
|
4212
|
+
const id = target?.dataset["previewId"];
|
|
4213
|
+
if (!id || target?.disabled) return;
|
|
4214
|
+
trigger.disabled = true;
|
|
4215
|
+
status.textContent = "Checking preview…";
|
|
4216
|
+
try {
|
|
4217
|
+
const destination = (await load()).find((p) => p.id === id);
|
|
4218
|
+
if (!destination?.ready || !destination.url) throw new Error("unavailable");
|
|
4219
|
+
doc.defaultView?.location.assign(previewDestination(destination.url, doc.location.href));
|
|
4220
|
+
} catch {
|
|
4221
|
+
status.textContent = "Preview unavailable. Refresh to try again.";
|
|
4222
|
+
trigger.disabled = false;
|
|
4223
|
+
}
|
|
4224
|
+
});
|
|
4225
|
+
refresh();
|
|
4226
|
+
}
|
|
4227
|
+
//#endregion
|
|
4201
4228
|
//#region src/index.ts
|
|
4202
4229
|
/** Register <pinbox-toolbar>; no-op outside a browser or when already defined. */
|
|
4203
4230
|
function defineToolbarElement() {
|
|
@@ -4213,4 +4240,4 @@ const Pinbox = { init(config) {
|
|
|
4213
4240
|
} };
|
|
4214
4241
|
defineToolbarElement();
|
|
4215
4242
|
//#endregion
|
|
4216
|
-
export {
|
|
4243
|
+
export { HubTransport as a, applyHubEvent as c, upsertPin as d, PinboxToolbarElement as i, createStore as l, defineToolbarElement as n, HubError as o, mountPreviewSwitcher as r, appendThreadMessage as s, Pinbox as t, deriveUiStatus as u };
|