Shell: settings window sized to its content, with a slim grab bar

No title and no ✕ — the window sits beside its tile and closes on a click
anywhere else or Escape — and it is as wide as its fields want, so a
couple of switches make a small window. Help lines wrap to the fields'
width instead of widening it.

Guide: one control per action — whatever has a header button is not
offered again in the gear. ProdCom 1.2.2 drops its timestamps and
newest-first gear switches (both are header buttons).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-14 16:31:02 -04:00
co-authored by Claude Fable 5.1
parent 5dd1b734aa
commit 20d4e0f524
4 changed files with 54 additions and 74 deletions
+10 -8
View File
@@ -184,9 +184,10 @@ explanation there.
popover (`collapsed: true | false`, `columns`, `help`, `toggle`). That is how popover (`collapsed: true | false`, `columns`, `help`, `toggle`). That is how
a module with a long show/hide checklist keeps the popover usable — see a module with a long show/hide checklist keeps the popover usable — see
`pco-plan`. Display choices belong there, per tile; admin config is for `pco-plan`. Display choices belong there, per tile; admin config is for
connections. The popover itself is a small window beside the tile (moved by connections. The popover itself is a small window beside the tile — sized to
its bar, closed by clicking away) so a tile never has to be big enough to its content, moved by the slim bar on top, closed by clicking anywhere else
hold its own settings, and a change can be watched as it lands. or pressing Escape — so a tile never has to be big enough to hold its own
settings, and a change can be watched as it lands.
A picker entry (see **Presenting multiple tiles**) may carry its own A picker entry (see **Presenting multiple tiles**) may carry its own
`instanceSchema` and `instanceGroups`: tiles added from that entry get those `instanceSchema` and `instanceGroups`: tiles added from that entry get those
@@ -350,11 +351,12 @@ Everything you add is removed automatically when your instance stops, so a
remount never duplicates controls. Keep them compact (icon buttons, short remount never duplicates controls. Keep them compact (icon buttons, short
labels): the title bar is 32 px tall and shared with the tile's name. labels): the title bar is 32 px tall and shared with the tile's name.
**Text size lives here, not in the gear popover.** A module whose text can **One control per action.** Whatever has a dedicated header button is not
be sized offers A− / A+ header buttons that persist through offered again in the gear popover: text size is A− / A+ up here (persisted
`saveInstanceSettings({ textSize })` and reads the value back with a sane through `saveInstanceSettings({ textSize })`, read back with a sane
fallback; it does not also expose a "Text size" number field in fallback), never a "Text size" field in `instanceSchema`; a timestamps or
`instanceSchema` — one control per setting. newest-first toggle in the header means no such switch in the gear. The
gear is for display choices that have no button of their own.
One more thing to know: the user can hide the whole title bar with the One more thing to know: the user can hide the whole title bar with the
small notch on its bottom edge (a clean-view mode for wall displays). Your small notch on its bottom edge (a clean-view mode for wall displays). Your
+17 -15
View File
@@ -1,22 +1,34 @@
{ {
"id": "prodcom-transcript", "id": "prodcom-transcript",
"name": "ProdCom", "name": "ProdCom",
"version": "1.2.1", "version": "1.2.2",
"proddash": ">=1.1.0", "proddash": ">=1.1.0",
"description": "Live ProdCom transcript stream", "description": "Live ProdCom transcript stream",
"permissions": [ "permissions": [
{ "kind": "local-network", "reason": "Reaching the ProdCom API on this network" } {
"kind": "local-network",
"reason": "Reaching the ProdCom API on this network"
}
], ],
"client": "client.js", "client": "client.js",
"server": "server.js", "server": "server.js",
"style": "style.css", "style": "style.css",
"minSize": { "w": 3, "h": 2 }, "minSize": {
"defaultSize": { "w": 4, "h": 4 }, "w": 3,
"h": 2
},
"defaultSize": {
"w": 4,
"h": 4
},
"configSchema": { "configSchema": {
"prodcom": { "prodcom": {
"type": "endpoint", "type": "endpoint",
"label": "ProdCom server", "label": "ProdCom server",
"default": { "host": "10.3.11.152", "port": 24480 } "default": {
"host": "10.3.11.152",
"port": 24480
}
}, },
"apiKey": { "apiKey": {
"type": "password", "type": "password",
@@ -29,16 +41,6 @@
"type": "boolean", "type": "boolean",
"label": "Show channel icons", "label": "Show channel icons",
"default": true "default": true
},
"newestFirst": {
"type": "boolean",
"label": "Newest first (transcript reads bottom-up)",
"default": false
},
"showTimes": {
"type": "boolean",
"label": "Show timestamps",
"default": false
} }
} }
} }
+5 -13
View File
@@ -1157,20 +1157,12 @@ function toggleSettingsPopover(tile, el) {
pop.setAttribute('role', 'dialog'); pop.setAttribute('role', 'dialog');
pop.setAttribute('aria-label', `${tile.title || man.name} settings`); pop.setAttribute('aria-label', `${tile.title || man.name} settings`);
// A bar to move it by, and the one control every window has. // A slim bar to move it by — nothing else. No title (the window sits beside
// the tile it belongs to) and no close button: a click anywhere else, or
// Escape, closes it.
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.className = 'tile-settings-bar'; bar.className = 'tile-settings-bar';
const barTitle = document.createElement('span'); bar.title = 'Drag to move';
barTitle.className = 'tile-settings-title';
barTitle.textContent = tile.title || man.name;
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'tile-settings-close';
closeBtn.title = 'Close';
closeBtn.setAttribute('aria-label', 'Close settings');
closeBtn.textContent = '✕';
closeBtn.addEventListener('click', () => closeSettingsPopovers());
bar.append(barTitle, closeBtn);
const body = document.createElement('div'); const body = document.createElement('div');
body.className = 'tile-settings-body'; body.className = 'tile-settings-body';
pop.append(bar, body); pop.append(bar, body);
@@ -1486,7 +1478,7 @@ function toggleSettingsPopover(tile, el) {
// Moved by its bar. // Moved by its bar.
bar.addEventListener('pointerdown', (e) => { bar.addEventListener('pointerdown', (e) => {
if (e.target === closeBtn || (e.button !== 0 && e.pointerType === 'mouse')) return; if (e.button !== 0 && e.pointerType === 'mouse') return;
e.preventDefault(); e.preventDefault();
bar.setPointerCapture(e.pointerId); bar.setPointerCapture(e.pointerId);
const dx = e.clientX - pop.offsetLeft; const dx = e.clientX - pop.offsetLeft;
+22 -38
View File
@@ -542,7 +542,10 @@ body.fullscreen #grid { padding-bottom: 0; }
z-index: 80; z-index: 80;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 260px; /* as wide as its content wants and no wider: a couple of switches make a
small window, a three-column checklist a wide one */
width: max-content;
min-width: 150px;
max-width: min(560px, calc(100vw - 16px)); max-width: min(560px, calc(100vw - 16px));
max-height: calc(100vh - 16px); max-height: calc(100vh - 16px);
overflow: hidden; overflow: hidden;
@@ -551,12 +554,13 @@ body.fullscreen #grid { padding-bottom: 0; }
border-radius: 10px; border-radius: 10px;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
} }
/* the grab bar: a slim strip with a grip mark, nothing to read or press */
.tile-settings-bar { .tile-settings-bar {
flex: none; flex: none;
height: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: center;
padding: 6px 6px 6px 12px;
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
cursor: grab; cursor: grab;
@@ -564,36 +568,28 @@ body.fullscreen #grid { padding-bottom: 0; }
-webkit-user-select: none; -webkit-user-select: none;
touch-action: none; touch-action: none;
} }
.tile-settings-bar::before {
content: "";
width: 34px;
height: 4px;
border-radius: 2px;
background: var(--muted);
opacity: 0.55;
transition: opacity 0.12s;
}
.tile-settings-bar:hover::before { opacity: 0.9; }
.tile-settings-bar:active { cursor: grabbing; } .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 { .tile-settings-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
padding: 10px; padding: 10px;
} }
/* Help lines never widen the window: they take no room of their own when the
content is measured, then wrap to whatever width the fields settled on. */
.tile-settings .field-help,
.tile-settings .field-group-help,
.tile-settings .multiselect-empty { width: 0; min-width: 100%; }
/* a group's on/off sits in its title row; the body dims while it is off */ /* 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-title { display: flex; align-items: center; gap: 8px; }
.tile-settings .field-group-name { flex: 1; min-width: 0; } .tile-settings .field-group-name { flex: 1; min-width: 0; }
@@ -633,12 +629,6 @@ body.fullscreen #grid { padding-bottom: 0; }
.tile-settings .template-var:hover { border-color: var(--accent); } .tile-settings .template-var:hover { border-color: var(--accent); }
.tile-settings .template-var:active { cursor: grabbing; } .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(100vw - 16px));
container-type: inline-size;
}
.tile-settings .field-group { .tile-settings .field-group {
margin: 0 0 10px; margin: 0 0 10px;
padding: 8px 10px; padding: 8px 10px;
@@ -688,12 +678,6 @@ body.fullscreen #grid { padding-bottom: 0; }
many columns the group lays its other fields out in */ many columns the group lays its other fields out in */
.tile-settings .field-group-body.is-columns .multiselect-field, .tile-settings .field-group-body.is-columns .multiselect-field,
.tile-settings .field-group-body.is-columns .template-field { grid-column: 1 / -1; } .tile-settings .field-group-body.is-columns .template-field { grid-column: 1 / -1; }
@container (max-width: 420px) {
.tile-settings .field-group-body.is-columns { grid-template-columns: 1fr 1fr; }
}
@container (max-width: 260px) {
.tile-settings .field-group-body.is-columns { grid-template-columns: 1fr; }
}
.tile-settings .field-group-help, .tile-settings .field-group-help,
.tile-settings .field-help { .tile-settings .field-help {
display: block; display: block;