feat(gui-v2): StateBlock 3-state wrapper (exhaustive Vitest, no @visual per constraint #5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 11:00:20 +02:00
parent b0d5e9611f
commit 284fdcc437
3 changed files with 167 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
/**
* StateBlock — the CLAUDE.md mandatory three-state wrapper (loading /
* error / empty) + success passthrough. Built fresh (no crewli-starter
* source). Per Plan 3 constraint #5 this component intentionally has
* NO @visual baseline yet (self-baseline is tautological); correctness
* is locked by the exhaustive Vitest spec. Visual baseline is a
* Plans 45 follow-up after first real page usage.
*/
import Skeleton from 'primevue/skeleton'
import Message from 'primevue/message'
import Button from 'primevue/button'
import Card from 'primevue/card'
defineProps<{
state: 'loading' | 'error' | 'empty' | 'success'
errorMessage?: string
emptyMessage?: string
actionLabel?: string
retryLabel?: string
}>()
const emit = defineEmits<{ retry: []; action: [] }>()
</script>
<template>
<div
v-if="state === 'loading'"
class="flex flex-col gap-3"
data-state="loading"
>
<Skeleton height="2rem" />
<Skeleton
height="2rem"
width="80%"
/>
<Skeleton
height="2rem"
width="60%"
/>
</div>
<Message
v-else-if="state === 'error'"
severity="error"
:closable="false"
data-state="error"
>
<div class="flex items-center justify-between gap-4 w-full">
<span>{{ errorMessage ?? 'Er ging iets mis.' }}</span>
<Button
:label="retryLabel ?? 'Opnieuw proberen'"
size="small"
@click="emit('retry')"
/>
</div>
</Message>
<Card
v-else-if="state === 'empty'"
data-state="empty"
:pt="{ root: 'border border-dashed border-[var(--p-content-border-color)] shadow-none', content: 'flex flex-col items-center gap-3 py-10 text-center' }"
>
<template #content>
<p class="m-0 text-[var(--p-text-muted-color)]">
{{ emptyMessage ?? 'Nog niets om te tonen.' }}
</p>
<Button
v-if="actionLabel"
:label="actionLabel"
@click="emit('action')"
/>
</template>
</Card>
<slot v-else />
</template>