import { z } from "zod"; import type { SerializedWidgetMeta } from "@/modules/_core/registry"; export type WidgetPlacement = { widgetId: string; config: unknown; x: number; y: number; w: number; h: number; }; export function widgetContentKey(placement: Pick): string { return `${placement.widgetId}::${JSON.stringify(placement.config)}`; } export type DashboardLayout = { version: 1; widgets: WidgetPlacement[]; }; const placementSchema = z.object({ widgetId: z.string(), config: z.unknown(), x: z.number().int().min(0), y: z.number().int().min(0), w: z.number().int().min(1).max(12), h: z.number().int().min(1), }); const layoutSchema = z.object({ version: z.literal(1), widgets: z.array(placementSchema), }); export function parseDashboardLayout(raw: unknown): DashboardLayout | null { const result = layoutSchema.safeParse(raw); if (!result.success) return null; return result.data; } export type PresetId = "classic" | "split" | "glance"; /** Client-safe layout builder: pass the metas explicitly. The server-only * variants (`computePresetLayout`, `computeDefaultLayout`) live in * `dashboard.server.ts` because they reach into the module registry. */ export function computePresetLayoutFromMetas( preset: PresetId, metas: SerializedWidgetMeta[], ): DashboardLayout { switch (preset) { case "classic": return classicLayout(metas); case "split": return splitLayout(metas); case "glance": return glanceLayout(metas); } } function classicLayout(metas: SerializedWidgetMeta[]): DashboardLayout { const placements: WidgetPlacement[] = []; let mainY = 0; let railY = 0; metas.forEach((m, i) => { const inRail = i % 3 === 0 && i > 0; if (inRail) { placements.push({ widgetId: m.id, config: m.defaultConfig, x: 8, y: railY, w: 4, h: m.defaultSize.h, }); railY += m.defaultSize.h; } else { placements.push({ widgetId: m.id, config: m.defaultConfig, x: 0, y: mainY, w: 8, h: m.defaultSize.h, }); mainY += m.defaultSize.h; } }); return { version: 1, widgets: placements }; } function splitLayout(metas: SerializedWidgetMeta[]): DashboardLayout { const placements: WidgetPlacement[] = []; let leftY = 0; let rightY = 0; metas.forEach((m, i) => { const left = i % 2 === 0; if (left) { placements.push({ widgetId: m.id, config: m.defaultConfig, x: 0, y: leftY, w: 6, h: m.defaultSize.h, }); leftY += m.defaultSize.h; } else { placements.push({ widgetId: m.id, config: m.defaultConfig, x: 6, y: rightY, w: 6, h: m.defaultSize.h, }); rightY += m.defaultSize.h; } }); return { version: 1, widgets: placements }; } function glanceLayout(metas: SerializedWidgetMeta[]): DashboardLayout { const placements: WidgetPlacement[] = []; let y = 0; metas.forEach((m) => { placements.push({ widgetId: m.id, config: m.defaultConfig, x: 0, y, w: 12, h: m.defaultSize.h, }); y += m.defaultSize.h; }); return { version: 1, widgets: placements }; }