From 2af7ef27ba5385fff6854dc331580f580b9c4547 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Tue, 22 Sep 2026 10:19:49 -0400 Subject: [PATCH 1/3] Grid density: cut the board finer or coarser, per browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twelve columns fit a booth monitor and nothing else well: on a 4K wall a three-wide tile is nearly a thousand pixels, and on a small screen four tiles across is the most the grid allows. A stepper in the Layouts menu now cuts the board into 8, 10, 12, 16, 20 or 24 columns. Row height follows the column width, so cells keep their shape and a 3×2 tile is still a 3×2 tile, only smaller — finer lets tiles be smaller so more fit, coarser suits a screen read close up. It is this browser's choice, not the server's: two displays at one location can want different densities, so it saves with the layout — the local one and a named one on the server — and a named layout comes back at the density it was arranged on, which the load message says. An older layout with no density is the default twelve. Tiles keep their grid units when the density changes; the board simply gets denser or coarser around them, and a tile now past the right edge is pulled in. A module's minSize is enforced in cells as declared, so on a finer grid a tile can be smaller than it could at twelve, which is the point; a new tile's defaultSize is scaled to the density so it lands at about the size its author meant. Leaving fullscreen goes back to the chosen density rather than the stylesheet's twelve — that reset predates any notion of a chosen density. Shell 1.9.0. The round-trip test carries gridCols and drops a value off the scale. Co-Authored-By: Claude Fable 5.1 --- README.md | 7 +- docs/MODULE-GUIDE.md | 2 +- package.json | 2 +- public/shell.js | 119 +++++++++++++++++++++++++++++++-- public/style.css | 31 +++++++++ server.js | 4 ++ tools/layout-roundtrip.test.js | 6 +- 7 files changed, 159 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0c23e5a..c8a88e7 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,12 @@ below. Nothing is installed without someone clicking. place and shows nothing, then appears when it is; it is never replaced by the module's default tile. Saving over an existing name asks first, and the server keeps the copy it replaced: **Restore previous** in Admin → - Saved layouts swaps it back (and swapping again undoes that). + Saved layouts swaps it back (and swapping again undoes that). The menu's + **Grid** stepper sets how many columns this browser cuts the board into — + 8 to 24, 12 by default. Finer lets tiles be smaller so more fit across (a + 4K wall), coarser suits a small screen read close up; row height follows, + so tiles keep their shape and simply change size. It saves with the + layout, so a named layout comes back at the density it was arranged on. - Every tile has a **status dot**: green = live, amber = connecting, red = upstream unreachable (hover it for details). Tiles reconnect automatically. diff --git a/docs/MODULE-GUIDE.md b/docs/MODULE-GUIDE.md index 9a911f2..e0d9200 100644 --- a/docs/MODULE-GUIDE.md +++ b/docs/MODULE-GUIDE.md @@ -136,7 +136,7 @@ the app); its settings are kept for a later reinstall. | `client` | yes | Client entry file, loaded as an ES module. | | `server` | no | Server entry file (CommonJS), loaded with `require()`. | | `style` | no | A stylesheet the shell injects once per module. | -| `minSize`, `defaultSize` | no | Tile size in grid cells (12 columns wide; rows are ~72 px). Defaults: min 1×1, default 4×3. | +| `minSize`, `defaultSize` | no | Tile size in grid cells, declared for the default grid (12 columns, 72 px rows). Defaults: min 1×1, default 4×3. A dashboard may cut its board finer or coarser (8–24 columns, from the Layouts menu; row height follows). `defaultSize` is scaled to that density when a tile is added, so a new tile lands at about the size you meant; `minSize` is enforced in cells as declared, so on a finer grid a tile can be made smaller than it could at 12. | | `configSchema` | no | **Admin config** — server-wide settings (URLs, ports, keys) edited once at `/admin` for everyone. | | `instanceSchema` | no | **Per-tile settings** — chosen in each tile's gear menu and stored with that browser's layout (filters, text sizes, display options). They apply as they are changed, so keep them to display choices. | | `tiles` | no | Fixed multi-tile list for client-only modules; server modules export a dynamic `tiles()` instead. See **Presenting multiple tiles**. | diff --git a/package.json b/package.json index ba4bb22..b18a3f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proddash", - "version": "1.8.0", + "version": "1.9.0", "private": true, "description": "Modular production dashboard for Waters Church \u2014 zero-dependency Node server + vanilla client", "license": "UNLICENSED", diff --git a/public/shell.js b/public/shell.js index 128aa97..79de9f2 100644 --- a/public/shell.js +++ b/public/shell.js @@ -5,7 +5,18 @@ settings) autosaves to this browser's localStorage — every browser is fully independent. Modules render inside tile bodies and never touch the shell. */ -const COLS = 12; +/* ── grid density ───────────────────────────────────────────────────── + How many columns the board is cut into. Twelve is the default; a finer + grid lets tiles be smaller and more of them fit across (a 4K wall), a + coarser one suits a small screen read close up. It is this browser's + choice — two displays at one location may want different densities — so + it saves with the layout, local and named, and a named layout comes back + at the density it was arranged on. Row height follows the column width, so + cells keep their shape: a 3×2 tile is still a 3×2 tile, only smaller. */ +const GRID_STEPS = [8, 10, 12, 16, 20, 24]; +const GRID_DEFAULT = 12; +const CELL_H_DEFAULT = 72; // px per row at the default density +let COLS = GRID_DEFAULT; const LS_LAYOUT = 'proddash:layout'; const grid = document.getElementById('grid'); @@ -224,7 +235,7 @@ const LS_LAYOUT_PREVIOUS = LS_LAYOUT + '.previous'; function saveLayout() { try { - const next = JSON.stringify({ v: 1, autoHideHeads, tiles }); + const next = JSON.stringify({ v: 1, autoHideHeads, gridCols: COLS, tiles }); const current = localStorage.getItem(LS_LAYOUT); if (current && current !== next) localStorage.setItem(LS_LAYOUT_BACKUP, current); localStorage.setItem(LS_LAYOUT, next); @@ -275,6 +286,84 @@ function newId() { /* ── grid geometry & collision ──────────────────────────────────────── */ +function normalizeGridCols(value) { + const n = Number(value); + return GRID_STEPS.includes(n) ? n : GRID_DEFAULT; +} + +function cellHeightFor(cols) { + return Math.round((CELL_H_DEFAULT * GRID_DEFAULT) / cols); +} + +/** Cut the board into `cols` columns. Tiles keep their grid units — the + board gets denser or coarser and everything on it changes size with it; + only a tile now past the right edge is pulled back in. */ +function setGridCols(value, { save = true } = {}) { + const next = normalizeGridCols(value); + const changed = next !== COLS; + COLS = next; + grid.style.gridTemplateColumns = `repeat(${COLS}, minmax(0, 1fr))`; + document.documentElement.style.setProperty('--cell-h', cellHeightFor(COLS) + 'px'); + if (changed) { + for (const tile of tiles) { + tile.w = Math.max(1, Math.min(COLS, tile.w)); + tile.x = Math.max(0, Math.min(COLS - tile.w, tile.x)); + applyRect(tile); + notifyResize(tile); + } + rebuildDividers(); + if (document.body.classList.contains('fullscreen')) applyFullscreenFit(); + } + if (save) saveLayout(); +} + +const STEP_MINUS_SVG = ''; +const STEP_PLUS_SVG = ''; + +/** The Layouts menu's density row — − 12 columns + — which steps without + closing the menu, so the board can be watched changing behind it. */ +function gridStepper() { + const row = document.createElement('div'); + row.className = 'menu-stepper'; + const label = document.createElement('span'); + label.className = 'menu-stepper-label'; + const name = document.createElement('span'); + name.textContent = 'Grid'; + const sub = document.createElement('span'); + sub.className = 'sub'; + label.append(name, sub); + const minus = document.createElement('button'); + minus.type = 'button'; + minus.className = 'menu-stepper-btn'; + minus.setAttribute('data-tip', 'Coarser'); + minus.innerHTML = STEP_MINUS_SVG; + const value = document.createElement('span'); + value.className = 'menu-stepper-value'; + const plus = document.createElement('button'); + plus.type = 'button'; + plus.className = 'menu-stepper-btn'; + plus.setAttribute('data-tip', 'Finer'); + plus.innerHTML = STEP_PLUS_SVG; + const paint = () => { + const i = GRID_STEPS.indexOf(COLS); + value.textContent = `${COLS} columns`; + sub.textContent = COLS === GRID_DEFAULT ? 'default' : COLS < GRID_DEFAULT ? 'coarser' : 'finer'; + minus.disabled = i <= 0; + plus.disabled = i >= GRID_STEPS.length - 1; + }; + const step = (dir) => { + const i = GRID_STEPS.indexOf(COLS) + dir; + if (i < 0 || i >= GRID_STEPS.length) return; + setGridCols(GRID_STEPS[i]); + paint(); + }; + minus.addEventListener('click', () => step(-1)); + plus.addEventListener('click', () => step(1)); + paint(); + row.append(label, minus, value, plus); + return row; +} + function cellMetrics() { const style = getComputedStyle(grid); const gapRaw = parseFloat(style.columnGap); @@ -990,10 +1079,14 @@ function addTile(moduleId, entry = null) { toast(`${entry.name} is already on this dashboard`); return; } + // Sizes are declared for the default twelve-column grid; scaled so a new + // tile lands at about the size its author meant, whatever the density here. + const scale = COLS / GRID_DEFAULT; const defaultSize = entry?.defaultSize || man?.defaultSize; + const floor = minSizeOf({ module: moduleId, minSize: entry?.minSize }); const size = { - w: Math.max(1, Math.min(COLS, defaultSize?.w || 4)), - h: Math.max(1, defaultSize?.h || 3), + w: Math.max(floor.w, 1, Math.min(COLS, Math.round((defaultSize?.w || 4) * scale))), + h: Math.max(floor.h, 1, Math.round((defaultSize?.h || 3) * scale)), }; const spot = findSpot(size.w, size.h); const settings = {}; @@ -1991,6 +2084,8 @@ function renderLayout(layout) { // "reset" can simply hand over no tiles. const list = Array.isArray(layout) ? layout : (layout?.tiles || []); clearAllTiles(); + // Density first: the tiles are clamped and placed against it. + if (!Array.isArray(layout)) setGridCols(layout?.gridCols ?? GRID_DEFAULT, { save: false }); tiles = list.map(sanitizeTile); if (!Array.isArray(layout)) setAutoHideHeads(Boolean(layout?.autoHideHeads), { save: false }); for (const tile of tiles) if (tileIsOffered(tile)) buildTile(tile); @@ -2057,6 +2152,13 @@ function renderAddMenu(menu) { wireDropdown('layout-btn', 'layout-menu', async (menu, stillOpen) => { menu.innerHTML = ''; + // Density first: the one thing in here that is not a layout but the board + // the layouts are arranged on. + menu.appendChild(gridStepper()); + const stepperDivider = document.createElement('div'); + stepperDivider.className = 'menu-divider'; + menu.appendChild(stepperDivider); + menu.appendChild(menuItem('Save as named layout…', async () => { menu.hidden = true; if (!tiles.length) { @@ -2088,7 +2190,7 @@ wireDropdown('layout-btn', 'layout-menu', async (menu, stillOpen) => { const res = await fetch('/api/layouts/' + encodeURIComponent(name), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ layout: { autoHideHeads, tiles } }), + body: JSON.stringify({ layout: { autoHideHeads, gridCols: COLS, tiles } }), }); const body = await res.json().catch(() => ({})); if (!res.ok) throw new Error(body.error || 'Save failed (' + res.status + ')'); @@ -2136,15 +2238,17 @@ wireDropdown('layout-btn', 'layout-menu', async (menu, stillOpen) => { // does not live-link browsers together. let outgoing = null; try { outgoing = localStorage.getItem(LS_LAYOUT); } catch { /* fine */ } + const gridBefore = COLS; renderLayout(body.layout); saveLayout(); try { if (outgoing) localStorage.setItem(LS_LAYOUT_PREVIOUS, outgoing); } catch { /* fine */ } const unshown = unshownTileNames(); const count = `${tiles.length} tile${tiles.length === 1 ? '' : 's'}`; + const gridNote = COLS !== gridBefore ? ` on a ${COLS}-column grid` : ''; const note = unshown.length ? `, ${unshown.length} not shown — ${unshown.join(', ')} ${unshown.length === 1 ? 'isn’t' : 'aren’t'} offered right now` : ''; - toast(`Loaded “${l.name}”: ${count}${note}`, false, { + toast(`Loaded “${l.name}”: ${count}${gridNote}${note}`, false, { ms: unshown.length ? 10000 : 7000, action: outgoing ? { label: 'Undo', @@ -2300,7 +2404,8 @@ function applyFullscreenFit() { } function clearFullscreenFit() { - grid.style.gridTemplateColumns = ''; + // Back to this browser's density, not the stylesheet's twelve. + grid.style.gridTemplateColumns = `repeat(${COLS}, minmax(0, 1fr))`; grid.style.gridAutoRows = ''; gridwrap.classList.remove('no-scroll'); for (const tile of tiles) { diff --git a/public/style.css b/public/style.css index 66d891b..1eacba1 100644 --- a/public/style.css +++ b/public/style.css @@ -187,6 +187,37 @@ body.menu-hidden #topbar { white-space: normal; } .menu-divider { height: 1px; background: var(--border); margin: 5px 4px; } + +/* the Layouts menu's grid-density row: − 12 columns + */ +.menu-stepper { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px 8px; + font-size: 13px; +} +.menu-stepper-label { flex: 1; } +.menu-stepper-label .sub { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; } +.menu-stepper-btn { + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + cursor: pointer; +} +.menu-stepper-btn:hover { border-color: var(--muted); } +.menu-stepper-btn[disabled] { opacity: 0.35; cursor: default; } +.menu-stepper-btn svg { width: 12px; height: 12px; display: block; } +.menu-stepper-value { + min-width: 86px; + text-align: center; + font-variant-numeric: tabular-nums; +} .menu-title { padding: 6px 10px 3px; color: var(--muted); diff --git a/server.js b/server.js index 2542d92..ebb66b9 100644 --- a/server.js +++ b/server.js @@ -1619,6 +1619,7 @@ function previousLayoutPath(name) { so an old file does not haunt a new client. */ const LAYOUT_VERSION = 2; +const GRID_STEPS = [8, 10, 12, 16, 20, 24]; // the shell's grid densities const MAX_LAYOUT_TILES = 100; const KNOWN_TILE_FIELDS = new Set(['id', 'module', 'x', 'y', 'w', 'h', 'settings', 'variant', 'title', 'headPinned', 'minSize']); const RETIRED_TILE_FIELDS = new Set(['headHidden']); @@ -1644,6 +1645,9 @@ function sanitizeLayout(raw) { // Whether tile title bars auto-hide is part of how a dashboard is // arranged, so it travels with the layout like the tiles themselves. autoHideHeads: Boolean(raw?.autoHideHeads), + // The grid density the layout was arranged on, when it says and it is one + // the shell knows; a browser loading it cuts its board the same way. + ...(GRID_STEPS.includes(Number(raw?.gridCols)) ? { gridCols: Number(raw.gridCols) } : {}), tiles: tiles.map((t) => { const tile = { id: String(t?.id || ''), diff --git a/tools/layout-roundtrip.test.js b/tools/layout-roundtrip.test.js index efec351..68e2b02 100644 --- a/tools/layout-roundtrip.test.js +++ b/tools/layout-roundtrip.test.js @@ -88,7 +88,7 @@ async function waitFor(url, ms = 15000) { headHidden: true, }; - let r = await api('/api/layouts/Booth%20A', { method: 'PUT', body: JSON.stringify({ layout: { autoHideHeads: true, tiles: [tile] } }) }); + let r = await api('/api/layouts/Booth%20A', { method: 'PUT', body: JSON.stringify({ layout: { autoHideHeads: true, gridCols: 16, tiles: [tile] } }) }); assert.equal(r.status, 200, 'save: ' + JSON.stringify(r.body)); assert.equal(r.body.replaced, false, 'a first save replaces nothing'); @@ -97,6 +97,7 @@ async function waitFor(url, ms = 15000) { const got = r.body.layout; assert.equal(got.v, 2, 'the file carries a schema version'); assert.equal(got.autoHideHeads, true); + assert.equal(got.gridCols, 16, 'grid density travels with the layout'); const t = got.tiles[0]; for (const k of ['id', 'module', 'x', 'y', 'w', 'h', 'variant', 'title', 'headPinned']) assert.deepEqual(t[k], tile[k], k); assert.deepEqual(t.settings, tile.settings, 'settings'); @@ -107,7 +108,7 @@ async function waitFor(url, ms = 15000) { assert.equal('headHidden' in t, false, 'a retired field is dropped'); // Saving over the name keeps the copy it replaced. - r = await api('/api/layouts/Booth%20A', { method: 'PUT', body: JSON.stringify({ layout: { tiles: [{ ...tile, title: 'Service v2' }] } }) }); + r = await api('/api/layouts/Booth%20A', { method: 'PUT', body: JSON.stringify({ layout: { gridCols: 13, tiles: [{ ...tile, title: 'Service v2' }] } }) }); assert.equal(r.status, 200); assert.equal(r.body.replaced, true, 'the second save reports a replacement'); assert.ok(fs.existsSync(path.join(layoutsDir, 'Booth A.previous.json')), 'the replaced copy is kept'); @@ -125,6 +126,7 @@ async function waitFor(url, ms = 15000) { assert.equal(r.status, 200); r = await api('/api/layouts/Booth%20A'); assert.equal(r.body.layout.tiles[0].title, 'Service v2', 'restoring again goes forward again'); + assert.equal('gridCols' in r.body.layout, false, 'a density the shell does not offer is dropped, not stored'); r = await api('/api/layouts/Never%20Saved/restore', { method: 'POST', body: '{}' }); assert.equal(r.status, 404, 'nothing to restore is said, not faked'); -- 2.54.0 From e383070a9e7333345827f518f4061525ca7aa426 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Tue, 22 Sep 2026 11:26:05 -0400 Subject: [PATCH 2/3] Grid density: a coarser board settles its tiles instead of stacking them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stepping the grid coarser pulled tiles past the right edge back in and left them wherever that put them — on top of one another, which the board never allows under a drag. Tiles now settle in reading order: one that would land on a tile already settled takes the first free spot, the same rule a newly added tile follows. Co-Authored-By: Claude Fable 5.1 --- README.md | 6 ++++-- public/shell.js | 25 ++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c8a88e7..419ced4 100644 --- a/README.md +++ b/README.md @@ -277,8 +277,10 @@ below. Nothing is installed without someone clicking. **Grid** stepper sets how many columns this browser cuts the board into — 8 to 24, 12 by default. Finer lets tiles be smaller so more fit across (a 4K wall), coarser suits a small screen read close up; row height follows, - so tiles keep their shape and simply change size. It saves with the - layout, so a named layout comes back at the density it was arranged on. + so tiles keep their shape and simply change size. Going coarser, a tile + that no longer fits where it was moves to the first free spot rather than + onto another tile. It saves with the layout, so a named layout comes back + at the density it was arranged on. - Every tile has a **status dot**: green = live, amber = connecting, red = upstream unreachable (hover it for details). Tiles reconnect automatically. diff --git a/public/shell.js b/public/shell.js index 79de9f2..67c49dc 100644 --- a/public/shell.js +++ b/public/shell.js @@ -296,8 +296,9 @@ function cellHeightFor(cols) { } /** Cut the board into `cols` columns. Tiles keep their grid units — the - board gets denser or coarser and everything on it changes size with it; - only a tile now past the right edge is pulled back in. */ + board gets denser or coarser and everything on it changes size with it. + A tile now past the right edge is pulled back in, and one that would then + sit on another moves to the first free spot instead. */ function setGridCols(value, { save = true } = {}) { const next = normalizeGridCols(value); const changed = next !== COLS; @@ -305,9 +306,16 @@ function setGridCols(value, { save = true } = {}) { grid.style.gridTemplateColumns = `repeat(${COLS}, minmax(0, 1fr))`; document.documentElement.style.setProperty('--cell-h', cellHeightFor(COLS) + 'px'); if (changed) { - for (const tile of tiles) { + // A coarser board may not have room for every tile where it was. Settle + // them in reading order: pull a tile past the right edge back in, and + // give one that would then land on a tile already settled the first free + // spot instead — tiles never overlap here, any more than under a drag. + const settled = []; + for (const tile of tiles.slice().sort((a, b) => a.y - b.y || a.x - b.x)) { tile.w = Math.max(1, Math.min(COLS, tile.w)); tile.x = Math.max(0, Math.min(COLS - tile.w, tile.x)); + if (settled.some((s) => overlaps(tile, s))) Object.assign(tile, spotAmong(tile.w, tile.h, settled)); + settled.push(tile); applyRect(tile); notifyResize(tile); } @@ -386,6 +394,17 @@ function isFree(rect, excludeId) { return isFreeExcept(rect, [excludeId]); } +/** First spot, scanning top-to-bottom, left-to-right, clear of these tiles. */ +function spotAmong(w, h, others) { + for (let y = 0; y < 400; y += 1) { + for (let x = 0; x <= COLS - w; x += 1) { + const rect = { x, y, w, h }; + if (!others.some((o) => overlaps(rect, o))) return { x, y }; + } + } + return { x: 0, y: 0 }; +} + /** First free spot scanning top-to-bottom, left-to-right. */ function findSpot(w, h) { for (let y = 0; y < 200; y += 1) { -- 2.54.0 From 7ca65a845f6b215e984ad9d807e0dcd2119126d9 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Tue, 22 Sep 2026 11:35:26 -0400 Subject: [PATCH 3/3] Grid Size: the stepper's row is a name and a number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Layouts menu row reads "Grid Size − 12 +": the label is the setting's name, the readout is the bare column count, and the default/finer/coarser caption under the label is gone. The buttons keep their tooltips. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- public/shell.js | 11 +++-------- public/style.css | 5 ++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 419ced4..9934e2d 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ below. Nothing is installed without someone clicking. the module's default tile. Saving over an existing name asks first, and the server keeps the copy it replaced: **Restore previous** in Admin → Saved layouts swaps it back (and swapping again undoes that). The menu's - **Grid** stepper sets how many columns this browser cuts the board into — + **Grid Size** stepper sets how many columns this browser cuts the board into — 8 to 24, 12 by default. Finer lets tiles be smaller so more fit across (a 4K wall), coarser suits a small screen read close up; row height follows, so tiles keep their shape and simply change size. Going coarser, a tile diff --git a/public/shell.js b/public/shell.js index 67c49dc..0f64e2d 100644 --- a/public/shell.js +++ b/public/shell.js @@ -328,18 +328,14 @@ function setGridCols(value, { save = true } = {}) { const STEP_MINUS_SVG = ''; const STEP_PLUS_SVG = ''; -/** The Layouts menu's density row — − 12 columns + — which steps without +/** The Layouts menu's density row — Grid Size − 12 + — which steps without closing the menu, so the board can be watched changing behind it. */ function gridStepper() { const row = document.createElement('div'); row.className = 'menu-stepper'; const label = document.createElement('span'); label.className = 'menu-stepper-label'; - const name = document.createElement('span'); - name.textContent = 'Grid'; - const sub = document.createElement('span'); - sub.className = 'sub'; - label.append(name, sub); + label.textContent = 'Grid Size'; const minus = document.createElement('button'); minus.type = 'button'; minus.className = 'menu-stepper-btn'; @@ -354,8 +350,7 @@ function gridStepper() { plus.innerHTML = STEP_PLUS_SVG; const paint = () => { const i = GRID_STEPS.indexOf(COLS); - value.textContent = `${COLS} columns`; - sub.textContent = COLS === GRID_DEFAULT ? 'default' : COLS < GRID_DEFAULT ? 'coarser' : 'finer'; + value.textContent = String(COLS); minus.disabled = i <= 0; plus.disabled = i >= GRID_STEPS.length - 1; }; diff --git a/public/style.css b/public/style.css index 1eacba1..922fc62 100644 --- a/public/style.css +++ b/public/style.css @@ -188,7 +188,7 @@ body.menu-hidden #topbar { } .menu-divider { height: 1px; background: var(--border); margin: 5px 4px; } -/* the Layouts menu's grid-density row: − 12 columns + */ +/* the Layouts menu's grid-density row: Grid Size − 12 + */ .menu-stepper { display: flex; align-items: center; @@ -197,7 +197,6 @@ body.menu-hidden #topbar { font-size: 13px; } .menu-stepper-label { flex: 1; } -.menu-stepper-label .sub { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; } .menu-stepper-btn { width: 26px; height: 26px; @@ -214,7 +213,7 @@ body.menu-hidden #topbar { .menu-stepper-btn[disabled] { opacity: 0.35; cursor: default; } .menu-stepper-btn svg { width: 12px; height: 12px; display: block; } .menu-stepper-value { - min-width: 86px; + min-width: 28px; text-align: center; font-variant-numeric: tabular-nums; } -- 2.54.0