feat(gui-v2): StatCard (PrimeVue Card KPI tile, replaces AppKpiCard) + story

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 10:41:34 +02:00
parent 9d1fd16f0f
commit 12cff8c03a
3 changed files with 115 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import StatCard from '@/components-v2/shared/StatCard.vue'
const meta: Meta<typeof StatCard> = {
title: 'Shared/StatCard',
component: StatCard,
tags: ['autodocs'],
argTypes: {
icon: { control: 'text' },
label: { control: 'text' },
value: { control: 'text' },
trend: { control: 'text' },
trendDir: { control: 'inline-radio', options: ['', 'up', 'down'] },
},
}
export default meta
type Story = StoryObj<typeof StatCard>
export const Plain: Story = { args: { icon: 'tabler-users', label: 'Vrijwilligers', value: 128 } }
export const TrendUp: Story = { args: { icon: 'tabler-calendar-event', label: 'Diensten', value: 42, trend: '+12% vs vorige week', trendDir: 'up' } }
export const TrendDown: Story = { args: { icon: 'tabler-alert-triangle', label: 'No-shows', value: 3, trend: '-2 vs vorige week', trendDir: 'down' } }

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
/**
* StatCard — KPI tile. crewli-starter port (replaces v1 AppKpiCard).
* CSS translation (main.css .stat-card 10191039 → Tailwind + Aura tokens):
* .stat-card .lbl → text-[11.5px] font-semibold uppercase tracking-[0.06em] text-[var(--p-text-muted-color)]
* .stat-card .val → text-[28px] font-bold tracking-[-0.01em] tabular-nums
* .trend.up/.down → text-[var(--p-green-600)] / text-[var(--p-red-600)]
*/
import Card from 'primevue/card'
import Icon from '@/components/Icon.vue'
defineProps<{
icon: string
label: string
value: string | number
trend?: string
trendDir?: 'up' | 'down'
}>()
</script>
<template>
<Card
:pt="{
root: 'border border-[var(--p-content-border-color)] rounded-[var(--p-border-radius-lg)] shadow-none',
body: 'p-[18px]',
content: 'flex flex-col gap-1.5',
}"
>
<template #content>
<div class="flex items-center gap-2 text-[11.5px] font-semibold uppercase tracking-[0.06em] text-[var(--p-text-muted-color)]">
<Icon
:name="icon"
:size="16"
class="text-[var(--p-primary-color)]"
/>
{{ label }}
</div>
<div class="text-[28px] font-bold tracking-[-0.01em] tabular-nums">
{{ value }}
</div>
<div
v-if="trend"
:data-trend="trendDir"
class="flex items-center gap-1 text-xs text-[var(--p-text-muted-color)]"
:class="{
'text-[var(--p-green-600)]!': trendDir === 'up',
'text-[var(--p-red-600)]!': trendDir === 'down',
}"
>
<Icon
v-if="trendDir === 'up'"
name="tabler-trending-up"
:size="14"
/>
<Icon
v-if="trendDir === 'down'"
name="tabler-trending-down"
:size="14"
/>
{{ trend }}
</div>
</template>
</Card>
</template>

View File

@@ -0,0 +1,29 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
import StatCard from '@/components-v2/shared/StatCard.vue'
const CardStub = defineComponent({ name: 'CardStub', template: '<div class="card-stub"><slot name="content" /></div>' })
const IconStub = defineComponent({ name: 'Icon', props: ['name', 'size'], template: '<i class="icon-stub" :data-icon="name" />' })
const mountCard = (props: { icon: string; label: string; value: string | number; trend?: string; trendDir?: 'up' | 'down' }) =>
mount(StatCard, { props, global: { stubs: { Card: CardStub, Icon: IconStub } } })
describe('StatCard', () => {
it('renders label, value and the leading icon', () => {
const w = mountCard({ icon: 'tabler-users', label: 'Vrijwilligers', value: 128 })
expect(w.text()).toContain('Vrijwilligers')
expect(w.text()).toContain('128')
expect(w.get('.icon-stub').attributes('data-icon')).toBe('tabler-users')
})
it('shows the trend row with direction class only when trend is set', () => {
expect(mountCard({ icon: 'tabler-users', label: 'X', value: 1 }).find('[data-trend]').exists()).toBe(false)
const up = mountCard({ icon: 'tabler-users', label: 'X', value: 1, trend: '+12%', trendDir: 'up' })
expect(up.get('[data-trend]').attributes('data-trend')).toBe('up')
expect(up.text()).toContain('+12%')
})
})