@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 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
- * Animated skeleton of a dashboard (chart + stats + bar + list) whose tiles
4
- * slowly drift and settle. Purely decorative — always `aria-hidden`.
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":"AAqBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoD9B;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBA4CzE"}
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, a couple of stat cards, a
6
- // bar, a list) that slowly drift and swap places, like puzzle pieces settling
7
- // into a layout. The metaphor is "your board is getting ready", not "empty".
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 deliberately simple so it ships from a package):
10
- // - Pure CSS keyframes, no dependencies. One slow ~11s loop, subtle motion
11
- // (small translate + scale, never opacity strobing).
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
- // - The whole mock is decorative aria-hidden; the copy below carries the
15
- // accessible meaning.
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
- // The keyframes ship inline in a <style> tag rather than as Tailwind utilities:
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:11s}
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
- .mc-demock-t1{animation-name:mc-demock-drift-a}
32
- .mc-demock-t2{animation-name:mc-demock-drift-b;animation-delay:-1.4s}
33
- .mc-demock-t3{animation-name:mc-demock-drift-c;animation-delay:-3.1s}
34
- .mc-demock-t4{animation-name:mc-demock-drift-d;animation-delay:-2.2s}
35
- .mc-demock-t5{animation-name:mc-demock-drift-e;animation-delay:-4.6s}
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 30%,currentColor 50%,transparent 70%);
39
- opacity:.05;transform:translateX(-100%);
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
- @keyframes mc-demock-drift-a{0%,100%{transform:translate3d(0,0,0) scale(1)}
42
- 50%{transform:translate3d(3%,4%,0) scale(1.015)}}
43
- @keyframes mc-demock-drift-b{0%,100%{transform:translate3d(0,0,0) scale(1)}
44
- 50%{transform:translate3d(-4%,-3%,0) scale(.985)}}
45
- @keyframes mc-demock-drift-c{0%,100%{transform:translate3d(0,0,0) scale(1)}
46
- 50%{transform:translate3d(5%,-2%,0) scale(1.02)}}
47
- @keyframes mc-demock-drift-d{0%,100%{transform:translate3d(0,0,0) scale(1)}
48
- 50%{transform:translate3d(-3%,3%,0) scale(.99)}}
49
- @keyframes mc-demock-drift-e{0%,100%{transform:translate3d(0,0,0) scale(1)}
50
- 50%{transform:translate3d(2%,-4%,0) scale(1.01)}}
51
- @keyframes mc-demock-sheen{0%,100%{transform:translateX(-120%)}55%,70%{transform:translateX(120%)}}
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 tile = 'rounded-lg bg-muted mc-demock-tile mc-demock-shimmer text-foreground';
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
- * Animated skeleton of a dashboard (chart + stats + bar + list) whose tiles
73
- * slowly drift and settle. Purely decorative — always `aria-hidden`.
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 w-full max-w-md select-none', className), children: _jsxs("div", { className: "grid grid-cols-3 grid-rows-[repeat(3,minmax(0,1fr))] gap-3 [aspect-ratio:16/11]", children: [_jsx("div", { className: cn(tile, 'mc-demock-t1 col-span-2 row-span-2 flex flex-col justify-end gap-2 p-3'), children: _jsxs("div", { className: "flex items-end gap-1.5", children: [_jsx("div", { className: "h-6 w-full rounded-sm bg-foreground/10" }), _jsx("div", { className: "h-10 w-full rounded-sm bg-foreground/10" }), _jsx("div", { className: "h-7 w-full rounded-sm bg-foreground/10" }), _jsx("div", { className: "h-12 w-full rounded-sm bg-foreground/10" }), _jsx("div", { className: "h-9 w-full rounded-sm bg-foreground/10" })] }) }), _jsxs("div", { className: cn(tile, 'mc-demock-t2 flex 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" })] }), _jsxs("div", { className: cn(tile, 'mc-demock-t3 flex 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-1/2 rounded bg-foreground/15" })] }), _jsxs("div", { className: cn(tile, 'mc-demock-t4 flex 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" })] }), _jsx("div", { className: cn(tile, 'mc-demock-t5 col-span-2 flex 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" }) }) })] }) }));
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,qBAiJpB"}
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"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 (_jsxs("div", { "data-testid": "dashboard-empty", className: cn('flex min-h-[40vh] flex-col items-center justify-center gap-6 rounded-xl border border-dashed border-border/60 p-10 text-center', className), children: [_jsx(DashboardEmptyMockup, {}), _jsxs("div", { className: "flex flex-col items-center", children: [_jsx("h3", { className: "text-base font-semibold text-foreground", children: tr(undefined, s.emptyTitle) || s.emptyTitle }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: s.emptyDescription })] })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "27.3.0",
3
+ "version": "27.3.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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, a couple of stat cards, a
5
- // bar, a list) that slowly drift and swap places, like puzzle pieces settling
6
- // into a layout. The metaphor is "your board is getting ready", not "empty".
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 deliberately simple so it ships from a package):
9
- // - Pure CSS keyframes, no dependencies. One slow ~11s loop, subtle motion
10
- // (small translate + scale, never opacity strobing).
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
- // - The whole mock is decorative aria-hidden; the copy below carries the
14
- // accessible meaning.
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
- // The keyframes ship inline in a <style> tag rather than as Tailwind utilities:
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:11s}
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
- .mc-demock-t1{animation-name:mc-demock-drift-a}
34
- .mc-demock-t2{animation-name:mc-demock-drift-b;animation-delay:-1.4s}
35
- .mc-demock-t3{animation-name:mc-demock-drift-c;animation-delay:-3.1s}
36
- .mc-demock-t4{animation-name:mc-demock-drift-d;animation-delay:-2.2s}
37
- .mc-demock-t5{animation-name:mc-demock-drift-e;animation-delay:-4.6s}
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 30%,currentColor 50%,transparent 70%);
41
- opacity:.05;transform:translateX(-100%);
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
- @keyframes mc-demock-drift-a{0%,100%{transform:translate3d(0,0,0) scale(1)}
44
- 50%{transform:translate3d(3%,4%,0) scale(1.015)}}
45
- @keyframes mc-demock-drift-b{0%,100%{transform:translate3d(0,0,0) scale(1)}
46
- 50%{transform:translate3d(-4%,-3%,0) scale(.985)}}
47
- @keyframes mc-demock-drift-c{0%,100%{transform:translate3d(0,0,0) scale(1)}
48
- 50%{transform:translate3d(5%,-2%,0) scale(1.02)}}
49
- @keyframes mc-demock-drift-d{0%,100%{transform:translate3d(0,0,0) scale(1)}
50
- 50%{transform:translate3d(-3%,3%,0) scale(.99)}}
51
- @keyframes mc-demock-drift-e{0%,100%{transform:translate3d(0,0,0) scale(1)}
52
- 50%{transform:translate3d(2%,-4%,0) scale(1.01)}}
53
- @keyframes mc-demock-sheen{0%,100%{transform:translateX(-120%)}55%,70%{transform:translateX(120%)}}
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 tile = 'rounded-lg bg-muted mc-demock-tile mc-demock-shimmer text-foreground'
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
- * Animated skeleton of a dashboard (chart + stats + bar + list) whose tiles
76
- * slowly drift and settle. Purely decorative — always `aria-hidden`.
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 w-full max-w-md select-none', className)}
177
+ className={cn('mc-demock pointer-events-none h-full w-full select-none', className)}
85
178
  >
86
- <div className="grid grid-cols-3 grid-rows-[repeat(3,minmax(0,1fr))] gap-3 [aspect-ratio:16/11]">
87
- {/* Big chart tile */}
88
- <div className={cn(tile, 'mc-demock-t1 col-span-2 row-span-2 flex flex-col justify-end gap-2 p-3')}>
89
- <div className="flex items-end gap-1.5">
90
- <div className="h-6 w-full rounded-sm bg-foreground/10" />
91
- <div className="h-10 w-full rounded-sm bg-foreground/10" />
92
- <div className="h-7 w-full rounded-sm bg-foreground/10" />
93
- <div className="h-12 w-full rounded-sm bg-foreground/10" />
94
- <div className="h-9 w-full rounded-sm bg-foreground/10" />
95
- </div>
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
- </div>
190
+ ))}
119
191
  </div>
120
192
  </div>
121
193
  )
@@ -154,19 +154,14 @@ export function DashboardGrid({
154
154
  <div
155
155
  data-testid="dashboard-empty"
156
156
  className={cn(
157
- 'flex min-h-[40vh] flex-col items-center justify-center gap-6 rounded-xl border border-dashed border-border/60 p-10 text-center',
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
  }