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.
|
All notable changes to Void 2.0 are documented here.
|
||||||
Format: [Keep a Changelog](https://keepachangelog.com).
|
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
|
## 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.
|
- "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.
|
- 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({
|
const layoutSchema = z.object({
|
||||||
card_order: z.array(z.string()).default([]),
|
card_order: z.array(z.string()).default([]),
|
||||||
hidden: 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) => {
|
router.get('/layout', asyncWrap(async (_req, res) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "void-server",
|
"name": "void-server",
|
||||||
"version": "2.0.0-alpha.12",
|
"version": "2.0.0-alpha.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { el } from '../dom.js';
|
|||||||
// fills `body` in its mount(); start()/stop() own its refresh timer.
|
// fills `body` in its mount(); start()/stop() own its refresh timer.
|
||||||
export function svCard(def) {
|
export function svCard(def) {
|
||||||
const body = el('div', { class: 'sv-card-body' });
|
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),
|
el('div', { class: 'sv-card-title' }, def.title),
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -174,12 +174,14 @@ ul.plain li:last-child { border-bottom: none; }
|
|||||||
/* reserved for a future agent-output phase — unused now:
|
/* reserved for a future agent-output phase — unused now:
|
||||||
--hue-dross: #ff4f2e; --hue-yerin: #c45a4a; --hue-orthos: #6fa86a; */
|
--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-cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; align-items: start; }
|
||||||
.sv-card { grid-column: span 2; } /* s / fallback (factory always sets data-size) */
|
.sv-card { grid-column: span 6; } /* fallback; svCard sets an inline span (1–12) */
|
||||||
.sv-card[data-size="s"] { grid-column: span 2; }
|
@media (max-width: 700px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } }
|
||||||
.sv-card[data-size="m"] { grid-column: span 3; }
|
.sv-ed-span { display: inline-flex; align-items: center; gap: 3px; }
|
||||||
.sv-card[data-size="l"] { grid-column: span 6; }
|
.sv-ed-step { width: 18px; height: 20px; border: 1px solid var(--border); background: transparent; color: var(--muted);
|
||||||
@media (max-width: 900px) { #sv-cards { grid-template-columns: 1fr; } .sv-card { grid-column: 1 / -1 !important; } }
|
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 {
|
.sv-card {
|
||||||
position: relative; border: 1px solid #2c242a; border-radius: 10px; padding: 16px 18px;
|
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
|
// ---- 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) {
|
function editOverlay(def) {
|
||||||
const grip = el('span', { class: 'sv-grip', draggable: true, title: 'Drag to reorder' }, '⠿');
|
const grip = el('span', { class: 'sv-grip', draggable: true, title: 'Drag to reorder' }, '⠿');
|
||||||
const sizes = el('span', { class: 'sv-ed-sizes' },
|
const stepper = el('span', { class: 'sv-ed-span' },
|
||||||
...['s', 'm', 'l'].map(s =>
|
el('button', { class: 'sv-ed-step', title: 'Narrower', onclick: () => setSpan(def.id, -1) }, '−'),
|
||||||
el('button', { class: 'sv-ed-size', dataset: { s }, onclick: () => setSize(def.id, s) }, s.toUpperCase())));
|
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) }, '✕');
|
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) {
|
function mountOne(def) {
|
||||||
const size = layout.sizes?.[def.id] || def.size;
|
const span = spanOf(def);
|
||||||
const { root, body } = svCard({ ...def, size });
|
const { root, body } = svCard({ ...def, span });
|
||||||
root.appendChild(editOverlay(def));
|
root.appendChild(editOverlay(def));
|
||||||
grid().appendChild(root);
|
grid().appendChild(root);
|
||||||
try { def.mount(body); def.start && def.start(); active.push(def); }
|
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 { startCron } from './lib/cron/index.js';
|
||||||
import { seedFromConfig } from './lib/health/registry.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() {
|
export function createApp() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|||||||
Reference in New Issue
Block a user