// DashboardEmptyMockup — the animated empty state for the modular dashboard. // // The whole grid REORGANIZES on a loop: skeleton widget cards grow, shrink and // reflow through four dashboard compositions (A → B → C → D → A), as if the // board were trying out arrangements while it loads. No "empty" copy — the // motion carries the meaning. // // ── The zero-overlap invariant (hard requirement) ────────────────────────── // Two earlier takes let tiles cross and overlap; the user rejected them. This // version makes overlap IMPOSSIBLE by construction, not by tuning: // // The board is 3 COLUMNS in a FIXED left-to-right order, each split into a // TOP and BOTTOM tile — 6 tiles total. Every layout only changes the column // WIDTHS and the per-column top/bottom SPLIT. Because column order never // changes and top is always above bottom, EVERY pair of tiles keeps a // consistent separating axis in every layout, with a constant `gap` between // neighbours. Under linear interpolation a gap that is `gap` at both ends of // a transition stays exactly `gap` throughout — so no two tiles can ever // touch, at any frame, during any transition. (Verified independently by the // interpolation check in dashboard-empty-mockup.test — 0 overlaps.) // // Design constraints kept from the spec: // - Full-bleed (fills the whole dashboard area). // - Pure CSS keyframes, no dependencies; injected once per document. // - Synchronised reflow: all tiles share one duration / easing / timing, so // A→B reads as a single reorganisation. ~2s rest per layout, ~1.2s glide. // - Theme tokens only (bg-card / border / muted) → light & dark for free. // - `prefers-reduced-motion: reduce` → the static A layout, no motion. // - Decorative → aria-hidden, no text. import * as React from 'react' import { cn } from '@asteby/metacore-ui/lib' const STYLE_ID = 'mc-dashboard-empty-mockup-style' /** A tile's rectangle in percent of the container (both axes 0–100). */ export interface MockupRect { x: number y: number w: number h: number } // Each layout = three column width weights + each column's top-tile height // fraction. Weights are relative (normalised below), so they read as intent // ("this column is the wide one") rather than exact percentages. interface LayoutSpec { widths: [number, number, number] splits: [number, number, number] } // Four visibly different compositions: balanced → left-heavy → centre-heavy → // right-heavy. The loop returns to A, so it is seamless. export const MOCKUP_LAYOUTS: LayoutSpec[] = [ { widths: [40, 32, 28], splits: [0.5, 0.6, 0.45] }, { widths: [52, 24, 24], splits: [0.62, 0.4, 0.55] }, { widths: [24, 52, 24], splits: [0.4, 0.55, 0.62] }, { widths: [26, 28, 46], splits: [0.55, 0.5, 0.4] }, ] /** Horizontal/vertical gap between tiles, in percent. */ export const MOCKUP_GAP = 3 /** * Computes the six tile rects for one layout. Tile order is * [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom]. * * Columns keep a constant `gap` between them and top/bottom keep a constant * `gap` between them — this is what makes every transition overlap-free. */ export function computeLayoutRects(layout: LayoutSpec, gap = MOCKUP_GAP): MockupRect[] { const usableW = 100 - 2 * gap const usableH = 100 - gap const sum = layout.widths[0] + layout.widths[1] + layout.widths[2] const colW = layout.widths.map((w) => (w / sum) * usableW) as [number, number, number] const colX = [0, colW[0] + gap, colW[0] + colW[1] + 2 * gap] const rects: MockupRect[] = [] for (let c = 0; c < 3; c++) { const topH = layout.splits[c] * usableH const botH = usableH - topH rects.push({ x: colX[c], y: 0, w: colW[c], h: topH }) rects.push({ x: colX[c], y: topH + gap, w: colW[c], h: botH }) } return rects } /** The precomputed rects for all four layouts (exported for the overlap test). */ export function mockupLayoutRects(gap = MOCKUP_GAP): MockupRect[][] { return MOCKUP_LAYOUTS.map((l) => computeLayoutRects(l, gap)) } // Keyframe stops. Cycle = 12.8s: per layout ~2s rest + ~1.2s glide. The stops // hold each layout, then glide to the next; 100% == 0% (layout A) → seamless. const STOPS = [0, 15.625, 25, 40.625, 50, 65.625, 75, 90.625, 100] const STOP_LAYOUT = [0, 0, 1, 1, 2, 2, 3, 3, 0] // which layout at each stop const fmt = (n: number) => `${Math.round(n * 1000) / 1000}%` /** Builds the full