contactsheet 0.1.5 → 0.1.7

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
@@ -57,10 +57,11 @@ flags:`--port`(外壳端口,默认 5199)、`--target`(你的 dev serve
57
57
  **端口被占了怎么办**(都不用你手动排查):
58
58
 
59
59
  - `init` 会读你 package.json 的 dev script——`next dev -p 3100` 这种写法会被认出来,target 自动写成 3100;
60
- - 5199 被**别的进程**占着:自动顺延到下一个空闲端口(最多 +20)并醒目提示。注意顺延 = 换浏览器源,
61
- 画板位置这些本机记忆按源隔离,`.mcp.json`/hook 也仍指向原端口——想固定就改 config 的 port 再跑一次 `init`;
62
- - 5199 上跑的是**本项目的另一个 contactsheet**(最常见:忘了已经起过):直接提示地址退出,不再起第二个;
63
- - 显式传了 `--port` 时不猜你的意图:被占就报错,附排查命令;
60
+ - 5199 被**别的进程**占着:**硬失败**,报错里带占用者 PID 和两条出路(kill 它,或改 config.port 重跑 init)。
61
+ 不自动顺延——`.mcp.json`/hook/config 全指着这个端口,静默换端口的话浏览器里看着一切正常,
62
+ agent 侧接线却全指向别的进程,谁都收不到错误(实测这样吞掉过一次升级);
63
+ - 5199 上跑的是**本项目的另一个 contactsheet**:版本相同 → 提示地址直接退出,不起第二个;
64
+ **版本不同 → 报错退出**,提示先 kill 旧实例——静默复用旧的等于升级没生效;
64
65
  - **判定「被占」看的是双栈**:外壳同时绑 `127.0.0.1` 和 `::1`(浏览器解析 localhost 普遍优先 IPv6——
65
66
  只绑 v4 的话,v6 侧被别的进程占着时 curl 一切正常、浏览器却拿到别人的空响应,Docker 的端口转发就常年蹲在
66
67
  IPv6 通配上)。任何一侧绑不上都算冲突,照常顺延。
@@ -151,7 +152,8 @@ open(橙) ──标记完成──▶ resolved(绿·待核验) ──核验通
151
152
  这份文件就是历史记录,日后沉淀 skill / 复盘"当初都提过什么、怎么改的"的原料。
152
153
  - 「删除」只留给误钉的 pin,会真的从历史里抹掉。
153
154
  - `Cmd/Ctrl + V` → 贴图,存进 `design/.canvas/refs/`。**在批注输入框或气泡编辑态里粘贴 = 挂到这条批注**
154
- (缩略图跟着 pin 走,推给 Claude 的上下文里带上它的路径);在别处粘贴才进右下角的全局坞
155
+ (缩略图跟着 pin 走,推给 Claude 的上下文里带上它的路径);在别处粘贴才进右下角的全局坞。
156
+ 气泡里的图显示为「图片 n」占位符 + 缩略图,点开大图;**悬停缩略图出 ✕ 可从批注移除**(文件保留在 refs/)
155
157
  - `Ctrl + 滚轮` / 捏合 → 以光标为锚点缩放(0.05–2 倍);空白处拖拽或双指滚动 → 平移
156
158
  - `1` 全景(一屏看完整面墙)· `2` 聚焦当前画板 · `0` 回到 100% · `+` / `-` 步进缩放
157
159
  - 点一个 pin 选中它,然后 `Enter` 编辑(编辑中再按 `Enter` 保存,`Shift+Enter` 换行)、
@@ -383,7 +383,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
383
383
  function refSrc(path) {
384
384
  return localSrc.get(path) ?? refUrl(path);
385
385
  }
386
- function refThumb(path) {
386
+ function refThumb(path, opts) {
387
387
  const card = h("div", "cs-ref-thumb");
388
388
  card.title = path;
389
389
  const img = h("img");
@@ -395,11 +395,21 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
395
395
  img.alt = path;
396
396
  img.src = refSrc(path);
397
397
  if (img.complete && img.naturalWidth === 0) broken();
398
- card.append(img, h("span", "cs-ref-name", fileOf(path)));
398
+ card.append(img, h("span", "cs-ref-name", opts?.index ? `\u56FE\u7247 ${opts.index}` : fileOf(path)));
399
399
  card.addEventListener("click", (e) => {
400
400
  e.stopPropagation();
401
401
  openLightbox(refSrc(path), path);
402
402
  });
403
+ if (opts?.onRemove) {
404
+ const rm = h("button", "cs-ref-rm", "\u2715");
405
+ rm.title = "\u4ECE\u8FD9\u6761\u6279\u6CE8\u79FB\u9664(\u6587\u4EF6\u4FDD\u7559\u5728 refs/)";
406
+ rm.setAttribute("aria-label", rm.title);
407
+ rm.addEventListener("click", (e) => {
408
+ e.stopPropagation();
409
+ opts.onRemove?.();
410
+ });
411
+ card.appendChild(rm);
412
+ }
403
413
  return card;
404
414
  }
405
415
  function openLightbox(src, caption) {
@@ -806,16 +816,25 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
806
816
  }, 250);
807
817
  });
808
818
  }
809
- function refStrip(paths) {
819
+ function refStrip(paths, onRemove) {
810
820
  const strip = h("div", "cs-ref-strip");
811
- for (const p of paths) strip.appendChild(refThumb(p));
821
+ paths.forEach(
822
+ (p, i) => strip.appendChild(refThumb(p, { index: i + 1, onRemove: onRemove ? () => onRemove(p) : void 0 }))
823
+ );
812
824
  return strip;
813
825
  }
814
826
  function bubble(ann) {
815
827
  const box = h("div", "cs-pin-bubble");
816
828
  const textEl = h("div", "cs-pin-text", ann.text);
817
829
  box.appendChild(textEl);
818
- if (ann.refs?.length) box.appendChild(refStrip(ann.refs));
830
+ if (ann.refs?.length) {
831
+ box.appendChild(
832
+ refStrip(ann.refs, (path) => {
833
+ const next = (ann.refs ?? []).filter((r) => r !== path);
834
+ void transition(ann.id, { refs: next }, "\u53C2\u8003\u56FE\u5DF2\u79FB\u9664(\u6587\u4EF6\u4FDD\u7559\u5728 refs/ \u91CC)");
835
+ })
836
+ );
837
+ }
819
838
  const meta = h("div", "cs-pin-meta");
820
839
  meta.appendChild(h("span", void 0, ann.status === "open" ? "open" : "\u5F85\u6838\u9A8C"));
821
840
  const act = (label, cls, fn) => {
@@ -861,15 +880,24 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
861
880
  const ta = h("textarea", "cs-pin-edit");
862
881
  ta.value = ann.text;
863
882
  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"));
883
+ let strip = box.querySelector(".cs-ref-strip");
884
+ const redrawStrip = () => {
885
+ const next = refStrip(refs, (path) => {
886
+ refs.splice(refs.indexOf(path), 1);
887
+ redrawStrip();
888
+ });
889
+ if (strip?.isConnected) strip.replaceWith(next);
890
+ else box.insertBefore(next, box.querySelector(".cs-pin-meta"));
891
+ strip = next;
892
+ };
893
+ redrawStrip();
866
894
  setPasteTarget({
867
895
  el: box,
868
896
  onFile: (file) => {
869
897
  void uploadRef(file).then((path) => {
870
898
  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");
899
+ redrawStrip();
900
+ toast(`\u53C2\u8003\u56FE\u5DF2\u4E0A\u4F20 \u2014\u2014 \u70B9\u300C\u4FDD\u5B58\u300D\u540E\u624D\u6302\u5230\u8FD9\u6761\u6279\u6CE8`, "ok");
873
901
  }).catch((err) => toast(`\u53C2\u8003\u56FE\u4E0A\u4F20\u5931\u8D25:${String(err)}`, "error"));
874
902
  }
875
903
  });
@@ -890,7 +918,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
890
918
  e.stopPropagation();
891
919
  const text = ta.value.trim();
892
920
  const textChanged = !!text && text !== ann.text;
893
- const refsChanged = refs.length !== (ann.refs?.length ?? 0);
921
+ const refsChanged = JSON.stringify(refs) !== JSON.stringify(ann.refs ?? []);
894
922
  if (!textChanged && !refsChanged) return renderPins();
895
923
  const patch = {};
896
924
  if (textChanged) patch.text = text;
@@ -1255,6 +1283,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1255
1283
  viewport.addEventListener(
1256
1284
  "wheel",
1257
1285
  (e) => {
1286
+ if (e.target?.closest?.(".cs-pin-bubble, .cs-pin-input")) return;
1258
1287
  e.preventDefault();
1259
1288
  if (e.ctrlKey || e.metaKey) {
1260
1289
  zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY / 260));