Shell: tile settings in a window beside the tile; schemas per tile kind

The gear popover lived inside its tile, so a tile could be too small for its
own settings, and the settings sat on top of the thing they change. It is now
a small window: placed beside the tile (right when there is room, else left,
else over it), clamped to the viewport, moved by a grab bar, closed by
clicking away, Escape, the ✕ or the gear. Settings still apply as they
change — that is the point of putting the window beside the tile.

With it, four pieces of contract the module work needs:

- A picker entry may carry its own `instanceSchema` / `instanceGroups`:
  tiles added from it get settings that fit that tile, nothing else. A tile
  showing one ProPresenter timer has no use for a clock's format switches.
- `instanceGroups[name].toggle` puts a group's on/off switch in its title row
  and dims the body while it is off — the admin page's convention, in the
  popover.
- `select` and `multiselect` in tile settings may take `optionsRoute`;
  `multiselect` also `columns` and `allByDefault` (nothing saved until
  someone flips a switch, so options that turn up later are on too).
- `template`: a text with placeholders built by dragging or clicking the
  variables a module offers into it.

And the + picker's descriptions are tooltips now — the list stays a list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-14 15:20:38 -04:00
co-authored by Claude Fable 5.1
parent b9e895d324
commit 7a44b25d4b
4 changed files with 419 additions and 63 deletions
+95 -12
View File
@@ -534,26 +534,109 @@ body.fullscreen #grid { padding-bottom: 0; }
/* ── per-tile settings popover ─────────────────────────────────────── */
/* A small window beside its tile (shell.js places and clamps it), not a
box inside the tile: a tile can be smaller than its own settings, and the
whole point of live settings is watching the tile change. */
.tile-settings {
position: absolute;
top: 36px;
right: 6px;
z-index: 45;
min-width: 220px;
max-width: calc(100% - 12px);
max-height: calc(100% - 40px);
overflow-y: auto;
position: fixed;
z-index: 80;
display: flex;
flex-direction: column;
min-width: 260px;
max-width: min(560px, calc(100vw - 16px));
max-height: calc(100vh - 16px);
overflow: hidden;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
}
.tile-settings-bar {
flex: none;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 6px 6px 12px;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid var(--border);
cursor: grab;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.tile-settings-bar:active { cursor: grabbing; }
.tile-settings-title {
flex: 1;
min-width: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tile-settings-close {
flex: none;
width: 24px;
height: 24px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--muted);
font-size: 12px;
cursor: pointer;
}
.tile-settings-close:hover { background: rgba(255, 255, 255, 0.08); color: var(--text); }
.tile-settings-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10px;
}
/* a group's on/off sits in its title row; the body dims while it is off */
.tile-settings .field-group-title { display: flex; align-items: center; gap: 8px; }
.tile-settings .field-group-name { flex: 1; min-width: 0; }
.tile-settings .group-toggle { margin: 0 0 0 auto; }
.tile-settings .group-toggle .check-label { display: none; }
.tile-settings .field-group.is-off .field-group-body { opacity: 0.45; }
/* several picks from a list (type `multiselect`) */
.tile-settings .multiselect-field { display: flex; flex-direction: column; gap: 4px; }
.tile-settings .multiselect { display: flex; flex-direction: column; gap: 3px; }
.tile-settings .multiselect.is-columns {
display: grid;
grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
gap: 3px 14px;
}
.tile-settings .multiselect-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text);
cursor: pointer;
}
.tile-settings .multiselect-row .check-label { flex: 1 1 0; min-width: 0; }
.tile-settings .multiselect-empty { color: var(--muted); font-size: 12px; }
/* a text built from offered variables (type `template`) */
.tile-settings .template-vars { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 2px; }
.tile-settings .template-var {
font: inherit;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
cursor: grab;
}
.tile-settings .template-var:hover { border-color: var(--accent); }
.tile-settings .template-var:active { cursor: grabbing; }
/* A popover with grouped settings needs room for its columns; it still
never grows past the tile (max-width above). */
.tile-settings.has-groups {
min-width: min(540px, calc(100% - 12px));
min-width: min(540px, calc(100vw - 16px));
container-type: inline-size;
}
.tile-settings .field-group {
@@ -593,7 +676,7 @@ body.fullscreen #grid { padding-bottom: 0; }
.tile-settings .field-group .field:last-child { margin-bottom: 0; }
/* Settings apply as they change, so nothing sits under the last field — its
bottom margin would only pad the popover's own padding a second time. */
.tile-settings > :last-child { margin-bottom: 0; }
.tile-settings-body > :last-child { margin-bottom: 0; }
.tile-settings .field-group-body.is-columns {
display: grid;
grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));