Shell: single-instance tile entries, and a multiselect admin field

Two small pieces of contract for modules that offer several tiles from live
data:

- A `tiles()` entry may say `single: true`: at most one such tile per
  dashboard. The + picker greys the entry out once it is placed ("Already on
  this dashboard") and `addTile` refuses a second — for a control surface
  that would only confuse in duplicate, a send box say.
- Admin config gains `multiselect`: several picks from one list, the same
  `options` / `optionsRoute` as `select`, one switch per option, stored as an
  array of values. A saved value the list no longer offers stays checked and
  marked rather than being dropped in silence. The wrapper is a <label>, so
  it is told to label nothing — otherwise a click anywhere would flip the
  first switch.

Both documented in the module guide.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-14 12:56:10 -04:00
co-authored by Claude Fable 5.1
parent babe8263eb
commit 7d34604f2f
6 changed files with 98 additions and 3 deletions
+5
View File
@@ -463,6 +463,8 @@ function sanitizeTileEntry(raw) {
name: String(raw.name),
description: raw.description ? String(raw.description) : '',
settings: raw.settings && typeof raw.settings === 'object' ? raw.settings : {},
// at most one tile of this entry per dashboard (the picker greys it out)
single: raw.single === true,
};
for (const key of ['minSize', 'defaultSize']) {
const size = raw[key];
@@ -1055,6 +1057,9 @@ function applyConfigPatch(id, patch) {
}
if (type === 'number') value = Number(value) || 0;
else if (type === 'boolean' || type === 'switch') value = Boolean(value);
else if (type === 'multiselect') {
value = (Array.isArray(value) ? value : []).map((v) => String(v)).filter(Boolean);
}
else if (type === 'endpoint') {
const src = value && typeof value === 'object' ? value : {};
value = {