contactsheet 0.1.3 → 0.1.4

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