diff --git a/resources/js/components/ui/avatar/Avatar.vue b/resources/js/components/ui/avatar/Avatar.vue
index bb7e669..9343bb4 100644
--- a/resources/js/components/ui/avatar/Avatar.vue
+++ b/resources/js/components/ui/avatar/Avatar.vue
@@ -1,18 +1,35 @@
-
-
-
+
+
+
diff --git a/resources/js/components/ui/avatar/AvatarFallback.vue b/resources/js/components/ui/avatar/AvatarFallback.vue
index 16b588a..4683221 100644
--- a/resources/js/components/ui/avatar/AvatarFallback.vue
+++ b/resources/js/components/ui/avatar/AvatarFallback.vue
@@ -1,21 +1,28 @@
-
-
-
+
+
+
diff --git a/resources/js/components/ui/avatar/UserAvatar.vue b/resources/js/components/ui/avatar/UserAvatar.vue
index 3412c64..a2b89f7 100644
--- a/resources/js/components/ui/avatar/UserAvatar.vue
+++ b/resources/js/components/ui/avatar/UserAvatar.vue
@@ -1,41 +1,39 @@
-
+
-
- {{ getInitials(auth.user?.name) }}
+
+ {{ initials }}
diff --git a/resources/js/components/ui/avatar/index.ts b/resources/js/components/ui/avatar/index.ts
index cf0e003..940c101 100644
--- a/resources/js/components/ui/avatar/index.ts
+++ b/resources/js/components/ui/avatar/index.ts
@@ -1,3 +1,31 @@
-export { default as Avatar } from "./Avatar.vue"
-export { default as AvatarFallback } from "./AvatarFallback.vue"
-export { default as AvatarImage } from "./AvatarImage.vue"
+import type { VariantProps } from "class-variance-authority";
+import { cva } from "class-variance-authority";
+
+export { default as Avatar } from "./Avatar.vue";
+export { default as AvatarFallback } from "./AvatarFallback.vue";
+export { default as AvatarImage } from "./AvatarImage.vue";
+export { default as UserAvatar } from "./UserAvatar.vue";
+
+export const avatarVariants = cva(
+ "border-border bg-muted relative flex shrink-0 overflow-hidden border",
+ {
+ variants: {
+ size: {
+ sm: "size-6 text-label",
+ default: "size-8 text-caption",
+ lg: "size-10 text-body",
+ xl: "size-16 text-title",
+ },
+ shape: {
+ circle: "rounded-full",
+ rounded: "rounded-lg",
+ square: "rounded-sm",
+ },
+ },
+ defaultVariants: {
+ size: "default",
+ shape: "circle",
+ },
+ },
+);
+export type AvatarVariants = VariantProps;
diff --git a/resources/js/components/ui/badge/Badge.vue b/resources/js/components/ui/badge/Badge.vue
index d894dfe..0a3c879 100644
--- a/resources/js/components/ui/badge/Badge.vue
+++ b/resources/js/components/ui/badge/Badge.vue
@@ -1,26 +1,28 @@
-
-
-
+
+
+
diff --git a/resources/js/components/ui/badge/index.ts b/resources/js/components/ui/badge/index.ts
index bbc0dfa..d1b532e 100644
--- a/resources/js/components/ui/badge/index.ts
+++ b/resources/js/components/ui/badge/index.ts
@@ -1,26 +1,31 @@
-import type { VariantProps } from "class-variance-authority"
-import { cva } from "class-variance-authority"
+import type { VariantProps } from "class-variance-authority";
+import { cva } from "class-variance-authority";
-export { default as Badge } from "./Badge.vue"
+export { default as Badge } from "./Badge.vue";
export const badgeVariants = cva(
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
- destructive:
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
- },
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2.5 py-1 text-label whitespace-nowrap transition-[background-color,border-color,color,box-shadow] outline-none [&>svg]:pointer-events-none [&>svg]:size-3 [&>svg]:shrink-0 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/35",
+ outline:
+ "border-border bg-card text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ success:
+ "border-transparent bg-success text-success-foreground [a&]:hover:brightness-95",
+ warning:
+ "border-transparent bg-warning text-warning-foreground [a&]:hover:brightness-95",
+ info: "border-transparent bg-info text-info-foreground [a&]:hover:brightness-95",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
},
- defaultVariants: {
- variant: "default",
- },
- },
-)
-export type BadgeVariants = VariantProps
+);
+export type BadgeVariants = VariantProps;
diff --git a/resources/js/components/ui/button/Button.vue b/resources/js/components/ui/button/Button.vue
index 374320b..ed0d99c 100644
--- a/resources/js/components/ui/button/Button.vue
+++ b/resources/js/components/ui/button/Button.vue
@@ -1,29 +1,47 @@
-
-
-
+
+
+
+
diff --git a/resources/js/components/ui/button/index.ts b/resources/js/components/ui/button/index.ts
index 7cba0b4..4f8a2d2 100644
--- a/resources/js/components/ui/button/index.ts
+++ b/resources/js/components/ui/button/index.ts
@@ -1,38 +1,37 @@
-import type { VariantProps } from "class-variance-authority"
-import { cva } from "class-variance-authority"
+import type { VariantProps } from "class-variance-authority";
+import { cva } from "class-variance-authority";
-export { default as Button } from "./Button.vue"
+export { default as Button } from "./Button.vue";
export const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
- outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- "default": "h-9 px-4 py-2 has-[>svg]:px-3",
- "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- "lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
- "icon": "size-9",
- "icon-sm": "size-8",
- "icon-lg": "size-10",
- },
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-body font-semibold transition-[background-color,border-color,color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[loading]:cursor-wait data-[loading]:opacity-80 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-2 focus-visible:ring-offset-background [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default:
+ "border border-transparent bg-brand text-brand-foreground shadow-xs hover:brightness-95",
+ destructive:
+ "border border-transparent bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/35",
+ outline:
+ "border border-input bg-card text-foreground shadow-xs hover:border-primary/45 hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "border border-transparent hover:bg-accent hover:text-accent-foreground",
+ link: "min-h-0 border-0 px-0 text-primary underline-offset-4 shadow-none hover:underline",
+ },
+ size: {
+ default: "min-h-control px-4 py-2 has-[>svg]:px-3",
+ sm: "min-h-9 gap-1.5 px-3 py-1.5 has-[>svg]:px-2.5",
+ lg: "min-h-12 px-6 py-3 has-[>svg]:px-4",
+ icon: "size-control",
+ "icon-sm": "size-9",
+ "icon-lg": "size-12",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
},
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-)
-export type ButtonVariants = VariantProps
+);
+export type ButtonVariants = VariantProps;
diff --git a/resources/js/components/ui/checkbox/Checkbox.vue b/resources/js/components/ui/checkbox/Checkbox.vue
index 6604cbd..fabdc5a 100644
--- a/resources/js/components/ui/checkbox/Checkbox.vue
+++ b/resources/js/components/ui/checkbox/Checkbox.vue
@@ -1,35 +1,48 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/resources/js/components/ui/input/Input.vue b/resources/js/components/ui/input/Input.vue
index e6486c7..60dfaad 100644
--- a/resources/js/components/ui/input/Input.vue
+++ b/resources/js/components/ui/input/Input.vue
@@ -1,33 +1,38 @@
-
+
diff --git a/resources/js/components/ui/select/SelectContent.vue b/resources/js/components/ui/select/SelectContent.vue
index adf04ec..dd16dbf 100644
--- a/resources/js/components/ui/select/SelectContent.vue
+++ b/resources/js/components/ui/select/SelectContent.vue
@@ -1,51 +1,60 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/components/ui/select/SelectItem.vue b/resources/js/components/ui/select/SelectItem.vue
index 9371764..01237ce 100644
--- a/resources/js/components/ui/select/SelectItem.vue
+++ b/resources/js/components/ui/select/SelectItem.vue
@@ -1,44 +1,48 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/resources/js/components/ui/select/SelectTrigger.vue b/resources/js/components/ui/select/SelectTrigger.vue
index 667908b..dc738b9 100644
--- a/resources/js/components/ui/select/SelectTrigger.vue
+++ b/resources/js/components/ui/select/SelectTrigger.vue
@@ -1,33 +1,45 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/resources/js/components/ui/spinner/Spinner.vue b/resources/js/components/ui/spinner/Spinner.vue
index 57cd1a9..191889f 100644
--- a/resources/js/components/ui/spinner/Spinner.vue
+++ b/resources/js/components/ui/spinner/Spinner.vue
@@ -1,17 +1,23 @@
-
+
diff --git a/tests/Feature/UiComponentsTest.php b/tests/Feature/UiComponentsTest.php
new file mode 100644
index 0000000..a5c4088
--- /dev/null
+++ b/tests/Feature/UiComponentsTest.php
@@ -0,0 +1,19 @@
+setTimeout(60);
+ $process->run();
+
+ $this->assertTrue($process->isSuccessful(), $process->getOutput().$process->getErrorOutput());
+ $this->assertStringContainsString('UI component behavior checks passed.', $process->getOutput());
+ }
+}
diff --git a/tests/Fixtures/ui-components-preview.mjs b/tests/Fixtures/ui-components-preview.mjs
new file mode 100644
index 0000000..04ba3bd
--- /dev/null
+++ b/tests/Fixtures/ui-components-preview.mjs
@@ -0,0 +1,247 @@
+import { createApp, h, ref } from 'vue';
+import '../../resources/css/app.css';
+import AppearanceTabs from '../../resources/js/components/AppearanceTabs.vue';
+import {
+ Avatar,
+ AvatarFallback,
+} from '../../resources/js/components/ui/avatar';
+import { Badge } from '../../resources/js/components/ui/badge';
+import { Button } from '../../resources/js/components/ui/button';
+import { Checkbox } from '../../resources/js/components/ui/checkbox';
+import { Input } from '../../resources/js/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '../../resources/js/components/ui/select';
+import { createAppearance } from '../../resources/js/composables/useAppearance';
+import { createLocalization } from '../../resources/js/lib/localization';
+
+const Demo = {
+ setup() {
+ const selectedStatus = ref('review');
+ const subscribed = ref(true);
+
+ return () =>
+ h('main', { class: 'mx-auto grid max-w-4xl gap-column p-page' }, [
+ h(
+ 'div',
+ {
+ class: 'flex flex-wrap items-center justify-between gap-stack',
+ },
+ [
+ h('div', {}, [
+ h(
+ 'h1',
+ { class: 'text-headline' },
+ 'Foundational UI components',
+ ),
+ h(
+ 'p',
+ { class: 'text-muted-foreground' },
+ 'Fluid Emerald states and controls',
+ ),
+ ]),
+ h(AppearanceTabs),
+ ],
+ ),
+ h('section', { class: 'grid gap-column sm:grid-cols-2' }, [
+ h(
+ 'article',
+ {
+ class: 'grid content-start gap-column rounded-md border bg-card p-card',
+ },
+ [
+ h(
+ 'h2',
+ { class: 'text-title' },
+ 'Buttons and badges',
+ ),
+ h('div', { class: 'flex flex-wrap gap-stack' }, [
+ h(Button, {}, () => 'Primary'),
+ h(
+ Button,
+ { variant: 'secondary' },
+ () => 'Secondary',
+ ),
+ h(
+ Button,
+ { variant: 'outline' },
+ () => 'Outline',
+ ),
+ h(
+ Button,
+ { variant: 'destructive' },
+ () => 'Delete',
+ ),
+ h(Button, { disabled: true }, () => 'Disabled'),
+ h(Button, { loading: true }, () => 'Saving'),
+ ]),
+ h('div', { class: 'flex flex-wrap gap-stack' }, [
+ h(Badge, {}, () => 'Active'),
+ h(Badge, { variant: 'success' }, () => 'Done'),
+ h(
+ Badge,
+ { variant: 'warning' },
+ () => 'Medium',
+ ),
+ h(Badge, { variant: 'info' }, () => 'Low'),
+ h(
+ Badge,
+ { variant: 'destructive' },
+ () => 'High',
+ ),
+ h(
+ Badge,
+ { variant: 'outline' },
+ () => 'KAN-18',
+ ),
+ ]),
+ ],
+ ),
+ h(
+ 'article',
+ {
+ class: 'grid content-start gap-column rounded-md border bg-card p-card',
+ },
+ [
+ h(
+ 'h2',
+ { class: 'text-title' },
+ 'Inputs and selection',
+ ),
+ h(
+ 'label',
+ { class: 'grid gap-stack', for: 'title' },
+ [
+ h(
+ 'span',
+ { class: 'text-body font-medium' },
+ 'Title',
+ ),
+ h(Input, {
+ id: 'title',
+ placeholder: 'Ticket title',
+ }),
+ ],
+ ),
+ h(
+ 'label',
+ {
+ class: 'grid gap-stack',
+ for: 'invalid-title',
+ },
+ [
+ h(
+ 'span',
+ { class: 'text-body font-medium' },
+ 'Invalid title',
+ ),
+ h(Input, {
+ id: 'invalid-title',
+ invalid: true,
+ modelValue: 'Needs attention',
+ }),
+ ],
+ ),
+ h(
+ 'label',
+ { class: 'grid gap-stack', for: 'status' },
+ [
+ h(
+ 'span',
+ { class: 'text-body font-medium' },
+ 'Status',
+ ),
+ h(
+ Select,
+ {
+ modelValue: selectedStatus.value,
+ 'onUpdate:modelValue': (value) => {
+ selectedStatus.value = value;
+ },
+ },
+ {
+ default: () => [
+ h(
+ SelectTrigger,
+ {
+ id: 'status',
+ class: 'w-full',
+ },
+ {
+ default: () =>
+ h(SelectValue, {
+ placeholder:
+ 'Choose a status',
+ }),
+ },
+ ),
+ h(SelectContent, {}, () => [
+ h(
+ SelectItem,
+ { value: 'todo' },
+ () => 'To do',
+ ),
+ h(
+ SelectItem,
+ { value: 'review' },
+ () => 'Review',
+ ),
+ h(
+ SelectItem,
+ { value: 'done' },
+ () => 'Done',
+ ),
+ ]),
+ ],
+ },
+ ),
+ ],
+ ),
+ h(
+ 'label',
+ { class: 'flex items-center gap-stack' },
+ [
+ h(Checkbox, {
+ modelValue: subscribed.value,
+ 'onUpdate:modelValue': (value) => {
+ subscribed.value = Boolean(value);
+ },
+ }),
+ h('span', {}, 'Notify me about changes'),
+ ],
+ ),
+ ],
+ ),
+ ]),
+ h(
+ 'section',
+ {
+ class: 'flex items-center gap-stack rounded-md border bg-card p-card',
+ },
+ [
+ h(Avatar, { size: 'sm' }, () =>
+ h(AvatarFallback, {}, () => 'NS'),
+ ),
+ h(Avatar, {}, () => h(AvatarFallback, {}, () => 'AK')),
+ h(Avatar, { size: 'lg' }, () =>
+ h(AvatarFallback, {}, () => 'LT'),
+ ),
+ h(
+ 'span',
+ { class: 'text-muted-foreground' },
+ '24px, 32px and 40px avatars',
+ ),
+ ],
+ ),
+ ]);
+ },
+};
+
+createApp(Demo)
+ .use(createLocalization())
+ .use(createAppearance('light'))
+ .mount('#app');
diff --git a/tests/Fixtures/ui-components.html b/tests/Fixtures/ui-components.html
new file mode 100644
index 0000000..012c274
--- /dev/null
+++ b/tests/Fixtures/ui-components.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ UI component preview
+
+
+
+
+
+
diff --git a/tests/Fixtures/ui-components.mjs b/tests/Fixtures/ui-components.mjs
new file mode 100644
index 0000000..aaa13da
--- /dev/null
+++ b/tests/Fixtures/ui-components.mjs
@@ -0,0 +1,184 @@
+import assert from 'node:assert/strict';
+import { fileURLToPath } from 'node:url';
+import vue from '@vitejs/plugin-vue';
+import { createServer } from 'vite';
+import { createSSRApp, h } from 'vue';
+import { renderToString } from 'vue/server-renderer';
+
+const server = await createServer({
+ configFile: false,
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
+ },
+ },
+ server: { middlewareMode: true, hmr: false, watch: null },
+ appType: 'custom',
+});
+
+try {
+ const { createLocalization } = await server.ssrLoadModule(
+ '/resources/js/lib/localization.ts',
+ );
+ const { en } = await server.ssrLoadModule('/resources/js/locales/en.ts');
+ const { default: Button } = await server.ssrLoadModule(
+ '/resources/js/components/ui/button/Button.vue',
+ );
+ const { buttonVariants } = await server.ssrLoadModule(
+ '/resources/js/components/ui/button/index.ts',
+ );
+ const { default: Input } = await server.ssrLoadModule(
+ '/resources/js/components/ui/input/Input.vue',
+ );
+ const { default: Select } = await server.ssrLoadModule(
+ '/resources/js/components/ui/select/Select.vue',
+ );
+ const { default: SelectTrigger } = await server.ssrLoadModule(
+ '/resources/js/components/ui/select/SelectTrigger.vue',
+ );
+ const { default: SelectValue } = await server.ssrLoadModule(
+ '/resources/js/components/ui/select/SelectValue.vue',
+ );
+ const { default: Checkbox } = await server.ssrLoadModule(
+ '/resources/js/components/ui/checkbox/Checkbox.vue',
+ );
+ const { default: Badge } = await server.ssrLoadModule(
+ '/resources/js/components/ui/badge/Badge.vue',
+ );
+ const { badgeVariants } = await server.ssrLoadModule(
+ '/resources/js/components/ui/badge/index.ts',
+ );
+ const { default: Avatar } = await server.ssrLoadModule(
+ '/resources/js/components/ui/avatar/Avatar.vue',
+ );
+ const { default: AvatarFallback } = await server.ssrLoadModule(
+ '/resources/js/components/ui/avatar/AvatarFallback.vue',
+ );
+ const { default: UserAvatar } = await server.ssrLoadModule(
+ '/resources/js/components/ui/avatar/UserAvatar.vue',
+ );
+ const { avatarVariants } = await server.ssrLoadModule(
+ '/resources/js/components/ui/avatar/index.ts',
+ );
+
+ const german = createLocalization(
+ { locale: 'de' },
+ { de: { ...en, 'common.loading': 'Wird geladen…' } },
+ );
+ const render = (component, props = {}, slots = {}) =>
+ renderToString(
+ createSSRApp({ render: () => h(component, props, slots) }).use(
+ german,
+ ),
+ );
+
+ const loadingButton = await render(
+ Button,
+ { loading: true },
+ { default: () => 'Save' },
+ );
+ assert.match(loadingButton, /Save);
+ assert.match(buttonVariants(), /min-h-control/);
+ assert.match(buttonVariants(), /bg-brand/);
+ assert.match(buttonVariants({ variant: 'outline' }), /bg-card/);
+
+ const invalidInput = await render(Input, {
+ disabled: true,
+ invalid: true,
+ name: 'title',
+ });
+ assert.match(invalidInput, /
+ h(
+ SelectTrigger,
+ { invalid: true },
+ {
+ default: () =>
+ h(SelectValue, { placeholder: 'Choose' }),
+ },
+ ),
+ },
+ );
+ assert.match(select, /role="combobox"/);
+ assert.match(select, /disabled/);
+ assert.match(select, /aria-invalid="true"/);
+ assert.match(select, /data-invalid/);
+ assert.match(select, /min-h-control/);
+
+ const checkbox = await render(Checkbox, {
+ disabled: true,
+ invalid: true,
+ });
+ assert.match(checkbox, /role="checkbox"/);
+ assert.match(checkbox, /aria-checked="false"/);
+ assert.match(checkbox, /disabled/);
+ assert.match(checkbox, /aria-invalid="true"/);
+ assert.match(checkbox, /data-invalid/);
+
+ const successBadge = await render(
+ Badge,
+ { variant: 'success' },
+ { default: () => 'Done' },
+ );
+ assert.match(successBadge, /bg-success/);
+ assert.match(successBadge, />Done);
+ assert.doesNotMatch(successBadge, /variant="success"/);
+ assert.match(badgeVariants({ variant: 'success' }), /bg-success/);
+ assert.match(badgeVariants({ variant: 'warning' }), /bg-warning/);
+ assert.match(badgeVariants({ variant: 'info' }), /bg-info/);
+
+ const avatar = await render(
+ Avatar,
+ { size: 'xl', shape: 'square' },
+ { default: () => h(AvatarFallback, {}, () => 'NS') },
+ );
+ assert.match(avatar, /data-size="xl"/);
+ assert.match(avatar, /data-shape="square"/);
+ assert.match(avatar, /size-16/);
+ assert.match(avatar, /rounded-sm/);
+ assert.match(avatar, />NS);
+ assert.match(avatarVariants({ size: 'sm' }), /size-6/);
+
+ const userAvatar = await render(UserAvatar, {
+ rounded: true,
+ size: 'lg',
+ user: {
+ id: 1,
+ name: 'Nico Schröder',
+ email: 'nico@example.test',
+ avatar: null,
+ avatar_color: null,
+ avatar_is_generated: false,
+ email_verified_at: null,
+ must_change_password_on_next_login: false,
+ created_at: '2026-09-09T00:00:00Z',
+ updated_at: '2026-09-09T00:00:00Z',
+ },
+ });
+ assert.match(userAvatar, /data-size="lg"/);
+ assert.match(userAvatar, /data-shape="circle"/);
+ assert.match(userAvatar, /aria-label="Nico Schröder"/);
+ assert.match(userAvatar, />NS);
+
+ console.log('UI component behavior checks passed.');
+} finally {
+ await server.close();
+}