Key editor: folded hold and say lines, one-line check rows that unfold, stacked fields, inputs never overflow the card
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+16
-12
@@ -314,7 +314,7 @@
|
|||||||
if (state.draft) { try { const v = await post("/api/validate", {}); if (v.plan && v.plan.pages.length) note = `<div class="notice">Layout changed: Apply re-imports the ${v.plan.pages.filter((p) => p === "status" || p === "restart").map((p) => p[0].toUpperCase() + p.slice(1)).join(" and ")} page.</div>`; } catch (e) {} }
|
if (state.draft) { try { const v = await post("/api/validate", {}); if (v.plan && v.plan.pages.length) note = `<div class="notice">Layout changed: Apply re-imports the ${v.plan.pages.filter((p) => p === "status" || p === "restart").map((p) => p[0].toUpperCase() + p.slice(1)).join(" and ")} page.</div>`; } catch (e) {} }
|
||||||
const adders = editing() ? (restart ? `<button id="newrkey" class="small">+ Restart key</button>` : `<button id="newkey" class="small">+ Key</button>`) : "";
|
const adders = editing() ? (restart ? `<button id="newrkey" class="small">+ Restart key</button>` : `<button id="newkey" class="small">+ Key</button>`) : "";
|
||||||
main.innerHTML = `<h1>Keys${hint("The deck as people hold it. Click a key to see what it watches and what holding it does.")}</h1>${tabs}${note}
|
main.innerHTML = `<h1>Keys${hint("The deck as people hold it. Click a key to see what it watches and what holding it does.")}</h1>${tabs}${note}
|
||||||
<div class="cols"><div>${restart && !c.restart_page ? "<p class='muted'>No Restart page.</p>" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)}
|
<div class="cols keycols"><div>${restart && !c.restart_page ? "<p class='muted'>No Restart page.</p>" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)}
|
||||||
${!restart ? `<p class="small muted tray" id="tray" style="margin-top:8px">In a group only${hint("Keys with no spot of their own: they show inside their group key. Drop a key here to hide it.")}: ${tray || "<span class='muted'>none</span>"}</p>` : ""}
|
${!restart ? `<p class="small muted tray" id="tray" style="margin-top:8px">In a group only${hint("Keys with no spot of their own: they show inside their group key. Drop a key here to hide it.")}: ${tray || "<span class='muted'>none</span>"}</p>` : ""}
|
||||||
<div class="row" style="margin-top:8px">${adders}</div></div><div id="panel">${panel}</div></div>`;
|
<div class="row" style="margin-top:8px">${adders}</div></div><div id="panel">${panel}</div></div>`;
|
||||||
wireKeyPanel(sel, restart);
|
wireKeyPanel(sel, restart);
|
||||||
@@ -412,10 +412,13 @@
|
|||||||
const k = await api("/api/keys");
|
const k = await api("/api/keys");
|
||||||
const kt = k.tiles.find((x) => x.id === t.id) || { says: [], holding: "", checks: [] };
|
const kt = k.tiles.find((x) => x.id === t.id) || { says: [], holding: "", checks: [] };
|
||||||
const fixOptions = (cur) => `<option value="">none</option><option value="restart-connection" ${cur === "restart-connection" ? "selected" : ""}>reconnect its Companion connection</option><option value="dante-auto" ${cur === "dante-auto" ? "selected" : ""}>Dante auto (PoE port or the Dante module)</option>${Object.entries(c.fixes).map(([id, f]) => `<option value="${esc(id)}" ${cur === id ? "selected" : ""}>${esc(f.short || id)} – ${esc(f.label || "")}</option>`).join("")}`;
|
const fixOptions = (cur) => `<option value="">none</option><option value="restart-connection" ${cur === "restart-connection" ? "selected" : ""}>reconnect its Companion connection</option><option value="dante-auto" ${cur === "dante-auto" ? "selected" : ""}>Dante auto (PoE port or the Dante module)</option>${Object.entries(c.fixes).map(([id, f]) => `<option value="${esc(id)}" ${cur === id ? "selected" : ""}>${esc(f.short || id)} – ${esc(f.label || "")}</option>`).join("")}`;
|
||||||
|
const short = (text, n) => { text = String(text || ""); return text.length > n ? text.slice(0, n - 1) + "…" : text; };
|
||||||
const checks = (t.checks || []).map((chk, j) => {
|
const checks = (t.checks || []).map((chk, j) => {
|
||||||
const spec = state.schema.checks[chk.type] || { fields: [], question: "" };
|
const spec = state.schema.checks[chk.type] || { fields: [], question: "" };
|
||||||
const sentence = (kt.checks[j] || {}).sentence || chk.type;
|
const sentence = (kt.checks[j] || {}).sentence || chk.type;
|
||||||
if (!editing()) return `<li>${esc(sentence)}</li>`;
|
const what = chk.conn || chk.peer || chk.name || chk.msg || (chk.params || {}).process || chk.host || chk.label || "";
|
||||||
|
const summary = `<summary><b>${esc(spec.question || chk.type)}</b> <span class="muted">${esc(short(what || sentence, 40))}</span>${hint(sentence)}</summary>`;
|
||||||
|
if (!editing()) return `<details class="chk">${summary}<p class="small">${esc(sentence)}</p></details>`;
|
||||||
const base = `tiles.${i}.checks.${j}.`;
|
const base = `tiles.${i}.checks.${j}.`;
|
||||||
let fields = (spec.fields || []).filter((f) => !["fix", "type", "template", "params"].includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("");
|
let fields = (spec.fields || []).filter((f) => !["fix", "type", "template", "params"].includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("");
|
||||||
if (chk.type === "remote") {
|
if (chk.type === "remote") {
|
||||||
@@ -427,21 +430,22 @@
|
|||||||
const fixSel = typeof chk.fix === "object" && chk.fix ? `<div class="field"><span class="fl">fix per device${hint('A table: device → fix, "*" for everything else')}</span><textarea data-path="${base}fix" rows="2" class="fixtable mono">${esc(JSON.stringify(chk.fix))}</textarea></div>`
|
const fixSel = typeof chk.fix === "object" && chk.fix ? `<div class="field"><span class="fl">fix per device${hint('A table: device → fix, "*" for everything else')}</span><textarea data-path="${base}fix" rows="2" class="fixtable mono">${esc(JSON.stringify(chk.fix))}</textarea></div>`
|
||||||
: `<div class="field"><span class="fl">fix${hint("What holding the key runs for this check")}</span><select data-path="${base}fix" class="fixsel">${fixOptions(chk.fix)}</select></div>`;
|
: `<div class="field"><span class="fl">fix${hint("What holding the key runs for this check")}</span><select data-path="${base}fix" class="fixsel">${fixOptions(chk.fix)}</select></div>`;
|
||||||
const run = typeof chk.fix === "string" && chk.fix && canRunNow(chk.fix) ? `<button class="small" data-run="${esc(t.id)}|${esc(chk.fix)}|${j}" data-hint="Through the watchdog, exactly like a hold">Run the fix</button>` : "";
|
const run = typeof chk.fix === "string" && chk.fix && canRunNow(chk.fix) ? `<button class="small" data-run="${esc(t.id)}|${esc(chk.fix)}|${j}" data-hint="Through the watchdog, exactly like a hold">Run the fix</button>` : "";
|
||||||
return `<li><b>${esc(spec.question || chk.type)}</b> <span class="muted small">${esc(sentence)}</span><div class="checkbox">${fields}${fixSel}
|
return `<details class="chk">${summary}<div class="chkbody">${fields}${fixSel}
|
||||||
<details><summary>more</summary>${["tag", "service_only", "debounce_seconds"].filter((f) => !(spec.fields || []).includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("")}<p class="mono small">type ${esc(chk.type)}</p></details>
|
<details><summary>more</summary>${["tag", "service_only", "debounce_seconds"].filter((f) => !(spec.fields || []).includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("")}<p class="mono small">type ${esc(chk.type)}</p></details>
|
||||||
<div class="row">${run}<button class="small danger" data-removecheck="${j}">Remove</button></div></div></li>`;
|
<div class="row">${run}<button class="small danger" data-removecheck="${j}">Remove</button></div></div></details>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
const plain = !t.members && t.kind !== "master";
|
const plain = !t.members && t.kind !== "master";
|
||||||
const members = t.members ? (editing() ? `<div class="field"><span class="fl">group members</span><span>${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => `<label class="inline"><input type="checkbox" data-member="${esc(x.id)}" ${t.members.includes(x.id) ? "checked" : ""}> ${esc(x.label)}</label>`).join(" ")}</span></div>` : `<p class="small"><b>Sums up:</b> ${t.members.map((m) => `<a href="#keys/${esc(m)}">${esc((c.tiles.find((x) => x.id === m) || {}).label || m)}</a>`).join(", ")}</p>`) : "";
|
const members = t.members ? (editing() ? `<div class="field"><span class="fl">group members</span><span>${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => `<label class="inline"><input type="checkbox" data-member="${esc(x.id)}" ${t.members.includes(x.id) ? "checked" : ""}> ${esc(x.label)}</label>`).join(" ")}</span></div>` : `<p class="small"><b>Sums up</b> ${t.members.map((m) => `<a href="#keys/${esc(m)}">${esc((c.tiles.find((x) => x.id === m) || {}).label || m)}</a>`).join(", ")}</p>`) : "";
|
||||||
const addMenu = editing() && plain ? `<div class="field"><span class="fl">add a check</span><select id="addcheck"><option value="">– pick a question –</option>${Object.entries(groupBy(state.schema.checks, (s) => s.question || "Other")).map(([q, types]) => `<optgroup label="${esc(q)}">${types.map((ty) => `<option value="${esc(ty)}">${esc(state.schema.checks[ty].words || ty)}</option>`).join("")}</optgroup>`).join("")}</select></div>` : "";
|
const addMenu = editing() && plain ? `<select id="addcheck" class="small" style="margin-top:8px"><option value="">+ check…</option>${Object.entries(groupBy(state.schema.checks, (s) => s.question || "Other")).map(([q, types]) => `<optgroup label="${esc(q)}">${types.map((ty) => `<option value="${esc(ty)}">${esc(state.schema.checks[ty].words || ty)}</option>`).join("")}</optgroup>`).join("")}</select>` : "";
|
||||||
const label = editing() ? inputHtml(`tiles.${i}.label`, F("tiles[].label"), t.label) : "";
|
const label = editing() ? inputHtml(`tiles.${i}.label`, F("tiles[].label"), t.label) : "";
|
||||||
const holdAlways = editing() && plain ? `<div class="field"><span class="fl">hold also when green${hint("A fix that runs on every hold, even with nothing failing")}</span><select data-path="tiles.${i}.hold_always" class="holdsel"><option value="">no</option>${Object.entries(c.fixes).map(([id, f]) => `<option value="${esc(id)}" ${t.hold_always === id ? "selected" : ""}>${esc(f.short || id)} – ${esc(f.label || "")}</option>`).join("")}</select></div>` : "";
|
const holdAlways = editing() && plain ? `<div class="field"><span class="fl">hold also when green${hint("A fix that runs on every hold, even with nothing failing")}</span><select data-path="tiles.${i}.hold_always" class="holdsel"><option value="">no</option>${Object.entries(c.fixes).map(([id, f]) => `<option value="${esc(id)}" ${t.hold_always === id ? "selected" : ""}>${esc(f.short || id)} – ${esc(f.label || "")}</option>`).join("")}</select></div>` : "";
|
||||||
const pos = `<details><summary>more</summary><dl class="kv"><dt>id</dt><dd class="mono">${esc(t.id)}</dd></dl>${editing() && !t.kind ? `<div class="field"><span class="fl">position${hint("row, column; blank = inside its group only")}</span><input id="pos" value="${esc(t.pos ? t.pos.join(", ") : "")}" placeholder="row, column"></div>` : ""}</details>`;
|
const holdList = (kt.holding || "").split(/;\s*/).filter(Boolean);
|
||||||
return `<div class="card"><h2 style="margin-top:4px">${esc(t.label)} <span class="muted small">${t.pos ? `row ${t.pos[0]}, col ${t.pos[1]}` : "in a group"}${t.kind ? ` · ${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}` : ""}</span></h2>
|
const more = `<details><summary>more</summary><dl class="kv small"><dt>id</dt><dd class="mono">${esc(t.id)}</dd>${t.kind ? `<dt>kind</dt><dd>${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}</dd>` : ""}</dl>${editing() && !t.kind ? `<div class="field"><span class="fl">position${hint("row, column; blank = inside its group only")}</span><input id="pos" value="${esc(t.pos ? t.pos.join(", ") : "")}" placeholder="row, column"></div>` : ""}${editing() && (!t.kind || t.kind === "network") ? `<p><button class="small danger" id="delkey">Delete key</button></p>` : ""}</details>`;
|
||||||
${kt.says && kt.says.length ? `<p class="small"><b>Can say</b> <span class="mono">${esc(kt.says.join(", "))}</span></p>` : ""}
|
return `<div class="card keycard"><h2 style="margin:4px 0 8px">${esc(t.label)} <span class="muted small">${t.pos ? `row ${t.pos[0]}, col ${t.pos[1]}` : "in a group"}</span></h2>
|
||||||
<p class="small"><b>Hold ${esc(hold)}</b> ${esc(kt.holding)}${t.hold_always ? " <span class='pill'>also when green</span>" : ""}</p>
|
${label}${members}${holdAlways}
|
||||||
${label}${members}${holdAlways}${checks ? `<ol class="checks">${checks}</ol>` : ""}${addMenu}${pos}
|
${holdList.length ? `<details class="fold"><summary>Hold ${esc(hold)}: ${esc(short(holdList[0], 48))}${holdList.length > 1 ? ` <span class="muted">+${holdList.length - 1}</span>` : ""}${t.hold_always ? " <span class='pill'>also when green</span>" : ""}</summary><ol class="small">${holdList.map((h) => `<li>${esc(h)}</li>`).join("")}</ol></details>` : ""}
|
||||||
${editing() && (!t.kind || t.kind === "network") ? `<p style="margin-top:12px"><button class="small danger" id="delkey">Delete key</button></p>` : ""}</div>`;
|
${kt.says && kt.says.length ? `<details class="fold"><summary>Can say <span class="muted">${kt.says.length} things</span></summary><p class="mono small">${esc(kt.says.join(" · "))}</p></details>` : ""}
|
||||||
|
${checks ? `<div class="checks">${checks}</div>` : ""}${addMenu}${more}</div>`;
|
||||||
}
|
}
|
||||||
function groupBy(obj, fn) { const out = {}; for (const [k, v] of Object.entries(obj)) (out[fn(v)] = out[fn(v)] || []).push(k); return out; }
|
function groupBy(obj, fn) { const out = {}; for (const [k, v] of Object.entries(obj)) (out[fn(v)] = out[fn(v)] || []).push(k); return out; }
|
||||||
function wireKeyPanel(sel, restart) {
|
function wireKeyPanel(sel, restart) {
|
||||||
|
|||||||
@@ -124,3 +124,14 @@ details.script textarea { width: 100%; font-size: .8rem; }
|
|||||||
.top { flex-wrap: wrap; gap: 6px; } .nav { order: 3; width: 100%; justify-content: space-between; margin: 0; } .nav a { padding: 4px 4px; } #search { max-width: none; margin-left: 0; }
|
.top { flex-wrap: wrap; gap: 6px; } .nav { order: 3; width: 100%; justify-content: space-between; margin: 0; } .nav a { padding: 4px 4px; } #search { max-width: none; margin-left: 0; }
|
||||||
main { padding: 12px 14px 40px; } .brand .muted { display: none; }
|
main { padding: 12px 14px 40px; } .brand .muted { display: none; }
|
||||||
}
|
}
|
||||||
|
/* the key editor: a narrow column that never overflows; checks fold */
|
||||||
|
.field > * { min-width: 0; } .field input, .field select, .field textarea { max-width: 100%; }
|
||||||
|
.keycols { grid-template-columns: minmax(0, 620px) minmax(300px, 1fr); }
|
||||||
|
.keycard .field { grid-template-columns: minmax(90px, 130px) minmax(0, 1fr); }
|
||||||
|
.chk { border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; margin: 6px 0; background: var(--bg); }
|
||||||
|
.chk > summary { list-style: none; cursor: pointer; display: flex; gap: 6px; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
|
||||||
|
.chk > summary::-webkit-details-marker { display: none; } .chk > summary::before { content: "▸"; color: var(--muted); font-size: .8rem; } .chk[open] > summary::before { content: "▾"; }
|
||||||
|
.chk > summary .muted { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.chkbody { margin-top: 6px; } .chkbody .field { grid-template-columns: 1fr; gap: 2px; margin: 4px 0; }
|
||||||
|
.fold { margin: 4px 0; } .fold > summary { color: var(--fg); }
|
||||||
|
.checks { margin-top: 8px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user