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:
22
apps/app/src/components-v2/shared/StatCard.stories.ts
Normal file
22
apps/app/src/components-v2/shared/StatCard.stories.ts
Normal 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' } }
|
||||||
64
apps/app/src/components-v2/shared/StatCard.vue
Normal file
64
apps/app/src/components-v2/shared/StatCard.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* StatCard — KPI tile. crewli-starter port (replaces v1 AppKpiCard).
|
||||||
|
* CSS translation (main.css .stat-card 1019–1039 → 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>
|
||||||
29
apps/app/src/components-v2/shared/__tests__/StatCard.spec.ts
Normal file
29
apps/app/src/components-v2/shared/__tests__/StatCard.spec.ts
Normal 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%')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user