@codecanvas-ide/engine 0.3.0 → 0.4.0

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/dist/index.js CHANGED
@@ -2024,13 +2024,13 @@ function contains(arrangement, unit) {
2024
2024
 
2025
2025
  // src/layout.ts
2026
2026
  var M = {
2027
- headerH: 32,
2028
- headerCharW: 7.9,
2029
- // 13px monospace
2030
- previewLineH: 15,
2031
- previewCharW: 6.7,
2032
- // 11px monospace
2033
- previewPad: 10,
2027
+ headerH: 34,
2028
+ headerCharW: 7.6,
2029
+ // 12.5px monospace
2030
+ previewLineH: 19,
2031
+ previewCharW: 6.95,
2032
+ // 11.5px monospace
2033
+ previewPad: 16,
2034
2034
  pad: 14,
2035
2035
  // inner padding of containers
2036
2036
  gap: 28,
@@ -2298,103 +2298,220 @@ function setHTML(element, html) {
2298
2298
  const htmlPolicy = trustedHtmlPolicy();
2299
2299
  element.innerHTML = htmlPolicy ? htmlPolicy.createHTML(html) : html;
2300
2300
  }
2301
+ function esc(s) {
2302
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2303
+ }
2304
+
2305
+ // src/highlight.ts
2306
+ var KEYWORDS = /* @__PURE__ */ new Set([
2307
+ "import",
2308
+ "from",
2309
+ "export",
2310
+ "default",
2311
+ "const",
2312
+ "let",
2313
+ "var",
2314
+ "function",
2315
+ "return",
2316
+ "async",
2317
+ "await",
2318
+ "if",
2319
+ "else",
2320
+ "for",
2321
+ "while",
2322
+ "do",
2323
+ "switch",
2324
+ "case",
2325
+ "break",
2326
+ "continue",
2327
+ "new",
2328
+ "class",
2329
+ "extends",
2330
+ "implements",
2331
+ "interface",
2332
+ "type",
2333
+ "enum",
2334
+ "namespace",
2335
+ "this",
2336
+ "super",
2337
+ "throw",
2338
+ "try",
2339
+ "catch",
2340
+ "finally",
2341
+ "typeof",
2342
+ "instanceof",
2343
+ "in",
2344
+ "of",
2345
+ "yield",
2346
+ "void",
2347
+ "delete",
2348
+ "null",
2349
+ "undefined",
2350
+ "true",
2351
+ "false",
2352
+ "static",
2353
+ "public",
2354
+ "private",
2355
+ "protected",
2356
+ "readonly",
2357
+ "as",
2358
+ "satisfies",
2359
+ "get",
2360
+ "set"
2361
+ ]);
2362
+ var TOKEN = /(\/\*.*?\*\/|\/\/.*$|'(?:\\.|[^'\\])*'?|"(?:\\.|[^"\\])*"?|`(?:\\.|[^`\\])*`?|[A-Za-z_$][\w$]*|\d[\w.]*|\s+|.)/g;
2363
+ function highlightLine(line) {
2364
+ let html = "";
2365
+ for (const [token] of line.matchAll(TOKEN)) {
2366
+ const syntaxClass = classOf(token);
2367
+ html += syntaxClass ? `<span class="${syntaxClass}">${esc(token)}</span>` : esc(token);
2368
+ }
2369
+ return html;
2370
+ }
2371
+ function classOf(token) {
2372
+ if (token.startsWith("//") || token.startsWith("/*")) return "s-com";
2373
+ if (/^['"`]/.test(token)) return "s-str";
2374
+ if (/^\d/.test(token)) return "s-con";
2375
+ if (KEYWORDS.has(token)) return "s-kw";
2376
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(token)) return "s-con";
2377
+ if (/^[A-Z][a-z]\w*$/.test(token)) return "s-type";
2378
+ return void 0;
2379
+ }
2301
2380
 
2302
2381
  // src/styles.ts
2303
2382
  var CSS = (
2304
2383
  /* css */
2305
2384
  `
2306
2385
  .cc-root {
2307
- --cc-bg: #15171c;
2308
- --cc-dot: rgba(255,255,255,.07);
2309
- --cc-frame-bg: rgba(255,255,255,.025);
2310
- --cc-frame-border: rgba(255,255,255,.10);
2311
- --cc-text: #e8eaee;
2312
- --cc-muted: #9aa1ad;
2313
- --cc-faint: #c4cad4;
2314
- --cc-panel: #1f2229;
2315
- --cc-panel-border: rgba(255,255,255,.12);
2316
- --cc-accent: #7aa2ff;
2317
- --cc-danger: #ff6b6b;
2318
- --cc-shadow: 0 10px 30px rgba(0,0,0,.45);
2386
+ --cc-bg: #0c0d0f;
2387
+ --cc-dot: #25272c;
2388
+ --cc-frame-bg: rgba(255,255,255,.015);
2389
+ --cc-frame-border: #2e3036;
2390
+ --cc-node-bg: #16171a;
2391
+ --cc-node-border: #2b2d33;
2392
+ --cc-node-head-border: #24262b;
2393
+ --cc-text: #e6e7ea;
2394
+ --cc-strong: #f2f2f3;
2395
+ --cc-muted: #a9adb6;
2396
+ --cc-faint: #8b8f98;
2397
+ --cc-faintest: #6e727b;
2398
+ --cc-code: #c9ccd2;
2399
+ --cc-panel: #16171a;
2400
+ --cc-surface2: #1c1d21;
2401
+ --cc-raised: #2a2c31;
2402
+ --cc-panel-border: #26282d;
2403
+ --cc-accent: #e8c27a;
2404
+ --cc-ink: #e8c27a;
2405
+ --cc-on-accent: #1a1408;
2406
+ --cc-danger: #f2877d;
2407
+ --cc-shadow: 0 10px 30px rgba(0,0,0,.35);
2408
+ --cc-shadow-sm: 0 1px 0 rgba(0,0,0,.2);
2409
+ --cc-shadow-lg: 0 24px 60px rgba(0,0,0,.55);
2410
+ --cc-syn-kw: #c3a6f0;
2411
+ --cc-syn-str: #9fd3ec;
2412
+ --cc-syn-type: #b8c4ff;
2413
+ --cc-syn-com: #7d828c;
2414
+ --cc-syn-con: #7cc4e8;
2415
+ --cc-ui-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
2319
2416
 
2320
- --k-import: #6b7280; --k-import-head: #3b3f46; --k-import-bg: #25272c;
2321
- --k-class: #34b27a; --k-class-head: #1d6b4a; --k-class-bg: rgba(52,178,122,.08);
2322
- --k-function: #ec5fa5; --k-function-head: #8f2f62; --k-function-bg: rgba(236,95,165,.09);
2323
- --k-component: #22b8cf; --k-component-head: #146c7a; --k-component-bg: rgba(34,184,207,.09);
2324
- --k-state: #f0883e; --k-state-head: #8a4614; --k-state-bg: rgba(240,136,62,.09);
2325
- --k-variable: #4b8ef7; --k-variable-head: #23508f; --k-variable-bg: rgba(75,142,247,.09);
2326
- --k-type: #d4a92a; --k-type-head: #6e5714; --k-type-bg: rgba(212,169,42,.08);
2327
- --k-namespace: #9b8cf8; --k-namespace-head: #4d4291; --k-namespace-bg: rgba(155,140,248,.08);
2328
- --k-statement: #7c8799; --k-statement-head: #39414f; --k-statement-bg: rgba(124,135,153,.08);
2417
+ --k-import: #b4b9c2;
2418
+ --k-variable: #7cc4e8;
2419
+ --k-function: #e8c27a;
2420
+ --k-class: #8fd0a5;
2421
+ --k-type: #b8b0f0;
2422
+ --k-component: #5fd1b5;
2423
+ --k-state: #f0a36a;
2424
+ --k-namespace: #9aa6bd;
2425
+ --k-statement: #8b8f98;
2329
2426
 
2330
2427
  position: relative; overflow: hidden; width: 100%; height: 100%;
2331
2428
  background-color: var(--cc-bg);
2332
- background-image: radial-gradient(var(--cc-dot) 1.2px, transparent 1.3px);
2429
+ background-image: radial-gradient(circle, var(--cc-dot) 1px, transparent 1.4px);
2333
2430
  color: var(--cc-text);
2334
- font: 13px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
2431
+ font: 13px/1.3 var(--cc-ui-font);
2335
2432
  touch-action: none; user-select: none; -webkit-user-select: none; outline: none;
2336
2433
  -webkit-tap-highlight-color: transparent;
2337
2434
  }
2338
2435
  .cc-root[data-theme="light"] {
2339
- --cc-bg: #f6f7f9; --cc-dot: rgba(0,0,0,.09);
2340
- --cc-frame-bg: rgba(255,255,255,.7); --cc-frame-border: rgba(0,0,0,.12);
2341
- --cc-text: #1d2129; --cc-muted: #5d6573; --cc-faint: #39404c;
2342
- --cc-panel: #ffffff; --cc-panel-border: rgba(0,0,0,.12); --cc-accent: #3366e6;
2343
- --cc-shadow: 0 10px 30px rgba(20,30,50,.18);
2344
- --k-import: #6b7280; --k-import-head: #4b5058; --k-import-bg: #eceef1;
2345
- --k-class: #1f9963; --k-class-head: #1f9963; --k-class-bg: rgba(31,153,99,.07);
2346
- --k-function: #d63e8c; --k-function-head: #d63e8c; --k-function-bg: rgba(214,62,140,.07);
2347
- --k-component: #0f8fa3; --k-component-head: #0f8fa3; --k-component-bg: rgba(15,143,163,.07);
2348
- --k-state: #d4691c; --k-state-head: #d4691c; --k-state-bg: rgba(212,105,28,.07);
2349
- --k-variable: #2f6fe0; --k-variable-head: #2f6fe0; --k-variable-bg: rgba(47,111,224,.07);
2350
- --k-type: #b58a0c; --k-type-head: #a87f08; --k-type-bg: rgba(181,138,12,.08);
2351
- --k-namespace: #6f5ce6; --k-namespace-head: #6f5ce6; --k-namespace-bg: rgba(111,92,230,.07);
2352
- --k-statement: #64708a; --k-statement-head: #64708a; --k-statement-bg: rgba(100,112,138,.07);
2436
+ --cc-bg: #f3f3f1; --cc-dot: #d4d4cf;
2437
+ --cc-frame-bg: rgba(255,255,255,.5); --cc-frame-border: #cfcfc9;
2438
+ --cc-node-bg: #ffffff; --cc-node-border: #dadad4; --cc-node-head-border: #edede9;
2439
+ --cc-text: #1f2023; --cc-strong: #0f1012; --cc-muted: #4a4d55; --cc-faint: #5f626a; --cc-faintest: #8a8d94; --cc-code: #2e3036;
2440
+ --cc-panel: #ffffff; --cc-surface2: #f5f5f3; --cc-raised: #ececE8; --cc-panel-border: #e1e1dc;
2441
+ --cc-ink: #8a5d0e; --cc-danger: #b4362b;
2442
+ --cc-shadow: 0 8px 24px rgba(20,20,10,.08); --cc-shadow-sm: 0 1px 2px rgba(20,20,10,.06); --cc-shadow-lg: 0 24px 60px rgba(20,20,10,.18);
2443
+ --cc-syn-kw: #7a3fc0; --cc-syn-str: #1d6a92; --cc-syn-type: #3b4fb0; --cc-syn-com: #6f737b; --cc-syn-con: #1d6a92;
2444
+ --k-import: #555a63; --k-variable: #1d6a92; --k-function: #8a5d0e; --k-class: #2f7a4a; --k-type: #5a4bb0;
2445
+ --k-component: #11756a; --k-state: #a8521a; --k-namespace: #4f5b73; --k-statement: #5f626a;
2353
2446
  }
2354
2447
  .cc-world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
2355
2448
  .cc-root.cc-moving .cc-world { will-change: transform; }
2356
- .cc-frame { position: absolute; border: 1px solid var(--cc-frame-border); background: var(--cc-frame-bg); border-radius: 18px; }
2357
- .cc-frame-title { position: absolute; left: 20px; top: 12px; font: 600 15px/20px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); color: var(--cc-muted); white-space: nowrap; }
2449
+ .cc-root.cc-tool-pan, .cc-root.cc-tool-pan .cc-block { cursor: grab; }
2450
+ .cc-mono { font-family: var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); }
2451
+ .cc-frame { position: absolute; border: 1px dashed var(--cc-frame-border); background: var(--cc-frame-bg); border-radius: 18px; }
2452
+ .cc-frame-title {
2453
+ position: absolute; left: 20px; top: 14px; display: flex; align-items: center; gap: 8px; white-space: nowrap;
2454
+ font: 12px/18px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2455
+ }
2456
+ .cc-frame-folder { color: var(--cc-faint); }
2457
+ .cc-frame-file { color: var(--cc-text); font-weight: 500; }
2458
+ .cc-frame-count { padding: 2px 6px; border-radius: 5px; background: var(--cc-surface2); color: var(--cc-faint); font-size: 10.5px; line-height: 14px; }
2459
+ .cc-frame-count:empty { display: none; }
2358
2460
  .cc-empty { position: absolute; font-size: 15px; color: var(--cc-muted); white-space: nowrap; }
2359
2461
 
2360
2462
  .cc-block {
2361
- position: absolute; left: 0; top: 0; box-sizing: border-box; border-radius: 10px;
2362
- border: 1.5px solid var(--k); background: var(--kbg); overflow: hidden; contain: layout style;
2363
- cursor: default;
2364
- }
2365
- .cc-block.k-import { --k: var(--k-import); --kh: var(--k-import-head); --kbg: var(--k-import-bg); }
2366
- .cc-block.k-class { --k: var(--k-class); --kh: var(--k-class-head); --kbg: var(--k-class-bg); }
2367
- .cc-block.k-function { --k: var(--k-function); --kh: var(--k-function-head); --kbg: var(--k-function-bg); }
2368
- .cc-block.k-component { --k: var(--k-component); --kh: var(--k-component-head); --kbg: var(--k-component-bg); }
2369
- .cc-block.k-state { --k: var(--k-state); --kh: var(--k-state-head); --kbg: var(--k-state-bg); }
2370
- .cc-block.k-variable { --k: var(--k-variable); --kh: var(--k-variable-head); --kbg: var(--k-variable-bg); }
2371
- .cc-block.k-type { --k: var(--k-type); --kh: var(--k-type-head); --kbg: var(--k-type-bg); }
2372
- .cc-block.k-namespace { --k: var(--k-namespace); --kh: var(--k-namespace-head); --kbg: var(--k-namespace-bg); }
2373
- .cc-block.k-statement { --k: var(--k-statement); --kh: var(--k-statement-head); --kbg: var(--k-statement-bg); }
2374
- .cc-block.k-import, .cc-root:not([data-theme="light"]) .cc-block.k-import { border-color: #555a63; }
2463
+ --kglyph: color-mix(in srgb, var(--k) 16%, transparent);
2464
+ position: absolute; left: 0; top: 0; box-sizing: border-box; border-radius: 12px;
2465
+ border: 1px solid var(--cc-node-border); background: var(--cc-node-bg); overflow: hidden; contain: layout style;
2466
+ box-shadow: var(--cc-shadow-sm); cursor: default; transition: opacity 160ms ease, box-shadow 160ms ease;
2467
+ }
2468
+ .cc-root[data-theme="light"] .cc-block { --kglyph: color-mix(in srgb, var(--k) 10%, transparent); }
2469
+ .cc-block.k-import { --k: var(--k-import); }
2470
+ .cc-block.k-class { --k: var(--k-class); }
2471
+ .cc-block.k-function { --k: var(--k-function); }
2472
+ .cc-block.k-component { --k: var(--k-component); }
2473
+ .cc-block.k-state { --k: var(--k-state); }
2474
+ .cc-block.k-variable { --k: var(--k-variable); }
2475
+ .cc-block.k-type { --k: var(--k-type); }
2476
+ .cc-block.k-namespace { --k: var(--k-namespace); }
2477
+ .cc-block.k-statement { --k: var(--k-statement); }
2375
2478
 
2376
2479
  .cc-head {
2377
- display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 10px; box-sizing: border-box;
2378
- background: var(--kh); color: #fff; white-space: nowrap; overflow: hidden;
2379
- font: 13px/32px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2380
- }
2381
- .cc-kind { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; opacity: .75; flex: none; }
2382
- .cc-name { font-weight: 700; flex: none; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
2383
- .cc-detail { opacity: .8; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
2384
- .cc-tags { font-size: 10px; opacity: .7; flex: none; }
2385
- .cc-exp { width: 6px; height: 6px; border-radius: 50%; background: #fff; opacity: .8; flex: none; }
2480
+ display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px; box-sizing: border-box;
2481
+ border-bottom: 1px solid var(--cc-node-head-border); color: var(--cc-strong); white-space: nowrap; overflow: hidden;
2482
+ font: 12.5px/34px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2483
+ }
2484
+ .cc-glyph {
2485
+ flex: none; width: 18px; min-width: 18px; height: 18px; padding: 0 2px; box-sizing: border-box; border-radius: 5px;
2486
+ background: var(--kglyph); color: var(--k); display: flex; align-items: center; justify-content: center;
2487
+ font: 600 10.5px/18px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); letter-spacing: -.04em;
2488
+ }
2489
+ .k-component > .cc-head .cc-glyph, .k-namespace > .cc-head .cc-glyph { width: auto; }
2490
+ .cc-name { font-weight: 600; flex: none; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
2491
+ .cc-detail { font-size: 11.5px; color: var(--cc-faint); overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
2492
+ .cc-tag {
2493
+ flex: none; padding: 0 7px; border-radius: 6px; background: var(--kglyph); color: var(--k);
2494
+ font: 500 10.5px/18px var(--cc-ui-font);
2495
+ }
2496
+ .cc-exp { width: 5px; height: 5px; border-radius: 50%; background: var(--cc-faint); flex: none; }
2386
2497
  .cc-fold {
2387
2498
  flex: none; margin-left: auto; width: 22px; height: 22px; border: 0; border-radius: 6px; padding: 0;
2388
- background: rgba(255,255,255,.14); color: #fff; font-size: 12px; line-height: 22px; cursor: pointer;
2499
+ background: transparent; color: var(--cc-faint); font-size: 12px; line-height: 22px; cursor: pointer;
2389
2500
  }
2390
- .cc-fold:hover { background: rgba(255,255,255,.28); }
2501
+ .cc-fold:hover { background: var(--cc-raised); color: var(--cc-strong); }
2391
2502
  .cc-prev {
2392
- margin: 0; padding: 5px 12px 0; white-space: pre; overflow: hidden; color: var(--cc-faint); opacity: .5;
2393
- font: 11px/15px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2394
- }
2395
- .cc-more { opacity: .7; font-style: italic; }
2396
- .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .45; }
2397
- .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: rgba(255,255,255,.06); }
2503
+ margin: 0; padding: 7px 12px 9px; white-space: pre; overflow: hidden; color: var(--cc-code);
2504
+ font: 11.5px/19px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2505
+ }
2506
+ .cc-prev .s-kw { color: var(--cc-syn-kw); }
2507
+ .cc-prev .s-str { color: var(--cc-syn-str); }
2508
+ .cc-prev .s-type { color: var(--cc-syn-type); }
2509
+ .cc-prev .s-com { color: var(--cc-syn-com); }
2510
+ .cc-prev .s-con { color: var(--cc-syn-con); }
2511
+ .k-import > .cc-prev { color: var(--cc-muted); }
2512
+ .cc-more { color: var(--cc-faintest); font-style: italic; }
2513
+ .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .55; }
2514
+ .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: transparent; }
2398
2515
  .cc-mini > i.k-class { border-color: var(--k-class); } .cc-mini > i.k-function { border-color: var(--k-function); }
2399
2516
  .cc-mini > i.k-component { border-color: var(--k-component); } .cc-mini > i.k-state { border-color: var(--k-state); }
2400
2517
  .cc-mini > i.k-variable { border-color: var(--k-variable); } .cc-mini > i.k-type { border-color: var(--k-type); }
@@ -2404,9 +2521,10 @@ var CSS = (
2404
2521
  background: var(--cc-danger); color: #fff; font: 700 11px/18px ui-sans-serif, system-ui, sans-serif; text-align: center;
2405
2522
  }
2406
2523
  .cc-block.cc-has-error { border-color: var(--cc-danger); }
2524
+ .cc-err { color: #1a0d0b; }
2407
2525
  .cc-root.lod-2 .cc-err { min-width: 34px; height: 34px; border-radius: 17px; font-size: 22px; line-height: 34px; }
2408
2526
  .cc-rename {
2409
- font: inherit; font-weight: 700; color: #fff; background: rgba(0,0,0,.35); border: 1px solid var(--cc-accent);
2527
+ font: inherit; font-weight: 600; color: var(--cc-strong); background: var(--cc-raised); border: 1px solid var(--cc-ink);
2410
2528
  border-radius: 4px; padding: 0 4px; height: 22px; outline: none; user-select: text; -webkit-user-select: text; max-width: 100%;
2411
2529
  }
2412
2530
  .cc-rename.invalid { border-color: var(--cc-danger); }
@@ -2419,10 +2537,7 @@ var CSS = (
2419
2537
  font: 900 13px/18px ui-sans-serif, system-ui, sans-serif; text-align: center;
2420
2538
  }
2421
2539
  .cc-root.lod-2 .cc-conflict-chip { min-width: 34px; height: 34px; border-radius: 17px; font-size: 24px; line-height: 34px; }
2422
- .cc-block.cc-conflict-incoming {
2423
- --k: #f0883e; --kh: #8a4a17; --kbg: rgba(240, 136, 62, .08);
2424
- border-style: dashed; pointer-events: none; opacity: .9;
2425
- }
2540
+ .cc-block.cc-conflict-incoming { --k: #f0883e; border: 1px dashed #f0883e; pointer-events: none; opacity: .9; }
2426
2541
  .cc-diff-chip.incoming { background: #f0883e; }
2427
2542
  .cc-banner.cc-banner-conflicts { border-color: #f0883e; }
2428
2543
 
@@ -2437,10 +2552,9 @@ var CSS = (
2437
2552
  .cc-diff-chip.removed { background: var(--cc-danger); }
2438
2553
  .cc-root.lod-2 .cc-diff-chip { min-width: 34px; height: 34px; border-radius: 8px; font-size: 24px; line-height: 34px; }
2439
2554
  .cc-block.cc-diff-removed {
2440
- --k: var(--cc-danger); --kh: #9c2f2f; --kbg: rgba(255, 107, 107, .10);
2441
- border-style: dashed; pointer-events: none; opacity: .9;
2555
+ --k: var(--cc-danger); border: 1px dashed var(--cc-danger); background: color-mix(in srgb, var(--cc-danger) 8%, var(--cc-node-bg));
2556
+ pointer-events: none; opacity: .9;
2442
2557
  }
2443
- .cc-root[data-theme="light"] .cc-block.cc-diff-removed { --kh: #d64545; --kbg: rgba(214, 69, 69, .08); }
2444
2558
  .cc-prev.cc-diff-prev { opacity: .95; }
2445
2559
  .cc-diff-prev > span { display: block; margin: 0 -12px; padding: 0 12px; }
2446
2560
  .cc-diff-prev .cc-line-same { opacity: .5; }
@@ -2448,23 +2562,24 @@ var CSS = (
2448
2562
  .cc-diff-prev .cc-line-removed { background: rgba(248, 81, 73, .26); text-decoration: line-through; text-decoration-color: rgba(248, 81, 73, .7); }
2449
2563
  .cc-diff-summary { padding: 0 8px; font-size: 12px; color: var(--cc-muted); white-space: nowrap; }
2450
2564
 
2451
- .cc-block.cc-sel { box-shadow: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--k); }
2452
- .cc-block.cc-editing { box-shadow: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--cc-accent); }
2453
- .cc-block.cc-dragging { box-shadow: var(--cc-shadow); opacity: .92; z-index: 9999 !important; }
2454
- .cc-root.has-hl .cc-block.cc-linked { box-shadow: 0 0 0 2px var(--k); }
2565
+ .cc-block.cc-sel { box-shadow: 0 0 0 2px var(--cc-ink), var(--cc-shadow-lg); }
2566
+ .cc-block.cc-editing { box-shadow: 0 0 0 2px var(--cc-ink), var(--cc-shadow-lg); }
2567
+ .cc-block.cc-dragging { box-shadow: var(--cc-shadow-lg); opacity: .92; z-index: 9999 !important; }
2568
+ .cc-root.has-sel .cc-block:not(.cc-related) { opacity: .38; }
2455
2569
 
2456
2570
  .cc-edges { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
2457
- .cc-edge { fill: none; stroke-width: 1.5; opacity: .22; }
2458
- .cc-edge.u-type { stroke-dasharray: 4 4; }
2459
- .cc-root.has-hl .cc-edge { opacity: .07; }
2460
- .cc-root.has-hl .cc-edge.hl { opacity: .95; stroke-width: 2.5; }
2571
+ .cc-edge { fill: none; stroke: var(--edge); stroke-width: 1.5; opacity: .5; }
2572
+ .cc-edge.to-import, .cc-edge.u-type { stroke-dasharray: 3 4; }
2573
+ .cc-root.has-hl .cc-edge { opacity: .18; }
2574
+ .cc-root.has-hl .cc-edge.hl { stroke: var(--cc-ink); opacity: .95; stroke-width: 2; }
2461
2575
 
2462
2576
  /* level of detail when zoomed out */
2463
- .cc-root.lod-1 .cc-tags { visibility: hidden; }
2577
+ .cc-root.lod-1 .cc-tag { visibility: hidden; }
2464
2578
  .cc-root.lod-2 .cc-prev, .cc-root.lod-2 .cc-detail, .cc-root.lod-2 .cc-mini { visibility: hidden; }
2465
2579
  .cc-root.lod-2 .cc-head { font-size: 26px; }
2466
- .cc-root.lod-2 .cc-kind, .cc-root.lod-2 .cc-fold { display: none; }
2580
+ .cc-root.lod-2 .cc-glyph, .cc-root.lod-2 .cc-fold, .cc-root.lod-2 .cc-tag { display: none; }
2467
2581
  .cc-root.lod-2 .cc-frame-title { font-size: 40px; line-height: 44px; top: -8px; }
2582
+ .cc-root.lod-2 .cc-frame-count { font-size: 28px; line-height: 36px; padding: 2px 12px; border-radius: 10px; }
2468
2583
 
2469
2584
  /* overlays (screen space) */
2470
2585
  .cc-banner {
@@ -2472,38 +2587,66 @@ var CSS = (
2472
2587
  padding: 8px 12px; border-radius: 10px; background: var(--cc-panel); border: 1px solid var(--cc-danger); box-shadow: var(--cc-shadow);
2473
2588
  font-size: 13px; max-width: calc(100% - 32px);
2474
2589
  }
2475
- .cc-banner[hidden], .cc-bar[hidden], .cc-refs[hidden] { display: none; }
2590
+ .cc-banner[hidden], .cc-bar[hidden], .cc-refs[hidden], .cc-selbar[hidden], .cc-menu[hidden] { display: none; }
2476
2591
  .cc-toast {
2477
2592
  position: absolute; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: 30; padding: 8px 14px; border-radius: 10px;
2478
2593
  background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow); font-size: 13px;
2479
2594
  transition: opacity .2s; pointer-events: none; max-width: calc(100% - 32px);
2480
2595
  }
2481
2596
  .cc-bar {
2482
- position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 20; display: flex; align-items: center; gap: 4px;
2483
- padding: 5px; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2597
+ position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); z-index: 20; display: flex; align-items: center; gap: 2px;
2598
+ padding: 4px; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2484
2599
  width: max-content; max-width: calc(100% - 24px); box-sizing: border-box; flex-wrap: wrap; justify-content: center;
2485
2600
  }
2486
- .cc-bar .cc-chip { padding: 0 10px 0 6px; font: 600 12px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); white-space: nowrap; display: flex; align-items: center; gap: 6px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
2487
- .cc-bar .cc-chip i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2488
2601
  .cc-btn {
2489
- border: 0; background: transparent; color: var(--cc-text); font: 500 13px/30px inherit; font-family: inherit; padding: 0 10px; border-radius: 8px;
2490
- cursor: pointer; white-space: nowrap; min-height: 32px;
2491
- }
2492
- .cc-btn:hover { background: rgba(127,127,127,.16); }
2602
+ display: inline-flex; align-items: center; justify-content: center; border: 0; background: transparent; color: var(--cc-muted);
2603
+ font: 400 13px/32px var(--cc-ui-font); padding: 0 12px; border-radius: 8px; cursor: pointer; white-space: nowrap; height: 32px; box-sizing: border-box;
2604
+ }
2605
+ .cc-btn:hover { background: var(--cc-raised); color: var(--cc-strong); }
2606
+ .cc-btn:focus-visible { outline: 2px solid var(--cc-ink); outline-offset: -2px; }
2607
+ .cc-btn.strong { color: var(--cc-strong); font-weight: 500; }
2608
+ .cc-btn.square { padding: 0; min-width: 30px; }
2609
+ .cc-btn.on { background: var(--cc-raised); color: var(--cc-strong); }
2493
2610
  .cc-btn.danger { color: var(--cc-danger); }
2494
- .cc-btn.primary { background: var(--cc-accent); color: #fff; }
2495
- .cc-language { padding: 0 8px; font: 600 11px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-muted); white-space: nowrap; }
2496
- .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 2px; flex: none; }
2611
+ .cc-btn.primary { background: var(--cc-accent); color: var(--cc-on-accent); font-weight: 600; }
2612
+ .cc-btn.primary:hover { filter: brightness(1.06); }
2613
+ .cc-zoom { min-width: 44px; text-align: center; font: 12px/32px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-muted); }
2614
+ .cc-language { padding: 0 8px; font: 500 11px/32px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-faint); white-space: nowrap; }
2615
+ .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 4px; flex: none; }
2616
+ .cc-selbar {
2617
+ position: absolute; z-index: 19; display: flex; align-items: center; gap: 2px; padding: 3px; border-radius: 10px; white-space: nowrap;
2618
+ background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2619
+ }
2620
+ .cc-selbar .cc-btn { height: 26px; line-height: 26px; padding: 0 9px; border-radius: 7px; font-size: 12px; color: var(--cc-strong); }
2621
+ .cc-selbar .cc-btn.square { padding: 0; min-width: 26px; color: var(--cc-faint); }
2622
+ .cc-selbar .cc-btn.meta { color: var(--cc-faint); font-size: 11.5px; padding: 0 8px; }
2623
+ .cc-selbar .cc-btn.danger { color: var(--cc-danger); }
2624
+ .cc-selbar .cc-btn.primary { color: var(--cc-on-accent); }
2625
+ .cc-selbar .cc-sep { height: 16px; margin: 0 2px; }
2626
+ .cc-menu {
2627
+ position: absolute; z-index: 40; display: flex; flex-direction: column; min-width: 168px; padding: 4px; border-radius: 10px;
2628
+ background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow-lg);
2629
+ }
2630
+ .cc-menu .cc-btn { justify-content: flex-start; height: 30px; line-height: 30px; padding: 0 10px; border-radius: 7px; font-size: 12.5px; color: var(--cc-text); }
2631
+ .cc-menu .cc-btn.danger { color: var(--cc-danger); }
2632
+ .cc-minimap {
2633
+ position: absolute; right: var(--cc-minimap-right, 12px); bottom: var(--cc-minimap-bottom, 12px); z-index: 18; width: 148px; height: 92px;
2634
+ box-sizing: border-box; border-radius: 10px; overflow: hidden; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); cursor: pointer;
2635
+ }
2636
+ .cc-minimap-blocks { position: absolute; left: 10px; top: 13px; width: 128px; height: 66px; }
2637
+ .cc-minimap-blocks > i { position: absolute; border-radius: 2px; background: var(--k); opacity: .55; }
2638
+ .cc-minimap-view { position: absolute; left: 10px; top: 13px; box-sizing: border-box; border: 1.5px solid var(--cc-ink); border-radius: 6px; margin: 13px 0 0 10px; pointer-events: none; }
2639
+ @media (max-width: 640px) { .cc-minimap { display: none; } }
2497
2640
  .cc-refs {
2498
2641
  position: absolute; left: 50%; bottom: 64px; transform: translateX(-50%); z-index: 21; width: min(360px, calc(100% - 24px)); max-height: 45%;
2499
2642
  overflow: auto; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow); padding: 6px;
2500
2643
  }
2501
- .cc-refs h4 { margin: 8px 8px 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--cc-muted); }
2644
+ .cc-refs h4 { margin: 8px 8px 4px; font: 600 11px/16px var(--cc-ui-font); text-transform: uppercase; letter-spacing: .06em; color: var(--cc-faint); }
2502
2645
  .cc-refs button {
2503
2646
  display: flex; width: 100%; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--cc-text); text-align: left;
2504
2647
  padding: 6px 8px; border-radius: 8px; cursor: pointer; font: 12px/18px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace);
2505
2648
  }
2506
- .cc-refs button:hover { background: rgba(127,127,127,.16); }
2649
+ .cc-refs button:hover { background: var(--cc-raised); }
2507
2650
  .cc-refs button i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2508
2651
  .cc-refs button span:last-child { margin-left: auto; color: var(--cc-muted); }
2509
2652
  .cc-refs button:disabled { cursor: default; opacity: .7; }
@@ -2549,6 +2692,17 @@ var KIND_LABEL = {
2549
2692
  namespace: "ns",
2550
2693
  statement: "code"
2551
2694
  };
2695
+ var KIND_GLYPH = {
2696
+ import: "\u2193",
2697
+ class: "C",
2698
+ function: "\u0192",
2699
+ component: "\u27E8\u27E9",
2700
+ state: "\u25C6",
2701
+ variable: "=",
2702
+ type: "T",
2703
+ namespace: "{}",
2704
+ statement: "\u25B8"
2705
+ };
2552
2706
  var DIFF_CHIPS = {
2553
2707
  added: '<span class="cc-diff-chip added" title="Added">+</span>',
2554
2708
  changed: '<span class="cc-diff-chip changed" title="Changed">~</span>',
@@ -2557,6 +2711,9 @@ var DIFF_CHIPS = {
2557
2711
  containsChanges: "",
2558
2712
  unchanged: ""
2559
2713
  };
2714
+ var MINIMAP = { width: 128, height: 66 };
2715
+ var ICON_SELECT = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M2.5 1.5l9 4.5-4 1.2-1.4 4.3z"/></svg>';
2716
+ var ICON_PAN = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M7 1.5v11M1.5 7h11M7 1.5l-2 2M7 1.5l2 2M7 12.5l-2-2M7 12.5l2-2M1.5 7l2-2M1.5 7l2 2M12.5 7l-2-2M12.5 7l-2 2"/></svg>';
2560
2717
  var LABEL_TAGS = /* @__PURE__ */ new Set(["const", "let", "var", "interface", "type", "enum", "namespace", "get", "set"]);
2561
2718
  var MIN_SCALE = 0.04;
2562
2719
  var MAX_SCALE = 4;
@@ -2573,18 +2730,28 @@ var CodeCanvas = class {
2573
2730
  this.blocksEl = el("div", "cc-blocks");
2574
2731
  this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2575
2732
  this.svg.classList.add("cc-edges");
2576
- setHTML(this.svg, `<defs>${["import", "class", "function", "component", "state", "variable", "type", "namespace", "statement"].map(
2577
- (k) => `<marker id="cc-arrow-${k}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="var(--k-${k})"/></marker>`
2578
- ).join("")}</defs>`);
2733
+ const dotMarker = (id, color) => `<marker id="${id}" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" markerUnits="userSpaceOnUse"><circle cx="4" cy="4" r="3" fill="${color}"/></marker>`;
2734
+ setHTML(this.svg, `<defs>${Object.keys(KIND_GLYPH).map((k) => dotMarker(`cc-dot-${k}`, `var(--k-${k})`)).join("")}${dotMarker("cc-dot-hl", "var(--cc-ink)")}</defs>`);
2579
2735
  this.world.append(this.frameEl, this.blocksEl, this.svg);
2580
2736
  this.banner = el("div", "cc-banner");
2581
2737
  this.banner.hidden = true;
2582
2738
  this.toastEl = el("div", "cc-toast");
2583
2739
  this.toastEl.style.opacity = "0";
2584
- this.bar = el("div", "cc-bar");
2740
+ this.bar = el("div", "cc-bar cc-dock");
2741
+ this.selectionBar = el("div", "cc-selbar");
2742
+ this.selectionBar.hidden = true;
2743
+ this.menuEl = el("div", "cc-menu");
2744
+ this.menuEl.setAttribute("role", "menu");
2745
+ this.menuEl.hidden = true;
2746
+ this.zoomLabel = el("span", "cc-zoom");
2585
2747
  this.refsEl = el("div", "cc-refs");
2586
2748
  this.refsEl.hidden = true;
2587
- root.append(this.world, this.banner, this.refsEl, this.bar, this.toastEl);
2749
+ root.append(this.world, this.banner, this.refsEl, this.bar, this.selectionBar, this.menuEl, this.toastEl);
2750
+ if (opts.minimap ?? true) {
2751
+ this.minimapEl = el("div", "cc-minimap");
2752
+ this.minimapEl.setAttribute("aria-hidden", "true");
2753
+ root.append(this.minimapEl);
2754
+ }
2588
2755
  container.appendChild(root);
2589
2756
  this.bindEvents();
2590
2757
  this.ro = new ResizeObserver(() => this.fittedToView ? this.fit(false) : this.queueCamera());
@@ -2603,7 +2770,14 @@ var CodeCanvas = class {
2603
2770
  frameEl;
2604
2771
  blocksEl;
2605
2772
  svg;
2773
+ /** The dock at the bottom. */
2606
2774
  bar;
2775
+ selectionBar;
2776
+ menuEl;
2777
+ minimapEl = null;
2778
+ minimapScale = 1;
2779
+ zoomLabel;
2780
+ tool = "select";
2607
2781
  refsEl;
2608
2782
  banner;
2609
2783
  toastEl;
@@ -2965,7 +3139,9 @@ var CodeCanvas = class {
2965
3139
  if (!g || !view) return;
2966
3140
  const f = g.frame;
2967
3141
  Object.assign(this.frameEl.style, { transform: `translate(${f.x}px,${f.y}px)`, width: f.w + "px", height: f.h + "px" });
2968
- setHTML(this.frameEl, `<div class="cc-frame-title">${esc(this.path)}</div>${view.roots.length ? "" : `<div class="cc-empty" style="left:${M.filePad}px;top:${M.filePad + M.fileHeader}px">Empty file. Double-click (or double-tap) to create a function.</div>`}`);
3142
+ const folder = this.path.includes("/") ? this.path.slice(0, this.path.lastIndexOf("/")) + " /" : "";
3143
+ const fileName2 = this.path.slice(this.path.lastIndexOf("/") + 1);
3144
+ setHTML(this.frameEl, `<div class="cc-frame-title">${folder ? `<span class="cc-frame-folder">${esc(folder)}</span>` : ""}<span class="cc-frame-file">${esc(fileName2)}</span><span class="cc-frame-count"></span></div>${view.roots.length ? "" : `<div class="cc-empty" style="left:${M.filePad}px;top:${M.filePad + M.fileHeader}px">Empty file. Double-click (or double-tap) to create a function.</div>`}`);
2969
3145
  const seen = /* @__PURE__ */ new Set();
2970
3146
  const diff = this.currentDiff();
2971
3147
  for (const geo of g.order) {
@@ -3016,6 +3192,7 @@ var CodeCanvas = class {
3016
3192
  this.renderEdges();
3017
3193
  this.updateHighlight();
3018
3194
  this.updateErrorMarkers();
3195
+ this.renderMinimap();
3019
3196
  }
3020
3197
  /** Blocks only the incoming side of a conflict has, drawn dashed where they would go. */
3021
3198
  renderIncomingBlocks() {
@@ -3096,16 +3273,18 @@ var CodeCanvas = class {
3096
3273
  }
3097
3274
  blockHTML(b, geo, mini, change, changedLines = "", isConflicted = false) {
3098
3275
  const label = blockLabel(b);
3099
- const tags = b.tags.filter((t) => t !== label && t !== "export" && (b.kind === "import" || !LABEL_TAGS.has(t))).join(" ");
3276
+ const chips = b.tags.filter((t) => t !== label && t !== "export" && (b.kind === "import" || !LABEL_TAGS.has(t)));
3100
3277
  const exp = b.tags.includes("export") ? '<span class="cc-exp" title="exported"></span>' : "";
3101
3278
  const isRemoved = change === "removed" || change === "incoming";
3102
3279
  const fold = b.children.length && !isRemoved ? `<button class="cc-fold" title="${geo.collapsed ? "Expand" : "Collapse"}">${geo.collapsed ? "\u25B8" : "\u25BE"}</button>` : "";
3103
- const prev = changedLines ? `<pre class="cc-prev cc-diff-prev">${changedLines}</pre>` : b.preview.length ? `<pre class="cc-prev">${esc(b.preview.join("\n"))}${b.previewMore ? `
3104
- <span class="cc-more">\u2026 ${b.previewMore} more line${b.previewMore > 1 ? "s" : ""}</span>` : ""}</pre>` : "";
3280
+ const more = b.previewMore ? `
3281
+ <span class="cc-more">\u2026 ${b.previewMore} more line${b.previewMore > 1 ? "s" : ""}</span>` : "";
3282
+ const prev = changedLines ? `<pre class="cc-prev cc-diff-prev">${changedLines}</pre>` : b.preview.length ? `<pre class="cc-prev">${b.preview.map(highlightLine).join("\n")}${more}</pre>` : "";
3105
3283
  const diffChip = (change ? DIFF_CHIPS[change] : "") + (isConflicted ? '<span class="cc-conflict-chip" title="Merge conflict">!</span>' : "");
3106
- return `<div class="cc-head"><span class="cc-kind">${esc(label)}</span>${exp}<span class="cc-name">${esc(b.name)}</span>${diffChip}<span class="cc-detail">${esc(
3284
+ const chipsHTML = chips.map((chip) => `<span class="cc-tag">${esc(chip)}</span>`).join("");
3285
+ return `<div class="cc-head"><span class="cc-glyph" title="${esc(label)}">${KIND_GLYPH[b.kind]}</span><span class="cc-name">${esc(b.name)}</span>${exp}${diffChip}<span class="cc-detail">${esc(
3107
3286
  b.detail
3108
- )}</span>${tags ? `<span class="cc-tags">${esc(tags)}</span>` : ""}${fold}</div>${prev}${mini ? `<div class="cc-mini">${mini}</div>` : ""}`;
3287
+ )}</span>${chipsHTML}${fold}</div>${prev}${mini ? `<div class="cc-mini">${mini}</div>` : ""}`;
3109
3288
  }
3110
3289
  /** Faint miniature of a collapsed block's nested blocks. */
3111
3290
  miniMap(geo) {
@@ -3150,34 +3329,41 @@ var CodeCanvas = class {
3150
3329
  this.svg.appendChild(p);
3151
3330
  e = { key, from: w.from.id, to: w.to.id, el: p };
3152
3331
  }
3153
- e.el.setAttribute("class", `cc-edge u-${w.usage}`);
3154
- e.el.setAttribute("stroke", `var(--k-${w.kind})`);
3155
- e.el.setAttribute("marker-end", `url(#cc-arrow-${w.kind})`);
3332
+ e.el.setAttribute("class", `cc-edge u-${w.usage}${w.kind === "import" ? " to-import" : ""}`);
3333
+ e.el.style.setProperty("--edge", `var(--k-${w.kind})`);
3334
+ e.el.dataset.kind = w.kind;
3156
3335
  e.el.setAttribute("d", edgePath(w.from, w.to));
3157
3336
  next.push(e);
3158
3337
  }
3159
3338
  for (const e of old.values()) e.el.remove();
3160
3339
  this.edges = next;
3340
+ const counter = this.frameEl.querySelector(".cc-frame-count");
3341
+ if (counter) counter.textContent = `${plural(view.blocks.size, "node")} \xB7 ${plural(next.length, "link")}`;
3161
3342
  }
3162
3343
  updateHighlight() {
3163
3344
  const h = this.selected ?? this.hovered;
3164
3345
  const inH = (id) => !!h && (id === h || id.startsWith(h + "/"));
3346
+ const isAroundSelection = (id) => !!this.selected && (inH(id) || this.selected.startsWith(id + "/"));
3165
3347
  const linked = /* @__PURE__ */ new Set();
3166
3348
  for (const e of this.edges) {
3167
3349
  const on = inH(e.from) || inH(e.to);
3168
3350
  e.el.classList.toggle("hl", on);
3351
+ e.el.setAttribute("marker-end", on ? "url(#cc-dot-hl)" : `url(#cc-dot-${e.el.dataset.kind})`);
3169
3352
  if (on) {
3170
3353
  linked.add(e.from);
3171
3354
  linked.add(e.to);
3172
3355
  }
3173
3356
  }
3174
3357
  this.root.classList.toggle("has-hl", !!h);
3358
+ this.root.classList.toggle("has-sel", !!this.selected);
3175
3359
  for (const [id, { el: e }] of this.els) {
3176
3360
  e.classList.toggle("cc-sel", id === this.selected);
3177
3361
  e.classList.toggle("cc-linked", linked.has(id) && !inH(id));
3362
+ e.classList.toggle("cc-related", isAroundSelection(id) || linked.has(id));
3178
3363
  e.classList.toggle("cc-editing", !!this.editing && id === this.editing.id);
3179
3364
  }
3180
3365
  if (this.refsOpen) this.renderRefs();
3366
+ this.placeSelectionBar();
3181
3367
  }
3182
3368
  // ---------------------------------------------------------------- camera
3183
3369
  queueCamera() {
@@ -3191,7 +3377,7 @@ var CodeCanvas = class {
3191
3377
  applyCamera() {
3192
3378
  const { x, y, s } = this.cam;
3193
3379
  this.world.style.transform = `translate(${x}px,${y}px) scale(${s})`;
3194
- const grid = 24 * s;
3380
+ const grid = 22 * s;
3195
3381
  this.root.style.backgroundSize = s < 0.25 ? "0 0" : `${grid}px ${grid}px`;
3196
3382
  this.root.style.backgroundPosition = `${x}px ${y}px`;
3197
3383
  const lod = s < 0.3 ? 2 : s < 0.55 ? 1 : 0;
@@ -3203,6 +3389,9 @@ var CodeCanvas = class {
3203
3389
  this.root.classList.add("cc-moving");
3204
3390
  clearTimeout(this.movingTimer);
3205
3391
  this.movingTimer = window.setTimeout(() => this.root.classList.remove("cc-moving"), 160);
3392
+ this.zoomLabel.textContent = Math.round(s * 100) + "%";
3393
+ this.placeSelectionBar();
3394
+ this.placeMinimapView();
3206
3395
  this.opts.onCameraChange?.(s);
3207
3396
  }
3208
3397
  setCamera(c) {
@@ -3276,6 +3465,19 @@ var CodeCanvas = class {
3276
3465
  on(r, "pointercancel", (e) => this.onUp(e, true));
3277
3466
  on(r, "wheel", (e) => this.onWheel(e), { passive: false });
3278
3467
  on(r, "keydown", (e) => this.onKey(e));
3468
+ on(r, "contextmenu", (e) => {
3469
+ const id = e.target.closest?.(".cc-block")?.getAttribute("data-id");
3470
+ if (!id || this.isOverlay(e.target) || this.stale) return;
3471
+ e.preventDefault();
3472
+ this.openBlockMenu(id, this.local(e));
3473
+ });
3474
+ on(this.menuEl, "keydown", (e) => {
3475
+ if (e.key === "Escape") {
3476
+ e.stopPropagation();
3477
+ this.closeMenu();
3478
+ this.root.focus({ preventScroll: true });
3479
+ }
3480
+ });
3279
3481
  on(r, "pointerover", (e) => {
3280
3482
  if (e.pointerType !== "mouse" || this.gesture) return;
3281
3483
  const id = e.target.closest?.(".cc-block")?.getAttribute("data-id") ?? null;
@@ -3297,9 +3499,11 @@ var CodeCanvas = class {
3297
3499
  on(doc, "paste", (e) => this.onClipboard(e, "paste"));
3298
3500
  }
3299
3501
  isOverlay(t) {
3300
- return !!t?.closest?.(".cc-editor, .cc-bar, .cc-refs, .cc-banner, .cc-rename");
3502
+ return !!t?.closest?.(".cc-editor, .cc-bar, .cc-selbar, .cc-menu, .cc-minimap, .cc-refs, .cc-banner, .cc-rename");
3301
3503
  }
3302
3504
  onDown(e) {
3505
+ if (!e.target?.closest?.(".cc-menu")) this.closeMenu();
3506
+ if (e.target?.closest?.(".cc-minimap")) return this.moveToMinimapPoint(e);
3303
3507
  if (this.isOverlay(e.target)) return;
3304
3508
  if (e.pointerType === "mouse" && e.button !== 0 && e.button !== 1) return;
3305
3509
  this.root.focus({ preventScroll: true });
@@ -3313,7 +3517,7 @@ var CodeCanvas = class {
3313
3517
  }
3314
3518
  if (this.pointers.size > 2) return;
3315
3519
  const p = this.local(e);
3316
- if (e.button === 1) {
3520
+ if (e.button === 1 || this.tool === "pan") {
3317
3521
  this.gesture = { t: "pan", id: e.pointerId, sx: p.x, sy: p.y, cam: { ...this.cam } };
3318
3522
  return;
3319
3523
  }
@@ -3812,63 +4016,180 @@ var CodeCanvas = class {
3812
4016
  }
3813
4017
  // ---------------------------------------------------------------- toolbar & references
3814
4018
  renderBar() {
3815
- const bar = this.bar;
4019
+ this.renderDock();
4020
+ this.renderSelectionBar();
4021
+ this.renderRefs();
4022
+ }
4023
+ /** The dock at the bottom: tools, file actions, zoom and the language. */
4024
+ renderDock() {
4025
+ const dock = this.bar;
4026
+ dock.replaceChildren();
4027
+ dock.hidden = !!this.editing;
4028
+ const toolButton = (tool, label, icon) => {
4029
+ const toolBtn = iconButton(label, icon, () => this.setTool(tool));
4030
+ toolBtn.classList.toggle("on", this.tool === tool);
4031
+ toolBtn.setAttribute("aria-pressed", String(this.tool === tool));
4032
+ return toolBtn;
4033
+ };
4034
+ dock.append(toolButton("select", "Select", ICON_SELECT), toolButton("pan", "Pan", ICON_PAN), el("div", "cc-sep"));
4035
+ const firstErrorBlock = this.diagnostics.find((d) => d.blockId)?.blockId;
4036
+ if (firstErrorBlock) dock.append(button(plural(this.diagnostics.length, "error"), () => this.showErrorsOf(firstErrorBlock), "danger"));
4037
+ const diff = this.currentDiff();
4038
+ if (diff) dock.append(changesSummary(diff), el("div", "cc-sep"));
4039
+ dock.append(button("+ Function", () => this.createFunctionAtCenter(), "strong"));
4040
+ if (this.clip) dock.append(button("Paste", () => (this.lastPointer = null, this.paste())));
4041
+ dock.append(
4042
+ this.layoutBeforeArrange ? button("Undo arrange", () => this.undoArrange()) : button("Arrange", () => this.arrange()),
4043
+ button("Fit", () => this.fit()),
4044
+ el("div", "cc-sep"),
4045
+ button("\u2212", () => this.zoomBy(0.8), "square"),
4046
+ this.zoomLabel,
4047
+ button("+", () => this.zoomBy(1.25), "square"),
4048
+ ...this.languageLabelParts()
4049
+ );
4050
+ this.zoomLabel.title = "Zoom";
4051
+ }
4052
+ /** The bar above the selected block: what it uses and what uses it, and actions on it. */
4053
+ renderSelectionBar() {
4054
+ const bar = this.selectionBar;
3816
4055
  bar.replaceChildren();
3817
- bar.hidden = !!this.editing;
3818
4056
  const b = this.selected ? this.view?.blocks.get(this.selected) : null;
3819
- if (b) {
3820
- const chip = el("div", "cc-chip");
3821
- chip.style.setProperty("--k", `var(--k-${b.kind})`);
3822
- setHTML(chip, `<i></i>${esc(b.name)}`);
3823
- const refs = this.refsFor(b.id);
4057
+ bar.hidden = !b || !!this.editing;
4058
+ if (!b) return;
4059
+ const refs = this.refsFor(b.id);
4060
+ const usedBy = `used by ${refs.incoming.length}`;
4061
+ const refsToggle = button(refs.outgoing.length ? `${plural(refs.outgoing.length, "dep")} \xB7 ${usedBy}` : usedBy, () => this.toggleRefs(), "meta");
4062
+ refsToggle.classList.add("cc-refs-toggle");
4063
+ refsToggle.title = "References";
4064
+ refsToggle.setAttribute("aria-expanded", String(this.refsOpen));
4065
+ const close = button("\xD7", () => this.select(null), "square");
4066
+ close.setAttribute("aria-label", "Clear selection");
4067
+ if (this.conflicts) {
4068
+ const isConflicted = this.conflicts.hunksByBlock.has(b.id);
4069
+ const firstHunk = isConflicted ? this.conflicts.hunks[this.conflicts.hunksByBlock.get(b.id)[0]] : void 0;
4070
+ const keep = (label, choice, side) => {
4071
+ const keepButton = button(label, () => this.keepConflictSide(b.id, choice), choice === "current" ? "primary" : void 0);
4072
+ if (side) keepButton.title = side;
4073
+ return keepButton;
4074
+ };
4075
+ const keepButtons = firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [];
4076
+ bar.append(refsToggle, el("div", "cc-sep"), ...keepButtons, button("Edit", () => this.openEditor(b.id)), this.moreButton(b), close);
4077
+ } else {
3824
4078
  const errors = this.diagnosticsInside(b.id);
3825
- if (this.conflicts) {
3826
- const isConflicted = this.conflicts.hunksByBlock.has(b.id);
3827
- const firstHunk = isConflicted ? this.conflicts.hunks[this.conflicts.hunksByBlock.get(b.id)[0]] : void 0;
3828
- const keep = (label, choice, side) => {
3829
- const keepButton = button(label, () => this.keepConflictSide(b.id, choice), choice === "current" ? "primary" : void 0);
3830
- if (side) keepButton.title = side;
3831
- return keepButton;
3832
- };
3833
- bar.append(
3834
- chip,
3835
- el("div", "cc-sep"),
3836
- ...firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [],
3837
- button("Edit", () => this.openEditor(b.id)),
3838
- button("Copy", () => this.copy(b.id)),
3839
- ...this.languageLabelParts()
3840
- );
3841
- return this.renderRefs();
3842
- }
3843
4079
  bar.append(
3844
- chip,
4080
+ refsToggle,
3845
4081
  el("div", "cc-sep"),
3846
4082
  ...errors.length ? [button(plural(errors.length, "error"), () => this.toggleRefs(), "danger")] : [],
3847
- button("Edit", () => this.openEditor(b.id)),
4083
+ ...this.opts.onOpenCode ? [button("Open code", () => this.opts.onOpenCode(b))] : [],
4084
+ ...this.opts.onAsk ? [button("Ask", () => this.opts.onAsk(b))] : [],
3848
4085
  ...isRenamable(b) ? [button("Rename", () => this.startRename(b.id))] : [],
3849
- button(`Refs ${refs.incoming.length + refs.outgoing.length}`, () => this.toggleRefs()),
3850
- button("Copy", () => this.copy(b.id)),
3851
- ...b.kind !== "import" && b.kind !== "statement" ? [button("Duplicate", () => this.duplicate(b.id))] : [],
3852
- ...b.insert ? [button("+ Inside", () => this.createFunctionAtCenter())] : [],
3853
- button("Delete", () => this.deleteBlock(b.id), "danger")
3854
- );
3855
- } else {
3856
- const firstErrorBlock = this.diagnostics.find((d) => d.blockId)?.blockId;
3857
- if (firstErrorBlock) bar.append(button(plural(this.diagnostics.length, "error"), () => this.showErrorsOf(firstErrorBlock), "danger"));
3858
- const diff = this.currentDiff();
3859
- if (diff) bar.append(changesSummary(diff), el("div", "cc-sep"));
3860
- bar.append(button("+ Function", () => this.createFunctionAtCenter()));
3861
- if (this.clip) bar.append(button("Paste", () => (this.lastPointer = null, this.paste())));
3862
- bar.append(
3863
- this.layoutBeforeArrange ? button("Undo arrange", () => this.undoArrange()) : button("Arrange", () => this.arrange()),
3864
- el("div", "cc-sep"),
3865
- button("Fit", () => this.fit()),
3866
- button("\u2212", () => this.zoomBy(0.8)),
3867
- button("+", () => this.zoomBy(1.25))
4086
+ button("Delete", () => this.deleteBlock(b.id), "danger"),
4087
+ this.moreButton(b),
4088
+ close
3868
4089
  );
3869
4090
  }
3870
- bar.append(...this.languageLabelParts());
3871
- this.renderRefs();
4091
+ this.placeSelectionBar();
4092
+ }
4093
+ moreButton(b) {
4094
+ const more = button("\u22EF", () => {
4095
+ const box = more.getBoundingClientRect();
4096
+ const rootBox = this.root.getBoundingClientRect();
4097
+ this.openBlockMenu(b.id, { x: box.left - rootBox.left, y: box.bottom - rootBox.top + 4 });
4098
+ }, "square");
4099
+ more.setAttribute("aria-label", "More actions");
4100
+ more.setAttribute("aria-haspopup", "menu");
4101
+ return more;
4102
+ }
4103
+ /** The block's actions as a menu, from ⋯ or a right-click. */
4104
+ openBlockMenu(id, at) {
4105
+ const b = this.view?.blocks.get(id);
4106
+ if (!b) return;
4107
+ if (this.selected !== id) this.select(id);
4108
+ const item = (label, action, variant) => button(label, () => (this.closeMenu(), action()), variant);
4109
+ const items = [
4110
+ item("Edit", () => this.openEditor(id)),
4111
+ ...this.opts.onOpenCode ? [item("Open code", () => this.opts.onOpenCode(b))] : [],
4112
+ ...this.opts.onAsk ? [item("Ask", () => this.opts.onAsk(b))] : [],
4113
+ ...isRenamable(b) && !this.conflicts ? [item("Rename", () => this.startRename(id))] : [],
4114
+ item("References", () => (this.refsOpen = true, this.renderBar())),
4115
+ item("Copy", () => this.copy(id)),
4116
+ ...b.kind !== "import" && b.kind !== "statement" && !this.conflicts ? [item("Duplicate", () => this.duplicate(id))] : [],
4117
+ ...b.insert && !this.conflicts ? [item("+ Inside", () => this.createFunctionAtCenter())] : [],
4118
+ ...!this.conflicts ? [item("Delete", () => this.deleteBlock(id), "danger")] : []
4119
+ ];
4120
+ const menu = this.menuEl;
4121
+ menu.replaceChildren(...items);
4122
+ menu.hidden = false;
4123
+ const left = clamp2(at.x, 8, Math.max(8, this.root.clientWidth - menu.offsetWidth - 8));
4124
+ const top = clamp2(at.y, 8, Math.max(8, this.root.clientHeight - menu.offsetHeight - 8));
4125
+ Object.assign(menu.style, { left: left + "px", top: top + "px" });
4126
+ items[0]?.focus({ preventScroll: true });
4127
+ }
4128
+ closeMenu() {
4129
+ this.menuEl.hidden = true;
4130
+ }
4131
+ /** Keep the selection bar right above the selected block (below it when there is no room). */
4132
+ placeSelectionBar() {
4133
+ const bar = this.selectionBar;
4134
+ const geo = this.selected ? this.geom?.geo.get(this.selected) : void 0;
4135
+ if (bar.hidden || !geo || geo.hidden) {
4136
+ bar.style.visibility = "hidden";
4137
+ return;
4138
+ }
4139
+ bar.style.visibility = "";
4140
+ const { x, y, s } = this.cam;
4141
+ const blockTop = geo.ay * s + y;
4142
+ const blockBottom = (geo.ay + geo.h) * s + y;
4143
+ const barHeight = bar.offsetHeight || 34;
4144
+ const fitsAbove = blockTop - barHeight - 8 >= 8;
4145
+ const left = clamp2(geo.ax * s + x, 8, Math.max(8, this.root.clientWidth - bar.offsetWidth - 8));
4146
+ bar.style.left = left + "px";
4147
+ bar.style.top = (fitsAbove ? blockTop - barHeight - 8 : blockBottom + 8) + "px";
4148
+ }
4149
+ setTool(tool) {
4150
+ this.tool = tool;
4151
+ this.root.classList.toggle("cc-tool-pan", tool === "pan");
4152
+ this.renderDock();
4153
+ }
4154
+ /** The whole file in small, in a corner; the visible part outlined. A click moves there. */
4155
+ renderMinimap() {
4156
+ const g = this.geom;
4157
+ const minimap = this.minimapEl;
4158
+ if (!minimap || !g) return;
4159
+ const f = g.frame;
4160
+ const scale = Math.min(MINIMAP.width / f.w, MINIMAP.height / f.h);
4161
+ this.minimapScale = scale;
4162
+ const bars = g.order.filter((geo) => !geo.hidden && geo.block.depth <= 1).map(
4163
+ (geo) => `<i style="--k:var(--k-${geo.block.kind});left:${((geo.ax - f.x) * scale).toFixed(1)}px;top:${((geo.ay - f.y) * scale).toFixed(1)}px;width:${Math.max(2, geo.w * scale).toFixed(1)}px;height:${Math.max(
4164
+ 2,
4165
+ geo.h * scale
4166
+ ).toFixed(1)}px"></i>`
4167
+ ).join("");
4168
+ setHTML(minimap, `<div class="cc-minimap-blocks">${bars}</div><span class="cc-minimap-view"></span>`);
4169
+ this.placeMinimapView();
4170
+ }
4171
+ placeMinimapView() {
4172
+ const g = this.geom;
4173
+ const view = this.minimapEl?.querySelector(".cc-minimap-view");
4174
+ if (!g || !view) return;
4175
+ const { x, y, s } = this.cam;
4176
+ const f = g.frame;
4177
+ const scale = this.minimapScale;
4178
+ const left = clamp2((-x / s - f.x) * scale, -2, MINIMAP.width);
4179
+ const top = clamp2((-y / s - f.y) * scale, -2, MINIMAP.height);
4180
+ const right = clamp2(((this.root.clientWidth - x) / s - f.x) * scale, left, MINIMAP.width + 2);
4181
+ const bottom = clamp2(((this.root.clientHeight - y) / s - f.y) * scale, top, MINIMAP.height + 2);
4182
+ Object.assign(view.style, { left: left + "px", top: top + "px", width: right - left + "px", height: bottom - top + "px" });
4183
+ }
4184
+ moveToMinimapPoint(e) {
4185
+ const g = this.geom;
4186
+ const inner = this.minimapEl?.querySelector(".cc-minimap-blocks");
4187
+ if (!g || !inner) return;
4188
+ const box = inner.getBoundingClientRect();
4189
+ const worldX = g.frame.x + (e.clientX - box.left) / this.minimapScale;
4190
+ const worldY = g.frame.y + (e.clientY - box.top) / this.minimapScale;
4191
+ const s = this.cam.s;
4192
+ this.moveCamera({ x: this.root.clientWidth / 2 - worldX * s, y: this.root.clientHeight / 2 - worldY * s, s }, true);
3872
4193
  }
3873
4194
  languageLabelParts() {
3874
4195
  if (!this.view) return [];
@@ -4030,6 +4351,13 @@ function el(tag, cls) {
4030
4351
  if (cls) e.className = cls;
4031
4352
  return e;
4032
4353
  }
4354
+ function iconButton(label, icon, onClick) {
4355
+ const iconBtn = button("", onClick, "square");
4356
+ setHTML(iconBtn, icon);
4357
+ iconBtn.setAttribute("aria-label", label);
4358
+ iconBtn.title = label;
4359
+ return iconBtn;
4360
+ }
4033
4361
  function button(label, onClick, variant) {
4034
4362
  const b = keepsCanvasFocus(document.createElement("button"));
4035
4363
  b.className = "cc-btn" + (variant ? " " + variant : "");
@@ -4044,9 +4372,6 @@ function keepsCanvasFocus(element) {
4044
4372
  element.addEventListener("pointerdown", (e) => e.preventDefault());
4045
4373
  return element;
4046
4374
  }
4047
- function esc(s) {
4048
- return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4049
- }
4050
4375
  function clamp2(v, lo, hi) {
4051
4376
  return Math.min(hi, Math.max(lo, v));
4052
4377
  }