A pin on the title bar, and no more notch

Pointing at a tile's top edge already brings its bar back, which left the
notch doing half a job: it could still hide a bar, but hiding one tile at a
time stopped being the interesting question once the whole dashboard could
fold its bars at once. It is gone.

In its place, a pin beside each tile's label. Pinned, that tile keeps its
bar while auto-hide is on — for the one or two tiles whose controls are
wanted at a glance. It sits next to the label rather than out with the gear
and ✕ because it is about the bar, not about the tile's contents, and it
stays faint until the tile is pointed at or the pin is on, so a wall of
unpinned tiles is not a wall of pins.

Fullscreen keeps its own rule: every bar folds, pinned or not, because that
is a mode for watching. What it loses with the notch it gets back better —
the same hover at a tile's top edge peeks a bar there too, and now as an
overlay, so peeking no longer pushes the tile's content down and disturbs
the auto-fit. The peek timer the notch needed goes with it.

Per-tile `headHidden` gives way to `headPinned` in the layout, on disk and
on the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-17 12:04:59 -04:00
co-authored by Claude Opus 5
parent 68f0ba806d
commit 7f50fa0ba9
5 changed files with 93 additions and 148 deletions
+15 -12
View File
@@ -209,18 +209,21 @@ below. Nothing is installed without someone clicking.
gap; alone in its columns it stays as it is. Nothing is saved — the layout gap; alone in its columns it stays as it is. Nothing is saved — the layout
reverts when you leave fullscreen. A layout deeper than the screen keeps reverts when you leave fullscreen. A layout deeper than the screen keeps
its normal row height and scrolls. its normal row height and scrolls.
- A tile's actions (menus, text size, clear…) live in its **title bar**; - A tile's actions (menus, text size, clear…) live in its **title bar**.
the small **notch** on the bar's bottom edge hides/shows the whole bar - **Auto-hide bars** (the button left of ⛶) folds every title bar away, for
for a clean wall-display look. A hidden bar is not gone: point at the a windowed screen that should read like a wall display. A folded bar is
tile's top edge and it slides back in over the content, where its buttons not a gone bar: point at the tile's top edge and it slides back in over
click and its width drags the tile exactly as before — so a dashboard can the content, where its buttons click and its width drags the tile exactly
be arranged with every bar out of the way. as before — so a dashboard can be arranged with every bar out of the way.
- **Auto-hide bars** (the button left of ⛶) folds every title bar away at The switch is off on a fresh dashboard and saves with the layout —
once, for a windowed screen that should read like a wall display. It is including a named layout on the server, so a booth loads its screen with
off on a fresh dashboard and saves with the layout — including a named the bars already tucked away. Fullscreen folds every bar regardless, and
layout on the server, so a booth loads its screen with the bars already the same hover brings one back there too.
tucked away. Fullscreen folds the bars away regardless; this is the - The **pin** beside a tile's title keeps *that* tile's bar showing while
windowed equivalent, asked for by hand. The menu bar's handle is the **ProdDash auto-hide is on — for the one or two tiles whose controls are wanted at
a glance. It turns the accent colour when it is on and saves with the
layout. Fullscreen still folds a pinned bar with the rest.
- The menu bar's own handle is the **ProdDash
name itself**, wrapped in a translucent green pill at the left — click it name itself**, wrapped in a translucent green pill at the left — click it
to hide the bar; the slim green lip it leaves at the top-left edge brings to hide the bar; the slim green lip it leaves at the top-left edge brings
the bar back. It stays at the left so it never sits on top of a tile's the bar back. It stays at the left so it never sits on top of a tile's
+4 -3
View File
@@ -367,9 +367,10 @@ fallback), never a "Text size" field in `instanceSchema`; a timestamps or
newest-first toggle in the header means no such switch in the gear. The 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. 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 whole title bar folds away when the dashboard
small notch on its bottom edge (a clean-view mode for wall displays). Your auto-hides it, and always in fullscreen (a clean-view mode for wall
controls disappear with it, so nothing essential — status, reconnects, displays) — it comes back only while the pointer is at the tile's top edge.
Your controls fold away with it, so nothing essential — status, reconnects,
live data — may exist *only* as a header control. live data — may exist *only* as a header control.
`sse(path, handlers)` handlers: `sse(path, handlers)` handlers:
+42 -76
View File
@@ -193,7 +193,7 @@ function sanitizeTile(t) {
w, w,
h: Math.max(1, Number(t.h) || 2), h: Math.max(1, Number(t.h) || 2),
settings: (t.settings && typeof t.settings === 'object') ? t.settings : {}, settings: (t.settings && typeof t.settings === 'object') ? t.settings : {},
headHidden: Boolean(t.headHidden), headPinned: Boolean(t.headPinned),
// set when the tile was added from a module's tile list (multi-tile modules) // set when the tile was added from a module's tile list (multi-tile modules)
variant: t.variant ? String(t.variant) : '', variant: t.variant ? String(t.variant) : '',
title: t.title ? String(t.title) : '', title: t.title ? String(t.title) : '',
@@ -259,10 +259,10 @@ function minSizeOf(tile) {
/* ── title bars that fold away ──────────────────────────────────────── */ /* ── title bars that fold away ──────────────────────────────────────── */
/* A bar is "off" when it is folded out of sight but the tile is still being /* A bar is "off" when it is folded out of sight: the dashboard has auto-hide
worked on — either this tile's own notch hid it, or the dashboard has on and this tile is not pinned, or the screen is in fullscreen, which folds
auto-hide switched on. Fullscreen is not one of these: there the bars fold every bar for watching whatever the tiles say. Off is never gone — the top
away for watching, and arranging is switched off entirely. */ edge of the tile brings it back for as long as the pointer is there. */
/** How close to the tile's top edge the pointer opens a folded-away bar. */ /** How close to the tile's top edge the pointer opens a folded-away bar. */
const HEAD_REVEAL_PX = 22; const HEAD_REVEAL_PX = 22;
@@ -270,8 +270,8 @@ const HEAD_REVEAL_PX = 22;
const HEAD_HOLD_PX = 38; const HEAD_HOLD_PX = 38;
function headIsOff(tile) { function headIsOff(tile) {
if (document.body.classList.contains('fullscreen')) return false; if (document.body.classList.contains('fullscreen')) return true;
return autoHideHeads || Boolean(tile.headHidden); return autoHideHeads && !tile.headPinned;
} }
function applyHeadState(tile, el = tileEls.get(tile.id)?.el) { function applyHeadState(tile, el = tileEls.get(tile.id)?.el) {
@@ -298,6 +298,7 @@ function setAutoHideHeads(on, { save = true } = {}) {
/* ── tile DOM ───────────────────────────────────────────────────────── */ /* ── tile DOM ───────────────────────────────────────────────────────── */
const CLOSE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>'; const CLOSE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
const PIN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 16.5V22"/><path class="pin-head" d="M8.5 3h7l-1.2 7 3.2 3.2H6.5L9.7 10 8.5 3z"/></svg>';
const GEAR_SVG = '<svg viewBox="0 0 90 90" fill="currentColor" aria-hidden="true"><path d="M 82.94 32.538 c -2.685 0 -5.093 -1.545 -6.327 -3.929 c -0.245 -0.476 -0.504 -0.948 -0.774 -1.416 c -0.27 -0.468 -0.55 -0.928 -0.84 -1.378 c -1.448 -2.26 -1.582 -5.117 -0.239 -7.443 c 1.951 -3.378 0.792 -7.696 -2.584 -9.645 l -9.352 -5.4 c -3.378 -1.951 -7.696 -0.792 -9.645 2.584 l -0.002 0.003 c -1.322 2.291 -3.815 3.641 -6.456 3.512 c -1.149 -0.056 -2.301 -0.057 -3.455 0 c -2.638 0.128 -5.124 -1.226 -6.444 -3.513 l -0.002 -0.002 c -1.951 -3.378 -6.269 -4.535 -9.645 -2.584 l -9.352 5.4 c -3.378 1.951 -4.535 6.269 -2.584 9.645 l 0.002 0.002 c 1.321 2.286 1.25 5.117 -0.18 7.336 c -0.625 0.972 -1.201 1.97 -1.726 2.993 c -1.209 2.353 -3.625 3.836 -6.269 3.836 H 7.063 c -3.9 0 -7.061 3.162 -7.061 7.061 v 10.801 c 0 3.9 3.162 7.061 7.061 7.061 l 0 0 c 2.685 0 5.093 1.545 6.327 3.929 c 0.245 0.475 0.504 0.948 0.774 1.416 c 0.27 0.468 0.55 0.928 0.84 1.378 c 1.448 2.26 1.582 5.117 0.239 7.443 c -1.951 3.378 -0.792 7.696 2.584 9.645 l 9.352 5.4 c 3.378 1.951 7.696 0.792 9.645 -2.584 l 0.002 -0.003 c 1.322 -2.291 3.815 -3.641 6.456 -3.512 c 1.149 0.056 2.301 0.057 3.455 0 c 2.638 -0.128 5.124 1.226 6.444 3.513 l 0.002 0.002 c 1.951 3.378 6.268 4.535 9.645 2.584 l 9.352 -5.4 c 3.378 -1.949 4.535 -6.268 2.584 -9.645 l -0.002 -0.002 c -1.321 -2.286 -1.25 -5.117 0.18 -7.336 c 0.625 -0.972 1.201 -1.97 1.726 -2.993 c 1.209 -2.353 3.625 -3.836 6.269 -3.836 h 0.003 c 3.9 0 7.061 -3.162 7.061 -7.061 V 39.599 C 90.001 35.7 86.84 32.538 82.94 32.538 z M 54.998 62.318 c -9.565 5.523 -21.796 2.245 -27.317 -7.32 c -5.523 -9.565 -2.245 -21.796 7.32 -27.317 s 21.796 -2.245 27.317 7.32 C 67.841 44.566 64.563 56.797 54.998 62.318 z"/></svg>'; const GEAR_SVG = '<svg viewBox="0 0 90 90" fill="currentColor" aria-hidden="true"><path d="M 82.94 32.538 c -2.685 0 -5.093 -1.545 -6.327 -3.929 c -0.245 -0.476 -0.504 -0.948 -0.774 -1.416 c -0.27 -0.468 -0.55 -0.928 -0.84 -1.378 c -1.448 -2.26 -1.582 -5.117 -0.239 -7.443 c 1.951 -3.378 0.792 -7.696 -2.584 -9.645 l -9.352 -5.4 c -3.378 -1.951 -7.696 -0.792 -9.645 2.584 l -0.002 0.003 c -1.322 2.291 -3.815 3.641 -6.456 3.512 c -1.149 -0.056 -2.301 -0.057 -3.455 0 c -2.638 0.128 -5.124 -1.226 -6.444 -3.513 l -0.002 -0.002 c -1.951 -3.378 -6.269 -4.535 -9.645 -2.584 l -9.352 5.4 c -3.378 1.951 -4.535 6.269 -2.584 9.645 l 0.002 0.002 c 1.321 2.286 1.25 5.117 -0.18 7.336 c -0.625 0.972 -1.201 1.97 -1.726 2.993 c -1.209 2.353 -3.625 3.836 -6.269 3.836 H 7.063 c -3.9 0 -7.061 3.162 -7.061 7.061 v 10.801 c 0 3.9 3.162 7.061 7.061 7.061 l 0 0 c 2.685 0 5.093 1.545 6.327 3.929 c 0.245 0.475 0.504 0.948 0.774 1.416 c 0.27 0.468 0.55 0.928 0.84 1.378 c 1.448 2.26 1.582 5.117 0.239 7.443 c -1.951 3.378 -0.792 7.696 2.584 9.645 l 9.352 5.4 c 3.378 1.951 7.696 0.792 9.645 -2.584 l 0.002 -0.003 c 1.322 -2.291 3.815 -3.641 6.456 -3.512 c 1.149 0.056 2.301 0.057 3.455 0 c 2.638 -0.128 5.124 1.226 6.444 3.513 l 0.002 0.002 c 1.951 3.378 6.268 4.535 9.645 2.584 l 9.352 -5.4 c 3.378 -1.949 4.535 -6.268 2.584 -9.645 l -0.002 -0.002 c -1.321 -2.286 -1.25 -5.117 0.18 -7.336 c 0.625 -0.972 1.201 -1.97 1.726 -2.993 c 1.209 -2.353 3.625 -3.836 6.269 -3.836 h 0.003 c 3.9 0 7.061 -3.162 7.061 -7.061 V 39.599 C 90.001 35.7 86.84 32.538 82.94 32.538 z M 54.998 62.318 c -9.565 5.523 -21.796 2.245 -27.317 -7.32 c -5.523 -9.565 -2.245 -21.796 7.32 -27.317 s 21.796 -2.245 27.317 7.32 C 67.841 44.566 64.563 56.797 54.998 62.318 z"/></svg>';
function buildTile(tile) { function buildTile(tile) {
@@ -324,7 +325,34 @@ function buildTile(tile) {
const controls = document.createElement('span'); const controls = document.createElement('span');
controls.className = 'tile-controls'; controls.className = 'tile-controls';
head.append(dot, title, controls); /* Pinned tiles keep their bar through auto-hide. The switch sits right
beside the label rather than out with the actions, because it is about
this bar rather than about the tile's contents. */
const pin = document.createElement('button');
pin.className = 'tile-btn pin';
pin.innerHTML = PIN_SVG;
const paintPin = () => {
const on = Boolean(tile.headPinned);
pin.classList.toggle('is-pinned', on);
pin.setAttribute('aria-pressed', on ? 'true' : 'false');
// Instant tooltips read `title` the first time and `data-tip` after.
pin.title = on ? 'Unpin bar' : 'Pin bar';
pin.setAttribute('data-tip', pin.title);
};
paintPin();
pin.addEventListener('click', (e) => {
e.stopPropagation();
tile.headPinned = !tile.headPinned;
paintPin();
applyHeadState(tile, el);
saveLayout();
});
// Keeps the label hugging the pin however wide the tile gets.
const gap = document.createElement('span');
gap.className = 'tile-head-gap';
head.append(dot, title, pin, gap, controls);
// Every tile has something to set: its own settings when the module // Every tile has something to set: its own settings when the module
// offers any, and the tint either way. // offers any, and the tint either way.
@@ -371,23 +399,6 @@ function buildTile(tile) {
const edgeRight = grip('tile-resize-right'); const edgeRight = grip('tile-resize-right');
const edgeLeft = grip('tile-resize-side'); const edgeLeft = grip('tile-resize-side');
// notch on the title bar's bottom edge: hides/shows the bar
const notch = document.createElement('button');
notch.className = 'tile-notch';
notch.title = 'Hide / show the title bar';
notch.addEventListener('click', (e) => {
e.stopPropagation();
// In fullscreen the bars are all folded away for watching; the notch
// peeks one open for a moment without touching what this tile saved.
if (document.body.classList.contains('fullscreen')) {
peekHead(tile, el, !el.classList.contains('head-peek'));
return;
}
tile.headHidden = !tile.headHidden;
applyHeadState(tile, el);
saveLayout();
});
// A folded-away bar is still reachable: point at the tile's top edge and it // A folded-away bar is still reachable: point at the tile's top edge and it
// slides in over the content, where its buttons click and its whole width // slides in over the content, where its buttons click and its whole width
// drags the tile exactly as when it is showing. // drags the tile exactly as when it is showing.
@@ -404,20 +415,12 @@ function buildTile(tile) {
else if (shown && y > HEAD_HOLD_PX) el.classList.remove('head-peek'); else if (shown && y > HEAD_HOLD_PX) el.classList.remove('head-peek');
}); });
// A peeked title bar stays while the pointer is on its tile, and starts
// counting down as soon as it leaves.
el.addEventListener('pointerenter', () => clearPeekTimer(tile.id));
el.addEventListener('pointerleave', () => { el.addEventListener('pointerleave', () => {
if (!el.classList.contains('head-peek')) return;
if (el.classList.contains('dragging') || el.classList.contains('resizing')) return; if (el.classList.contains('dragging') || el.classList.contains('resizing')) return;
// Windowed, the bar is a hover-reveal and goes the moment the pointer el.classList.remove('head-peek');
// does. In fullscreen it was opened deliberately from the notch, so it
// lingers (PEEK_HIDE_MS) before folding away again.
if (document.body.classList.contains('fullscreen')) schedulePeekHide(tile.id, el);
else el.classList.remove('head-peek');
}); });
el.append(head, body, resize, resizeLeft, resizeBottom, edgeTop, edgeRight, edgeLeft, notch); el.append(head, body, resize, resizeLeft, resizeBottom, edgeTop, edgeRight, edgeLeft);
applyHeadState(tile, el); applyHeadState(tile, el);
grid.appendChild(el); grid.appendChild(el);
@@ -445,7 +448,7 @@ function refreshEmptyState() {
function wireDrag(tile, el, handle) { function wireDrag(tile, el, handle) {
handle.addEventListener('pointerdown', (ev) => { handle.addEventListener('pointerdown', (ev) => {
if (ev.target.closest('.tile-btn') || ev.target.closest('.tile-menu') || ev.target.closest('.tile-notch')) return; if (ev.target.closest('.tile-btn') || ev.target.closest('.tile-menu')) return;
if (ev.button !== 0 && ev.pointerType === 'mouse') return; if (ev.button !== 0 && ev.pointerType === 'mouse') return;
if (document.body.classList.contains('fullscreen')) return; if (document.body.classList.contains('fullscreen')) return;
ev.preventDefault(); ev.preventDefault();
@@ -867,7 +870,7 @@ function addTile(moduleId, entry = null) {
if (spec && 'default' in spec) settings[key] = spec.default; if (spec && 'default' in spec) settings[key] = spec.default;
} }
Object.assign(settings, entry?.settings || {}); Object.assign(settings, entry?.settings || {});
const tile = { id: newId(), module: moduleId, ...spot, ...size, settings }; const tile = { id: newId(), module: moduleId, ...spot, ...size, settings, headPinned: false };
if (entry) { if (entry) {
tile.variant = String(entry.id); tile.variant = String(entry.id);
tile.title = String(entry.name || ''); tile.title = String(entry.name || '');
@@ -883,7 +886,6 @@ function addTile(moduleId, entry = null) {
function removeTile(id) { function removeTile(id) {
const idx = tiles.findIndex((t) => t.id === id); const idx = tiles.findIndex((t) => t.id === id);
if (idx < 0) return; if (idx < 0) return;
clearPeekTimer(id);
unmountModule(tiles[idx]); unmountModule(tiles[idx]);
tiles.splice(idx, 1); tiles.splice(idx, 1);
const entry = tileEls.get(id); const entry = tileEls.get(id);
@@ -1137,7 +1139,7 @@ function buildModuleApi(tile, man, entry) {
/** /**
* The tile's title bar. Controls added here live next to the gear/close * The tile's title bar. Controls added here live next to the gear/close
* buttons and are removed automatically when the instance stops. The * buttons and are removed automatically when the instance stops. The
* whole bar can be hidden by the user via the notch, so nothing * whole bar folds away when the dashboard auto-hides it, so nothing
* essential should exist ONLY as a header control. * essential should exist ONLY as a header control.
*/ */
header: { header: {
@@ -1994,7 +1996,7 @@ wireDropdown('layout-btn', 'layout-menu', async (menu, stillOpen) => {
menu.insertBefore(block, reset); menu.insertBefore(block, reset);
}); });
/* ── menu bar show/hide (notch, like the tile title bars) ───────────── */ /* ── menu bar show/hide (its own notch, at the top-left) ────────────── */
const topbar = document.getElementById('topbar'); const topbar = document.getElementById('topbar');
const topbarNotch = document.getElementById('topbar-notch'); const topbarNotch = document.getElementById('topbar-notch');
@@ -2021,41 +2023,6 @@ try {
if (localStorage.getItem(LS_MENU) === '1') setMenuHidden(true, { persist: false }); if (localStorage.getItem(LS_MENU) === '1') setMenuHidden(true, { persist: false });
} catch { /* fine */ } } catch { /* fine */ }
/* ── fullscreen: peeking at a tile's title bar ──────────────────────── */
/** How long a peeked title bar stays after the pointer leaves its tile. */
const PEEK_HIDE_MS = 10000;
const peekTimers = new Map(); // tile id → timeout
function clearPeekTimer(id) {
const timer = peekTimers.get(id);
if (timer === undefined) return;
clearTimeout(timer);
peekTimers.delete(id);
}
function schedulePeekHide(id, el) {
clearPeekTimer(id);
peekTimers.set(id, setTimeout(() => {
peekTimers.delete(id);
el.classList.remove('head-peek');
}, PEEK_HIDE_MS));
}
function peekHead(tile, el, on) {
el.classList.toggle('head-peek', on);
clearPeekTimer(tile.id);
// Opened from a click the pointer is on the tile, so the countdown waits
// for it to leave — unless it already has (a keyboard or touch open).
if (on && !el.matches(':hover')) schedulePeekHide(tile.id, el);
}
/** Back to a plain screen: no peeks left open, no timers left running. */
function clearPeeks() {
for (const id of [...peekTimers.keys()]) clearPeekTimer(id);
document.querySelectorAll('.tile.head-peek').forEach((el) => el.classList.remove('head-peek'));
}
/* ── fullscreen: auto-fit the layout to the screen ──────────────────── */ /* ── fullscreen: auto-fit the layout to the screen ──────────────────── */
/* Fullscreen is a viewing mode. The occupied columns stretch proportionally /* Fullscreen is a viewing mode. The occupied columns stretch proportionally
@@ -2197,7 +2164,6 @@ document.addEventListener('fullscreenchange', () => {
requestAnimationFrame(() => requestAnimationFrame(applyFullscreenFit)); requestAnimationFrame(() => requestAnimationFrame(applyFullscreenFit));
} else { } else {
setMenuHidden(menuHiddenBeforeFs, { persist: false }); setMenuHidden(menuHiddenBeforeFs, { persist: false });
clearPeeks();
clearFullscreenFit(); clearFullscreenFit();
applyAllHeadStates(); applyAllHeadStates();
} }
+31 -56
View File
@@ -284,7 +284,7 @@ body.menu-hidden #topbar {
Pointing at the tile's top edge slides it back in over the content Pointing at the tile's top edge slides it back in over the content
(shell.js adds .head-peek) — and because it is the same element, its (shell.js adds .head-peek) — and because it is the same element, its
buttons click and its width drags the tile just as they always did. */ buttons click and its width drags the tile just as they always did. */
body:not(.fullscreen) .tile.head-off > .tile-head { .tile.head-off > .tile-head {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -298,17 +298,14 @@ body:not(.fullscreen) .tile.head-off > .tile-head {
pointer-events: none; pointer-events: none;
transition: transform 0.18s ease, opacity 0.18s ease; transition: transform 0.18s ease, opacity 0.18s ease;
} }
body:not(.fullscreen) .tile.head-off.head-peek > .tile-head { .tile.head-off.head-peek > .tile-head {
transform: none; transform: none;
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
} }
body:not(.fullscreen) .tile.is-tinted.head-off > .tile-head { .tile.is-tinted.head-off > .tile-head {
background: color-mix(in srgb, var(--tile-tint) 20%, var(--panel)); background: color-mix(in srgb, var(--tile-tint) 20%, var(--panel));
} }
/* Auto-hide already reveals on hover, so the notch would be a dead control;
a bar this tile hid by itself keeps its notch, which is how it comes back. */
body.autohide-heads .tile-notch { display: none; }
.tile-head .dot { .tile-head .dot {
flex: none; flex: none;
@@ -323,7 +320,7 @@ body.autohide-heads .tile-notch { display: none; }
.tile-head .dot.error { background: var(--danger); } .tile-head .dot.error { background: var(--danger); }
.tile-title { .tile-title {
flex: 1; flex: 0 1 auto;
min-width: 0; min-width: 0;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
@@ -354,6 +351,33 @@ body.autohide-heads .tile-notch { display: none; }
.tile-btn.is-open, .tile-btn.is-open,
.tile-btn.is-open:hover { background: var(--accent); color: var(--accent-text); } .tile-btn.is-open:hover { background: var(--accent); color: var(--accent-text); }
/* The pin: a state, not an action, so it stays quiet until it is on and
sits closer to the label than to the buttons at the other end. It fades
in with the rest of the tile's chrome so a wall of unpinned tiles is not
a wall of pins. */
.tile-btn.pin {
width: 18px;
height: 18px;
margin-left: 2px;
opacity: 0;
transition: opacity 0.15s ease;
}
.tile-btn.pin svg { width: 12px; height: 12px; }
.tile:hover .tile-btn.pin,
.tile-btn.pin:focus-visible { opacity: 0.75; }
.tile-btn.pin:hover { opacity: 1; }
/* Pinned reads at a glance, whether or not anything is being pointed at. */
.tile-btn.pin.is-pinned,
.tile:hover .tile-btn.pin.is-pinned {
opacity: 1;
color: var(--accent);
}
.tile-btn.pin.is-pinned .pin-head { fill: currentColor; }
.tile-btn.pin.is-pinned:hover { background: var(--border); }
/* Keeps the label hugging its pin however wide the tile is. */
.tile-head-gap { flex: 1 1 auto; min-width: 0; }
/* the shell's tooltip: shown at once under the button the pointer is on */ /* the shell's tooltip: shown at once under the button the pointer is on */
#tip { #tip {
position: fixed; position: fixed;
@@ -407,55 +431,6 @@ body.autohide-heads .tile-notch { display: none; }
} }
.tile-menu[hidden] { display: none; } .tile-menu[hidden] { display: none; }
/* notch on the title bar's bottom edge: click to hide/show the bar
(same idea as the ProdCom Listener's pull-down menubar handle) */
.tile-notch {
position: absolute;
top: 32px;
left: 50%;
transform: translateX(-50%);
width: 46px;
height: 13px;
padding: 0;
background: var(--panel);
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 10px 10px;
cursor: pointer;
z-index: 6;
opacity: 0;
transition: opacity 0.15s ease, top 0.22s ease;
}
.tile:hover .tile-notch { opacity: 0.6; }
.tile .tile-notch:hover { opacity: 1; }
body:not(.fullscreen) .tile.head-off .tile-notch { top: 0; }
.tile-notch::after {
content: "";
display: block;
width: 18px;
height: 3px;
margin: 4px auto 0;
border-radius: 999px;
background: var(--muted);
}
.tile-notch:hover::after { background: var(--text); }
/* Fullscreen is for watching, not arranging: every title bar folds away, and
a tile's own hidden/shown preference is left untouched for when the screen
comes back. The notch still peeks one open, and it folds back once the
pointer has been off that tile for a while (PEEK_HIDE_MS in shell.js). */
body.fullscreen .tile:not(.head-peek) .tile-head {
height: 0;
border-bottom-width: 0;
overflow: hidden;
}
body.fullscreen .tile:not(.head-peek) .tile-notch { top: 0; }
body.fullscreen .tile.head-peek .tile-head {
height: 32px;
border-bottom-width: 1px;
}
body.fullscreen .tile.head-peek .tile-notch { top: 32px; }
.tile-body { .tile-body {
flex: 1; flex: 1;
position: relative; position: relative;
+1 -1
View File
@@ -1411,7 +1411,7 @@ function sanitizeLayout(raw) {
// card, and a solo timer comes back with a full tile's settings. // card, and a solo timer comes back with a full tile's settings.
variant: String(t?.variant || ''), variant: String(t?.variant || ''),
title: String(t?.title || ''), title: String(t?.title || ''),
headHidden: Boolean(t?.headHidden), headPinned: Boolean(t?.headPinned),
}; };
if (Number(t?.minSize?.w) > 0 && Number(t?.minSize?.h) > 0) { if (Number(t?.minSize?.w) > 0 && Number(t?.minSize?.h) > 0) {
tile.minSize = { w: Number(t.minSize.w), h: Number(t.minSize.h) }; tile.minSize = { w: Number(t.minSize.w), h: Number(t.minSize.h) };