contactsheet 0.1.3 → 0.1.5

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.
@@ -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");
@@ -1296,220 +1298,210 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1296
1298
  return viewport.classList.contains("is-panning");
1297
1299
  }
1298
1300
 
1299
- // src/canvas/modes.ts
1300
- var viewport2;
1301
- function boardOf(target) {
1302
- const el = target?.closest?.(".cs-board");
1303
- if (!el?.dataset.id) return null;
1304
- return state.boards.get(el.dataset.id) ?? null;
1301
+ // src/canvas/sidebar.ts
1302
+ var root;
1303
+ var listEl;
1304
+ var filter = "";
1305
+ var searchCollapsed = null;
1306
+ var uid = 0;
1307
+ function initSidebar() {
1308
+ root = qs("#cs-sidebar");
1309
+ root.textContent = "";
1310
+ const head = h("div", "cs-sb-head");
1311
+ const search = document.createElement("input");
1312
+ search.className = "cs-sb-search";
1313
+ search.type = "search";
1314
+ search.placeholder = "\u641C\u7D22\u753B\u677F\u2026";
1315
+ search.addEventListener("input", () => {
1316
+ filter = search.value.trim().toLowerCase();
1317
+ searchCollapsed = filter ? /* @__PURE__ */ new Set() : null;
1318
+ renderSidebar();
1319
+ });
1320
+ search.addEventListener("keydown", (e) => {
1321
+ e.stopPropagation();
1322
+ if (e.key === "Escape") {
1323
+ search.value = "";
1324
+ filter = "";
1325
+ searchCollapsed = null;
1326
+ renderSidebar();
1327
+ search.blur();
1328
+ }
1329
+ });
1330
+ head.append(h("span", "cs-sb-title", "\u753B\u677F"), search);
1331
+ listEl = h("div", "cs-sb-list");
1332
+ listEl.addEventListener("keydown", (e) => {
1333
+ if ((e.key === "Enter" || e.key === " ") && e.target.closest("button")) {
1334
+ e.stopPropagation();
1335
+ }
1336
+ });
1337
+ root.append(head, listEl);
1338
+ const toggle = qs("#cs-sb-toggle");
1339
+ toggle.addEventListener("click", () => {
1340
+ const closed = document.body.dataset.sidebar === "off";
1341
+ document.body.dataset.sidebar = closed ? "on" : "off";
1342
+ toggle.title = closed ? "\u6536\u8D77\u5217\u8868" : "\u5C55\u5F00\u5217\u8868";
1343
+ });
1344
+ renderSidebar();
1305
1345
  }
1306
- function setActive(id) {
1307
- if (state.activeId === id) return;
1308
- state.activeId = id;
1309
- for (const b of state.boards.values()) b.el.classList.toggle("is-active", b.entry.id === id);
1346
+ function match(text) {
1347
+ return !filter || text.toLowerCase().includes(filter);
1310
1348
  }
1311
- function setMode(mode, boardId) {
1312
- if (mode !== "browse" && boardId) setActive(boardId);
1313
- if (mode === "review" && !state.activeId) return;
1314
- const from = state.mode;
1315
- state.mode = mode;
1316
- hideHover();
1317
- if (mode !== "browse") clearSelection();
1318
- syncHud();
1319
- if (mode === "review" && from !== "review") {
1320
- state.modeBeforeReview = from;
1321
- state.viewBeforeReview = { scale: state.scale, tx: state.tx, ty: state.ty };
1322
- closeArgsPanel();
1323
- fitActiveBoard();
1324
- } else if (mode !== "review" && from === "review" && state.viewBeforeReview) {
1325
- const v = state.viewBeforeReview;
1326
- state.viewBeforeReview = null;
1327
- setView(v.tx, v.ty, v.scale);
1349
+ function collapseKey() {
1350
+ return `cs-collapsed:${state.info?.projectRoot ?? "unknown"}`;
1351
+ }
1352
+ function collapsedSet() {
1353
+ try {
1354
+ return new Set(JSON.parse(localStorage.getItem(collapseKey()) ?? "[]"));
1355
+ } catch {
1356
+ return /* @__PURE__ */ new Set();
1328
1357
  }
1329
- setProbe(
1330
- mode === "interact" ? "\u4EA4\u4E92\u6A21\u5F0F:\u76F4\u63A5\u64CD\u4F5C\u8FD9\u5757\u753B\u677F,Esc \u56DE\u6D4F\u89C8" : mode === "review" ? "\u8D70\u67E5\u6A21\u5F0F:Esc \u9000\u51FA" : "\u79FB\u5230\u753B\u677F\u4E0A\u53CD\u67E5\u5143\u7D20"
1331
- );
1332
1358
  }
1333
- function fitActiveBoard() {
1334
- const board = state.activeId ? state.boards.get(state.activeId) : null;
1335
- if (!board) return;
1336
- const r = board.frameEl.getBoundingClientRect();
1337
- const worldX = (r.left - state.tx) / state.scale;
1338
- const worldY = (r.top - state.ty) / state.scale;
1339
- const vw = window.innerWidth;
1340
- const vh = window.innerHeight;
1341
- const s = clamp(Math.min(vw / board.width, vh / board.height), MIN_SCALE, MAX_SCALE);
1342
- setView((vw - board.width * s) / 2 - worldX * s, (vh - board.height * s) / 2 - worldY * s, s);
1359
+ function isCollapsed(key) {
1360
+ return searchCollapsed ? searchCollapsed.has(key) : collapsedSet().has(key);
1343
1361
  }
1344
- function escape() {
1345
- if (isComposing()) {
1346
- closeComposer();
1347
- exitPinMode();
1348
- return;
1349
- }
1350
- if (state.pinPending) {
1351
- exitPinMode();
1352
- return;
1353
- }
1354
- if (selectedPin()) {
1355
- selectPin(null);
1356
- return;
1357
- }
1358
- if (state.mode === "review") {
1359
- setMode(state.modeBeforeReview === "review" ? "browse" : state.modeBeforeReview);
1362
+ function toggleCollapsed(key) {
1363
+ if (searchCollapsed) {
1364
+ if (!searchCollapsed.delete(key)) searchCollapsed.add(key);
1360
1365
  return;
1361
1366
  }
1362
- if (state.mode === "interact") {
1363
- setMode("browse");
1364
- return;
1367
+ const s = collapsedSet();
1368
+ if (s.has(key)) s.delete(key);
1369
+ else s.add(key);
1370
+ try {
1371
+ localStorage.setItem(collapseKey(), JSON.stringify([...s]));
1372
+ } catch {
1365
1373
  }
1366
- clearSelection();
1367
- closeArgsPanel();
1368
- setProbe("\u79FB\u5230\u753B\u677F\u4E0A\u53CD\u67E5\u5143\u7D20");
1369
1374
  }
1370
- function isEditable(target) {
1371
- const el = target;
1372
- if (!el) return false;
1373
- const tag = el.tagName;
1374
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable === true) return true;
1375
- return el.closest("#cs-sidebar, #cs-args, .cs-picker, #cs-topbar") !== null;
1375
+ function bulkMode(ids) {
1376
+ const shown = ids.filter((id) => !isHidden(id)).length;
1377
+ return { mode: shown === ids.length ? "all" : shown === 0 ? "none" : "some", shown };
1376
1378
  }
1377
- function onKeyDown(e) {
1378
- if (e.key === "Escape") {
1379
- escape();
1380
- return;
1381
- }
1382
- if (isEditable(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;
1383
- if (e.key === "Enter") {
1384
- if (selectedPin()) {
1385
- e.preventDefault();
1386
- editSelectedPin();
1387
- return;
1388
- }
1389
- if (state.mode !== "review" && state.activeId) {
1390
- e.preventDefault();
1391
- setMode("review", state.activeId);
1392
- }
1393
- return;
1394
- }
1395
- if (e.key === "Backspace" || e.key === "Delete") {
1396
- if (selectedPin()) {
1397
- e.preventDefault();
1398
- deleteSelectedPin();
1399
- }
1400
- return;
1401
- }
1402
- if (e.key === "c" || e.key === "C") {
1403
- if (state.mode === "browse" && !state.pinPending) {
1404
- e.preventDefault();
1405
- enterPinMode();
1406
- }
1407
- return;
1408
- }
1409
- if (e.key === "y" || e.key === "Y") {
1410
- e.preventDefault();
1411
- void copyContext();
1412
- return;
1413
- }
1414
- if (e.key === "p") {
1415
- e.preventDefault();
1416
- void pushToClaudeCode();
1417
- return;
1418
- }
1419
- if (e.key === "1") {
1420
- e.preventDefault();
1421
- fitAll();
1422
- return;
1423
- }
1424
- if (e.key === "2") {
1425
- e.preventDefault();
1426
- fitBoard();
1427
- return;
1428
- }
1429
- if (e.key === "0") {
1430
- e.preventDefault();
1431
- zoomReset();
1432
- return;
1433
- }
1434
- if (e.key === "=" || e.key === "+") {
1435
- e.preventDefault();
1436
- zoomStep(1.25);
1437
- return;
1438
- }
1439
- if (e.key === "-" || e.key === "_") {
1440
- e.preventDefault();
1441
- zoomStep(0.8);
1442
- }
1379
+ function setGroupHidden(ids, hide) {
1380
+ setHidden(ids, hide);
1443
1381
  }
1444
- function attachFrameKeys(doc) {
1445
- doc.addEventListener("keydown", onKeyDown);
1382
+ function focusedKey() {
1383
+ const el2 = document.activeElement;
1384
+ return el2 && listEl.contains(el2) ? el2.dataset.fk ?? null : null;
1446
1385
  }
1447
- function onMouseMove(e) {
1448
- if (isPanning()) return;
1449
- const overlay = e.target?.closest?.(".cs-ovl");
1450
- const board = overlay ? boardOf(e.target) : null;
1451
- if (!board) {
1452
- hideHover();
1453
- return;
1386
+ function restoreFocus(key) {
1387
+ if (!key) return;
1388
+ const el2 = listEl.querySelector(`[data-fk="${CSS.escape(key)}"]`);
1389
+ el2?.focus({ preventScroll: true });
1390
+ }
1391
+ function renderSidebar() {
1392
+ if (!listEl) return;
1393
+ const keepFocus = focusedKey();
1394
+ listEl.textContent = "";
1395
+ uid = 0;
1396
+ const screens = state.entries.filter((e) => e.kind === "screen");
1397
+ const comps = state.entries.filter((e) => e.kind !== "screen");
1398
+ const visScreens = screens.filter((e) => match(e.exportName) || match(e.url ?? ""));
1399
+ if (visScreens.length) {
1400
+ const g = group("sec:screen", "\u9875\u9762", visScreens.map((e) => e.id), "cs-sb-section");
1401
+ for (const e of visScreens) g.body.appendChild(row(e.id, e.exportName, e.url ?? "", "screen"));
1402
+ listEl.appendChild(g.el);
1454
1403
  }
1455
- if (state.mode !== "browse") return;
1456
- setActive(board.entry.id);
1457
- const el = elementAt(board, e.clientX, e.clientY);
1458
- if (!el) {
1459
- hideHover();
1460
- return;
1404
+ const byFile = /* @__PURE__ */ new Map();
1405
+ for (const e of comps) {
1406
+ if (!(match(e.exportName) || match(groupLabel(e.file)))) continue;
1407
+ const l = byFile.get(e.file);
1408
+ if (l) l.push(e);
1409
+ else byFile.set(e.file, [e]);
1461
1410
  }
1462
- showHover(board, el);
1463
- setProbe(`${board.entry.exportName} ${describe(el)}`);
1464
- }
1465
- function onClick(e) {
1466
- const target = e.target;
1467
- const title = target?.closest?.(".cs-board-title");
1468
- if (title) {
1469
- const board2 = boardOf(target);
1470
- if (!board2) return;
1471
- setActive(board2.entry.id);
1472
- if (isArgsOpen(board2.entry.id)) closeArgsPanel();
1473
- else openArgsPanel(board2);
1474
- return;
1411
+ if (byFile.size) {
1412
+ const files = [...byFile.keys()].sort();
1413
+ const allIds = files.flatMap((f) => (byFile.get(f) ?? []).map((e) => e.id));
1414
+ const g = group("sec:component", "\u7EC4\u4EF6", allIds, "cs-sb-section");
1415
+ for (const file of files) {
1416
+ const list = byFile.get(file) ?? [];
1417
+ const fg = group(`file:${file}`, groupLabel(file), list.map((e) => e.id), "cs-sb-file");
1418
+ for (const e of list) fg.body.appendChild(row(e.id, e.exportName, "", "component"));
1419
+ g.body.appendChild(fg.el);
1420
+ }
1421
+ listEl.appendChild(g.el);
1475
1422
  }
1476
- const overlay = target?.closest?.(".cs-ovl");
1477
- if (!overlay) return;
1478
- const board = boardOf(target);
1479
- if (!board) return;
1480
- if (state.mode === "interact") return;
1481
- setActive(board.entry.id);
1482
- const el = elementAt(board, e.clientX, e.clientY);
1483
- if (!el) return;
1484
- if (state.pinPending) {
1485
- openComposer(board, el, e.clientX, e.clientY);
1486
- return;
1423
+ if (!listEl.childElementCount) {
1424
+ listEl.appendChild(h("div", "cs-sb-empty", filter ? "\u6CA1\u6709\u5339\u914D\u7684\u753B\u677F" : "\u8FD8\u6CA1\u6709\u753B\u677F"));
1487
1425
  }
1488
- if (state.mode !== "browse") return;
1489
- const selector = buildSelector(el);
1490
- const rel = relPos(el, e.clientX, e.clientY, board);
1491
- showSelection(board, selector);
1492
- state.selection = { artboardId: board.entry.id, selector, x: rel.x, y: rel.y, ts: Date.now() };
1493
- setProbe(`\u5DF2\u9009\u4E2D ${board.entry.exportName} \u2192 ${selector}`);
1494
- postSelection(state.selection).catch((err) => setProbe(`selection \u53D1\u9001\u5931\u8D25:${String(err)}`));
1426
+ syncActive();
1427
+ restoreFocus(keepFocus);
1495
1428
  }
1496
- function onDblClick(e) {
1497
- const board = boardOf(e.target);
1498
- if (!board || !e.target.closest(".cs-ovl")) return;
1499
- setMode("interact", board.entry.id);
1429
+ function group(key, label, ids, headCls) {
1430
+ const el2 = h("div", "cs-sb-group");
1431
+ if (headCls === "cs-sb-file") el2.classList.add("is-file");
1432
+ const headEl = h("div", headCls);
1433
+ const body = h("div", "cs-sb-items");
1434
+ body.id = `cs-sb-g${++uid}`;
1435
+ const collapsed = isCollapsed(key);
1436
+ body.hidden = collapsed;
1437
+ const { mode, shown } = bulkMode(ids);
1438
+ const caret = h("button", "cs-sb-caret");
1439
+ caret.type = "button";
1440
+ caret.dataset.fk = `caret:${key}`;
1441
+ caret.setAttribute("aria-expanded", String(!collapsed));
1442
+ caret.setAttribute("aria-controls", body.id);
1443
+ caret.title = collapsed ? `\u5C55\u5F00\u300C${label}\u300D` : `\u6298\u53E0\u300C${label}\u300D`;
1444
+ const arrow = h("span", "cs-sb-arrow", "\u25B8");
1445
+ arrow.setAttribute("aria-hidden", "true");
1446
+ const count = h("span", "cs-sb-count", mode === "all" ? String(ids.length) : `${shown}/${ids.length}`);
1447
+ caret.append(arrow, h("span", "cs-sb-label", label), count);
1448
+ caret.addEventListener("click", () => {
1449
+ toggleCollapsed(key);
1450
+ renderSidebar();
1451
+ });
1452
+ const bulk = h("button", "cs-sb-bulk", mode === "all" ? "\u25CF" : mode === "none" ? "\u25CC" : "\u25D0");
1453
+ bulk.type = "button";
1454
+ bulk.dataset.fk = `bulk:${key}`;
1455
+ bulk.dataset.state = mode;
1456
+ bulk.setAttribute("aria-pressed", mode === "all" ? "true" : mode === "none" ? "false" : "mixed");
1457
+ const act = mode === "all" ? `\u9690\u85CF\u300C${label}\u300D\u4E0B\u5168\u90E8 ${ids.length} \u5757\u753B\u677F` : `\u663E\u793A\u300C${label}\u300D\u4E0B\u5168\u90E8 ${ids.length} \u5757\u753B\u677F`;
1458
+ bulk.setAttribute("aria-label", act);
1459
+ bulk.title = mode === "some" ? `${act}(\u5F53\u524D ${shown}/${ids.length} \u663E\u793A\u4E2D)` : act;
1460
+ bulk.addEventListener("click", (e) => {
1461
+ e.stopPropagation();
1462
+ setGroupHidden(ids, mode === "all");
1463
+ renderSidebar();
1464
+ });
1465
+ headEl.append(caret, bulk);
1466
+ el2.append(headEl, body);
1467
+ return { el: el2, body };
1500
1468
  }
1501
- function initModes() {
1502
- viewport2 = qs("#cs-viewport");
1503
- viewport2.addEventListener("mousemove", onMouseMove);
1504
- viewport2.addEventListener("mouseleave", hideHover);
1505
- viewport2.addEventListener("click", onClick);
1506
- viewport2.addEventListener("dblclick", onDblClick);
1507
- document.addEventListener("keydown", onKeyDown);
1508
- window.addEventListener("resize", () => {
1509
- if (state.mode === "review") fitActiveBoard();
1510
- else applyView();
1469
+ function row(id, name, sub, kind) {
1470
+ const el2 = h("div", "cs-sb-row");
1471
+ el2.dataset.id = id;
1472
+ const go = h("button", "cs-sb-go");
1473
+ go.type = "button";
1474
+ go.dataset.fk = `go:${id}`;
1475
+ go.dataset.kind = kind;
1476
+ go.title = sub ? `\u805A\u7126 ${name} (${sub})` : `\u805A\u7126 ${name}`;
1477
+ go.append(h("span", "cs-sb-dot"), h("span", "cs-sb-name", name));
1478
+ if (sub) go.appendChild(h("span", "cs-sb-route", sub));
1479
+ go.addEventListener("click", () => {
1480
+ if (isHidden(id)) toggleHidden(id);
1481
+ fitBoard(id);
1482
+ state.activeId = id;
1483
+ syncActive();
1511
1484
  });
1512
- syncHud();
1485
+ const eye = h("button", "cs-sb-eye", isHidden(id) ? "\u25CC" : "\u25CF");
1486
+ eye.type = "button";
1487
+ eye.dataset.fk = `eye:${id}`;
1488
+ eye.title = isHidden(id) ? "\u663E\u793A\u8FD9\u5757\u753B\u677F" : "\u4ECE\u5899\u4E0A\u9690\u85CF(\u4E0D\u5220,\u968F\u65F6\u70B9\u56DE\u6765)";
1489
+ eye.setAttribute("aria-label", isHidden(id) ? `\u663E\u793A ${name}` : `\u9690\u85CF ${name}`);
1490
+ eye.setAttribute("aria-pressed", String(!isHidden(id)));
1491
+ eye.addEventListener("click", (e) => {
1492
+ e.stopPropagation();
1493
+ toggleHidden(id);
1494
+ renderSidebar();
1495
+ });
1496
+ el2.append(go, eye);
1497
+ if (isHidden(id)) el2.classList.add("is-hidden-board");
1498
+ return el2;
1499
+ }
1500
+ function syncActive() {
1501
+ if (!listEl) return;
1502
+ for (const r of listEl.querySelectorAll(".cs-sb-row")) {
1503
+ r.classList.toggle("is-active", r.dataset.id === state.activeId);
1504
+ }
1513
1505
  }
1514
1506
 
1515
1507
  // src/canvas/wall.ts
@@ -1609,9 +1601,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1609
1601
  return titleEl;
1610
1602
  }
1611
1603
  function createBoard(entry) {
1612
- const el = h("div", "cs-board");
1613
- el.dataset.id = entry.id;
1614
- el.dataset.kind = entry.kind;
1604
+ const el2 = h("div", "cs-board");
1605
+ el2.dataset.id = entry.id;
1606
+ el2.dataset.kind = entry.kind;
1615
1607
  const titleEl = h("div", "cs-board-title");
1616
1608
  const nameEl = h("span", "cs-name", entry.exportName);
1617
1609
  const badgeEl = h("span", "cs-badge", entry.kind);
@@ -1623,11 +1615,11 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1623
1615
  const overlayEl = h("div", "cs-ovl");
1624
1616
  const pinLayerEl = h("div", "cs-pins");
1625
1617
  frameEl.append(placeholderEl, overlayEl);
1626
- el.append(titleEl, frameEl, pinLayerEl);
1627
- world2.appendChild(el);
1618
+ el2.append(titleEl, frameEl, pinLayerEl);
1619
+ world2.appendChild(el2);
1628
1620
  const board = {
1629
1621
  entry,
1630
- el,
1622
+ el: el2,
1631
1623
  titleEl,
1632
1624
  nameEl,
1633
1625
  badgeEl,
@@ -1650,10 +1642,10 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1650
1642
  }
1651
1643
  attachResizeHandles(board);
1652
1644
  attachBoardDrag(board);
1653
- el.hidden = isHidden(entry.id);
1645
+ el2.hidden = isHidden(entry.id);
1654
1646
  applySize(board);
1655
1647
  updateBoard(board, entry);
1656
- io.observe(el);
1648
+ io.observe(el2);
1657
1649
  return board;
1658
1650
  }
1659
1651
  var WIDTH_PRESETS = [390, 768, 1024, 1280, 1440];
@@ -2393,210 +2385,269 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2393
2385
  settleTimer = null;
2394
2386
  }
2395
2387
 
2396
- // src/canvas/sidebar.ts
2397
- var root;
2398
- var listEl;
2399
- var filter = "";
2400
- var searchCollapsed = null;
2401
- var uid = 0;
2402
- function initSidebar() {
2403
- root = qs("#cs-sidebar");
2404
- root.textContent = "";
2405
- const head = h("div", "cs-sb-head");
2406
- const search = document.createElement("input");
2407
- search.className = "cs-sb-search";
2408
- search.type = "search";
2409
- search.placeholder = "\u641C\u7D22\u753B\u677F\u2026";
2410
- search.addEventListener("input", () => {
2411
- filter = search.value.trim().toLowerCase();
2412
- searchCollapsed = filter ? /* @__PURE__ */ new Set() : null;
2413
- renderSidebar();
2414
- });
2415
- search.addEventListener("keydown", (e) => {
2416
- e.stopPropagation();
2417
- if (e.key === "Escape") {
2418
- search.value = "";
2419
- filter = "";
2420
- searchCollapsed = null;
2421
- renderSidebar();
2422
- search.blur();
2388
+ // src/canvas/modes.ts
2389
+ var viewport2;
2390
+ function boardOf(target) {
2391
+ const el2 = target?.closest?.(".cs-board");
2392
+ if (!el2?.dataset.id) return null;
2393
+ return state.boards.get(el2.dataset.id) ?? null;
2394
+ }
2395
+ function setActive(id) {
2396
+ if (state.activeId === id) return;
2397
+ state.activeId = id;
2398
+ for (const b of state.boards.values()) b.el.classList.toggle("is-active", b.entry.id === id);
2399
+ }
2400
+ function setMode(mode, boardId) {
2401
+ if (mode !== "browse" && boardId) setActive(boardId);
2402
+ if (mode === "review" && !state.activeId) return;
2403
+ const from = state.mode;
2404
+ state.mode = mode;
2405
+ hideHover();
2406
+ if (mode !== "browse") clearSelection();
2407
+ syncHud();
2408
+ if (mode === "review" && from !== "review") {
2409
+ state.modeBeforeReview = from;
2410
+ state.viewBeforeReview = { scale: state.scale, tx: state.tx, ty: state.ty };
2411
+ closeArgsPanel();
2412
+ fitActiveBoard();
2413
+ } else if (mode !== "review" && from === "review" && state.viewBeforeReview) {
2414
+ const v = state.viewBeforeReview;
2415
+ state.viewBeforeReview = null;
2416
+ setView(v.tx, v.ty, v.scale);
2417
+ }
2418
+ setProbe(
2419
+ mode === "interact" ? "\u4EA4\u4E92\u6A21\u5F0F:\u76F4\u63A5\u64CD\u4F5C\u8FD9\u5757\u753B\u677F,Esc \u56DE\u6D4F\u89C8" : mode === "review" ? "\u8D70\u67E5\u6A21\u5F0F:Esc \u9000\u51FA" : "\u79FB\u5230\u753B\u677F\u4E0A\u53CD\u67E5\u5143\u7D20"
2420
+ );
2421
+ }
2422
+ function fitActiveBoard() {
2423
+ const board = state.activeId ? state.boards.get(state.activeId) : null;
2424
+ if (!board) return;
2425
+ const r = board.frameEl.getBoundingClientRect();
2426
+ const worldX = (r.left - state.tx) / state.scale;
2427
+ const worldY = (r.top - state.ty) / state.scale;
2428
+ const vw = window.innerWidth;
2429
+ const vh = window.innerHeight;
2430
+ const s = clamp(Math.min(vw / board.width, vh / board.height), MIN_SCALE, MAX_SCALE);
2431
+ setView((vw - board.width * s) / 2 - worldX * s, (vh - board.height * s) / 2 - worldY * s, s);
2432
+ }
2433
+ function escape() {
2434
+ if (isComposing()) {
2435
+ closeComposer();
2436
+ exitPinMode();
2437
+ return;
2438
+ }
2439
+ if (state.pinPending) {
2440
+ exitPinMode();
2441
+ return;
2442
+ }
2443
+ if (selectedPin()) {
2444
+ selectPin(null);
2445
+ return;
2446
+ }
2447
+ if (state.mode === "review") {
2448
+ setMode(state.modeBeforeReview === "review" ? "browse" : state.modeBeforeReview);
2449
+ return;
2450
+ }
2451
+ if (state.mode === "interact") {
2452
+ setMode("browse");
2453
+ return;
2454
+ }
2455
+ clearSelection();
2456
+ closeArgsPanel();
2457
+ setProbe("\u79FB\u5230\u753B\u677F\u4E0A\u53CD\u67E5\u5143\u7D20");
2458
+ }
2459
+ function isEditable(target) {
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;
2465
+ }
2466
+ function onKeyDown(e) {
2467
+ if (e.key === "Escape") {
2468
+ escape();
2469
+ return;
2470
+ }
2471
+ if (isEditable(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;
2472
+ if (e.key === "Enter") {
2473
+ if (selectedPin()) {
2474
+ e.preventDefault();
2475
+ editSelectedPin();
2476
+ return;
2477
+ }
2478
+ if (state.mode !== "review" && state.activeId) {
2479
+ e.preventDefault();
2480
+ setMode("review", state.activeId);
2481
+ }
2482
+ return;
2483
+ }
2484
+ if (e.key === "Backspace" || e.key === "Delete") {
2485
+ if (selectedPin()) {
2486
+ e.preventDefault();
2487
+ deleteSelectedPin();
2488
+ return;
2489
+ }
2490
+ if (state.mode === "browse" && state.activeId) {
2491
+ e.preventDefault();
2492
+ const b = state.boards.get(state.activeId);
2493
+ setHidden([state.activeId], true);
2494
+ clearSelection();
2495
+ setActive(null);
2496
+ toast(`\u5DF2\u6536\u8D77\u300C${b?.entry.exportName ?? state.activeId}\u300D\xB7 \u5DE6\u4FA7\u5217\u8868\u968F\u65F6\u70B9\u56DE\u6765`, "info");
2423
2497
  }
2424
- });
2425
- head.append(h("span", "cs-sb-title", "\u753B\u677F"), search);
2426
- listEl = h("div", "cs-sb-list");
2427
- listEl.addEventListener("keydown", (e) => {
2428
- if ((e.key === "Enter" || e.key === " ") && e.target.closest("button")) {
2429
- e.stopPropagation();
2498
+ return;
2499
+ }
2500
+ if (e.key === "c" || e.key === "C") {
2501
+ if (state.mode === "browse" && !state.pinPending) {
2502
+ e.preventDefault();
2503
+ enterPinMode();
2430
2504
  }
2431
- });
2432
- root.append(head, listEl);
2433
- const toggle = qs("#cs-sb-toggle");
2434
- toggle.addEventListener("click", () => {
2435
- const closed = document.body.dataset.sidebar === "off";
2436
- document.body.dataset.sidebar = closed ? "on" : "off";
2437
- toggle.title = closed ? "\u6536\u8D77\u5217\u8868" : "\u5C55\u5F00\u5217\u8868";
2438
- });
2439
- renderSidebar();
2440
- }
2441
- function match(text) {
2442
- return !filter || text.toLowerCase().includes(filter);
2443
- }
2444
- function collapseKey() {
2445
- return `cs-collapsed:${state.info?.projectRoot ?? "unknown"}`;
2446
- }
2447
- function collapsedSet() {
2448
- try {
2449
- return new Set(JSON.parse(localStorage.getItem(collapseKey()) ?? "[]"));
2450
- } catch {
2451
- return /* @__PURE__ */ new Set();
2505
+ return;
2452
2506
  }
2453
- }
2454
- function isCollapsed(key) {
2455
- return searchCollapsed ? searchCollapsed.has(key) : collapsedSet().has(key);
2456
- }
2457
- function toggleCollapsed(key) {
2458
- if (searchCollapsed) {
2459
- if (!searchCollapsed.delete(key)) searchCollapsed.add(key);
2507
+ if (e.key === "y" || e.key === "Y") {
2508
+ e.preventDefault();
2509
+ void copyContext();
2460
2510
  return;
2461
2511
  }
2462
- const s = collapsedSet();
2463
- if (s.has(key)) s.delete(key);
2464
- else s.add(key);
2465
- try {
2466
- localStorage.setItem(collapseKey(), JSON.stringify([...s]));
2467
- } catch {
2512
+ if (e.key === "p") {
2513
+ e.preventDefault();
2514
+ void pushToClaudeCode();
2515
+ return;
2516
+ }
2517
+ if (e.key === "1") {
2518
+ e.preventDefault();
2519
+ fitAll();
2520
+ return;
2521
+ }
2522
+ if (e.key === "2") {
2523
+ e.preventDefault();
2524
+ fitBoard();
2525
+ return;
2526
+ }
2527
+ if (e.key === "0") {
2528
+ e.preventDefault();
2529
+ zoomReset();
2530
+ return;
2531
+ }
2532
+ if (e.key === "=" || e.key === "+") {
2533
+ e.preventDefault();
2534
+ zoomStep(1.25);
2535
+ return;
2536
+ }
2537
+ if (e.key === "-" || e.key === "_") {
2538
+ e.preventDefault();
2539
+ zoomStep(0.8);
2468
2540
  }
2469
2541
  }
2470
- function bulkMode(ids) {
2471
- const shown = ids.filter((id) => !isHidden(id)).length;
2472
- return { mode: shown === ids.length ? "all" : shown === 0 ? "none" : "some", shown };
2473
- }
2474
- function setGroupHidden(ids, hide) {
2475
- setHidden(ids, hide);
2476
- }
2477
- function focusedKey() {
2478
- const el = document.activeElement;
2479
- return el && listEl.contains(el) ? el.dataset.fk ?? null : null;
2480
- }
2481
- function restoreFocus(key) {
2482
- if (!key) return;
2483
- const el = listEl.querySelector(`[data-fk="${CSS.escape(key)}"]`);
2484
- el?.focus({ preventScroll: true });
2542
+ function attachFrameKeys(doc) {
2543
+ doc.addEventListener("keydown", onKeyDown);
2485
2544
  }
2486
- function renderSidebar() {
2487
- if (!listEl) return;
2488
- const keepFocus = focusedKey();
2489
- listEl.textContent = "";
2490
- uid = 0;
2491
- const screens = state.entries.filter((e) => e.kind === "screen");
2492
- const comps = state.entries.filter((e) => e.kind !== "screen");
2493
- const visScreens = screens.filter((e) => match(e.exportName) || match(e.url ?? ""));
2494
- if (visScreens.length) {
2495
- const g = group("sec:screen", "\u9875\u9762", visScreens.map((e) => e.id), "cs-sb-section");
2496
- for (const e of visScreens) g.body.appendChild(row(e.id, e.exportName, e.url ?? "", "screen"));
2497
- listEl.appendChild(g.el);
2545
+ function onMouseMove(e) {
2546
+ if (isPanning()) return;
2547
+ const overlay = e.target?.closest?.(".cs-ovl");
2548
+ const board = overlay ? boardOf(e.target) : null;
2549
+ if (!board) {
2550
+ hideHover();
2551
+ return;
2498
2552
  }
2499
- const byFile = /* @__PURE__ */ new Map();
2500
- for (const e of comps) {
2501
- if (!(match(e.exportName) || match(groupLabel(e.file)))) continue;
2502
- const l = byFile.get(e.file);
2503
- if (l) l.push(e);
2504
- else byFile.set(e.file, [e]);
2553
+ if (state.mode !== "browse") return;
2554
+ setActive(board.entry.id);
2555
+ const el2 = elementAt(board, e.clientX, e.clientY);
2556
+ if (!el2) {
2557
+ hideHover();
2558
+ return;
2505
2559
  }
2506
- if (byFile.size) {
2507
- const files = [...byFile.keys()].sort();
2508
- const allIds = files.flatMap((f) => (byFile.get(f) ?? []).map((e) => e.id));
2509
- const g = group("sec:component", "\u7EC4\u4EF6", allIds, "cs-sb-section");
2510
- for (const file of files) {
2511
- const list = byFile.get(file) ?? [];
2512
- const fg = group(`file:${file}`, groupLabel(file), list.map((e) => e.id), "cs-sb-file");
2513
- for (const e of list) fg.body.appendChild(row(e.id, e.exportName, "", "component"));
2514
- g.body.appendChild(fg.el);
2515
- }
2516
- listEl.appendChild(g.el);
2560
+ showHover(board, el2);
2561
+ setProbe(`${board.entry.exportName} ${describe(el2)}`);
2562
+ }
2563
+ function onClick(e) {
2564
+ const target = e.target;
2565
+ const title = target?.closest?.(".cs-board-title");
2566
+ if (title) {
2567
+ const board2 = boardOf(target);
2568
+ if (!board2) return;
2569
+ setActive(board2.entry.id);
2570
+ if (isArgsOpen(board2.entry.id)) closeArgsPanel();
2571
+ else openArgsPanel(board2);
2572
+ return;
2517
2573
  }
2518
- if (!listEl.childElementCount) {
2519
- listEl.appendChild(h("div", "cs-sb-empty", filter ? "\u6CA1\u6709\u5339\u914D\u7684\u753B\u677F" : "\u8FD8\u6CA1\u6709\u753B\u677F"));
2574
+ const overlay = target?.closest?.(".cs-ovl");
2575
+ if (!overlay) return;
2576
+ const board = boardOf(target);
2577
+ if (!board) return;
2578
+ if (state.mode === "interact") {
2579
+ if (board.entry.id !== state.activeId) hintInactive(board);
2580
+ return;
2520
2581
  }
2521
- syncActive();
2522
- restoreFocus(keepFocus);
2582
+ setActive(board.entry.id);
2583
+ const el2 = elementAt(board, e.clientX, e.clientY);
2584
+ if (!el2) return;
2585
+ if (state.pinPending) {
2586
+ openComposer(board, el2, e.clientX, e.clientY);
2587
+ return;
2588
+ }
2589
+ if (state.mode !== "browse") return;
2590
+ const selector = buildSelector(el2);
2591
+ const rel = relPos(el2, e.clientX, e.clientY, board);
2592
+ showSelection(board, selector);
2593
+ state.selection = { artboardId: board.entry.id, selector, x: rel.x, y: rel.y, ts: Date.now() };
2594
+ setProbe(`\u5DF2\u9009\u4E2D ${board.entry.exportName} \u2192 ${selector}`);
2595
+ postSelection(state.selection).catch((err) => setProbe(`selection \u53D1\u9001\u5931\u8D25:${String(err)}`));
2523
2596
  }
2524
- function group(key, label, ids, headCls) {
2525
- const el = h("div", "cs-sb-group");
2526
- if (headCls === "cs-sb-file") el.classList.add("is-file");
2527
- const headEl = h("div", headCls);
2528
- const body = h("div", "cs-sb-items");
2529
- body.id = `cs-sb-g${++uid}`;
2530
- const collapsed = isCollapsed(key);
2531
- body.hidden = collapsed;
2532
- const { mode, shown } = bulkMode(ids);
2533
- const caret = h("button", "cs-sb-caret");
2534
- caret.type = "button";
2535
- caret.dataset.fk = `caret:${key}`;
2536
- caret.setAttribute("aria-expanded", String(!collapsed));
2537
- caret.setAttribute("aria-controls", body.id);
2538
- caret.title = collapsed ? `\u5C55\u5F00\u300C${label}\u300D` : `\u6298\u53E0\u300C${label}\u300D`;
2539
- const arrow = h("span", "cs-sb-arrow", "\u25B8");
2540
- arrow.setAttribute("aria-hidden", "true");
2541
- const count = h("span", "cs-sb-count", mode === "all" ? String(ids.length) : `${shown}/${ids.length}`);
2542
- caret.append(arrow, h("span", "cs-sb-label", label), count);
2543
- caret.addEventListener("click", () => {
2544
- toggleCollapsed(key);
2545
- renderSidebar();
2546
- });
2547
- const bulk = h("button", "cs-sb-bulk", mode === "all" ? "\u25CF" : mode === "none" ? "\u25CC" : "\u25D0");
2548
- bulk.type = "button";
2549
- bulk.dataset.fk = `bulk:${key}`;
2550
- bulk.dataset.state = mode;
2551
- bulk.setAttribute("aria-pressed", mode === "all" ? "true" : mode === "none" ? "false" : "mixed");
2552
- const act = mode === "all" ? `\u9690\u85CF\u300C${label}\u300D\u4E0B\u5168\u90E8 ${ids.length} \u5757\u753B\u677F` : `\u663E\u793A\u300C${label}\u300D\u4E0B\u5168\u90E8 ${ids.length} \u5757\u753B\u677F`;
2553
- bulk.setAttribute("aria-label", act);
2554
- bulk.title = mode === "some" ? `${act}(\u5F53\u524D ${shown}/${ids.length} \u663E\u793A\u4E2D)` : act;
2555
- bulk.addEventListener("click", (e) => {
2556
- e.stopPropagation();
2557
- setGroupHidden(ids, mode === "all");
2558
- renderSidebar();
2559
- });
2560
- headEl.append(caret, bulk);
2561
- el.append(headEl, body);
2562
- return { el, body };
2597
+ var lastHint = "";
2598
+ var lastHintAt = 0;
2599
+ function hintInactive(board) {
2600
+ const cur = state.boards.get(state.activeId ?? "")?.entry.exportName ?? "?";
2601
+ setProbe(`\u4EA4\u4E92\u4E2D\u7684\u662F\u300C${cur}\u300D\u2014\u2014 \u8981\u4EA4\u4E92\u8FD9\u5757\u8BF7\u53CC\u51FB\u5B83,Esc \u9000\u51FA\u4EA4\u4E92`);
2602
+ const now = Date.now();
2603
+ if (lastHint === board.entry.id && now - lastHintAt < 4e3) return;
2604
+ lastHint = board.entry.id;
2605
+ lastHintAt = now;
2606
+ toast(`\u300C${board.entry.exportName}\u300D\u672A\u6FC0\u6D3B \xB7 \u53CC\u51FB\u5B83\u5207\u6362\u4EA4\u4E92\u76EE\u6807`, "info");
2563
2607
  }
2564
- function row(id, name, sub, kind) {
2565
- const el = h("div", "cs-sb-row");
2566
- el.dataset.id = id;
2567
- const go = h("button", "cs-sb-go");
2568
- go.type = "button";
2569
- go.dataset.fk = `go:${id}`;
2570
- go.dataset.kind = kind;
2571
- go.title = sub ? `\u805A\u7126 ${name} (${sub})` : `\u805A\u7126 ${name}`;
2572
- go.append(h("span", "cs-sb-dot"), h("span", "cs-sb-name", name));
2573
- if (sub) go.appendChild(h("span", "cs-sb-route", sub));
2574
- go.addEventListener("click", () => {
2575
- if (isHidden(id)) toggleHidden(id);
2576
- fitBoard(id);
2577
- state.activeId = id;
2578
- syncActive();
2579
- });
2580
- const eye = h("button", "cs-sb-eye", isHidden(id) ? "\u25CC" : "\u25CF");
2581
- eye.type = "button";
2582
- eye.dataset.fk = `eye:${id}`;
2583
- eye.title = isHidden(id) ? "\u663E\u793A\u8FD9\u5757\u753B\u677F" : "\u4ECE\u5899\u4E0A\u9690\u85CF(\u4E0D\u5220,\u968F\u65F6\u70B9\u56DE\u6765)";
2584
- eye.setAttribute("aria-label", isHidden(id) ? `\u663E\u793A ${name}` : `\u9690\u85CF ${name}`);
2585
- eye.setAttribute("aria-pressed", String(!isHidden(id)));
2586
- eye.addEventListener("click", (e) => {
2587
- e.stopPropagation();
2588
- toggleHidden(id);
2589
- renderSidebar();
2608
+ function onDblClick(e) {
2609
+ const board = boardOf(e.target);
2610
+ if (!board || !e.target.closest(".cs-ovl")) return;
2611
+ setMode("interact", board.entry.id);
2612
+ }
2613
+ function initModes() {
2614
+ viewport2 = qs("#cs-viewport");
2615
+ viewport2.addEventListener("mousemove", onMouseMove);
2616
+ viewport2.addEventListener("mouseleave", hideHover);
2617
+ viewport2.addEventListener("click", onClick);
2618
+ viewport2.addEventListener("dblclick", onDblClick);
2619
+ document.addEventListener("keydown", onKeyDown);
2620
+ window.addEventListener("resize", () => {
2621
+ if (state.mode === "review") fitActiveBoard();
2622
+ else applyView();
2590
2623
  });
2591
- el.append(go, eye);
2592
- if (isHidden(id)) el.classList.add("is-hidden-board");
2593
- return el;
2624
+ syncHud();
2594
2625
  }
2595
- function syncActive() {
2596
- if (!listEl) return;
2597
- for (const r of listEl.querySelectorAll(".cs-sb-row")) {
2598
- r.classList.toggle("is-active", r.dataset.id === state.activeId);
2599
- }
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;
2600
2651
  }
2601
2652
 
2602
2653
  // src/canvas/app.ts
@@ -2655,9 +2706,9 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2655
2706
  }
2656
2707
  function showTarget() {
2657
2708
  const info = state.info;
2658
- const el = qs("#cs-target");
2659
- el.textContent = info ? `${info.target} \xB7 ${info.designDir}` : "";
2660
- 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}` : "";
2661
2712
  }
2662
2713
  function onRegistryEvent(entries) {
2663
2714
  void reloadUntilMatches(entries.map((e) => e.id));
@@ -2701,16 +2752,31 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
2701
2752
  }
2702
2753
  showTarget();
2703
2754
  restoreView();
2704
- await tryLoadRegistry();
2705
2755
  try {
2706
- state.annotations = await fetchAnnotations();
2707
- 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);
2708
2760
  } catch {
2709
2761
  }
2710
2762
  connectEvents({
2711
2763
  registry: onRegistryEvent,
2712
- 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
+ }
2713
2773
  });
2774
+ await tryLoadRegistry();
2775
+ try {
2776
+ state.annotations = await fetchAnnotations();
2777
+ renderPins();
2778
+ } catch {
2779
+ }
2714
2780
  }
2715
2781
  void boot();
2716
2782
  })();