@gamaze/hicortex 0.20.2 → 0.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/assets/dashboard.html +318 -8
- package/assets/identity.html +349 -8
- package/assets/viz.html +352 -8
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -0
- package/dist/dashboard.d.ts +17 -0
- package/dist/dashboard.js +32 -0
- package/dist/mcp-server.js +10 -0
- package/dist/mcp-stdio.d.ts +138 -0
- package/dist/mcp-stdio.js +308 -0
- package/package.json +4 -2
- package/server.json +44 -0
package/assets/identity.html
CHANGED
|
@@ -191,6 +191,68 @@
|
|
|
191
191
|
display: inline-block; padding: 1px 8px; border-radius: 10px;
|
|
192
192
|
background: rgba(128,128,128,0.18); font-size: 11px; color: var(--text-dim);
|
|
193
193
|
}
|
|
194
|
+
|
|
195
|
+
/* ---- account menu (#365) ---- */
|
|
196
|
+
/* Dropdown under the nav account element: token reveal/copy, connect
|
|
197
|
+
snippets, plan & billing, sign-out. The wrapper (#nav-account) becomes
|
|
198
|
+
the positioning context; the panel is absolute + right-aligned under it,
|
|
199
|
+
above page content (the panel gets z-60). */
|
|
200
|
+
.hc-nav-account { position: relative; }
|
|
201
|
+
.acct-trigger {
|
|
202
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
203
|
+
background: transparent; color: var(--text); border: none; border-radius: 6px;
|
|
204
|
+
padding: 4px 8px; font: inherit; cursor: pointer;
|
|
205
|
+
}
|
|
206
|
+
.acct-trigger:hover { background: rgba(128,128,128,0.15); }
|
|
207
|
+
.acct-caret { color: var(--text-dim); font-size: 10px; }
|
|
208
|
+
.acct-menu {
|
|
209
|
+
display: none; position: absolute; top: calc(100% + 8px); right: 0;
|
|
210
|
+
min-width: 320px; max-width: min(420px, calc(100vw - 40px));
|
|
211
|
+
background: var(--panel); border: 1px solid var(--panel-border); border-radius: 8px;
|
|
212
|
+
padding: 12px; z-index: 60;
|
|
213
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
|
214
|
+
color: var(--text); font-size: 13px; text-align: left;
|
|
215
|
+
}
|
|
216
|
+
.acct-menu.open { display: block; }
|
|
217
|
+
.acct-label {
|
|
218
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
|
|
219
|
+
color: var(--text-dim); margin: 8px 0 4px;
|
|
220
|
+
}
|
|
221
|
+
.acct-menu .acct-label:first-child { margin-top: 0; }
|
|
222
|
+
.acct-token-row { display: flex; align-items: center; gap: 6px; }
|
|
223
|
+
.acct-token {
|
|
224
|
+
flex: 1; min-width: 0;
|
|
225
|
+
font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
226
|
+
background: var(--bg); border: 1px solid var(--panel-border); border-radius: 4px;
|
|
227
|
+
padding: 4px 8px; color: var(--text);
|
|
228
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
229
|
+
}
|
|
230
|
+
.acct-btn {
|
|
231
|
+
background: var(--panel); color: var(--text);
|
|
232
|
+
border: 1px solid var(--panel-border); border-radius: 4px;
|
|
233
|
+
padding: 4px 10px; font: inherit; font-size: 12px; cursor: pointer;
|
|
234
|
+
}
|
|
235
|
+
.acct-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
236
|
+
.acct-btn:disabled { opacity: 0.5; cursor: default; }
|
|
237
|
+
.acct-menu details { border-top: 1px solid var(--panel-border); margin-top: 10px; padding-top: 8px; }
|
|
238
|
+
.acct-menu summary { cursor: pointer; color: var(--text-dim); font-size: 13px; }
|
|
239
|
+
.acct-menu summary:hover { color: var(--accent); }
|
|
240
|
+
.acct-snippet { margin-bottom: 8px; }
|
|
241
|
+
.acct-snippet pre {
|
|
242
|
+
background: var(--bg); border: 1px solid var(--panel-border); border-radius: 4px;
|
|
243
|
+
padding: 8px; margin: 4px 0;
|
|
244
|
+
font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
245
|
+
white-space: pre-wrap; word-break: break-all;
|
|
246
|
+
max-height: 200px; overflow-y: auto;
|
|
247
|
+
}
|
|
248
|
+
.acct-link {
|
|
249
|
+
display: block; width: 100%; text-align: left;
|
|
250
|
+
background: transparent; border: none; border-radius: 4px;
|
|
251
|
+
color: var(--text); font: inherit; font-size: 13px;
|
|
252
|
+
padding: 6px 8px; cursor: pointer;
|
|
253
|
+
}
|
|
254
|
+
.acct-link:hover { background: rgba(128,128,128,0.15); }
|
|
255
|
+
.acct-link.acct-danger:hover { color: var(--danger); }
|
|
194
256
|
</style>
|
|
195
257
|
</head>
|
|
196
258
|
<body>
|
|
@@ -325,28 +387,43 @@
|
|
|
325
387
|
}
|
|
326
388
|
|
|
327
389
|
// =========================================================================
|
|
328
|
-
// Account identity in the nav — GET /account (the
|
|
329
|
-
// dashboard payload also carries). Fail-soft:
|
|
330
|
-
// span empty (the main load() owns the 401 token
|
|
331
|
-
// account (self-hosted default) renders nothing
|
|
332
|
-
// only — this page never
|
|
390
|
+
// Account identity in the nav + the ACCOUNT MENU (#365) — GET /account (the
|
|
391
|
+
// lightweight endpoint the dashboard payload also carries). Fail-soft:
|
|
392
|
+
// 401/fetch error leaves the span empty (the main load() owns the 401 token
|
|
393
|
+
// prompt); an all-null account (self-hosted default) renders nothing — no
|
|
394
|
+
// menu either. Built with createElement/textContent only — this page never
|
|
395
|
+
// uses innerHTML for server-derived values.
|
|
333
396
|
// =========================================================================
|
|
334
397
|
function renderAccount(a) {
|
|
335
398
|
var host = document.getElementById("nav-account");
|
|
336
399
|
if (!host) return;
|
|
337
400
|
host.textContent = "";
|
|
338
401
|
if (!a || (a.name === null && a.org === null && a.plan === null)) return;
|
|
402
|
+
var trigger = document.createElement("button");
|
|
403
|
+
trigger.type = "button";
|
|
404
|
+
trigger.id = "acct-trigger";
|
|
405
|
+
trigger.className = "acct-trigger";
|
|
406
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
407
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
339
408
|
var who = [a.name, a.org].filter(function (s) { return typeof s === "string" && s !== ""; });
|
|
340
409
|
who.forEach(function (s, i) {
|
|
341
|
-
if (i > 0)
|
|
342
|
-
|
|
410
|
+
if (i > 0) trigger.appendChild(document.createTextNode(" · "));
|
|
411
|
+
trigger.appendChild(document.createTextNode(s));
|
|
343
412
|
});
|
|
344
413
|
if (typeof a.plan === "string" && a.plan !== "") {
|
|
345
414
|
var pill = document.createElement("span");
|
|
346
415
|
pill.className = "plan";
|
|
347
416
|
pill.textContent = a.plan;
|
|
348
|
-
|
|
417
|
+
trigger.appendChild(pill);
|
|
349
418
|
}
|
|
419
|
+
var caret = document.createElement("span");
|
|
420
|
+
caret.className = "acct-caret";
|
|
421
|
+
caret.setAttribute("aria-hidden", "true");
|
|
422
|
+
caret.textContent = "▾";
|
|
423
|
+
trigger.appendChild(caret);
|
|
424
|
+
host.appendChild(trigger);
|
|
425
|
+
host.appendChild(buildAcctMenu());
|
|
426
|
+
wireAcctMenu();
|
|
350
427
|
}
|
|
351
428
|
function loadAccount() {
|
|
352
429
|
fetch("/account", { headers: authHeaders() })
|
|
@@ -355,6 +432,270 @@
|
|
|
355
432
|
.catch(function () { /* leave the span empty — nav is never worth an error */ });
|
|
356
433
|
}
|
|
357
434
|
|
|
435
|
+
// =========================================================================
|
|
436
|
+
// Account menu (#365) — dropdown under the nav account element: token
|
|
437
|
+
// reveal/copy, connect snippets, plan & billing, sign-out. The token shown
|
|
438
|
+
// is AUTHORITATIVE server truth: a lazy GET /account/token on the FIRST
|
|
439
|
+
// open (echo-only endpoint — the fetch already proves the caller holds the
|
|
440
|
+
// token). localStorage is NEVER trusted for the display: it can be stale
|
|
441
|
+
// after token rotation and is absent entirely for browser sessions the
|
|
442
|
+
// hosted router authenticates via its session→bearer injection. On fetch
|
|
443
|
+
// failure the row fails explicitly ("token unavailable", actions disabled).
|
|
444
|
+
// Built with createElement/textContent only (page convention).
|
|
445
|
+
// =========================================================================
|
|
446
|
+
var acctToken = null; // token from /account/token (server truth)
|
|
447
|
+
var acctTokenState = "unfetched"; // "unfetched" | "ok" | "unavailable"
|
|
448
|
+
var acctTokenFetched = false; // one lazy fetch attempt per page load
|
|
449
|
+
var acctRevealed = false; // unmasked while the menu is open
|
|
450
|
+
|
|
451
|
+
function maskToken(t) {
|
|
452
|
+
// hctx-••••••23f9 — first 5 chars + 6 bullets + last 4. Real tokens are
|
|
453
|
+
// hctx-<32hex>; the guard keeps a degenerate short token from leaking.
|
|
454
|
+
if (typeof t !== "string" || t.length < 10) return "••••••••••";
|
|
455
|
+
return t.slice(0, 5) + "••••••" + t.slice(-4);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function el(tag, id, cls, text) {
|
|
459
|
+
var n = document.createElement(tag);
|
|
460
|
+
if (id) n.id = id;
|
|
461
|
+
if (cls) n.className = cls;
|
|
462
|
+
if (text !== undefined) n.textContent = text;
|
|
463
|
+
return n;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Static menu skeleton — token-dependent values fill in renderAcctToken().
|
|
467
|
+
function buildAcctMenu() {
|
|
468
|
+
var menu = el("div", "acct-menu", "acct-menu");
|
|
469
|
+
menu.appendChild(el("div", null, "acct-label", "Connection token"));
|
|
470
|
+
var row = el("div", null, "acct-token-row");
|
|
471
|
+
row.appendChild(el("code", "acct-token-val", "acct-token", "…"));
|
|
472
|
+
row.appendChild(el("button", "acct-reveal", "acct-btn", "Reveal"));
|
|
473
|
+
row.appendChild(el("button", "acct-copy-token", "acct-btn", "Copy"));
|
|
474
|
+
menu.appendChild(row);
|
|
475
|
+
var details = el("details", "acct-connect");
|
|
476
|
+
details.appendChild(el("summary", null, null, "Connect an agent"));
|
|
477
|
+
details.appendChild(el("div", null, "acct-label", "CLI"));
|
|
478
|
+
var snip1 = el("div", null, "acct-snippet");
|
|
479
|
+
snip1.appendChild(el("pre", "acct-cli"));
|
|
480
|
+
snip1.appendChild(el("button", "acct-copy-cli", "acct-btn", "Copy"));
|
|
481
|
+
details.appendChild(snip1);
|
|
482
|
+
details.appendChild(el("div", null, "acct-label", "MCP — ~/.claude.json"));
|
|
483
|
+
var snip2 = el("div", null, "acct-snippet");
|
|
484
|
+
snip2.appendChild(el("pre", "acct-mcp"));
|
|
485
|
+
snip2.appendChild(el("button", "acct-copy-mcp", "acct-btn", "Copy"));
|
|
486
|
+
details.appendChild(snip2);
|
|
487
|
+
menu.appendChild(details);
|
|
488
|
+
menu.appendChild(el("button", "acct-plan", "acct-link", "Plan & billing"));
|
|
489
|
+
menu.appendChild(el("button", "acct-signout", "acct-link acct-danger", "Sign out"));
|
|
490
|
+
// All menu buttons are type=button (never submit).
|
|
491
|
+
var btns = menu.querySelectorAll("button");
|
|
492
|
+
for (var i = 0; i < btns.length; i++) btns[i].type = "button";
|
|
493
|
+
return menu;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// The connect line: server URL + token on ONE line, space-separated.
|
|
497
|
+
function acctConnectLine() {
|
|
498
|
+
return location.origin + " " + acctToken;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// CLI snippet — init has NO --token flag; it prompts interactively. The
|
|
502
|
+
// comment mirrors init.ts's own printed hint ("When prompted for the token,
|
|
503
|
+
// paste: …").
|
|
504
|
+
function acctCliText() {
|
|
505
|
+
return "# When prompted for the token, paste: " + acctToken +
|
|
506
|
+
"\nnpx @gamaze/hicortex init --server " + location.origin;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// MCP snippet — mirrors EXACTLY what runClientInit writes into
|
|
510
|
+
// ~/.claude.json (init.ts): SSE transport at <origin>/sse with a bearer
|
|
511
|
+
// Authorization header.
|
|
512
|
+
function acctMcpText() {
|
|
513
|
+
return JSON.stringify({
|
|
514
|
+
mcpServers: {
|
|
515
|
+
hicortex: {
|
|
516
|
+
type: "sse",
|
|
517
|
+
url: location.origin + "/sse",
|
|
518
|
+
headers: { Authorization: "Bearer " + acctToken }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}, null, 2);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function renderAcctToken() {
|
|
525
|
+
var val = document.getElementById("acct-token-val");
|
|
526
|
+
if (!val) return; // menu not rendered (self-hosted all-null account)
|
|
527
|
+
var revealBtn = document.getElementById("acct-reveal");
|
|
528
|
+
var copyToken = document.getElementById("acct-copy-token");
|
|
529
|
+
var copyCli = document.getElementById("acct-copy-cli");
|
|
530
|
+
var copyMcp = document.getElementById("acct-copy-mcp");
|
|
531
|
+
var cli = document.getElementById("acct-cli");
|
|
532
|
+
var mcp = document.getElementById("acct-mcp");
|
|
533
|
+
if (acctTokenState !== "ok") {
|
|
534
|
+
val.textContent = acctTokenState === "unfetched" ? "…" : "token unavailable";
|
|
535
|
+
revealBtn.disabled = true;
|
|
536
|
+
copyToken.disabled = true;
|
|
537
|
+
copyCli.disabled = true;
|
|
538
|
+
copyMcp.disabled = true;
|
|
539
|
+
cli.textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
|
|
540
|
+
mcp.textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
revealBtn.disabled = false;
|
|
544
|
+
copyToken.disabled = false;
|
|
545
|
+
copyCli.disabled = false;
|
|
546
|
+
copyMcp.disabled = false;
|
|
547
|
+
val.textContent = acctRevealed ? acctToken : maskToken(acctToken);
|
|
548
|
+
revealBtn.textContent = acctRevealed ? "Hide" : "Reveal";
|
|
549
|
+
cli.textContent = acctCliText();
|
|
550
|
+
mcp.textContent = acctMcpText();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function fetchAcctToken() {
|
|
554
|
+
if (acctTokenFetched) return;
|
|
555
|
+
acctTokenFetched = true;
|
|
556
|
+
fetch("/account/token", { headers: authHeaders() })
|
|
557
|
+
.then(function (resp) {
|
|
558
|
+
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
|
559
|
+
return resp.json();
|
|
560
|
+
})
|
|
561
|
+
.then(function (data) {
|
|
562
|
+
if (typeof data.token !== "string" || !data.token) throw new Error("bad payload");
|
|
563
|
+
acctToken = data.token;
|
|
564
|
+
acctTokenState = "ok";
|
|
565
|
+
renderAcctToken();
|
|
566
|
+
})
|
|
567
|
+
.catch(function () {
|
|
568
|
+
acctTokenState = "unavailable"; // fail explicitly — no localStorage fallback
|
|
569
|
+
renderAcctToken();
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function closeAcctMenu() {
|
|
574
|
+
var menu = document.getElementById("acct-menu");
|
|
575
|
+
if (!menu) return;
|
|
576
|
+
menu.classList.remove("open");
|
|
577
|
+
var trigger = document.getElementById("acct-trigger");
|
|
578
|
+
if (trigger) trigger.setAttribute("aria-expanded", "false");
|
|
579
|
+
// Masked again + connect section collapsed on EVERY close (locked design).
|
|
580
|
+
acctRevealed = false;
|
|
581
|
+
var details = document.getElementById("acct-connect");
|
|
582
|
+
if (details) details.removeAttribute("open");
|
|
583
|
+
renderAcctToken();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function openAcctMenu() {
|
|
587
|
+
var menu = document.getElementById("acct-menu");
|
|
588
|
+
if (!menu) return;
|
|
589
|
+
menu.classList.add("open");
|
|
590
|
+
document.getElementById("acct-trigger").setAttribute("aria-expanded", "true");
|
|
591
|
+
fetchAcctToken(); // lazy — one attempt, on first open only
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Clipboard helper: async API when available, temporary <textarea> +
|
|
595
|
+
// execCommand fallback (http remote origins lack the async clipboard API).
|
|
596
|
+
// Brief "Copied" feedback (label swap for ~1.5 s); total failure alerts.
|
|
597
|
+
function copyToClipboard(text, btn) {
|
|
598
|
+
var flash = function () {
|
|
599
|
+
var orig = btn.dataset.origLabel || (btn.dataset.origLabel = btn.textContent);
|
|
600
|
+
btn.textContent = "Copied";
|
|
601
|
+
clearTimeout(btn._copiedTimer);
|
|
602
|
+
btn._copiedTimer = setTimeout(function () { btn.textContent = orig; }, 1500);
|
|
603
|
+
};
|
|
604
|
+
var legacy = function () {
|
|
605
|
+
try {
|
|
606
|
+
var ta = document.createElement("textarea");
|
|
607
|
+
ta.value = text;
|
|
608
|
+
ta.style.position = "fixed";
|
|
609
|
+
ta.style.opacity = "0";
|
|
610
|
+
document.body.appendChild(ta);
|
|
611
|
+
ta.select();
|
|
612
|
+
var ok = document.execCommand("copy");
|
|
613
|
+
document.body.removeChild(ta);
|
|
614
|
+
return ok;
|
|
615
|
+
} catch (e) { return false; }
|
|
616
|
+
};
|
|
617
|
+
var fallback = function () {
|
|
618
|
+
if (legacy()) { flash(); return; }
|
|
619
|
+
alert("Copy failed — select the text and copy it manually.");
|
|
620
|
+
};
|
|
621
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
622
|
+
navigator.clipboard.writeText(text).then(flash, fallback);
|
|
623
|
+
} else {
|
|
624
|
+
fallback();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Sign-out is CLIENT-SIDE ONLY (#365): there is no server session to
|
|
629
|
+
// invalidate in the Hicortex server itself — auth is per-request bearer, so
|
|
630
|
+
// dropping the locally stored tokens IS the sign-out. (The hosted router
|
|
631
|
+
// separately keeps a Google session cookie and offers /auth/logout — out of
|
|
632
|
+
// scope here; this only ends the browser's console session.)
|
|
633
|
+
function signOut() {
|
|
634
|
+
try {
|
|
635
|
+
localStorage.removeItem("hicortexToken");
|
|
636
|
+
localStorage.removeItem("hicortex-dashboard-token");
|
|
637
|
+
} catch (e) { /* private mode — reload anyway */ }
|
|
638
|
+
location.reload(); // back to the unauthenticated 401-prompt state
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function wireAcctMenu() {
|
|
642
|
+
var trigger = document.getElementById("acct-trigger");
|
|
643
|
+
if (!trigger) return;
|
|
644
|
+
trigger.addEventListener("click", function () {
|
|
645
|
+
var menu = document.getElementById("acct-menu");
|
|
646
|
+
if (menu && menu.classList.contains("open")) closeAcctMenu();
|
|
647
|
+
else openAcctMenu();
|
|
648
|
+
});
|
|
649
|
+
document.getElementById("acct-reveal").addEventListener("click", function () {
|
|
650
|
+
acctRevealed = !acctRevealed;
|
|
651
|
+
renderAcctToken();
|
|
652
|
+
});
|
|
653
|
+
var copyToken = document.getElementById("acct-copy-token");
|
|
654
|
+
copyToken.addEventListener("click", function () {
|
|
655
|
+
if (acctTokenState === "ok") copyToClipboard(acctConnectLine(), copyToken);
|
|
656
|
+
});
|
|
657
|
+
var copyCli = document.getElementById("acct-copy-cli");
|
|
658
|
+
copyCli.addEventListener("click", function () {
|
|
659
|
+
if (acctTokenState === "ok") {
|
|
660
|
+
copyToClipboard("npx @gamaze/hicortex init --server " + location.origin, copyCli);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
var copyMcp = document.getElementById("acct-copy-mcp");
|
|
664
|
+
copyMcp.addEventListener("click", function () {
|
|
665
|
+
if (acctTokenState === "ok") copyToClipboard(acctMcpText(), copyMcp);
|
|
666
|
+
});
|
|
667
|
+
// Plan & billing lives on the dashboard page — navigate there (append
|
|
668
|
+
// ?token= when one is known, mirroring the shared-nav link handler) and
|
|
669
|
+
// land on the digest panel's budget/usage anchor (#371 lands there).
|
|
670
|
+
document.getElementById("acct-plan").addEventListener("click", function () {
|
|
671
|
+
var t = null;
|
|
672
|
+
try {
|
|
673
|
+
t = token
|
|
674
|
+
|| localStorage.getItem("hicortexToken")
|
|
675
|
+
|| localStorage.getItem("hicortex-dashboard-token");
|
|
676
|
+
} catch (e) { /* private mode — navigate without a token */ }
|
|
677
|
+
var u = new URL("/dashboard", location.origin);
|
|
678
|
+
if (t) u.searchParams.set("token", t);
|
|
679
|
+
u.hash = "plan-billing";
|
|
680
|
+
location.href = u.toString();
|
|
681
|
+
});
|
|
682
|
+
document.getElementById("acct-signout").addEventListener("click", signOut);
|
|
683
|
+
renderAcctToken();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Click-outside + Escape close the menu (wired once; the handlers no-op
|
|
687
|
+
// when the menu isn't rendered).
|
|
688
|
+
document.addEventListener("click", function (e) {
|
|
689
|
+
var menu = document.getElementById("acct-menu");
|
|
690
|
+
if (!menu || !menu.classList.contains("open")) return;
|
|
691
|
+
var wrap = document.getElementById("nav-account");
|
|
692
|
+
if (wrap && wrap.contains(e.target)) return;
|
|
693
|
+
closeAcctMenu();
|
|
694
|
+
});
|
|
695
|
+
window.addEventListener("keydown", function (e) {
|
|
696
|
+
if (e.key === "Escape") closeAcctMenu();
|
|
697
|
+
});
|
|
698
|
+
|
|
358
699
|
// =========================================================================
|
|
359
700
|
// DOM refs
|
|
360
701
|
// =========================================================================
|