diff --git a/CHANGELOG.md b/CHANGELOG.md index 0003f6e..a924ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to Void 2.0 are documented here. Format: [Keep a Changelog](https://keepachangelog.com). +## 2.0.0-alpha.13 — Finer Sacred Valley tile scaling +- Cards now sit on a 12-column grid with a per-card width **−/+ stepper** (span 1–12) in edit mode, replacing the coarse S/M/L. "Small" defaults to 1/6 width (half its previous size) so clock/weather aren't oversized. +- Layout `sizes` now store an integer column span (legacy 's'/'m'/'l' still accepted). + ## 2.0.0-alpha.12 — Editable Sacred Valley layout - "Edit layout" mode on the dashboard: per-card **resize** (S/M/L column span), **show/hide** (with a hidden-cards tray to re-add), clearer **drag-to-reorder** via a dedicated grip handle, and a **Reset** to defaults. - All changes persist through the existing `/api/dashboard/layout` (order/sizes/hidden) — no backend changes. diff --git a/lib/api/routes/dashboard.js b/lib/api/routes/dashboard.js index 142b5bd..4e42436 100644 --- a/lib/api/routes/dashboard.js +++ b/lib/api/routes/dashboard.js @@ -11,7 +11,8 @@ router.use(requireOwner); const layoutSchema = z.object({ card_order: z.array(z.string()).default([]), hidden: z.array(z.string()).default([]), - sizes: z.record(z.enum(['s', 'm', 'l'])).default({}) + // Per-card width: an integer column span 1–12 (legacy 's'|'m'|'l' still accepted). + sizes: z.record(z.union([z.number().int().min(1).max(12), z.enum(['s', 'm', 'l'])])).default({}) }); router.get('/layout', asyncWrap(async (_req, res) => { diff --git a/package.json b/package.json index 4346872..c85141e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "void-server", - "version": "2.0.0-alpha.12", + "version": "2.0.0-alpha.13", "type": "module", "private": true, "scripts": { diff --git a/public/components/sv_card.js b/public/components/sv_card.js index c30b4da..bc71854 100644 --- a/public/components/sv_card.js +++ b/public/components/sv_card.js @@ -4,7 +4,10 @@ import { el } from '../dom.js'; // fills `body` in its mount(); start()/stop() own its refresh timer. export function svCard(def) { const body = el('div', { class: 'sv-card-body' }); - const root = el('div', { class: 'sv-card', dataset: { size: def.size || 'm', cardId: def.id } }, + const root = el('div', { + class: 'sv-card', dataset: { cardId: def.id }, + style: { gridColumn: 'span ' + (def.span || 6) } // 12-col grid; per-card width + }, el('div', { class: 'sv-card-title' }, def.title), body ); diff --git a/public/style.css b/public/style.css index 30a4243..fa25b38 100644 --- a/public/style.css +++ b/public/style.css @@ -174,12 +174,14 @@ ul.plain li:last-child { border-bottom: none; } /* reserved for a future agent-output phase — unused now: --hue-dross: #ff4f2e; --hue-yerin: #c45a4a; --hue-orthos: #6fa86a; */ } -#sv-cards { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; align-items: start; } -.sv-card { grid-column: span 2; } /* s / fallback (factory always sets data-size) */ -.sv-card[data-size="s"] { grid-column: span 2; } -.sv-card[data-size="m"] { grid-column: span 3; } -.sv-card[data-size="l"] { grid-column: span 6; } -@media (max-width: 900px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } } +#sv-cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; align-items: start; } +.sv-card { grid-column: span 6; } /* fallback; svCard sets an inline span (1–12) */ +@media (max-width: 700px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } } +.sv-ed-span { display: inline-flex; align-items: center; gap: 3px; } +.sv-ed-step { width: 18px; height: 20px; border: 1px solid var(--border); background: transparent; color: var(--muted); + border-radius: 3px; font-size: 13px; line-height: 1; cursor: pointer; padding: 0; } +.sv-ed-step:hover { color: var(--accent); border-color: var(--accent-dim); } +.sv-span-val { font-family: var(--font-mono); font-size: 11px; color: var(--text); min-width: 14px; text-align: center; } .sv-card { position: relative; border: 1px solid #2c242a; border-radius: 10px; padding: 16px 18px; diff --git a/public/views/sacred_valley.js b/public/views/sacred_valley.js index 45bce0e..5ccfcd1 100644 --- a/public/views/sacred_valley.js +++ b/public/views/sacred_valley.js @@ -29,18 +29,43 @@ async function saveLayout() { } // ---- per-card edit controls (drag grip + size + hide), shown only in edit mode via CSS +const STR_SPAN = { s: 2, m: 6, l: 12 }; // legacy size → 12-col span (s = 1/6, m = 1/2, l = full) +function spanOf(def) { + const v = layout.sizes?.[def.id]; + if (typeof v === 'number') return Math.max(1, Math.min(12, v)); + if (typeof v === 'string') return STR_SPAN[v] || 6; + return STR_SPAN[def.size] || 6; +} +function curSpan(id) { + const node = grid().querySelector(`.sv-card[data-card-id="${id}"]`); + const m = node && (node.style.gridColumn || '').match(/span (\d+)/); + return m ? +m[1] : spanOf(BY_ID.get(id) || {}); +} +function setSpan(id, delta) { + const span = Math.max(1, Math.min(12, curSpan(id) + delta)); + layout.sizes = { ...layout.sizes, [id]: span }; + const node = grid().querySelector(`.sv-card[data-card-id="${id}"]`); + if (node) { + node.style.gridColumn = 'span ' + span; + const lbl = node.querySelector('.sv-span-val'); + if (lbl) lbl.textContent = span; + } + saveLayout(); +} + function editOverlay(def) { const grip = el('span', { class: 'sv-grip', draggable: true, title: 'Drag to reorder' }, '⠿'); - const sizes = el('span', { class: 'sv-ed-sizes' }, - ...['s', 'm', 'l'].map(s => - el('button', { class: 'sv-ed-size', dataset: { s }, onclick: () => setSize(def.id, s) }, s.toUpperCase()))); + const stepper = el('span', { class: 'sv-ed-span' }, + el('button', { class: 'sv-ed-step', title: 'Narrower', onclick: () => setSpan(def.id, -1) }, '−'), + el('span', { class: 'sv-span-val', title: 'Width (of 12)' }, String(spanOf(def))), + el('button', { class: 'sv-ed-step', title: 'Wider', onclick: () => setSpan(def.id, +1) }, '+')); const hide = el('button', { class: 'sv-ed-hide', title: 'Hide card', onclick: () => hideCard(def.id) }, '✕'); - return el('div', { class: 'sv-card-edit' }, grip, sizes, hide); + return el('div', { class: 'sv-card-edit' }, grip, stepper, hide); } function mountOne(def) { - const size = layout.sizes?.[def.id] || def.size; - const { root, body } = svCard({ ...def, size }); + const span = spanOf(def); + const { root, body } = svCard({ ...def, span }); root.appendChild(editOverlay(def)); grid().appendChild(root); try { def.mount(body); def.start && def.start(); active.push(def); } diff --git a/server.js b/server.js index 7a38e8d..46d9f46 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,7 @@ import { router as iconsRouter } from './lib/api/routes/icons.js'; import { startCron } from './lib/cron/index.js'; import { seedFromConfig } from './lib/health/registry.js'; -const VERSION = '2.0.0-alpha.12'; +const VERSION = '2.0.0-alpha.13'; export function createApp() { const app = express();