contactsheet 0.1.4 → 0.1.6

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 通配上)。任何一侧绑不上都算冲突,照常顺延。
@@ -271,6 +272,19 @@ iframe 的画布永远是不透明的,所以组件画板必然有一层底。
271
272
  - 画板路由在生产环境不可访问:`next build` 的路由清单里它仍然在,但运行时一律 404(`notFound()` 守卫)。
272
273
  想连清单都不进,`next build` 前跑一次 `contactsheet clean`。
273
274
 
275
+ ## dev server 出问题时(画布会替你归因)
276
+
277
+ 画布长开时,被代理的 `next dev` 自己可能出问题。外壳每 20 秒探测一次它的 SSR 路径,
278
+ 连挂两次就在画布顶部拉**红色横幅**——看到横幅,坏的是 dev server,不是画布。两种实测指纹:
279
+
280
+ 1. **所有路由秒回 500、连请求日志都不打**:`.next` 开发缓存被硬杀打坏(next/font 会进入
281
+ 失败重试死循环,`.next` 疯狂膨胀)。重启救不回来,`rm -rf .next` 后再起即愈。
282
+ 2. **静态资源 200、SSR 报错、API 挂死、worker 数暴涨**:dev server 僵死,重启 `next dev` 即愈。
283
+
284
+ 外壳侧的配合:客户端中途放弃的请求(僵死期浏览器的重试洪水就是这样)会**向上游同步中断**——
285
+ 否则每次放弃都在 dev server 里留一个永远"渲染中"的挂起请求,把它压得更死(这同时也是外壳自身的
286
+ fd 泄漏源,实测 300 次中断泄 300 个 fd,已修)。
287
+
274
288
  ## 卸载
275
289
 
276
290
  ```bash
@@ -15,7 +15,7 @@
15
15
  return req(`${BASE}/api/state`);
16
16
  }
17
17
  function fetchRegistry() {
18
- return req(`${BASE}/registry`);
18
+ return req(`${BASE}/registry`, { signal: AbortSignal.timeout(15e3) });
19
19
  }
20
20
  function fetchAnnotations() {
21
21
  return req(`${BASE}/api/annotations`);
@@ -72,24 +72,26 @@
72
72
  }
73
73
  if (ev.type === "registry") on.registry(ev.entries ?? []);
74
74
  else if (ev.type === "annotations") on.annotations(ev.annotations ?? []);
75
+ else if (ev.type === "health") on.health(ev.ok, ev.detail);
75
76
  };
76
77
  es.addEventListener("registry", (e) => dispatch(e.data));
77
78
  es.addEventListener("annotations", (e) => dispatch(e.data));
79
+ es.addEventListener("health", (e) => dispatch(e.data));
78
80
  es.onmessage = (e) => dispatch(e.data);
79
81
  return es;
80
82
  }
81
83
 
82
84
  // src/canvas/dom.ts
83
85
  function h(tag, cls, text) {
84
- const el = document.createElement(tag);
85
- if (cls) el.className = cls;
86
- if (text !== void 0) el.textContent = text;
87
- return el;
86
+ const el2 = document.createElement(tag);
87
+ if (cls) el2.className = cls;
88
+ if (text !== void 0) el2.textContent = text;
89
+ return el2;
88
90
  }
89
91
  function qs(sel) {
90
- const el = document.querySelector(sel);
91
- if (!el) throw new Error(`canvas: \u7F3A\u5C11\u9AA8\u67B6\u5143\u7D20 ${sel}`);
92
- return el;
92
+ const el2 = document.querySelector(sel);
93
+ if (!el2) throw new Error(`canvas: \u7F3A\u5C11\u9AA8\u67B6\u5143\u7D20 ${sel}`);
94
+ return el2;
93
95
  }
94
96
  function clamp(v, lo, hi) {
95
97
  return v < lo ? lo : v > hi ? hi : v;
@@ -312,14 +314,14 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
312
314
  return host;
313
315
  }
314
316
  function toast(text, kind = "info") {
315
- const el = h("div", `cs-toast is-${kind}`, text);
316
- ensureHost().appendChild(el);
317
- requestAnimationFrame(() => el.classList.add("is-in"));
317
+ const el2 = h("div", `cs-toast is-${kind}`, text);
318
+ ensureHost().appendChild(el2);
319
+ requestAnimationFrame(() => el2.classList.add("is-in"));
318
320
  const life = kind === "error" ? 6e3 : kind === "warn" ? 4500 : 3e3;
319
321
  setTimeout(() => {
320
- el.classList.remove("is-in");
321
- el.addEventListener("transitionend", () => el.remove(), { once: true });
322
- setTimeout(() => el.remove(), 400);
322
+ el2.classList.remove("is-in");
323
+ el2.addEventListener("transitionend", () => el2.remove(), { once: true });
324
+ setTimeout(() => el2.remove(), 400);
323
325
  }, life);
324
326
  }
325
327
 
@@ -472,10 +474,10 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
472
474
  return null;
473
475
  }
474
476
  if (board.entry.kind !== "component") return stack[0] ?? null;
475
- for (const el of stack) {
476
- const t = el.tagName;
477
- if (t === "HTML" || t === "BODY" || el.hasAttribute("data-cs-artboard")) return null;
478
- if (paintsSomething(el, doc)) return el;
477
+ for (const el2 of stack) {
478
+ const t = el2.tagName;
479
+ if (t === "HTML" || t === "BODY" || el2.hasAttribute("data-cs-artboard")) return null;
480
+ if (paintsSomething(el2, doc)) return el2;
479
481
  }
480
482
  return null;
481
483
  }
@@ -499,15 +501,15 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
499
501
  const m = bg.match(/^rgba\([^)]*,\s*([\d.]+)\s*\)$/);
500
502
  return m ? parseFloat(m[1]) : 1;
501
503
  }
502
- function paintsSomething(el, doc) {
503
- if (VISIBLE_TAGS.has(el.tagName)) return true;
504
- if (el.closest("svg")) return true;
505
- for (const n of el.childNodes) {
504
+ function paintsSomething(el2, doc) {
505
+ if (VISIBLE_TAGS.has(el2.tagName)) return true;
506
+ if (el2.closest("svg")) return true;
507
+ for (const n of el2.childNodes) {
506
508
  if (n.nodeType === 3 && n.textContent && n.textContent.trim()) return true;
507
509
  }
508
510
  const win = doc.defaultView;
509
511
  if (!win) return false;
510
- const cs = win.getComputedStyle(el);
512
+ const cs = win.getComputedStyle(el2);
511
513
  if (cs.backgroundImage !== "none") return true;
512
514
  if (bgAlpha(cs.backgroundColor) > 0) return true;
513
515
  if (cs.boxShadow !== "none") return true;
@@ -515,19 +517,19 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
515
517
  if (cs.outlineStyle !== "none" && (parseFloat(cs.outlineWidth) || 0) > 0) return true;
516
518
  return false;
517
519
  }
518
- function screenRect(board, el) {
520
+ function screenRect(board, el2) {
519
521
  if (!board.iframe || !board.el.isConnected) return null;
520
522
  const fr = board.iframe.getBoundingClientRect();
521
523
  const s = liveScale(board, board.frameEl.getBoundingClientRect());
522
- const b = el.getBoundingClientRect();
524
+ const b = el2.getBoundingClientRect();
523
525
  return new DOMRect(fr.left + b.left * s, fr.top + b.top * s, b.width * s, b.height * s);
524
526
  }
525
527
  function liveScale(board, frame) {
526
528
  if (board.width > 0 && frame.width > 0) return frame.width / board.width;
527
529
  return state.scale || 1;
528
530
  }
529
- function relPos(el, clientX, clientY, board) {
530
- const r = screenRect(board, el);
531
+ function relPos(el2, clientX, clientY, board) {
532
+ const r = screenRect(board, el2);
531
533
  if (!r || r.width === 0 || r.height === 0) return { x: 0, y: 0 };
532
534
  const round = (v) => Math.round(clamp(v, 0, 1) * 1e3) / 1e3;
533
535
  return { x: round((clientX - r.left) / r.width), y: round((clientY - r.top) / r.height) };
@@ -539,31 +541,31 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
539
541
  return false;
540
542
  }
541
543
  }
542
- function idSelector(el) {
543
- const id = el.getAttribute("id");
544
+ function idSelector(el2) {
545
+ const id = el2.getAttribute("id");
544
546
  if (!id) return null;
545
547
  return `#${CSS.escape(id)}`;
546
548
  }
547
- function dataSelector(el) {
548
- for (const attr of Array.from(el.attributes)) {
549
+ function dataSelector(el2) {
550
+ for (const attr of Array.from(el2.attributes)) {
549
551
  if (!attr.name.startsWith("data-") || !attr.value) continue;
550
552
  const v = attr.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
551
553
  return `[${attr.name}="${v}"]`;
552
554
  }
553
555
  return null;
554
556
  }
555
- function nthPart(el) {
556
- const tag = el.localName;
557
- const parent = el.parentElement;
557
+ function nthPart(el2) {
558
+ const tag = el2.localName;
559
+ const parent = el2.parentElement;
558
560
  if (!parent) return tag;
559
561
  const sames = Array.from(parent.children).filter((c) => c.localName === tag);
560
562
  if (sames.length < 2) return tag;
561
- return `${tag}:nth-of-type(${sames.indexOf(el) + 1})`;
563
+ return `${tag}:nth-of-type(${sames.indexOf(el2) + 1})`;
562
564
  }
563
- function buildSelector(el) {
564
- const doc = el.ownerDocument;
565
+ function buildSelector(el2) {
566
+ const doc = el2.ownerDocument;
565
567
  const parts = [];
566
- let cur = el;
568
+ let cur = el2;
567
569
  while (cur && cur !== doc.documentElement && parts.length < 5) {
568
570
  const byId = idSelector(cur);
569
571
  if (byId && isUnique(doc, byId)) {
@@ -580,12 +582,12 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
580
582
  if (isUnique(doc, joined)) return joined;
581
583
  cur = cur.parentElement;
582
584
  }
583
- return parts.join(" > ") || el.localName;
585
+ return parts.join(" > ") || el2.localName;
584
586
  }
585
- function describe(el) {
586
- const probe = el.closest("[data-probe]")?.getAttribute("data-probe");
587
- const cls = typeof el.className === "string" ? el.className : "";
588
- return `<${el.localName}>` + (probe ? ` probe=${probe}` : "") + (cls ? ` .${cls.trim().split(/\s+/).slice(0, 3).join(".")}` : "");
587
+ function describe(el2) {
588
+ const probe = el2.closest("[data-probe]")?.getAttribute("data-probe");
589
+ const cls = typeof el2.className === "string" ? el2.className : "";
590
+ return `<${el2.localName}>` + (probe ? ` probe=${probe}` : "") + (cls ? ` .${cls.trim().split(/\s+/).slice(0, 3).join(".")}` : "");
589
591
  }
590
592
  var TAG_GAP = 1;
591
593
  function placeTag(tag, box, frame, st, label) {
@@ -606,8 +608,8 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
606
608
  tag.style.left = `${x - box.left - 1}px`;
607
609
  tag.style.top = `${y - box.top - 1}px`;
608
610
  }
609
- function paint(box, board, el, label) {
610
- const r = screenRect(board, el);
611
+ function paint(box, board, el2, label) {
612
+ const r = screenRect(board, el2);
611
613
  if (!r || r.width + r.height === 0) {
612
614
  const off = box.classList.contains("is-on");
613
615
  box.classList.remove("is-on");
@@ -654,8 +656,8 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
654
656
  }
655
657
  }
656
658
  if (selectRef) {
657
- const el = resolveSelected();
658
- if (el) changed = paint(selectBox, selectRef.board, el, selectRef.selector) || changed;
659
+ const el2 = resolveSelected();
660
+ if (el2) changed = paint(selectBox, selectRef.board, el2, selectRef.selector) || changed;
659
661
  else if (selectBox.classList.contains("is-on")) {
660
662
  selectBox.classList.remove("is-on");
661
663
  changed = true;
@@ -676,9 +678,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
676
678
  stillFrames = 0;
677
679
  if (!followRaf && (hoverRef || selectRef)) followRaf = requestAnimationFrame(follow);
678
680
  }
679
- function showHover(board, el) {
680
- hoverRef = { board, el };
681
- paint(hoverBox, board, el, el.localName);
681
+ function showHover(board, el2) {
682
+ hoverRef = { board, el: el2 };
683
+ paint(hoverBox, board, el2, el2.localName);
682
684
  wake();
683
685
  }
684
686
  function hideHover() {
@@ -721,14 +723,14 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
721
723
  let top = 8;
722
724
  let orphan = true;
723
725
  if (doc && ann.anchor) {
724
- let el = null;
726
+ let el2 = null;
725
727
  try {
726
- el = doc.querySelector(ann.anchor.selector);
728
+ el2 = doc.querySelector(ann.anchor.selector);
727
729
  } catch {
728
- el = null;
730
+ el2 = null;
729
731
  }
730
- if (el) {
731
- const r = el.getBoundingClientRect();
732
+ if (el2) {
733
+ const r = el2.getBoundingClientRect();
732
734
  left = r.left + ann.anchor.x * r.width;
733
735
  top = r.top + ann.anchor.y * r.height;
734
736
  orphan = false;
@@ -1013,10 +1015,10 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1013
1015
  if (i >= 0) state.annotations[i] = ann;
1014
1016
  else state.annotations.push(ann);
1015
1017
  }
1016
- function openComposer(board, el, clientX, clientY) {
1018
+ function openComposer(board, el2, clientX, clientY) {
1017
1019
  closeComposer();
1018
- const selector = buildSelector(el);
1019
- const rel = relPos(el, clientX, clientY, board);
1020
+ const selector = buildSelector(el2);
1021
+ const rel = relPos(el2, clientX, clientY, board);
1020
1022
  const box = h("div", "cs-pin-input");
1021
1023
  box.style.left = `${Math.min(clientX + 10, window.innerWidth - 250)}px`;
1022
1024
  box.style.top = `${Math.min(clientY + 10, window.innerHeight - 170)}px`;
@@ -1242,9 +1244,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1242
1244
  applyView();
1243
1245
  }
1244
1246
  function isBlank(target) {
1245
- const el = target;
1246
- if (!el) return false;
1247
- return !el.closest(".cs-board");
1247
+ const el2 = target;
1248
+ if (!el2) return false;
1249
+ return !el2.closest(".cs-board");
1248
1250
  }
1249
1251
  function initView() {
1250
1252
  viewport = qs("#cs-viewport");
@@ -1378,13 +1380,13 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1378
1380
  setHidden(ids, hide);
1379
1381
  }
1380
1382
  function focusedKey() {
1381
- const el = document.activeElement;
1382
- return el && listEl.contains(el) ? el.dataset.fk ?? null : null;
1383
+ const el2 = document.activeElement;
1384
+ return el2 && listEl.contains(el2) ? el2.dataset.fk ?? null : null;
1383
1385
  }
1384
1386
  function restoreFocus(key) {
1385
1387
  if (!key) return;
1386
- const el = listEl.querySelector(`[data-fk="${CSS.escape(key)}"]`);
1387
- el?.focus({ preventScroll: true });
1388
+ const el2 = listEl.querySelector(`[data-fk="${CSS.escape(key)}"]`);
1389
+ el2?.focus({ preventScroll: true });
1388
1390
  }
1389
1391
  function renderSidebar() {
1390
1392
  if (!listEl) return;
@@ -1425,8 +1427,8 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1425
1427
  restoreFocus(keepFocus);
1426
1428
  }
1427
1429
  function group(key, label, ids, headCls) {
1428
- const el = h("div", "cs-sb-group");
1429
- if (headCls === "cs-sb-file") el.classList.add("is-file");
1430
+ const el2 = h("div", "cs-sb-group");
1431
+ if (headCls === "cs-sb-file") el2.classList.add("is-file");
1430
1432
  const headEl = h("div", headCls);
1431
1433
  const body = h("div", "cs-sb-items");
1432
1434
  body.id = `cs-sb-g${++uid}`;
@@ -1461,12 +1463,12 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1461
1463
  renderSidebar();
1462
1464
  });
1463
1465
  headEl.append(caret, bulk);
1464
- el.append(headEl, body);
1465
- return { el, body };
1466
+ el2.append(headEl, body);
1467
+ return { el: el2, body };
1466
1468
  }
1467
1469
  function row(id, name, sub, kind) {
1468
- const el = h("div", "cs-sb-row");
1469
- el.dataset.id = id;
1470
+ const el2 = h("div", "cs-sb-row");
1471
+ el2.dataset.id = id;
1470
1472
  const go = h("button", "cs-sb-go");
1471
1473
  go.type = "button";
1472
1474
  go.dataset.fk = `go:${id}`;
@@ -1491,9 +1493,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1491
1493
  toggleHidden(id);
1492
1494
  renderSidebar();
1493
1495
  });
1494
- el.append(go, eye);
1495
- if (isHidden(id)) el.classList.add("is-hidden-board");
1496
- return el;
1496
+ el2.append(go, eye);
1497
+ if (isHidden(id)) el2.classList.add("is-hidden-board");
1498
+ return el2;
1497
1499
  }
1498
1500
  function syncActive() {
1499
1501
  if (!listEl) return;
@@ -1599,9 +1601,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1599
1601
  return titleEl;
1600
1602
  }
1601
1603
  function createBoard(entry) {
1602
- const el = h("div", "cs-board");
1603
- el.dataset.id = entry.id;
1604
- el.dataset.kind = entry.kind;
1604
+ const el2 = h("div", "cs-board");
1605
+ el2.dataset.id = entry.id;
1606
+ el2.dataset.kind = entry.kind;
1605
1607
  const titleEl = h("div", "cs-board-title");
1606
1608
  const nameEl = h("span", "cs-name", entry.exportName);
1607
1609
  const badgeEl = h("span", "cs-badge", entry.kind);
@@ -1613,11 +1615,11 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1613
1615
  const overlayEl = h("div", "cs-ovl");
1614
1616
  const pinLayerEl = h("div", "cs-pins");
1615
1617
  frameEl.append(placeholderEl, overlayEl);
1616
- el.append(titleEl, frameEl, pinLayerEl);
1617
- world2.appendChild(el);
1618
+ el2.append(titleEl, frameEl, pinLayerEl);
1619
+ world2.appendChild(el2);
1618
1620
  const board = {
1619
1621
  entry,
1620
- el,
1622
+ el: el2,
1621
1623
  titleEl,
1622
1624
  nameEl,
1623
1625
  badgeEl,
@@ -1640,10 +1642,10 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1640
1642
  }
1641
1643
  attachResizeHandles(board);
1642
1644
  attachBoardDrag(board);
1643
- el.hidden = isHidden(entry.id);
1645
+ el2.hidden = isHidden(entry.id);
1644
1646
  applySize(board);
1645
1647
  updateBoard(board, entry);
1646
- io.observe(el);
1648
+ io.observe(el2);
1647
1649
  return board;
1648
1650
  }
1649
1651
  var WIDTH_PRESETS = [390, 768, 1024, 1280, 1440];
@@ -2386,9 +2388,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2386
2388
  // src/canvas/modes.ts
2387
2389
  var viewport2;
2388
2390
  function boardOf(target) {
2389
- const el = target?.closest?.(".cs-board");
2390
- if (!el?.dataset.id) return null;
2391
- return state.boards.get(el.dataset.id) ?? null;
2391
+ const el2 = target?.closest?.(".cs-board");
2392
+ if (!el2?.dataset.id) return null;
2393
+ return state.boards.get(el2.dataset.id) ?? null;
2392
2394
  }
2393
2395
  function setActive(id) {
2394
2396
  if (state.activeId === id) return;
@@ -2455,11 +2457,11 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2455
2457
  setProbe("\u79FB\u5230\u753B\u677F\u4E0A\u53CD\u67E5\u5143\u7D20");
2456
2458
  }
2457
2459
  function isEditable(target) {
2458
- const el = target;
2459
- if (!el) return false;
2460
- const tag = el.tagName;
2461
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable === true) return true;
2462
- return el.closest("#cs-sidebar, #cs-args, .cs-picker, #cs-topbar") !== null;
2460
+ const el2 = target;
2461
+ if (!el2) return false;
2462
+ const tag = el2.tagName;
2463
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el2.isContentEditable === true) return true;
2464
+ return el2.closest("#cs-sidebar, #cs-args, .cs-picker, #cs-topbar") !== null;
2463
2465
  }
2464
2466
  function onKeyDown(e) {
2465
2467
  if (e.key === "Escape") {
@@ -2550,13 +2552,13 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2550
2552
  }
2551
2553
  if (state.mode !== "browse") return;
2552
2554
  setActive(board.entry.id);
2553
- const el = elementAt(board, e.clientX, e.clientY);
2554
- if (!el) {
2555
+ const el2 = elementAt(board, e.clientX, e.clientY);
2556
+ if (!el2) {
2555
2557
  hideHover();
2556
2558
  return;
2557
2559
  }
2558
- showHover(board, el);
2559
- setProbe(`${board.entry.exportName} ${describe(el)}`);
2560
+ showHover(board, el2);
2561
+ setProbe(`${board.entry.exportName} ${describe(el2)}`);
2560
2562
  }
2561
2563
  function onClick(e) {
2562
2564
  const target = e.target;
@@ -2578,15 +2580,15 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2578
2580
  return;
2579
2581
  }
2580
2582
  setActive(board.entry.id);
2581
- const el = elementAt(board, e.clientX, e.clientY);
2582
- if (!el) return;
2583
+ const el2 = elementAt(board, e.clientX, e.clientY);
2584
+ if (!el2) return;
2583
2585
  if (state.pinPending) {
2584
- openComposer(board, el, e.clientX, e.clientY);
2586
+ openComposer(board, el2, e.clientX, e.clientY);
2585
2587
  return;
2586
2588
  }
2587
2589
  if (state.mode !== "browse") return;
2588
- const selector = buildSelector(el);
2589
- const rel = relPos(el, e.clientX, e.clientY, board);
2590
+ const selector = buildSelector(el2);
2591
+ const rel = relPos(el2, e.clientX, e.clientY, board);
2590
2592
  showSelection(board, selector);
2591
2593
  state.selection = { artboardId: board.entry.id, selector, x: rel.x, y: rel.y, ts: Date.now() };
2592
2594
  setProbe(`\u5DF2\u9009\u4E2D ${board.entry.exportName} \u2192 ${selector}`);
@@ -2622,6 +2624,32 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2622
2624
  syncHud();
2623
2625
  }
2624
2626
 
2627
+ // src/canvas/health-banner.ts
2628
+ var el = null;
2629
+ var lastOkAt;
2630
+ function setLastOk(ts) {
2631
+ lastOkAt = ts;
2632
+ }
2633
+ function showUnhealthy(detail) {
2634
+ hideUnhealthy();
2635
+ const at = lastOkAt ? new Date(lastOkAt).toLocaleTimeString("zh-CN", { hour12: false }) : null;
2636
+ el = h("div", "cs-health");
2637
+ el.setAttribute("role", "alert");
2638
+ el.appendChild(h("b", void 0, "\u4F60\u7684 dev server \u6CA1\u54CD\u5E94\u4E86"));
2639
+ el.appendChild(
2640
+ h(
2641
+ "span",
2642
+ void 0,
2643
+ `${detail ?? "SSR \u63A2\u6D4B\u5931\u8D25"}${at ? ` \xB7 \u6700\u540E\u6B63\u5E38 ${at}` : ""} \u2014\u2014 \u753B\u5E03\u6CA1\u574F,\u753B\u677F\u7834\u56FE\u662F\u5B83\u6E32\u67D3\u4E0D\u51FA\u6765\u3002\u901A\u5E38\u91CD\u542F next dev \u5373\u6108\u3002`
2644
+ )
2645
+ );
2646
+ document.body.appendChild(el);
2647
+ }
2648
+ function hideUnhealthy() {
2649
+ el?.remove();
2650
+ el = null;
2651
+ }
2652
+
2625
2653
  // src/canvas/app.ts
2626
2654
  function showBootError(err) {
2627
2655
  const boot2 = qs("#cs-boot");
@@ -2678,9 +2706,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2678
2706
  }
2679
2707
  function showTarget() {
2680
2708
  const info = state.info;
2681
- const el = qs("#cs-target");
2682
- el.textContent = info ? `${info.target} \xB7 ${info.designDir}` : "";
2683
- el.title = info ? `v${info.version} \xB7 ${info.projectRoot}` : "";
2709
+ const el2 = qs("#cs-target");
2710
+ el2.textContent = info ? `${info.target} \xB7 ${info.designDir}` : "";
2711
+ el2.title = info ? `v${info.version} \xB7 ${info.projectRoot}` : "";
2684
2712
  }
2685
2713
  function onRegistryEvent(entries) {
2686
2714
  void reloadUntilMatches(entries.map((e) => e.id));
@@ -2724,16 +2752,31 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2724
2752
  }
2725
2753
  showTarget();
2726
2754
  restoreView();
2727
- await tryLoadRegistry();
2728
2755
  try {
2729
- state.annotations = await fetchAnnotations();
2730
- renderPins();
2756
+ const hres = await fetch("/__cs/api/health");
2757
+ const hstate = await hres.json();
2758
+ setLastOk(hstate.lastOkAt);
2759
+ if (!hstate.ok) showUnhealthy(hstate.detail);
2731
2760
  } catch {
2732
2761
  }
2733
2762
  connectEvents({
2734
2763
  registry: onRegistryEvent,
2735
- annotations: onAnnotations
2764
+ annotations: onAnnotations,
2765
+ health: (ok, detail) => {
2766
+ if (ok) {
2767
+ setLastOk(Date.now());
2768
+ hideUnhealthy();
2769
+ } else {
2770
+ showUnhealthy(detail);
2771
+ }
2772
+ }
2736
2773
  });
2774
+ await tryLoadRegistry();
2775
+ try {
2776
+ state.annotations = await fetchAnnotations();
2777
+ renderPins();
2778
+ } catch {
2779
+ }
2737
2780
  }
2738
2781
  void boot();
2739
2782
  })();