Skip to content

Commit 685cf8f

Browse files
committed
feat: add interactive dashboard motion
1 parent db8291b commit 685cf8f

5 files changed

Lines changed: 326 additions & 56 deletions

File tree

docs/design-system.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Owned shadcn-compatible primitives live under `src/components/ui/`. Domain rende
3131

3232
The synthetic **Component lab** is the executable catalog: validated JSON appears on the left and the production renderer appears on the right. A new block is not complete until it appears there through a public synthetic fixture, with schema, type, renderer, tests, privacy review, and documentation.
3333

34+
Charts are explorable rather than decorative. Line points and bars expose the same exact-value callout on pointer hover and keyboard focus. Donut legends act as controls that highlight the matching segment and update its center value. Supporting blocks use quiet hover feedback and brief entrance motion; interaction must never be required to recover a fact that is absent from the accessible table, list, or text equivalent.
35+
3436
Use semantic tokens such as `background`, `card`, `muted`, `primary`, `warning`, and chart tokens. Do not hardcode provider or source colors inside components.
3537

3638
## Accessibility

src/components/BlockRenderer.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ function Panel({
6464
layout: Layout
6565
}) {
6666
return (
67-
<Card className={cn("min-w-0 overflow-hidden", layoutSpan(block, layout, emphasized), emphasized && "border-primary/35", className)}>
67+
<Card
68+
className={cn(
69+
"zaati-block min-w-0 overflow-hidden",
70+
layoutSpan(block, layout, emphasized),
71+
emphasized && "border-primary/35",
72+
className,
73+
)}
74+
>
6875
<CardHeader>
6976
<CardTitle>{block.title}</CardTitle>
7077
{"description" in block && block.description ? <CardDescription>{block.description}</CardDescription> : null}
@@ -98,13 +105,13 @@ export function BlockRenderer({
98105
)}
99106
>
100107
{block.metrics.map((metric) => (
101-
<div className="min-w-0 bg-card px-4 py-3" key={metric.label}>
108+
<div className="group min-w-0 bg-card px-4 py-3 transition-colors hover:bg-accent/45" key={metric.label}>
102109
<div className="mb-2 flex items-center gap-2 text-xs font-medium text-muted-foreground">
103110
<span aria-hidden="true" className={cn("size-1.5 rounded-full", toneDot[metric.tone || "neutral"])} />
104111
{metric.label}
105112
</div>
106113
<div className="flex items-end gap-2">
107-
<span className="truncate text-2xl font-semibold tracking-tight">
114+
<span className="truncate text-2xl font-semibold tracking-tight transition-transform duration-200 group-hover:translate-x-0.5">
108115
{formatValue(metric.value, metric.format, instance)}
109116
{metric.unit ? <span className="ml-1 text-sm font-medium text-muted-foreground">{metric.unit}</span> : null}
110117
</span>
@@ -149,7 +156,7 @@ export function BlockRenderer({
149156
)
150157
return item.href ? (
151158
<a
152-
className="flex gap-3 py-3 first:pt-0 last:pb-0 hover:text-primary"
159+
className="group flex gap-3 rounded-lg px-2 py-3 transition-colors first:pt-0 last:pb-0 hover:bg-muted/55 hover:text-primary focus-visible:bg-muted/55"
153160
href={item.href}
154161
key={item.id}
155162
rel="noreferrer"
@@ -158,7 +165,10 @@ export function BlockRenderer({
158165
{content}
159166
</a>
160167
) : (
161-
<div className="flex gap-3 py-3 first:pt-0 last:pb-0" key={item.id}>
168+
<div
169+
className="group flex gap-3 rounded-lg px-2 py-3 transition-colors first:pt-0 last:pb-0 hover:bg-muted/45"
170+
key={item.id}
171+
>
162172
{content}
163173
</div>
164174
)
@@ -205,7 +215,10 @@ export function BlockRenderer({
205215
{block.events.length ? (
206216
<div className="space-y-1">
207217
{block.events.map((event) => (
208-
<div className="group flex gap-3 rounded-lg px-2 py-2.5 hover:bg-muted/60" key={event.id}>
218+
<div
219+
className="group flex gap-3 rounded-lg px-2 py-2.5 transition-[background-color,transform] duration-200 hover:-translate-y-0.5 hover:bg-muted/60"
220+
key={event.id}
221+
>
209222
<div className="w-16 shrink-0 pt-0.5 text-xs font-medium text-muted-foreground">{time(event.start)}</div>
210223
<span className={cn("mt-1.5 h-8 w-0.5 rounded-full", toneDot[event.tone || "neutral"])} />
211224
<div className="min-w-0">
@@ -249,7 +262,7 @@ export function BlockRenderer({
249262
</thead>
250263
<tbody className="divide-y divide-border">
251264
{block.rows.map((row, index) => (
252-
<tr className="hover:bg-muted/40" key={index}>
265+
<tr className="transition-colors hover:bg-muted/55" key={index}>
253266
{block.columns.map((column) => (
254267
<td className="max-w-64 px-3 py-3 align-top" key={column.key}>
255268
{formatValue(row[column.key] ?? null, column.format, instance)}
@@ -274,7 +287,7 @@ export function BlockRenderer({
274287
{block.items.map((item) => {
275288
const percent = (item.value / item.max) * 100
276289
return (
277-
<div key={item.label}>
290+
<div className="group rounded-lg px-1 py-1 transition-colors hover:bg-muted/45" key={item.label}>
278291
<div className="mb-2 flex items-center justify-between gap-4">
279292
<span className="text-sm font-medium">{item.label}</span>
280293
<span className="text-xs font-medium text-muted-foreground">{item.value_label || `${Math.round(percent)}%`}</span>
@@ -302,7 +315,7 @@ export function BlockRenderer({
302315
return (
303316
<div
304317
className={cn(
305-
"rounded-xl border p-5",
318+
"zaati-block rounded-xl border p-5 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-0.5 hover:shadow-sm",
306319
layoutSpan(block, layout, emphasized),
307320
block.tone === "warning" && "border-warning/30 bg-warning/10",
308321
block.tone === "danger" && "border-destructive/30 bg-destructive/10",
@@ -340,9 +353,17 @@ export function BlockRenderer({
340353
{block.items.length ? (
341354
<div>
342355
{block.items.map((item, index) => (
343-
<div className="relative flex gap-4 pb-5 last:pb-0" key={`${item.label}-${item.title}`}>
356+
<div
357+
className="group relative flex gap-4 rounded-lg pb-5 transition-colors hover:bg-muted/35 last:pb-0"
358+
key={`${item.label}-${item.title}`}
359+
>
344360
<div className="flex w-3 shrink-0 flex-col items-center">
345-
<span className={cn("mt-1.5 size-2.5 rounded-full ring-4 ring-background", toneDot[item.tone || "neutral"])} />
361+
<span
362+
className={cn(
363+
"mt-1.5 size-2.5 rounded-full ring-4 ring-background transition-transform duration-200 group-hover:scale-125",
364+
toneDot[item.tone || "neutral"],
365+
)}
366+
/>
346367
{index < block.items.length - 1 ? <span className="mt-1 h-full w-px bg-border" /> : null}
347368
</div>
348369
<div className="min-w-0">

0 commit comments

Comments
 (0)