Shell: every tile can be tinted from its gear
A colour picker at the foot of every gear window washes that tile's chrome — panel, border and title bar — in a colour of the operator's choosing, so one screen's tiles can be told apart at a glance. "Clear" takes it off again. The value saves with the layout under the reserved key __tint; it is the shell's, not a module's, so picking a colour paints the tile without remounting the module, and every tile now has a gear even when its module offers no settings of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f29e43373b
commit
720e5bb590
@@ -187,7 +187,10 @@ a module with a long show/hide checklist keeps the popover usable — see
|
|||||||
connections. The popover itself is a small window beside the tile — sized to
|
connections. The popover itself is a small window beside the tile — sized to
|
||||||
its content, moved by the slim bar on top, closed by clicking anywhere else
|
its content, moved by the slim bar on top, closed by clicking anywhere else
|
||||||
or pressing Escape — so a tile never has to be big enough to hold its own
|
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.
|
settings, and a change can be watched as it lands. Under everything you
|
||||||
|
offer, the shell adds **Tile tint** — a colour the operator can wash that
|
||||||
|
tile's chrome in, saved with the layout. It is not yours to declare and it
|
||||||
|
never remounts you; don't offer one of your own.
|
||||||
|
|
||||||
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
|
||||||
@@ -315,7 +318,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
| `instanceId` | Unique per tile — two tiles of the same module get different ids. Handy for keying anything per-tile. |
|
| `instanceId` | Unique per tile — two tiles of the same module get different ids. Handy for keying anything per-tile. |
|
||||||
| `variant` | For multi-tile modules: the id of the tile-list entry this tile was added as, `''` otherwise (see **Presenting multiple tiles**). |
|
| `variant` | For multi-tile modules: the id of the tile-list entry this tile was added as, `''` otherwise (see **Presenting multiple tiles**). |
|
||||||
| `config` | Admin (server-wide) config, read-only, always current. Password fields are absent — they never reach the client. |
|
| `config` | Admin (server-wide) config, read-only, always current. Password fields are absent — they never reach the client. |
|
||||||
| `instanceSettings` | This tile's settings: your `instanceSchema` defaults overlaid with whatever this tile has saved. Read it fresh whenever you render — don't cache it. |
|
| `instanceSettings` | This tile's settings: your `instanceSchema` defaults overlaid with whatever this tile has saved. Read it fresh whenever you render — don't cache it. Keys beginning `__` are the shell's own (`__tint`, the tile's colour) — never use that prefix, and ignore the ones you find. |
|
||||||
| `saveInstanceSettings(patch)` | Merge `patch` into this tile's settings and persist them with the layout. No restart happens — you made the change, you already know. Use it for state the user sets *inside* your tile (a toggled filter, a chosen tab). |
|
| `saveInstanceSettings(patch)` | Merge `patch` into this tile's settings and persist them with the layout. No restart happens — you made the change, you already know. Use it for state the user sets *inside* your tile (a toggled filter, a chosen tab). |
|
||||||
| `fetch(path, opts)` | `fetch` scoped to your server routes: `fetch('/state')` hits `/api/modules/<id>/state`. |
|
| `fetch(path, opts)` | `fetch` scoped to your server routes: `fetch('/state')` hits `/api/modules/<id>/state`. |
|
||||||
| `sse(path, handlers)` | An `EventSource` scoped the same way, **with auto-reconnect** (see below). Returns a handle with `close()`. |
|
| `sse(path, handlers)` | An `EventSource` scoped the same way, **with auto-reconnect** (see below). Returns a handle with `close()`. |
|
||||||
|
|||||||
+59
-3
@@ -261,6 +261,7 @@ function buildTile(tile) {
|
|||||||
el.className = 'tile';
|
el.className = 'tile';
|
||||||
el.dataset.tileId = tile.id;
|
el.dataset.tileId = tile.id;
|
||||||
if (man) el.dataset.module = man.id;
|
if (man) el.dataset.module = man.id;
|
||||||
|
applyTint(tile, el);
|
||||||
|
|
||||||
const head = document.createElement('div');
|
const head = document.createElement('div');
|
||||||
head.className = 'tile-head';
|
head.className = 'tile-head';
|
||||||
@@ -279,8 +280,9 @@ function buildTile(tile) {
|
|||||||
|
|
||||||
head.append(dot, title, controls);
|
head.append(dot, title, controls);
|
||||||
|
|
||||||
const hasSettings = man && Object.keys(tileSchema(tile, man).schema).length;
|
// Every tile has something to set: its own settings when the module
|
||||||
if (hasSettings) {
|
// offers any, and the tint either way.
|
||||||
|
if (man) {
|
||||||
const gear = document.createElement('button');
|
const gear = document.createElement('button');
|
||||||
gear.className = 'tile-btn';
|
gear.className = 'tile-btn';
|
||||||
gear.title = 'Settings';
|
gear.title = 'Settings';
|
||||||
@@ -1133,6 +1135,23 @@ function unmountModule(tile) {
|
|||||||
/* ── per-tile settings popover ──────────────────────────────────────── */
|
/* ── per-tile settings popover ──────────────────────────────────────── */
|
||||||
|
|
||||||
/** `<input type="color">` only accepts #rrggbb — anything else falls back. */
|
/** `<input type="color">` only accepts #rrggbb — anything else falls back. */
|
||||||
|
/* ── the tile tint ───────────────────────────────────────────────────
|
||||||
|
Every tile can be washed in a colour of its own — a way to tell one
|
||||||
|
screen's tiles apart at a glance (this campus green, that one amber).
|
||||||
|
It is the shell's, not a module's: the key is reserved, it is offered in
|
||||||
|
every gear window, and it saves with the layout like any other tile
|
||||||
|
setting. A tile with no tint looks exactly as before. */
|
||||||
|
const TINT_KEY = '__tint';
|
||||||
|
|
||||||
|
/** Paint (or clear) a tile's tint on its element. */
|
||||||
|
function applyTint(tile, el = tileEls.get(tile.id)?.el) {
|
||||||
|
if (!el) return;
|
||||||
|
const hex = colorHex(tile.settings?.[TINT_KEY], '');
|
||||||
|
if (hex) el.style.setProperty('--tile-tint', hex);
|
||||||
|
else el.style.removeProperty('--tile-tint');
|
||||||
|
el.classList.toggle('is-tinted', Boolean(hex));
|
||||||
|
}
|
||||||
|
|
||||||
function colorHex(value, fallback) {
|
function colorHex(value, fallback) {
|
||||||
const s = String(value ?? '').trim();
|
const s = String(value ?? '').trim();
|
||||||
if (/^#[0-9a-fA-F]{6}$/.test(s)) return s.toLowerCase();
|
if (/^#[0-9a-fA-F]{6}$/.test(s)) return s.toLowerCase();
|
||||||
@@ -1267,8 +1286,8 @@ function toggleSettingsPopover(tile, el, button = null) {
|
|||||||
closeSettingsPopovers();
|
closeSettingsPopovers();
|
||||||
if (existing) return;
|
if (existing) return;
|
||||||
const man = registry.get(tile.module);
|
const man = registry.get(tile.module);
|
||||||
|
if (!man) return;
|
||||||
const { schema, groups: groupMeta } = tileSchema(tile, man);
|
const { schema, groups: groupMeta } = tileSchema(tile, man);
|
||||||
if (!man || !Object.keys(schema).length) return;
|
|
||||||
|
|
||||||
const pop = document.createElement('div');
|
const pop = document.createElement('div');
|
||||||
pop.className = 'tile-settings';
|
pop.className = 'tile-settings';
|
||||||
@@ -1588,6 +1607,43 @@ function toggleSettingsPopover(tile, el, button = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The tint sits on its own at the foot of the window, under a hairline —
|
||||||
|
it is the shell's setting, not one of the module's. Unlike those, a
|
||||||
|
change needs no remount: nothing but the tile's own chrome reads it. */
|
||||||
|
{
|
||||||
|
const field = document.createElement('div');
|
||||||
|
field.className = 'field color-field tint-field'; // the swatch reads like a module's colour field
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.textContent = 'Tile tint';
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'color';
|
||||||
|
input.setAttribute('aria-label', 'Tile tint');
|
||||||
|
const clear = document.createElement('button');
|
||||||
|
clear.type = 'button';
|
||||||
|
clear.className = 'tint-clear';
|
||||||
|
clear.textContent = 'Clear';
|
||||||
|
clear.title = 'No tint';
|
||||||
|
const paint = () => {
|
||||||
|
const hex = colorHex(tile.settings[TINT_KEY], '');
|
||||||
|
// With no tint the swatch opens on the panel colour, so the picker
|
||||||
|
// starts somewhere sane rather than at black.
|
||||||
|
input.value = hex || colorHex(getComputedStyle(document.documentElement).getPropertyValue('--panel'), '#1a1d21');
|
||||||
|
clear.hidden = !hex;
|
||||||
|
};
|
||||||
|
const set = (hex) => {
|
||||||
|
if (hex) tile.settings[TINT_KEY] = hex;
|
||||||
|
else delete tile.settings[TINT_KEY];
|
||||||
|
applyTint(tile);
|
||||||
|
paint();
|
||||||
|
saveLayout();
|
||||||
|
};
|
||||||
|
input.addEventListener('input', () => set(colorHex(input.value, '')));
|
||||||
|
clear.addEventListener('click', () => set(''));
|
||||||
|
paint();
|
||||||
|
field.append(text, clear, input);
|
||||||
|
body.appendChild(field);
|
||||||
|
}
|
||||||
|
|
||||||
// "Short day" appears once "Date" is on: a field with showWhen:"K" follows
|
// "Short day" appears once "Date" is on: a field with showWhen:"K" follows
|
||||||
// K's switch, live — the same rule the admin page applies.
|
// K's switch, live — the same rule the admin page applies.
|
||||||
if (shownWhen.length) {
|
if (shownWhen.length) {
|
||||||
|
|||||||
@@ -234,6 +234,18 @@ body.menu-hidden #topbar {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A tile the user gave a colour: a wash over the panel, a stronger edge and
|
||||||
|
a tinted title bar. Module content keeps its own colours — this is the
|
||||||
|
tile's chrome, so a tinted tile still reads like every other one. */
|
||||||
|
.tile.is-tinted {
|
||||||
|
background: color-mix(in srgb, var(--tile-tint) 14%, var(--panel));
|
||||||
|
border-color: color-mix(in srgb, var(--tile-tint) 45%, var(--border));
|
||||||
|
}
|
||||||
|
.tile.is-tinted > .tile-head {
|
||||||
|
background: color-mix(in srgb, var(--tile-tint) 20%, transparent);
|
||||||
|
border-bottom-color: color-mix(in srgb, var(--tile-tint) 35%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
.tile.dragging,
|
.tile.dragging,
|
||||||
.tile.resizing {
|
.tile.resizing {
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
@@ -599,6 +611,26 @@ body.fullscreen #grid { padding-bottom: 0; }
|
|||||||
}
|
}
|
||||||
.tile-settings-bar:hover::before { opacity: 1; }
|
.tile-settings-bar:hover::before { opacity: 1; }
|
||||||
.tile-settings .field[hidden] { display: none; }
|
.tile-settings .field[hidden] { display: none; }
|
||||||
|
/* the tint, the shell's own setting, under a hairline at the foot */
|
||||||
|
.tile-settings .tint-field {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding-top: 9px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.tile-settings .tint-clear {
|
||||||
|
flex: none;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tile-settings .tint-clear:hover { color: var(--text); background: var(--border); }
|
||||||
|
.tile-settings .tint-clear[hidden] { display: none; }
|
||||||
.tile-settings-bar:active { cursor: grabbing; }
|
.tile-settings-bar:active { cursor: grabbing; }
|
||||||
.tile-settings-body {
|
.tile-settings-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user