Replace masonry grid with an absolute-positioned 12-col canvas: drag to move, corner to resize, per-card free/overlap toggle (Alt = no-snap). Geometry persisted (migration 027: dashboard_layout.geom + extras). Two new addable decorative cards: blank spacer + animated blackflame (canvas particle flame). Old layout auto-migrates by flow-placement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40 lines
1.3 KiB
JavaScript
40 lines
1.3 KiB
JavaScript
import { Router } from 'express';
|
||
import { z } from 'zod';
|
||
import { validate } from '../validate.js';
|
||
import { asyncWrap } from '../errors.js';
|
||
import { requireOwner } from '../cap.js';
|
||
import * as repo from '../../db/repos/dashboard_layout.js';
|
||
|
||
export const router = Router();
|
||
router.use(requireOwner);
|
||
|
||
const geomCell = z.object({
|
||
x: z.number(), y: z.number(),
|
||
w: z.number().positive(), h: z.number().positive(),
|
||
free: z.boolean().optional()
|
||
});
|
||
const layoutSchema = z.object({
|
||
card_order: z.array(z.string()).default([]),
|
||
hidden: z.array(z.string()).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({}),
|
||
// Hybrid-canvas geometry, keyed by card id → {x,y,w,h,free} in 12-col grid units.
|
||
geom: z.record(geomCell).default({}),
|
||
// User-added decorative card instances that must survive reloads.
|
||
extras: z.array(z.object({
|
||
id: z.string().min(1).max(64),
|
||
type: z.enum(['blank', 'blackflame'])
|
||
})).default([])
|
||
});
|
||
|
||
router.get('/layout', asyncWrap(async (_req, res) => {
|
||
res.json(await repo.get());
|
||
}));
|
||
|
||
router.put('/layout',
|
||
validate({ body: layoutSchema }),
|
||
asyncWrap(async (req, res) => {
|
||
res.json(await repo.put(req.body));
|
||
})
|
||
);
|