feat(gui-v2): StatusTag (PrimeVue Tag + statusSeverity map) + story
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
24
apps/app/src/components-v2/shared/StatusTag.stories.ts
Normal file
24
apps/app/src/components-v2/shared/StatusTag.stories.ts
Normal 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' } }
|
||||
44
apps/app/src/components-v2/shared/StatusTag.vue
Normal file
44
apps/app/src/components-v2/shared/StatusTag.vue
Normal 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>
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user