@enigmax/dashboard 0.1.109 → 0.1.111

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/assets/index.html CHANGED
@@ -651,12 +651,15 @@
651
651
  .modal-back[hidden] { display: none; }
652
652
  .modal-back.pal-back { align-items: flex-start; padding-top: 12vh; }
653
653
  .modal.pal { max-width: 520px; padding: 12px; }
654
+ /* WebKit's own clear cross, on every search field. Where we want one we draw it
655
+ ourselves, so the platform's is a second cross for WebKit users only. */
656
+ input[type="search"]::-webkit-search-cancel-button,
657
+ input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
654
658
  .pal-input {
655
659
  width: 100%; background: transparent; color: var(--text); border: none; border-bottom: 1px solid var(--border);
656
660
  padding: 8px 6px 12px; font-size: 14px; font-family: var(--mono);
657
661
  }
658
662
  .pal-input:focus { outline: none; }
659
- .pal-input::-webkit-search-cancel-button { display: none; }
660
663
  .pal-list { max-height: 46vh; overflow-y: auto; overscroll-behavior: contain; margin-top: 8px; }
661
664
  .pal-gt {
662
665
  font-family: var(--mono); font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.4px;
@@ -671,7 +674,7 @@
671
674
  .pal-row .pal-hint { margin-left: auto; font-family: var(--mono); font-size: 11px; opacity: .6; }
672
675
  .pal-row.sel { background: var(--surface2); color: var(--text); }
673
676
  .pal-row.sel .tab-ic { opacity: 1; color: var(--accent); }
674
- .pal-empty { padding: 18px 8px; color: var(--muted); font-size: 13px; }
677
+ .pal-empty { padding: 18px 8px; color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }
675
678
  .pal-foot {
676
679
  display: flex; gap: 14px; padding: 10px 8px 2px; border-top: 1px solid var(--border-soft);
677
680
  font-family: var(--mono); font-size: 10.5px; color: var(--muted); opacity: .7;
@@ -759,6 +762,30 @@
759
762
  rather than a second label competing with the name. */
760
763
  .set-def{opacity:.72;font-variant-numeric:tabular-nums;white-space:nowrap}
761
764
  .set-ctl .gate-reset{flex:none}
765
+
766
+ /* --- Code graph canvas -------------------------------------------------------
767
+ The picture is drawn on one canvas: at a few hundred nodes an SVG/DOM node per
768
+ symbol costs more in layout than the whole simulation does in maths, and the
769
+ label/zoom rules below need per-frame control the DOM cannot give. */
770
+ .cg-wrap { position: relative; height: 460px; margin-top: 10px; border: 1px solid var(--border); border-radius: 10px; background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.03), transparent 70%), var(--surface2); overflow: hidden; }
771
+ .cg-wrap canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
772
+ .cg-wrap canvas.grabbing { cursor: grabbing; }
773
+ .cg-wrap canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
774
+ .cg-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; padding: 20px; color: var(--muted); font-size: 12.5px; line-height: 1.6; background: rgba(8,10,16,0.55); }
775
+ /* `display: flex` outranks the UA's [hidden] rule, so without this the dismissed overlay
776
+ keeps covering the canvas and swallows every click, drag and wheel event on it. */
777
+ .cg-overlay[hidden], .cg-detail[hidden] { display: none; }
778
+ .cg-zoom { position: absolute; right: 10px; bottom: 10px; display: flex; flex-direction: column; gap: 4px; }
779
+ .cg-zoom .iconbtn { background: rgba(8,10,16,0.72); border: 1px solid var(--border); border-radius: 7px; padding: 5px; }
780
+ .cg-legend { position: absolute; left: 10px; top: 10px; display: flex; flex-wrap: wrap; gap: 4px 10px; max-width: 62%; pointer-events: none; }
781
+ .cg-legend span { display: inline-flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
782
+ .cg-legend i { width: 8px; height: 8px; border-radius: 50%; display: block; }
783
+ .cg-detail { position: absolute; right: 10px; top: 10px; width: min(300px, 62%); max-height: calc(100% - 70px); overflow: auto; background: rgba(8,10,16,0.9); border: 1px solid var(--border); border-radius: 9px; padding: 10px 11px; }
784
+ .cg-detail h4 { margin: 0 6px 2px 0; font-size: 12.5px; font-family: var(--mono); color: var(--text); word-break: break-all; }
785
+ .cg-detail .cg-sig { font-family: var(--mono); font-size: 10.5px; color: var(--muted); white-space: pre-wrap; word-break: break-word; margin: 6px 0 0; }
786
+ .cg-detail .cg-where { font-family: var(--mono); font-size: 10.5px; color: var(--accent2); word-break: break-all; }
787
+ .cg-detail .cg-acts { display: flex; gap: 6px; margin-top: 9px; flex-wrap: wrap; }
788
+ .cg-detail .cg-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 6px; }
762
789
  </style>
763
790
  </head>
764
791
  <body>
@@ -1345,6 +1372,36 @@
1345
1372
  <button id="cgRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
1346
1373
  </div>
1347
1374
  <div id="cgNote" class="set-note"></div>
1375
+ <div class="panel">
1376
+ <div class="set-cat-h">Graph</div>
1377
+ <div class="set-hint" style="padding:4px 0">What depends on what. Click a node for its signature and location, double-click to pull in its neighbours. Empty focus draws the code the rest of the repo leans on most.</div>
1378
+ <div class="ladd" style="flex-wrap:wrap;align-items:center">
1379
+ <input id="cgFocus" class="linput" placeholder="centre on a symbol or file (optional)" style="flex:1;min-width:220px">
1380
+ <select id="cgScope" aria-label="What the nodes are">
1381
+ <option value="symbols">Symbols</option>
1382
+ <option value="files">Files</option>
1383
+ </select>
1384
+ <select id="cgDepth" aria-label="Hops from the focus">
1385
+ <option value="1">1 hop</option>
1386
+ <option value="2">2 hops</option>
1387
+ <option value="3">3 hops</option>
1388
+ <option value="4">4 hops</option>
1389
+ </select>
1390
+ <button id="cgDraw" type="button" class="toggle on">Draw</button>
1391
+ </div>
1392
+ <div id="cgWrap" class="cg-wrap">
1393
+ <canvas id="cgCanvas" tabindex="0" role="img" aria-label="Code graph"></canvas>
1394
+ <div class="cg-legend" id="cgLegend"></div>
1395
+ <div class="cg-zoom">
1396
+ <button id="cgZoomIn" type="button" class="iconbtn" title="Zoom in" aria-label="Zoom in"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
1397
+ <button id="cgZoomOut" type="button" class="iconbtn" title="Zoom out" aria-label="Zoom out"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"/></svg></button>
1398
+ <button id="cgFit" type="button" class="iconbtn" title="Fit to view" aria-label="Fit to view"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M16 21h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg></button>
1399
+ </div>
1400
+ <div id="cgDetail" class="cg-detail" hidden></div>
1401
+ <div id="cgGraphOverlay" class="cg-overlay">Drawing...</div>
1402
+ </div>
1403
+ <div id="cgGraphMeta" class="set-hint" style="padding:6px 0 0"></div>
1404
+ </div>
1348
1405
  <div class="panel">
1349
1406
  <div class="set-cat-h">Index a project</div>
1350
1407
  <div class="set-hint" style="padding:4px 0">Build the graph for a codebase. Leave the path empty to index enigma's own working directory.</div>
@@ -2415,6 +2472,11 @@ export {
2415
2472
  };
2416
2473
  </script>
2417
2474
  <script src="/lib/chart.min.js"></script>
2475
+ <!-- The framework-agnostic half of @enigmax/primitives, vendored beside Chart.js by
2476
+ scripts/sync-primitives.mjs. The dashboard writes its own DOM; what it takes from
2477
+ the package is the search engine, the keyboard arithmetic, the grouping and the
2478
+ history - all of which it used to hand-roll. -->
2479
+ <script src="/lib/enigma-primitives.js"></script>
2418
2480
  <script>
2419
2481
  const POLL_MS = 5000;
2420
2482
  const DAY = 86400000;
@@ -2481,7 +2543,7 @@ export {
2481
2543
  "cli": "CLI (enigma compress)", "unknown": "Unattributed",
2482
2544
  };
2483
2545
  function prettySource(k) { return SOURCE_LABELS[k] || String(k).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); }
2484
- const TYPE_LABELS = { json: "JSON", log: "Logs", text: "Text", code: "Code", diff: "Diffs", markdown: "Markdown", unknown: "Other" };
2546
+ const TYPE_LABELS = { json: "JSON", log: "Logs", text: "Text", code: "Code", diff: "Diffs", markdown: "Markdown", shell: "Command output", unknown: "Other" };
2485
2547
  function prettyType(k) { return TYPE_LABELS[k] || String(k).replace(/\b\w/g, (c) => c.toUpperCase()); }
2486
2548
 
2487
2549
  // One bar per key (source or type), sorted by tokens saved, with an optional $ column.
@@ -4566,7 +4628,7 @@ export {
4566
4628
  : "";
4567
4629
  return '<div class="gate-run"><div class="gate-run-h">'
4568
4630
  + '<span class="gate-run-b">' + esc(r.branch) + "</span>"
4569
- + '<span class="cstatus ' + (r.status === "completed" ? "ok" : r.status === "running" ? "orange" : r.status === "failed" ? "bad" : "") + '"><span class="cdot"></span>' + esc(r.status) + (r.awaitingAgent ? " · parked" : "") + "</span>"
4631
+ + '<span class="cstatus ' + (r.status === "completed" ? "ok" : r.status === "running" ? "orange" : r.status === "failed" ? "bad" : "") + '"><span class="cdot"></span>' + esc(r.status) + (r.blockedReason ? " · needs you: " + esc(r.blockedReason) : r.awaitingAgent ? " · parked" : "") + "</span>"
4570
4632
  + '<span class="gate-run-meta">' + esc(r.headSha) + pr + "</span>"
4571
4633
  + '<span style="flex:1"></span>' + stop
4572
4634
  + "</div>" + '<div class="gate-steps">' + steps + "</div>"
@@ -4998,30 +5060,41 @@ export {
4998
5060
  // Palette"). enigma: the dashboard is a single dependency-free HTML file served over
4999
5061
  // loopback, so the ~20-line matcher below stands in for fuse.js - upgrade to fuse.js
5000
5062
  // if this page ever gains a build step.
5063
+ // Guarded rather than destructured outright: this page is ONE script, so a missing
5064
+ // vendored file would throw here and take every panel below it with it - which is
5065
+ // exactly how the README's preview render came out with "-" in every card. Only the
5066
+ // four things called at MODULE scope need a stand-in; the rest are called from
5067
+ // handlers, where a failure is contained to the palette that is already degraded.
5068
+ const primitives = window.EnigmaPrimitives || {};
5069
+ const { moveActive, groupRows, isPaletteShortcut } = primitives;
5070
+ const subsequenceMatcher = primitives.subsequenceMatcher || (() => undefined);
5071
+ // The empty state quotes what was typed, and what was typed is arbitrary: a pasted
5072
+ // string with no spaces in it has no break opportunity, so it runs out of the panel.
5073
+ const shortenQuery = primitives.shortenQuery || ((value) => value);
5074
+ const createSearch = primitives.createSearch || (() => ({ setItems() {}, search() {}, searchNow() {}, update() {}, destroy() {} }));
5075
+ const createRecentStore = primitives.createRecentStore || (() => ({ list: () => [], remember: () => [], forget: () => [], clear() {} }));
5076
+ const shortcutLabel = primitives.shortcutLabel || ((key) => "Ctrl " + key);
5001
5077
  const palBack = $("palBack"), palInput = $("palInput"), palList = $("palList");
5002
- const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
5003
5078
  let palItems = [], palSel = 0, palReturnFocus = null;
5004
- $("palHint").textContent = IS_MAC ? "Cmd K" : "Ctrl K";
5079
+ $("palHint").textContent = shortcutLabel("K");
5005
5080
 
5006
5081
  /**
5007
- * Fuzzy rank of `query` inside `text`: -1 when the query is not a subsequence, otherwise
5008
- * a score that rewards consecutive runs and word starts. Beats a substring filter because
5009
- * "qgate" finds "Quality gate" and "plyg" finds "Playground", which .includes() cannot.
5082
+ * The palette's engine, from @enigmax/primitives: subsequence ranking (so "qgate"
5083
+ * finds "Quality gate" and "plyg" finds "Playground"), the debounce and the
5084
+ * cancellation. The scorer this replaced is the one the package now ships, so the
5085
+ * dashboard and every other consumer rank the same way.
5010
5086
  */
5011
- function palScore(query, text) {
5012
- const q = query.toLowerCase(), t = text.toLowerCase();
5013
- if (!q) return 0;
5014
- let at = 0, from = 0, score = 0, run = 0;
5015
- for (let i = 0; i < q.length; i++) {
5016
- at = t.indexOf(q[i], from);
5017
- if (at === -1) return -1;
5018
- run = (i > 0 && at === from) ? run + 1 : 0;
5019
- const wordStart = at === 0 || /[^a-z0-9]/.test(t[at - 1]);
5020
- score += 10 + run * 6 + (wordStart ? 8 : 0) - Math.min(at - from, 6);
5021
- from = at + 1;
5087
+ const palRecents = createRecentStore({ key: "enigma:dashboard:palette", limit: 5 });
5088
+ const palSearch = createSearch({
5089
+ keys: ["name", "kind"],
5090
+ matcher: subsequenceMatcher(["name", "kind"]),
5091
+ debounce: 0,
5092
+ onResults: (matches) => {
5093
+ palItems = matches.map((match) => match.item);
5094
+ palSel = 0;
5095
+ palPaint();
5022
5096
  }
5023
- return score;
5024
- }
5097
+ });
5025
5098
 
5026
5099
  /** Built from the live DOM on every open, so it can never drift from the rail or the scopes. */
5027
5100
  function palIndex() {
@@ -5051,36 +5124,59 @@ export {
5051
5124
 
5052
5125
  function palRender(query) {
5053
5126
  const q = query.trim();
5054
- const all = palIndex();
5055
- palItems = (!q ? all : all
5056
- .map((it) => ({ it, s: Math.max(palScore(q, it.name), palScore(q, it.kind + " " + it.name) - 4) }))
5057
- .filter((r) => r.s >= 0)
5058
- .sort((a, b) => b.s - a.s)
5059
- .map((r) => r.it));
5060
- palSel = 0;
5127
+ const index = palIndex();
5128
+ palSearch.setItems(index);
5129
+ if (!q) {
5130
+ // An empty query opens on what was run before, which is the shortcut on the
5131
+ // second visit. Anything no longer in the index is dropped rather than shown
5132
+ // as a row that goes nowhere.
5133
+ const remembered = palRecents.list()
5134
+ .map((entry) => index.find((item) => item.kind + "::" + item.name === entry.term))
5135
+ .filter(Boolean)
5136
+ .map((item) => ({ ...item, kind: "Recent" }));
5137
+ palItems = remembered.concat(index);
5138
+ palSel = 0;
5139
+ palPaint();
5140
+ return;
5141
+ }
5142
+ palSearch.searchNow(q);
5143
+ }
5144
+
5145
+ /** Draws whatever is in `palItems`, grouped. The selection is applied by palMove. */
5146
+ function palPaint() {
5061
5147
  if (!palItems.length) {
5062
- palList.innerHTML = '<div class="pal-empty">No view or project matches "' + esc(q) + '".</div>';
5148
+ palList.innerHTML = '<div class="pal-empty">No view or project matches "' + esc(shortenQuery(palInput.value)) + '".</div>';
5063
5149
  palInput.removeAttribute("aria-activedescendant");
5064
5150
  return;
5065
5151
  }
5066
- let html = "", kind = null;
5067
- palItems.forEach((it, i) => {
5068
- if (it.kind !== kind) { kind = it.kind; html += '<div class="pal-gt">' + esc(kind === "View" ? "Views" : "Switch scope") + "</div>"; }
5069
- html += '<div class="pal-row' + (i === 0 ? " sel" : "") + '" role="option" id="pal-o' + i + '"'
5070
- + ' aria-selected="' + (i === 0 ? "true" : "false") + '" data-i="' + i + '">'
5071
- + it.icon
5072
- + '<span class="pal-name">' + esc(it.name) + "</span>"
5073
- + (it.group ? '<span class="pal-hint">' + esc(it.group) + "</span>" : (it.hint ? '<span class="pal-hint">' + esc(it.hint) + "</span>" : ""))
5074
- + "</div>";
5075
- });
5076
- palList.innerHTML = html;
5152
+ // Grouped for the headings, but each row carries its FLAT index, so the arrows
5153
+ // run straight through a group boundary.
5154
+ const heading = (kind) => (kind === "View" ? "Views" : kind === "Recent" ? "Recent" : "Switch scope");
5155
+ palList.innerHTML = groupRows(palItems, (item) => item.kind).map((group) => (
5156
+ '<div class="pal-gt">' + esc(heading(group.label)) + "</div>"
5157
+ + group.rows.map(({ row, position }) =>
5158
+ '<div class="pal-row" role="option" id="pal-o' + position + '"'
5159
+ + ' aria-selected="false" data-i="' + position + '">'
5160
+ + row.icon
5161
+ + '<span class="pal-name">' + esc(row.name) + "</span>"
5162
+ + (row.group ? '<span class="pal-hint">' + esc(row.group) + "</span>"
5163
+ : (row.hint ? '<span class="pal-hint">' + esc(row.hint) + "</span>" : ""))
5164
+ + "</div>").join("")
5165
+ )).join("");
5077
5166
  palMove(0);
5078
5167
  }
5079
5168
 
5169
+ /**
5170
+ * Where the highlight lands. A key name goes through the package's own arithmetic -
5171
+ * it wraps, which in a short list is what the arrow keys are expected to do - and a
5172
+ * number is the pointer landing on a row directly.
5173
+ */
5080
5174
  function palMove(next) {
5081
5175
  if (!palItems.length) return;
5082
5176
  const rows = palList.querySelectorAll(".pal-row");
5083
- palSel = (next + palItems.length) % palItems.length;
5177
+ palSel = typeof next === "string"
5178
+ ? moveActive(palSel, palItems.length, next)
5179
+ : (next + palItems.length) % palItems.length;
5084
5180
  rows.forEach((r, i) => {
5085
5181
  const on = i === palSel;
5086
5182
  r.classList.toggle("sel", on);
@@ -5109,6 +5205,7 @@ export {
5109
5205
  function palRun() {
5110
5206
  const it = palItems[palSel];
5111
5207
  if (!it) return;
5208
+ palRecents.remember({ term: it.kind + "::" + it.name, label: it.name });
5112
5209
  palClose();
5113
5210
  setNav(false);
5114
5211
  it.run();
@@ -5121,8 +5218,7 @@ export {
5121
5218
  palBack.addEventListener("mousedown", (e) => { if (e.target === palBack) palClose(); });
5122
5219
  // Focus stays inside: the dialog holds one input and a list driven from it.
5123
5220
  palBack.addEventListener("keydown", (e) => {
5124
- if (e.key === "ArrowDown") { e.preventDefault(); palMove(palSel + 1); }
5125
- else if (e.key === "ArrowUp") { e.preventDefault(); palMove(palSel - 1); }
5221
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") { e.preventDefault(); palMove(e.key); }
5126
5222
  else if (e.key === "Enter") { e.preventDefault(); palRun(); }
5127
5223
  // Escape closes the topmost surface only: the drawer behind it stays open.
5128
5224
  else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); palClose(); }
@@ -5130,7 +5226,7 @@ export {
5130
5226
  });
5131
5227
  document.addEventListener("keydown", (e) => {
5132
5228
  // Ctrl+K is the browser's own search-bar shortcut, so it has to be taken explicitly.
5133
- if (e.key.toLowerCase() !== "k" || !(IS_MAC ? e.metaKey : e.ctrlKey) || e.altKey) return;
5229
+ if (!isPaletteShortcut(e) || e.altKey) return;
5134
5230
  e.preventDefault();
5135
5231
  if (palBack.hidden) palOpen(); else palClose();
5136
5232
  });
@@ -5578,6 +5674,539 @@ export {
5578
5674
  + entries.map(([k, v]) => '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(k) + '</div><div class="set-hint">' + cgVal(v) + "</div></div></div>").join("")
5579
5675
  + "</div>";
5580
5676
  }
5677
+ // --- Code graph canvas ------------------------------------------------------------
5678
+ // A force-directed picture of the slice the server cut. Everything is hand-rolled on one
5679
+ // canvas: no graph library, no CDN - the dashboard ships as a single offline file, and at
5680
+ // a few hundred nodes the whole simulation costs less than a DOM node per symbol would.
5681
+ // Colour says WHERE a node lives, shape says WHAT it is (a file is a rounded square, a
5682
+ // symbol a circle). Colouring by kind instead made the file view - where every node is a
5683
+ // file - a single grey cloud, and told you nothing you could not read off the shape.
5684
+ const CG_GROUP_COLOR = ["#e0a458", "#5aa9e6", "#6fcf97", "#a78bfa", "#d7875f", "#56c8c8", "#e3c14e", "#c586c0", "#7f9cf5", "#8a93a3"];
5685
+ const CG_UNGROUPED = "#8a93a3";
5686
+ const CG_REL_COLOR = {
5687
+ imports: "rgba(138,147,163,0.55)", calls: "rgba(224,164,88,0.55)",
5688
+ references: "rgba(138,147,163,0.30)", extends: "rgba(111,207,151,0.55)",
5689
+ implements: "rgba(90,169,230,0.55)", contains: "rgba(138,147,163,0.20)"
5690
+ };
5691
+ // Tuned together: weaker links or a longer cutoff and dense clusters never separate.
5692
+ const CG_LINK_LEN = 82, CG_LINK_K = 0.22, CG_CHARGE = 4200, CG_CUTOFF = 260, CG_GRAVITY = 0.05, CG_DAMP = 0.74;
5693
+ const CG_ALPHA_DECAY = 0.021, CG_ALPHA_MIN = 0.004, CG_MAX_TICKS_PER_FRAME = 3, CG_SEPARATION = 7;
5694
+ const cg = {
5695
+ nodes: [], byId: new Map(), edges: [], meta: null,
5696
+ alpha: 0, raf: 0, seq: 0,
5697
+ zoom: 1, tx: 0, ty: 0,
5698
+ hover: null, selected: null, drag: null, pan: null, moved: false,
5699
+ groupColor: new Map(), groupCount: new Map()
5700
+ };
5701
+ const cgColor = (group) => cg.groupColor.get(group) || CG_UNGROUPED;
5702
+ /**
5703
+ * Group every node by the first directory segment that actually separates them: the paths
5704
+ * in a slice usually share a long prefix (a whole slice can sit under one package), and
5705
+ * grouping on the raw first segment would put every node in the same bucket.
5706
+ */
5707
+ function cgRegroup() {
5708
+ const paths = cg.nodes.map((n) => n.path || "");
5709
+ let prefix = "";
5710
+ if (paths.length > 1) {
5711
+ const parts = paths[0].split("/").slice(0, -1);
5712
+ let common = parts.length;
5713
+ for (const path of paths) {
5714
+ const p = path.split("/");
5715
+ let i = 0;
5716
+ while (i < common && i < p.length - 1 && p[i] === parts[i]) i++;
5717
+ common = i;
5718
+ if (!common) break;
5719
+ }
5720
+ prefix = parts.slice(0, common).join("/");
5721
+ }
5722
+ const counts = new Map();
5723
+ for (const n of cg.nodes) {
5724
+ const rest = prefix && n.path.startsWith(prefix + "/") ? n.path.slice(prefix.length + 1) : n.path;
5725
+ const cut = rest.indexOf("/");
5726
+ // Files sitting directly in the shared prefix have no segment of their own; "."
5727
+ // reads as a path fragment in the legend, where the label has to name a place.
5728
+ n.group = cut === -1 ? "(root)" : rest.slice(0, cut);
5729
+ counts.set(n.group, (counts.get(n.group) || 0) + 1);
5730
+ }
5731
+ // The biggest groups take the most distinct colours; everything past the palette
5732
+ // shares the neutral one rather than repeating a colour that already means something.
5733
+ cg.groupColor = new Map([...counts.entries()]
5734
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
5735
+ .slice(0, CG_GROUP_COLOR.length - 1)
5736
+ .map(([name], i) => [name, CG_GROUP_COLOR[i]]));
5737
+ cg.groupCount = counts;
5738
+ }
5739
+ // In WORLD units, so the separation pass in cgTick can guarantee the drawn circles never
5740
+ // overlap: a radius measured in screen pixels is a size the simulation cannot see, and at
5741
+ // any zoom below 1 the dots grow past the spacing the layout worked out for them.
5742
+ const cgRadius = (n) => 4 + Math.min(8, Math.sqrt(n.inDegree || 0) * 2.2);
5743
+ // Labels declutter by zoom: far out only the hubs are named, and every step in lowers the
5744
+ // bar. Without it a 300-node slice is a wall of overlapping text at every scale - but the
5745
+ // bar is set by how crowded the picture actually is, not by the zoom alone, or a slice of
5746
+ // fifteen nodes hides most of its own names for no reason.
5747
+ const cgLabelFloor = (z, count) => {
5748
+ if (count <= 40) return z >= 0.45 ? 0 : 4;
5749
+ if (count <= 120) return z >= 1 ? 0 : z >= 0.7 ? 3 : 9;
5750
+ return z >= 2.6 ? 0 : z >= 1.8 ? 2 : z >= 1.2 ? 5 : z >= 0.8 ? 10 : 22;
5751
+ };
5752
+
5753
+ function cgCanvasSize() {
5754
+ const c = $("cgCanvas");
5755
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
5756
+ // Bail when unchanged: a ResizeObserver fires for subpixel and browser-zoom rounding
5757
+ // too, and resizing the canvas every time clears it and can feed back into a shake.
5758
+ const w = Math.round(c.clientWidth * dpr), h = Math.round(c.clientHeight * dpr);
5759
+ if (w > 0 && h > 0 && (c.width !== w || c.height !== h)) { c.width = w; c.height = h; }
5760
+ return { c, dpr, w: c.clientWidth, h: c.clientHeight };
5761
+ }
5762
+ const cgToWorld = (px, py, box) => ({ x: (px - box.w / 2 - cg.tx) / cg.zoom, y: (py - box.h / 2 - cg.ty) / cg.zoom });
5763
+
5764
+ function cgDraw() {
5765
+ const box = cgCanvasSize();
5766
+ const ctx = box.c.getContext("2d");
5767
+ ctx.setTransform(box.dpr, 0, 0, box.dpr, 0, 0);
5768
+ ctx.clearRect(0, 0, box.w, box.h);
5769
+ ctx.save();
5770
+ ctx.translate(box.w / 2 + cg.tx, box.h / 2 + cg.ty);
5771
+ ctx.scale(cg.zoom, cg.zoom);
5772
+ const focusId = (cg.selected && cg.selected.id) || (cg.hover && cg.hover.id) || null;
5773
+ const lit = new Set();
5774
+ if (focusId) for (const e of cg.edges) { if (e.source === focusId) lit.add(e.target); else if (e.target === focusId) lit.add(e.source); }
5775
+ ctx.lineWidth = 1 / cg.zoom;
5776
+ for (const e of cg.edges) {
5777
+ const s = e.s, t = e.t;
5778
+ if (!s || !t) continue;
5779
+ const on = focusId && (e.source === focusId || e.target === focusId);
5780
+ ctx.strokeStyle = on ? "rgba(224,164,88,0.85)" : (focusId ? "rgba(138,147,163,0.13)" : (CG_REL_COLOR[e.relation] || CG_REL_COLOR.references));
5781
+ ctx.beginPath();
5782
+ ctx.moveTo(s.x, s.y);
5783
+ ctx.lineTo(t.x, t.y);
5784
+ ctx.stroke();
5785
+ }
5786
+ const z = cg.zoom;
5787
+ const floor = cgLabelFloor(z, cg.nodes.length);
5788
+ const seeded = !!(cg.meta && cg.meta.focus && cg.meta.focus.length);
5789
+ const dimmed = (n) => focusId && n.id !== focusId && !lit.has(n.id);
5790
+ for (const n of cg.nodes) {
5791
+ const r = cgRadius(n);
5792
+ ctx.globalAlpha = dimmed(n) ? 0.28 : 1;
5793
+ ctx.beginPath();
5794
+ if (n.kind === "file") ctx.roundRect(n.x - r, n.y - r, r * 2, r * 2, r * 0.35);
5795
+ else ctx.arc(n.x, n.y, r, 0, 6.283185);
5796
+ ctx.fillStyle = cgColor(n.group);
5797
+ ctx.fill();
5798
+ if (n.id === focusId) { ctx.strokeStyle = "#eceef2"; ctx.lineWidth = 2 / z; ctx.stroke(); }
5799
+ else if (n.depth === 0 && seeded) { ctx.strokeStyle = "rgba(236,238,242,0.55)"; ctx.lineWidth = 1.5 / z; ctx.stroke(); }
5800
+ ctx.globalAlpha = 1;
5801
+ }
5802
+ // Labels in a second pass so a node drawn later cannot cover the caption of one drawn
5803
+ // earlier - the single most common way a readable graph turns into half-words.
5804
+ ctx.textBaseline = "middle";
5805
+ ctx.font = (11 / z) + "px ui-monospace, Menlo, Consolas, monospace";
5806
+ for (const n of cg.nodes) {
5807
+ if (dimmed(n) || (n.id !== focusId && (n.inDegree || 0) < floor)) continue;
5808
+ const r = cgRadius(n);
5809
+ ctx.textAlign = "left";
5810
+ const tw = ctx.measureText(n.name).width;
5811
+ ctx.fillStyle = "rgba(8,10,16,0.78)";
5812
+ ctx.fillRect(n.x + r + 3 / z, n.y - 7 / z, tw + 5 / z, 14 / z);
5813
+ ctx.fillStyle = n.id === focusId ? "#eceef2" : "#c3cad6";
5814
+ ctx.fillText(n.name, n.x + r + 5 / z, n.y);
5815
+ }
5816
+ // The "expand me" badge sits above both, or a neighbouring node hides the one cue that
5817
+ // says this node is standing in front of more of the graph.
5818
+ for (const n of cg.nodes) {
5819
+ if (!n.hidden || z < 1.1 || dimmed(n)) continue;
5820
+ const r = cgRadius(n);
5821
+ const bx = n.x + r * 0.78, by = n.y - r * 0.78;
5822
+ ctx.beginPath();
5823
+ ctx.arc(bx, by, 5.5 / z, 0, 6.283185);
5824
+ ctx.fillStyle = "rgba(8,10,16,0.9)";
5825
+ ctx.fill();
5826
+ ctx.fillStyle = "#8a93a3";
5827
+ ctx.font = (9 / z) + "px ui-monospace, Menlo, Consolas, monospace";
5828
+ ctx.textAlign = "center";
5829
+ ctx.fillText("+", bx, by);
5830
+ ctx.font = (11 / z) + "px ui-monospace, Menlo, Consolas, monospace";
5831
+ }
5832
+ ctx.restore();
5833
+ }
5834
+
5835
+ function cgTick() {
5836
+ const nodes = cg.nodes;
5837
+ if (!nodes.length) return;
5838
+ const a = cg.alpha;
5839
+ for (const e of cg.edges) {
5840
+ const s = e.s, t = e.t;
5841
+ if (!s || !t || s === t) continue;
5842
+ const dx = t.x - s.x, dy = t.y - s.y;
5843
+ const d = Math.sqrt(dx * dx + dy * dy) || 0.01;
5844
+ const f = ((d - CG_LINK_LEN) / d) * CG_LINK_K * a;
5845
+ s.vx += dx * f; s.vy += dy * f;
5846
+ t.vx -= dx * f; t.vy -= dy * f;
5847
+ }
5848
+ // Repulsion over a uniform grid, not every pair: at the 2000-node cap the all-pairs
5849
+ // version is four million distance checks a tick and the panel stops responding.
5850
+ const grid = new Map();
5851
+ for (const n of nodes) {
5852
+ const k = ((n.x / CG_CUTOFF) | 0) + ":" + ((n.y / CG_CUTOFF) | 0);
5853
+ const arr = grid.get(k);
5854
+ if (arr) arr.push(n); else grid.set(k, [n]);
5855
+ }
5856
+ for (const n of nodes) {
5857
+ const cx = (n.x / CG_CUTOFF) | 0, cy = (n.y / CG_CUTOFF) | 0;
5858
+ const rn = cgRadius(n);
5859
+ for (let gx = cx - 1; gx <= cx + 1; gx++) {
5860
+ for (let gy = cy - 1; gy <= cy + 1; gy++) {
5861
+ const arr = grid.get(gx + ":" + gy);
5862
+ if (!arr) continue;
5863
+ for (const o of arr) {
5864
+ if (o === n) continue;
5865
+ const dx = n.x - o.x, dy = n.y - o.y;
5866
+ const d2 = Math.max(1, dx * dx + dy * dy);
5867
+ if (d2 > CG_CUTOFF * CG_CUTOFF) continue;
5868
+ const d = Math.sqrt(d2);
5869
+ n.vx += (dx * CG_CHARGE * a) / (d2 * d);
5870
+ n.vy += (dy * CG_CHARGE * a) / (d2 * d);
5871
+ // Separation, applied to the POSITION and not the velocity: charge
5872
+ // alone loses to a node's own springs once it has a dozen neighbours,
5873
+ // and the result is the hairball where every hub sits under its
5874
+ // callers. This is the only thing that guarantees drawn circles do
5875
+ // not overlap, so it runs regardless of how cold the layout is.
5876
+ const minD = rn + cgRadius(o) + CG_SEPARATION;
5877
+ if (d >= minD) continue;
5878
+ const push = ((minD - d) / d) * 0.5;
5879
+ if (!n.pinned) { n.x += dx * push; n.y += dy * push; }
5880
+ if (!o.pinned) { o.x -= dx * push; o.y -= dy * push; }
5881
+ }
5882
+ }
5883
+ }
5884
+ }
5885
+ for (const n of nodes) {
5886
+ if (n.pinned) { n.vx = 0; n.vy = 0; continue; }
5887
+ n.vx -= n.x * CG_GRAVITY * a;
5888
+ n.vy -= n.y * CG_GRAVITY * a;
5889
+ n.vx *= CG_DAMP; n.vy *= CG_DAMP;
5890
+ // Clamped so a node that lands on top of another cannot be flung off the canvas
5891
+ // by the inverse-square term and take the layout's whole scale with it.
5892
+ n.x += Math.max(-40, Math.min(40, n.vx));
5893
+ n.y += Math.max(-40, Math.min(40, n.vy));
5894
+ }
5895
+ cg.alpha -= cg.alpha * CG_ALPHA_DECAY;
5896
+ if (cg.alpha < CG_ALPHA_MIN) cg.alpha = 0;
5897
+ }
5898
+
5899
+ function cgLoop() {
5900
+ cg.raf = 0;
5901
+ // A hidden tab must not keep a simulation running: the loop stops, and anything that
5902
+ // matters re-enters through cgKick when the tab comes back.
5903
+ if ($("view-codegraph").style.display === "none") { cg.alpha = 0; return; }
5904
+ for (let i = 0; i < CG_MAX_TICKS_PER_FRAME && cg.alpha > 0; i++) cgTick();
5905
+ cgDraw();
5906
+ if (cg.alpha > 0 || cg.drag) cg.raf = requestAnimationFrame(cgLoop);
5907
+ }
5908
+ function cgKick(alpha) {
5909
+ if (typeof alpha === "number") cg.alpha = Math.max(cg.alpha, alpha);
5910
+ if (!cg.raf) cg.raf = requestAnimationFrame(cgLoop);
5911
+ }
5912
+
5913
+ function cgFit() {
5914
+ if (!cg.nodes.length) return;
5915
+ const box = cgCanvasSize();
5916
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
5917
+ for (const n of cg.nodes) { x0 = Math.min(x0, n.x); y0 = Math.min(y0, n.y); x1 = Math.max(x1, n.x); y1 = Math.max(y1, n.y); }
5918
+ const bw = Math.max(1, x1 - x0), bh = Math.max(1, y1 - y0);
5919
+ cg.zoom = Math.max(0.12, Math.min(1.6, Math.min((box.w - 90) / bw, (box.h - 70) / bh)));
5920
+ cg.tx = -((x0 + x1) / 2) * cg.zoom;
5921
+ cg.ty = -((y0 + y1) / 2) * cg.zoom;
5922
+ cgKick(0);
5923
+ }
5924
+ function cgZoomBy(factor, at) {
5925
+ const box = cgCanvasSize();
5926
+ const next = Math.max(0.08, Math.min(8, cg.zoom * factor));
5927
+ const px = at ? at.x : box.w / 2, py = at ? at.y : box.h / 2;
5928
+ // Anchor the point under the cursor: without it wheel-zoom drifts the graph away from
5929
+ // whatever the user was pointing at.
5930
+ const w = cgToWorld(px, py, box);
5931
+ cg.zoom = next;
5932
+ cg.tx = px - box.w / 2 - w.x * next;
5933
+ cg.ty = py - box.h / 2 - w.y * next;
5934
+ cgKick(0);
5935
+ }
5936
+
5937
+ function cgNodeAt(px, py) {
5938
+ const box = cgCanvasSize();
5939
+ const w = cgToWorld(px, py, box);
5940
+ let best = null, bestD = Infinity;
5941
+ for (const n of cg.nodes) {
5942
+ const r = cgRadius(n) + 4 / cg.zoom;
5943
+ const d = (n.x - w.x) * (n.x - w.x) + (n.y - w.y) * (n.y - w.y);
5944
+ if (d <= r * r && d < bestD) { best = n; bestD = d; }
5945
+ }
5946
+ return best;
5947
+ }
5948
+
5949
+ function cgRenderLegend() {
5950
+ const rest = cg.groupCount.size - cg.groupColor.size;
5951
+ $("cgLegend").innerHTML = [...cg.groupColor.keys()]
5952
+ .map((g) => '<span><i style="background:' + cgColor(g) + '"></i>' + esc(g) + "</span>").join("")
5953
+ + (rest > 0 ? '<span><i style="background:' + CG_UNGROUPED + '"></i>' + rest + " more</span>" : "");
5954
+ }
5955
+ function cgRenderDetail() {
5956
+ const el = $("cgDetail");
5957
+ const n = cg.selected;
5958
+ if (!n) { el.hidden = true; el.innerHTML = ""; return; }
5959
+ const where = n.kind === "file" ? n.path : n.path + ":" + n.line;
5960
+ el.hidden = false;
5961
+ el.innerHTML = '<div class="cg-head"><h4>' + esc(n.name) + "</h4>"
5962
+ + '<button type="button" class="iconbtn" data-cg-close title="Close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div>'
5963
+ + '<div class="cg-where">' + esc(where) + "</div>"
5964
+ + '<div class="set-hint" style="padding:4px 0 0">' + esc(n.kind) + " - " + n.inDegree + " file" + (n.inDegree === 1 ? "" : "s") + " depend on it"
5965
+ + (n.hidden > 0 ? " - " + n.hidden + " neighbour" + (n.hidden === 1 ? "" : "s") + " not shown" : "") + "</div>"
5966
+ + (n.signature ? '<pre class="cg-sig">' + esc(n.signature.slice(0, 400)) + "</pre>" : "")
5967
+ + '<div class="cg-acts">'
5968
+ + '<button type="button" class="toggle on" data-cg-focus="' + esc(n.id) + '">Centre here</button>'
5969
+ + (n.hidden > 0 ? '<button type="button" class="toggle" data-cg-expand="' + esc(n.id) + '">Expand</button>' : "")
5970
+ + '<button type="button" class="iconbtn" data-cg-copy="' + esc(where) + '" title="Copy location" aria-label="Copy location"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
5971
+ + "</div>";
5972
+ }
5973
+ function cgRenderMeta() {
5974
+ const m = cg.meta;
5975
+ if (!m) { $("cgGraphMeta").textContent = ""; return; }
5976
+ // A real but tiny share rounds to "0%", which reads as "nothing" - the one thing it
5977
+ // is not - so it is reported as "<1%" instead. Measured against the population this
5978
+ // scope draws from: the file view carries import edges only.
5979
+ const totals = m.totals || {};
5980
+ const total = (m.scope === "files" ? totals.importEdges : totals.edges) || 0;
5981
+ const pct = total && cg.edges.length ? (cg.edges.length / total) * 100 : 0;
5982
+ const share = !pct ? "0%" : pct < 1 ? "<1%" : Math.round(pct) + "%";
5983
+ const bits = [cg.nodes.length + " nodes", cg.edges.length + " edges", share + " of the graph's wiring"];
5984
+ if (m.focus && m.focus.length) bits.push("centred on " + m.focus.map((f) => f.name).join(", "));
5985
+ // Both caps are stated: one hid part of the picture, the other means part of the tree
5986
+ // was never indexed - a drawn graph reads as complete unless it says otherwise.
5987
+ if (m.truncated) bits.push("truncated at the node cap");
5988
+ if (m.incomplete) bits.push("index incomplete: the scan hit a limit");
5989
+ $("cgGraphMeta").textContent = bits.join(" - ");
5990
+ $("cgCanvas").setAttribute("aria-label",
5991
+ "Code graph: " + cg.nodes.length + " nodes and " + cg.edges.length + " edges"
5992
+ + (m.focus && m.focus.length ? " around " + m.focus.map((f) => f.name).join(", ") : ""));
5993
+ }
5994
+ function cgOverlay(msg) {
5995
+ const el = $("cgGraphOverlay");
5996
+ if (!msg) { el.hidden = true; return; }
5997
+ el.hidden = false;
5998
+ el.textContent = msg;
5999
+ }
6000
+
6001
+ /** Seed a node deterministically: phyllotaxis, or beside the node it was expanded from. */
6002
+ function cgPlace(node, index, near) {
6003
+ const th = index * 2.399963;
6004
+ if (near) {
6005
+ node.x = near.x + Math.cos(th) * (30 + index * 1.5);
6006
+ node.y = near.y + Math.sin(th) * (30 + index * 1.5);
6007
+ } else {
6008
+ const r = 12 * Math.sqrt(index + 1);
6009
+ node.x = Math.cos(th) * r;
6010
+ node.y = Math.sin(th) * r;
6011
+ }
6012
+ node.vx = 0;
6013
+ node.vy = 0;
6014
+ }
6015
+
6016
+ /**
6017
+ * Recount, per node, the neighbours NOT on screen. The server's `hidden` is relative to
6018
+ * the slice it came from, so after a merge it would claim neighbours already drawn beside
6019
+ * it - a "+" badge and an Expand button that add nothing. `neighbours` is the whole-graph
6020
+ * total, so the honest count is that minus what the merged picture actually draws.
6021
+ *
6022
+ * Same asymmetry the engine counts by: expanding a node re-seeds the symbols a file
6023
+ * contains, but a symbol's own file is never reachable across a `contains` edge.
6024
+ */
6025
+ function cgRecount() {
6026
+ const drawn = new Map();
6027
+ const bump = (id) => drawn.set(id, (drawn.get(id) || 0) + 1);
6028
+ for (const e of cg.edges) {
6029
+ if (cg.byId.has(e.source)) bump(e.source);
6030
+ if (cg.byId.has(e.target) && e.relation !== "contains") bump(e.target);
6031
+ }
6032
+ for (const n of cg.nodes) n.hidden = Math.max(0, (n.neighbours || 0) - (drawn.get(n.id) || 0));
6033
+ }
6034
+
6035
+ function cgApply(slice, merge, near) {
6036
+ const before = cg.nodes.length;
6037
+ if (!merge) { cg.nodes = []; cg.byId = new Map(); cg.edges = []; cg.selected = null; cg.hover = null; }
6038
+ let added = 0;
6039
+ for (const raw of slice.nodes) {
6040
+ const seen = cg.byId.get(raw.id);
6041
+ // Keep the live object on a merge: replacing it would reset its position, and every
6042
+ // expand would look like the whole layout jumped.
6043
+ if (seen) { seen.inDegree = raw.inDegree; seen.neighbours = raw.neighbours; continue; }
6044
+ const n = Object.assign({}, raw);
6045
+ cgPlace(n, merge ? added : cg.nodes.length, near);
6046
+ cg.byId.set(n.id, n);
6047
+ cg.nodes.push(n);
6048
+ added++;
6049
+ }
6050
+ const key = (e) => e.source + "" + e.target + "" + e.relation;
6051
+ const have = new Set(cg.edges.map(key));
6052
+ for (const e of slice.edges) {
6053
+ if (have.has(key(e))) continue;
6054
+ have.add(key(e));
6055
+ cg.edges.push({ source: e.source, target: e.target, relation: e.relation });
6056
+ }
6057
+ for (const e of cg.edges) { e.s = cg.byId.get(e.source); e.t = cg.byId.get(e.target); }
6058
+ cgRecount();
6059
+ // An expand widens the picture; it does not re-answer what the picture is OF. Taking
6060
+ // the sub-slice's meta would relabel the whole canvas with the expanded node's focus
6061
+ // and report its cap instead of the one the drawn graph was cut at.
6062
+ cg.meta = merge && cg.meta
6063
+ ? Object.assign({}, cg.meta, { truncated: cg.meta.truncated || slice.truncated })
6064
+ : slice;
6065
+ cgRegroup();
6066
+ cgRenderLegend();
6067
+ cgRenderMeta();
6068
+ cgRenderDetail();
6069
+ cgKick(merge ? 0.55 : 1);
6070
+ if (!merge || before === 0) setTimeout(cgFit, 420);
6071
+ return added;
6072
+ }
6073
+
6074
+ async function cgFetchGraph(payload) {
6075
+ try {
6076
+ const res = await fetch("/api/codegraph", {
6077
+ method: "POST",
6078
+ headers: { "Content-Type": "application/json" },
6079
+ body: JSON.stringify(Object.assign({ op: "graph", project: cgProject || undefined }, payload))
6080
+ });
6081
+ return await res.json();
6082
+ } catch { return { ok: false, error: "Could not reach the server." }; }
6083
+ }
6084
+
6085
+ async function cgDrawGraph(opts) {
6086
+ const o = opts || {};
6087
+ const seq = ++cg.seq;
6088
+ if (!o.merge) cgOverlay("Drawing...");
6089
+ const out = await cgFetchGraph({
6090
+ focus: o.focus || undefined,
6091
+ depth: Number($("cgDepth").value) || 1,
6092
+ scope: $("cgScope").value,
6093
+ // Deliberately below the engine's cap: past roughly this many nodes an overview
6094
+ // stops being a picture you can read and becomes one you can only zoom into, and
6095
+ // the meta line says when the cap cut something off.
6096
+ limit: 200
6097
+ });
6098
+ // A slower earlier request must not overwrite a newer answer: clicking through three
6099
+ // nodes quickly would otherwise leave whichever call happened to finish last.
6100
+ if (seq !== cg.seq) return;
6101
+ // The legend goes with the nodes: leaving the previous slice's colour key over an
6102
+ // empty canvas describes a picture that is no longer on it.
6103
+ const reset = () => {
6104
+ cg.nodes = []; cg.edges = []; cg.byId = new Map(); cg.selected = null;
6105
+ cgRegroup(); cgRenderLegend(); cgRenderDetail(); cgDraw();
6106
+ };
6107
+ // The overlay covers the whole canvas, so it belongs only to a draw that has nothing
6108
+ // to show. An expand that failed leaves a perfectly good graph on screen: veiling it
6109
+ // would swallow every click on it until the next Draw, so that one reports as a toast.
6110
+ if (!out || !out.ok || !out.graph) {
6111
+ const why = (out && out.error) || "Could not draw the graph.";
6112
+ if (o.merge) { toast.error("Could not expand that node", { description: why }); return; }
6113
+ reset();
6114
+ cg.meta = null;
6115
+ cgRenderMeta();
6116
+ cgOverlay(why);
6117
+ return;
6118
+ }
6119
+ const slice = out.graph;
6120
+ if (!slice.nodes.length) {
6121
+ const why = slice.note || "Nothing to draw for this project yet.";
6122
+ if (o.merge) { toast.info("Nothing to add", { description: why }); return; }
6123
+ reset();
6124
+ cg.meta = slice;
6125
+ cgRenderMeta();
6126
+ cgOverlay(why);
6127
+ return;
6128
+ }
6129
+ cgOverlay("");
6130
+ const added = cgApply(slice, !!o.merge, o.merge ? cg.byId.get(o.focus) : null);
6131
+ if (o.merge && !added) toast.info("Nothing to add", { description: "Every neighbour of that node is already drawn." });
6132
+ }
6133
+
6134
+ function cgWireCanvas() {
6135
+ const c = $("cgCanvas");
6136
+ const local = (e) => { const r = c.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; };
6137
+ c.addEventListener("pointerdown", (e) => {
6138
+ const p = local(e);
6139
+ const hit = cgNodeAt(p.x, p.y);
6140
+ cg.moved = false;
6141
+ c.setPointerCapture(e.pointerId);
6142
+ if (hit) { cg.drag = { node: hit, wasPinned: hit.pinned }; hit.pinned = true; }
6143
+ else { cg.pan = { x: p.x - cg.tx, y: p.y - cg.ty }; c.classList.add("grabbing"); }
6144
+ });
6145
+ c.addEventListener("pointermove", (e) => {
6146
+ const p = local(e);
6147
+ if (cg.drag) {
6148
+ const w = cgToWorld(p.x, p.y, cgCanvasSize());
6149
+ cg.drag.node.x = w.x;
6150
+ cg.drag.node.y = w.y;
6151
+ cg.moved = true;
6152
+ cgKick(0.28);
6153
+ return;
6154
+ }
6155
+ if (cg.pan) { cg.tx = p.x - cg.pan.x; cg.ty = p.y - cg.pan.y; cg.moved = true; cgKick(0); return; }
6156
+ const hit = cgNodeAt(p.x, p.y);
6157
+ if (hit !== cg.hover) { cg.hover = hit; c.style.cursor = hit ? "pointer" : ""; cgKick(0); }
6158
+ });
6159
+ const release = (e) => {
6160
+ if (cg.drag) { if (!cg.moved) cg.drag.node.pinned = cg.drag.wasPinned; cg.drag = null; }
6161
+ if (cg.pan) { cg.pan = null; c.classList.remove("grabbing"); }
6162
+ if (!cg.moved && e) {
6163
+ const p = local(e);
6164
+ cg.selected = cgNodeAt(p.x, p.y) || null;
6165
+ cgRenderDetail();
6166
+ }
6167
+ cgKick(0);
6168
+ };
6169
+ c.addEventListener("pointerup", release);
6170
+ c.addEventListener("pointercancel", () => release(null));
6171
+ c.addEventListener("dblclick", (e) => {
6172
+ const p = local(e);
6173
+ const hit = cgNodeAt(p.x, p.y);
6174
+ if (hit) cgDrawGraph({ focus: hit.id, merge: true });
6175
+ });
6176
+ c.addEventListener("wheel", (e) => {
6177
+ e.preventDefault();
6178
+ cgZoomBy(e.deltaY < 0 ? 1.14 : 1 / 1.14, local(e));
6179
+ }, { passive: false });
6180
+ // The canvas is focusable, so the same picture is reachable without a pointer.
6181
+ c.addEventListener("keydown", (e) => {
6182
+ if (e.key === "Escape") { cg.selected = null; cgRenderDetail(); cgKick(0); }
6183
+ else if (e.key === "+" || e.key === "=") cgZoomBy(1.2);
6184
+ else if (e.key === "-") cgZoomBy(1 / 1.2);
6185
+ else if (e.key === "0") cgFit();
6186
+ else return;
6187
+ e.preventDefault();
6188
+ });
6189
+ new ResizeObserver(() => cgKick(0)).observe($("cgWrap"));
6190
+ const redraw = () => cgDrawGraph({ focus: $("cgFocus").value.trim() });
6191
+ $("cgZoomIn").addEventListener("click", () => cgZoomBy(1.25));
6192
+ $("cgZoomOut").addEventListener("click", () => cgZoomBy(1 / 1.25));
6193
+ $("cgFit").addEventListener("click", cgFit);
6194
+ $("cgDraw").addEventListener("click", redraw);
6195
+ $("cgFocus").addEventListener("keydown", (e) => { if (e.key === "Enter") redraw(); });
6196
+ $("cgScope").addEventListener("change", redraw);
6197
+ $("cgDepth").addEventListener("change", redraw);
6198
+ $("cgDetail").addEventListener("click", (e) => {
6199
+ const close = e.target.closest("[data-cg-close]");
6200
+ if (close) { cg.selected = null; cgRenderDetail(); cgKick(0); return; }
6201
+ const focus = e.target.closest("[data-cg-focus]");
6202
+ if (focus) { $("cgFocus").value = focus.dataset.cgFocus; cgDrawGraph({ focus: focus.dataset.cgFocus }); return; }
6203
+ const expand = e.target.closest("[data-cg-expand]");
6204
+ if (expand) { cgDrawGraph({ focus: expand.dataset.cgExpand, merge: true }); return; }
6205
+ const copy = e.target.closest("[data-cg-copy]");
6206
+ if (copy) copyToClipboard(copy.dataset.cgCopy, copy);
6207
+ });
6208
+ }
6209
+
5581
6210
  function renderCodeGraph(view) {
5582
6211
  view = view || {};
5583
6212
  const on = !!view.enabled;
@@ -5617,7 +6246,12 @@ export {
5617
6246
  $("cgNote").textContent = "";
5618
6247
  try {
5619
6248
  const q = cgProject ? "?project=" + encodeURIComponent(cgProject) : "";
5620
- renderCodeGraph(await (await fetch("/api/codegraph" + q, { cache: "no-store" })).json());
6249
+ const view = await (await fetch("/api/codegraph" + q, { cache: "no-store" })).json();
6250
+ renderCodeGraph(view);
6251
+ // The picture is a second request on purpose: it is the expensive half, and the
6252
+ // panel around it (projects, freshness, architecture) must not wait on it.
6253
+ if (view && (view.projects || []).length) cgDrawGraph({ focus: $("cgFocus").value.trim() });
6254
+ else cgOverlay("Index a project to draw its graph.");
5621
6255
  } catch { $("cgBody").innerHTML = '<div class="empty" style="padding:12px 0">Code graph unavailable.</div>'; }
5622
6256
  }
5623
6257
  async function cgPost(payload, working) {
@@ -5646,7 +6280,10 @@ export {
5646
6280
  async function cgIndexAction() {
5647
6281
  const root = $("cgRoot").value.trim();
5648
6282
  const out = await cgPost({ op: "index", root: root || undefined }, "Indexing...");
5649
- if (out) { if (out.view && out.view.selected) cgProject = out.view.selected; setNote("cgNote", out.ok ? "Indexed." : (out.error || "Indexing failed."), out.ok ? "ok" : "err"); }
6283
+ if (!out) return;
6284
+ if (out.view && out.view.selected) cgProject = out.view.selected;
6285
+ setNote("cgNote", out.ok ? "Indexed." : (out.error || "Indexing failed."), out.ok ? "ok" : "err");
6286
+ if (out.ok) cgDrawGraph({ focus: $("cgFocus").value.trim() });
5650
6287
  }
5651
6288
  async function cgAskAction() {
5652
6289
  const query = $("cgQuery").value.trim();
@@ -5655,6 +6292,7 @@ export {
5655
6292
  if (out) setNote("cgNote", out.ok ? "" : (out.error || "Query failed."), out.ok ? "ok" : "err");
5656
6293
  }
5657
6294
  function wireCodeGraph() {
6295
+ cgWireCanvas();
5658
6296
  $("cgRefresh").addEventListener("click", loadCodeGraph);
5659
6297
  $("cgToggle").addEventListener("click", cgToggleAction);
5660
6298
  $("cgIndex").addEventListener("click", cgIndexAction);
@@ -5664,12 +6302,17 @@ export {
5664
6302
  const reindex = e.target.closest("button[data-cg-reindex]");
5665
6303
  if (reindex) {
5666
6304
  cgPost({ op: "index", root: reindex.dataset.cgReindex }, "Re-indexing...")
5667
- .then((out) => { if (out) setNote("cgNote", out.ok ? "Re-indexed." : (out.error || "Indexing failed."), out.ok ? "ok" : "err"); });
6305
+ .then((out) => {
6306
+ if (!out) return;
6307
+ setNote("cgNote", out.ok ? "Re-indexed." : (out.error || "Indexing failed."), out.ok ? "ok" : "err");
6308
+ if (out.ok) cgDrawGraph({ focus: $("cgFocus").value.trim() });
6309
+ });
5668
6310
  return;
5669
6311
  }
5670
6312
  const b = e.target.closest("button[data-cg-project]");
5671
6313
  if (!b) return;
5672
6314
  cgProject = b.dataset.cgProject;
6315
+ $("cgFocus").value = "";
5673
6316
  loadCodeGraph();
5674
6317
  });
5675
6318
  }
@@ -0,0 +1,2 @@
1
+ /* @enigmax/primitives core, vendored by scripts/sync-primitives.mjs. Do not edit. */
2
+ "use strict";var EnigmaPrimitives=(()=>{var W=Object.defineProperty;var at=Object.getOwnPropertyDescriptor;var lt=Object.getOwnPropertyNames;var ct=Object.prototype.hasOwnProperty;var ut=(e,t)=>{for(var n in t)W(e,n,{get:t[n],enumerable:!0})},dt=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of lt(t))!ct.call(e,a)&&a!==n&&W(e,a,{get:()=>t[a],enumerable:!(o=at(t,a))||o.enumerable});return e};var ft=e=>dt(W({},"__esModule",{value:!0}),e);var Zt={};ut(Zt,{FLAG_CDN:()=>oe,FLAG_RATIO:()=>re,INPUT_ICON_PATHS:()=>_,SELECT_SEARCH_KEYS:()=>G,SERVER_NETWORK_STATE:()=>J,configureFlags:()=>qe,createButton:()=>Oe,createInput:()=>we,createMarquee:()=>ve,createNetworkMonitor:()=>Re,createNotifications:()=>ke,createRecentStore:()=>Ye,createSearch:()=>$,createSelect:()=>ot,ensureZone:()=>ee,estimatePasswordStrength:()=>Ae,flagAttributes:()=>Ze,flagConfig:()=>Ue,flagName:()=>ie,flagSrc:()=>se,flagView:()=>ae,generatePassword:()=>Ce,groupRows:()=>je,iconMarkup:()=>H,isPaletteShortcut:()=>Xe,moveActive:()=>We,normalizeDate:()=>te,normalizeFlagCode:()=>B,parseDuration:()=>ne,recentKey:()=>U,relativeTimeAttributes:()=>Fe,relativeTimeView:()=>De,resetFlagConfig:()=>Be,shortcutLabel:()=>Qe,shortenQuery:()=>Ee,subsequenceMatcher:()=>Te});var pt={speed:60,reverse:!1,vertical:!1,draggable:!0,hover:"off",decay:.12,manageStyles:!0,copies:"clone"},mt=24;function ve(e,t,n={}){let o={...pt,...n},a=0,i=Math.max(2,t.children.length),s=0,r=0,c=!1,g=!1,f=!1,h=0,T=0,v=!1,E=null,x=0,M=0,p=0,w=0,L=!1,m=0,b=typeof window.matchMedia=="function"?window.matchMedia("(prefers-reduced-motion: reduce)"):null,d=b?.matches??!1;function l(u){let S=o.hover??"off";return S==="off"?u:S==="pause"?0:typeof S=="number"?u*S:(u<0?-1:1)*Math.abs(S.speed)}function y(){if(d||g)return 0;let u=o.reverse?-o.speed:o.speed;return L?l(u):u}function I(u){return o.vertical?u.offsetHeight:u.offsetWidth}function O(u){return o.vertical?u.offsetTop:u.offsetLeft}function k(){o.manageStyles&&(e.style.overflow="hidden",e.style.touchAction=o.vertical?"pan-x":"pan-y",e.style.userSelect="none",e.style.webkitUserSelect="none",t.style.willChange="transform")}function C(){let u=o.vertical?0:-s,S=o.vertical?-s:0;t.style.transform=`translate3d(${u}px, ${S}px, 0)`}function N(){let u=t.children[0],S=t.children[1];if(!u)return 0;if(S){let A=O(S)-O(u);if(A>0)return A}return I(u)}function le(u){return u<=0?2:Math.min(mt,Math.max(2,Math.ceil(I(e)/u)+1))}function ce(u){let S=t.children[0];if(S){for(;t.children.length>u;)t.removeChild(t.lastElementChild);for(;t.children.length<u;){let A=S.cloneNode(!0);A.setAttribute("aria-hidden","true"),A.dataset.enigmaMarqueeCopy=String(t.children.length),t.appendChild(A)}}}function D(){if(f)return;let u=o.copies==="clone";u&&t.children.length<2&&ce(2);let S=N(),A=le(S);u&&t.children.length!==A&&(ce(A),S=N(),A=le(S));let j=S!==a||A!==i;a=S,A!==i&&(i=A,o.onCopyCountChange?.(A)),a>0&&(s=V(s)),C(),j&&o.onMeasure?.(a,i),R()}function V(u){return a<=0?u:(u%a+a)%a}function ue(u){if(h=0,f)return;let S=Math.min((u-T)/1e3,.05);if(T=u,v||(r+=(y()-r)*(1-Math.pow(o.decay,S)),s=V(s+r*S)),C(),!v&&Math.abs(r)<.01&&Math.abs(y())<.01){r=0,c=!1;return}h=requestAnimationFrame(ue)}function R(){f||c||(c=!0,T=performance.now(),h=requestAnimationFrame(ue))}function de(u){return o.vertical?u.clientY:u.clientX}function fe(u){u.pointerType!=="mouse"&&z(),!(!o.draggable||u.button>0||E!==null)&&(E=u.pointerId,v=!0,x=de(u),M=x,p=u.timeStamp,w=0,e.dataset.dragging="true",window.addEventListener("pointermove",Z),window.addEventListener("pointerup",F),window.addEventListener("pointercancel",F),R())}function Z(u){if(u.pointerId!==E)return;let S=de(u),A=S-x;x=S;let j=Math.max((u.timeStamp-p)/1e3,.001);p=u.timeStamp,w=Math.max(w,Math.abs(S-M)),s=V(s-A);let st=-A/j;r=r*.6+st*(1-.6)}function F(u){u.pointerId===E&&(E=null,v=!1,delete e.dataset.dragging,window.removeEventListener("pointermove",Z),window.removeEventListener("pointerup",F),window.removeEventListener("pointercancel",F),u.timeStamp-p>90&&(r=0),R())}function pe(u){w<=6||(u.preventDefault(),u.stopPropagation())}function me(u){w>6||o.onItemClick?.(u)}function ge(u){if(u.pointerType!=="mouse"){z();return}Date.now()-m<1e3||(L=!0,e.dataset.hovering="true",R())}function ye(){L=!1,delete e.dataset.hovering,R()}function z(){m=Date.now(),L&&(L=!1,delete e.dataset.hovering,R())}function he(u){d=u.matches,e.dataset.reducedMotion=String(d),R()}let Y=typeof ResizeObserver=="function"?new ResizeObserver(()=>D()):null;function rt(){var S;k(),e.dataset.enigmaMarquee="",e.dataset.reducedMotion=String(d),t.dataset.enigmaMarqueeTrack="";for(let A of Array.from(t.children))(S=A.dataset).enigmaMarqueeCopy??(S.enigmaMarqueeCopy="0");e.addEventListener("pointerdown",fe),e.addEventListener("pointerenter",ge),e.addEventListener("pointerleave",ye),e.addEventListener("touchstart",z,{passive:!0}),e.addEventListener("click",pe,!0),e.addEventListener("click",me),b?.addEventListener("change",he),Y?.observe(e);let u=t.children[0];u&&Y?.observe(u),D(),document.fonts?.ready.then(()=>D()),it()}function it(){for(let u of t.querySelectorAll("img")){if(u.complete)continue;let S=()=>{u.removeEventListener("load",S),u.removeEventListener("error",S),f||D()};u.addEventListener("load",S),u.addEventListener("error",S)}}return rt(),{get offset(){return s},get period(){return a},get copyCount(){return i},get dragging(){return v},get reducedMotion(){return d},update(u){let S=u.vertical!==void 0||u.manageStyles!==void 0;o={...o,...u},S&&k(),D()},measure:D,pause(){g=!0,R()},resume(){g=!1,R()},destroy(){f=!0,h&&cancelAnimationFrame(h),Y?.disconnect(),b?.removeEventListener("change",he),e.removeEventListener("pointerdown",fe),e.removeEventListener("pointerenter",ge),e.removeEventListener("pointerleave",ye),e.removeEventListener("touchstart",z),e.removeEventListener("click",pe,!0),e.removeEventListener("click",me),window.removeEventListener("pointermove",Z),window.removeEventListener("pointerup",F),window.removeEventListener("pointercancel",F)}}}var _={eye:["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z","M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],eyeOff:["M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9","m2 2 20 20","M9.9 9.9a3 3 0 0 0 4.2 4.2"],generate:["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z","M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"],clear:["m6 6 12 12","m18 6-12 12"],search:["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14Z","m20 20-4.2-4.2"]};function H(e){return`<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${e.map(n=>`<path d="${n}"/>`).join("")}</svg>`}var gt=H(_.eye),yt=H(_.eyeOff);function be(e,t){return typeof e=="function"?e(t):e}function ht(e,t){typeof t=="string"?e.innerHTML=t:e.replaceChildren(t)}function we(e,t={}){let n={...t},o=!1,a=!1,i=null,s=!n.container,r=n.container??document.createElement("span"),c=new Map;function g(){return{input:e,revealed:o,locked:e.disabled||e.readOnly}}function f(){return typeof n.reveal=="object"?n.reveal:{}}function h(){let d=f();return{name:"reveal",label:l=>l.revealed?d.hideLabel??"Hide password":d.showLabel??"Show password",icon:l=>l.revealed?d.hideIcon??yt:d.showIcon??gt,pressed:l=>l.revealed,onSelect:()=>E(!o)}}function T(){return e.type==="password"||o&&e.dataset.enigmaInputReveal==="on"}function v(){let d=n.actions??[];return[...n.reveal!==!1&&T()&&!d.some(I=>I.name==="reveal")?[h()]:[],...d]}function E(d){if(!T()&&d)return;let l=x();o=d,e.type=d?"text":"password",e.dataset.enigmaInputReveal=d?"on":"off",p(),l&&(M(l),i!==null&&clearTimeout(i),i=setTimeout(()=>{i=null,!a&&document.activeElement===e&&M(l)},0)),n.onRevealChange?.(d)}function x(){if(document.activeElement!==e)return null;try{let{selectionStart:d,selectionEnd:l}=e;return d===null||l===null?null:[d,l]}catch{return null}}function M(d){try{e.setSelectionRange(d[0],d[1])}catch{}}function p(){if(a)return;let d=x(),l=g(),y=v(),I=new Set;for(let O of y){I.add(O.name);let k=O.visible?O.visible(l):!0,C=c.get(O.name);if(C||(C=document.createElement("button"),C.type="button",C.dataset.enigmaInputAction=O.name,c.set(O.name,C),r.append(C)),!k){C.remove(),c.delete(O.name);continue}C.isConnected||r.append(C);let N=be(O.label,l);C.setAttribute("aria-label",N),C.title=N,C.disabled=l.locked,O.pressed?C.setAttribute("aria-pressed",String(O.pressed(l))):C.removeAttribute("aria-pressed"),ht(C,be(O.icon,l))}for(let[O,k]of[...c])I.has(O)||(k.remove(),c.delete(O));if(r.hidden=c.size===0,d)try{e.setSelectionRange(d[0],d[1])}catch{}}function w(d){d.target?.closest("[data-enigma-input-action]")&&d.preventDefault()}function L(d){let l=d.target?.closest("[data-enigma-input-action]");if(!l)return;let y=v().find(I=>I.name===l.dataset.enigmaInputAction);y&&(d.preventDefault(),y.onSelect(g()))}let m=typeof MutationObserver=="function"?new MutationObserver(()=>{!T()&&o&&(o=!1),p()}):null;function b(){e.dataset.enigmaInput="",r.dataset.enigmaInputActions="",r.dataset.position=n.position??"end",s&&!r.isConnected&&((n.position??"end")==="start"?e.before(r):e.after(r)),r.addEventListener("mousedown",w),r.addEventListener("click",L),m?.observe(e,{attributes:!0,attributeFilter:["type","disabled","readonly"]}),p()}return b(),{get revealed(){return o},reveal(d){E(d??!o)},refresh:p,update(d){let l=d.position!==void 0&&d.position!==n.position;n={...n,...d},l&&s&&(r.dataset.position=n.position??"end",n.position==="start"?e.before(r):e.after(r)),p()},destroy(){a=!0,i!==null&&clearTimeout(i),m?.disconnect(),r.removeEventListener("mousedown",w),r.removeEventListener("click",L);for(let d of c.values())d.remove();c.clear(),s&&r.remove(),delete e.dataset.enigmaInput,delete e.dataset.enigmaInputReveal,o&&(e.type="password")}}}function K(e){return e.normalize("NFD").replace(/\p{Diacritic}/gu,"").toLowerCase()}function Se(e,t){let n=e;for(let o of t.split(".")){if(n==null||typeof n!="object")return"";n=n[o]}return n==null?"":String(n)}function vt(e,t,n){let o=K(e),a=[];for(let i of t){let s=n.length?n:[""],r=null;for(let c=0;c<s.length;c++){let g=s[c],f=K(g?Se(i,g):String(i)),h=f.indexOf(o);if(h<0)continue;let T=f===o?0:(h+1)/(f.length+1)+c*.01;(!r||T<r.score)&&(r={item:i,score:T,key:g||void 0})}r&&a.push(r)}return a.sort((i,s)=>i.score-s.score)}function Te(e=[]){return(t,n)=>{let o=K(t),a=[];for(let i of n){let s=e.length?e:[""],r=null;for(let c of s){let g=K(c?Se(i,c):String(i)),f=bt(o,g);if(f<0)continue;let h=-f;(!r||h<r.score)&&(r={item:i,score:h,key:c||void 0})}r&&a.push(r)}return a.sort((i,s)=>i.score-s.score)}}function bt(e,t){if(!e)return 0;let n=0,o=0,a=0;for(let i=0;i<e.length;i++){let s=t.indexOf(e[i],n);if(s===-1)return-1;a=i>0&&s===n?a+1:0;let r=s===0||/[^a-z0-9]/.test(t[s-1]);o+=10+a*6+(r?8:0)-Math.min(s-n,6),n=s+1}return o}function Ee(e,t=32){let n=e.trim();return n.length<=t?n:`${n.slice(0,t-1).trimEnd()}\u2026`}function $(e={}){let t={...e},n=t.items??[],o="",a=[],i=null,s=null,r=!1,c=new Set;function g(){s=null,!(t.matcher||!t.fuse)&&(s=new t.fuse(n,{keys:t.keys??[],includeScore:!0,includeMatches:!0,threshold:.4,...t.fuseOptions}))}function f(v){let E=v.trim(),x=t.minLength??1,M;return E.length<x?M=(t.empty??"none")==="all"?n.map(p=>({item:p,score:0})):[]:t.matcher?M=t.matcher(E,n):s?M=s.search(E).map(p=>({item:p.item,score:p.score??0,key:p.matches?.[0]?.key})):M=vt(E,n,t.keys??[]),t.limit!=null?M.slice(0,t.limit):M}function h(v){o=v,a=f(v),t.onResults?.(a,o);for(let E of c)E(a,o);return a}function T(){i!==null&&(clearTimeout(i),i=null)}return g(),{get query(){return o},get results(){return a},search(v){if(r)return;T();let E=t.debounce??120;if(E<=0){h(v);return}i=setTimeout(()=>{i=null,h(v)},E)},searchNow(v){return T(),h(v)},setItems(v){n=v,g(),o&&h(o)},update(v){t={...t,...v},v.items&&(n=v.items),g(),o&&h(o)},attach(v){let E=()=>this.search(v.value),x=M=>{M.key!=="Escape"||!v.value||(M.stopPropagation(),v.value="",this.searchNow(""))};return v.addEventListener("input",E),v.addEventListener("keydown",x),v.dataset.enigmaSearch="",v.value&&this.searchNow(v.value),()=>{v.removeEventListener("input",E),v.removeEventListener("keydown",x),delete v.dataset.enigmaSearch}},subscribe(v){return c.add(v),()=>{c.delete(v)}},destroy(){r=!0,T(),c.clear(),s=null}}}function Q(e){if(!e.key||typeof window>"u")return null;try{let t=e.storage==="local"?window.localStorage:window.sessionStorage,n="__enigma_probe__";return t.setItem(n,"1"),t.removeItem(n),t}catch{return null}}function q(e){return e==null?null:typeof e=="number"?{ms:e}:e}function wt(e){let t=e;if(!t)return!1;let n=t.tagName;return n==="INPUT"||n==="TEXTAREA"||n==="SELECT"||t.isContentEditable===!0}function Oe(e={}){let t={...e},n=!!t.loading,o=0,a=null,i=!1,s=new Set,r=()=>{let p=q(t.cooldown);return p?.key?`enigma:cooldown:${p.key}`:null};function c(){let p=q(t.cooldown);if(!p)return;let w=Q(p),L=r();if(!w||!L)return;let m=Number(w.getItem(L));Number.isFinite(m)&&m>Date.now()&&(o=m)}function g(){return Math.max(0,o-Date.now())}function f(){let p=!!t.disabled,w=g();return{element:t.href?"a":"button",available:!p&&!n&&w===0,loading:n,disabled:p,cooldown:w,shortcut:t.shortcut??null}}function h(){let p=f();t.onChange?.(p);for(let w of s)w(p)}function T(){a!==null&&(clearInterval(a),a=null)}function v(){a!==null||g()===0||(a=setInterval(()=>{if(g()>0){h();return}T(),h()},100))}function E(){let p=q(t.cooldown);if(!p||p.ms<=0)return;o=Date.now()+p.ms;let w=Q(p),L=r();if(w&&L)try{w.setItem(L,String(o))}catch{}v()}async function x(p){if(i||!f().available)return;let w=t.onPress?.(p);if(w instanceof Promise){n=!0,h();try{await w}finally{n=!1,E(),h()}return}E(),h()}function M(p){!t.shortcut||wt(p.target)||p.ctrlKey||p.metaKey||p.altKey||p.shiftKey||p.key.toLowerCase()===t.shortcut.toLowerCase()&&f().available&&(p.preventDefault(),x(p))}return typeof window<"u"&&(c(),v(),window.addEventListener("keydown",M)),{get state(){return f()},press:x,update(p){let w=JSON.stringify(q(t.cooldown));t={...t,...p},p.loading!==void 0&&(n=!!p.loading),JSON.stringify(q(t.cooldown))!==w&&c(),h()},reset(){o=0,T();let p=q(t.cooldown),w=r();p&&w&&Q(p)?.removeItem(w),h()},subscribe(p){return s.add(p),()=>{s.delete(p)}},destroy(){i=!0,T(),s.clear(),typeof window<"u"&&window.removeEventListener("keydown",M)}}}var St="abcdefghijklmnopqrstuvwxyz",Tt="ABCDEFGHIJKLMNOPQRSTUVWXYZ",Et="0123456789",Le="!@#$%^&*()-_=+[]{};:,.?",Ot="Il1O0";function xe(e){let t=globalThis.crypto?.getRandomValues?.bind(globalThis.crypto);if(!t)throw new Error("Generating a password needs crypto.getRandomValues, which browsers only expose over HTTPS (or on localhost).");let n=Math.floor(2**32/e)*e,o=new Uint32Array(1),a;do t(o),a=o[0];while(a>=n);return a%e}function Me(e){return e[xe(e.length)]}function Mt(e){let{lowercase:t=!0,uppercase:n=!0,digits:o=!0,symbols:a=!0,excludeAmbiguous:i=!1,exclude:s=""}=e,r=new Set([...i?Ot:"",...s]),c=g=>[...g].filter(f=>!r.has(f)).join("");return[t?c(St):"",n?c(Tt):"",o?c(Et):"",a?c(Le):""].filter(Boolean)}function It(e){for(let t=e.length-1;t>0;t--){let n=xe(t+1);[e[t],e[n]]=[e[n],e[t]]}return e}function Ce(e={}){let{length:t=20,requireEachClass:n=!0}=e,o=Mt(e);if(!o.length)throw new Error("generatePassword: every character class was excluded.");if(t<1)throw new Error("generatePassword: length must be at least 1.");if(n&&t<o.length)throw new Error(`generatePassword: length ${t} cannot hold one character from each of the ${o.length} classes requested.`);let a=o.join(""),i=n?o.map(Me):[],s=Array.from({length:t-i.length},()=>Me(a));return It([...i,...s]).join("")}var Ie=new Set(["password","passwd","123456","12345678","123456789","1234567890","qwerty","qwertyuiop","abc123","111111","123123","admin","letmein","welcome","monkey","dragon","sunshine","iloveyou","princess","football","baseball","master","shadow","superman","batman","trustno1","hello","freedom","whatever","starwars","changeme","secret","login","root","toor","test","guest","azerty","1q2w3e4r","zaq12wsx"]),Lt=["abcdefghijklmnopqrstuvwxyz","0123456789","qwertyuiop","asdfghjkl","zxcvbnm"];function X(e){let t=0;return/[a-z]/.test(e)&&(t+=26),/[A-Z]/.test(e)&&(t+=26),/\d/.test(e)&&(t+=10),/[^\w\s]|_/.test(e)&&(t+=Le.length),/\s/.test(e)&&(t+=1),t||1}function xt(e){let t=1,n=1;for(let o=1;o<e.length;o++)n=e[o]===e[o-1]?n+1:1,t=Math.max(t,n);return t}function Ct(e){let t=e.toLowerCase(),n=0;for(let o of Lt){let a=[...o].reverse().join("");for(let i of[o,a])for(let s=0;s<i.length;s++)for(let r=i.length;r>s+n;r--)if(t.includes(i.slice(s,r))){n=Math.max(n,r-s);break}}return n}function At(e){return e.toLowerCase().replace(/^[^a-z]+/,"").replace(/[^a-z]+$/,"").replace(/[0!@$]/g,t=>({0:"o","!":"i","@":"a",$:"s"})[t]??t)}function Ae(e,t={}){if(!e)return{score:0,bits:0,warnings:[],empty:!0};let n=[],o=e.length*Math.log2(X(e)),a=At(e);(Ie.has(e.toLowerCase())||Ie.has(a))&&(o=Math.min(o,8),n.push("This is one of the most common passwords there is."));for(let c of t.userInputs??[]){let g=c.trim().toLowerCase();if(!(g.length<4||!e.toLowerCase().includes(g))){o=Math.min(o,16),n.push("It contains something you already typed on this form.");break}}let i=xt(e);i>=3&&(o-=(i-2)*Math.log2(X(e)),n.push("A character repeats several times in a row."));let s=Ct(e);return s>=4&&(o-=s*Math.log2(X(e))*.75,n.push("Part of it runs straight along the keyboard or the alphabet.")),e.length<8&&n.push("Short passwords fall to a brute force whatever they contain."),o=Math.max(0,Math.round(o)),{score:o<28?0:o<40?1:o<60?2:o<80?3:4,bits:o,warnings:n,empty:!1}}function ke(e={}){let t=typeof document>"u"?null:document,n=e.max??4,o=e.duration??5e3,a=new Set([...e.stickyTones??["error"],"loading"]),i=[],s=!1,r=0,c=new Set,g=new Map;function f(){let m=Object.freeze([...i]);for(let b of c)b(m)}function h(m){let b=g.get(m);b?.handle&&clearTimeout(b.handle),g.delete(m)}function T(m,b){if(!Number.isFinite(b)||b<=0)return;let d=s?null:setTimeout(()=>v(m),b);g.set(m,{handle:d,remaining:b,startedAt:Date.now()})}function v(m){let b=i.filter(d=>d.id!==m);b.length!==i.length&&(i=b,h(m),f())}function E(m){let b=m.tone??"info",d=m.duration??(a.has(b)?1/0:o),l=m.key?i.find(O=>O.key===m.key):void 0,y=l?.id??`n${++r}`,I={id:y,key:m.key,title:m.title,body:m.body,tone:b,duration:d,action:m.action,data:m.data,createdAt:Date.now()};if(i=l?i.map(O=>O.id===y?I:O):[...i,I],i.length>n){let O=i.find(k=>Number.isFinite(k.duration)&&k.id!==y);O?(i=i.filter(k=>k.id!==O.id),h(O.id)):i=i.slice(i.length-n)}return h(y),T(y,d),f(),y}let x=()=>{t&&(t.hidden?L.pause():L.resume())};function M(m,b){let d=i.find(I=>I.id===m);if(!d)return;let l=b.tone??d.tone,y=b.duration??(b.tone?a.has(l)?1/0:o:d.duration);i=i.map(I=>I.id===m?{...d,...b,tone:l,duration:y}:I),h(m),T(m,y),f()}function p(m,b){let d=typeof m=="function"?m(b):m;return typeof d=="string"?{title:d}:d}async function w(m,b){let d=typeof b.loading=="string"?{title:b.loading}:b.loading,l=d.key??`p${++r}`;E({...d,key:l,tone:"loading"});try{let y=await m;return E({...p(b.success,y),key:l,tone:"success"}),y}catch(y){throw E({...p(b.error,y),key:l,tone:"error"}),y}}let L={get items(){return Object.freeze([...i])},notify:E,update:M,promise:w,dismiss:v,dismissAll(){for(let m of i)h(m.id);i=[],f()},pause(){if(!s){s=!0;for(let[m,b]of g)b.handle&&(clearTimeout(b.handle),g.set(m,{handle:null,remaining:Math.max(b.remaining-(Date.now()-b.startedAt),0),startedAt:Date.now()}))}},resume(){if(s){s=!1;for(let[m,b]of[...g])h(m),T(m,b.remaining)}},subscribe(m){return c.add(m),()=>{c.delete(m)}},destroy(){for(let m of i)h(m.id);i=[],c.clear(),t?.removeEventListener("visibilitychange",x)}};return t?.addEventListener("visibilitychange",x),L}var J={online:!0,downlink:null,downlinkMax:null,effectiveType:null,rtt:null,saveData:null,type:null,recovered:!1,slow:!1};function Ne(){if(typeof navigator>"u")return null;let e=navigator;return e.connection??e.mozConnection??e.webkitConnection??null}function kt(e,t){for(let n of Object.keys(e))if(e[n]!==t[n])return!1;return!0}function Re(){if(typeof window>"u")return{get state(){return J},subscribe(){return()=>{}},destroy(){}};let e=new Set,t=!1,n=o();function o(){let s=Ne(),r=navigator.onLine,c=s?.effectiveType??null,g=s?.saveData??null;return{online:r,downlink:s?.downlink??null,downlinkMax:s?.downlinkMax??null,effectiveType:c,rtt:s?.rtt??null,saveData:g,type:s?.type??null,recovered:r&&t,slow:g===!0||c==="2g"||c==="slow-2g"}}function a(){navigator.onLine||(t=!0);let s=o();if(!kt(n,s)){n=s;for(let r of e)r(n)}}let i=Ne();return window.addEventListener("online",a,{passive:!0}),window.addEventListener("offline",a,{passive:!0}),i?.addEventListener("change",a,{passive:!0}),{get state(){return n},subscribe(s){return e.add(s),()=>{e.delete(s)}},destroy(){e.clear(),window.removeEventListener("online",a),window.removeEventListener("offline",a),i?.removeEventListener("change",a)}}}var Nt=/([zZ]|[+-]\d{2}:?\d{2})$/,Rt=/^\d{4}-\d{2}-\d{2}$/,Pt=/\d{2}:\d{2}/,Dt=/^([+-])?P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;function ee(e){let t=e.trim();if(!t||Rt.test(t)||Nt.test(t))return t;let n=t.replace(" ","T");return Pt.test(n)?`${n}Z`:n}function te(e){if(e==null)return null;if(e instanceof Date)return Number.isNaN(e.getTime())?null:e;if(typeof e=="number")return Number.isFinite(e)?new Date(e):null;let t=new Date(ee(e));return Number.isNaN(t.getTime())?null:t}function ne(e){let t=Dt.exec(e.trim());if(!t)return 0;let[,n,o,a,i,s,r,c,g]=t,f=T=>T?Number(T):0,h=f(o)*31536e6+f(a)*2592e6+f(i)*6048e5+f(s)*864e5+f(r)*36e5+f(c)*6e4+f(g)*1e3;return n==="-"?-h:h}function Ft(e){let{second:t,minute:n,hour:o,weekday:a,day:i,month:s,year:r,timeZone:c,timeZoneName:g}=e,f={timeZone:c,timeZoneName:g,second:t,minute:n,hour:o,weekday:a,day:i,month:s,year:r};for(let h of Object.keys(f))f[h]===void 0&&delete f[h];return f}function Pe(e,t,n){try{return new Intl.DateTimeFormat(t,n).format(e)}catch{return e.toISOString()}}function De(e,t={},n=new Date){let o=te(e);if(!o)return{date:null,iso:"",label:"",exact:"",beyondThreshold:!1,absoluteOnly:!1};let{locale:a,threshold:i="P30D",prefix:s="on",numericBeyondThreshold:r=!1}=t,g=Math.abs(o.getTime()-n.getTime())>ne(i),f=r&&g,h=f?{day:"numeric",month:"numeric",year:"numeric",timeZone:t.timeZone}:{day:"numeric",month:"short",year:"numeric",...Ft(t)},T=Pe(o,a,h);return{date:o,iso:o.toISOString(),label:!f&&s?`${s} ${T}`:T,exact:Pe(o,a,{dateStyle:"full",timeStyle:"long",timeZone:t.timeZone}),beyondThreshold:g,absoluteOnly:f}}function Fe(e,t={}){let n={datetime:e.iso,format:t.format,tense:t.tense,precision:t.precision,threshold:t.threshold,prefix:t.prefix,"format-style":t.formatStyle,second:t.second,minute:t.minute,hour:t.hour,weekday:t.weekday,day:t.day,month:t.month,year:t.year,lang:t.locale,"time-zone":t.timeZone,"time-zone-name":t.timeZoneName,"no-title":t.noTitle?"":void 0},o={};for(let[a,i]of Object.entries(n))i!==void 0&&(o[a]=i);return o}var oe="https://cdn.jsdelivr.net/gh/FJRG2007/enigma@main/assets/flags",He={shape:"rect",format:"svg",source:"cdn",basePath:"/flags",size:16},P={...He};function qe(e){return P={...P,...e},P}function Ue(){return P}function Be(){return P={...He},P}var re={rect:4/3,square:1,circle:1},_t=/^[\u{1F1E6}-\u{1F1FF}]{2}$/u,Ht=/^([A-Za-z]{2,3})[-_]([A-Z]{2})$/,qt=/^[a-z]{2,}[-_][a-z0-9_-]+$/,ze=/^[a-z]{2}$/;function Ke(e){return e.normalize("NFD").replace(/\p{Diacritic}/gu,"").replace(/['‘’ʼ`´]/g,"").toLowerCase().replace(/&/g," and ").replace(/\bst\.?\s/g,"saint ").replace(/^the\s+/,"").replace(/[.,]/g,"").replace(/\s+/g," ").trim()}var _e=new Map;function $e(e){try{return new Intl.Locale(`und-${e}`).region?.toLowerCase()??e.toLowerCase()}catch{return e.toLowerCase()}}function Ge(e){return e.of("ZZ")}function Ut(e){let t=_e.get(e);if(t!==void 0)return t;let n=null;try{let o=new Intl.DisplayNames([e],{type:"region"}),a=Ge(o);n=new Map;for(let i=65;i<=90;i++)for(let s=65;s<=90;s++){let r=String.fromCharCode(i,s),c=o.of(r);if(!c||c===r||c===a)continue;let g=Ke(c);n.has(g)||n.set(g,$e(r))}}catch{n=null}return _e.set(e,n),n}function Bt(e){try{let t=new Intl.DisplayNames(["en"],{type:"region"}),n=e.toUpperCase(),o=t.of(n);return!!o&&o!==n&&o!==Ge(t)}catch{return!0}}function B(e,t){if(!e)return null;let n=e.trim();if(!n)return null;if(_t.test(n))return[...n].map(r=>String.fromCharCode((r.codePointAt(0)??0)-127462+97)).join("");let o=Ht.exec(n);if(o)return o[2].toLowerCase();let a=n.toLowerCase();if(ze.test(a))return Bt(a)?$e(a):null;if(qt.test(a))return a;let i=Ke(n);for(let s of[t,Ve(),"en",zt()]){if(!s)continue;let r=Ut(s)?.get(i);if(r)return r}return null}function ie(e,t){let n=B(e);if(!n||!ze.test(n))return null;let o=n.toUpperCase();try{let i=new Intl.DisplayNames([t??Ve()??"en"],{type:"region"}).of(o);return!i||i===o?null:i}catch{return null}}function zt(){try{return Intl.DateTimeFormat().resolvedOptions().locale}catch{return}}function Ve(){if(!(typeof document>"u"))return document.documentElement.lang||void 0}var Kt=!1;function se(e,t={}){let n={...P,...t},o=B(e);if(!o)return null;let a=n.source==="cdn",i=n.format;return a&&i!=="svg"&&(!Kt&&typeof process<"u",i="svg"),`${a?oe:(n.source==="local"?n.basePath:n.source).replace(/\/+$/,"")}/${n.shape}/${o}.${i}`}function ae(e,t={}){let n={...P,...t},o=B(e),a=se(o,t);if(!o||!a)return null;let i=Math.max(1,Math.round(n.size));return{code:o,src:a,shape:n.shape,format:n.source==="cdn"?"svg":n.format,width:Math.round(i*re[n.shape]),height:i,name:ie(o,n.locale)}}function Ze(e,t={}){let n=ae(e,t);if(!n)return null;let o=t.decorative?"":t.label?.trim()||n.name||"";return{src:n.src,alt:o,width:n.width,height:n.height,loading:"lazy",decoding:"async","data-enigma-flag":"","data-flag-code":n.code,"data-flag-shape":n.shape,...o?{title:o}:{"aria-hidden":"true"}}}function U(e){return`${e.scope??""}\0${e.href??""}\0${e.href?"":e.term.trim().toLowerCase()}`}function Ye(e={}){let{key:t="enigma:palette:recent",limit:n=8,storage:o="local"}=e;function a(){if(typeof window>"u")return null;try{return o==="session"?window.sessionStorage:window.localStorage}catch{return null}}function i(){let r=a();if(!r)return[];try{let c=r.getItem(t);if(!c)return[];let g=JSON.parse(c);return Array.isArray(g)?g.filter(f=>!!f&&typeof f.term=="string").sort((f,h)=>h.at-f.at).slice(0,n):[]}catch{return[]}}function s(r){let c=a(),g=r.slice(0,n);try{c?.setItem(t,JSON.stringify(g))}catch{}return g}return{list:i,remember(r){let c={...r,at:r.at??Date.now()};if(!c.term.trim()&&!c.href)return i();let g=U(c);return s([c,...i().filter(f=>U(f)!==g)])},forget(r){let c=U(r);return s(i().filter(g=>U(g)!==c))},clear(){let r=a();try{r?.removeItem(t)}catch{}}}}function je(e,t){let n=[],o=new Map;return e.forEach((a,i)=>{let s=t(a),r=o.get(s);r||(r={label:s,rows:[]},o.set(s,r),n.push(r)),r.rows.push({row:a,position:i})}),n}function We(e,t,n,o=5){if(t<=0)return 0;switch(n){case"ArrowDown":return(e+1)%t;case"ArrowUp":return(e-1+t)%t;case"Home":return 0;case"End":return t-1;case"PageDown":return Math.min(t-1,e+o);case"PageUp":return Math.max(0,e-o)}}function Qe(e="K"){return`${typeof navigator<"u"&&/mac|iphone|ipad|ipod/i.test(navigator.platform||navigator.userAgent||"")?"\u2318":"Ctrl"} ${e.toUpperCase()}`}function Xe(e,t="k"){return e.key.toLowerCase()!==t.toLowerCase()?!1:e.metaKey||e.ctrlKey}var G=["label","description","group","keywords"],$t=600,Je=5;function et(e){return e.normalize("NFD").replace(/\p{Diacritic}/gu,"").toLowerCase()}function tt(e){return e==null?[]:(Array.isArray(e)?[...e]:[e]).filter(n=>n!=="")}function Gt(e,t){return e.length===t.length&&e.every((n,o)=>n===t[o])}function nt(e,t){return e.length===t.length&&e.every((n,o)=>n===t[o])}function Vt(e,t){return e.open===t.open&&e.query===t.query&&e.active===t.active&&e.multiple===t.multiple&&e.empty===t.empty&&Gt(e.value,t.value)&&nt(e.visible,t.visible)&&nt(e.selected,t.selected)}function ot(e={}){let t={...e},n=t.options??[],o=tt(t.value),a=!1,i="",s=-1,r=[...n],c="",g=0,f=!1,h=new Set,T=$({items:n,keys:t.searchKeys??G,fuse:t.fuse,fuseOptions:t.fuseOptions,matcher:t.matcher,debounce:0,empty:"all"});function v(l){return n.find(y=>y.value===l)}function E(){return o.map(v).filter(l=>!!l)}function x(){return{open:a,query:i,active:s,visible:r,selected:E(),value:[...o],multiple:!!t.multiple,empty:r.length===0}}function M(){let l=x();t.onChange?.(l);for(let y of h)y(l)}function p(l){let y=r.length;return(l%y+y)%y}function w(l,y){if(r.length===0)return-1;for(let I=0;I<r.length;I++){let O=p(l+y*I);if(!r[O]?.disabled)return O}return-1}let L;function m(){r=t.searchable&&i.trim()?T.searchNow(i).map(y=>y.item):[...n];let l=s>=0?r.findIndex(y=>y===L):-1;s=l>=0&&!r[l]?.disabled?l:w(0,1),L=r[s]}function b(){let l=E();t.onValueChange?.(t.multiple?[...o]:o[0]??"",l)}function d(){return t.closeOnSelect??!t.multiple}return m(),{get state(){return x()},setOpen(l){if(!(f||a===l)){if(a=l,a){i="",m();let y=o[0]?r.findIndex(I=>I.value===o[0]):-1;s=y>=0&&!r[y]?.disabled?y:w(0,1),L=r[s]}else i="",c="",m();M()}},toggleOpen(){this.setOpen(!a)},setQuery(l){f||(i=l,m(),M())},move(l){if(f||r.length===0)return;let y=s<0?-1:s;switch(l){case"ArrowDown":s=w(y+1,1);break;case"ArrowUp":s=w(y-1+r.length,-1);break;case"Home":s=w(0,1);break;case"End":s=w(r.length-1,-1);break;case"PageDown":s=w(Math.min(r.length-1,y+Je),1);break;case"PageUp":s=w(Math.max(0,y-Je),-1);break}L=r[s],M()},setActive(l){f||l===s||l<0||l>=r.length||r[l]?.disabled||(s=l,L=r[s],M())},select(l){if(f)return;let y=v(l);!y||y.disabled||(t.multiple?o=o.includes(l)?o.filter(I=>I!==l):[...o,l]:o=[l],b(),d()&&(a=!1,i="",m()),M())},selectActive(){let l=r[s];l&&this.select(l.value)},remove(l){f||!o.includes(l)||(o=o.filter(y=>y!==l),b(),M())},clear(){f||o.length===0||(o=[],b(),M())},typeahead(l){if(f||l.length!==1)return;let y=Date.now();c=y-g>$t?l:c+l,g=y;let I=et(c),O=c.length===1?1:0;for(let k=O;k<r.length+O;k++){let C=p(Math.max(s,0)+k),N=r[C];if(!(!N||N.disabled)&&et(N.label).startsWith(I)){s=C,L=N,M();return}}},update(l){if(f)return;let y=x(),I=l.options!==void 0;t={...t,...l},I&&(n=l.options??[]),l.value!==void 0&&(o=tt(l.value)),(I||l.searchKeys||l.fuse||l.fuseOptions||l.matcher)&&T.update({items:n,keys:t.searchKeys??G,fuse:t.fuse,fuseOptions:t.fuseOptions,matcher:t.matcher}),m(),Vt(y,x())||M()},subscribe(l){return h.add(l),()=>{h.delete(l)}},destroy(){f=!0,h.clear(),T.destroy()}}}return ft(Zt);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
5
5
  "type": "module",
6
6
  "files": [