@cychien/ezdesign 0.0.0-stage → 0.9.1

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +615 -2
  3. package/dist/announcements/template-ERNKAKI5.svg +33 -0
  4. package/dist/cli.mjs +10095 -0
  5. package/dist/constants.d.ts +73 -0
  6. package/dist/events-worker.js +77 -0
  7. package/dist/extension/g/0.9.1-16d05e759b/dialog.css +1 -0
  8. package/dist/extension/g/0.9.1-16d05e759b/dialog.html +13 -0
  9. package/dist/extension/g/0.9.1-16d05e759b/dialog.js +1 -0
  10. package/dist/extension/g/0.9.1-16d05e759b/icon-128.png +0 -0
  11. package/dist/extension/g/0.9.1-16d05e759b/icon-16.png +0 -0
  12. package/dist/extension/g/0.9.1-16d05e759b/icon-32.png +0 -0
  13. package/dist/extension/g/0.9.1-16d05e759b/icon-48.png +0 -0
  14. package/dist/extension/g/0.9.1-16d05e759b/media.html +9 -0
  15. package/dist/extension/g/0.9.1-16d05e759b/media.js +1 -0
  16. package/dist/extension/g/0.9.1-16d05e759b/page.js +39 -0
  17. package/dist/extension/g/0.9.1-16d05e759b/setup.css +1 -0
  18. package/dist/extension/g/0.9.1-16d05e759b/setup.html +17 -0
  19. package/dist/extension/g/0.9.1-16d05e759b/setup.js +1 -0
  20. package/dist/extension/g/0.9.1-16d05e759b/worker.js +57 -0
  21. package/dist/extension/manifest.json +57 -0
  22. package/dist/native-host.mjs +20594 -0
  23. package/dist/overlay.css +422 -0
  24. package/dist/overlay.js +2642 -0
  25. package/dist/project-root.d.ts +10 -0
  26. package/dist/shell.css +4060 -0
  27. package/dist/shell.js +8284 -0
  28. package/dist/vite.d.ts +26 -0
  29. package/dist/vite.mjs +105 -0
  30. package/package.json +82 -4
  31. package/skills/ezdesign/SKILL.md +35 -0
  32. package/skills/ezdesign/reference/baseline.md +131 -0
  33. package/skills/ezdesign/reference/components.md +102 -0
  34. package/skills/ezdesign/reference/critique.md +129 -0
  35. package/skills/ezdesign/reference/design.md +247 -0
  36. package/skills/ezdesign/reference/inspiration.md +125 -0
  37. package/skills/ezdesign/reference/make.md +331 -0
  38. package/skills/ezdesign/reference/principles.md +130 -0
  39. package/skills/ezdesign/reference/product.md +127 -0
  40. package/skills/ezdesign/reference/stack.md +123 -0
  41. package/skills/ezdesign/reference/start.md +78 -0
  42. package/skills/ezdesign/scripts/page.d.mts +61 -0
  43. package/skills/ezdesign/scripts/page.mjs +908 -0
@@ -0,0 +1,2642 @@
1
+ "use strict";
2
+ (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/Add01Icon.js
8
+ var Add01Icon = [
9
+ ["path", { d: "M12.001 5.00003V19.002", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
10
+ ["path", { d: "M19.002 12.002L4.99998 12.002", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }]
11
+ ];
12
+
13
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/AlignSelectionIcon.js
14
+ var AlignSelectionIcon = [
15
+ ["path", { d: "M20 6V18M18 4H6M18 20H6M4 18V6", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
16
+ ["path", { d: "M22 4C22 5.10457 21.1046 6 20 6C18.8954 6 18 5.10457 18 4C18 2.89543 18.8954 2 20 2C21.1046 2 22 2.89543 22 4Z", stroke: "currentColor", strokeWidth: "1.5", key: "1" }],
17
+ ["path", { d: "M6 4C6 5.10457 5.10457 6 4 6C2.89543 6 2 5.10457 2 4C2 2.89543 2.89543 2 4 2C5.10457 2 6 2.89543 6 4Z", stroke: "currentColor", strokeWidth: "1.5", key: "2" }],
18
+ ["path", { d: "M22 20C22 21.1046 21.1046 22 20 22C18.8954 22 18 21.1046 18 20C18 18.8954 18.8954 18 20 18C21.1046 18 22 18.8954 22 20Z", stroke: "currentColor", strokeWidth: "1.5", key: "3" }],
19
+ ["path", { d: "M6 20C6 21.1046 5.10457 22 4 22C2.89543 22 2 21.1046 2 20C2 18.8954 2.89543 18 4 18C5.10457 18 6 18.8954 6 20Z", stroke: "currentColor", strokeWidth: "1.5", key: "4" }]
20
+ ];
21
+
22
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/TextSelectIcon.js
23
+ var TextSelectIcon = [
24
+ ["path", { d: "M9.99877 2.5C9.45899 2.50516 8.96002 2.51488 8.49784 2.53315M14.0012 2.5C14.541 2.50516 15.04 2.51488 15.5022 2.53315M19.0043 3.13088C19.43 3.32055 19.7943 3.56673 20.1138 3.88624C20.4333 4.20575 20.6794 4.56995 20.8691 4.99568M3.13088 4.99568C3.32056 4.56995 3.56673 4.20575 3.88624 3.88624C4.20575 3.56673 4.56995 3.32055 4.99568 3.13088M21.5 9.99877C21.4948 9.45899 21.4851 8.96002 21.4669 8.49784M21.5 14.0012C21.4948 14.5409 21.4851 15.0399 21.4669 15.5021M20.8691 19.0043C20.6794 19.43 20.4333 19.7943 20.1138 20.1138C19.7943 20.4333 19.43 20.6794 19.0043 20.8691M15.5022 21.4668C15.04 21.4851 14.541 21.4948 14.0012 21.5M9.99877 21.5C9.45898 21.4948 8.96 21.4851 8.49781 21.4668M4.99568 20.8691C4.56995 20.6794 4.20575 20.4333 3.88624 20.1138C3.56673 19.7943 3.32056 19.43 3.13088 19.0043M2.5 14.0012C2.50516 14.5409 2.51488 15.0399 2.53315 15.5021M2.5 9.99884C2.50516 9.45904 2.51488 8.96004 2.53315 8.49784", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
25
+ ["path", { d: "M7 8H15M7 12H17M7 16H13", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }]
26
+ ];
27
+
28
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/MagicWand01Icon.js
29
+ var MagicWand01Icon = [
30
+ ["path", { d: "M13.9258 12.7775L11.7775 10.6292C11.4847 10.3364 11.3383 10.19 11.1803 10.1117C10.8798 9.96277 10.527 9.96277 10.2264 10.1117C10.0685 10.19 9.92207 10.3364 9.62923 10.6292C9.33638 10.9221 9.18996 11.0685 9.11169 11.2264C8.96277 11.527 8.96277 11.8798 9.11169 12.1803C9.18996 12.3383 9.33638 12.4847 9.62923 12.7775L11.7775 14.9258M13.9258 12.7775L20.3708 19.2225C20.6636 19.5153 20.81 19.6617 20.8883 19.8197C21.0372 20.1202 21.0372 20.473 20.8883 20.7736C20.81 20.9315 20.6636 21.0779 20.3708 21.3708C20.0779 21.6636 19.9315 21.81 19.7736 21.8883C19.473 22.0372 19.1202 22.0372 18.8197 21.8883C18.6617 21.81 18.5153 21.6636 18.2225 21.3708L11.7775 14.9258M13.9258 12.7775L11.7775 14.9258", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
31
+ ["path", { d: "M17 2L17.2948 2.7966C17.6813 3.84117 17.8746 4.36345 18.2556 4.74445C18.6366 5.12545 19.1588 5.31871 20.2034 5.70523L21 6L20.2034 6.29477C19.1588 6.68129 18.6366 6.87456 18.2556 7.25555C17.8746 7.63655 17.6813 8.15883 17.2948 9.2034L17 10L16.7052 9.2034C16.3187 8.15884 16.1254 7.63655 15.7444 7.25555C15.3634 6.87455 14.8412 6.68129 13.7966 6.29477L13 6L13.7966 5.70523C14.8412 5.31871 15.3634 5.12545 15.7444 4.74445C16.1254 4.36345 16.3187 3.84117 16.7052 2.7966L17 2Z", stroke: "currentColor", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }],
32
+ ["path", { d: "M6 4L6.22108 4.59745C6.51097 5.38087 6.65592 5.77259 6.94167 6.05834C7.22741 6.34408 7.61913 6.48903 8.40255 6.77892L9 7L8.40255 7.22108C7.61913 7.51097 7.22741 7.65592 6.94166 7.94167C6.65592 8.22741 6.51097 8.61913 6.22108 9.40255L6 10L5.77892 9.40255C5.48903 8.61913 5.34408 8.22741 5.05833 7.94167C4.77259 7.65592 4.38087 7.51097 3.59745 7.22108L3 7L3.59745 6.77892C4.38087 6.48903 4.77259 6.34408 5.05833 6.05833C5.34408 5.77259 5.48903 5.38087 5.77892 4.59745L6 4Z", stroke: "currentColor", strokeLinejoin: "round", strokeWidth: "1.5", key: "2" }]
33
+ ];
34
+
35
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/Album02Icon.js
36
+ var Album02Icon = [
37
+ ["path", { d: "M17.5 5.73041V6.29128M17.7801 6.00085H17.25M18 6.00085C18 6.277 17.7761 6.50085 17.5 6.50085C17.2239 6.50085 17 6.277 17 6.00085C17 5.72471 17.2239 5.50085 17.5 5.50085C17.7761 5.50085 18 5.72471 18 6.00085Z", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
38
+ ["path", { d: "M9.5 7.99945C9.5 5.17102 9.5 3.75681 10.3787 2.87813C11.2574 1.99945 12.6716 1.99945 15.5 1.99945C18.3284 1.99945 19.7426 1.99945 20.6213 2.87813C21.5 3.75681 21.5 5.17102 21.5 7.99945V11.9995C21.5 14.8279 21.5 16.2421 20.6213 17.1208C19.7426 17.9995 18.3284 17.9995 15.5 17.9995C12.6716 17.9995 11.2574 17.9995 10.3787 17.1208C9.5 16.2421 9.5 14.8279 9.5 11.9995V7.99945Z", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }],
39
+ ["path", { d: "M6.62279 5.99878C4.49094 6.58122 3.36407 6.978 2.81621 7.92692C2.19489 9.00308 2.56092 10.3691 3.29297 13.1012L4.32825 16.9649C5.0603 19.6969 5.42632 21.0629 6.50248 21.6843C7.57864 22.3056 8.94467 21.9396 11.6767 21.2075C12.1593 21.0782 12.5992 20.9603 13 20.8479", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "2" }],
40
+ ["path", { d: "M9.5 13.9995L12.8693 10.6301C13.2731 10.2263 13.8208 9.99945 14.3919 9.99945C15.0873 9.99945 15.7399 10.3353 16.1441 10.9011L20.5 16.9995", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "3" }]
41
+ ];
42
+
43
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/File02Icon.js
44
+ var File02Icon = [
45
+ ["path", { d: "M8 17H16", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
46
+ ["path", { d: "M8 13H12", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }],
47
+ ["path", { d: "M13 2.5V3C13 5.82843 13 7.24264 13.8787 8.12132C14.7574 9 16.1716 9 19 9H19.5M20 10.6569V14C20 17.7712 20 19.6569 18.8284 20.8284C17.6569 22 15.7712 22 12 22C8.22876 22 6.34315 22 5.17157 20.8284C4 19.6569 4 17.7712 4 14V9.45584C4 6.21082 4 4.58831 4.88607 3.48933C5.06508 3.26731 5.26731 3.06508 5.48933 2.88607C6.58831 2 8.21082 2 11.4558 2C12.1614 2 12.5141 2 12.8372 2.11401C12.9044 2.13772 12.9702 2.165 13.0345 2.19575C13.3436 2.34355 13.593 2.593 14.0919 3.09188L18.8284 7.82843C19.4065 8.40649 19.6955 8.69552 19.8478 9.06306C20 9.4306 20 9.83935 20 10.6569Z", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "2" }]
48
+ ];
49
+
50
+ // src/client/draft.ts
51
+ var NBSP = "\xA0";
52
+ var refMarker = (n) => `[ref ${n}]`;
53
+ var refChipText = (n, chosen = false) => `${chosen ? "\u63A2\u7D22\u6A23\u5F0F" : "\u9078\u53D6\u5143\u7D20"} ${n}`;
54
+ var pickChipText = (n) => `\u9078\u53D6\u7D20\u6750 ${n}`;
55
+ var fileMarker = (n) => `[file ${n}]`;
56
+ var skillMarker = (n) => `[skill ${n}]`;
57
+ function draftText(body) {
58
+ const out = [];
59
+ let file = 0;
60
+ let skill = 0;
61
+ for (const node of body) {
62
+ if (node.t === "text") out.push(node.v);
63
+ else if (node.t === "ref" && node.anchor) out.push(refMarker(node.n));
64
+ else if (node.t === "file" && node.id) out.push(fileMarker(++file));
65
+ else if (node.t === "skill") out.push(skillMarker(++skill));
66
+ else if (node.t === "pick") out.push(node.files.map(() => fileMarker(++file)).join(" "));
67
+ }
68
+ return out.join("").replaceAll(NBSP, " ").trim();
69
+ }
70
+ function draftSkills(body) {
71
+ return body.flatMap((node) => node.t === "skill" ? [node.name] : []);
72
+ }
73
+ function draftRefs(body) {
74
+ const out = [];
75
+ for (const node of body) {
76
+ if (node.t === "ref" && node.anchor) {
77
+ out.push({
78
+ n: node.n,
79
+ anchor: node.anchor,
80
+ label: node.label,
81
+ ...node.variant ? { variant: node.variant } : {}
82
+ });
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ function nextRefNumber(body) {
88
+ let max = 0;
89
+ for (const node of body) {
90
+ if (node.t === "ref" && node.anchor && node.n > max) max = node.n;
91
+ }
92
+ return max + 1;
93
+ }
94
+ function draftFileIds(body) {
95
+ return body.flatMap((n) => n.t === "file" && n.id ? [n.id] : n.t === "pick" ? n.files.map((f) => f.id) : []);
96
+ }
97
+ function draftPendingNames(body) {
98
+ return body.flatMap((n) => n.t === "file" && !n.id ? [n.name] : []);
99
+ }
100
+ function hasPendingRef(body) {
101
+ return body.some((n) => n.t === "ref" && !n.anchor);
102
+ }
103
+ function restorableBody(body) {
104
+ return normalizeDraft(body.filter((n) => !(n.t === "file" && !n.id)));
105
+ }
106
+ function normalizeDraft(body) {
107
+ const out = [];
108
+ for (const node of body) {
109
+ if (node.t !== "text") {
110
+ out.push(node);
111
+ continue;
112
+ }
113
+ if (!node.v) continue;
114
+ const last = out[out.length - 1];
115
+ if (last?.t === "text") out[out.length - 1] = { t: "text", v: last.v + node.v };
116
+ else out.push(node);
117
+ }
118
+ return out;
119
+ }
120
+ var GRACE_MS = 24 * 60 * 6e4;
121
+ function draftExpired(draft, now, graceMs) {
122
+ return now - draft.createdAt >= graceMs;
123
+ }
124
+
125
+ // src/client/icon.ts
126
+ var SVG_NS = "http://www.w3.org/2000/svg";
127
+ var kebab = (key) => key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
128
+ function icon(node, size = 16) {
129
+ const svg = document.createElementNS(SVG_NS, "svg");
130
+ svg.setAttribute("viewBox", "0 0 24 24");
131
+ svg.setAttribute("width", String(size));
132
+ svg.setAttribute("height", String(size));
133
+ svg.setAttribute("fill", "none");
134
+ svg.setAttribute("aria-hidden", "true");
135
+ svg.setAttribute("data-ez-ui", "");
136
+ for (const [tag, attrs] of node) {
137
+ const child = document.createElementNS(SVG_NS, tag);
138
+ for (const [key, value] of Object.entries(attrs)) {
139
+ if (key === "key") continue;
140
+ child.setAttribute(kebab(key), String(value));
141
+ }
142
+ svg.appendChild(child);
143
+ }
144
+ return svg;
145
+ }
146
+
147
+ // src/client/slash.ts
148
+ var MAX_QUERY = 24;
149
+ var BREAK = /[\s ]/;
150
+ var menuCount = 0;
151
+ function detectSlash(before, trigger = "/") {
152
+ const start = before.lastIndexOf(trigger);
153
+ if (start === -1) return null;
154
+ const prev = start === 0 ? "" : before[start - 1];
155
+ if (prev && !BREAK.test(prev)) return null;
156
+ const query = before.slice(start + 1);
157
+ if (query.length > MAX_QUERY || BREAK.test(query)) return null;
158
+ return { start, query };
159
+ }
160
+ function filterCommands(commands, query) {
161
+ const available = commands.filter((c) => c.enabled?.() ?? true);
162
+ const q = query.trim().toLowerCase();
163
+ if (!q) return available;
164
+ return available.filter(
165
+ (c) => [c.id, c.label, ...c.keywords].some((term) => term.toLowerCase().includes(q))
166
+ );
167
+ }
168
+ function attachSlashMenu(editable, {
169
+ mk,
170
+ commands,
171
+ triggerChar = "/",
172
+ title = "\u6307\u4EE4",
173
+ load
174
+ }) {
175
+ const doc = editable.ownerDocument;
176
+ const view = doc.defaultView;
177
+ const listId = `ez-slash-${++menuCount}`;
178
+ let menu = null;
179
+ let trigger = null;
180
+ let shown = [];
181
+ let active = 0;
182
+ let loaded = null;
183
+ let loading = false;
184
+ function readTrigger() {
185
+ const selection = doc.getSelection();
186
+ if (!selection?.isCollapsed || selection.rangeCount === 0) return null;
187
+ const range = selection.getRangeAt(0);
188
+ const node = range.startContainer;
189
+ if (!editable.contains(node) || node.nodeType !== Node.TEXT_NODE) return null;
190
+ const text = node;
191
+ const hit = detectSlash((text.nodeValue ?? "").slice(0, range.startOffset), triggerChar);
192
+ return hit ? { node: text, start: hit.start, query: hit.query } : null;
193
+ }
194
+ function slashRect() {
195
+ if (!trigger?.node.isConnected) return null;
196
+ const range = doc.createRange();
197
+ range.setStart(trigger.node, trigger.start);
198
+ range.setEnd(trigger.node, Math.min(trigger.node.length, trigger.start + 1));
199
+ return range.getBoundingClientRect();
200
+ }
201
+ function place() {
202
+ if (!menu || !view) return;
203
+ const at = slashRect();
204
+ if (!at) return;
205
+ const box = menu.getBoundingClientRect();
206
+ const left = Math.max(8, Math.min(at.left, view.innerWidth - box.width - 8));
207
+ const below = at.bottom + 6;
208
+ const above = at.top - box.height - 6;
209
+ const crowded = below + box.height > view.innerHeight - 8 || at.top > view.innerHeight * 0.6;
210
+ const top = crowded && above >= 8 ? above : Math.min(below, view.innerHeight - box.height - 8);
211
+ menu.style.left = `${left}px`;
212
+ menu.style.top = `${top}px`;
213
+ }
214
+ function paintActive() {
215
+ const list = menu?.querySelector(".ez-slash-list");
216
+ if (!list) return;
217
+ [...list.children].forEach((item, i) => {
218
+ item.toggleAttribute("data-active", i === active);
219
+ item.setAttribute("aria-selected", String(i === active));
220
+ });
221
+ editable.setAttribute("aria-activedescendant", `${listId}-${active}`);
222
+ }
223
+ function render() {
224
+ if (!menu) return;
225
+ menu.textContent = "";
226
+ const headEl = mk("div", "ez-slash-head");
227
+ headEl.textContent = title;
228
+ const list = mk("div", "ez-slash-list");
229
+ list.id = listId;
230
+ list.setAttribute("role", "listbox");
231
+ list.setAttribute("aria-label", title);
232
+ menu.append(headEl, list);
233
+ shown.forEach((command, i) => {
234
+ const item = mk("div", "ez-slash-item");
235
+ item.id = `${listId}-${i}`;
236
+ item.setAttribute("role", "option");
237
+ const label = mk("span", "ez-slash-label");
238
+ label.textContent = command.label;
239
+ if (command.icon) item.append(icon(command.icon, 16));
240
+ item.append(label);
241
+ const hintText = typeof command.hint === "function" ? command.hint() : command.hint;
242
+ if (hintText) {
243
+ const hint = mk("span", "ez-slash-hint");
244
+ hint.textContent = hintText;
245
+ item.append(hint);
246
+ }
247
+ item.addEventListener("mousedown", (e) => {
248
+ e.preventDefault();
249
+ choose(command);
250
+ });
251
+ item.addEventListener("mousemove", () => {
252
+ if (active === i) return;
253
+ active = i;
254
+ paintActive();
255
+ });
256
+ list.appendChild(item);
257
+ });
258
+ paintActive();
259
+ }
260
+ function onSelectionChange() {
261
+ if (!readTrigger()) close();
262
+ }
263
+ function onBlur() {
264
+ close();
265
+ }
266
+ function open() {
267
+ menu = mk("div", "ez-slash-menu");
268
+ doc.body.appendChild(menu);
269
+ editable.setAttribute("aria-controls", listId);
270
+ doc.addEventListener("selectionchange", onSelectionChange);
271
+ editable.addEventListener("blur", onBlur);
272
+ }
273
+ function close() {
274
+ if (!menu) return false;
275
+ menu.remove();
276
+ menu = null;
277
+ trigger = null;
278
+ editable.removeAttribute("aria-controls");
279
+ editable.removeAttribute("aria-activedescendant");
280
+ doc.removeEventListener("selectionchange", onSelectionChange);
281
+ editable.removeEventListener("blur", onBlur);
282
+ return true;
283
+ }
284
+ function choose(command) {
285
+ const at = trigger;
286
+ close();
287
+ if (at?.node.isConnected) {
288
+ const end = Math.min(at.node.length, at.start + 1 + at.query.length);
289
+ const cut = doc.createRange();
290
+ cut.setStart(at.node, at.start);
291
+ cut.setEnd(at.node, end);
292
+ cut.deleteContents();
293
+ const caret = doc.createRange();
294
+ caret.setStart(at.node, at.start);
295
+ caret.collapse(true);
296
+ const selection = doc.getSelection();
297
+ selection?.removeAllRanges();
298
+ selection?.addRange(caret);
299
+ editable.dispatchEvent(new InputEvent("input", { bubbles: true }));
300
+ }
301
+ editable.focus();
302
+ command.run();
303
+ }
304
+ function sync() {
305
+ const hit = readTrigger();
306
+ if (!hit) {
307
+ close();
308
+ return;
309
+ }
310
+ if (load && !loaded && !loading) {
311
+ loading = true;
312
+ void Promise.resolve(load()).then((result) => {
313
+ loaded = result;
314
+ }).catch(() => {
315
+ loaded = [];
316
+ }).finally(() => {
317
+ loading = false;
318
+ if (readTrigger()) sync();
319
+ });
320
+ }
321
+ const matches = filterCommands(load ? loaded ?? [] : commands, hit.query);
322
+ if (matches.length === 0) {
323
+ close();
324
+ return;
325
+ }
326
+ const queryChanged = trigger?.query !== hit.query;
327
+ trigger = hit;
328
+ shown = matches;
329
+ if (!menu) open();
330
+ if (queryChanged) active = 0;
331
+ active = Math.min(active, shown.length - 1);
332
+ render();
333
+ place();
334
+ }
335
+ editable.addEventListener("input", (e) => {
336
+ if (e.isComposing) return;
337
+ sync();
338
+ });
339
+ editable.addEventListener("keydown", (e) => {
340
+ if (!menu) return;
341
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
342
+ e.preventDefault();
343
+ e.stopPropagation();
344
+ const step = e.key === "ArrowDown" ? 1 : shown.length - 1;
345
+ active = (active + step) % shown.length;
346
+ paintActive();
347
+ return;
348
+ }
349
+ if (e.key === "Enter" || e.key === "Tab") {
350
+ if (e.metaKey || e.ctrlKey) return;
351
+ e.preventDefault();
352
+ e.stopPropagation();
353
+ const command = shown[active];
354
+ if (command) choose(command);
355
+ return;
356
+ }
357
+ if (e.key === "Escape") {
358
+ e.preventDefault();
359
+ e.stopPropagation();
360
+ close();
361
+ }
362
+ });
363
+ return { close };
364
+ }
365
+
366
+ // src/client/attach.ts
367
+ var MAX_BYTES = 8 * 1024 * 1024;
368
+ function directAttachments(api) {
369
+ return {
370
+ async upload(file) {
371
+ const res = await fetch(`${api}/attachments`, {
372
+ method: "POST",
373
+ headers: {
374
+ "content-type": file.type || "application/octet-stream",
375
+ // A filename is free to hold bytes no header may carry.
376
+ "x-ez-name": encodeURIComponent(file.name || "image.png")
377
+ },
378
+ body: file
379
+ });
380
+ if (!res.ok) {
381
+ const body = await res.json().catch(() => null);
382
+ throw new Error(body?.error ?? `HTTP ${res.status}`);
383
+ }
384
+ return await res.json();
385
+ },
386
+ discard(id) {
387
+ fetch(`${api}/attachments/${id}`, { method: "DELETE" }).catch(() => {
388
+ });
389
+ }
390
+ };
391
+ }
392
+ var CHIP_ATTR = "data-ez-chip";
393
+ var REF_ATTR = "data-ez-ref";
394
+ var SKILL_ATTR = "data-ez-skill";
395
+ var PICK_ATTR = "data-ez-pick";
396
+ function attachify({
397
+ attachments,
398
+ mk,
399
+ className,
400
+ placeholder,
401
+ onChange,
402
+ commands,
403
+ skills,
404
+ chooseLibrary
405
+ }) {
406
+ const editable = mk("div", className);
407
+ editable.setAttribute("contenteditable", "plaintext-only");
408
+ editable.setAttribute("role", "textbox");
409
+ editable.setAttribute("aria-multiline", "true");
410
+ editable.setAttribute("aria-label", placeholder);
411
+ editable.dataset.placeholder = placeholder;
412
+ const field = mk("div", "ez-attach-field");
413
+ const error = mk("div", "ez-attach-error");
414
+ const wrap = mk("div", "ez-attach");
415
+ error.hidden = true;
416
+ field.append(editable);
417
+ const picker = mk("input", "ez-file-picker");
418
+ picker.type = "file";
419
+ picker.multiple = true;
420
+ picker.hidden = true;
421
+ picker.addEventListener("change", () => {
422
+ for (const file of take(picker.files ?? void 0)) add(file);
423
+ picker.value = "";
424
+ });
425
+ wrap.append(field, error, picker);
426
+ const slash = attachSlashMenu(editable, {
427
+ mk,
428
+ commands: [
429
+ {
430
+ id: "file",
431
+ label: "File",
432
+ hint: "\u9644\u52A0\u6A94\u6848",
433
+ keywords: ["file", "attach", "upload", "\u6A94\u6848", "\u9644\u4EF6", "\u5716\u7247"],
434
+ icon: File02Icon,
435
+ run: () => picker.click()
436
+ },
437
+ ...chooseLibrary ? [
438
+ {
439
+ id: "library",
440
+ label: "Library",
441
+ hint: "\u5F9E\u7D20\u6750\u5EAB\u9078\u64C7",
442
+ keywords: ["library", "reference", "asset", "inspiration", "\u7D20\u6750", "\u7D20\u6750\u5EAB", "\u53C3\u8003", "\u9748\u611F"],
443
+ icon: Album02Icon,
444
+ run: () => void addFromLibrary(chooseLibrary)
445
+ }
446
+ ] : [],
447
+ ...commands ?? []
448
+ ]
449
+ });
450
+ const skillMenu = attachSlashMenu(editable, {
451
+ mk,
452
+ triggerChar: "$",
453
+ title: "Skills",
454
+ commands: [],
455
+ load: skills
456
+ });
457
+ const uploading = /* @__PURE__ */ new Set();
458
+ let errorTimer = 0;
459
+ function showError(message) {
460
+ error.textContent = message;
461
+ error.hidden = false;
462
+ clearTimeout(errorTimer);
463
+ errorTimer = setTimeout(() => {
464
+ error.hidden = true;
465
+ }, 6e3);
466
+ }
467
+ function paintEmpty() {
468
+ const empty = !editable.textContent?.trim() && !editable.querySelector(".ez-chip");
469
+ editable.toggleAttribute("data-empty", empty);
470
+ }
471
+ function paintPicks() {
472
+ editable.querySelectorAll(`[${PICK_ATTR}] .ez-chip-name`).forEach((name, i) => {
473
+ name.textContent = pickChipText(i + 1);
474
+ });
475
+ }
476
+ function changed() {
477
+ paintEmpty();
478
+ paintPicks();
479
+ onChange?.();
480
+ }
481
+ function del(id) {
482
+ attachments.discard(id);
483
+ }
484
+ function makeChip(name, attr, value, glyph) {
485
+ const chip = mk("span", "ez-chip");
486
+ chip.setAttribute("contenteditable", "false");
487
+ chip.setAttribute(attr, value);
488
+ if (glyph) chip.append(icon(glyph, 11));
489
+ chip.append(mk("span", "ez-chip-name"));
490
+ chip.querySelector(".ez-chip-name").textContent = name;
491
+ if (attr === REF_ATTR) chip.classList.add("ez-chip-ref");
492
+ if (attr === SKILL_ATTR) chip.classList.add("ez-chip-skill");
493
+ if (!value) chip.classList.add("ez-chip-pending");
494
+ return chip;
495
+ }
496
+ const fileChip = (name, id = "") => makeChip(name, CHIP_ATTR, id, File02Icon);
497
+ const refChip = (ref, placeholder2) => {
498
+ const chip = makeChip(
499
+ ref ? refChipText(ref.n, !!ref.variant) : placeholder2,
500
+ REF_ATTR,
501
+ ref ? JSON.stringify(ref) : "",
502
+ ref?.variant ? MagicWand01Icon : AlignSelectionIcon
503
+ );
504
+ if (ref?.variant) chip.classList.add("ez-chip-chosen");
505
+ const title = ref?.variant ? [ref.variant.name, ref.label].filter(Boolean).join(" \xB7 ") : ref?.label ?? "";
506
+ if (title) chip.title = title;
507
+ return chip;
508
+ };
509
+ const pickChip = (files) => {
510
+ const chip = makeChip("", PICK_ATTR, JSON.stringify(files), Album02Icon);
511
+ chip.title = files.map((f) => f.name).join("\n");
512
+ return chip;
513
+ };
514
+ const skillChip = (name) => makeChip(`$${name}`, SKILL_ATTR, name, null);
515
+ const pendingRef = () => editable.querySelector(`[${REF_ATTR}=""]`);
516
+ function placeCaretAfter(node) {
517
+ const caret = document.createRange();
518
+ caret.setStartAfter(node);
519
+ caret.collapse(true);
520
+ const selection = document.getSelection();
521
+ selection?.removeAllRanges();
522
+ selection?.addRange(caret);
523
+ }
524
+ function insertAtCaret(node) {
525
+ const selection = document.getSelection();
526
+ const live = selection?.rangeCount ? selection.getRangeAt(0) : null;
527
+ const range = live && editable.contains(live.commonAncestorContainer) ? live : null;
528
+ if (range) {
529
+ range.deleteContents();
530
+ range.insertNode(node);
531
+ } else {
532
+ editable.appendChild(node);
533
+ }
534
+ const after = document.createTextNode(NBSP);
535
+ node.parentNode?.insertBefore(after, node.nextSibling);
536
+ placeCaretAfter(after);
537
+ }
538
+ function add(file) {
539
+ if (file.size > MAX_BYTES) {
540
+ showError(`${file.name || "\u9019\u500B\u6A94\u6848"} \u8D85\u904E\u55AE\u6A94 8 MB \u4E0A\u9650`);
541
+ return;
542
+ }
543
+ const chip = fileChip(file.name || "\u8CBC\u4E0A\u7684\u5716\u7247");
544
+ insertAtCaret(chip);
545
+ uploading.add(chip);
546
+ changed();
547
+ void upload(chip, file);
548
+ }
549
+ async function upload(chip, file) {
550
+ try {
551
+ const saved = await attachments.upload(file);
552
+ if (!editable.contains(chip)) {
553
+ del(saved.id);
554
+ return;
555
+ }
556
+ chip.setAttribute(CHIP_ATTR, saved.id);
557
+ chip.querySelector(".ez-chip-name").textContent = saved.name;
558
+ chip.classList.remove("ez-chip-pending");
559
+ } catch (err) {
560
+ chip.remove();
561
+ showError(`\u300C${file.name || "\u6A94\u6848"}\u300D\u4E0A\u50B3\u5931\u6557\uFF1A${err.message}`);
562
+ } finally {
563
+ uploading.delete(chip);
564
+ changed();
565
+ }
566
+ }
567
+ async function addFromLibrary(choose) {
568
+ const selection = document.getSelection();
569
+ const live = selection?.rangeCount ? selection.getRangeAt(0) : null;
570
+ const spot = live && editable.contains(live.commonAncestorContainer) ? live.cloneRange() : null;
571
+ let files;
572
+ try {
573
+ files = await choose();
574
+ } catch (err) {
575
+ showError(`\u6C92\u9644\u4E0A\u7D20\u6750\uFF1A${err.message}`);
576
+ return;
577
+ }
578
+ editable.focus();
579
+ if (spot && editable.contains(spot.commonAncestorContainer)) {
580
+ selection?.removeAllRanges();
581
+ selection?.addRange(spot);
582
+ }
583
+ if (!files.length) return;
584
+ insertAtCaret(pickChip(files));
585
+ changed();
586
+ }
587
+ function take(list) {
588
+ return list ? [...list] : [];
589
+ }
590
+ function beginRef(label) {
591
+ if (hasPendingRef(snapshot())) return false;
592
+ insertAtCaret(refChip(null, label));
593
+ changed();
594
+ return true;
595
+ }
596
+ function resolveRef(ref) {
597
+ const chip = pendingRef();
598
+ if (!chip) return;
599
+ const numbered = { ...ref, n: nextRefNumber(snapshot()) };
600
+ chip.setAttribute(REF_ATTR, JSON.stringify(numbered));
601
+ chip.querySelector(".ez-chip-name").textContent = refChipText(numbered.n, !!numbered.variant);
602
+ if (numbered.label) chip.title = numbered.label;
603
+ chip.classList.remove("ez-chip-pending");
604
+ if (chip.nextSibling) placeCaretAfter(chip.nextSibling);
605
+ changed();
606
+ }
607
+ function insertRef(ref) {
608
+ const numbered = { ...ref, n: nextRefNumber(snapshot()) };
609
+ insertAtCaret(refChip(numbered, numbered.label));
610
+ changed();
611
+ }
612
+ function cancelRef() {
613
+ const chip = pendingRef();
614
+ if (!chip) return;
615
+ const after = chip.nextSibling;
616
+ chip.remove();
617
+ if (after?.nodeType === Node.TEXT_NODE && after.nodeValue?.startsWith(NBSP)) {
618
+ after.nodeValue = after.nodeValue.slice(1);
619
+ if (!after.nodeValue) after.parentNode?.removeChild(after);
620
+ }
621
+ changed();
622
+ }
623
+ function snapshot() {
624
+ const out = [];
625
+ collectNodes(editable, out);
626
+ return out;
627
+ }
628
+ function restore(body) {
629
+ editable.textContent = "";
630
+ const nodes = restorableBody(body);
631
+ for (const node of nodes) {
632
+ if (node.t === "text") editable.appendChild(document.createTextNode(node.v));
633
+ else if (node.t === "file") editable.appendChild(fileChip(node.name, node.id));
634
+ else if (node.t === "skill") editable.appendChild(skillChip(node.name));
635
+ else if (node.t === "pick") editable.appendChild(pickChip(node.files));
636
+ else {
637
+ const ref = node.anchor ? {
638
+ n: node.n,
639
+ anchor: node.anchor,
640
+ label: node.label,
641
+ ...node.variant ? { variant: node.variant } : {}
642
+ } : null;
643
+ editable.appendChild(refChip(ref, node.label));
644
+ }
645
+ }
646
+ if (nodes[nodes.length - 1]?.t !== "text") {
647
+ editable.appendChild(document.createTextNode(NBSP));
648
+ }
649
+ if (editable.lastChild) placeCaretAfter(editable.lastChild);
650
+ changed();
651
+ }
652
+ editable.addEventListener("paste", (e) => {
653
+ const data = e.clipboardData;
654
+ const files = take(data?.files);
655
+ if (files.length === 0) return;
656
+ if (!data?.getData("text/plain")) e.preventDefault();
657
+ e.stopPropagation();
658
+ for (const file of files) add(file);
659
+ });
660
+ const isChipNode = (node) => node instanceof HTMLElement && (node.hasAttribute(CHIP_ATTR) || node.hasAttribute(REF_ATTR) || node.hasAttribute(SKILL_ATTR) || node.hasAttribute(PICK_ATTR));
661
+ editable.addEventListener("beforeinput", (e) => {
662
+ if (e.inputType !== "deleteContentBackward") return;
663
+ const selection = document.getSelection();
664
+ if (!selection?.isCollapsed || selection.rangeCount === 0) return;
665
+ const range = selection.getRangeAt(0);
666
+ const node = range.startContainer;
667
+ if (node.nodeType !== Node.TEXT_NODE) return;
668
+ const text = node;
669
+ if (text.nodeValue !== NBSP || range.startOffset !== 1) return;
670
+ const before = text.previousSibling;
671
+ if (!isChipNode(before)) return;
672
+ e.preventDefault();
673
+ text.remove();
674
+ placeCaretAfter(before);
675
+ changed();
676
+ });
677
+ editable.addEventListener("input", changed);
678
+ const dragging = (e) => e.dataTransfer?.types.includes("Files") ?? false;
679
+ let depth = 0;
680
+ function endDrag() {
681
+ depth = 0;
682
+ field.removeAttribute("data-drop");
683
+ }
684
+ field.addEventListener("dragenter", (e) => {
685
+ if (!dragging(e)) return;
686
+ e.preventDefault();
687
+ depth++;
688
+ field.toggleAttribute("data-drop", true);
689
+ });
690
+ field.addEventListener("dragover", (e) => {
691
+ if (!dragging(e)) return;
692
+ e.preventDefault();
693
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
694
+ });
695
+ field.addEventListener("dragleave", (e) => {
696
+ if (!dragging(e)) return;
697
+ if (--depth <= 0) endDrag();
698
+ });
699
+ field.addEventListener("drop", (e) => {
700
+ const files = take(e.dataTransfer?.files);
701
+ endDrag();
702
+ if (files.length === 0) return;
703
+ e.preventDefault();
704
+ e.stopPropagation();
705
+ if (document.activeElement !== editable) editable.focus();
706
+ for (const file of files) add(file);
707
+ });
708
+ paintEmpty();
709
+ return {
710
+ wrap,
711
+ field,
712
+ editable,
713
+ // All three read the same single walk, so the text a comment reads as, the
714
+ // files it carries and the elements it points at can never disagree about
715
+ // order - and all three are testable without a browser.
716
+ text: () => draftText(snapshot()),
717
+ skills: () => draftSkills(snapshot()),
718
+ insertSkill(name) {
719
+ insertAtCaret(skillChip(name));
720
+ changed();
721
+ },
722
+ ids: () => draftFileIds(snapshot()),
723
+ refs: () => draftRefs(snapshot()),
724
+ pending: () => [...uploading].filter((c) => editable.contains(c)).length,
725
+ pendingNames: () => draftPendingNames(snapshot()),
726
+ // Either menu, whichever is up - at most one can be, since a trigger only
727
+ // counts at the start of the word being typed.
728
+ closeSlash: () => slash.close() || skillMenu.close(),
729
+ beginRef,
730
+ resolveRef,
731
+ insertRef,
732
+ cancelRef,
733
+ snapshot,
734
+ restore,
735
+ reset() {
736
+ slash.close();
737
+ uploading.clear();
738
+ editable.textContent = "";
739
+ changed();
740
+ },
741
+ discard() {
742
+ slash.close();
743
+ for (const id of draftFileIds(snapshot())) del(id);
744
+ uploading.clear();
745
+ editable.textContent = "";
746
+ changed();
747
+ }
748
+ };
749
+ }
750
+ function collectNodes(node, out) {
751
+ for (const child of node.childNodes) {
752
+ if (child.nodeType === Node.TEXT_NODE) {
753
+ out.push({ t: "text", v: child.nodeValue ?? "" });
754
+ continue;
755
+ }
756
+ if (!(child instanceof HTMLElement)) continue;
757
+ const name = child.querySelector(".ez-chip-name")?.textContent ?? "";
758
+ if (child.hasAttribute(REF_ATTR)) {
759
+ const ref = parseRef(child.getAttribute(REF_ATTR));
760
+ out.push({
761
+ t: "ref",
762
+ n: ref?.n ?? 0,
763
+ anchor: ref?.anchor ?? null,
764
+ label: ref?.label ?? name,
765
+ ...ref?.variant ? { variant: ref.variant } : {}
766
+ });
767
+ continue;
768
+ }
769
+ if (child.hasAttribute(CHIP_ATTR)) {
770
+ out.push({ t: "file", id: child.getAttribute(CHIP_ATTR) ?? "", name });
771
+ continue;
772
+ }
773
+ if (child.hasAttribute(PICK_ATTR)) {
774
+ out.push({ t: "pick", files: parsePick(child.getAttribute(PICK_ATTR)) });
775
+ continue;
776
+ }
777
+ if (child.hasAttribute(SKILL_ATTR)) {
778
+ out.push({ t: "skill", name: child.getAttribute(SKILL_ATTR) ?? "" });
779
+ continue;
780
+ }
781
+ if (child.tagName === "BR") {
782
+ out.push({ t: "text", v: "\n" });
783
+ continue;
784
+ }
785
+ const prev = lastText(out);
786
+ if (prev !== void 0 && !prev.endsWith("\n")) out.push({ t: "text", v: "\n" });
787
+ collectNodes(child, out);
788
+ }
789
+ }
790
+ function lastText(out) {
791
+ for (let i = out.length - 1; i >= 0; i--) {
792
+ const node = out[i];
793
+ if (node?.t === "text") return node.v;
794
+ }
795
+ return void 0;
796
+ }
797
+ function parseRef(raw) {
798
+ if (!raw) return null;
799
+ try {
800
+ return JSON.parse(raw);
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+ function parsePick(raw) {
806
+ try {
807
+ return JSON.parse(raw ?? "[]");
808
+ } catch {
809
+ return [];
810
+ }
811
+ }
812
+
813
+ // src/client/overlay-link.ts
814
+ function linkFromScript(script, api) {
815
+ if (script?.dataset.ezLink !== "relay") return directLink(api);
816
+ const shellOrigin = script.dataset.ezShell;
817
+ return shellOrigin ? relayLink({ shellOrigin }) : inertLink();
818
+ }
819
+ function libraryChooser(post2, fromShell) {
820
+ const pending = /* @__PURE__ */ new Map();
821
+ let seq = 0;
822
+ window.addEventListener("message", (e) => {
823
+ if (!fromShell(e)) return;
824
+ const data = e.data;
825
+ if (data?.type !== "ez:library-picked" || typeof data.reqId !== "string") return;
826
+ const waiting = pending.get(data.reqId);
827
+ pending.delete(data.reqId);
828
+ if (!waiting) return;
829
+ if (typeof data.error === "string") waiting.reject(new Error(data.error));
830
+ else waiting.resolve(Array.isArray(data.files) ? data.files : []);
831
+ });
832
+ return () => new Promise((resolve, reject) => {
833
+ const reqId = `l${++seq}-${Date.now().toString(36)}`;
834
+ pending.set(reqId, { resolve, reject });
835
+ post2({ type: "ez:library-pick", reqId });
836
+ });
837
+ }
838
+ function directLink(api) {
839
+ const post2 = (message) => window.parent?.postMessage(message, location.origin);
840
+ const fromShell = (e) => e.source === window.parent && e.origin === location.origin;
841
+ return {
842
+ kind: "direct",
843
+ post: post2,
844
+ annotate: async (body) => {
845
+ const res = await fetch(`${api}/annotations`, {
846
+ method: "POST",
847
+ headers: { "content-type": "application/json" },
848
+ body: JSON.stringify(body)
849
+ });
850
+ if (!res.ok) throw new Error(`annotation rejected (${res.status})`);
851
+ },
852
+ attachments: directAttachments(api),
853
+ ...window.parent !== window ? { chooseLibrary: libraryChooser(post2, fromShell) } : {},
854
+ subscribe: (onSnapshot) => {
855
+ if (window.parent === window) {
856
+ const events = new EventSource(`${api}/events`);
857
+ events.onmessage = (e) => onSnapshot(JSON.parse(e.data));
858
+ return;
859
+ }
860
+ window.addEventListener("message", (e) => {
861
+ const data = e.data;
862
+ if (e.source === window.parent && e.origin === location.origin && data?.type === "ez:snapshot" && data.snapshot) {
863
+ onSnapshot(data.snapshot);
864
+ }
865
+ });
866
+ },
867
+ fromShell: (e) => e.origin === location.origin
868
+ };
869
+ }
870
+ var RELAY_TIMEOUT_MS = 1e4;
871
+ function relayLink(config) {
872
+ const { shellOrigin } = config;
873
+ const pending = /* @__PURE__ */ new Map();
874
+ const listeners = [];
875
+ let last = null;
876
+ let seq = 0;
877
+ const fromShell = (e) => e.source === window.parent && e.origin === shellOrigin;
878
+ const post2 = (message) => window.parent?.postMessage(message, shellOrigin);
879
+ window.addEventListener("message", (e) => {
880
+ if (!fromShell(e)) return;
881
+ const data = e.data;
882
+ if (data?.type === "ez:relay-done" && typeof data.reqId === "string") {
883
+ const waiting = pending.get(data.reqId);
884
+ pending.delete(data.reqId);
885
+ if (!waiting) return;
886
+ if (data.ok) waiting.resolve(data.body);
887
+ else waiting.reject(new Error(data.body?.error ?? "refused"));
888
+ }
889
+ if (data?.type === "ez:snapshot" && data.snapshot) {
890
+ last = data.snapshot;
891
+ for (const listener of listeners) listener(data.snapshot);
892
+ }
893
+ });
894
+ const request = (op, transfer) => new Promise((resolve, reject) => {
895
+ const reqId = `r${++seq}-${Date.now().toString(36)}`;
896
+ pending.set(reqId, { resolve, reject });
897
+ window.setTimeout(() => {
898
+ if (pending.delete(reqId)) reject(new Error("the review did not answer"));
899
+ }, RELAY_TIMEOUT_MS);
900
+ window.parent?.postMessage({ type: "ez:relay", reqId, op }, shellOrigin, transfer);
901
+ });
902
+ return {
903
+ kind: "relay",
904
+ post: post2,
905
+ annotate: async (body) => {
906
+ await request({ op: "annotate", body });
907
+ },
908
+ attachments: {
909
+ upload: async (file) => {
910
+ const bytes = await file.arrayBuffer();
911
+ const name = file.name || "image.png";
912
+ return await request(
913
+ { op: "upload", name, mime: file.type || "application/octet-stream", bytes },
914
+ [bytes]
915
+ );
916
+ },
917
+ discard: (id) => {
918
+ request({ op: "discard-upload", id }).catch(() => {
919
+ });
920
+ }
921
+ },
922
+ chooseLibrary: libraryChooser(post2, fromShell),
923
+ subscribe: (onSnapshot) => {
924
+ listeners.push(onSnapshot);
925
+ if (last) onSnapshot(last);
926
+ },
927
+ fromShell
928
+ };
929
+ }
930
+ function inertLink() {
931
+ const refused = () => Promise.reject(new Error("this frame has no review to report to"));
932
+ return {
933
+ kind: "relay",
934
+ post: () => {
935
+ },
936
+ annotate: refused,
937
+ attachments: { upload: refused, discard: () => {
938
+ } },
939
+ subscribe: () => {
940
+ },
941
+ fromShell: () => false
942
+ };
943
+ }
944
+
945
+ // src/client/variant.ts
946
+ var SWAPPED_ATTR = "data-ez-swapped";
947
+ var VARIANT_ATTR = "data-ez-variant";
948
+ var SOURCE_ATTR = "data-ez-source";
949
+ function scoreCandidate(anchor, candidate) {
950
+ if (anchor.source && candidate.source && anchor.source !== candidate.source) return -1;
951
+ let score = 0;
952
+ if (anchor.source && candidate.source === anchor.source) score += 100;
953
+ if (anchor.text && candidate.text) {
954
+ if (candidate.text === anchor.text) score += 40;
955
+ else if (candidate.text.includes(anchor.text) || anchor.text.includes(candidate.text)) {
956
+ score += 12;
957
+ }
958
+ }
959
+ if (anchor.selector && candidate.selector === anchor.selector) score += 20;
960
+ if (anchor.components?.length && candidate.components?.length) {
961
+ if (candidate.components[0] === anchor.components[0]) score += 4;
962
+ if (anchor.components.every((name) => candidate.components.includes(name))) score += 2;
963
+ }
964
+ return score;
965
+ }
966
+ var MIN_SCORE = 20;
967
+ function locate(anchor, candidates) {
968
+ let best = -1;
969
+ let bestScore = MIN_SCORE - 1;
970
+ candidates.forEach((candidate, i) => {
971
+ const score = scoreCandidate(anchor, candidate);
972
+ if (score > bestScore) {
973
+ bestScore = score;
974
+ best = i;
975
+ }
976
+ });
977
+ return best === -1 ? null : best;
978
+ }
979
+ var FORBIDDEN_TAGS = ["script", "iframe", "object", "embed", "link", "meta", "base"];
980
+ function sanitize(root) {
981
+ for (const tag of FORBIDDEN_TAGS) {
982
+ for (const node of [...root.querySelectorAll(tag)]) node.remove();
983
+ }
984
+ for (const element of [root, ...root.querySelectorAll("*")]) {
985
+ for (const attribute of [...element.attributes]) {
986
+ const name = attribute.name.toLowerCase();
987
+ if (name.startsWith("on")) element.removeAttribute(attribute.name);
988
+ else if ((name === "href" || name === "src" || name === "xlink:href") && /^\s*javascript:/i.test(attribute.value)) {
989
+ element.removeAttribute(attribute.name);
990
+ }
991
+ }
992
+ }
993
+ }
994
+ function buildVariant(html, doc) {
995
+ const template = doc.createElement("template");
996
+ template.innerHTML = html;
997
+ const root = template.content.firstElementChild;
998
+ if (!root) return null;
999
+ sanitize(root);
1000
+ for (const style of root.querySelectorAll("style")) {
1001
+ style.textContent = (style.textContent ?? "").replace(/:root\b/g, ":host");
1002
+ }
1003
+ return root;
1004
+ }
1005
+ var HOST_TAG = "ez-variant";
1006
+ function mountVariant(root, exploreId, doc, boxSizing) {
1007
+ const host = doc.createElement(HOST_TAG);
1008
+ host.setAttribute(VARIANT_ATTR, exploreId);
1009
+ host.style.display = "contents";
1010
+ const baseline = doc.createElement("style");
1011
+ baseline.textContent = `*, *::before, *::after { box-sizing: ${boxSizing}; }`;
1012
+ host.attachShadow({ mode: "open" }).append(baseline, root);
1013
+ return host;
1014
+ }
1015
+ var VariantSwapper = class {
1016
+ constructor(doc, describe2) {
1017
+ this.doc = doc;
1018
+ this.describe = describe2;
1019
+ __publicField(this, "swaps", /* @__PURE__ */ new Map());
1020
+ __publicField(this, "observer", null);
1021
+ __publicField(this, "queued", false);
1022
+ }
1023
+ /** Put a round's variant on the page, or take it off with `html: null`.
1024
+ *
1025
+ * Always restores first. `apply` leaves a round alone that already has
1026
+ * something standing, which is what makes re-applying after a re-render
1027
+ * idempotent - but the user picking a *different* variant of the same round is
1028
+ * a real change, and without this it would be swallowed by that same guard. */
1029
+ set(swap) {
1030
+ this.restore(swap.exploreId);
1031
+ if (swap.html === null) {
1032
+ this.swaps.delete(swap.exploreId);
1033
+ } else {
1034
+ this.swaps.set(swap.exploreId, swap);
1035
+ this.apply(swap);
1036
+ }
1037
+ this.watch();
1038
+ }
1039
+ /** Give the page back, whole. */
1040
+ clear() {
1041
+ for (const exploreId of [...this.swaps.keys()]) this.restore(exploreId);
1042
+ this.swaps.clear();
1043
+ this.watch();
1044
+ }
1045
+ /** Re-apply everything to whatever is mounted now. Called by the observer, and
1046
+ * safe to call at any time: a round already standing where it should be is
1047
+ * left alone. */
1048
+ reapply() {
1049
+ for (const swap of this.swaps.values()) this.apply(swap);
1050
+ }
1051
+ apply(swap) {
1052
+ if (this.doc.querySelector(`[${VARIANT_ATTR}="${cssEscape(swap.exploreId)}"]`)) return;
1053
+ const elements = [...this.doc.querySelectorAll(`[${SOURCE_ATTR}], body *`)].filter(
1054
+ (el2) => !el2.closest(`[${VARIANT_ATTR}]`) && !el2.hasAttribute(SWAPPED_ATTR)
1055
+ );
1056
+ const index = locate(
1057
+ swap.anchor,
1058
+ elements.map((el2) => this.describe(el2))
1059
+ );
1060
+ if (index === null) return;
1061
+ const target = elements[index];
1062
+ const root = buildVariant(swap.html, this.doc);
1063
+ if (!root) return;
1064
+ const boxSizing = this.doc.defaultView?.getComputedStyle(target).boxSizing || "content-box";
1065
+ target.setAttribute(SWAPPED_ATTR, swap.exploreId);
1066
+ target.insertAdjacentElement(
1067
+ "afterend",
1068
+ mountVariant(root, swap.exploreId, this.doc, boxSizing)
1069
+ );
1070
+ }
1071
+ restore(exploreId) {
1072
+ const id = cssEscape(exploreId);
1073
+ for (const node of this.doc.querySelectorAll(`[${VARIANT_ATTR}="${id}"]`)) node.remove();
1074
+ for (const node of this.doc.querySelectorAll(`[${SWAPPED_ATTR}="${id}"]`)) {
1075
+ node.removeAttribute(SWAPPED_ATTR);
1076
+ }
1077
+ }
1078
+ /** Watch only while something is swapped: an observer on the whole body is not
1079
+ * free, and a review with no explore open should not be paying for one. */
1080
+ watch() {
1081
+ if (!this.swaps.size) {
1082
+ this.observer?.disconnect();
1083
+ this.observer = null;
1084
+ return;
1085
+ }
1086
+ if (this.observer) return;
1087
+ this.observer = new MutationObserver((records) => {
1088
+ const theirs = records.some(
1089
+ (record) => [...record.addedNodes, ...record.removedNodes].some(
1090
+ (node) => !(node instanceof Element) || !this.ours(node)
1091
+ )
1092
+ );
1093
+ if (!theirs) return;
1094
+ if (this.queued) return;
1095
+ this.queued = true;
1096
+ queueMicrotask(() => {
1097
+ this.queued = false;
1098
+ this.collectOrphans();
1099
+ this.reapply();
1100
+ });
1101
+ });
1102
+ this.observer.observe(this.doc.body, { childList: true, subtree: true });
1103
+ }
1104
+ ours(node) {
1105
+ return node.hasAttribute(VARIANT_ATTR) || node.hasAttribute(SWAPPED_ATTR);
1106
+ }
1107
+ /** A variant whose original is gone - the app removed one child rather than
1108
+ * the subtree - would otherwise be left standing on its own, which reads as
1109
+ * the variant having become the page. */
1110
+ collectOrphans() {
1111
+ for (const variant of this.doc.querySelectorAll(`[${VARIANT_ATTR}]`)) {
1112
+ const id = variant.getAttribute(VARIANT_ATTR);
1113
+ if (!this.doc.querySelector(`[${SWAPPED_ATTR}="${cssEscape(id)}"]`)) variant.remove();
1114
+ }
1115
+ }
1116
+ };
1117
+ var cssEscape = (value) => typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
1118
+ var CAPTURE_STYLES = [
1119
+ "box-sizing",
1120
+ "font-family",
1121
+ "font-size",
1122
+ "font-weight",
1123
+ "line-height",
1124
+ "letter-spacing",
1125
+ "color",
1126
+ "background-color",
1127
+ "border",
1128
+ "border-radius",
1129
+ "padding",
1130
+ "gap",
1131
+ "box-shadow",
1132
+ "text-transform"
1133
+ ];
1134
+ var INHERITED = [
1135
+ "font-family",
1136
+ "font-size",
1137
+ "font-weight",
1138
+ "line-height",
1139
+ "letter-spacing",
1140
+ "color"
1141
+ ];
1142
+ var SLOT_LAYOUT = {
1143
+ display: "display",
1144
+ direction: "flex-direction",
1145
+ align: "align-items",
1146
+ justify: "justify-content",
1147
+ gap: "gap"
1148
+ };
1149
+ var MAX_CAPTURE_HTML = 16 * 1024;
1150
+ var MAX_RULES_BYTES = 4 * 1024;
1151
+ var MAX_RULES = 40;
1152
+ var MAX_TOKENS = 40;
1153
+ var MAX_SIBLINGS = 12;
1154
+ function captureElement(element) {
1155
+ const doc = element.ownerDocument;
1156
+ const view = doc.defaultView;
1157
+ const html = element.outerHTML;
1158
+ const capture = {
1159
+ html: html.slice(0, MAX_CAPTURE_HTML),
1160
+ ...html.length > MAX_CAPTURE_HTML ? { truncated: true } : {}
1161
+ };
1162
+ if (!view) return capture;
1163
+ const styles = pick(view.getComputedStyle(element), CAPTURE_STYLES);
1164
+ if (Object.keys(styles).length) capture.styles = styles;
1165
+ const rules = matchedRules(element, doc);
1166
+ if (rules.length) capture.rules = rules;
1167
+ const parent = element.parentElement;
1168
+ if (parent) {
1169
+ const width = parent.getBoundingClientRect().width;
1170
+ if (width > 0) capture.parentWidth = Math.round(width);
1171
+ const computed = view.getComputedStyle(parent);
1172
+ const slot = {};
1173
+ for (const [key, property] of Object.entries(SLOT_LAYOUT)) {
1174
+ const value = computed.getPropertyValue(property).trim();
1175
+ if (value && value !== "normal") slot[key] = value;
1176
+ }
1177
+ const inherits = pick(computed, INHERITED);
1178
+ if (Object.keys(inherits).length) slot.inherits = inherits;
1179
+ if (Object.keys(slot).length) capture.slot = slot;
1180
+ const siblings = [...parent.children].filter(
1181
+ (el2) => el2 !== element && !el2.hasAttribute(VARIANT_ATTR) && !el2.hasAttribute(SWAPPED_ATTR)
1182
+ ).slice(0, MAX_SIBLINGS).map((el2) => {
1183
+ const rect = el2.getBoundingClientRect();
1184
+ const text = (el2.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
1185
+ const cls = el2.getAttribute("class")?.trim().slice(0, 80);
1186
+ return {
1187
+ tag: el2.tagName.toLowerCase(),
1188
+ ...cls ? { class: cls } : {},
1189
+ ...text ? { text } : {},
1190
+ width: Math.round(rect.width),
1191
+ height: Math.round(rect.height)
1192
+ };
1193
+ });
1194
+ if (siblings.length) capture.siblings = siblings;
1195
+ }
1196
+ const tokens = rootTokens(doc);
1197
+ if (Object.keys(tokens).length) capture.tokens = tokens;
1198
+ const rootEl = doc.documentElement;
1199
+ const theme = {};
1200
+ const scheme = view.getComputedStyle(rootEl).getPropertyValue("color-scheme").trim();
1201
+ if (scheme && scheme !== "normal") theme.scheme = scheme;
1202
+ const classes = [rootEl.className, doc.body?.className ?? ""].filter(Boolean).join(" ").trim();
1203
+ if (classes) theme.classes = classes.slice(0, 120);
1204
+ const dataTheme = rootEl.dataset.theme ?? doc.body?.dataset.theme;
1205
+ if (dataTheme) theme.dataTheme = dataTheme.slice(0, 40);
1206
+ if (Object.keys(theme).length) capture.theme = theme;
1207
+ return capture;
1208
+ }
1209
+ function pick(computed, properties) {
1210
+ const out = {};
1211
+ for (const property of properties) {
1212
+ const value = computed.getPropertyValue(property).trim();
1213
+ if (value) out[property] = value;
1214
+ }
1215
+ return out;
1216
+ }
1217
+ var PSEUDO = /::?[a-zA-Z-]+(\([^)]*\))?/g;
1218
+ function matchedRules(element, doc) {
1219
+ const targets = [element, ...element.querySelectorAll("*")];
1220
+ const seen = /* @__PURE__ */ new Set();
1221
+ const out = [];
1222
+ let bytes = 0;
1223
+ const applies = (selectorText) => selectorText.split(",").some((one) => {
1224
+ const base = one.replace(PSEUDO, "").trim();
1225
+ if (!base) return false;
1226
+ try {
1227
+ return targets.some((t) => t.matches(base));
1228
+ } catch {
1229
+ return false;
1230
+ }
1231
+ });
1232
+ const take = (text) => {
1233
+ if (seen.has(text)) return true;
1234
+ if (out.length >= MAX_RULES || bytes + text.length > MAX_RULES_BYTES) return false;
1235
+ seen.add(text);
1236
+ out.push(text);
1237
+ bytes += text.length;
1238
+ return true;
1239
+ };
1240
+ const walk = (rules, wrap) => {
1241
+ for (const rule of rules) {
1242
+ if (rule instanceof CSSStyleRule) {
1243
+ if (applies(rule.selectorText) && !take(wrap(rule.cssText))) return false;
1244
+ } else if (rule instanceof CSSMediaRule) {
1245
+ const condition = rule.conditionText;
1246
+ if (!walk(rule.cssRules, (text) => wrap(`@media ${condition} { ${text} }`))) return false;
1247
+ } else if (rule instanceof CSSSupportsRule) {
1248
+ const condition = rule.conditionText;
1249
+ if (!walk(rule.cssRules, (text) => wrap(`@supports ${condition} { ${text} }`))) return false;
1250
+ }
1251
+ }
1252
+ return true;
1253
+ };
1254
+ for (const sheet of doc.styleSheets) {
1255
+ let rules;
1256
+ try {
1257
+ rules = sheet.cssRules;
1258
+ } catch {
1259
+ continue;
1260
+ }
1261
+ if (!walk(rules, (text) => text)) break;
1262
+ }
1263
+ return out;
1264
+ }
1265
+ function rootTokens(doc) {
1266
+ const names = /* @__PURE__ */ new Set();
1267
+ const walk = (rules) => {
1268
+ for (const rule of rules) {
1269
+ if (rule instanceof CSSStyleRule) {
1270
+ if (!/(^|,)\s*(:root|html)\s*(,|$)/.test(rule.selectorText)) continue;
1271
+ for (let i = 0; i < rule.style.length; i += 1) {
1272
+ const name = rule.style[i];
1273
+ if (name.startsWith("--")) names.add(name);
1274
+ }
1275
+ } else if (rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule) {
1276
+ walk(rule.cssRules);
1277
+ }
1278
+ }
1279
+ };
1280
+ for (const sheet of doc.styleSheets) {
1281
+ try {
1282
+ walk(sheet.cssRules);
1283
+ } catch {
1284
+ continue;
1285
+ }
1286
+ }
1287
+ const computed = doc.defaultView?.getComputedStyle(doc.documentElement);
1288
+ const out = {};
1289
+ if (!computed) return out;
1290
+ for (const name of [...names].slice(0, MAX_TOKENS)) {
1291
+ const value = computed.getPropertyValue(name).trim();
1292
+ if (value) out[name] = value.slice(0, 120);
1293
+ }
1294
+ return out;
1295
+ }
1296
+
1297
+ // src/client/scroll-sync.ts
1298
+ var QUIET_MS = 250;
1299
+ function scrollRange(scrollHeight, innerHeight) {
1300
+ return Math.max(0, scrollHeight - innerHeight);
1301
+ }
1302
+ function scrollRatio(scrollY, range) {
1303
+ if (range <= 0) return null;
1304
+ return Math.min(1, Math.max(0, scrollY / range));
1305
+ }
1306
+ function following(now, appliedAt) {
1307
+ return appliedAt !== null && now - appliedAt < QUIET_MS;
1308
+ }
1309
+
1310
+ // src/client/region.ts
1311
+ var DRAG_SLOP = 8;
1312
+ var EDGE_SLACK = 1;
1313
+ function regionFrom(from, to) {
1314
+ return {
1315
+ x: Math.round(Math.min(from.x, to.x)),
1316
+ y: Math.round(Math.min(from.y, to.y)),
1317
+ width: Math.round(Math.abs(to.x - from.x)),
1318
+ height: Math.round(Math.abs(to.y - from.y))
1319
+ };
1320
+ }
1321
+ function isRegion(region) {
1322
+ return region.width >= DRAG_SLOP || region.height >= DRAG_SLOP;
1323
+ }
1324
+ function centerOf(region) {
1325
+ return { x: region.x + region.width / 2, y: region.y + region.height / 2 };
1326
+ }
1327
+ function containsRect(region, rect) {
1328
+ return rect.x >= region.x - EDGE_SLACK && rect.y >= region.y - EDGE_SLACK && rect.x + rect.width <= region.x + region.width + EDGE_SLACK && rect.y + rect.height <= region.y + region.height + EDGE_SLACK;
1329
+ }
1330
+ function intersectsRect(region, rect) {
1331
+ return rect.x < region.x + region.width && rect.x + rect.width > region.x && rect.y < region.y + region.height && rect.y + rect.height > region.y;
1332
+ }
1333
+
1334
+ // src/label.ts
1335
+ function shortAnchor(anchor) {
1336
+ const framed2 = anchor.contains?.length ? `\u6846\u9078 ${anchor.contains.length} \u9805` : "";
1337
+ const what = anchor.text && truncate(anchor.text, 22) || anchor.section || anchor.components?.[0] || anchor.source?.split("/").pop() || "";
1338
+ const at = viewportTag(anchor);
1339
+ return [framed2, what, at].filter(Boolean).join(" \xB7 ");
1340
+ }
1341
+ function viewportTag(anchor) {
1342
+ const vp = anchor.viewport;
1343
+ if (!vp || vp.preset === "desktop") return "";
1344
+ return `${vp.width}px`;
1345
+ }
1346
+ function truncate(s, max) {
1347
+ const t = s.replace(/\s+/g, " ").trim();
1348
+ return t.length > max ? `${t.slice(0, max)}\u2026` : t;
1349
+ }
1350
+
1351
+ // src/client/overlay.ts
1352
+ var framed = window.parent !== window;
1353
+ var PREFIX = (() => {
1354
+ const script = document.currentScript;
1355
+ try {
1356
+ return new URL(script.src).pathname.replace(/\/overlay\.js$/, "");
1357
+ } catch {
1358
+ return "/__ezdesign";
1359
+ }
1360
+ })();
1361
+ var API = `${PREFIX}/api`;
1362
+ var link = linkFromScript(document.currentScript, API);
1363
+ var SOURCE_ATTR2 = "data-ez-source";
1364
+ var mode = "off";
1365
+ var ended = false;
1366
+ var viewportPreset;
1367
+ var uiScale = 1;
1368
+ var scopedToDevice = false;
1369
+ var appliedScrollAt = null;
1370
+ var scrollRaf = 0;
1371
+ function setFrameZoom(zoom) {
1372
+ const next = zoom > 0 ? 1 / zoom : 1;
1373
+ if (next === uiScale) return;
1374
+ uiScale = next;
1375
+ document.documentElement.style.setProperty("--ez-ui-scale", String(uiScale));
1376
+ scheduleRepaint();
1377
+ }
1378
+ var annotations = [];
1379
+ var canExplore = false;
1380
+ var swapper = new VariantSwapper(document, (element) => {
1381
+ const source = element.closest(`[${SOURCE_ATTR2}]`)?.getAttribute(SOURCE_ATTR2);
1382
+ const text = (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 80);
1383
+ return {
1384
+ ...source ? { source } : {},
1385
+ components: componentChain(element),
1386
+ selector: cssPath(element),
1387
+ ...text ? { text } : {}
1388
+ };
1389
+ });
1390
+ var hoverTarget = null;
1391
+ var held = false;
1392
+ var pick2 = null;
1393
+ var lastPointer = null;
1394
+ var modHeld = false;
1395
+ var framing = null;
1396
+ var markedRegion = null;
1397
+ var pendingFrame = null;
1398
+ var swallowClick = false;
1399
+ var popupSubject = null;
1400
+ var highlightTarget = null;
1401
+ var pinPage = null;
1402
+ var popupRect = null;
1403
+ var selectionRange = null;
1404
+ var ui = {
1405
+ highlight: el("div", "ez-highlight"),
1406
+ badge: el("div", "ez-badge"),
1407
+ pin: el("div", "ez-pin"),
1408
+ markers: el("div", "ez-markers"),
1409
+ /** The box being dragged, with its size read out in the corner. */
1410
+ region: el("div", "ez-region"),
1411
+ regionSize: el("div", "ez-region-size"),
1412
+ /** The selected run, drawn by us. The browser stops painting the page's own
1413
+ * selection the moment the composer takes focus, and the run is the subject. */
1414
+ selection: el("div", "ez-selection"),
1415
+ popup: null,
1416
+ /** Lives beside the popup: its uploads are only ever discarded with it. */
1417
+ popupAttach: null,
1418
+ /** A step *inside* the open popup that Escape should take back before the
1419
+ * popup itself. Only `/explore` has one today. Returns whether it took the
1420
+ * press, the way `closeSlash` does - the layers above it are settled the
1421
+ * same way, and a hook that cannot say "not mine" would eat presses that
1422
+ * belong to the popup. */
1423
+ popupBack: null,
1424
+ selectionBubble: null
1425
+ };
1426
+ function popupLive() {
1427
+ return Boolean(ui.popup) && !pick2?.suspended;
1428
+ }
1429
+ function el(tag, className) {
1430
+ const node = document.createElement(tag);
1431
+ node.className = className;
1432
+ node.setAttribute("data-ez-ui", "");
1433
+ return node;
1434
+ }
1435
+ function isOwnUi(target) {
1436
+ return target instanceof Element && target.closest("[data-ez-ui]") !== null;
1437
+ }
1438
+ function fiberOf(node) {
1439
+ for (const key of Object.keys(node)) {
1440
+ if (key.startsWith("__reactFiber$")) return node[key];
1441
+ }
1442
+ return null;
1443
+ }
1444
+ function componentChain(element) {
1445
+ let node = element;
1446
+ let fiber = null;
1447
+ while (node && !fiber) {
1448
+ fiber = fiberOf(node);
1449
+ if (!fiber) node = node.parentElement;
1450
+ }
1451
+ const names = [];
1452
+ let current = fiber;
1453
+ while (current && names.length < 3) {
1454
+ const type = current.type;
1455
+ if (type && typeof type !== "string") {
1456
+ const name = type.displayName || type.name;
1457
+ if (name && /^[A-Z]/.test(name) && !names.includes(name)) names.push(name);
1458
+ }
1459
+ current = current.return;
1460
+ }
1461
+ return names;
1462
+ }
1463
+ function cssPath(element) {
1464
+ if (element.id) return `#${CSS.escape(element.id)}`;
1465
+ const parts = [];
1466
+ let node = element;
1467
+ while (node && node !== document.body && parts.length < 6) {
1468
+ if (node.id) {
1469
+ parts.unshift(`#${CSS.escape(node.id)}`);
1470
+ break;
1471
+ }
1472
+ const section = node.getAttribute("data-section");
1473
+ if (section) {
1474
+ parts.unshift(`[data-section="${CSS.escape(section)}"]`);
1475
+ break;
1476
+ }
1477
+ let part = node.tagName.toLowerCase();
1478
+ const parent = node.parentElement;
1479
+ if (parent) {
1480
+ const siblings = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
1481
+ if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(node) + 1})`;
1482
+ }
1483
+ parts.unshift(part);
1484
+ node = parent;
1485
+ }
1486
+ return parts.join(" > ");
1487
+ }
1488
+ function buildAnchor(element, selectedText, pin) {
1489
+ const rect = element.getBoundingClientRect();
1490
+ const sourceHost = element.closest(`[${SOURCE_ATTR2}]`);
1491
+ const sectionHost = element.closest("[data-section]");
1492
+ const text = selectedText ?? (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 80);
1493
+ return {
1494
+ source: sourceHost?.getAttribute(SOURCE_ATTR2) ?? void 0,
1495
+ components: componentChain(element),
1496
+ section: sectionHost?.getAttribute("data-section") ?? void 0,
1497
+ selector: cssPath(element),
1498
+ text: text || void 0,
1499
+ point: pin ? {
1500
+ x: Math.round(pin.x),
1501
+ y: Math.round(pin.y),
1502
+ rel: {
1503
+ x: rect.width ? clamp01((pin.x - rect.left - window.scrollX) / rect.width) : 0,
1504
+ y: rect.height ? clamp01((pin.y - rect.top - window.scrollY) / rect.height) : 0
1505
+ }
1506
+ } : void 0,
1507
+ rect: {
1508
+ x: Math.round(rect.x + window.scrollX),
1509
+ y: Math.round(rect.y + window.scrollY),
1510
+ width: Math.round(rect.width),
1511
+ height: Math.round(rect.height)
1512
+ },
1513
+ viewport: { width: window.innerWidth, height: window.innerHeight, preset: viewportPreset },
1514
+ // A canvas frame is one document with no page in the app: its `frame`,
1515
+ // stamped by the shell, says where it is.
1516
+ ...link.kind === "relay" ? {} : { page: location.pathname }
1517
+ };
1518
+ }
1519
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
1520
+ function describe(element) {
1521
+ const chain = componentChain(element);
1522
+ const source = element.closest(`[${SOURCE_ATTR2}]`)?.getAttribute(SOURCE_ATTR2);
1523
+ const tag = element.tagName.toLowerCase();
1524
+ const head = chain.length ? `<${chain[0]}>` : tag;
1525
+ return source ? `${head} \xB7 ${source}` : head;
1526
+ }
1527
+ var BADGE_GAP = 4;
1528
+ var FRAME_RING = 1;
1529
+ function insetToViewport(rect) {
1530
+ const top = Math.max(FRAME_RING, rect.top);
1531
+ const left = Math.max(FRAME_RING, rect.left);
1532
+ const right = Math.min(window.innerWidth - FRAME_RING, rect.right);
1533
+ const bottom = Math.min(window.innerHeight - FRAME_RING, rect.bottom);
1534
+ return { top, left, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
1535
+ }
1536
+ function paintHighlight() {
1537
+ if (!highlightTarget) {
1538
+ ui.highlight.style.display = "none";
1539
+ ui.badge.style.display = "none";
1540
+ return;
1541
+ }
1542
+ const rect = highlightTarget.getBoundingClientRect();
1543
+ const frame = insetToViewport(rect);
1544
+ Object.assign(ui.highlight.style, {
1545
+ display: "block",
1546
+ top: `${frame.top}px`,
1547
+ left: `${frame.left}px`,
1548
+ width: `${frame.width}px`,
1549
+ height: `${frame.height}px`
1550
+ });
1551
+ if (popupLive()) {
1552
+ ui.badge.style.display = "none";
1553
+ return;
1554
+ }
1555
+ ui.badge.textContent = describe(highlightTarget);
1556
+ ui.badge.style.display = "block";
1557
+ placeBadge(rect, ui.badge.offsetWidth * uiScale, ui.badge.offsetHeight * uiScale);
1558
+ }
1559
+ function selectionOwner(range) {
1560
+ const node = range.commonAncestorContainer;
1561
+ const owner = node instanceof Element ? node : node.parentElement;
1562
+ return owner && !isOwnUi(owner) ? owner : null;
1563
+ }
1564
+ function liveSelectionRange() {
1565
+ const selection = window.getSelection();
1566
+ if (!selection || selection.isCollapsed || !selection.rangeCount) return null;
1567
+ const range = selection.getRangeAt(0);
1568
+ if (!range.toString().trim() || !selectionOwner(range)) return null;
1569
+ return range;
1570
+ }
1571
+ function paintSelection() {
1572
+ ui.selection.textContent = "";
1573
+ if (!selectionRange) return;
1574
+ for (const rect of selectionRange.getClientRects()) {
1575
+ if (!rect.width || !rect.height) continue;
1576
+ const band = el("div", "ez-selection-band");
1577
+ Object.assign(band.style, {
1578
+ top: `${rect.top}px`,
1579
+ left: `${rect.left}px`,
1580
+ width: `${rect.width}px`,
1581
+ height: `${rect.height}px`
1582
+ });
1583
+ ui.selection.append(band);
1584
+ }
1585
+ }
1586
+ function placeBadge(rect, width, height) {
1587
+ const above = rect.top - height - BADGE_GAP;
1588
+ const below = rect.bottom + BADGE_GAP;
1589
+ const floor = BADGE_GAP;
1590
+ const ceiling = Math.max(floor, window.innerHeight - height - BADGE_GAP);
1591
+ const top = above >= floor ? above : below <= ceiling ? below : Math.min(Math.max(rect.top + BADGE_GAP, floor), ceiling);
1592
+ const right = Math.max(floor, window.innerWidth - width - BADGE_GAP);
1593
+ Object.assign(ui.badge.style, {
1594
+ top: `${top}px`,
1595
+ left: `${Math.min(Math.max(rect.left, floor), right)}px`
1596
+ });
1597
+ }
1598
+ function paintPin() {
1599
+ if (!pinPage || pick2) {
1600
+ ui.pin.style.display = "none";
1601
+ return;
1602
+ }
1603
+ Object.assign(ui.pin.style, {
1604
+ display: "block",
1605
+ top: `${pinPage.y - window.scrollY}px`,
1606
+ left: `${pinPage.x - window.scrollX}px`
1607
+ });
1608
+ }
1609
+ function paintPopup() {
1610
+ const popup = ui.popup;
1611
+ if (!popup || !popupRect) return;
1612
+ const rect = popupRect();
1613
+ const width = popup.offsetWidth * uiScale;
1614
+ const height = popup.offsetHeight * uiScale;
1615
+ const top = Math.min(rect.bottom + 8, window.innerHeight - height - 12);
1616
+ const left = Math.min(Math.max(8, rect.left), window.innerWidth - width - 12);
1617
+ Object.assign(popup.style, { top: `${Math.max(8, top)}px`, left: `${left}px` });
1618
+ }
1619
+ function movePin(at) {
1620
+ pinPage = at;
1621
+ paintPin();
1622
+ }
1623
+ function moveHighlight(element) {
1624
+ highlightTarget = element;
1625
+ paintHighlight();
1626
+ }
1627
+ function subjectOf(kind, element, extra) {
1628
+ return {
1629
+ kind,
1630
+ page: location.pathname,
1631
+ anchor: buildAnchor(element, extra?.selectedText, extra?.pin),
1632
+ ...extra?.selectedText ? { selectedText: extra.selectedText } : {},
1633
+ ...extra?.pin ? { pin: extra.pin } : {}
1634
+ };
1635
+ }
1636
+ function closePopup() {
1637
+ ui.popupAttach?.discard();
1638
+ ui.popupAttach = null;
1639
+ ui.popupBack = null;
1640
+ if (ui.popup) post({ type: "ez:popup", open: false });
1641
+ ui.popup?.remove();
1642
+ ui.popup = null;
1643
+ popupRect = null;
1644
+ popupSubject = null;
1645
+ selectionRange = null;
1646
+ paintSelection();
1647
+ }
1648
+ function dismiss() {
1649
+ closePopup();
1650
+ hoverTarget = null;
1651
+ moveHighlight(null);
1652
+ movePin(null);
1653
+ markedRegion = null;
1654
+ paintRegion();
1655
+ }
1656
+ function exitToIdle() {
1657
+ dismiss();
1658
+ setMode("off");
1659
+ }
1660
+ function escape() {
1661
+ if (ui.popupAttach?.closeSlash()) return;
1662
+ if (!pick2 && (framing || pendingFrame)) {
1663
+ endFraming();
1664
+ return;
1665
+ }
1666
+ if (pick2) {
1667
+ cancelPick("escape");
1668
+ return;
1669
+ }
1670
+ if (ui.popupBack?.()) return;
1671
+ if (ui.popup) {
1672
+ dismiss();
1673
+ return;
1674
+ }
1675
+ if (mode === "off") return;
1676
+ exitToIdle();
1677
+ }
1678
+ var COMPOSE_PLACEHOLDER = "\u60F3\u600E\u9EBC\u8ABF\u6574\uFF1F\u8F38\u5165 / \u7528\u6307\u4EE4 (\u2318+Enter \u9001\u51FA)";
1679
+ var EXPLORE_PLACEHOLDER = "\u7D66\u4E88\u63A2\u7D22\u65B9\u5411\uFF0C\u6216\u7559\u767D\u81EA\u7531\u63A2\u7D22";
1680
+ function openPopup(subject, anchor, onSave, run = null, exploreElement = null) {
1681
+ closePopup();
1682
+ selectionRange = run;
1683
+ paintSelection();
1684
+ popupSubject = subject;
1685
+ const popup = el("div", "ez-popup");
1686
+ const actions = el("div", "ez-actions");
1687
+ const save = el("button", "ez-btn ez-btn-primary");
1688
+ const saveIcon = icon(Add01Icon, 14);
1689
+ const saveLabel = document.createTextNode("\u52A0\u5165\u5F85\u9001\u6E05\u55AE");
1690
+ save.append(saveIcon, saveLabel);
1691
+ const cancel = el("button", "ez-btn");
1692
+ cancel.textContent = "\u53D6\u6D88";
1693
+ actions.append(cancel, save);
1694
+ let saving = false;
1695
+ const exploreTarget = subject.kind === "element" ? exploreElement : null;
1696
+ let exploring = false;
1697
+ const exploreHead = el("div", "ez-explore-head");
1698
+ exploreHead.append(icon(MagicWand01Icon, 15), el("span", "ez-explore-title"));
1699
+ exploreHead.querySelector(".ez-explore-title").textContent = "\u63A2\u7D22\u6A23\u5F0F";
1700
+ exploreHead.hidden = true;
1701
+ const attach = attachify({
1702
+ attachments: link.attachments,
1703
+ ...link.chooseLibrary ? { chooseLibrary: link.chooseLibrary } : {},
1704
+ mk: el,
1705
+ className: "ez-input",
1706
+ placeholder: COMPOSE_PLACEHOLDER,
1707
+ onChange: () => {
1708
+ save.disabled = saving || attach.pending() > 0;
1709
+ if (pick2?.suspended) postDraft();
1710
+ },
1711
+ // `enabled` rather than a built-once list, because what this box is for can
1712
+ // change while it is open: arming an explore from inside one is a command
1713
+ // with nothing to do, and the menu should not offer it.
1714
+ commands: [
1715
+ {
1716
+ id: "element",
1717
+ label: "Element",
1718
+ hint: `\u9078\u53D6\u9801\u9762\u5143\u7D20\u6216\u7BC4\u570D`,
1719
+ keywords: ["element", "pick", "ref", "reference", "\u5143\u7D20", "\u6307\u5B9A", "\u53C3\u8003", "\u6846\u9078"],
1720
+ icon: AlignSelectionIcon,
1721
+ run: () => void armPick("popup", newPickId())
1722
+ },
1723
+ {
1724
+ id: "explore",
1725
+ label: "Explore",
1726
+ hint: "\u63A2\u7D22\u6A23\u5F0F",
1727
+ keywords: ["explore", "variant", "ui", "\u63A2\u7D22", "\u6A23\u5F0F", "\u7248\u672C"],
1728
+ icon: MagicWand01Icon,
1729
+ enabled: () => canExplore && !!exploreTarget && !exploring,
1730
+ run: () => setExploring(true)
1731
+ }
1732
+ ]
1733
+ });
1734
+ const input = attach.editable;
1735
+ ui.popupAttach = attach;
1736
+ attach.field.prepend(exploreHead);
1737
+ let stashed = null;
1738
+ function setExploring(on) {
1739
+ if (on === exploring) return;
1740
+ exploring = on;
1741
+ const current = document.createDocumentFragment();
1742
+ current.append(...input.childNodes);
1743
+ if (on) stashed = current;
1744
+ else if (stashed) {
1745
+ input.append(stashed);
1746
+ stashed = null;
1747
+ }
1748
+ input.dispatchEvent(new InputEvent("input", { bubbles: true }));
1749
+ exploreHead.hidden = !on;
1750
+ popup.classList.toggle("ez-popup-explore", on);
1751
+ input.dataset.placeholder = on ? EXPLORE_PLACEHOLDER : COMPOSE_PLACEHOLDER;
1752
+ input.setAttribute("aria-label", on ? EXPLORE_PLACEHOLDER : COMPOSE_PLACEHOLDER);
1753
+ cancel.textContent = on ? "\u8FD4\u56DE" : "\u53D6\u6D88";
1754
+ saveIcon.toggleAttribute("hidden", on);
1755
+ saveLabel.textContent = on ? "\u63A2\u7D22" : "\u52A0\u5165\u5F85\u9001\u6E05\u55AE";
1756
+ input.focus();
1757
+ caretToEnd();
1758
+ }
1759
+ function caretToEnd() {
1760
+ const range = document.createRange();
1761
+ range.selectNodeContents(input);
1762
+ range.collapse(false);
1763
+ const selection = document.getSelection();
1764
+ selection?.removeAllRanges();
1765
+ selection?.addRange(range);
1766
+ }
1767
+ ui.popupBack = () => {
1768
+ if (!exploring) return false;
1769
+ setExploring(false);
1770
+ return true;
1771
+ };
1772
+ const submit = async () => {
1773
+ if (saving || attach.pending() > 0) return;
1774
+ const comment = attach.text();
1775
+ const attachments = attach.ids();
1776
+ const references = attach.refs();
1777
+ if (!exploring && !comment && attachments.length === 0 && references.length === 0) {
1778
+ input.focus();
1779
+ return;
1780
+ }
1781
+ ui.popupAttach = null;
1782
+ save.disabled = true;
1783
+ saving = true;
1784
+ try {
1785
+ if (exploring && exploreTarget) {
1786
+ post({
1787
+ type: "ez:explore",
1788
+ anchor: buildAnchor(exploreTarget),
1789
+ capture: captureElement(exploreTarget),
1790
+ direction: comment,
1791
+ // The direction travels with whatever was put in the sentence. "Like
1792
+ // this screenshot", "make it match that button" are directions, and
1793
+ // the markers in the text name these.
1794
+ attachments,
1795
+ references
1796
+ });
1797
+ } else {
1798
+ await onSave(comment, attachments, references);
1799
+ }
1800
+ } catch {
1801
+ if (ui.popup !== popup) return;
1802
+ ui.popupAttach = attach;
1803
+ save.disabled = false;
1804
+ showPopupNotice(["\u6C92\u6709\u9001\u51FA\u6210\u529F\uFF0C\u8ACB\u518D\u8A66\u4E00\u6B21"]);
1805
+ return;
1806
+ } finally {
1807
+ saving = false;
1808
+ }
1809
+ if (ui.popup !== popup) return;
1810
+ if (exploring && exploreTarget) {
1811
+ setExploring(false);
1812
+ const draft = exploreDraft(attach);
1813
+ if (draft) post({ type: "ez:explore-held", draft });
1814
+ dismiss();
1815
+ return;
1816
+ }
1817
+ dismiss();
1818
+ };
1819
+ save.onclick = () => void submit();
1820
+ cancel.onclick = () => exploring ? setExploring(false) : dismiss();
1821
+ input.onkeydown = (e) => {
1822
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") void submit();
1823
+ e.stopPropagation();
1824
+ };
1825
+ popup.append(attach.wrap, actions);
1826
+ popup.setAttribute("data-ez-subject", subject.kind);
1827
+ document.body.appendChild(popup);
1828
+ ui.popup = popup;
1829
+ post({ type: "ez:popup", open: true });
1830
+ popupRect = anchor;
1831
+ paintPopup();
1832
+ paintHighlight();
1833
+ input.focus();
1834
+ }
1835
+ async function saveAnnotation(kind, element, comment, attachments, references, extra) {
1836
+ const anchor = extra?.anchor ?? (element ? buildAnchor(element, extra?.selectedText, extra?.pin) : null);
1837
+ if (!anchor) throw new Error("no anchor to save against");
1838
+ await link.annotate({ kind, comment, attachments, references, anchor });
1839
+ }
1840
+ function corroborates(hit, anchor) {
1841
+ if (!anchor.source) return true;
1842
+ return hit.closest(`[${SOURCE_ATTR2}]`)?.getAttribute(SOURCE_ATTR2) === anchor.source;
1843
+ }
1844
+ function bySource(source) {
1845
+ for (const node of document.querySelectorAll(`[${SOURCE_ATTR2}]`)) {
1846
+ if (node.getAttribute(SOURCE_ATTR2) === source) return node;
1847
+ }
1848
+ return null;
1849
+ }
1850
+ function byText(needle) {
1851
+ let best = null;
1852
+ let bestLength = Number.POSITIVE_INFINITY;
1853
+ for (const node of document.body.querySelectorAll("*")) {
1854
+ if (isOwnUi(node)) continue;
1855
+ const text = (node.textContent ?? "").replace(/\s+/g, " ");
1856
+ if (!text.includes(needle) || text.length >= bestLength) continue;
1857
+ best = node;
1858
+ bestLength = text.length;
1859
+ }
1860
+ return best;
1861
+ }
1862
+ function resolveSubject(subject) {
1863
+ const anchor = subject.anchor;
1864
+ const hit = resolveSelector(anchor.selector);
1865
+ if (hit && corroborates(hit, anchor)) return hit;
1866
+ if (anchor.source) {
1867
+ const found = bySource(anchor.source);
1868
+ if (found) return found;
1869
+ }
1870
+ if (subject.selectedText) {
1871
+ const found = byText(subject.selectedText);
1872
+ if (found) return found;
1873
+ }
1874
+ return hit;
1875
+ }
1876
+ function staleRect(rect) {
1877
+ if (!rect) {
1878
+ return new DOMRect(window.innerWidth / 2 - 6, window.innerHeight / 2 - 6, 12, 12);
1879
+ }
1880
+ return new DOMRect(rect.x - window.scrollX, rect.y - window.scrollY, rect.width, rect.height);
1881
+ }
1882
+ function showPopupNotice(lines) {
1883
+ if (!ui.popup || lines.length === 0) return;
1884
+ ui.popup.querySelector(".ez-popup-notice")?.remove();
1885
+ const notice = el("div", "ez-popup-notice");
1886
+ notice.textContent = lines.join("\u3000");
1887
+ ui.popup.prepend(notice);
1888
+ paintPopup();
1889
+ }
1890
+ var SETTLE_MS = 800;
1891
+ function whenSubjectSettles(subject, done) {
1892
+ const deadline = Date.now() + SETTLE_MS;
1893
+ const attempt = () => {
1894
+ const found = resolveSubject(subject);
1895
+ const target = found?.hasAttribute(SWAPPED_ATTR) ? null : found;
1896
+ if (target || Date.now() >= deadline) {
1897
+ done(target);
1898
+ return;
1899
+ }
1900
+ requestAnimationFrame(attempt);
1901
+ };
1902
+ attempt();
1903
+ }
1904
+ function restoreDraft(draft) {
1905
+ const subject = draft.subject;
1906
+ if (!subject) return;
1907
+ if (draftExpired(draft, Date.now(), GRACE_MS)) {
1908
+ post({ type: "ez:draft-expired", pickId: draft.id });
1909
+ return;
1910
+ }
1911
+ whenSubjectSettles(subject, (target) => rebuildPopup(draft, subject, target));
1912
+ }
1913
+ function rebuildPopup(draft, subject, target) {
1914
+ restoreView(subject.scroll, target);
1915
+ const box = subject.anchor.contains?.length ? subject.anchor.rect : void 0;
1916
+ const lines = [];
1917
+ if (!target && !box) lines.push("\u539F\u672C\u7684\u5143\u7D20\u5DF2\u7D93\u4E0D\u5728\u9801\u9762\u4E0A\uFF0C\u9019\u5247\u6A19\u8A3B\u6703\u7528\u7576\u6642\u7684\u4F4D\u7F6E\u9001\u51FA");
1918
+ const lost = draftPendingNames(draft.body);
1919
+ if (lost.length) lines.push(`\u4E0A\u50B3\u4E2D\u7684\u6A94\u6848\uFF08${lost.join("\u3001")}\uFF09\u6C92\u6709\u8DDF\u8457\u56DE\u4F86\uFF0C\u8ACB\u91CD\u65B0\u8CBC\u4E00\u6B21`);
1920
+ moveHighlight(box ? null : target);
1921
+ if (box) {
1922
+ markedRegion = box;
1923
+ paintRegion();
1924
+ }
1925
+ if (subject.pin) movePin(subject.pin);
1926
+ openPopup(
1927
+ subject,
1928
+ box ? () => regionViewportRect(box) : target ? () => target.getBoundingClientRect() : () => staleRect(subject.anchor.rect),
1929
+ (comment, files, refs) => saveAnnotation(subject.kind, target, comment, files, refs, {
1930
+ selectedText: subject.selectedText,
1931
+ pin: subject.pin,
1932
+ // Keeps the recorded viewport too: it says what the user was actually
1933
+ // looking at when they framed this, which a repaint would overwrite.
1934
+ ...target && !box ? {} : { anchor: subject.anchor }
1935
+ }),
1936
+ null,
1937
+ // The element this comment is about, so `/explore` is on offer again. A
1938
+ // rebuilt popup used to come back without it, and every comment that had
1939
+ // just been through one explore could not start another - which is the one
1940
+ // thing a user with a variant in hand and a note to go with it wants next.
1941
+ // A region has no single element to explore; `openPopup` already refuses a
1942
+ // subject that is not an element, so only the box has to be kept out here.
1943
+ box ? null : target
1944
+ );
1945
+ ui.popupAttach?.restore(draft.body);
1946
+ showPopupNotice(lines);
1947
+ }
1948
+ var markerRaf = 0;
1949
+ var MARKER_SIZE = 20;
1950
+ function resolveSelector(selector) {
1951
+ if (!selector) return null;
1952
+ try {
1953
+ return document.querySelector(selector);
1954
+ } catch {
1955
+ return null;
1956
+ }
1957
+ }
1958
+ function madeHere(a) {
1959
+ if (!scopedToDevice) return true;
1960
+ const preset = a.anchor.viewport?.preset;
1961
+ return !preset || preset === viewportPreset;
1962
+ }
1963
+ function markerPosition(a) {
1964
+ const target = resolveSelector(a.anchor.selector);
1965
+ const rect = target?.getBoundingClientRect();
1966
+ const live = rect && (rect.width > 0 || rect.height > 0) ? rect : null;
1967
+ if (a.kind === "point" && a.anchor.point) {
1968
+ if (live) {
1969
+ return {
1970
+ top: live.top + a.anchor.point.rel.y * live.height,
1971
+ left: live.left + a.anchor.point.rel.x * live.width
1972
+ };
1973
+ }
1974
+ if (a.anchor.viewport && window.innerWidth !== a.anchor.viewport.width) return null;
1975
+ return { top: a.anchor.point.y - window.scrollY, left: a.anchor.point.x - window.scrollX };
1976
+ }
1977
+ if (a.anchor.contains?.length && a.anchor.rect) {
1978
+ if (!a.anchor.viewport || window.innerWidth === a.anchor.viewport.width) {
1979
+ return { top: a.anchor.rect.y - window.scrollY, left: a.anchor.rect.x - window.scrollX };
1980
+ }
1981
+ return live ? { top: live.top, left: live.left } : null;
1982
+ }
1983
+ return live ? { top: live.top, left: live.left } : null;
1984
+ }
1985
+ function renderMarkers() {
1986
+ ui.markers.textContent = "";
1987
+ if (pick2) return;
1988
+ const relevant = annotations.filter(
1989
+ (a) => (link.kind === "relay" || a.anchor.page === location.pathname) && madeHere(a)
1990
+ );
1991
+ relevant.forEach((a) => {
1992
+ const at = markerPosition(a);
1993
+ if (!at) return;
1994
+ const marker = el("div", "ez-marker");
1995
+ marker.textContent = String(a.n ?? annotations.indexOf(a) + 1);
1996
+ marker.title = a.comment;
1997
+ const r = MARKER_SIZE / 2;
1998
+ let top = at.top - r;
1999
+ let left = at.left - r;
2000
+ const onScreen = at.top >= 0 && at.top <= window.innerHeight && at.left >= 0 && at.left <= window.innerWidth;
2001
+ if (onScreen) {
2002
+ top = Math.min(Math.max(top, 2), window.innerHeight - MARKER_SIZE - 2);
2003
+ left = Math.min(Math.max(left, 2), window.innerWidth - MARKER_SIZE - 2);
2004
+ }
2005
+ Object.assign(marker.style, { top: `${top}px`, left: `${left}px` });
2006
+ ui.markers.appendChild(marker);
2007
+ });
2008
+ }
2009
+ function scheduleRepaint() {
2010
+ cancelAnimationFrame(markerRaf);
2011
+ markerRaf = requestAnimationFrame(() => {
2012
+ renderMarkers();
2013
+ paintHighlight();
2014
+ paintSelection();
2015
+ paintPin();
2016
+ paintRegion();
2017
+ paintPopup();
2018
+ });
2019
+ }
2020
+ var PICKING_LABEL = "\u9078\u53D6\u4E2D\u2026";
2021
+ var pickSeq = 0;
2022
+ var newPickId = () => `p${++pickSeq}-${Date.now().toString(36)}`;
2023
+ function post(message) {
2024
+ link.post(message);
2025
+ }
2026
+ function suspendPopup() {
2027
+ if (!ui.popup) return false;
2028
+ ui.popup.setAttribute("data-ez-hidden", "");
2029
+ return true;
2030
+ }
2031
+ function restoreView(scroll, target) {
2032
+ if (scroll) window.scrollTo({ left: scroll.x, top: scroll.y, behavior: "instant" });
2033
+ if (!target) return;
2034
+ const rect = target.getBoundingClientRect();
2035
+ if (rect.bottom > 0 && rect.top < window.innerHeight) return;
2036
+ target.scrollIntoView({ block: "center", behavior: "instant" });
2037
+ }
2038
+ function resumePopup(view) {
2039
+ if (!ui.popup) return;
2040
+ if (view) restoreView(view.scroll, view.prevHighlight);
2041
+ ui.popup.removeAttribute("data-ez-hidden");
2042
+ paintPopup();
2043
+ paintHighlight();
2044
+ ui.popupAttach?.editable.focus();
2045
+ }
2046
+ var inExplore = false;
2047
+ function currentDraft() {
2048
+ if (!pick2 || pick2.host !== "popup" || !ui.popupAttach || !popupSubject) return null;
2049
+ return {
2050
+ id: pick2.id,
2051
+ host: "popup",
2052
+ createdAt: pick2.draftAt,
2053
+ // Read now rather than at popup-open: this is the view the user is leaving.
2054
+ subject: { ...popupSubject, scroll: { x: window.scrollX, y: window.scrollY } },
2055
+ body: normalizeDraft(ui.popupAttach.snapshot())
2056
+ };
2057
+ }
2058
+ function exploreDraft(attach) {
2059
+ if (!popupSubject) return null;
2060
+ return {
2061
+ id: `x${Date.now().toString(36)}`,
2062
+ host: "popup",
2063
+ createdAt: Date.now(),
2064
+ subject: { ...popupSubject, scroll: { x: window.scrollX, y: window.scrollY } },
2065
+ body: normalizeDraft(attach.snapshot())
2066
+ };
2067
+ }
2068
+ function postDraft() {
2069
+ const draft = currentDraft();
2070
+ if (draft) post({ type: "ez:draft", pickId: draft.id, draft });
2071
+ }
2072
+ function armPick(host, id) {
2073
+ if (ended || pick2) return false;
2074
+ const attach = host === "popup" ? ui.popupAttach : null;
2075
+ pick2 = {
2076
+ id,
2077
+ host,
2078
+ suspended: false,
2079
+ prevHighlight: highlightTarget,
2080
+ draftAt: Date.now(),
2081
+ scroll: { x: window.scrollX, y: window.scrollY }
2082
+ };
2083
+ post({ type: "ez:pick-armed", pickId: id, host });
2084
+ if (attach) {
2085
+ attach.beginRef(PICKING_LABEL);
2086
+ pick2.suspended = suspendPopup();
2087
+ postDraft();
2088
+ }
2089
+ document.documentElement.setAttribute("data-ez-pick", "");
2090
+ hoverTarget = null;
2091
+ moveHighlight(null);
2092
+ scheduleRepaint();
2093
+ return true;
2094
+ }
2095
+ function frameUnderPointer() {
2096
+ const at = lastPointer;
2097
+ const target = at ? document.elementFromPoint(at.x, at.y) : null;
2098
+ hoverTarget = target && !isOwnUi(target) ? target : null;
2099
+ moveHighlight(hoverTarget);
2100
+ }
2101
+ function setModHeld(next) {
2102
+ if (modHeld === next) return;
2103
+ modHeld = next;
2104
+ if (!pick2) return;
2105
+ document.documentElement.toggleAttribute("data-ez-pick-hot", next);
2106
+ if (next) {
2107
+ frameUnderPointer();
2108
+ return;
2109
+ }
2110
+ hoverTarget = null;
2111
+ moveHighlight(null);
2112
+ }
2113
+ var isModKey = (key) => key === "Meta" || key === "Control";
2114
+ function endPick() {
2115
+ const done = pick2;
2116
+ pick2 = null;
2117
+ endFraming();
2118
+ document.documentElement.removeAttribute("data-ez-pick");
2119
+ document.documentElement.removeAttribute("data-ez-pick-hot");
2120
+ hoverTarget = null;
2121
+ moveHighlight(done.prevHighlight);
2122
+ scheduleRepaint();
2123
+ return done;
2124
+ }
2125
+ function refFor(element) {
2126
+ const anchor = buildAnchor(element);
2127
+ return { anchor, label: shortAnchor(anchor) || describe(element) };
2128
+ }
2129
+ function landPick(e) {
2130
+ if (!pick2) return;
2131
+ const target = e.target instanceof Element ? e.target : document.elementFromPoint(e.clientX, e.clientY);
2132
+ if (!target || isOwnUi(target)) return;
2133
+ deliverPick(refFor(target));
2134
+ }
2135
+ function deliverPick(ref) {
2136
+ const done = endPick();
2137
+ if (done.host === "popup" && done.suspended) {
2138
+ resumePopup(done);
2139
+ ui.popupAttach?.resolveRef(ref);
2140
+ post({ type: "ez:draft-done", pickId: done.id });
2141
+ return;
2142
+ }
2143
+ post({ type: "ez:picked", pickId: done.id, ref, page: location.pathname });
2144
+ }
2145
+ function pagePoint(e) {
2146
+ return { x: e.clientX + window.scrollX, y: e.clientY + window.scrollY };
2147
+ }
2148
+ function pageRect(rect) {
2149
+ return {
2150
+ x: rect.x + window.scrollX,
2151
+ y: rect.y + window.scrollY,
2152
+ width: rect.width,
2153
+ height: rect.height
2154
+ };
2155
+ }
2156
+ function paintRegion() {
2157
+ const box = framing ? regionFrom(framing.from, framing.to) : pick2 ? null : markedRegion;
2158
+ if (!box) {
2159
+ ui.region.style.display = "none";
2160
+ return;
2161
+ }
2162
+ Object.assign(ui.region.style, {
2163
+ display: "block",
2164
+ top: `${box.y - window.scrollY}px`,
2165
+ left: `${box.x - window.scrollX}px`,
2166
+ width: `${box.width}px`,
2167
+ height: `${box.height}px`
2168
+ });
2169
+ ui.regionSize.style.display = framing ? "block" : "none";
2170
+ if (!framing) return;
2171
+ ui.regionSize.textContent = `${box.width} \xD7 ${box.height}`;
2172
+ const bottom = box.y - window.scrollY + box.height;
2173
+ ui.regionSize.toggleAttribute("data-ez-inside", bottom > window.innerHeight - 24);
2174
+ }
2175
+ function startFraming(at) {
2176
+ framing = { from: at, to: at };
2177
+ pendingFrame = null;
2178
+ document.documentElement.setAttribute("data-ez-framing", "");
2179
+ document.getSelection()?.removeAllRanges();
2180
+ moveHighlight(null);
2181
+ paintRegion();
2182
+ }
2183
+ function endFraming() {
2184
+ pendingFrame = null;
2185
+ if (!framing) return;
2186
+ framing = null;
2187
+ document.documentElement.removeAttribute("data-ez-framing");
2188
+ paintRegion();
2189
+ }
2190
+ var MAX_FRAMED = 16;
2191
+ function elementsIn(box) {
2192
+ const found = [];
2193
+ const walk = (parent) => {
2194
+ for (const child of parent.children) {
2195
+ if (found.length >= MAX_FRAMED) return;
2196
+ if (child.hasAttribute("data-ez-ui")) continue;
2197
+ const rect = pageRect(child.getBoundingClientRect());
2198
+ if (!rect.width || !rect.height) {
2199
+ walk(child);
2200
+ continue;
2201
+ }
2202
+ if (containsRect(box, rect)) {
2203
+ found.push(child);
2204
+ continue;
2205
+ }
2206
+ if (intersectsRect(box, rect)) walk(child);
2207
+ }
2208
+ };
2209
+ walk(document.body);
2210
+ return found;
2211
+ }
2212
+ function commonAncestor(elements) {
2213
+ let node = elements[0] ?? null;
2214
+ while (node && !elements.every((e) => node.contains(e))) node = node.parentElement;
2215
+ return node;
2216
+ }
2217
+ function hostAt(page) {
2218
+ const at = document.elementFromPoint(page.x - window.scrollX, page.y - window.scrollY);
2219
+ return at && !isOwnUi(at) ? at : null;
2220
+ }
2221
+ function framedText(elements) {
2222
+ return elements.map((e) => (e.textContent ?? "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" / ").slice(0, 80);
2223
+ }
2224
+ function framedLabel(element) {
2225
+ const text = (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 24);
2226
+ return text ? `${describe(element)} \xB7 "${text}"` : describe(element);
2227
+ }
2228
+ function regionRef(box) {
2229
+ const framed2 = elementsIn(box);
2230
+ if (framed2.length <= 1) {
2231
+ const host2 = framed2[0] ?? hostAt(centerOf(box));
2232
+ if (host2) return refFor(host2);
2233
+ }
2234
+ const host = commonAncestor(framed2) ?? document.body;
2235
+ const text = framedText(framed2);
2236
+ const anchor = {
2237
+ ...buildAnchor(host),
2238
+ // The box the user drew, not the ancestor's: the ancestor is only how the box
2239
+ // gets a file:line, and its own rect is usually far larger than the area.
2240
+ rect: box,
2241
+ ...framed2.length ? { contains: framed2.map(framedLabel), text: text || void 0 } : {}
2242
+ };
2243
+ return { anchor, label: shortAnchor(anchor) || describe(host) };
2244
+ }
2245
+ var regionViewportRect = (box) => new DOMRect(box.x - window.scrollX, box.y - window.scrollY, box.width, box.height);
2246
+ function openRegionPopup(box) {
2247
+ const framed2 = elementsIn(box);
2248
+ const host = commonAncestor(framed2) ?? hostAt(centerOf(box)) ?? document.body;
2249
+ const text = framedText(framed2);
2250
+ const anchor = {
2251
+ ...buildAnchor(host),
2252
+ // The box the user drew, not the host's rect - same contract as a pick's
2253
+ // region: `rect` with `contains` beside it is only ever that box.
2254
+ rect: box,
2255
+ ...framed2.length ? { contains: framed2.map(framedLabel), text: text || void 0 } : {}
2256
+ };
2257
+ markedRegion = box;
2258
+ paintRegion();
2259
+ openPopup(
2260
+ { kind: "element", page: location.pathname, anchor },
2261
+ () => regionViewportRect(box),
2262
+ // The recorded anchor verbatim: the box is the annotation's meaning, and
2263
+ // re-measuring the host at save time would overwrite it.
2264
+ (comment, files, refs) => saveAnnotation("element", null, comment, files, refs, { anchor })
2265
+ );
2266
+ }
2267
+ function cancelPick(reason) {
2268
+ if (!pick2) return;
2269
+ const done = endPick();
2270
+ if (done.host === "popup") {
2271
+ ui.popupAttach?.cancelRef();
2272
+ if (done.suspended) resumePopup(done);
2273
+ }
2274
+ if (reason !== "abort") {
2275
+ post({ type: "ez:pick-cancelled", pickId: done.id, reason, resumed: done.suspended });
2276
+ }
2277
+ }
2278
+ function setMode(next) {
2279
+ if (ended && next !== "off") return;
2280
+ if (mode === next) return;
2281
+ cancelPick("mode");
2282
+ endFraming();
2283
+ mode = next;
2284
+ const root = document.documentElement;
2285
+ if (next === "off") root.removeAttribute("data-ez-mode");
2286
+ else root.setAttribute("data-ez-mode", next);
2287
+ dismiss();
2288
+ ui.selectionBubble?.remove();
2289
+ ui.selectionBubble = null;
2290
+ post({ type: "ez:mode", mode });
2291
+ }
2292
+ function onMouseMove(e) {
2293
+ lastPointer = { x: e.clientX, y: e.clientY };
2294
+ if (framing) {
2295
+ if (!(e.buttons & 1)) {
2296
+ endFraming();
2297
+ return;
2298
+ }
2299
+ framing.to = pagePoint(e);
2300
+ paintRegion();
2301
+ return;
2302
+ }
2303
+ if (pendingFrame) {
2304
+ if (!(e.buttons & 1)) {
2305
+ pendingFrame = null;
2306
+ } else {
2307
+ const at = pagePoint(e);
2308
+ if (!isRegion(regionFrom(pendingFrame, at))) return;
2309
+ startFraming(pendingFrame);
2310
+ framing.to = at;
2311
+ paintRegion();
2312
+ return;
2313
+ }
2314
+ }
2315
+ if (pick2) setModHeld(e.metaKey || e.ctrlKey);
2316
+ if (isOwnUi(e.target)) return;
2317
+ if (pick2 && !modHeld) return;
2318
+ if (!pick2 && (mode === "off" || mode === "region" || ui.popup || held)) return;
2319
+ const run = pick2 ? null : liveSelectionRange();
2320
+ const target = run ? selectionOwner(run) : e.target instanceof Element ? e.target : null;
2321
+ if (target !== hoverTarget) {
2322
+ hoverTarget = target;
2323
+ moveHighlight(target);
2324
+ }
2325
+ }
2326
+ function onMouseLeave() {
2327
+ if (ui.popup || !hoverTarget) return;
2328
+ if (!pick2 && liveSelectionRange()) return;
2329
+ hoverTarget = null;
2330
+ moveHighlight(null);
2331
+ }
2332
+ function onMouseDown(e) {
2333
+ if (e.button !== 0 || isOwnUi(e.target)) return;
2334
+ const doc = document.documentElement;
2335
+ if (e.clientX >= doc.clientWidth || e.clientY >= doc.clientHeight) return;
2336
+ if (pick2) {
2337
+ const modified = e.metaKey || e.ctrlKey;
2338
+ if (!modified && isTyping(e.target)) return;
2339
+ if (modified) e.preventDefault();
2340
+ pendingFrame = pagePoint(e);
2341
+ return;
2342
+ }
2343
+ if (mode === "off") return;
2344
+ if (ui.popup || held) {
2345
+ e.preventDefault();
2346
+ return;
2347
+ }
2348
+ if (mode !== "region") return;
2349
+ e.preventDefault();
2350
+ pendingFrame = pagePoint(e);
2351
+ }
2352
+ function onClick(e) {
2353
+ if (isOwnUi(e.target)) return;
2354
+ if (swallowClick) {
2355
+ swallowClick = false;
2356
+ e.preventDefault();
2357
+ e.stopPropagation();
2358
+ return;
2359
+ }
2360
+ if (pick2) {
2361
+ if (!e.metaKey && !e.ctrlKey) return;
2362
+ e.preventDefault();
2363
+ e.stopPropagation();
2364
+ landPick(e);
2365
+ return;
2366
+ }
2367
+ if (mode === "off") return;
2368
+ if (ui.popup || held) {
2369
+ e.preventDefault();
2370
+ e.stopPropagation();
2371
+ return;
2372
+ }
2373
+ if (mode === "region") {
2374
+ e.preventDefault();
2375
+ e.stopPropagation();
2376
+ return;
2377
+ }
2378
+ const selection = window.getSelection();
2379
+ if (selection && !selection.isCollapsed) return;
2380
+ const target = e.target instanceof Element ? e.target : null;
2381
+ if (!target || isOwnUi(target)) return;
2382
+ e.preventDefault();
2383
+ e.stopPropagation();
2384
+ moveHighlight(target);
2385
+ openPopup(
2386
+ subjectOf("element", target),
2387
+ () => target.getBoundingClientRect(),
2388
+ (comment, files, refs) => saveAnnotation("element", target, comment, files, refs),
2389
+ null,
2390
+ target
2391
+ );
2392
+ }
2393
+ function onMouseUp(e) {
2394
+ pendingFrame = null;
2395
+ if (framing) {
2396
+ const box = regionFrom(framing.from, pagePoint(e));
2397
+ endFraming();
2398
+ if (!isRegion(box)) return;
2399
+ swallowClick = true;
2400
+ if (pick2) {
2401
+ deliverPick(regionRef(box));
2402
+ return;
2403
+ }
2404
+ if (mode !== "region" || ui.popup || held) return;
2405
+ openRegionPopup(box);
2406
+ return;
2407
+ }
2408
+ if (pick2 || mode === "off" || mode === "region" || ui.popup || held) return;
2409
+ setTimeout(() => {
2410
+ ui.selectionBubble?.remove();
2411
+ ui.selectionBubble = null;
2412
+ const range = liveSelectionRange();
2413
+ if (!range) return;
2414
+ const container = selectionOwner(range);
2415
+ if (!container) return;
2416
+ const text = range.toString().trim();
2417
+ const rect = range.getBoundingClientRect();
2418
+ const bubble = el("button", "ez-select-btn");
2419
+ bubble.append(icon(TextSelectIcon, 14), document.createTextNode("\u6A19\u8A3B\u9078\u53D6\u6587\u5B57"));
2420
+ Object.assign(bubble.style, {
2421
+ top: `${Math.max(4, rect.bottom + window.scrollY + 6)}px`,
2422
+ left: `${Math.max(4, rect.left + window.scrollX)}px`
2423
+ });
2424
+ bubble.onclick = () => {
2425
+ bubble.remove();
2426
+ ui.selectionBubble = null;
2427
+ openPopup(
2428
+ subjectOf("text", container, { selectedText: text }),
2429
+ () => range.getBoundingClientRect(),
2430
+ (comment, files, refs) => saveAnnotation("text", container, comment, files, refs, { selectedText: text }),
2431
+ range
2432
+ );
2433
+ };
2434
+ document.body.appendChild(bubble);
2435
+ ui.selectionBubble = bubble;
2436
+ }, 0);
2437
+ }
2438
+ function isTyping(target) {
2439
+ if (!(target instanceof HTMLElement)) return false;
2440
+ return target.isContentEditable || target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
2441
+ }
2442
+ function onScrollAttempt(e) {
2443
+ const w = e.type === "wheel" ? e : null;
2444
+ if (w && (w.metaKey || w.ctrlKey) && window.parent !== window) {
2445
+ e.preventDefault();
2446
+ const unit = w.deltaMode === 1 ? 16 : w.deltaMode === 2 ? window.innerHeight : 1;
2447
+ post({
2448
+ type: "ez:wheel",
2449
+ dx: w.deltaX * unit,
2450
+ dy: w.deltaY * unit,
2451
+ ...link.kind === "relay" ? { zoom: true, at: { x: w.clientX, y: w.clientY } } : {}
2452
+ });
2453
+ return;
2454
+ }
2455
+ if (pick2 || !popupLive() && !held || isOwnUi(e.target)) return;
2456
+ e.preventDefault();
2457
+ }
2458
+ function onKeyDown(e) {
2459
+ if (isModKey(e.key)) {
2460
+ setModHeld(true);
2461
+ return;
2462
+ }
2463
+ if (e.key === "Escape") {
2464
+ escape();
2465
+ return;
2466
+ }
2467
+ if (pick2 || ui.popup || isTyping(e.target)) return;
2468
+ post({ type: "ez:key", chord: { key: e.key, metaKey: e.metaKey, ctrlKey: e.ctrlKey } });
2469
+ }
2470
+ function onScroll(e) {
2471
+ scheduleRepaint();
2472
+ if (e.target !== document && e.target !== document.documentElement) return;
2473
+ cancelAnimationFrame(scrollRaf);
2474
+ scrollRaf = requestAnimationFrame(reportScroll);
2475
+ }
2476
+ function reportScroll() {
2477
+ if (following(performance.now(), appliedScrollAt)) return;
2478
+ appliedScrollAt = null;
2479
+ const range = scrollRange(document.documentElement.scrollHeight, window.innerHeight);
2480
+ const ratio = scrollRatio(window.scrollY, range);
2481
+ if (ratio === null) return;
2482
+ post({ type: "ez:scroll", ratio });
2483
+ }
2484
+ function scrollToRatio(ratio) {
2485
+ const range = scrollRange(document.documentElement.scrollHeight, window.innerHeight);
2486
+ if (range <= 0) return;
2487
+ const target = ratio * range;
2488
+ if (Math.abs(target - window.scrollY) < 0.5) return;
2489
+ appliedScrollAt = performance.now();
2490
+ window.scrollTo({ top: target, left: window.scrollX, behavior: "instant" });
2491
+ }
2492
+ function here() {
2493
+ return location.pathname + location.search + location.hash;
2494
+ }
2495
+ function watchLocation() {
2496
+ let url = here();
2497
+ const report = () => {
2498
+ if (here() === url) return;
2499
+ url = here();
2500
+ post({ type: "ez:page", page: location.pathname, url });
2501
+ };
2502
+ const pushState = history.pushState.bind(history);
2503
+ const replaceState = history.replaceState.bind(history);
2504
+ const reporting = (write) => (data, unused, to) => {
2505
+ write(data, unused, to);
2506
+ report();
2507
+ };
2508
+ history.pushState = reporting(framed ? replaceState : pushState);
2509
+ history.replaceState = reporting(replaceState);
2510
+ window.addEventListener("popstate", report);
2511
+ window.addEventListener("hashchange", report);
2512
+ }
2513
+ function linkTarget(e) {
2514
+ if (e.defaultPrevented || e.button !== 0) return null;
2515
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return null;
2516
+ const anchor = e.target instanceof Element ? e.target.closest("a") : null;
2517
+ if (!(anchor instanceof HTMLAnchorElement)) return null;
2518
+ if (isOwnUi(anchor) || !anchor.hasAttribute("href") || anchor.hasAttribute("download")) {
2519
+ return null;
2520
+ }
2521
+ const target = anchor.getAttribute("target");
2522
+ if (target && target !== "_self") return null;
2523
+ let url;
2524
+ try {
2525
+ url = new URL(anchor.href);
2526
+ } catch {
2527
+ return null;
2528
+ }
2529
+ if (url.origin !== location.origin) return null;
2530
+ return url.pathname + url.search + url.hash;
2531
+ }
2532
+ function onNavClick(e) {
2533
+ if (link.kind === "relay") {
2534
+ const href = e.target?.closest?.("a[href]")?.getAttribute("href");
2535
+ if (href && !href.startsWith("#")) e.preventDefault();
2536
+ return;
2537
+ }
2538
+ const to = linkTarget(e);
2539
+ if (!to) return;
2540
+ e.preventDefault();
2541
+ post({ type: "ez:navigate", url: to });
2542
+ }
2543
+ function boot() {
2544
+ watchLocation();
2545
+ ui.region.append(ui.regionSize);
2546
+ document.body.append(
2547
+ ui.selection,
2548
+ ui.highlight,
2549
+ ui.badge,
2550
+ ui.pin,
2551
+ ui.markers,
2552
+ ui.region
2553
+ );
2554
+ moveHighlight(null);
2555
+ movePin(null);
2556
+ document.addEventListener("mousedown", onMouseDown, true);
2557
+ document.addEventListener("mousemove", onMouseMove, true);
2558
+ document.addEventListener("mouseleave", onMouseLeave);
2559
+ document.addEventListener("click", onClick, true);
2560
+ if (framed) document.addEventListener("click", onNavClick);
2561
+ document.addEventListener("mouseup", onMouseUp, true);
2562
+ document.addEventListener("keydown", onKeyDown, true);
2563
+ document.addEventListener(
2564
+ "keyup",
2565
+ (e) => {
2566
+ if (isModKey(e.key)) setModHeld(false);
2567
+ },
2568
+ true
2569
+ );
2570
+ window.addEventListener("blur", () => {
2571
+ setModHeld(false);
2572
+ endFraming();
2573
+ });
2574
+ document.addEventListener("wheel", onScrollAttempt, { passive: false, capture: true });
2575
+ document.addEventListener("touchmove", onScrollAttempt, { passive: false, capture: true });
2576
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
2577
+ new MutationObserver(scheduleRepaint).observe(document.body, {
2578
+ childList: true,
2579
+ subtree: true
2580
+ });
2581
+ window.addEventListener("message", (e) => {
2582
+ if (!link.fromShell(e)) return;
2583
+ const data = e.data;
2584
+ if (data?.type === "ez:variant" && data.exploreId && data.anchor !== void 0) {
2585
+ swapper.set({
2586
+ exploreId: data.exploreId,
2587
+ anchor: data.anchor,
2588
+ html: data.html ?? null
2589
+ });
2590
+ }
2591
+ if (data?.type === "ez:variants-clear") swapper.clear();
2592
+ if (data?.type === "ez:can-explore") canExplore = data.on === true;
2593
+ if (data?.type === "ez:in-explore") inExplore = data.on === true;
2594
+ if (data?.type === "ez:set-mode") setMode(data.mode ?? "off");
2595
+ if (data?.type === "ez:escape") escape();
2596
+ if (data?.type === "ez:viewport") {
2597
+ viewportPreset = data.preset;
2598
+ const scoped = Boolean(data.scoped);
2599
+ if (scoped !== scopedToDevice) {
2600
+ scopedToDevice = scoped;
2601
+ scheduleRepaint();
2602
+ }
2603
+ setFrameZoom(typeof data.zoom === "number" ? data.zoom : 1);
2604
+ }
2605
+ if (data?.type === "ez:pick" && data.pickId) {
2606
+ armPick(data.host ?? "note", data.pickId);
2607
+ }
2608
+ if (data?.type === "ez:hold") {
2609
+ held = Boolean(data.held);
2610
+ if (held) {
2611
+ endFraming();
2612
+ hoverTarget = null;
2613
+ moveHighlight(null);
2614
+ ui.selectionBubble?.remove();
2615
+ ui.selectionBubble = null;
2616
+ }
2617
+ }
2618
+ if (data?.type === "ez:scroll-to" && typeof data.ratio === "number") scrollToRatio(data.ratio);
2619
+ if (data?.type === "ez:pick-abort") cancelPick("abort");
2620
+ if (data?.type === "ez:pick-resolve" && data.pickId) {
2621
+ if (pick2?.id === data.pickId && pick2.host === "popup" && pick2.suspended && data.ref) deliverPick(data.ref);
2622
+ else if (data.draft) restoreDraft(data.draft);
2623
+ }
2624
+ if (data?.type === "ez:restore" && data.draft) restoreDraft(data.draft);
2625
+ });
2626
+ link.subscribe((snapshot) => {
2627
+ annotations = snapshot.annotations;
2628
+ ended = snapshot.state === "ended";
2629
+ if (ended) {
2630
+ cancelPick("ended");
2631
+ setMode("off");
2632
+ }
2633
+ scheduleRepaint();
2634
+ });
2635
+ post({ type: "ez:ready", page: location.pathname, url: here() });
2636
+ }
2637
+ if (document.readyState === "loading") {
2638
+ document.addEventListener("DOMContentLoaded", boot);
2639
+ } else {
2640
+ boot();
2641
+ }
2642
+ })();