@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 +691 -48
- package/assets/lib/enigma-primitives.js +2 -0
- package/package.json +1 -1
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 =
|
|
5079
|
+
$("palHint").textContent = shortcutLabel("K");
|
|
5005
5080
|
|
|
5006
5081
|
/**
|
|
5007
|
-
*
|
|
5008
|
-
*
|
|
5009
|
-
*
|
|
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
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
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
|
-
|
|
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
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
.
|
|
5059
|
-
|
|
5060
|
-
|
|
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(
|
|
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
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
+
|
|
5074
|
-
+
|
|
5075
|
-
|
|
5076
|
-
|
|
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 =
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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)
|
|
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) => {
|
|
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.
|
|
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": [
|