@asteby/metacore-runtime-react 27.3.0 → 27.3.1
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 +15 -0
- package/dist/dashboard-empty-mockup.d.ts +3 -2
- package/dist/dashboard-empty-mockup.d.ts.map +1 -1
- package/dist/dashboard-empty-mockup.js +90 -32
- package/dist/dashboard-grid.d.ts.map +1 -1
- package/dist/dashboard-grid.js +6 -2
- package/package.json +1 -1
- package/src/__tests__/dashboard-grid.test.tsx +3 -0
- package/src/dashboard-empty-mockup.tsx +136 -64
- package/src/dashboard-grid.tsx +5 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 27.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ccfb7b0: Empty state del Tablero: el mockup animado ahora es full-bleed, se mueve más y
|
|
8
|
+
va sin texto. Antes se percibía como una ilustración centrada casi estática con
|
|
9
|
+
un caption en inglés. Ahora los tiles skeleton llenan todo el área del
|
|
10
|
+
dashboard en una grilla 4×4 y se reacomodan de forma visible —pares que se
|
|
11
|
+
intercambian de lugar (horizontal, vertical y diagonal) con translate suave,
|
|
12
|
+
escalonados para que siempre haya 2-3 piezas en movimiento— como si el tablero
|
|
13
|
+
se estuviera armando solo. Se eliminó el caption ("Your dashboard is taking
|
|
14
|
+
shape"): la animación habla sola y así no se envía copy sin localizar. Se
|
|
15
|
+
mantiene el loop lento (~13s), tokens de tema y `prefers-reduced-motion: reduce`
|
|
16
|
+
(congela todo). El mock sigue decorativo (`aria-hidden`).
|
|
17
|
+
|
|
3
18
|
## 27.3.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Full-bleed animated skeleton of a dashboard whose tiles slide and swap places,
|
|
4
|
+
* as if the layout were assembling itself. Purely decorative — always
|
|
5
|
+
* `aria-hidden`, no text.
|
|
5
6
|
*/
|
|
6
7
|
export declare function DashboardEmptyMockup({ className }: {
|
|
7
8
|
className?: string;
|
|
@@ -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":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2I9B;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAuBzE"}
|
|
@@ -2,19 +2,24 @@ 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
4
|
// Instead of a static "nothing here" card, we paint a silhouette of the
|
|
5
|
-
// dashboard itself: skeleton tiles (a big chart,
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// dashboard itself and let it BUILD ITSELF: skeleton tiles (a big chart, stat
|
|
6
|
+
// cards, a bar, lists) that live in a full-bleed grid and visibly slide past
|
|
7
|
+
// each other, swapping places like pieces reorganizing into a layout. The
|
|
8
|
+
// metaphor is "your board is assembling", not "empty".
|
|
8
9
|
//
|
|
9
|
-
// Design constraints (kept
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
10
|
+
// Design constraints (kept simple so it ships from a package):
|
|
11
|
+
// - Full-bleed: fills the whole dashboard area (100% w/h), NOT a centered
|
|
12
|
+
// illustration. A responsive grid so it reads like the real dashboard.
|
|
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.
|
|
12
16
|
// - Theme tokens only (bg-muted / border tokens) → light & dark for free.
|
|
13
17
|
// - `prefers-reduced-motion: reduce` freezes everything (tiles rest in place).
|
|
14
|
-
// -
|
|
15
|
-
//
|
|
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.
|
|
16
21
|
//
|
|
17
|
-
//
|
|
22
|
+
// Keyframes ship inline in a <style> tag rather than as Tailwind utilities:
|
|
18
23
|
// consumer apps scan only their OWN source for Tailwind classes, so arbitrary
|
|
19
24
|
// animation utilities declared here would never be generated in their build.
|
|
20
25
|
// Static tokens (bg-muted, rounded-lg…) are standard classes the host emits
|
|
@@ -23,32 +28,49 @@ import * as React from 'react';
|
|
|
23
28
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
24
29
|
// Scoped, collision-proof keyframe + class names (prefixed, unlikely to clash).
|
|
25
30
|
const STYLE_ID = 'mc-dashboard-empty-mockup-style';
|
|
31
|
+
// Each tile lives in a grid cell and animates on a shared ~13s loop. Pairs move
|
|
32
|
+
// in opposite directions at the same phase so it reads as a SWAP, and the phases
|
|
33
|
+
// are staggered (via negative delays) so 2-3 tiles are always in motion at
|
|
34
|
+
// different moments of the cycle — never a frozen frame.
|
|
26
35
|
const MOCKUP_CSS = `
|
|
27
|
-
.mc-demock{--mc-demock-dur:
|
|
36
|
+
.mc-demock{--mc-demock-dur:13s}
|
|
28
37
|
.mc-demock-tile{will-change:transform;transform-origin:center;
|
|
29
38
|
animation-duration:var(--mc-demock-dur);animation-timing-function:cubic-bezier(.65,0,.35,1);
|
|
30
39
|
animation-iteration-count:infinite}
|
|
31
|
-
|
|
32
|
-
.mc-demock-
|
|
33
|
-
.mc-demock-
|
|
34
|
-
|
|
35
|
-
.mc-demock-
|
|
40
|
+
/* Horizontal swap pair (col A <-> col B) */
|
|
41
|
+
.mc-demock-swapL{animation-name:mc-demock-swap-left}
|
|
42
|
+
.mc-demock-swapR{animation-name:mc-demock-swap-right;animation-delay:-.2s}
|
|
43
|
+
/* Vertical swap pair */
|
|
44
|
+
.mc-demock-swapU{animation-name:mc-demock-swap-up;animation-delay:-4.3s}
|
|
45
|
+
.mc-demock-swapD{animation-name:mc-demock-swap-down;animation-delay:-4.5s}
|
|
46
|
+
/* Diagonal reshuffle pair */
|
|
47
|
+
.mc-demock-shuffleA{animation-name:mc-demock-shuffle-a;animation-delay:-8.1s}
|
|
48
|
+
.mc-demock-shuffleB{animation-name:mc-demock-shuffle-b;animation-delay:-8.3s}
|
|
49
|
+
/* Gentle drifters filling the rhythm between swaps */
|
|
50
|
+
.mc-demock-driftA{animation-name:mc-demock-drift-a;animation-delay:-2.4s}
|
|
51
|
+
.mc-demock-driftB{animation-name:mc-demock-drift-b;animation-delay:-6.7s}
|
|
36
52
|
.mc-demock-shimmer{position:relative;overflow:hidden}
|
|
37
53
|
.mc-demock-shimmer::after{content:"";position:absolute;inset:0;
|
|
38
|
-
background:linear-gradient(105deg,transparent
|
|
39
|
-
opacity:.05;transform:translateX(-
|
|
54
|
+
background:linear-gradient(105deg,transparent 35%,currentColor 50%,transparent 65%);
|
|
55
|
+
opacity:.05;transform:translateX(-120%);
|
|
40
56
|
animation:mc-demock-sheen var(--mc-demock-dur) ease-in-out infinite}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@keyframes mc-demock-
|
|
44
|
-
|
|
45
|
-
@keyframes mc-demock-
|
|
46
|
-
|
|
47
|
-
@keyframes mc-demock-
|
|
48
|
-
|
|
49
|
-
@keyframes mc-demock-
|
|
50
|
-
|
|
51
|
-
@keyframes mc-demock-
|
|
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)}}
|
|
73
|
+
@keyframes mc-demock-sheen{0%,100%{transform:translateX(-120%)}55%,72%{transform:translateX(120%)}}
|
|
52
74
|
@media (prefers-reduced-motion:reduce){
|
|
53
75
|
.mc-demock-tile{animation:none!important}
|
|
54
76
|
.mc-demock-shimmer::after{animation:none!important;opacity:0}
|
|
@@ -67,12 +89,48 @@ function useMockupStyles() {
|
|
|
67
89
|
document.head.appendChild(el);
|
|
68
90
|
}, []);
|
|
69
91
|
}
|
|
70
|
-
const
|
|
92
|
+
const tileBase = 'rounded-lg bg-muted mc-demock-tile mc-demock-shimmer text-foreground overflow-hidden';
|
|
93
|
+
/** A skeleton "chart" tile: a row of bars. */
|
|
94
|
+
function ChartGlyph() {
|
|
95
|
+
return (_jsx("div", { className: "flex h-full items-end gap-1.5 p-3", children: [6, 10, 7, 12, 9, 11, 8].map((h, i) => (_jsx("div", { className: "w-full rounded-sm bg-foreground/10", style: { height: `${h * 8}%` } }, i))) }));
|
|
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" })] }));
|
|
100
|
+
}
|
|
101
|
+
/** A skeleton "list" tile: stacked rows. */
|
|
102
|
+
function ListGlyph() {
|
|
103
|
+
return (_jsxs("div", { className: "flex h-full flex-col justify-center gap-2 p-3", children: [_jsx("div", { className: "h-2 w-full rounded-full bg-foreground/12" }), _jsx("div", { className: "h-2 w-4/5 rounded-full bg-foreground/12" }), _jsx("div", { className: "h-2 w-2/3 rounded-full bg-foreground/12" })] }));
|
|
104
|
+
}
|
|
105
|
+
/** A skeleton "progress bar" tile. */
|
|
106
|
+
function BarGlyph() {
|
|
107
|
+
return (_jsx("div", { className: "flex h-full items-center px-3", children: _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" }) }) }));
|
|
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
|
+
];
|
|
71
125
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
126
|
+
* Full-bleed animated skeleton of a dashboard whose tiles slide and swap places,
|
|
127
|
+
* as if the layout were assembling itself. Purely decorative — always
|
|
128
|
+
* `aria-hidden`, no text.
|
|
74
129
|
*/
|
|
75
130
|
export function DashboardEmptyMockup({ className }) {
|
|
76
131
|
useMockupStyles();
|
|
77
|
-
return (_jsx("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('mc-demock pointer-events-none
|
|
132
|
+
return (_jsx("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('mc-demock pointer-events-none h-full w-full select-none', className), children: _jsx("div", { className: "grid h-full w-full gap-3", style: {
|
|
133
|
+
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
|
|
134
|
+
gridTemplateRows: 'repeat(4, minmax(0, 1fr))',
|
|
135
|
+
}, children: TILES.map((t, i) => (_jsx("div", { style: t.area, className: cn(tileBase, t.motion), children: t.glyph }, i))) }) }));
|
|
78
136
|
}
|
|
@@ -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,qBA4IpB"}
|
package/dist/dashboard-grid.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// DashboardGrid — the modular dashboard surface. Renders the union of
|
|
3
3
|
// declarative + federated widgets in a responsive 4-column grid, grouped with
|
|
4
4
|
// headings, honouring per-widget size, permission gating (useCan), batch data
|
|
@@ -120,7 +120,11 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
|
|
|
120
120
|
}, [visibleGroups]);
|
|
121
121
|
// Global empty state (no widgets at all / none visible after gating).
|
|
122
122
|
if (visibleGroups.length === 0) {
|
|
123
|
-
return (
|
|
123
|
+
return (_jsx("div", { "data-testid": "dashboard-empty", className: cn(
|
|
124
|
+
// Full-bleed: the mockup fills the whole dashboard area, not a
|
|
125
|
+
// centered illustration. No caption — the animation speaks for
|
|
126
|
+
// itself (and dodges shipping un-localized copy).
|
|
127
|
+
'min-h-[60vh] w-full rounded-xl border border-dashed border-border/60 p-4', className), children: _jsx(DashboardEmptyMockup, { className: "h-full min-h-[inherit]" }) }));
|
|
124
128
|
}
|
|
125
129
|
return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
|
|
126
130
|
// Masonry: balanced CSS columns. Cards take their natural height
|
package/package.json
CHANGED
|
@@ -164,6 +164,9 @@ describe('DashboardGrid render', () => {
|
|
|
164
164
|
expect(mock.getAttribute('aria-hidden')).toBe('true')
|
|
165
165
|
// Keyframes are injected once into the document head.
|
|
166
166
|
expect(document.getElementById('mc-dashboard-empty-mockup-style')).toBeTruthy()
|
|
167
|
+
// No caption text — the animation carries the meaning (and avoids
|
|
168
|
+
// shipping un-localized copy from the package).
|
|
169
|
+
expect(screen.queryByText(/dashboard/i)).toBeNull()
|
|
167
170
|
})
|
|
168
171
|
|
|
169
172
|
it('survives an empty → populated transition (React #310 regression)', async () => {
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
// DashboardEmptyMockup — the animated empty state for the modular dashboard.
|
|
2
2
|
//
|
|
3
3
|
// Instead of a static "nothing here" card, we paint a silhouette of the
|
|
4
|
-
// dashboard itself: skeleton tiles (a big chart,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// dashboard itself and let it BUILD ITSELF: skeleton tiles (a big chart, stat
|
|
5
|
+
// cards, a bar, lists) that live in a full-bleed grid and visibly slide past
|
|
6
|
+
// each other, swapping places like pieces reorganizing into a layout. The
|
|
7
|
+
// metaphor is "your board is assembling", not "empty".
|
|
7
8
|
//
|
|
8
|
-
// Design constraints (kept
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
9
|
+
// Design constraints (kept simple so it ships from a package):
|
|
10
|
+
// - Full-bleed: fills the whole dashboard area (100% w/h), NOT a centered
|
|
11
|
+
// illustration. A responsive grid so it reads like the real dashboard.
|
|
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.
|
|
11
15
|
// - Theme tokens only (bg-muted / border tokens) → light & dark for free.
|
|
12
16
|
// - `prefers-reduced-motion: reduce` freezes everything (tiles rest in place).
|
|
13
|
-
// -
|
|
14
|
-
//
|
|
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.
|
|
15
20
|
//
|
|
16
|
-
//
|
|
21
|
+
// Keyframes ship inline in a <style> tag rather than as Tailwind utilities:
|
|
17
22
|
// consumer apps scan only their OWN source for Tailwind classes, so arbitrary
|
|
18
23
|
// animation utilities declared here would never be generated in their build.
|
|
19
24
|
// Static tokens (bg-muted, rounded-lg…) are standard classes the host emits
|
|
@@ -25,32 +30,49 @@ import { cn } from '@asteby/metacore-ui/lib'
|
|
|
25
30
|
// Scoped, collision-proof keyframe + class names (prefixed, unlikely to clash).
|
|
26
31
|
const STYLE_ID = 'mc-dashboard-empty-mockup-style'
|
|
27
32
|
|
|
33
|
+
// Each tile lives in a grid cell and animates on a shared ~13s loop. Pairs move
|
|
34
|
+
// in opposite directions at the same phase so it reads as a SWAP, and the phases
|
|
35
|
+
// are staggered (via negative delays) so 2-3 tiles are always in motion at
|
|
36
|
+
// different moments of the cycle — never a frozen frame.
|
|
28
37
|
const MOCKUP_CSS = `
|
|
29
|
-
.mc-demock{--mc-demock-dur:
|
|
38
|
+
.mc-demock{--mc-demock-dur:13s}
|
|
30
39
|
.mc-demock-tile{will-change:transform;transform-origin:center;
|
|
31
40
|
animation-duration:var(--mc-demock-dur);animation-timing-function:cubic-bezier(.65,0,.35,1);
|
|
32
41
|
animation-iteration-count:infinite}
|
|
33
|
-
|
|
34
|
-
.mc-demock-
|
|
35
|
-
.mc-demock-
|
|
36
|
-
|
|
37
|
-
.mc-demock-
|
|
42
|
+
/* Horizontal swap pair (col A <-> col B) */
|
|
43
|
+
.mc-demock-swapL{animation-name:mc-demock-swap-left}
|
|
44
|
+
.mc-demock-swapR{animation-name:mc-demock-swap-right;animation-delay:-.2s}
|
|
45
|
+
/* Vertical swap pair */
|
|
46
|
+
.mc-demock-swapU{animation-name:mc-demock-swap-up;animation-delay:-4.3s}
|
|
47
|
+
.mc-demock-swapD{animation-name:mc-demock-swap-down;animation-delay:-4.5s}
|
|
48
|
+
/* Diagonal reshuffle pair */
|
|
49
|
+
.mc-demock-shuffleA{animation-name:mc-demock-shuffle-a;animation-delay:-8.1s}
|
|
50
|
+
.mc-demock-shuffleB{animation-name:mc-demock-shuffle-b;animation-delay:-8.3s}
|
|
51
|
+
/* Gentle drifters filling the rhythm between swaps */
|
|
52
|
+
.mc-demock-driftA{animation-name:mc-demock-drift-a;animation-delay:-2.4s}
|
|
53
|
+
.mc-demock-driftB{animation-name:mc-demock-drift-b;animation-delay:-6.7s}
|
|
38
54
|
.mc-demock-shimmer{position:relative;overflow:hidden}
|
|
39
55
|
.mc-demock-shimmer::after{content:"";position:absolute;inset:0;
|
|
40
|
-
background:linear-gradient(105deg,transparent
|
|
41
|
-
opacity:.05;transform:translateX(-
|
|
56
|
+
background:linear-gradient(105deg,transparent 35%,currentColor 50%,transparent 65%);
|
|
57
|
+
opacity:.05;transform:translateX(-120%);
|
|
42
58
|
animation:mc-demock-sheen var(--mc-demock-dur) ease-in-out infinite}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@keyframes mc-demock-
|
|
46
|
-
|
|
47
|
-
@keyframes mc-demock-
|
|
48
|
-
|
|
49
|
-
@keyframes mc-demock-
|
|
50
|
-
|
|
51
|
-
@keyframes mc-demock-
|
|
52
|
-
|
|
53
|
-
@keyframes mc-demock-
|
|
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)}}
|
|
75
|
+
@keyframes mc-demock-sheen{0%,100%{transform:translateX(-120%)}55%,72%{transform:translateX(120%)}}
|
|
54
76
|
@media (prefers-reduced-motion:reduce){
|
|
55
77
|
.mc-demock-tile{animation:none!important}
|
|
56
78
|
.mc-demock-shimmer::after{animation:none!important;opacity:0}
|
|
@@ -69,11 +91,82 @@ function useMockupStyles() {
|
|
|
69
91
|
}, [])
|
|
70
92
|
}
|
|
71
93
|
|
|
72
|
-
const
|
|
94
|
+
const tileBase =
|
|
95
|
+
'rounded-lg bg-muted mc-demock-tile mc-demock-shimmer text-foreground overflow-hidden'
|
|
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
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A skeleton "stat card": label + big number. */
|
|
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
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A skeleton "list" tile: stacked rows. */
|
|
123
|
+
function ListGlyph() {
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex h-full flex-col justify-center gap-2 p-3">
|
|
126
|
+
<div className="h-2 w-full rounded-full bg-foreground/12" />
|
|
127
|
+
<div className="h-2 w-4/5 rounded-full bg-foreground/12" />
|
|
128
|
+
<div className="h-2 w-2/3 rounded-full bg-foreground/12" />
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A skeleton "progress bar" tile. */
|
|
134
|
+
function BarGlyph() {
|
|
135
|
+
return (
|
|
136
|
+
<div className="flex h-full items-center px-3">
|
|
137
|
+
<div className="h-2.5 w-full rounded-full bg-foreground/10">
|
|
138
|
+
<div className="h-full w-2/5 rounded-full bg-foreground/20" />
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The board: a 4-col x 4-row grid that fills the container. Each tile declares
|
|
145
|
+
// its cell (via inline gridColumn/gridRow) and a motion class. Swap pairs sit in
|
|
146
|
+
// adjacent cells and move toward each other, so the eye reads a reshuffle.
|
|
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
|
+
]
|
|
73
165
|
|
|
74
166
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
167
|
+
* Full-bleed animated skeleton of a dashboard whose tiles slide and swap places,
|
|
168
|
+
* as if the layout were assembling itself. Purely decorative — always
|
|
169
|
+
* `aria-hidden`, no text.
|
|
77
170
|
*/
|
|
78
171
|
export function DashboardEmptyMockup({ className }: { className?: string }) {
|
|
79
172
|
useMockupStyles()
|
|
@@ -81,41 +174,20 @@ export function DashboardEmptyMockup({ className }: { className?: string }) {
|
|
|
81
174
|
<div
|
|
82
175
|
aria-hidden="true"
|
|
83
176
|
data-testid="dashboard-empty-mockup"
|
|
84
|
-
className={cn('mc-demock pointer-events-none
|
|
177
|
+
className={cn('mc-demock pointer-events-none h-full w-full select-none', className)}
|
|
85
178
|
>
|
|
86
|
-
<div
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</div>
|
|
97
|
-
{/* Stat card */}
|
|
98
|
-
<div className={cn(tile, 'mc-demock-t2 flex flex-col justify-center gap-2 p-3')}>
|
|
99
|
-
<div className="h-2 w-1/2 rounded-full bg-foreground/15" />
|
|
100
|
-
<div className="h-4 w-2/3 rounded bg-foreground/15" />
|
|
101
|
-
</div>
|
|
102
|
-
{/* Stat card */}
|
|
103
|
-
<div className={cn(tile, 'mc-demock-t3 flex flex-col justify-center gap-2 p-3')}>
|
|
104
|
-
<div className="h-2 w-1/2 rounded-full bg-foreground/15" />
|
|
105
|
-
<div className="h-4 w-1/2 rounded bg-foreground/15" />
|
|
106
|
-
</div>
|
|
107
|
-
{/* List tile */}
|
|
108
|
-
<div className={cn(tile, 'mc-demock-t4 flex flex-col justify-center gap-2 p-3')}>
|
|
109
|
-
<div className="h-2 w-full rounded-full bg-foreground/12" />
|
|
110
|
-
<div className="h-2 w-4/5 rounded-full bg-foreground/12" />
|
|
111
|
-
<div className="h-2 w-2/3 rounded-full bg-foreground/12" />
|
|
112
|
-
</div>
|
|
113
|
-
{/* Progress bar tile */}
|
|
114
|
-
<div className={cn(tile, 'mc-demock-t5 col-span-2 flex items-center px-3')}>
|
|
115
|
-
<div className="h-2.5 w-full rounded-full bg-foreground/10">
|
|
116
|
-
<div className="h-full w-2/5 rounded-full bg-foreground/20" />
|
|
179
|
+
<div
|
|
180
|
+
className="grid h-full w-full gap-3"
|
|
181
|
+
style={{
|
|
182
|
+
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
|
|
183
|
+
gridTemplateRows: 'repeat(4, minmax(0, 1fr))',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{TILES.map((t, i) => (
|
|
187
|
+
<div key={i} style={t.area} className={cn(tileBase, t.motion)}>
|
|
188
|
+
{t.glyph}
|
|
117
189
|
</div>
|
|
118
|
-
|
|
190
|
+
))}
|
|
119
191
|
</div>
|
|
120
192
|
</div>
|
|
121
193
|
)
|
package/src/dashboard-grid.tsx
CHANGED
|
@@ -154,19 +154,14 @@ export function DashboardGrid({
|
|
|
154
154
|
<div
|
|
155
155
|
data-testid="dashboard-empty"
|
|
156
156
|
className={cn(
|
|
157
|
-
|
|
157
|
+
// Full-bleed: the mockup fills the whole dashboard area, not a
|
|
158
|
+
// centered illustration. No caption — the animation speaks for
|
|
159
|
+
// itself (and dodges shipping un-localized copy).
|
|
160
|
+
'min-h-[60vh] w-full rounded-xl border border-dashed border-border/60 p-4',
|
|
158
161
|
className,
|
|
159
162
|
)}
|
|
160
163
|
>
|
|
161
|
-
<DashboardEmptyMockup />
|
|
162
|
-
<div className="flex flex-col items-center">
|
|
163
|
-
<h3 className="text-base font-semibold text-foreground">
|
|
164
|
-
{tr(undefined, s.emptyTitle) || s.emptyTitle}
|
|
165
|
-
</h3>
|
|
166
|
-
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
|
167
|
-
{s.emptyDescription}
|
|
168
|
-
</p>
|
|
169
|
-
</div>
|
|
164
|
+
<DashboardEmptyMockup className="h-full min-h-[inherit]" />
|
|
170
165
|
</div>
|
|
171
166
|
)
|
|
172
167
|
}
|