@enigmax/dashboard 0.1.15 → 0.1.17
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 +109 -14
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -161,6 +161,24 @@
|
|
|
161
161
|
}
|
|
162
162
|
.toggle:hover { color: var(--text); border-color: #2f3744; }
|
|
163
163
|
.toggle.on { background: var(--good); color: #06210f; border-color: var(--good); font-weight: 600; }
|
|
164
|
+
/* Boolean settings render as a sliding switch. Only the bool controls carry
|
|
165
|
+
data-kind="bool" - action buttons reuse .toggle and stay buttons. The On/Off
|
|
166
|
+
text is kept for screen readers but pushed off-screen; the knob is drawn with
|
|
167
|
+
::after, and the .on class (toggled by the same JS) slides it and greens the track. */
|
|
168
|
+
.toggle[data-kind="bool"] {
|
|
169
|
+
position: relative; width: 46px; min-width: 46px; height: 26px; padding: 0;
|
|
170
|
+
border-radius: 999px; background: var(--surface2); border: 1px solid var(--border);
|
|
171
|
+
color: transparent; overflow: hidden; text-indent: -999px;
|
|
172
|
+
transition: background .18s, border-color .18s;
|
|
173
|
+
}
|
|
174
|
+
.toggle[data-kind="bool"]::after {
|
|
175
|
+
content: ""; position: absolute; top: 50%; left: 3px; transform: translateY(-50%);
|
|
176
|
+
width: 18px; height: 18px; border-radius: 50%; background: #cdd2da;
|
|
177
|
+
transition: left .18s, background .18s;
|
|
178
|
+
}
|
|
179
|
+
.toggle[data-kind="bool"]:hover { border-color: #2f3744; }
|
|
180
|
+
.toggle[data-kind="bool"].on { background: var(--good); border-color: var(--good); }
|
|
181
|
+
.toggle[data-kind="bool"].on::after { left: 23px; background: #06210f; }
|
|
164
182
|
select.choice {
|
|
165
183
|
background: var(--surface2); color: var(--text); border: 1px solid var(--border);
|
|
166
184
|
border-radius: var(--radius-sm); padding: 6px 10px; font-size: 12px; font-family: var(--mono); cursor: pointer;
|
|
@@ -239,6 +257,35 @@
|
|
|
239
257
|
.tabs .tab { white-space: nowrap; }
|
|
240
258
|
.nav { margin-left: 0; }
|
|
241
259
|
}
|
|
260
|
+
/* Pretty scrollbars across the whole dashboard (thin, rounded, themed). */
|
|
261
|
+
* { scrollbar-width: thin; scrollbar-color: #2b3340 transparent; }
|
|
262
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
263
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
264
|
+
::-webkit-scrollbar-corner { background: transparent; }
|
|
265
|
+
::-webkit-scrollbar-thumb { background: #2b3340; border-radius: 999px; }
|
|
266
|
+
::-webkit-scrollbar-thumb:hover { background: #3a4150; }
|
|
267
|
+
/* Animated modal dialog - replaces native alert/prompt/confirm so dialogs keep the design. */
|
|
268
|
+
.modal-back {
|
|
269
|
+
position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center;
|
|
270
|
+
padding: 20px; background: rgba(4,6,10,0.62); backdrop-filter: blur(3px);
|
|
271
|
+
opacity: 0; transition: opacity .18s ease;
|
|
272
|
+
}
|
|
273
|
+
.modal-back.show { opacity: 1; }
|
|
274
|
+
.modal {
|
|
275
|
+
width: 100%; max-width: 420px; background: var(--surface); border: 1px solid var(--border);
|
|
276
|
+
border-radius: var(--radius); box-shadow: var(--shadow-2); padding: 22px;
|
|
277
|
+
transform: translateY(10px) scale(.98); opacity: 0; transition: transform .18s ease, opacity .18s ease;
|
|
278
|
+
}
|
|
279
|
+
.modal-back.show .modal { transform: none; opacity: 1; }
|
|
280
|
+
.modal-title { font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--text); }
|
|
281
|
+
.modal-msg { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 8px; word-break: break-word; }
|
|
282
|
+
.modal-input {
|
|
283
|
+
width: 100%; margin-top: 14px; background: var(--surface2); color: var(--text);
|
|
284
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 12px; font-size: 13px; font-family: var(--mono);
|
|
285
|
+
}
|
|
286
|
+
.modal-input:focus { outline: none; border-color: var(--accent); }
|
|
287
|
+
.modal-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
|
288
|
+
.modal-ok.danger { background: var(--accent2); border-color: var(--accent2); color: #2a0f08; }
|
|
242
289
|
</style>
|
|
243
290
|
</head>
|
|
244
291
|
<body>
|
|
@@ -938,6 +985,50 @@
|
|
|
938
985
|
// --- settings subpage (mirrors the TUI registry over /api/settings) ---
|
|
939
986
|
function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); }
|
|
940
987
|
|
|
988
|
+
// Capitalize a choice value for display ("on-demand" -> "On demand"), keeping the raw
|
|
989
|
+
// value as the option's value so the server still receives the exact token.
|
|
990
|
+
function prettyChoice(c) { c = String(c).replace(/[-_]/g, " "); return c.charAt(0).toUpperCase() + c.slice(1); }
|
|
991
|
+
|
|
992
|
+
// Animated modal replacing native alert/prompt/confirm (keeps the design). Returns a
|
|
993
|
+
// Promise: a prompt resolves to the entered string or null; a confirm resolves a boolean.
|
|
994
|
+
function openModal({ title, message, field, danger, okLabel }) {
|
|
995
|
+
return new Promise((resolve) => {
|
|
996
|
+
const back = document.createElement("div");
|
|
997
|
+
back.className = "modal-back";
|
|
998
|
+
let inner = "";
|
|
999
|
+
if (field && field.options) {
|
|
1000
|
+
inner = '<select class="modal-input">' + field.options.map((o) =>
|
|
1001
|
+
'<option value="' + esc(o.value) + '"' + (o.value === field.value ? " selected" : "") + ">" + esc(o.label || o.value) + "</option>").join("") + "</select>";
|
|
1002
|
+
} else if (field) {
|
|
1003
|
+
inner = '<input class="modal-input" type="text" value="' + esc(field.value || "") + '" placeholder="' + esc(field.placeholder || "") + '">';
|
|
1004
|
+
}
|
|
1005
|
+
back.innerHTML = '<div class="modal" role="dialog" aria-modal="true">'
|
|
1006
|
+
+ '<div class="modal-title">' + esc(title) + "</div>"
|
|
1007
|
+
+ (message ? '<div class="modal-msg">' + esc(message) + "</div>" : "")
|
|
1008
|
+
+ inner
|
|
1009
|
+
+ '<div class="modal-acts"><button type="button" class="toggle modal-cancel">Cancel</button>'
|
|
1010
|
+
+ '<button type="button" class="toggle on modal-ok' + (danger ? " danger" : "") + '">' + esc(okLabel || "OK") + "</button></div></div>";
|
|
1011
|
+
document.body.appendChild(back);
|
|
1012
|
+
requestAnimationFrame(() => back.classList.add("show"));
|
|
1013
|
+
const inp = back.querySelector(".modal-input");
|
|
1014
|
+
let done = false;
|
|
1015
|
+
const close = (val) => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); resolve(val); };
|
|
1016
|
+
const ok = () => close(field ? (inp ? inp.value : true) : true);
|
|
1017
|
+
const cancel = () => close(field ? null : false);
|
|
1018
|
+
back.querySelector(".modal-ok").addEventListener("click", ok);
|
|
1019
|
+
back.querySelector(".modal-cancel").addEventListener("click", cancel);
|
|
1020
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back) cancel(); });
|
|
1021
|
+
back.addEventListener("keydown", (e) => {
|
|
1022
|
+
if (e.key === "Escape") { e.preventDefault(); cancel(); }
|
|
1023
|
+
else if (e.key === "Enter" && (!inp || inp.tagName !== "TEXTAREA")) { e.preventDefault(); ok(); }
|
|
1024
|
+
});
|
|
1025
|
+
if (inp) { inp.focus(); if (inp.select) inp.select(); } else back.querySelector(".modal-ok").focus();
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
const askText = (title, value, message) => openModal({ title, message, field: { value: value || "" } }).then((v) => (v == null ? null : String(v).trim()));
|
|
1029
|
+
const askChoice = (title, options, value, message) => openModal({ title, message, field: { options, value } });
|
|
1030
|
+
const askConfirm = (title, message, danger) => openModal({ title, message, danger, okLabel: danger ? "Remove" : "OK" }).then(Boolean);
|
|
1031
|
+
|
|
941
1032
|
// Turn "(experimental)" and " · measured/experimental" label suffixes into visual badges.
|
|
942
1033
|
function labelWithBadges(text) {
|
|
943
1034
|
let name = String(text);
|
|
@@ -961,9 +1052,12 @@
|
|
|
961
1052
|
+ '<div class="set-hint">' + esc(s.hint) + (s.globalOnly ? " · global" : "") + "</div>"
|
|
962
1053
|
+ chips + adder + "</div></div>";
|
|
963
1054
|
}
|
|
964
|
-
|
|
1055
|
+
// A 2-value choice whose "off" is one of the two values is really a boolean, so
|
|
1056
|
+
// render it as a switch (like a pure bool); 3+ values stay a select with pretty labels.
|
|
1057
|
+
const asSwitch = !s.choices || (s.choices.length === 2 && s.choices.indexOf(s.offChoice) !== -1);
|
|
1058
|
+
const ctl = !asSwitch
|
|
965
1059
|
? '<select class="choice" data-skey="' + esc(s.key) + '">'
|
|
966
|
-
+ s.choices.map((c) => '<option value="' + esc(c) + '"' + (c === s.choice ? " selected" : "") + ">" + esc(c) + "</option>").join("")
|
|
1060
|
+
+ s.choices.map((c) => '<option value="' + esc(c) + '"' + (c === s.choice ? " selected" : "") + ">" + esc(prettyChoice(c)) + "</option>").join("")
|
|
967
1061
|
+ "</select>"
|
|
968
1062
|
: '<button type="button" class="toggle' + (s.value ? " on" : "") + '" data-skey="' + esc(s.key)
|
|
969
1063
|
+ '" data-kind="bool" data-on="' + (s.value ? 1 : 0) + '">' + (s.value ? "On" : "Off") + "</button>";
|
|
@@ -1260,12 +1354,12 @@
|
|
|
1260
1354
|
}
|
|
1261
1355
|
|
|
1262
1356
|
function wireSkills() {
|
|
1263
|
-
$("skillsBody").addEventListener("click", (e) => {
|
|
1357
|
+
$("skillsBody").addEventListener("click", async (e) => {
|
|
1264
1358
|
const b = e.target.closest("button[data-action]");
|
|
1265
1359
|
if (!b) return;
|
|
1266
1360
|
const { name, action } = b.dataset;
|
|
1267
1361
|
if (action === "edit") { openEditor(name, b.dataset.enigma === "1"); return; }
|
|
1268
|
-
if (action === "remove" && !
|
|
1362
|
+
if (action === "remove" && !(await askConfirm("Remove skill", 'Remove the external skill "' + name + '" from all agents? This deletes it from disk.', true))) return;
|
|
1269
1363
|
postSkill(name, action);
|
|
1270
1364
|
});
|
|
1271
1365
|
$("seSave").addEventListener("click", saveEditor);
|
|
@@ -1333,27 +1427,28 @@
|
|
|
1333
1427
|
} catch { $("accountsNote").textContent = "Could not reach the server."; }
|
|
1334
1428
|
}
|
|
1335
1429
|
function wireAccounts() {
|
|
1336
|
-
$("accAdd").addEventListener("click", () => {
|
|
1337
|
-
const tool = accTools.length === 1 ? accTools[0].name
|
|
1430
|
+
$("accAdd").addEventListener("click", async () => {
|
|
1431
|
+
const tool = accTools.length === 1 ? accTools[0].name
|
|
1432
|
+
: await askChoice("Add account", accTools.map((t) => ({ value: t.name, label: t.label || t.name })), accTools[0] ? accTools[0].name : "", "Which tool is this account for?");
|
|
1338
1433
|
if (!tool) return;
|
|
1339
|
-
const name =
|
|
1434
|
+
const name = await askText("Add account", "", "New account name (e.g. work):");
|
|
1340
1435
|
if (name) accAction("account.add", { tool, name });
|
|
1341
1436
|
});
|
|
1342
|
-
$("profAdd").addEventListener("click", () => { const name =
|
|
1343
|
-
$("accountsBody").addEventListener("click", (e) => {
|
|
1437
|
+
$("profAdd").addEventListener("click", async () => { const name = await askText("Add profile", "", "New profile name:"); if (name) accAction("profile.add", { name }); });
|
|
1438
|
+
$("accountsBody").addEventListener("click", async (e) => {
|
|
1344
1439
|
const b = e.target.closest("button[data-acc]"); if (!b) return;
|
|
1345
1440
|
const op = b.dataset.acc, tool = b.dataset.tool, name = b.dataset.name;
|
|
1346
1441
|
if (op === "activate") accAction("account.activate", { tool, name });
|
|
1347
|
-
else if (op === "remove") { if (
|
|
1348
|
-
else if (op === "rename") { const nn =
|
|
1442
|
+
else if (op === "remove") { if (await askConfirm("Remove account", "Remove account '" + name + "' and delete its local config dir?", true)) accAction("account.remove", { tool, name }); }
|
|
1443
|
+
else if (op === "rename") { const nn = await askText("Rename account", name, "Rename '" + name + "' to:"); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
|
|
1349
1444
|
else if (op === "login") { $("accountsNote").textContent = "To log in, run this in a terminal: enigma " + tool + " " + name; }
|
|
1350
1445
|
});
|
|
1351
|
-
$("profilesBody").addEventListener("click", (e) => {
|
|
1446
|
+
$("profilesBody").addEventListener("click", async (e) => {
|
|
1352
1447
|
const b = e.target.closest("button[data-prof-act]"); if (!b) return;
|
|
1353
1448
|
const op = b.dataset.profAct, name = b.dataset.name;
|
|
1354
1449
|
if (op === "activate") accAction("profile.activate", { name });
|
|
1355
|
-
else if (op === "remove") { if (
|
|
1356
|
-
else if (op === "rename") { const nn =
|
|
1450
|
+
else if (op === "remove") { if (await askConfirm("Remove profile", "Remove profile '" + name + "'? (its accounts are kept)", true)) accAction("profile.remove", { name }); }
|
|
1451
|
+
else if (op === "rename") { const nn = await askText("Rename profile", name, "Rename profile '" + name + "' to:"); if (nn && nn !== name) accAction("profile.rename", { name, newName: nn }); }
|
|
1357
1452
|
});
|
|
1358
1453
|
$("profilesBody").addEventListener("change", (e) => {
|
|
1359
1454
|
const sel = e.target.closest("select[data-prof-tool]"); if (!sel) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
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": [
|