contactsheet 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,7 +152,10 @@ open(橙) ──标记完成──▶ resolved(绿·待核验) ──核验通
152
152
  这份文件就是历史记录,日后沉淀 skill / 复盘"当初都提过什么、怎么改的"的原料。
153
153
  - 「删除」只留给误钉的 pin,会真的从历史里抹掉。
154
154
  - `Cmd/Ctrl + V` → 贴图,存进 `design/.canvas/refs/`。**在批注输入框或气泡编辑态里粘贴 = 挂到这条批注**
155
- (缩略图跟着 pin 走,推给 Claude 的上下文里带上它的路径);在别处粘贴才进右下角的全局坞
155
+ (缩略图跟着 pin 走,推给 Claude 的上下文里带上它的路径);在别处粘贴才进右下角的全局坞。
156
+ 粘贴时在**光标处插入 `[图片 n]` 占位符**——图在文字里有位置,「把这里改成[图片 1]这样」是完整的话。
157
+ 气泡下方是编号对应的缩略图(点开大图);**悬停缩略图出 ✕ 可移除**,正文占位符同步摘掉并重编号(文件保留在 refs/)。
158
+ 推送给 Claude 的文本里正文自带 `[图片 n]`,下方 `图片 n:路径` 一一对应,Claude 能 Read 到图
156
159
  - `Ctrl + 滚轮` / 捏合 → 以光标为锚点缩放(0.05–2 倍);空白处拖拽或双指滚动 → 平移
157
160
  - `1` 全景(一屏看完整面墙)· `2` 聚焦当前画板 · `0` 回到 100% · `+` / `-` 步进缩放
158
161
  - 点一个 pin 选中它,然后 `Enter` 编辑(编辑中再按 `Enter` 保存,`Shift+Enter` 换行)、
@@ -362,6 +362,34 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
362
362
  return;
363
363
  }
364
364
  }
365
+ var UPLOADING = "__uploading__";
366
+ function removeRefToken(text, idx1) {
367
+ return text.replace(new RegExp(`\\[\u56FE\u7247 ${idx1}\\]`, "g"), "").replace(/\[图片 (\d+)\]/g, (m, d) => {
368
+ const n = Number(d);
369
+ return n > idx1 ? `[\u56FE\u7247 ${n - 1}]` : m;
370
+ });
371
+ }
372
+ function insertAtCursor(ta, token) {
373
+ const start = ta.selectionStart ?? ta.value.length;
374
+ const end = ta.selectionEnd ?? start;
375
+ ta.setRangeText(token, start, end, "end");
376
+ ta.dispatchEvent(new Event("input", { bubbles: true }));
377
+ }
378
+ function attachInline(ta, refs, file, redraw) {
379
+ const n = refs.length + 1;
380
+ refs.push(UPLOADING);
381
+ insertAtCursor(ta, `[\u56FE\u7247 ${n}]`);
382
+ redraw();
383
+ void uploadRef(file).then((path) => {
384
+ refs[n - 1] = path;
385
+ redraw();
386
+ }).catch((err) => {
387
+ refs.splice(n - 1, 1);
388
+ ta.value = removeRefToken(ta.value, n);
389
+ redraw();
390
+ toast(`\u53C2\u8003\u56FE\u4E0A\u4F20\u5931\u8D25:${String(err)}`, "error");
391
+ });
392
+ }
365
393
  async function uploadRef(file) {
366
394
  const dataUrl = await readDataUrl(file);
367
395
  const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
@@ -383,7 +411,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
383
411
  function refSrc(path) {
384
412
  return localSrc.get(path) ?? refUrl(path);
385
413
  }
386
- function refThumb(path) {
414
+ function refThumb(path, opts) {
387
415
  const card = h("div", "cs-ref-thumb");
388
416
  card.title = path;
389
417
  const img = h("img");
@@ -395,11 +423,21 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
395
423
  img.alt = path;
396
424
  img.src = refSrc(path);
397
425
  if (img.complete && img.naturalWidth === 0) broken();
398
- card.append(img, h("span", "cs-ref-name", fileOf(path)));
426
+ card.append(img, h("span", "cs-ref-name", opts?.index ? `\u56FE\u7247 ${opts.index}` : fileOf(path)));
399
427
  card.addEventListener("click", (e) => {
400
428
  e.stopPropagation();
401
429
  openLightbox(refSrc(path), path);
402
430
  });
431
+ if (opts?.onRemove) {
432
+ const rm = h("button", "cs-ref-rm", "\u2715");
433
+ rm.title = "\u4ECE\u8FD9\u6761\u6279\u6CE8\u79FB\u9664(\u6587\u4EF6\u4FDD\u7559\u5728 refs/)";
434
+ rm.setAttribute("aria-label", rm.title);
435
+ rm.addEventListener("click", (e) => {
436
+ e.stopPropagation();
437
+ opts.onRemove?.();
438
+ });
439
+ card.appendChild(rm);
440
+ }
403
441
  return card;
404
442
  }
405
443
  function openLightbox(src, caption) {
@@ -806,16 +844,33 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
806
844
  }, 250);
807
845
  });
808
846
  }
809
- function refStrip(paths) {
847
+ function refStrip(paths, onRemove) {
810
848
  const strip = h("div", "cs-ref-strip");
811
- for (const p of paths) strip.appendChild(refThumb(p));
849
+ paths.forEach((p, i) => {
850
+ if (p === UPLOADING) {
851
+ strip.appendChild(h("div", "cs-ref-thumb is-broken", `\u56FE\u7247 ${i + 1} \xB7 \u4E0A\u4F20\u4E2D\u2026`));
852
+ return;
853
+ }
854
+ strip.appendChild(refThumb(p, { index: i + 1, onRemove: onRemove ? () => onRemove(i) : void 0 }));
855
+ });
812
856
  return strip;
813
857
  }
814
858
  function bubble(ann) {
815
859
  const box = h("div", "cs-pin-bubble");
816
860
  const textEl = h("div", "cs-pin-text", ann.text);
817
861
  box.appendChild(textEl);
818
- if (ann.refs?.length) box.appendChild(refStrip(ann.refs));
862
+ if (ann.refs?.length) {
863
+ box.appendChild(
864
+ refStrip(ann.refs, (idx) => {
865
+ const next = (ann.refs ?? []).filter((_, i) => i !== idx);
866
+ void transition(
867
+ ann.id,
868
+ { refs: next, text: removeRefToken(ann.text, idx + 1) },
869
+ "\u53C2\u8003\u56FE\u5DF2\u79FB\u9664(\u6587\u4EF6\u4FDD\u7559\u5728 refs/ \u91CC)"
870
+ );
871
+ })
872
+ );
873
+ }
819
874
  const meta = h("div", "cs-pin-meta");
820
875
  meta.appendChild(h("span", void 0, ann.status === "open" ? "open" : "\u5F85\u6838\u9A8C"));
821
876
  const act = (label, cls, fn) => {
@@ -861,17 +916,21 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
861
916
  const ta = h("textarea", "cs-pin-edit");
862
917
  ta.value = ann.text;
863
918
  const refs = [...ann.refs ?? []];
864
- const strip = box.querySelector(".cs-ref-strip") ?? refStrip([]);
865
- if (!strip.isConnected) box.insertBefore(strip, box.querySelector(".cs-pin-meta"));
919
+ let strip = box.querySelector(".cs-ref-strip");
920
+ const redrawStrip = () => {
921
+ const next = refStrip(refs, (idx) => {
922
+ refs.splice(idx, 1);
923
+ ta.value = removeRefToken(ta.value, idx + 1);
924
+ redrawStrip();
925
+ });
926
+ if (strip?.isConnected) strip.replaceWith(next);
927
+ else box.insertBefore(next, box.querySelector(".cs-pin-meta"));
928
+ strip = next;
929
+ };
930
+ redrawStrip();
866
931
  setPasteTarget({
867
932
  el: box,
868
- onFile: (file) => {
869
- void uploadRef(file).then((path) => {
870
- refs.push(path);
871
- strip.appendChild(refThumb(path));
872
- toast(`\u53C2\u8003\u56FE\u5DF2\u4E0A\u4F20:${path} \u2014\u2014 \u70B9\u300C\u4FDD\u5B58\u300D\u540E\u624D\u6302\u5230\u8FD9\u6761\u6279\u6CE8`, "ok");
873
- }).catch((err) => toast(`\u53C2\u8003\u56FE\u4E0A\u4F20\u5931\u8D25:${String(err)}`, "error"));
874
- }
933
+ onFile: (file) => attachInline(ta, refs, file, redrawStrip)
875
934
  });
876
935
  ta.addEventListener("keydown", (e) => {
877
936
  e.stopPropagation();
@@ -888,9 +947,10 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
888
947
  const save = h("button", "cs-x", "\u4FDD\u5B58");
889
948
  save.addEventListener("click", (e) => {
890
949
  e.stopPropagation();
950
+ if (refs.includes(UPLOADING)) return toast("\u6709\u56FE\u7247\u8FD8\u5728\u4E0A\u4F20,\u7A0D\u7B49\u4E00\u4E0B\u518D\u4FDD\u5B58", "warn");
891
951
  const text = ta.value.trim();
892
952
  const textChanged = !!text && text !== ann.text;
893
- const refsChanged = refs.length !== (ann.refs?.length ?? 0);
953
+ const refsChanged = JSON.stringify(refs) !== JSON.stringify(ann.refs ?? []);
894
954
  if (!textChanged && !refsChanged) return renderPins();
895
955
  const patch = {};
896
956
  if (textChanged) patch.text = text;
@@ -1031,15 +1091,19 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1031
1091
  composer = box;
1032
1092
  ta.focus();
1033
1093
  const refs = [];
1094
+ let stripEl = strip;
1095
+ const redrawStrip = () => {
1096
+ const next = refStrip(refs, (idx) => {
1097
+ refs.splice(idx, 1);
1098
+ ta.value = removeRefToken(ta.value, idx + 1);
1099
+ redrawStrip();
1100
+ });
1101
+ stripEl.replaceWith(next);
1102
+ stripEl = next;
1103
+ };
1034
1104
  setPasteTarget({
1035
1105
  el: box,
1036
- onFile: (file) => {
1037
- void uploadRef(file).then((path) => {
1038
- refs.push(path);
1039
- strip.appendChild(refThumb(path));
1040
- setProbe(`\u53C2\u8003\u56FE\u5DF2\u6302\u5230\u8FD9\u6761\u6279\u6CE8:${path}`);
1041
- }).catch((err) => toast(`\u53C2\u8003\u56FE\u4E0A\u4F20\u5931\u8D25:${String(err)}`, "error"));
1042
- }
1106
+ onFile: (file) => attachInline(ta, refs, file, redrawStrip)
1043
1107
  });
1044
1108
  ta.addEventListener("keydown", (e) => {
1045
1109
  e.stopPropagation();
@@ -1048,6 +1112,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1048
1112
  exitPinMode();
1049
1113
  } else if (e.key === "Enter" && !e.shiftKey) {
1050
1114
  e.preventDefault();
1115
+ if (refs.includes(UPLOADING)) return toast("\u6709\u56FE\u7247\u8FD8\u5728\u4E0A\u4F20,\u7A0D\u7B49\u4E00\u4E0B\u518D\u63D0\u4EA4", "warn");
1051
1116
  const text = ta.value.trim();
1052
1117
  if (!text) {
1053
1118
  closeComposer();
@@ -1255,6 +1320,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1255
1320
  viewport.addEventListener(
1256
1321
  "wheel",
1257
1322
  (e) => {
1323
+ if (e.target?.closest?.(".cs-pin-bubble, .cs-pin-input")) return;
1258
1324
  e.preventDefault();
1259
1325
  if (e.ctrlKey || e.metaKey) {
1260
1326
  zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY / 260));