Admin: collapsible / multi-column config groups, help text, grouped and dependent selects

Ports the timers lane's admin schema additions unchanged (field `group`,
`showWhen`, type `switch`, `optionsRoute`) so the two branches merge
cleanly, and extends them for modules whose settings are richer than a
handful of fields:

- manifest `configGroups[name]` = { collapsed: true | false | "whenSet",
  columns: 2–4, help } — a credentials block that folds once it holds a
  saved secret, a show/hide checklist laid out as a compact grid
- `help` on any field: one line of guidance under the control
- select options may carry `group` → rendered as <optgroup>s (a folder
  tree browsable inside the select)
- `optionsDependsOn: "<key>"` refetches a select's options with that
  field's current value as a query parameter, now and on every change
- a save re-runs the card's dynamic option fetches, so a list that needed
  the just-saved credentials fills in without a page reload; a blank
  "— choose —" is no longer added when the module offers its own blank
  option

Documented in docs/MODULE-GUIDE.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-10 15:31:12 -04:00
co-authored by Claude Fable 5.1
parent 50caf02023
commit 106ce0c1ce
5 changed files with 332 additions and 391 deletions
+77
View File
@@ -113,6 +113,83 @@ body { overflow: auto; }
.endpoint-row .ep-sep { color: var(--muted); font-weight: 600; }
.module-form-empty { color: var(--muted); font-size: 12px; margin-top: 10px; }
/* labeled config subsections (manifest field `group`) */
.field-group {
margin: 14px 0 4px;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 9px;
}
.field-group-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 10px;
}
.field-group .field:last-child { margin-bottom: 0; }
/* collapsible groups (manifest configGroups[name].collapsed) */
details.field-group > summary.field-group-title {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 7px;
margin-bottom: 0;
user-select: none;
}
details.field-group > summary.field-group-title::-webkit-details-marker { display: none; }
details.field-group > summary.field-group-title::before {
content: "";
width: 0; height: 0;
border-left: 5px solid var(--muted);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
transition: transform 0.15s;
}
details.field-group[open] > summary.field-group-title::before { transform: rotate(90deg); }
details.field-group[open] > summary.field-group-title { margin-bottom: 10px; }
details.field-group > summary.field-group-title:hover { color: var(--text); }
/* multi-column groups (configGroups[name].columns): a neat checklist grid */
.field-group-body.is-columns {
display: grid;
grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
gap: 4px 16px;
}
.field-group-body.is-columns .field { margin-bottom: 4px; max-width: none; }
.field-group-body.is-columns .field-group-help { grid-column: 1 / -1; }
@media (max-width: 560px) { .field-group-body.is-columns { grid-template-columns: 1fr 1fr; } }
.field-group-help,
.field-help {
display: block;
font-size: 11.5px;
line-height: 1.45;
color: var(--muted);
}
.field-group-help { margin: -4px 0 8px; }
.field-help { margin-top: 2px; }
.field-help code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
/* showWhen: a hidden dependent field must actually collapse — `.field`'s
own `display:flex` (an author rule) otherwise outranks the [hidden] UA
default and the field stays visible. */
.module-form .field[hidden] { display: none; }
/* a boolean rendered as a toggle (manifest type `switch`) */
.field.switch-field {
flex-direction: row;
align-items: center;
gap: 10px;
color: var(--text);
cursor: pointer;
max-width: none;
}
.field.switch-field .switch-label { font-size: 13px; }
.module-actions {
display: flex;
align-items: center;