feat(gui-v2): StatusTag (PrimeVue Tag + statusSeverity map) + story

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

View File

@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import StatusTag from '@/components-v2/shared/StatusTag.vue'
const meta: Meta<typeof StatusTag> = {
title: 'Shared/StatusTag',
component: StatusTag,
tags: ['autodocs'],
argTypes: {
status: { control: 'text' },
label: { control: 'text' },
dot: { control: 'boolean' },
},
}
export default meta
type Story = StoryObj<typeof StatusTag>
export const Success: Story = { args: { status: 'approved' } }
export const Warn: Story = { args: { status: 'pending_approval' } }
export const Info: Story = { args: { status: 'invited' } }
export const Secondary: Story = { args: { status: 'draft' } }
export const Danger: Story = { args: { status: 'no_show' } }
export const WithDot: Story = { args: { status: 'confirmed', dot: true } }
export const CustomLabel: Story = { args: { status: 'rejected', label: 'Afgewezen' } }

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
/**
* StatusTag — PrimeVue <Tag> whose severity ALWAYS resolves through
* statusSeverity.ts (spec §8). Never inline a severity here.
*
* crewli-starter port: .tag/.dot visual is delegated to PrimeVue Tag
* (themes in both modes via Aura). The optional leading dot reproduces
* crewli-starter's `<span class="dot">` via :pt.root (a ::before-style
* inline dot) rather than scoped CSS — no bespoke spacing needed.
*/
import { computed } from 'vue'
import Tag from 'primevue/tag'
import { statusSeverity } from '@/components-v2/shared/statusSeverity'
const props = defineProps<{
status: string
label?: string
dot?: boolean
}>()
const severity = computed(() => statusSeverity(props.status))
const text = computed(() =>
props.label ?? props.status.replace(/_/g, ' '),
)
const pt = computed(() =>
props.dot
? {
root: {
class: 'before:content-[""] before:inline-block before:w-2 before:h-2 before:rounded-full before:bg-current before:mr-1.5 before:align-middle',
},
}
: {},
)
</script>
<template>
<Tag
:value="text"
:severity="severity"
:pt="pt"
/>
</template>

View File

@@ -0,0 +1,35 @@
// apps/app/src/components-v2/shared/__tests__/StatusTag.spec.ts
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
import StatusTag from '@/components-v2/shared/StatusTag.vue'
const TagStub = defineComponent({
name: 'TagStub',
props: { value: { type: String, default: '' }, severity: { type: String, default: '' }, pt: { type: Object, default: () => ({}) } },
template: '<span class="tag-stub" :data-severity="severity">{{ value }}<slot /></span>',
})
const mountTag = (props: { status: string; label?: string; dot?: boolean }) =>
mount(StatusTag, { props, global: { stubs: { Tag: TagStub } } })
describe('StatusTag', () => {
it('resolves severity from statusSeverity for the status prop', () => {
expect(mountTag({ status: 'approved' }).get('.tag-stub').attributes('data-severity')).toBe('success')
expect(mountTag({ status: 'no_show' }).get('.tag-stub').attributes('data-severity')).toBe('danger')
expect(mountTag({ status: 'deposit_paid' }).get('.tag-stub').attributes('data-severity')).toBe('info')
})
it('renders the label prop, defaulting to the humanised status', () => {
expect(mountTag({ status: 'pending_approval' }).text()).toContain('pending approval')
expect(mountTag({ status: 'approved', label: 'Goedgekeurd' }).text()).toContain('Goedgekeurd')
})
it('adds the dot passthrough only when dot=true', () => {
expect(mountTag({ status: 'draft' }).get('.tag-stub').attributes('data-severity')).toBe('secondary')
const pt = mountTag({ status: 'draft', dot: true }).getComponent(TagStub).props('pt') as Record<string, unknown>
expect(pt).toHaveProperty('root')
})
})