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 +4 -1
- package/dist/canvas/app.js +88 -22
- package/dist/canvas/app.js.map +2 -2
- package/dist/canvas/style-pins.css +54 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +2 -2
- package/package.json +1 -1
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` 换行)、
|
package/dist/canvas/app.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
|
|
865
|
-
|
|
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
|
|
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));
|