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:
co-authored by
Claude Opus 5
parent
68f0ba806d
commit
7f50fa0ba9
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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) };
|
||||||
|
|||||||
Reference in New Issue
Block a user