feat(ui): 2.0.0-alpha.13 — finer per-card width scaling (12-col grid + -/+ stepper)
clock/weather etc. default to 1/6 width; sizes store an integer span 1-12 (legacy s/m/l still accepted by /api/dashboard/layout). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "void-server",
|
||||
"version": "2.0.0-alpha.12",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user