From 2655e573e20dc4df052a9c86e12c6dfc59baf950 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 9 Aug 2026 12:50:18 +0200 Subject: [PATCH 1/2] fix(stat-card): only look interactive when it actually is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StatCard applied `whileHover={{ scale: 1.02 }}` unconditionally while having no click handler in its API at all — so every stat on every dashboard lifted under the cursor and did nothing. A scale-on-hover is the plainest "you can click this" signal there is; a card that lifts without acting is a promise the UI does not keep. Reported from the Punktfunk console's Overview tab, where four of them sit in a row. Adds an optional `onClick`. Given one, the card lifts, presses, shows a pointer, takes focus and responds to Enter/Space with role="button". Without one it is inert and looks inert. Existing call sites pass no handler, so they simply stop pretending. The new stat-card/StatCard → "Interactive vs static" story puts both side by side, which is the cheapest way to see the regression return. --- src/stat-card/index.tsx | 34 ++++++++++++++++++++++++++--- src/stat-card/stat-card.stories.tsx | 22 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/stat-card/index.tsx b/src/stat-card/index.tsx index e9e2139..ce5bdfc 100644 --- a/src/stat-card/index.tsx +++ b/src/stat-card/index.tsx @@ -43,11 +43,21 @@ export type StatCardProps = { icon?: LucideIcon; tone?: StatCardTone; className?: string; + /** + * Makes the card a button — and only then does it lift on hover. + * + * A scale-on-hover is the web's plainest "you can click this" signal, so a card that lifts + * without doing anything is a promise the UI does not keep. This used to be unconditional + * while the component had no click handler at all, which meant every stat on every dashboard + * invited a click that did nothing. + */ + onClick?: () => void; }; /** - * A single metric on translucent console chrome. Enters with the card - * animation token (staggered by the surrounding Section) and lifts on hover. + * A single metric on translucent console chrome. Enters with the card animation token (staggered + * by the surrounding Section). Interactive only when given an `onClick`, and it looks interactive + * only when it is. */ export const StatCard: FC = ({ label, @@ -56,15 +66,33 @@ export const StatCard: FC = ({ icon: Icon, tone = "default", className, + onClick, }) => { const token = useAnimation("card"); + const interactive = onClick !== undefined; return ( { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + } + : undefined + } className={cn( "flex flex-col gap-1 rounded-card bg-neutral/40 p-padding-card ring-1 ring-accent/40", + interactive && + "cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring", className, )} > diff --git a/src/stat-card/stat-card.stories.tsx b/src/stat-card/stat-card.stories.tsx index e00fb36..8230b89 100644 --- a/src/stat-card/stat-card.stories.tsx +++ b/src/stat-card/stat-card.stories.tsx @@ -29,6 +29,28 @@ export const Default: Story = { }, }; +/** + * Two cards side by side: the left one is inert, the right one has an `onClick`. + * + * Only the interactive one lifts, shows a pointer and takes focus. Hover both — if the static + * card moves, the component is promising a click it cannot deliver, which is the regression this + * story exists to catch. + */ +export const InteractiveVsStatic: Story = { + render: () => ( +
+ + {}} + /> +
+ ), +}; + export const Grid: Story = { render: () => (
-- 2.54.0 From 05c636023a013466e0843a839e6ac0d99ae4172a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 9 Aug 2026 13:02:32 +0200 Subject: [PATCH 2/2] chore: refresh bun.lock with the peer deps package.json already declares The lockfile predated @unom/ui, class-variance-authority, lucide-react and motion being listed as peers; an install adds them back. No dependency change. --- bun.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index e58d5a6..464af56 100644 --- a/bun.lock +++ b/bun.lock @@ -29,6 +29,10 @@ }, "peerDependencies": { "@unom/style": "^0.4.4", + "@unom/ui": "^0.9.1", + "class-variance-authority": "^0.7.1", + "lucide-react": "^1.17.0", + "motion": "^12.40.0", "react": "^19.0.0", }, }, -- 2.54.0