@cychien/ezdesign 0.8.1 → 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.
package/dist/overlay.js CHANGED
@@ -4,14 +4,13 @@
4
4
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
5
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
6
 
7
- // node_modules/.pnpm/@hugeicons+core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/Add01Icon.js
7
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/Add01Icon.js
8
8
  var Add01Icon = [
9
9
  ["path", { d: "M12.001 5.00003V19.002", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
10
10
  ["path", { d: "M19.002 12.002L4.99998 12.002", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }]
11
11
  ];
12
- var Add01Icon_default = Add01Icon;
13
12
 
14
- // node_modules/.pnpm/@hugeicons+core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/AlignSelectionIcon.js
13
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/AlignSelectionIcon.js
15
14
  var AlignSelectionIcon = [
16
15
  ["path", { d: "M20 6V18M18 4H6M18 20H6M4 18V6", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
17
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" }],
@@ -19,35 +18,40 @@
19
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" }],
20
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" }]
21
20
  ];
22
- var AlignSelectionIcon_default = AlignSelectionIcon;
23
21
 
24
- // node_modules/.pnpm/@hugeicons+core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/TextSelectIcon.js
22
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/TextSelectIcon.js
25
23
  var TextSelectIcon = [
26
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" }],
27
25
  ["path", { d: "M7 8H15M7 12H17M7 16H13", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }]
28
26
  ];
29
- var TextSelectIcon_default = TextSelectIcon;
30
27
 
31
- // node_modules/.pnpm/@hugeicons+core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/MagicWand01Icon.js
28
+ // node_modules/.pnpm/@hugeicons+core-free-icons@4.3.5/node_modules/@hugeicons/core-free-icons/dist/esm/MagicWand01Icon.js
32
29
  var MagicWand01Icon = [
33
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" }],
34
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" }],
35
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" }]
36
33
  ];
37
- var MagicWand01Icon_default = MagicWand01Icon;
38
34
 
39
- // node_modules/.pnpm/@hugeicons+core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/File02Icon.js
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
40
44
  var File02Icon = [
41
45
  ["path", { d: "M8 17H16", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "0" }],
42
46
  ["path", { d: "M8 13H12", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "1.5", key: "1" }],
43
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" }]
44
48
  ];
45
- var File02Icon_default = File02Icon;
46
49
 
47
50
  // src/client/draft.ts
48
51
  var NBSP = "\xA0";
49
52
  var refMarker = (n) => `[ref ${n}]`;
50
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}`;
51
55
  var fileMarker = (n) => `[file ${n}]`;
52
56
  var skillMarker = (n) => `[skill ${n}]`;
53
57
  function draftText(body) {
@@ -59,6 +63,7 @@
59
63
  else if (node.t === "ref" && node.anchor) out.push(refMarker(node.n));
60
64
  else if (node.t === "file" && node.id) out.push(fileMarker(++file));
61
65
  else if (node.t === "skill") out.push(skillMarker(++skill));
66
+ else if (node.t === "pick") out.push(node.files.map(() => fileMarker(++file)).join(" "));
62
67
  }
63
68
  return out.join("").replaceAll(NBSP, " ").trim();
64
69
  }
@@ -87,7 +92,7 @@
87
92
  return max + 1;
88
93
  }
89
94
  function draftFileIds(body) {
90
- return body.flatMap((n) => n.t === "file" && n.id ? [n.id] : []);
95
+ return body.flatMap((n) => n.t === "file" && n.id ? [n.id] : n.t === "pick" ? n.files.map((f) => f.id) : []);
91
96
  }
92
97
  function draftPendingNames(body) {
93
98
  return body.flatMap((n) => n.t === "file" && !n.id ? [n.name] : []);
@@ -142,6 +147,7 @@
142
147
  // src/client/slash.ts
143
148
  var MAX_QUERY = 24;
144
149
  var BREAK = /[\s ]/;
150
+ var menuCount = 0;
145
151
  function detectSlash(before, trigger = "/") {
146
152
  const start = before.lastIndexOf(trigger);
147
153
  if (start === -1) return null;
@@ -168,6 +174,7 @@
168
174
  }) {
169
175
  const doc = editable.ownerDocument;
170
176
  const view = doc.defaultView;
177
+ const listId = `ez-slash-${++menuCount}`;
171
178
  let menu = null;
172
179
  let trigger = null;
173
180
  let shown = [];
@@ -211,6 +218,7 @@
211
218
  item.toggleAttribute("data-active", i === active);
212
219
  item.setAttribute("aria-selected", String(i === active));
213
220
  });
221
+ editable.setAttribute("aria-activedescendant", `${listId}-${active}`);
214
222
  }
215
223
  function render() {
216
224
  if (!menu) return;
@@ -218,10 +226,13 @@
218
226
  const headEl = mk("div", "ez-slash-head");
219
227
  headEl.textContent = title;
220
228
  const list = mk("div", "ez-slash-list");
229
+ list.id = listId;
221
230
  list.setAttribute("role", "listbox");
231
+ list.setAttribute("aria-label", title);
222
232
  menu.append(headEl, list);
223
233
  shown.forEach((command, i) => {
224
234
  const item = mk("div", "ez-slash-item");
235
+ item.id = `${listId}-${i}`;
225
236
  item.setAttribute("role", "option");
226
237
  const label = mk("span", "ez-slash-label");
227
238
  label.textContent = command.label;
@@ -255,6 +266,7 @@
255
266
  function open() {
256
267
  menu = mk("div", "ez-slash-menu");
257
268
  doc.body.appendChild(menu);
269
+ editable.setAttribute("aria-controls", listId);
258
270
  doc.addEventListener("selectionchange", onSelectionChange);
259
271
  editable.addEventListener("blur", onBlur);
260
272
  }
@@ -263,6 +275,8 @@
263
275
  menu.remove();
264
276
  menu = null;
265
277
  trigger = null;
278
+ editable.removeAttribute("aria-controls");
279
+ editable.removeAttribute("aria-activedescendant");
266
280
  doc.removeEventListener("selectionchange", onSelectionChange);
267
281
  editable.removeEventListener("blur", onBlur);
268
282
  return true;
@@ -351,17 +365,43 @@
351
365
 
352
366
  // src/client/attach.ts
353
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
+ }
354
392
  var CHIP_ATTR = "data-ez-chip";
355
393
  var REF_ATTR = "data-ez-ref";
356
394
  var SKILL_ATTR = "data-ez-skill";
395
+ var PICK_ATTR = "data-ez-pick";
357
396
  function attachify({
358
- api,
397
+ attachments,
359
398
  mk,
360
399
  className,
361
400
  placeholder,
362
401
  onChange,
363
402
  commands,
364
- skills
403
+ skills,
404
+ chooseLibrary
365
405
  }) {
366
406
  const editable = mk("div", className);
367
407
  editable.setAttribute("contenteditable", "plaintext-only");
@@ -391,9 +431,19 @@
391
431
  label: "File",
392
432
  hint: "\u9644\u52A0\u6A94\u6848",
393
433
  keywords: ["file", "attach", "upload", "\u6A94\u6848", "\u9644\u4EF6", "\u5716\u7247"],
394
- icon: File02Icon_default,
434
+ icon: File02Icon,
395
435
  run: () => picker.click()
396
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
+ ] : [],
397
447
  ...commands ?? []
398
448
  ]
399
449
  });
@@ -406,7 +456,6 @@
406
456
  });
407
457
  const uploading = /* @__PURE__ */ new Set();
408
458
  let errorTimer = 0;
409
- const chips = () => [...editable.querySelectorAll(`[${CHIP_ATTR}]`)];
410
459
  function showError(message) {
411
460
  error.textContent = message;
412
461
  error.hidden = false;
@@ -416,16 +465,21 @@
416
465
  }, 6e3);
417
466
  }
418
467
  function paintEmpty() {
419
- const empty = !editable.textContent?.trim() && chips().length === 0;
468
+ const empty = !editable.textContent?.trim() && !editable.querySelector(".ez-chip");
420
469
  editable.toggleAttribute("data-empty", empty);
421
470
  }
471
+ function paintPicks() {
472
+ editable.querySelectorAll(`[${PICK_ATTR}] .ez-chip-name`).forEach((name, i) => {
473
+ name.textContent = pickChipText(i + 1);
474
+ });
475
+ }
422
476
  function changed() {
423
477
  paintEmpty();
478
+ paintPicks();
424
479
  onChange?.();
425
480
  }
426
481
  function del(id) {
427
- fetch(`${api}/attachments/${id}`, { method: "DELETE" }).catch(() => {
428
- });
482
+ attachments.discard(id);
429
483
  }
430
484
  function makeChip(name, attr, value, glyph) {
431
485
  const chip = mk("span", "ez-chip");
@@ -439,19 +493,24 @@
439
493
  if (!value) chip.classList.add("ez-chip-pending");
440
494
  return chip;
441
495
  }
442
- const fileChip = (name, id = "") => makeChip(name, CHIP_ATTR, id, File02Icon_default);
496
+ const fileChip = (name, id = "") => makeChip(name, CHIP_ATTR, id, File02Icon);
443
497
  const refChip = (ref, placeholder2) => {
444
498
  const chip = makeChip(
445
499
  ref ? refChipText(ref.n, !!ref.variant) : placeholder2,
446
500
  REF_ATTR,
447
501
  ref ? JSON.stringify(ref) : "",
448
- ref?.variant ? MagicWand01Icon_default : AlignSelectionIcon_default
502
+ ref?.variant ? MagicWand01Icon : AlignSelectionIcon
449
503
  );
450
504
  if (ref?.variant) chip.classList.add("ez-chip-chosen");
451
505
  const title = ref?.variant ? [ref.variant.name, ref.label].filter(Boolean).join(" \xB7 ") : ref?.label ?? "";
452
506
  if (title) chip.title = title;
453
507
  return chip;
454
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
+ };
455
514
  const skillChip = (name) => makeChip(`$${name}`, SKILL_ATTR, name, null);
456
515
  const pendingRef = () => editable.querySelector(`[${REF_ATTR}=""]`);
457
516
  function placeCaretAfter(node) {
@@ -489,20 +548,7 @@
489
548
  }
490
549
  async function upload(chip, file) {
491
550
  try {
492
- const res = await fetch(`${api}/attachments`, {
493
- method: "POST",
494
- headers: {
495
- "content-type": file.type || "application/octet-stream",
496
- // A filename is free to hold bytes no header may carry.
497
- "x-ez-name": encodeURIComponent(file.name || "image.png")
498
- },
499
- body: file
500
- });
501
- if (!res.ok) {
502
- const body = await res.json().catch(() => null);
503
- throw new Error(body?.error ?? `HTTP ${res.status}`);
504
- }
505
- const saved = await res.json();
551
+ const saved = await attachments.upload(file);
506
552
  if (!editable.contains(chip)) {
507
553
  del(saved.id);
508
554
  return;
@@ -518,6 +564,26 @@
518
564
  changed();
519
565
  }
520
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
+ }
521
587
  function take(list) {
522
588
  return list ? [...list] : [];
523
589
  }
@@ -566,6 +632,7 @@
566
632
  if (node.t === "text") editable.appendChild(document.createTextNode(node.v));
567
633
  else if (node.t === "file") editable.appendChild(fileChip(node.name, node.id));
568
634
  else if (node.t === "skill") editable.appendChild(skillChip(node.name));
635
+ else if (node.t === "pick") editable.appendChild(pickChip(node.files));
569
636
  else {
570
637
  const ref = node.anchor ? {
571
638
  n: node.n,
@@ -590,7 +657,7 @@
590
657
  e.stopPropagation();
591
658
  for (const file of files) add(file);
592
659
  });
593
- const isChipNode = (node) => node instanceof HTMLElement && (node.hasAttribute(CHIP_ATTR) || node.hasAttribute(REF_ATTR) || node.hasAttribute(SKILL_ATTR));
660
+ const isChipNode = (node) => node instanceof HTMLElement && (node.hasAttribute(CHIP_ATTR) || node.hasAttribute(REF_ATTR) || node.hasAttribute(SKILL_ATTR) || node.hasAttribute(PICK_ATTR));
594
661
  editable.addEventListener("beforeinput", (e) => {
595
662
  if (e.inputType !== "deleteContentBackward") return;
596
663
  const selection = document.getSelection();
@@ -673,7 +740,7 @@
673
740
  },
674
741
  discard() {
675
742
  slash.close();
676
- for (const id of chips().map((c) => c.getAttribute(CHIP_ATTR))) if (id) del(id);
743
+ for (const id of draftFileIds(snapshot())) del(id);
677
744
  uploading.clear();
678
745
  editable.textContent = "";
679
746
  changed();
@@ -703,6 +770,10 @@
703
770
  out.push({ t: "file", id: child.getAttribute(CHIP_ATTR) ?? "", name });
704
771
  continue;
705
772
  }
773
+ if (child.hasAttribute(PICK_ATTR)) {
774
+ out.push({ t: "pick", files: parsePick(child.getAttribute(PICK_ATTR)) });
775
+ continue;
776
+ }
706
777
  if (child.hasAttribute(SKILL_ATTR)) {
707
778
  out.push({ t: "skill", name: child.getAttribute(SKILL_ATTR) ?? "" });
708
779
  continue;
@@ -731,10 +802,144 @@
731
802
  return null;
732
803
  }
733
804
  }
805
+ function parsePick(raw) {
806
+ try {
807
+ return JSON.parse(raw ?? "[]");
808
+ } catch {
809
+ return [];
810
+ }
811
+ }
734
812
 
735
- // src/client/pick.ts
736
- function modLabel(userAgent) {
737
- return /Mac|iP(hone|ad|od)/.test(userAgent) ? "\u2318" : "Ctrl";
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
+ };
738
943
  }
739
944
 
740
945
  // src/client/variant.ts
@@ -1154,6 +1359,7 @@
1154
1359
  }
1155
1360
  })();
1156
1361
  var API = `${PREFIX}/api`;
1362
+ var link = linkFromScript(document.currentScript, API);
1157
1363
  var SOURCE_ATTR2 = "data-ez-source";
1158
1364
  var mode = "off";
1159
1365
  var ended = false;
@@ -1200,10 +1406,6 @@
1200
1406
  badge: el("div", "ez-badge"),
1201
1407
  pin: el("div", "ez-pin"),
1202
1408
  markers: el("div", "ez-markers"),
1203
- /** Shown only while picking: a wash at the edges of the viewport, and a pill
1204
- * saying what the modifier does. The page stays clickable underneath, so the
1205
- * chrome is the only thing telling the user this moment is different. */
1206
- banner: el("div", "ez-pick-banner"),
1207
1409
  /** The box being dragged, with its size read out in the corner. */
1208
1410
  region: el("div", "ez-region"),
1209
1411
  regionSize: el("div", "ez-region-size"),
@@ -1309,7 +1511,9 @@
1309
1511
  height: Math.round(rect.height)
1310
1512
  },
1311
1513
  viewport: { width: window.innerWidth, height: window.innerHeight, preset: viewportPreset },
1312
- page: location.pathname
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 }
1313
1517
  };
1314
1518
  }
1315
1519
  var clamp01 = (n) => Math.min(1, Math.max(0, n));
@@ -1481,7 +1685,7 @@
1481
1685
  const popup = el("div", "ez-popup");
1482
1686
  const actions = el("div", "ez-actions");
1483
1687
  const save = el("button", "ez-btn ez-btn-primary");
1484
- const saveIcon = icon(Add01Icon_default, 14);
1688
+ const saveIcon = icon(Add01Icon, 14);
1485
1689
  const saveLabel = document.createTextNode("\u52A0\u5165\u5F85\u9001\u6E05\u55AE");
1486
1690
  save.append(saveIcon, saveLabel);
1487
1691
  const cancel = el("button", "ez-btn");
@@ -1491,11 +1695,12 @@
1491
1695
  const exploreTarget = subject.kind === "element" ? exploreElement : null;
1492
1696
  let exploring = false;
1493
1697
  const exploreHead = el("div", "ez-explore-head");
1494
- exploreHead.append(icon(MagicWand01Icon_default, 15), el("span", "ez-explore-title"));
1698
+ exploreHead.append(icon(MagicWand01Icon, 15), el("span", "ez-explore-title"));
1495
1699
  exploreHead.querySelector(".ez-explore-title").textContent = "\u63A2\u7D22\u6A23\u5F0F";
1496
1700
  exploreHead.hidden = true;
1497
1701
  const attach = attachify({
1498
- api: API,
1702
+ attachments: link.attachments,
1703
+ ...link.chooseLibrary ? { chooseLibrary: link.chooseLibrary } : {},
1499
1704
  mk: el,
1500
1705
  className: "ez-input",
1501
1706
  placeholder: COMPOSE_PLACEHOLDER,
@@ -1512,7 +1717,7 @@
1512
1717
  label: "Element",
1513
1718
  hint: `\u9078\u53D6\u9801\u9762\u5143\u7D20\u6216\u7BC4\u570D`,
1514
1719
  keywords: ["element", "pick", "ref", "reference", "\u5143\u7D20", "\u6307\u5B9A", "\u53C3\u8003", "\u6846\u9078"],
1515
- icon: AlignSelectionIcon_default,
1720
+ icon: AlignSelectionIcon,
1516
1721
  run: () => void armPick("popup", newPickId())
1517
1722
  },
1518
1723
  {
@@ -1520,7 +1725,7 @@
1520
1725
  label: "Explore",
1521
1726
  hint: "\u63A2\u7D22\u6A23\u5F0F",
1522
1727
  keywords: ["explore", "variant", "ui", "\u63A2\u7D22", "\u6A23\u5F0F", "\u7248\u672C"],
1523
- icon: MagicWand01Icon_default,
1728
+ icon: MagicWand01Icon,
1524
1729
  enabled: () => canExplore && !!exploreTarget && !exploring,
1525
1730
  run: () => setExploring(true)
1526
1731
  }
@@ -1630,12 +1835,7 @@
1630
1835
  async function saveAnnotation(kind, element, comment, attachments, references, extra) {
1631
1836
  const anchor = extra?.anchor ?? (element ? buildAnchor(element, extra?.selectedText, extra?.pin) : null);
1632
1837
  if (!anchor) throw new Error("no anchor to save against");
1633
- const res = await fetch(`${API}/annotations`, {
1634
- method: "POST",
1635
- headers: { "content-type": "application/json" },
1636
- body: JSON.stringify({ kind, comment, attachments, references, anchor })
1637
- });
1638
- if (!res.ok) throw new Error(`annotation rejected (${res.status})`);
1838
+ await link.annotate({ kind, comment, attachments, references, anchor });
1639
1839
  }
1640
1840
  function corroborates(hit, anchor) {
1641
1841
  if (!anchor.source) return true;
@@ -1785,12 +1985,14 @@
1785
1985
  function renderMarkers() {
1786
1986
  ui.markers.textContent = "";
1787
1987
  if (pick2) return;
1788
- const relevant = annotations.filter((a) => a.anchor.page === location.pathname && madeHere(a));
1789
- relevant.forEach((a, i) => {
1988
+ const relevant = annotations.filter(
1989
+ (a) => (link.kind === "relay" || a.anchor.page === location.pathname) && madeHere(a)
1990
+ );
1991
+ relevant.forEach((a) => {
1790
1992
  const at = markerPosition(a);
1791
1993
  if (!at) return;
1792
1994
  const marker = el("div", "ez-marker");
1793
- marker.textContent = String(i + 1);
1995
+ marker.textContent = String(a.n ?? annotations.indexOf(a) + 1);
1794
1996
  marker.title = a.comment;
1795
1997
  const r = MARKER_SIZE / 2;
1796
1998
  let top = at.top - r;
@@ -1815,12 +2017,11 @@
1815
2017
  paintPopup();
1816
2018
  });
1817
2019
  }
1818
- var MOD_LABEL = modLabel(navigator.userAgent);
1819
2020
  var PICKING_LABEL = "\u9078\u53D6\u4E2D\u2026";
1820
2021
  var pickSeq = 0;
1821
2022
  var newPickId = () => `p${++pickSeq}-${Date.now().toString(36)}`;
1822
2023
  function post(message) {
1823
- window.parent?.postMessage(message, location.origin);
2024
+ link.post(message);
1824
2025
  }
1825
2026
  function suspendPopup() {
1826
2027
  if (!ui.popup) return false;
@@ -1843,19 +2044,6 @@
1843
2044
  ui.popupAttach?.editable.focus();
1844
2045
  }
1845
2046
  var inExplore = false;
1846
- function paintPickChrome() {
1847
- const on = Boolean(pick2);
1848
- ui.banner.style.display = on ? "flex" : "none";
1849
- if (!pick2) return;
1850
- const back = pick2.returnTo && pick2.returnTo !== location.pathname ? pick2.returnTo : null;
1851
- ui.banner.textContent = "";
1852
- const text = el("span", "ez-pick-text");
1853
- text.textContent = back ? `${MOD_LABEL} + \u9EDE\u64CA\u5143\u7D20\u6216\u76F4\u63A5\u62D6\u66F3\u6846\u9078\u30FB\u5B8C\u6210\u5F8C\u56DE\u5230 ${back}` : `${MOD_LABEL} + \u9EDE\u64CA\u5143\u7D20\uFF0C\u6216\u62D6\u66F3\u6846\u9078\u7BC4\u570D`;
1854
- const cancel = el("button", "ez-pick-cancel");
1855
- cancel.textContent = "\u53D6\u6D88";
1856
- cancel.onclick = () => cancelPick("escape");
1857
- ui.banner.append(icon(AlignSelectionIcon_default, 14), text, cancel);
1858
- }
1859
2047
  function currentDraft() {
1860
2048
  if (!pick2 || pick2.host !== "popup" || !ui.popupAttach || !popupSubject) return null;
1861
2049
  return {
@@ -1881,7 +2069,7 @@
1881
2069
  const draft = currentDraft();
1882
2070
  if (draft) post({ type: "ez:draft", pickId: draft.id, draft });
1883
2071
  }
1884
- function armPick(host, id, returnTo) {
2072
+ function armPick(host, id) {
1885
2073
  if (ended || pick2) return false;
1886
2074
  const attach = host === "popup" ? ui.popupAttach : null;
1887
2075
  pick2 = {
@@ -1889,7 +2077,6 @@
1889
2077
  host,
1890
2078
  suspended: false,
1891
2079
  prevHighlight: highlightTarget,
1892
- returnTo,
1893
2080
  draftAt: Date.now(),
1894
2081
  scroll: { x: window.scrollX, y: window.scrollY }
1895
2082
  };
@@ -1902,7 +2089,6 @@
1902
2089
  document.documentElement.setAttribute("data-ez-pick", "");
1903
2090
  hoverTarget = null;
1904
2091
  moveHighlight(null);
1905
- paintPickChrome();
1906
2092
  scheduleRepaint();
1907
2093
  return true;
1908
2094
  }
@@ -1933,7 +2119,6 @@
1933
2119
  document.documentElement.removeAttribute("data-ez-pick-hot");
1934
2120
  hoverTarget = null;
1935
2121
  moveHighlight(done.prevHighlight);
1936
- paintPickChrome();
1937
2122
  scheduleRepaint();
1938
2123
  return done;
1939
2124
  }
@@ -2102,7 +2287,7 @@
2102
2287
  dismiss();
2103
2288
  ui.selectionBubble?.remove();
2104
2289
  ui.selectionBubble = null;
2105
- window.parent?.postMessage({ type: "ez:mode", mode }, location.origin);
2290
+ post({ type: "ez:mode", mode });
2106
2291
  }
2107
2292
  function onMouseMove(e) {
2108
2293
  lastPointer = { x: e.clientX, y: e.clientY };
@@ -2231,7 +2416,7 @@
2231
2416
  const text = range.toString().trim();
2232
2417
  const rect = range.getBoundingClientRect();
2233
2418
  const bubble = el("button", "ez-select-btn");
2234
- bubble.append(icon(TextSelectIcon_default, 14), document.createTextNode("\u6A19\u8A3B\u9078\u53D6\u6587\u5B57"));
2419
+ bubble.append(icon(TextSelectIcon, 14), document.createTextNode("\u6A19\u8A3B\u9078\u53D6\u6587\u5B57"));
2235
2420
  Object.assign(bubble.style, {
2236
2421
  top: `${Math.max(4, rect.bottom + window.scrollY + 6)}px`,
2237
2422
  left: `${Math.max(4, rect.left + window.scrollX)}px`
@@ -2259,7 +2444,12 @@
2259
2444
  if (w && (w.metaKey || w.ctrlKey) && window.parent !== window) {
2260
2445
  e.preventDefault();
2261
2446
  const unit = w.deltaMode === 1 ? 16 : w.deltaMode === 2 ? window.innerHeight : 1;
2262
- post({ type: "ez:wheel", dx: w.deltaX * unit, dy: w.deltaY * unit });
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
+ });
2263
2453
  return;
2264
2454
  }
2265
2455
  if (pick2 || !popupLive() && !held || isOwnUi(e.target)) return;
@@ -2340,6 +2530,11 @@
2340
2530
  return url.pathname + url.search + url.hash;
2341
2531
  }
2342
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
+ }
2343
2538
  const to = linkTarget(e);
2344
2539
  if (!to) return;
2345
2540
  e.preventDefault();
@@ -2354,12 +2549,10 @@
2354
2549
  ui.badge,
2355
2550
  ui.pin,
2356
2551
  ui.markers,
2357
- ui.region,
2358
- ui.banner
2552
+ ui.region
2359
2553
  );
2360
2554
  moveHighlight(null);
2361
2555
  movePin(null);
2362
- paintPickChrome();
2363
2556
  document.addEventListener("mousedown", onMouseDown, true);
2364
2557
  document.addEventListener("mousemove", onMouseMove, true);
2365
2558
  document.addEventListener("mouseleave", onMouseLeave);
@@ -2386,7 +2579,7 @@
2386
2579
  subtree: true
2387
2580
  });
2388
2581
  window.addEventListener("message", (e) => {
2389
- if (e.origin !== location.origin) return;
2582
+ if (!link.fromShell(e)) return;
2390
2583
  const data = e.data;
2391
2584
  if (data?.type === "ez:variant" && data.exploreId && data.anchor !== void 0) {
2392
2585
  swapper.set({
@@ -2410,7 +2603,7 @@
2410
2603
  setFrameZoom(typeof data.zoom === "number" ? data.zoom : 1);
2411
2604
  }
2412
2605
  if (data?.type === "ez:pick" && data.pickId) {
2413
- armPick(data.host ?? "note", data.pickId, data.returnTo);
2606
+ armPick(data.host ?? "note", data.pickId);
2414
2607
  }
2415
2608
  if (data?.type === "ez:hold") {
2416
2609
  held = Boolean(data.held);
@@ -2424,11 +2617,13 @@
2424
2617
  }
2425
2618
  if (data?.type === "ez:scroll-to" && typeof data.ratio === "number") scrollToRatio(data.ratio);
2426
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
+ }
2427
2624
  if (data?.type === "ez:restore" && data.draft) restoreDraft(data.draft);
2428
2625
  });
2429
- const events = new EventSource(`${API}/events`);
2430
- events.onmessage = (e) => {
2431
- const snapshot = JSON.parse(e.data);
2626
+ link.subscribe((snapshot) => {
2432
2627
  annotations = snapshot.annotations;
2433
2628
  ended = snapshot.state === "ended";
2434
2629
  if (ended) {
@@ -2436,7 +2631,7 @@
2436
2631
  setMode("off");
2437
2632
  }
2438
2633
  scheduleRepaint();
2439
- };
2634
+ });
2440
2635
  post({ type: "ez:ready", page: location.pathname, url: here() });
2441
2636
  }
2442
2637
  if (document.readyState === "loading") {