@asteby/metacore-runtime-react 27.3.1 → 27.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/dashboard-empty-mockup.d.ts +34 -3
- package/dist/dashboard-empty-mockup.d.ts.map +1 -1
- package/dist/dashboard-empty-mockup.js +115 -105
- package/dist/dashboard-grid.d.ts.map +1 -1
- package/dist/dashboard-grid.js +8 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +14 -4
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +8 -1
- package/package.json +1 -1
- package/src/__tests__/dashboard-empty-mockup.test.ts +83 -0
- package/src/dashboard-empty-mockup.tsx +196 -141
- package/src/dashboard-grid.tsx +9 -2
- package/src/dynamic-columns.tsx +15 -9
- package/src/dynamic-table.tsx +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 27.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 31ba021: DynamicTable: clamp genérico de texto largo en celdas (max-width 350px + tooltip con el valor completo) y las cards móviles muestran el label traducido de la columna en vez de la key cruda.
|
|
8
|
+
|
|
9
|
+
## 27.3.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c58b455: Empty state del Tablero: el mockup animado ahora reorganiza toda la grilla en
|
|
14
|
+
loop, pasando por cuatro composiciones de dashboard (A→B→C→D→A) — las skeleton
|
|
15
|
+
cards crecen, se achican y se redistribuyen como widgets reales probando
|
|
16
|
+
acomodos mientras carga. ~2s de reposo por layout y ~1.2s de transición fluida
|
|
17
|
+
sincronizada (todas las cards transicionan a la vez, mismo easing).
|
|
18
|
+
|
|
19
|
+
Cero solapes garantizado por construcción: la grilla son 3 columnas en orden
|
|
20
|
+
fijo × top/bottom, y cada layout solo cambia anchos de columna y el split
|
|
21
|
+
top/bottom, así que todo par de tiles conserva un eje de separación consistente
|
|
22
|
+
con un `gap` constante — bajo interpolación lineal eso jamás se cruza. Se agrega
|
|
23
|
+
un test que interpola los rects (t=0..1, paso 0.05) en cada transición y asserta
|
|
24
|
+
0 solapes (estático + en tránsito). Se mantiene tokens de tema, skeleton interno
|
|
25
|
+
tipo widget card, `prefers-reduced-motion: reduce` → grilla estática (layout A) y
|
|
26
|
+
`aria-hidden`.
|
|
27
|
+
|
|
3
28
|
## 27.3.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
/** A tile's rectangle in percent of the container (both axes 0–100). */
|
|
3
|
+
export interface MockupRect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
w: number;
|
|
7
|
+
h: number;
|
|
8
|
+
}
|
|
9
|
+
interface LayoutSpec {
|
|
10
|
+
widths: [number, number, number];
|
|
11
|
+
splits: [number, number, number];
|
|
12
|
+
}
|
|
13
|
+
export declare const MOCKUP_LAYOUTS: LayoutSpec[];
|
|
14
|
+
/** Horizontal/vertical gap between tiles, in percent. */
|
|
15
|
+
export declare const MOCKUP_GAP = 3;
|
|
2
16
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
17
|
+
* Computes the six tile rects for one layout. Tile order is
|
|
18
|
+
* [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom].
|
|
19
|
+
*
|
|
20
|
+
* Columns keep a constant `gap` between them and top/bottom keep a constant
|
|
21
|
+
* `gap` between them — this is what makes every transition overlap-free.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeLayoutRects(layout: LayoutSpec, gap?: number): MockupRect[];
|
|
24
|
+
/** The precomputed rects for all four layouts (exported for the overlap test). */
|
|
25
|
+
export declare function mockupLayoutRects(gap?: number): MockupRect[][];
|
|
26
|
+
/**
|
|
27
|
+
* Full-bleed skeleton dashboard that reflows through four layouts on a loop.
|
|
28
|
+
* Overlap-free by construction (see the invariant note above). Purely
|
|
29
|
+
* decorative — always `aria-hidden`, no text.
|
|
30
|
+
*
|
|
31
|
+
* Contract: tiles are positioned with percentage heights, so the CONTAINER
|
|
32
|
+
* must have a DEFINITE height (e.g. `h-[…]`, or a flex child with `flex-1`).
|
|
33
|
+
* A parent with only `min-height` leaves the box auto-height and the tiles
|
|
34
|
+
* collapse into a strip. The dashboard empty state mounts it inside a
|
|
35
|
+
* definite-height box for exactly this reason.
|
|
6
36
|
*/
|
|
7
37
|
export declare function DashboardEmptyMockup({ className }: {
|
|
8
38
|
className?: string;
|
|
9
39
|
}): React.JSX.Element;
|
|
40
|
+
export {};
|
|
10
41
|
//# sourceMappingURL=dashboard-empty-mockup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-empty-mockup.d.ts","sourceRoot":"","sources":["../src/dashboard-empty-mockup.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dashboard-empty-mockup.d.ts","sourceRoot":"","sources":["../src/dashboard-empty-mockup.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACvB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACZ;AAKD,UAAU,UAAU;IAChB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAID,eAAO,MAAM,cAAc,EAAE,UAAU,EAKtC,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,SAAa,GAAG,UAAU,EAAE,CAerF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,SAAa,GAAG,UAAU,EAAE,EAAE,CAElE;AA4HD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAuBzE"}
|
|
@@ -1,81 +1,104 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// DashboardEmptyMockup — the animated empty state for the modular dashboard.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// dashboard
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// metaphor is "your board is assembling", not "empty".
|
|
4
|
+
// The whole grid REORGANIZES on a loop: skeleton widget cards grow, shrink and
|
|
5
|
+
// reflow through four dashboard compositions (A → B → C → D → A), as if the
|
|
6
|
+
// board were trying out arrangements while it loads. No "empty" copy — the
|
|
7
|
+
// motion carries the meaning.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// - Pure CSS keyframes, no dependencies. One slow loop (~13s). Motion is
|
|
14
|
-
// PERCEPTIBLE — tiles translate a real distance and swap in pairs — but
|
|
15
|
-
// never opacity-strobing, and eased so it feels like settling.
|
|
16
|
-
// - Theme tokens only (bg-muted / border tokens) → light & dark for free.
|
|
17
|
-
// - `prefers-reduced-motion: reduce` freezes everything (tiles rest in place).
|
|
18
|
-
// - No caption text — the animation carries the meaning, and shipping copy
|
|
19
|
-
// from the package risks un-localized strings. The whole mock is
|
|
20
|
-
// decorative → aria-hidden.
|
|
9
|
+
// ── The zero-overlap invariant (hard requirement) ──────────────────────────
|
|
10
|
+
// Two earlier takes let tiles cross and overlap; the user rejected them. This
|
|
11
|
+
// version makes overlap IMPOSSIBLE by construction, not by tuning:
|
|
21
12
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
13
|
+
// The board is 3 COLUMNS in a FIXED left-to-right order, each split into a
|
|
14
|
+
// TOP and BOTTOM tile — 6 tiles total. Every layout only changes the column
|
|
15
|
+
// WIDTHS and the per-column top/bottom SPLIT. Because column order never
|
|
16
|
+
// changes and top is always above bottom, EVERY pair of tiles keeps a
|
|
17
|
+
// consistent separating axis in every layout, with a constant `gap` between
|
|
18
|
+
// neighbours. Under linear interpolation a gap that is `gap` at both ends of
|
|
19
|
+
// a transition stays exactly `gap` throughout — so no two tiles can ever
|
|
20
|
+
// touch, at any frame, during any transition. (Verified independently by the
|
|
21
|
+
// interpolation check in dashboard-empty-mockup.test — 0 overlaps.)
|
|
22
|
+
//
|
|
23
|
+
// Design constraints kept from the spec:
|
|
24
|
+
// - Full-bleed (fills the whole dashboard area).
|
|
25
|
+
// - Pure CSS keyframes, no dependencies; injected once per document.
|
|
26
|
+
// - Synchronised reflow: all tiles share one duration / easing / timing, so
|
|
27
|
+
// A→B reads as a single reorganisation. ~2s rest per layout, ~1.2s glide.
|
|
28
|
+
// - Theme tokens only (bg-card / border / muted) → light & dark for free.
|
|
29
|
+
// - `prefers-reduced-motion: reduce` → the static A layout, no motion.
|
|
30
|
+
// - Decorative → aria-hidden, no text.
|
|
27
31
|
import * as React from 'react';
|
|
28
32
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
29
|
-
// Scoped, collision-proof keyframe + class names (prefixed, unlikely to clash).
|
|
30
33
|
const STYLE_ID = 'mc-dashboard-empty-mockup-style';
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
.
|
|
34
|
+
// Four visibly different compositions: balanced → left-heavy → centre-heavy →
|
|
35
|
+
// right-heavy. The loop returns to A, so it is seamless.
|
|
36
|
+
export const MOCKUP_LAYOUTS = [
|
|
37
|
+
{ widths: [40, 32, 28], splits: [0.5, 0.6, 0.45] },
|
|
38
|
+
{ widths: [52, 24, 24], splits: [0.62, 0.4, 0.55] },
|
|
39
|
+
{ widths: [24, 52, 24], splits: [0.4, 0.55, 0.62] },
|
|
40
|
+
{ widths: [26, 28, 46], splits: [0.55, 0.5, 0.4] },
|
|
41
|
+
];
|
|
42
|
+
/** Horizontal/vertical gap between tiles, in percent. */
|
|
43
|
+
export const MOCKUP_GAP = 3;
|
|
44
|
+
/**
|
|
45
|
+
* Computes the six tile rects for one layout. Tile order is
|
|
46
|
+
* [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom].
|
|
47
|
+
*
|
|
48
|
+
* Columns keep a constant `gap` between them and top/bottom keep a constant
|
|
49
|
+
* `gap` between them — this is what makes every transition overlap-free.
|
|
50
|
+
*/
|
|
51
|
+
export function computeLayoutRects(layout, gap = MOCKUP_GAP) {
|
|
52
|
+
const usableW = 100 - 2 * gap;
|
|
53
|
+
const usableH = 100 - gap;
|
|
54
|
+
const sum = layout.widths[0] + layout.widths[1] + layout.widths[2];
|
|
55
|
+
const colW = layout.widths.map((w) => (w / sum) * usableW);
|
|
56
|
+
const colX = [0, colW[0] + gap, colW[0] + colW[1] + 2 * gap];
|
|
57
|
+
const rects = [];
|
|
58
|
+
for (let c = 0; c < 3; c++) {
|
|
59
|
+
const topH = layout.splits[c] * usableH;
|
|
60
|
+
const botH = usableH - topH;
|
|
61
|
+
rects.push({ x: colX[c], y: 0, w: colW[c], h: topH });
|
|
62
|
+
rects.push({ x: colX[c], y: topH + gap, w: colW[c], h: botH });
|
|
63
|
+
}
|
|
64
|
+
return rects;
|
|
65
|
+
}
|
|
66
|
+
/** The precomputed rects for all four layouts (exported for the overlap test). */
|
|
67
|
+
export function mockupLayoutRects(gap = MOCKUP_GAP) {
|
|
68
|
+
return MOCKUP_LAYOUTS.map((l) => computeLayoutRects(l, gap));
|
|
69
|
+
}
|
|
70
|
+
// Keyframe stops. Cycle = 12.8s: per layout ~2s rest + ~1.2s glide. The stops
|
|
71
|
+
// hold each layout, then glide to the next; 100% == 0% (layout A) → seamless.
|
|
72
|
+
const STOPS = [0, 15.625, 25, 40.625, 50, 65.625, 75, 90.625, 100];
|
|
73
|
+
const STOP_LAYOUT = [0, 0, 1, 1, 2, 2, 3, 3, 0]; // which layout at each stop
|
|
74
|
+
const fmt = (n) => `${Math.round(n * 1000) / 1000}%`;
|
|
75
|
+
/** Builds the full <style> text: per-tile keyframes cycling through the rects. */
|
|
76
|
+
function buildCss() {
|
|
77
|
+
const layouts = mockupLayoutRects();
|
|
78
|
+
const tiles = layouts[0].length;
|
|
79
|
+
let css = `
|
|
80
|
+
.mc-demock{position:relative;height:100%;width:100%}
|
|
81
|
+
.mc-demock-tile{position:absolute;
|
|
82
|
+
animation-duration:12.8s;animation-timing-function:cubic-bezier(.65,0,.35,1);
|
|
83
|
+
animation-iteration-count:infinite;will-change:top,left,width,height}
|
|
84
|
+
.mc-demock-shimmer{overflow:hidden}
|
|
53
85
|
.mc-demock-shimmer::after{content:"";position:absolute;inset:0;
|
|
54
86
|
background:linear-gradient(105deg,transparent 35%,currentColor 50%,transparent 65%);
|
|
55
|
-
opacity:.
|
|
56
|
-
animation:mc-demock-sheen
|
|
57
|
-
/* Swaps: hold home -> travel a full cell (+gap) to the partner's slot -> hold
|
|
58
|
-
-> travel back. ~108% of own size clears the tile plus the grid gap. */
|
|
59
|
-
@keyframes mc-demock-swap-left{
|
|
60
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(108%,0)}88%,100%{transform:translate(0,0)}}
|
|
61
|
-
@keyframes mc-demock-swap-right{
|
|
62
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(-108%,0)}88%,100%{transform:translate(0,0)}}
|
|
63
|
-
@keyframes mc-demock-swap-up{
|
|
64
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(0,112%)}88%,100%{transform:translate(0,0)}}
|
|
65
|
-
@keyframes mc-demock-swap-down{
|
|
66
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(0,-112%)}88%,100%{transform:translate(0,0)}}
|
|
67
|
-
@keyframes mc-demock-shuffle-a{
|
|
68
|
-
0%,15%{transform:translate(0,0) scale(1)}45%,60%{transform:translate(106%,110%) scale(.96)}90%,100%{transform:translate(0,0) scale(1)}}
|
|
69
|
-
@keyframes mc-demock-shuffle-b{
|
|
70
|
-
0%,15%{transform:translate(0,0) scale(1)}45%,60%{transform:translate(-106%,-110%) scale(1.04)}90%,100%{transform:translate(0,0) scale(1)}}
|
|
71
|
-
@keyframes mc-demock-drift-a{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(4%,-5%) scale(1.02)}}
|
|
72
|
-
@keyframes mc-demock-drift-b{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-5%,4%) scale(.98)}}
|
|
87
|
+
opacity:.045;transform:translateX(-120%);
|
|
88
|
+
animation:mc-demock-sheen 12.8s ease-in-out infinite}
|
|
73
89
|
@keyframes mc-demock-sheen{0%,100%{transform:translateX(-120%)}55%,72%{transform:translateX(120%)}}
|
|
74
|
-
@media (prefers-reduced-motion:reduce){
|
|
75
|
-
.mc-demock-tile{animation:none!important}
|
|
76
|
-
.mc-demock-shimmer::after{animation:none!important;opacity:0}
|
|
77
|
-
}
|
|
78
90
|
`;
|
|
91
|
+
for (let i = 0; i < tiles; i++) {
|
|
92
|
+
css += `.mc-demock-t${i}{animation-name:mc-demock-k${i}}\n@keyframes mc-demock-k${i}{`;
|
|
93
|
+
for (let s = 0; s < STOPS.length; s++) {
|
|
94
|
+
const r = layouts[STOP_LAYOUT[s]][i];
|
|
95
|
+
css += `${fmt(STOPS[s])}{left:${fmt(r.x)};top:${fmt(r.y)};width:${fmt(r.w)};height:${fmt(r.h)}}`;
|
|
96
|
+
}
|
|
97
|
+
css += `}\n`;
|
|
98
|
+
}
|
|
99
|
+
css += `@media (prefers-reduced-motion:reduce){.mc-demock-tile{animation:none!important}.mc-demock-shimmer::after{animation:none!important;opacity:0}}\n`;
|
|
100
|
+
return css;
|
|
101
|
+
}
|
|
79
102
|
/** Injects the keyframes once per document (idempotent, SSR-safe). */
|
|
80
103
|
function useMockupStyles() {
|
|
81
104
|
React.useInsertionEffect(() => {
|
|
@@ -85,52 +108,39 @@ function useMockupStyles() {
|
|
|
85
108
|
return;
|
|
86
109
|
const el = document.createElement('style');
|
|
87
110
|
el.id = STYLE_ID;
|
|
88
|
-
el.textContent =
|
|
111
|
+
el.textContent = buildCss();
|
|
89
112
|
document.head.appendChild(el);
|
|
90
113
|
}, []);
|
|
91
114
|
}
|
|
92
|
-
const tileBase = 'rounded-lg bg-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
/** A skeleton "stat card": label + big number. */
|
|
98
|
-
function StatGlyph() {
|
|
99
|
-
return (_jsxs("div", { className: "flex h-full flex-col justify-center gap-2 p-3", children: [_jsx("div", { className: "h-2 w-1/2 rounded-full bg-foreground/15" }), _jsx("div", { className: "h-4 w-2/3 rounded bg-foreground/15" })] }));
|
|
115
|
+
const tileBase = 'rounded-lg border border-border/60 bg-card mc-demock-tile mc-demock-shimmer text-foreground';
|
|
116
|
+
// Header shared by every card kind — icon chip + title bar + subtitle bar,
|
|
117
|
+
// mirroring the real WidgetCard chrome (widgets/widget-card.tsx).
|
|
118
|
+
function SkeletonHeader() {
|
|
119
|
+
return (_jsxs("div", { className: "flex items-start gap-2.5", children: [_jsx("div", { className: "size-8 shrink-0 rounded-lg bg-foreground/10" }), _jsxs("div", { className: "flex-1 space-y-1.5 pt-0.5", children: [_jsx("div", { className: "h-2.5 w-2/3 rounded bg-foreground/12" }), _jsx("div", { className: "h-2 w-2/5 rounded bg-foreground/[.07]" })] })] }));
|
|
100
120
|
}
|
|
101
|
-
|
|
102
|
-
function
|
|
103
|
-
return (_jsxs("div", { className: "flex h-full flex-col justify-center gap-2
|
|
121
|
+
// Bodies copy the anatomy of the matching real renderers (renderers.tsx).
|
|
122
|
+
function TileGlyph({ kind }) {
|
|
123
|
+
return (_jsxs("div", { className: "flex h-full flex-col gap-3 overflow-hidden p-3", children: [_jsx(SkeletonHeader, {}), _jsxs("div", { className: "min-h-0 flex-1", children: [kind === 'stat' && (_jsxs("div", { className: "flex h-full flex-col justify-center gap-2", children: [_jsx("div", { className: "h-6 w-1/2 rounded-md bg-foreground/15" }), _jsx("div", { className: "h-4 w-14 rounded-full bg-foreground/10" })] })), kind === 'chart' && (_jsx("div", { className: "flex h-full items-end gap-1.5", children: [46, 72, 38, 84, 58, 92, 50].map((h, i) => (_jsx("div", { className: "w-full rounded-sm bg-foreground/10", style: { height: `${h}%` } }, i))) })), kind === 'list' && (_jsx("div", { className: "flex h-full flex-col justify-center gap-2.5", children: [28, 40, 34].map((w, i) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "size-5 shrink-0 rounded-full bg-foreground/10" }), _jsx("div", { className: "h-2 flex-1 rounded-full bg-foreground/10" }), _jsx("div", { className: "h-2 shrink-0 rounded-full bg-foreground/12", style: { width: w } })] }, i))) })), kind === 'bar' && (_jsxs("div", { className: "flex h-full flex-col justify-center gap-3", children: [_jsx("div", { className: "h-2.5 w-full rounded-full bg-foreground/10", children: _jsx("div", { className: "h-full w-3/5 rounded-full bg-foreground/20" }) }), _jsx("div", { className: "h-2.5 w-full rounded-full bg-foreground/10", children: _jsx("div", { className: "h-full w-2/5 rounded-full bg-foreground/20" }) })] }))] })] }));
|
|
104
124
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
const TILES = [
|
|
110
|
-
// Big chart (top-left 2x2) — diagonal shuffle with the bottom stat.
|
|
111
|
-
{ area: { gridColumn: '1 / 3', gridRow: '1 / 3' }, motion: 'mc-demock-shuffleA', glyph: _jsx(ChartGlyph, {}) },
|
|
112
|
-
// Horizontal swap pair, top-right.
|
|
113
|
-
{ area: { gridColumn: '3 / 4', gridRow: '1 / 2' }, motion: 'mc-demock-swapL', glyph: _jsx(StatGlyph, {}) },
|
|
114
|
-
{ area: { gridColumn: '4 / 5', gridRow: '1 / 2' }, motion: 'mc-demock-swapR', glyph: _jsx(StatGlyph, {}) },
|
|
115
|
-
// Vertical swap pair, right column.
|
|
116
|
-
{ area: { gridColumn: '3 / 5', gridRow: '2 / 3' }, motion: 'mc-demock-swapU', glyph: _jsx(ListGlyph, {}) },
|
|
117
|
-
{ area: { gridColumn: '3 / 5', gridRow: '3 / 4' }, motion: 'mc-demock-swapD', glyph: _jsx(BarGlyph, {}) },
|
|
118
|
-
// Bottom-left cluster: shuffle partner + drifter.
|
|
119
|
-
{ area: { gridColumn: '1 / 2', gridRow: '3 / 4' }, motion: 'mc-demock-shuffleB', glyph: _jsx(StatGlyph, {}) },
|
|
120
|
-
{ area: { gridColumn: '2 / 3', gridRow: '3 / 4' }, motion: 'mc-demock-driftA', glyph: _jsx(StatGlyph, {}) },
|
|
121
|
-
// Full-width footer bar + trailing stat.
|
|
122
|
-
{ area: { gridColumn: '1 / 4', gridRow: '4 / 5' }, motion: 'mc-demock-driftB', glyph: _jsx(BarGlyph, {}) },
|
|
123
|
-
{ area: { gridColumn: '4 / 5', gridRow: '4 / 5' }, motion: 'mc-demock-driftA', glyph: _jsx(StatGlyph, {}) },
|
|
124
|
-
];
|
|
125
|
+
// Kind per tile, in the [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom]
|
|
126
|
+
// order used by computeLayoutRects.
|
|
127
|
+
const TILE_KINDS = ['stat', 'chart', 'chart', 'list', 'stat', 'bar'];
|
|
125
128
|
/**
|
|
126
|
-
* Full-bleed
|
|
127
|
-
*
|
|
128
|
-
* `aria-hidden`, no text.
|
|
129
|
+
* Full-bleed skeleton dashboard that reflows through four layouts on a loop.
|
|
130
|
+
* Overlap-free by construction (see the invariant note above). Purely
|
|
131
|
+
* decorative — always `aria-hidden`, no text.
|
|
132
|
+
*
|
|
133
|
+
* Contract: tiles are positioned with percentage heights, so the CONTAINER
|
|
134
|
+
* must have a DEFINITE height (e.g. `h-[…]`, or a flex child with `flex-1`).
|
|
135
|
+
* A parent with only `min-height` leaves the box auto-height and the tiles
|
|
136
|
+
* collapse into a strip. The dashboard empty state mounts it inside a
|
|
137
|
+
* definite-height box for exactly this reason.
|
|
129
138
|
*/
|
|
130
139
|
export function DashboardEmptyMockup({ className }) {
|
|
131
140
|
useMockupStyles();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
const layoutA = React.useMemo(() => computeLayoutRects(MOCKUP_LAYOUTS[0]), []);
|
|
142
|
+
return (_jsx("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('mc-demock pointer-events-none select-none', className), children: layoutA.map((r, i) => (_jsx("div", { "data-mockup-tile": "tile", className: cn(tileBase, `mc-demock-t${i}`),
|
|
143
|
+
// Base position = layout A, so reduced-motion (animation off)
|
|
144
|
+
// shows the full static composition.
|
|
145
|
+
style: { left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }, children: _jsx(TileGlyph, { kind: TILE_KINDS[i] }) }, i))) }));
|
|
136
146
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAY1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAY1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAmJpB"}
|
package/dist/dashboard-grid.js
CHANGED
|
@@ -124,7 +124,14 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
|
|
|
124
124
|
// Full-bleed: the mockup fills the whole dashboard area, not a
|
|
125
125
|
// centered illustration. No caption — the animation speaks for
|
|
126
126
|
// itself (and dodges shipping un-localized copy).
|
|
127
|
-
|
|
127
|
+
//
|
|
128
|
+
// DEFINITE height (clamp), not min-height: the mockup positions
|
|
129
|
+
// its tiles with percentage heights, which only resolve against a
|
|
130
|
+
// parent whose height is definite. A min-height alone leaves the
|
|
131
|
+
// box auto-height and the tiles collapse into a strip — so we own
|
|
132
|
+
// the height here instead of hoping the dashboard layout supplies
|
|
133
|
+
// one.
|
|
134
|
+
'h-[clamp(420px,60vh,720px)] w-full rounded-xl border border-dashed border-border/60 p-4', className), children: _jsx(DashboardEmptyMockup, { className: "h-full" }) }));
|
|
128
135
|
}
|
|
129
136
|
return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
|
|
130
137
|
// Masonry: balanced CSS columns. Cards take their natural height
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA2mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -731,12 +731,22 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
731
731
|
if (typeof value === 'object' && value !== null) {
|
|
732
732
|
return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t, itemFields: col.itemFields ?? col.item_fields, getImageUrl: getImageUrl }));
|
|
733
733
|
}
|
|
734
|
-
|
|
734
|
+
const text = value !== null && value !== undefined ? String(value) : '-';
|
|
735
|
+
// Clamp de texto largo GENÉRICO: un `truncate` en un
|
|
736
|
+
// span suelto no limita nada dentro de una celda de
|
|
737
|
+
// tabla (la celda crece con el contenido). Cualquier
|
|
738
|
+
// texto largo — biografías, notas, direcciones — se
|
|
739
|
+
// acota a 350px con tooltip del valor completo. La
|
|
740
|
+
// heurística por longitud cubre lo que la heurística
|
|
741
|
+
// por nombre ('description') dejaba pasar.
|
|
742
|
+
const isLongText = text.length > 60 ||
|
|
743
|
+
col.key === 'description' ||
|
|
735
744
|
col.key === 'features' ||
|
|
736
|
-
col.key.includes('description')
|
|
737
|
-
|
|
745
|
+
col.key.includes('description');
|
|
746
|
+
if (isLongText) {
|
|
747
|
+
return (_jsx("div", { className: "max-w-[350px]", title: text, children: _jsx("span", { className: "truncate font-medium block", children: text }) }));
|
|
738
748
|
}
|
|
739
|
-
return
|
|
749
|
+
return _jsx("span", { className: "truncate font-medium", children: text });
|
|
740
750
|
}
|
|
741
751
|
}
|
|
742
752
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAiEnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAiEnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+BAurCnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -976,8 +976,15 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
976
976
|
const actionsCell = cells.find((c) => c.column.id === 'actions');
|
|
977
977
|
const dataCells = cells.filter((c) => c.column.id !== 'actions' && c.column.id !== 'select');
|
|
978
978
|
return (_jsxs("div", { "data-state": row.getIsSelected() && 'selected', className: cn('flex flex-col gap-1.5 rounded-lg border bg-card p-3 data-[state=selected]:border-primary/40', onRowClick && 'cursor-pointer'), onClick: onRowClick ? () => onRowClick(row.original) : undefined, children: [dataCells.map((cell) => {
|
|
979
|
+
// El label humano vive en columnDef.meta.label (lo
|
|
980
|
+
// adjunta la fábrica de columnas); el header casi
|
|
981
|
+
// nunca es string (es un componente), así que caer a
|
|
982
|
+
// column.id mostraba keys crudas ('user.avatar',
|
|
983
|
+
// 'created_at') en las cards móviles.
|
|
984
|
+
const cellMeta = cell.column.columnDef.meta;
|
|
979
985
|
const header = cell.column.columnDef.header;
|
|
980
|
-
const label =
|
|
986
|
+
const label = cellMeta?.label ??
|
|
987
|
+
(typeof header === 'string' ? header : cell.column.id);
|
|
981
988
|
return (_jsxs("div", { className: 'flex items-start justify-between gap-3 text-sm', children: [_jsx("span", { className: 'shrink-0 text-muted-foreground', children: label }), _jsx("span", { className: 'min-w-0 break-words text-right font-medium', children: flexRender(cell.column.columnDef.cell, cell.getContext()) })] }, cell.id));
|
|
982
989
|
}), actionsCell && (_jsx("div", { className: 'flex justify-end border-t pt-2', onClick: onRowClick ? (e) => e.stopPropagation() : undefined, children: flexRender(actionsCell.column.columnDef.cell, actionsCell.getContext()) }))] }, row.id));
|
|
983
990
|
})) : (_jsxs("div", { className: 'flex flex-col items-center justify-center gap-2 rounded-lg border bg-card py-12 text-muted-foreground', children: [_jsx("div", { className: 'flex h-16 w-16 items-center justify-center rounded-full bg-muted/50', children: _jsx(Inbox, { className: 'h-8 w-8' }) }), _jsx("h3", { className: 'text-base font-semibold text-foreground', children: "No se encontraron resultados" }), _jsx("p", { className: 'text-sm text-muted-foreground', children: "No hay datos para mostrar en este momento." })] })), infiniteScroll && (_jsxs(_Fragment, { children: [loadingMore && (_jsx(Skeleton, { className: 'h-16 w-full shrink-0', "data-testid": 'table-loading-more-mobile' })), _jsx("div", { ref: infMobileSentinel, className: 'h-1 w-full shrink-0', "aria-hidden": true })] }))] }), _jsx("div", { className: 'shrink-0 pt-4', children: infiniteScroll ? (data.length > 0 && (_jsx("p", { className: 'text-center text-xs text-muted-foreground tabular-nums', children: t('common.showingCount', {
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Zero-overlap invariant for the reflowing empty-state mockup.
|
|
2
|
+
//
|
|
3
|
+
// The animation reflows through four layouts (A→B→C→D→A). The hard requirement
|
|
4
|
+
// is that NO two tiles overlap at ANY frame — including mid-transition, where
|
|
5
|
+
// CSS interpolates each tile's rect (left/top/width/height) linearly. This test
|
|
6
|
+
// reproduces that linear interpolation and asserts non-overlap across every
|
|
7
|
+
// transition at fine time steps, so a future layout edit that would introduce a
|
|
8
|
+
// crossing fails here instead of in production.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest'
|
|
11
|
+
import {
|
|
12
|
+
MockupRect,
|
|
13
|
+
mockupLayoutRects,
|
|
14
|
+
} from '../dashboard-empty-mockup'
|
|
15
|
+
|
|
16
|
+
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
|
|
17
|
+
const lerpRect = (a: MockupRect, b: MockupRect, t: number): MockupRect => ({
|
|
18
|
+
x: lerp(a.x, b.x, t),
|
|
19
|
+
y: lerp(a.y, b.y, t),
|
|
20
|
+
w: lerp(a.w, b.w, t),
|
|
21
|
+
h: lerp(a.h, b.h, t),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Overlap area with a tiny epsilon so exact edge-adjacency (gap = 0 would-be
|
|
25
|
+
// touching) does not count; here tiles are separated by a real gap anyway.
|
|
26
|
+
const EPS = 1e-6
|
|
27
|
+
function overlapArea(a: MockupRect, b: MockupRect): number {
|
|
28
|
+
const ox = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)
|
|
29
|
+
const oy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
|
|
30
|
+
if (ox <= EPS || oy <= EPS) return 0
|
|
31
|
+
return ox * oy
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('dashboard empty mockup — zero-overlap invariant', () => {
|
|
35
|
+
const layouts = mockupLayoutRects()
|
|
36
|
+
|
|
37
|
+
it('has 6 tiles in every layout, all within bounds', () => {
|
|
38
|
+
for (const rects of layouts) {
|
|
39
|
+
expect(rects).toHaveLength(6)
|
|
40
|
+
for (const r of rects) {
|
|
41
|
+
expect(r.x).toBeGreaterThanOrEqual(-EPS)
|
|
42
|
+
expect(r.y).toBeGreaterThanOrEqual(-EPS)
|
|
43
|
+
expect(r.x + r.w).toBeLessThanOrEqual(100 + EPS)
|
|
44
|
+
expect(r.y + r.h).toBeLessThanOrEqual(100 + EPS)
|
|
45
|
+
expect(r.w).toBeGreaterThan(0)
|
|
46
|
+
expect(r.h).toBeGreaterThan(0)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('no two tiles overlap in any static layout', () => {
|
|
52
|
+
for (let l = 0; l < layouts.length; l++) {
|
|
53
|
+
const rects = layouts[l]
|
|
54
|
+
for (let i = 0; i < rects.length; i++)
|
|
55
|
+
for (let j = i + 1; j < rects.length; j++)
|
|
56
|
+
expect(overlapArea(rects[i], rects[j]), `layout ${l}, tiles ${i}&${j}`).toBe(0)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('no two tiles overlap at any frame of any transition (A→B→C→D→A)', () => {
|
|
61
|
+
// The loop is cyclic: transition k goes layout k → (k+1) mod 4.
|
|
62
|
+
const n = layouts.length
|
|
63
|
+
let worst = 0
|
|
64
|
+
for (let k = 0; k < n; k++) {
|
|
65
|
+
const from = layouts[k]
|
|
66
|
+
const to = layouts[(k + 1) % n]
|
|
67
|
+
for (let step = 0; step <= 20; step++) {
|
|
68
|
+
const t = step / 20 // 0, 0.05, … , 1
|
|
69
|
+
const frame = from.map((r, idx) => lerpRect(r, to[idx], t))
|
|
70
|
+
for (let i = 0; i < frame.length; i++)
|
|
71
|
+
for (let j = i + 1; j < frame.length; j++) {
|
|
72
|
+
const area = overlapArea(frame[i], frame[j])
|
|
73
|
+
worst = Math.max(worst, area)
|
|
74
|
+
expect(
|
|
75
|
+
area,
|
|
76
|
+
`transition ${k}→${(k + 1) % n} t=${t.toFixed(2)} tiles ${i}&${j}`,
|
|
77
|
+
).toBe(0)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
expect(worst).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -1,83 +1,129 @@
|
|
|
1
1
|
// DashboardEmptyMockup — the animated empty state for the modular dashboard.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// dashboard
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// metaphor is "your board is assembling", not "empty".
|
|
3
|
+
// The whole grid REORGANIZES on a loop: skeleton widget cards grow, shrink and
|
|
4
|
+
// reflow through four dashboard compositions (A → B → C → D → A), as if the
|
|
5
|
+
// board were trying out arrangements while it loads. No "empty" copy — the
|
|
6
|
+
// motion carries the meaning.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// - Pure CSS keyframes, no dependencies. One slow loop (~13s). Motion is
|
|
13
|
-
// PERCEPTIBLE — tiles translate a real distance and swap in pairs — but
|
|
14
|
-
// never opacity-strobing, and eased so it feels like settling.
|
|
15
|
-
// - Theme tokens only (bg-muted / border tokens) → light & dark for free.
|
|
16
|
-
// - `prefers-reduced-motion: reduce` freezes everything (tiles rest in place).
|
|
17
|
-
// - No caption text — the animation carries the meaning, and shipping copy
|
|
18
|
-
// from the package risks un-localized strings. The whole mock is
|
|
19
|
-
// decorative → aria-hidden.
|
|
8
|
+
// ── The zero-overlap invariant (hard requirement) ──────────────────────────
|
|
9
|
+
// Two earlier takes let tiles cross and overlap; the user rejected them. This
|
|
10
|
+
// version makes overlap IMPOSSIBLE by construction, not by tuning:
|
|
20
11
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
12
|
+
// The board is 3 COLUMNS in a FIXED left-to-right order, each split into a
|
|
13
|
+
// TOP and BOTTOM tile — 6 tiles total. Every layout only changes the column
|
|
14
|
+
// WIDTHS and the per-column top/bottom SPLIT. Because column order never
|
|
15
|
+
// changes and top is always above bottom, EVERY pair of tiles keeps a
|
|
16
|
+
// consistent separating axis in every layout, with a constant `gap` between
|
|
17
|
+
// neighbours. Under linear interpolation a gap that is `gap` at both ends of
|
|
18
|
+
// a transition stays exactly `gap` throughout — so no two tiles can ever
|
|
19
|
+
// touch, at any frame, during any transition. (Verified independently by the
|
|
20
|
+
// interpolation check in dashboard-empty-mockup.test — 0 overlaps.)
|
|
21
|
+
//
|
|
22
|
+
// Design constraints kept from the spec:
|
|
23
|
+
// - Full-bleed (fills the whole dashboard area).
|
|
24
|
+
// - Pure CSS keyframes, no dependencies; injected once per document.
|
|
25
|
+
// - Synchronised reflow: all tiles share one duration / easing / timing, so
|
|
26
|
+
// A→B reads as a single reorganisation. ~2s rest per layout, ~1.2s glide.
|
|
27
|
+
// - Theme tokens only (bg-card / border / muted) → light & dark for free.
|
|
28
|
+
// - `prefers-reduced-motion: reduce` → the static A layout, no motion.
|
|
29
|
+
// - Decorative → aria-hidden, no text.
|
|
26
30
|
|
|
27
31
|
import * as React from 'react'
|
|
28
32
|
import { cn } from '@asteby/metacore-ui/lib'
|
|
29
33
|
|
|
30
|
-
// Scoped, collision-proof keyframe + class names (prefixed, unlikely to clash).
|
|
31
34
|
const STYLE_ID = 'mc-dashboard-empty-mockup-style'
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
/** A tile's rectangle in percent of the container (both axes 0–100). */
|
|
37
|
+
export interface MockupRect {
|
|
38
|
+
x: number
|
|
39
|
+
y: number
|
|
40
|
+
w: number
|
|
41
|
+
h: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Each layout = three column width weights + each column's top-tile height
|
|
45
|
+
// fraction. Weights are relative (normalised below), so they read as intent
|
|
46
|
+
// ("this column is the wide one") rather than exact percentages.
|
|
47
|
+
interface LayoutSpec {
|
|
48
|
+
widths: [number, number, number]
|
|
49
|
+
splits: [number, number, number]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Four visibly different compositions: balanced → left-heavy → centre-heavy →
|
|
53
|
+
// right-heavy. The loop returns to A, so it is seamless.
|
|
54
|
+
export const MOCKUP_LAYOUTS: LayoutSpec[] = [
|
|
55
|
+
{ widths: [40, 32, 28], splits: [0.5, 0.6, 0.45] },
|
|
56
|
+
{ widths: [52, 24, 24], splits: [0.62, 0.4, 0.55] },
|
|
57
|
+
{ widths: [24, 52, 24], splits: [0.4, 0.55, 0.62] },
|
|
58
|
+
{ widths: [26, 28, 46], splits: [0.55, 0.5, 0.4] },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** Horizontal/vertical gap between tiles, in percent. */
|
|
62
|
+
export const MOCKUP_GAP = 3
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Computes the six tile rects for one layout. Tile order is
|
|
66
|
+
* [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom].
|
|
67
|
+
*
|
|
68
|
+
* Columns keep a constant `gap` between them and top/bottom keep a constant
|
|
69
|
+
* `gap` between them — this is what makes every transition overlap-free.
|
|
70
|
+
*/
|
|
71
|
+
export function computeLayoutRects(layout: LayoutSpec, gap = MOCKUP_GAP): MockupRect[] {
|
|
72
|
+
const usableW = 100 - 2 * gap
|
|
73
|
+
const usableH = 100 - gap
|
|
74
|
+
const sum = layout.widths[0] + layout.widths[1] + layout.widths[2]
|
|
75
|
+
const colW = layout.widths.map((w) => (w / sum) * usableW) as [number, number, number]
|
|
76
|
+
const colX = [0, colW[0] + gap, colW[0] + colW[1] + 2 * gap]
|
|
77
|
+
|
|
78
|
+
const rects: MockupRect[] = []
|
|
79
|
+
for (let c = 0; c < 3; c++) {
|
|
80
|
+
const topH = layout.splits[c] * usableH
|
|
81
|
+
const botH = usableH - topH
|
|
82
|
+
rects.push({ x: colX[c], y: 0, w: colW[c], h: topH })
|
|
83
|
+
rects.push({ x: colX[c], y: topH + gap, w: colW[c], h: botH })
|
|
84
|
+
}
|
|
85
|
+
return rects
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The precomputed rects for all four layouts (exported for the overlap test). */
|
|
89
|
+
export function mockupLayoutRects(gap = MOCKUP_GAP): MockupRect[][] {
|
|
90
|
+
return MOCKUP_LAYOUTS.map((l) => computeLayoutRects(l, gap))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Keyframe stops. Cycle = 12.8s: per layout ~2s rest + ~1.2s glide. The stops
|
|
94
|
+
// hold each layout, then glide to the next; 100% == 0% (layout A) → seamless.
|
|
95
|
+
const STOPS = [0, 15.625, 25, 40.625, 50, 65.625, 75, 90.625, 100]
|
|
96
|
+
const STOP_LAYOUT = [0, 0, 1, 1, 2, 2, 3, 3, 0] // which layout at each stop
|
|
97
|
+
|
|
98
|
+
const fmt = (n: number) => `${Math.round(n * 1000) / 1000}%`
|
|
99
|
+
|
|
100
|
+
/** Builds the full <style> text: per-tile keyframes cycling through the rects. */
|
|
101
|
+
function buildCss(): string {
|
|
102
|
+
const layouts = mockupLayoutRects()
|
|
103
|
+
const tiles = layouts[0].length
|
|
104
|
+
let css = `
|
|
105
|
+
.mc-demock{position:relative;height:100%;width:100%}
|
|
106
|
+
.mc-demock-tile{position:absolute;
|
|
107
|
+
animation-duration:12.8s;animation-timing-function:cubic-bezier(.65,0,.35,1);
|
|
108
|
+
animation-iteration-count:infinite;will-change:top,left,width,height}
|
|
109
|
+
.mc-demock-shimmer{overflow:hidden}
|
|
55
110
|
.mc-demock-shimmer::after{content:"";position:absolute;inset:0;
|
|
56
111
|
background:linear-gradient(105deg,transparent 35%,currentColor 50%,transparent 65%);
|
|
57
|
-
opacity:.
|
|
58
|
-
animation:mc-demock-sheen
|
|
59
|
-
/* Swaps: hold home -> travel a full cell (+gap) to the partner's slot -> hold
|
|
60
|
-
-> travel back. ~108% of own size clears the tile plus the grid gap. */
|
|
61
|
-
@keyframes mc-demock-swap-left{
|
|
62
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(108%,0)}88%,100%{transform:translate(0,0)}}
|
|
63
|
-
@keyframes mc-demock-swap-right{
|
|
64
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(-108%,0)}88%,100%{transform:translate(0,0)}}
|
|
65
|
-
@keyframes mc-demock-swap-up{
|
|
66
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(0,112%)}88%,100%{transform:translate(0,0)}}
|
|
67
|
-
@keyframes mc-demock-swap-down{
|
|
68
|
-
0%,12%{transform:translate(0,0)}38%,62%{transform:translate(0,-112%)}88%,100%{transform:translate(0,0)}}
|
|
69
|
-
@keyframes mc-demock-shuffle-a{
|
|
70
|
-
0%,15%{transform:translate(0,0) scale(1)}45%,60%{transform:translate(106%,110%) scale(.96)}90%,100%{transform:translate(0,0) scale(1)}}
|
|
71
|
-
@keyframes mc-demock-shuffle-b{
|
|
72
|
-
0%,15%{transform:translate(0,0) scale(1)}45%,60%{transform:translate(-106%,-110%) scale(1.04)}90%,100%{transform:translate(0,0) scale(1)}}
|
|
73
|
-
@keyframes mc-demock-drift-a{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(4%,-5%) scale(1.02)}}
|
|
74
|
-
@keyframes mc-demock-drift-b{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-5%,4%) scale(.98)}}
|
|
112
|
+
opacity:.045;transform:translateX(-120%);
|
|
113
|
+
animation:mc-demock-sheen 12.8s ease-in-out infinite}
|
|
75
114
|
@keyframes mc-demock-sheen{0%,100%{transform:translateX(-120%)}55%,72%{transform:translateX(120%)}}
|
|
76
|
-
@media (prefers-reduced-motion:reduce){
|
|
77
|
-
.mc-demock-tile{animation:none!important}
|
|
78
|
-
.mc-demock-shimmer::after{animation:none!important;opacity:0}
|
|
79
|
-
}
|
|
80
115
|
`
|
|
116
|
+
for (let i = 0; i < tiles; i++) {
|
|
117
|
+
css += `.mc-demock-t${i}{animation-name:mc-demock-k${i}}\n@keyframes mc-demock-k${i}{`
|
|
118
|
+
for (let s = 0; s < STOPS.length; s++) {
|
|
119
|
+
const r = layouts[STOP_LAYOUT[s]][i]
|
|
120
|
+
css += `${fmt(STOPS[s])}{left:${fmt(r.x)};top:${fmt(r.y)};width:${fmt(r.w)};height:${fmt(r.h)}}`
|
|
121
|
+
}
|
|
122
|
+
css += `}\n`
|
|
123
|
+
}
|
|
124
|
+
css += `@media (prefers-reduced-motion:reduce){.mc-demock-tile{animation:none!important}.mc-demock-shimmer::after{animation:none!important;opacity:0}}\n`
|
|
125
|
+
return css
|
|
126
|
+
}
|
|
81
127
|
|
|
82
128
|
/** Injects the keyframes once per document (idempotent, SSR-safe). */
|
|
83
129
|
function useMockupStyles() {
|
|
@@ -86,109 +132,118 @@ function useMockupStyles() {
|
|
|
86
132
|
if (document.getElementById(STYLE_ID)) return
|
|
87
133
|
const el = document.createElement('style')
|
|
88
134
|
el.id = STYLE_ID
|
|
89
|
-
el.textContent =
|
|
135
|
+
el.textContent = buildCss()
|
|
90
136
|
document.head.appendChild(el)
|
|
91
137
|
}, [])
|
|
92
138
|
}
|
|
93
139
|
|
|
94
140
|
const tileBase =
|
|
95
|
-
'rounded-lg bg-
|
|
96
|
-
|
|
97
|
-
/** A skeleton "chart" tile: a row of bars. */
|
|
98
|
-
function ChartGlyph() {
|
|
99
|
-
return (
|
|
100
|
-
<div className="flex h-full items-end gap-1.5 p-3">
|
|
101
|
-
{[6, 10, 7, 12, 9, 11, 8].map((h, i) => (
|
|
102
|
-
<div
|
|
103
|
-
key={i}
|
|
104
|
-
className="w-full rounded-sm bg-foreground/10"
|
|
105
|
-
style={{ height: `${h * 8}%` }}
|
|
106
|
-
/>
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
)
|
|
110
|
-
}
|
|
141
|
+
'rounded-lg border border-border/60 bg-card mc-demock-tile mc-demock-shimmer text-foreground'
|
|
111
142
|
|
|
112
|
-
|
|
113
|
-
function StatGlyph() {
|
|
114
|
-
return (
|
|
115
|
-
<div className="flex h-full flex-col justify-center gap-2 p-3">
|
|
116
|
-
<div className="h-2 w-1/2 rounded-full bg-foreground/15" />
|
|
117
|
-
<div className="h-4 w-2/3 rounded bg-foreground/15" />
|
|
118
|
-
</div>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
143
|
+
type Glyph = 'chart' | 'stat' | 'list' | 'bar'
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
// Header shared by every card kind — icon chip + title bar + subtitle bar,
|
|
146
|
+
// mirroring the real WidgetCard chrome (widgets/widget-card.tsx).
|
|
147
|
+
function SkeletonHeader() {
|
|
124
148
|
return (
|
|
125
|
-
<div className="flex
|
|
126
|
-
<div className="
|
|
127
|
-
<div className="
|
|
128
|
-
|
|
149
|
+
<div className="flex items-start gap-2.5">
|
|
150
|
+
<div className="size-8 shrink-0 rounded-lg bg-foreground/10" />
|
|
151
|
+
<div className="flex-1 space-y-1.5 pt-0.5">
|
|
152
|
+
<div className="h-2.5 w-2/3 rounded bg-foreground/12" />
|
|
153
|
+
<div className="h-2 w-2/5 rounded bg-foreground/[.07]" />
|
|
154
|
+
</div>
|
|
129
155
|
</div>
|
|
130
156
|
)
|
|
131
157
|
}
|
|
132
158
|
|
|
133
|
-
|
|
134
|
-
function
|
|
159
|
+
// Bodies copy the anatomy of the matching real renderers (renderers.tsx).
|
|
160
|
+
function TileGlyph({ kind }: { kind: Glyph }) {
|
|
135
161
|
return (
|
|
136
|
-
<div className="flex h-full
|
|
137
|
-
<
|
|
138
|
-
|
|
162
|
+
<div className="flex h-full flex-col gap-3 overflow-hidden p-3">
|
|
163
|
+
<SkeletonHeader />
|
|
164
|
+
<div className="min-h-0 flex-1">
|
|
165
|
+
{kind === 'stat' && (
|
|
166
|
+
<div className="flex h-full flex-col justify-center gap-2">
|
|
167
|
+
<div className="h-6 w-1/2 rounded-md bg-foreground/15" />
|
|
168
|
+
<div className="h-4 w-14 rounded-full bg-foreground/10" />
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{kind === 'chart' && (
|
|
172
|
+
<div className="flex h-full items-end gap-1.5">
|
|
173
|
+
{[46, 72, 38, 84, 58, 92, 50].map((h, i) => (
|
|
174
|
+
<div
|
|
175
|
+
key={i}
|
|
176
|
+
className="w-full rounded-sm bg-foreground/10"
|
|
177
|
+
style={{ height: `${h}%` }}
|
|
178
|
+
/>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
{kind === 'list' && (
|
|
183
|
+
<div className="flex h-full flex-col justify-center gap-2.5">
|
|
184
|
+
{[28, 40, 34].map((w, i) => (
|
|
185
|
+
<div key={i} className="flex items-center gap-2">
|
|
186
|
+
<div className="size-5 shrink-0 rounded-full bg-foreground/10" />
|
|
187
|
+
<div className="h-2 flex-1 rounded-full bg-foreground/10" />
|
|
188
|
+
<div
|
|
189
|
+
className="h-2 shrink-0 rounded-full bg-foreground/12"
|
|
190
|
+
style={{ width: w }}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
{kind === 'bar' && (
|
|
197
|
+
<div className="flex h-full flex-col justify-center gap-3">
|
|
198
|
+
<div className="h-2.5 w-full rounded-full bg-foreground/10">
|
|
199
|
+
<div className="h-full w-3/5 rounded-full bg-foreground/20" />
|
|
200
|
+
</div>
|
|
201
|
+
<div className="h-2.5 w-full rounded-full bg-foreground/10">
|
|
202
|
+
<div className="h-full w-2/5 rounded-full bg-foreground/20" />
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
139
206
|
</div>
|
|
140
207
|
</div>
|
|
141
208
|
)
|
|
142
209
|
}
|
|
143
210
|
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
type Tile = { area: React.CSSProperties; motion: string; glyph: React.ReactNode }
|
|
148
|
-
|
|
149
|
-
const TILES: Tile[] = [
|
|
150
|
-
// Big chart (top-left 2x2) — diagonal shuffle with the bottom stat.
|
|
151
|
-
{ area: { gridColumn: '1 / 3', gridRow: '1 / 3' }, motion: 'mc-demock-shuffleA', glyph: <ChartGlyph /> },
|
|
152
|
-
// Horizontal swap pair, top-right.
|
|
153
|
-
{ area: { gridColumn: '3 / 4', gridRow: '1 / 2' }, motion: 'mc-demock-swapL', glyph: <StatGlyph /> },
|
|
154
|
-
{ area: { gridColumn: '4 / 5', gridRow: '1 / 2' }, motion: 'mc-demock-swapR', glyph: <StatGlyph /> },
|
|
155
|
-
// Vertical swap pair, right column.
|
|
156
|
-
{ area: { gridColumn: '3 / 5', gridRow: '2 / 3' }, motion: 'mc-demock-swapU', glyph: <ListGlyph /> },
|
|
157
|
-
{ area: { gridColumn: '3 / 5', gridRow: '3 / 4' }, motion: 'mc-demock-swapD', glyph: <BarGlyph /> },
|
|
158
|
-
// Bottom-left cluster: shuffle partner + drifter.
|
|
159
|
-
{ area: { gridColumn: '1 / 2', gridRow: '3 / 4' }, motion: 'mc-demock-shuffleB', glyph: <StatGlyph /> },
|
|
160
|
-
{ area: { gridColumn: '2 / 3', gridRow: '3 / 4' }, motion: 'mc-demock-driftA', glyph: <StatGlyph /> },
|
|
161
|
-
// Full-width footer bar + trailing stat.
|
|
162
|
-
{ area: { gridColumn: '1 / 4', gridRow: '4 / 5' }, motion: 'mc-demock-driftB', glyph: <BarGlyph /> },
|
|
163
|
-
{ area: { gridColumn: '4 / 5', gridRow: '4 / 5' }, motion: 'mc-demock-driftA', glyph: <StatGlyph /> },
|
|
164
|
-
]
|
|
211
|
+
// Kind per tile, in the [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom]
|
|
212
|
+
// order used by computeLayoutRects.
|
|
213
|
+
const TILE_KINDS: Glyph[] = ['stat', 'chart', 'chart', 'list', 'stat', 'bar']
|
|
165
214
|
|
|
166
215
|
/**
|
|
167
|
-
* Full-bleed
|
|
168
|
-
*
|
|
169
|
-
* `aria-hidden`, no text.
|
|
216
|
+
* Full-bleed skeleton dashboard that reflows through four layouts on a loop.
|
|
217
|
+
* Overlap-free by construction (see the invariant note above). Purely
|
|
218
|
+
* decorative — always `aria-hidden`, no text.
|
|
219
|
+
*
|
|
220
|
+
* Contract: tiles are positioned with percentage heights, so the CONTAINER
|
|
221
|
+
* must have a DEFINITE height (e.g. `h-[…]`, or a flex child with `flex-1`).
|
|
222
|
+
* A parent with only `min-height` leaves the box auto-height and the tiles
|
|
223
|
+
* collapse into a strip. The dashboard empty state mounts it inside a
|
|
224
|
+
* definite-height box for exactly this reason.
|
|
170
225
|
*/
|
|
171
226
|
export function DashboardEmptyMockup({ className }: { className?: string }) {
|
|
172
227
|
useMockupStyles()
|
|
228
|
+
const layoutA = React.useMemo(() => computeLayoutRects(MOCKUP_LAYOUTS[0]), [])
|
|
173
229
|
return (
|
|
174
230
|
<div
|
|
175
231
|
aria-hidden="true"
|
|
176
232
|
data-testid="dashboard-empty-mockup"
|
|
177
|
-
className={cn('mc-demock pointer-events-none
|
|
233
|
+
className={cn('mc-demock pointer-events-none select-none', className)}
|
|
178
234
|
>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
</div>
|
|
235
|
+
{layoutA.map((r, i) => (
|
|
236
|
+
<div
|
|
237
|
+
key={i}
|
|
238
|
+
data-mockup-tile="tile"
|
|
239
|
+
className={cn(tileBase, `mc-demock-t${i}`)}
|
|
240
|
+
// Base position = layout A, so reduced-motion (animation off)
|
|
241
|
+
// shows the full static composition.
|
|
242
|
+
style={{ left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }}
|
|
243
|
+
>
|
|
244
|
+
<TileGlyph kind={TILE_KINDS[i]} />
|
|
245
|
+
</div>
|
|
246
|
+
))}
|
|
192
247
|
</div>
|
|
193
248
|
)
|
|
194
249
|
}
|
package/src/dashboard-grid.tsx
CHANGED
|
@@ -157,11 +157,18 @@ export function DashboardGrid({
|
|
|
157
157
|
// Full-bleed: the mockup fills the whole dashboard area, not a
|
|
158
158
|
// centered illustration. No caption — the animation speaks for
|
|
159
159
|
// itself (and dodges shipping un-localized copy).
|
|
160
|
-
|
|
160
|
+
//
|
|
161
|
+
// DEFINITE height (clamp), not min-height: the mockup positions
|
|
162
|
+
// its tiles with percentage heights, which only resolve against a
|
|
163
|
+
// parent whose height is definite. A min-height alone leaves the
|
|
164
|
+
// box auto-height and the tiles collapse into a strip — so we own
|
|
165
|
+
// the height here instead of hoping the dashboard layout supplies
|
|
166
|
+
// one.
|
|
167
|
+
'h-[clamp(420px,60vh,720px)] w-full rounded-xl border border-dashed border-border/60 p-4',
|
|
161
168
|
className,
|
|
162
169
|
)}
|
|
163
170
|
>
|
|
164
|
-
<DashboardEmptyMockup className="h-full
|
|
171
|
+
<DashboardEmptyMockup className="h-full" />
|
|
165
172
|
</div>
|
|
166
173
|
)
|
|
167
174
|
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -1143,24 +1143,30 @@ export function makeDefaultGetDynamicColumns(
|
|
|
1143
1143
|
/>
|
|
1144
1144
|
)
|
|
1145
1145
|
}
|
|
1146
|
-
|
|
1146
|
+
const text =
|
|
1147
|
+
value !== null && value !== undefined ? String(value) : '-'
|
|
1148
|
+
// Clamp de texto largo GENÉRICO: un `truncate` en un
|
|
1149
|
+
// span suelto no limita nada dentro de una celda de
|
|
1150
|
+
// tabla (la celda crece con el contenido). Cualquier
|
|
1151
|
+
// texto largo — biografías, notas, direcciones — se
|
|
1152
|
+
// acota a 350px con tooltip del valor completo. La
|
|
1153
|
+
// heurística por longitud cubre lo que la heurística
|
|
1154
|
+
// por nombre ('description') dejaba pasar.
|
|
1155
|
+
const isLongText =
|
|
1156
|
+
text.length > 60 ||
|
|
1147
1157
|
col.key === 'description' ||
|
|
1148
1158
|
col.key === 'features' ||
|
|
1149
1159
|
col.key.includes('description')
|
|
1150
|
-
) {
|
|
1160
|
+
if (isLongText) {
|
|
1151
1161
|
return (
|
|
1152
|
-
<div className="max-w-[350px]" title={
|
|
1162
|
+
<div className="max-w-[350px]" title={text}>
|
|
1153
1163
|
<span className="truncate font-medium block">
|
|
1154
|
-
{
|
|
1164
|
+
{text}
|
|
1155
1165
|
</span>
|
|
1156
1166
|
</div>
|
|
1157
1167
|
)
|
|
1158
1168
|
}
|
|
1159
|
-
return
|
|
1160
|
-
<span className="truncate font-medium">
|
|
1161
|
-
{value !== null && value !== undefined ? String(value) : '-'}
|
|
1162
|
-
</span>
|
|
1163
|
-
)
|
|
1169
|
+
return <span className="truncate font-medium">{text}</span>
|
|
1164
1170
|
}
|
|
1165
1171
|
}
|
|
1166
1172
|
},
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -1286,8 +1286,18 @@ export function DynamicTable({
|
|
|
1286
1286
|
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
|
1287
1287
|
>
|
|
1288
1288
|
{dataCells.map((cell: Cell<any, unknown>) => {
|
|
1289
|
+
// El label humano vive en columnDef.meta.label (lo
|
|
1290
|
+
// adjunta la fábrica de columnas); el header casi
|
|
1291
|
+
// nunca es string (es un componente), así que caer a
|
|
1292
|
+
// column.id mostraba keys crudas ('user.avatar',
|
|
1293
|
+
// 'created_at') en las cards móviles.
|
|
1294
|
+
const cellMeta = cell.column.columnDef.meta as
|
|
1295
|
+
| { label?: string }
|
|
1296
|
+
| undefined
|
|
1289
1297
|
const header = cell.column.columnDef.header
|
|
1290
|
-
const label =
|
|
1298
|
+
const label =
|
|
1299
|
+
cellMeta?.label ??
|
|
1300
|
+
(typeof header === 'string' ? header : cell.column.id)
|
|
1291
1301
|
return (
|
|
1292
1302
|
<div key={cell.id} className='flex items-start justify-between gap-3 text-sm'>
|
|
1293
1303
|
<span className='shrink-0 text-muted-foreground'>{label}</span>
|