diff --git a/settings/ui/app.js b/settings/ui/app.js index a76c130..e8b7c35 100644 --- a/settings/ui/app.js +++ b/settings/ui/app.js @@ -314,7 +314,7 @@ if (state.draft) { try { const v = await post("/api/validate", {}); if (v.plan && v.plan.pages.length) note = `
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.
`; } catch (e) {} } const adders = editing() ? (restart ? `` : ``) : ""; main.innerHTML = `

Keys${hint("The deck as people hold it. Click a key to see what it watches and what holding it does.")}

${tabs}${note} -
${restart && !c.restart_page ? "

No Restart page.

" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)} +
${restart && !c.restart_page ? "

No Restart page.

" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)} ${!restart ? `

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 || "none"}

` : ""}
${adders}
${panel}
`; wireKeyPanel(sel, restart); @@ -412,10 +412,13 @@ const k = await api("/api/keys"); const kt = k.tiles.find((x) => x.id === t.id) || { says: [], holding: "", checks: [] }; const fixOptions = (cur) => `${Object.entries(c.fixes).map(([id, f]) => ``).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 spec = state.schema.checks[chk.type] || { fields: [], question: "" }; const sentence = (kt.checks[j] || {}).sentence || chk.type; - if (!editing()) return `
  • ${esc(sentence)}
  • `; + const what = chk.conn || chk.peer || chk.name || chk.msg || (chk.params || {}).process || chk.host || chk.label || ""; + const summary = `${esc(spec.question || chk.type)} ${esc(short(what || sentence, 40))}${hint(sentence)}`; + if (!editing()) return `
    ${summary}

    ${esc(sentence)}

    `; 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(""); if (chk.type === "remote") { @@ -427,21 +430,22 @@ const fixSel = typeof chk.fix === "object" && chk.fix ? `
    fix per device${hint('A table: device → fix, "*" for everything else')}
    ` : `
    fix${hint("What holding the key runs for this check")}
    `; const run = typeof chk.fix === "string" && chk.fix && canRunNow(chk.fix) ? `` : ""; - return `
  • ${esc(spec.question || chk.type)} ${esc(sentence)}
    ${fields}${fixSel} + return `
    ${summary}
    ${fields}${fixSel}
    more${["tag", "service_only", "debounce_seconds"].filter((f) => !(spec.fields || []).includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("")}

    type ${esc(chk.type)}

    -
    ${run}
  • `; +
    ${run}
    `; }).join(""); const plain = !t.members && t.kind !== "master"; - const members = t.members ? (editing() ? `
    group members${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => ``).join(" ")}
    ` : `

    Sums up: ${t.members.map((m) => `${esc((c.tiles.find((x) => x.id === m) || {}).label || m)}`).join(", ")}

    `) : ""; - const addMenu = editing() && plain ? `
    add a check
    ` : ""; + const members = t.members ? (editing() ? `
    group members${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => ``).join(" ")}
    ` : `

    Sums up ${t.members.map((m) => `${esc((c.tiles.find((x) => x.id === m) || {}).label || m)}`).join(", ")}

    `) : ""; + const addMenu = editing() && plain ? `` : ""; const label = editing() ? inputHtml(`tiles.${i}.label`, F("tiles[].label"), t.label) : ""; const holdAlways = editing() && plain ? `
    hold also when green${hint("A fix that runs on every hold, even with nothing failing")}
    ` : ""; - const pos = `
    more
    id
    ${esc(t.id)}
    ${editing() && !t.kind ? `
    position${hint("row, column; blank = inside its group only")}
    ` : ""}
    `; - return `

    ${esc(t.label)} ${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)}` : ""}

    - ${kt.says && kt.says.length ? `

    Can say ${esc(kt.says.join(", "))}

    ` : ""} -

    Hold ${esc(hold)} ${esc(kt.holding)}${t.hold_always ? " also when green" : ""}

    - ${label}${members}${holdAlways}${checks ? `
      ${checks}
    ` : ""}${addMenu}${pos} - ${editing() && (!t.kind || t.kind === "network") ? `

    ` : ""}
    `; + const holdList = (kt.holding || "").split(/;\s*/).filter(Boolean); + const more = `
    more
    id
    ${esc(t.id)}
    ${t.kind ? `
    kind
    ${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}
    ` : ""}
    ${editing() && !t.kind ? `
    position${hint("row, column; blank = inside its group only")}
    ` : ""}${editing() && (!t.kind || t.kind === "network") ? `

    ` : ""}
    `; + return `

    ${esc(t.label)} ${t.pos ? `row ${t.pos[0]}, col ${t.pos[1]}` : "in a group"}

    + ${label}${members}${holdAlways} + ${holdList.length ? `
    Hold ${esc(hold)}: ${esc(short(holdList[0], 48))}${holdList.length > 1 ? ` +${holdList.length - 1}` : ""}${t.hold_always ? " also when green" : ""}
      ${holdList.map((h) => `
    1. ${esc(h)}
    2. `).join("")}
    ` : ""} + ${kt.says && kt.says.length ? `
    Can say ${kt.says.length} things

    ${esc(kt.says.join(" · "))}

    ` : ""} + ${checks ? `
    ${checks}
    ` : ""}${addMenu}${more}
    `; } 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) { diff --git a/settings/ui/style.css b/settings/ui/style.css index 2f70bea..fd51f06 100644 --- a/settings/ui/style.css +++ b/settings/ui/style.css @@ -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; } 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; }