+
+export type FormInputEvents = 'input' | 'blur' | 'change'
+
+export interface FormError {
+ name: P
message: string
}
@@ -9,30 +27,48 @@ export interface FormErrorWithId extends FormError {
id: string
}
-export interface Form {
- validate(path?: string | string[], opts?: { silent?: true }): Promise;
- validate(path?: string | string[], opts?: { silent?: false }): Promise;
- clear(path?: string): void
- errors: Ref
- setErrors(errs: FormError[], path?: string): void
- getErrors(path?: string): FormError[]
- submit(): Promise
-}
-
export type FormSubmitEvent = SubmitEvent & { data: T }
-export type FormErrorEvent = SubmitEvent & { errors: FormErrorWithId[] }
-export type FormEventType = 'blur' | 'input' | 'change' | 'submit'
+export type FormValidationError = {
+ errors: FormErrorWithId[]
+ childrens: FormValidationError[]
+}
-export interface FormEvent {
+export type FormErrorEvent = SubmitEvent & FormValidationError
+
+export type FormEventType = FormInputEvents
+
+export type FormChildAttachEvent = {
+ type: 'attach'
+ formId: string | number
+ validate: Form['validate']
+}
+
+export type FormChildDetachEvent = {
+ type: 'detach'
+ formId: string | number
+}
+
+export type FormInputEvent = {
type: FormEventType
- path?: string
+ name?: string
}
-export interface InjectedFormGroupValue {
- inputId: Ref
- name: Ref
- size: Ref
- error: Ref
- eagerValidation: Ref
+export type FormEvent =
+ | FormInputEvent
+ | FormChildAttachEvent
+ | FormChildDetachEvent
+
+export interface FormInjectedOptions {
+ disabled?: ComputedRef
+ validateOnInputDelay?: ComputedRef
+}
+
+export interface FormFieldInjectedOptions {
+ inputId: Ref
+ name: ComputedRef
+ size: ComputedRef
+ error: ComputedRef
+ eagerValidation: ComputedRef
+ validateOnInputDelay: ComputedRef
}
diff --git a/src/runtime/types/index.d.ts b/src/runtime/types/index.d.ts
index 3b90d853..e69de29b 100644
--- a/src/runtime/types/index.d.ts
+++ b/src/runtime/types/index.d.ts
@@ -1,31 +0,0 @@
-export * from './accordion'
-export * from './alert'
-export * from './avatar'
-export * from './badge'
-export * from './breadcrumb'
-export * from './button'
-export * from './chip'
-export * from './clipboard'
-export * from './command-palette'
-export * from './divider'
-export * from './dropdown'
-export * from './form-group'
-export * from './form'
-export * from './horizontal-navigation'
-export * from './input'
-export * from './kbd'
-export * from './link'
-export * from './meter'
-export * from './modal'
-export * from './slideover'
-export * from './notification'
-export * from './popper'
-export * from './progress'
-export * from './range'
-export * from './select'
-export * from './tabs'
-export * from './textarea'
-export * from './toggle'
-export * from './tooltip'
-export * from './vertical-navigation'
-export * from './utils'
diff --git a/src/runtime/utils/form.ts b/src/runtime/utils/form.ts
new file mode 100644
index 00000000..c4858efd
--- /dev/null
+++ b/src/runtime/utils/form.ts
@@ -0,0 +1,83 @@
+import type { ZodSchema } from 'zod'
+import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
+import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
+import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
+import type { FormError } from '#ui/types/form'
+
+export function isYupSchema (schema: any): schema is YupObjectSchema {
+ return schema.validate && schema.__isYupSchema__
+}
+
+export function isYupError (error: any): error is YupError {
+ return error.inner !== undefined
+}
+
+export async function getYupErrors (state: any, schema: YupObjectSchema): Promise {
+ try {
+ await schema.validate(state, { abortEarly: false })
+ return []
+ } catch (error) {
+ if (isYupError(error)) {
+ return error.inner.map((issue) => ({
+ name: issue.path ?? '',
+ message: issue.message
+ }))
+ } else {
+ throw error
+ }
+ }
+}
+
+export function isZodSchema (schema: any): schema is ZodSchema {
+ return schema.parse !== undefined
+}
+
+export async function getZodErrors (state: any, schema: ZodSchema): Promise {
+ const result = await schema.safeParseAsync(state)
+ if (result.success === false) {
+ return result.error.issues.map((issue) => ({
+ name: issue.path.join('.'),
+ message: issue.message
+ }))
+ }
+ return []
+}
+
+export function isJoiSchema (schema: any): schema is JoiSchema {
+ return schema.validateAsync !== undefined && schema.id !== undefined
+}
+
+export function isJoiError (error: any): error is JoiError {
+ return error.isJoi === true
+}
+
+export async function getJoiErrors (state: any, schema: JoiSchema): Promise {
+ try {
+ await schema.validateAsync(state, { abortEarly: false })
+ return []
+ } catch (error) {
+ if (isJoiError(error)) {
+ return error.details.map((detail) => ({
+ name: detail.path.join('.'),
+ message: detail.message
+ }))
+ } else {
+ throw error
+ }
+ }
+}
+
+export function isValibotSchema (schema: any): schema is ValibotObjectSchema {
+ return schema._parse !== undefined
+}
+
+export async function getValibotError (state: any, schema: ValibotObjectSchema): Promise {
+ const result = await schema._parse(state)
+ if (result.issues) {
+ return result.issues.map((issue) => ({
+ name: issue.path?.map((p) => p.key).join('.') || '',
+ message: issue.message
+ }))
+ }
+ return []
+}
diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts
index 184ab445..07eb5c6e 100644
--- a/src/runtime/utils/index.ts
+++ b/src/runtime/utils/index.ts
@@ -1,86 +1,24 @@
-import { defu, createDefu } from 'defu'
-import { extendTailwindMerge } from 'tailwind-merge'
-import type { Strategy } from '../types'
+export function pick (data: Data, keys: Keys[]): Pick {
+ const result = {} as Pick
-const customTwMerge = extendTailwindMerge({
- extend: {
- classGroups: {
- icons: [(classPart: string) => /^i-/.test(classPart)]
- }
- }
-})
-
-const defuTwMerge = createDefu((obj, key, value, namespace) => {
- if (namespace === 'default' || namespace.startsWith('default.')) {
- return false
- }
- if (namespace === 'popper' || namespace.startsWith('popper.')) {
- return false
- }
- if (namespace.endsWith('avatar') && key === 'size') {
- return false
- }
- if (namespace.endsWith('chip') && key === 'size') {
- return false
- }
- if (namespace.endsWith('badge') && key === 'size' || key === 'color' || key === 'variant') {
- return false
- }
- if (typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
- // @ts-ignore
- obj[key] = customTwMerge(obj[key], value)
- return true
- }
-})
-
-export function mergeConfig (strategy: Strategy, ...configs): T {
- if (strategy === 'override') {
- return defu({}, ...configs) as T
+ for (const key of keys) {
+ result[key] = data[key]
}
- return defuTwMerge({}, ...configs) as T
-}
-
-export function hexToRgb (hex: string) {
- // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
- const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
- hex = hex.replace(shorthandRegex, function (_, r, g, b) {
- return r + r + g + g + b + b
- })
-
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
- ? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
- : null
}
-export function getSlotsChildren (slots: any) {
- let children = slots.default?.()
- if (children?.length) {
- children = children.flatMap(c => {
- if (typeof c.type === 'symbol') {
- if (typeof c.children === 'string') {
- // `v-if="false"` or commented node
- return
- }
- return c.children
- } else if (c.type.name === 'ContentSlot') {
- return c.ctx.slots.default?.()
- }
- return c
- }).filter(Boolean)
+export function omit (data: Data, keys: Keys[]): Omit {
+ const result = { ...data }
+
+ for (const key of keys) {
+ delete result[key]
}
- return children || []
+
+ return result as Omit
}
-/**
- * "123-foo" will be parsed to 123
- * This is used for the .number modifier in v-model
- */
export function looseToNumber (val: any): any {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
-
-export * from './lodash'
-export * from './link'
diff --git a/src/templates.ts b/src/templates.ts
index 5057e114..69db9a67 100644
--- a/src/templates.ts
+++ b/src/templates.ts
@@ -1,20 +1,99 @@
-import { useNuxt, addTemplate } from '@nuxt/kit'
+import { addTemplate, addTypeTemplate } from '@nuxt/kit'
+import type { Nuxt } from '@nuxt/schema'
+import type { ModuleOptions } from './module'
+import * as theme from './theme'
+
+export default function createTemplates (options: ModuleOptions, nuxt: Nuxt) {
+ const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
-export default function createTemplates (nuxt = useNuxt()) {
const template = addTemplate({
- filename: 'ui.colors.mjs',
- getContents: () => `export default ${JSON.stringify(nuxt.options.appConfig.ui.colors)};`,
- write: true
- })
- const typesTemplate = addTemplate({
- filename: 'ui.colors.d.ts',
- getContents: () => `declare module '#ui-colors' { const defaultExport: ${JSON.stringify(nuxt.options.appConfig.ui.colors)}; export default defaultExport; }`,
- write: true
+ filename: 'tailwind.css',
+ write: true,
+ getContents: () => `@import "tailwindcss";
+
+ @layer base {
+ :root {
+ color-scheme: light dark;
+ }
+ }
+
+ @theme {
+ --color-gray-*: initial;
+ --color-cool-50: #f9fafb;
+ --color-cool-100: #f3f4f6;
+ --color-cool-200: #e5e7eb;
+ --color-cool-300: #d1d5db;
+ --color-cool-400: #9ca3af;
+ --color-cool-500: #6b7280;
+ --color-cool-600: #4b5563;
+ --color-cool-700: #374151;
+ --color-cool-800: #1f2937;
+ --color-cool-900: #111827;
+ --color-cool-950: #030712;
+
+ ${shades.map(shade => `--color-primary-${shade}: var(--color-primary-${shade});`).join('\n')}
+ ${shades.map(shade => `--color-gray-${shade}: var(--color-gray-${shade});`).join('\n')}
+ }
+ `
})
- nuxt.options.alias['#ui-colors'] = template.dst
+ nuxt.options.css.push(template.dst)
- nuxt.hook('prepare:types', (opts) => {
- opts.references.push({ path: typesTemplate.dst })
+ for (const component in theme) {
+ addTemplate({
+ filename: `ui/${component}.ts`,
+ write: true,
+ getContents: async () => {
+ const template = (theme as any)[component]
+ const result = typeof template === 'function' ? template({ colors: options.colors }) : template
+
+ const variants = Object.keys(result.variants || {})
+
+ let json = JSON.stringify(result, null, 2)
+
+ for (const variant of variants) {
+ json = json.replaceAll(new RegExp(`("${variant}": "[0-9a-z-]+")`, 'g'), '$1 as const')
+ }
+
+ return `export default ${json}`
+ }
+ })
+ }
+
+ addTemplate({
+ filename: 'ui/index.ts',
+ write: true,
+ getContents: () => Object.keys(theme).map(component => `export { default as ${component} } from './${component}'`).join('\n')
+ })
+
+ // FIXME: `typeof colors[number]` should include all colors from the theme
+ addTypeTemplate({
+ filename: 'types/ui.d.ts',
+ getContents: () => `import * as ui from '#build/ui'
+
+ type DeepPartial = Partial<{
+ [P in keyof T]: DeepPartial | { [key: string]: string | object }
+ }>
+
+ const colors = ${JSON.stringify(options.colors)} as const;
+
+ type UI = {
+ primary?: typeof colors[number]
+ gray?: 'slate' | 'cool' | 'zinc' | 'neutral' | 'stone'
+ [key: string]: any
+ } & DeepPartial
+
+ declare module 'nuxt/schema' {
+ interface AppConfigInput {
+ ui?: UI
+ }
+ }
+ declare module '@nuxt/schema' {
+ interface AppConfigInput {
+ ui?: UI
+ }
+ }
+ export {}
+ `
})
}
diff --git a/src/theme/accordion.ts b/src/theme/accordion.ts
new file mode 100644
index 00000000..31891924
--- /dev/null
+++ b/src/theme/accordion.ts
@@ -0,0 +1,12 @@
+export default {
+ slots: {
+ root: 'w-full',
+ item: 'border-b border-gray-200 dark:border-gray-800 last:border-b-0',
+ header: 'flex',
+ trigger: 'group flex-1 flex items-center gap-1.5 font-medium text-sm hover:underline py-3.5 disabled:cursor-not-allowed disabled:opacity-75 disabled:hover:no-underline focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:outline-0',
+ content: 'text-sm pb-3.5 data-[state=open]:animate-[accordion-down_200ms_ease-in-out] data-[state=closed]:animate-[accordion-up_200ms_ease-in-out] overflow-hidden focus:outline-none',
+ leadingIcon: 'shrink-0 w-5 h-5',
+ trailingIcon: 'ms-auto w-5 h-5 group-data-[state=open]:rotate-180 transition-transform duration-200',
+ label: 'truncate'
+ }
+}
diff --git a/src/theme/avatar.ts b/src/theme/avatar.ts
new file mode 100644
index 00000000..0b332463
--- /dev/null
+++ b/src/theme/avatar.ts
@@ -0,0 +1,42 @@
+export default {
+ slots: {
+ root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-gray-100 dark:bg-gray-800',
+ image: 'h-full w-full rounded-[inherit] object-cover',
+ fallback: 'font-medium leading-none text-gray-500 dark:text-gray-400 truncate',
+ icon: 'text-gray-500 dark:text-gray-400 shrink-0'
+ },
+ variants: {
+ size: {
+ '3xs': {
+ root: 'size-4 text-[8px]'
+ },
+ '2xs': {
+ root: 'size-5 text-[10px]'
+ },
+ xs: {
+ root: 'size-6 text-xs'
+ },
+ sm: {
+ root: 'size-7 text-sm'
+ },
+ md: {
+ root: 'size-8 text-base'
+ },
+ lg: {
+ root: 'size-9 text-lg'
+ },
+ xl: {
+ root: 'size-10 text-xl'
+ },
+ '2xl': {
+ root: 'size-11 text-[22px]'
+ },
+ '3xl': {
+ root: 'size-12 text-2xl'
+ }
+ }
+ },
+ defaultVariants: {
+ size: 'sm'
+ }
+}
diff --git a/src/theme/badge.ts b/src/theme/badge.ts
new file mode 100644
index 00000000..5839d782
--- /dev/null
+++ b/src/theme/badge.ts
@@ -0,0 +1,57 @@
+export default (config: { colors: string[] }) => ({
+ base: 'rounded-md font-medium inline-flex items-center',
+ variants: {
+ color: {
+ ...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
+ white: '',
+ gray: '',
+ black: ''
+ },
+ variant: {
+ solid: '',
+ outline: '',
+ soft: '',
+ subtle: ''
+ },
+ size: {
+ xs: 'text-xs px-1.5 py-0.5',
+ sm: 'text-xs px-2 py-1',
+ md: 'text-sm px-2 py-1',
+ lg: 'text-sm px-2.5 py-1.5'
+ }
+ },
+ compoundVariants: [...config.colors.map((color: string) => ({
+ color,
+ variant: 'solid',
+ class: `bg-${color}-500 dark:bg-${color}-400 text-white dark:text-gray-900`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'outline',
+ class: `text-${color}-500 dark:text-${color}-400 ring ring-inset ring-${color}-500 dark:ring-${color}-400`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'soft',
+ class: `bg-${color}-50 dark:bg-${color}-400/10 text-${color}-500 dark:text-${color}-400`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'subtle',
+ class: `bg-${color}-50 dark:bg-${color}-400/10 text-${color}-500 dark:text-${color}-400 ring ring-inset ring-${color}-500/25 dark:ring-${color}-400/25`
+ })), {
+ color: 'white',
+ variant: 'solid',
+ class: 'ring ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white dark:bg-gray-900'
+ }, {
+ color: 'gray',
+ variant: 'solid',
+ class: 'ring ring-inset ring-gray-300 dark:ring-gray-700 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-800'
+ }, {
+ color: 'black',
+ variant: 'solid',
+ class: 'text-white dark:text-gray-900 bg-gray-900 dark:bg-white'
+ }],
+ defaultVariants: {
+ color: 'primary',
+ variant: 'solid',
+ size: 'sm'
+ }
+})
diff --git a/src/theme/button.ts b/src/theme/button.ts
new file mode 100644
index 00000000..76186f00
--- /dev/null
+++ b/src/theme/button.ts
@@ -0,0 +1,169 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ base: 'rounded-md font-medium inline-flex items-center focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75',
+ label: '',
+ leadingIcon: 'shrink-0',
+ trailingIcon: 'shrink-0'
+ },
+ variants: {
+ color: {
+ ...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
+ white: '',
+ gray: '',
+ black: ''
+ },
+ variant: {
+ solid: '',
+ outline: '',
+ soft: '',
+ ghost: '',
+ link: ''
+ },
+ size: {
+ '2xs': {
+ base: 'px-2 py-1 text-xs gap-x-1',
+ leadingIcon: 'size-4',
+ trailingIcon: 'size-4'
+ },
+ xs: {
+ base: 'px-2.5 py-1.5 text-xs gap-x-1.5',
+ leadingIcon: 'size-4',
+ trailingIcon: 'size-4'
+ },
+ sm: {
+ base: 'px-2.5 py-1.5 text-sm gap-x-1.5',
+ leadingIcon: 'size-5',
+ trailingIcon: 'size-5'
+ },
+ md: {
+ base: 'px-3 py-2 text-sm gap-x-2',
+ leadingIcon: 'size-5',
+ trailingIcon: 'size-5'
+ },
+ lg: {
+ base: 'px-3.5 py-2.5 text-sm gap-x-2.5',
+ leadingIcon: 'size-5',
+ trailingIcon: 'size-5'
+ },
+ xl: {
+ base: 'px-3.5 py-2.5 text-base gap-x-2.5',
+ leadingIcon: 'size-6',
+ trailingIcon: 'size-6'
+ }
+ },
+ truncate: {
+ true: {
+ label: 'truncate'
+ }
+ },
+ block: {
+ true: {
+ base: 'w-full',
+ trailingIcon: 'ms-auto'
+ }
+ },
+ square: {
+ true: ''
+ },
+ leading: {
+ true: ''
+ },
+ trailing: {
+ true: ''
+ },
+ loading: {
+ true: ''
+ }
+ },
+ compoundVariants: [...config.colors.map((color: string) => ({
+ color,
+ variant: 'solid',
+ class: `shadow-sm text-white dark:text-gray-900 bg-${color}-500 hover:bg-${color}-600 disabled:bg-${color}-500 dark:bg-${color}-400 dark:hover:bg-${color}-500 dark:disabled:bg-${color}-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-${color}-500 dark:focus-visible:outline-${color}-400`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'outline',
+ class: `ring ring-inset ring-current text-${color}-500 dark:text-${color}-400 hover:bg-${color}-50 disabled:bg-transparent dark:hover:bg-${color}-950 dark:disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'soft',
+ class: `text-${color}-500 dark:text-${color}-400 bg-${color}-50 hover:bg-${color}-100 disabled:bg-${color}-50 dark:bg-${color}-950 dark:hover:bg-${color}-900 dark:disabled:bg-${color}-950 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'ghost',
+ class: `text-${color}-500 dark:text-${color}-400 hover:bg-${color}-50 disabled:bg-transparent dark:hover:bg-${color}-950 dark:disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`
+ })), ...config.colors.map((color: string) => ({
+ color,
+ variant: 'link',
+ class: `text-${color}-500 hover:text-${color}-600 disabled:text-${color}-500 dark:text-${color}-400 dark:hover:text-${color}-500 dark:disabled:text-${color}-400 underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`
+ })), {
+ color: 'white',
+ variant: 'solid',
+ class: 'shadow-sm ring ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white hover:bg-gray-50 disabled:bg-white dark:bg-gray-900 dark:hover:bg-gray-800/50 dark:disabled:bg-gray-900 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ color: 'white',
+ variant: 'ghost',
+ class: 'text-gray-900 dark:text-white hover:bg-white dark:hover:bg-gray-900 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ color: 'gray',
+ variant: 'solid',
+ class: 'shadow-sm ring ring-inset ring-gray-300 dark:ring-gray-700 text-gray-700 dark:text-gray-200 bg-gray-50 hover:bg-gray-100 disabled:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50 dark:disabled:bg-gray-800 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ color: 'gray',
+ variant: 'ghost',
+ class: 'text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ color: 'gray',
+ variant: 'link',
+ class: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ color: 'black',
+ variant: 'solid',
+ class: 'shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ color: 'black',
+ variant: 'link',
+ class: 'text-gray-900 dark:text-white underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
+ }, {
+ size: '2xs',
+ square: true,
+ class: 'p-1'
+ }, {
+ size: 'xs',
+ square: true,
+ class: 'p-1.5'
+ }, {
+ size: 'sm',
+ square: true,
+ class: 'p-1.5'
+ }, {
+ size: 'md',
+ square: true,
+ class: 'p-2'
+ }, {
+ size: 'lg',
+ square: true,
+ class: 'p-2.5'
+ }, {
+ size: 'xl',
+ square: true,
+ class: 'p-2.5'
+ }, {
+ loading: true,
+ leading: true,
+ class: {
+ leadingIcon: 'animate-spin'
+ }
+ }, {
+ loading: true,
+ leading: false,
+ trailing: true,
+ class: {
+ trailingIcon: 'animate-spin'
+ }
+ }],
+ defaultVariants: {
+ color: 'primary',
+ variant: 'solid',
+ size: 'sm'
+ }
+})
diff --git a/src/theme/card.ts b/src/theme/card.ts
new file mode 100644
index 00000000..4fdf9608
--- /dev/null
+++ b/src/theme/card.ts
@@ -0,0 +1,8 @@
+export default {
+ slots: {
+ root: 'bg-white dark:bg-gray-900 ring ring-gray-200 dark:ring-gray-800 divide-y divide-gray-200 dark:divide-gray-800 rounded-lg shadow',
+ header: 'p-4 sm:px-6',
+ body: 'p-4 sm:p-6',
+ footer: 'p-4 sm:px-6'
+ }
+}
diff --git a/src/theme/checkbox.ts b/src/theme/checkbox.ts
new file mode 100644
index 00000000..f84fb4c1
--- /dev/null
+++ b/src/theme/checkbox.ts
@@ -0,0 +1,79 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ root: 'relative flex items-start',
+ base: 'shrink-0 text-white dark:text-gray-900 rounded ring ring-inset ring-gray-300 dark:ring-gray-700 focus:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
+ container: 'flex items-center',
+ wrapper: 'ms-2',
+ indicator: 'flex',
+ icon: 'size-full',
+ label: 'font-medium text-gray-700 dark:text-gray-200',
+ description: 'text-gray-500 dark:text-gray-400'
+ },
+ variants: {
+ color: Object.fromEntries(config.colors.map((color: string) => [color, `focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`])),
+ size: {
+ '2xs': {
+ base: 'size-3',
+ container: 'h-4',
+ wrapper: 'text-xs'
+ },
+ xs: {
+ base: 'size-3.5',
+ container: 'h-4',
+ wrapper: 'text-xs'
+ },
+ sm: {
+ base: 'size-4',
+ container: 'h-5',
+ wrapper: 'text-sm'
+ },
+ md: {
+ base: 'size-[18px]',
+ container: 'h-5',
+ wrapper: 'text-sm'
+ },
+ lg: {
+ base: 'size-5',
+ container: 'h-6',
+ wrapper: 'text-base'
+ },
+ xl: {
+ base: 'size-[22px]',
+ container: 'h-6',
+ wrapper: 'text-base'
+ }
+ },
+ required: {
+ true: {
+ label: 'after:content-[\'*\'] after:ms-0.5 after:text-red-500 dark:after:text-red-400'
+ }
+ },
+ disabled: {
+ true: {
+ base: 'cursor-not-allowed opacity-75',
+ label: 'cursor-not-allowed opacity-75',
+ description: 'cursor-not-allowed opacity-75'
+ }
+ },
+ checked: {
+ true: ''
+ },
+ indeterminate: {
+ true: ''
+ }
+ },
+ compoundVariants: config.colors.flatMap((color) => ([{
+ color,
+ checked: true,
+ class: `ring-2 ring-inset ring-${color}-500 dark:ring-${color}-400 bg-${color}-500 dark:bg-${color}-400`
+ }, {
+ color,
+ indeterminate: true,
+ class: `ring-2 ring-inset ring-${color}-500 dark:ring-${color}-400 bg-${color}-500 dark:bg-${color}-400`
+ }
+ ])),
+ defaultVariants: {
+ size: 'sm',
+ color: 'primary'
+ }
+})
diff --git a/src/theme/chip.ts b/src/theme/chip.ts
new file mode 100644
index 00000000..43269161
--- /dev/null
+++ b/src/theme/chip.ts
@@ -0,0 +1,56 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ root: 'relative inline-flex items-center justify-center shrink-0',
+ base: 'absolute rounded-full ring ring-white dark:ring-gray-900 flex items-center justify-center text-white dark:text-gray-900 font-medium whitespace-nowrap'
+ },
+ variants: {
+ color: {
+ ...Object.fromEntries(config.colors.map((color: string) => [color, `bg-${color}-500 dark:bg-${color}-400`])),
+ gray: 'bg-gray-500 dark:bg-gray-400',
+ white: 'bg-white dark:bg-gray-900',
+ black: 'bg-gray-900 dark:bg-white'
+ },
+ size: {
+ '3xs': 'h-[4px] min-w-[4px] text-[4px]',
+ '2xs': 'h-[5px] min-w-[5px] text-[5px]',
+ xs: 'h-[6px] min-w-[6px] text-[6px]',
+ sm: 'h-[7px] min-w-[7px] text-[7px]',
+ md: 'h-[8px] min-w-[8px] text-[8px]',
+ lg: 'h-[9px] min-w-[9px] text-[9px]',
+ xl: 'h-[10px] min-w-[10px] text-[10px]',
+ '2xl': 'h-[11px] min-w-[11px] text-[11px]',
+ '3xl': 'h-[12px] min-w-[12px] text-[12px]'
+ },
+ position: {
+ 'top-right': 'top-0 right-0',
+ 'bottom-right': 'bottom-0 right-0',
+ 'top-left': 'top-0 left-0',
+ 'bottom-left': 'bottom-0 left-0'
+ },
+ inset: {
+ false: ''
+ }
+ },
+ compoundVariants: [{
+ position: 'top-right',
+ inset: false,
+ class: '-translate-y-1/2 translate-x-1/2 transform'
+ }, {
+ position: 'bottom-right',
+ inset: false,
+ class: 'translate-y-1/2 translate-x-1/2 transform'
+ }, {
+ position: 'top-left',
+ inset: false,
+ class: '-translate-y-1/2 -translate-x-1/2 transform'
+ }, {
+ position: 'bottom-left',
+ inset: false,
+ class: 'translate-y-1/2 -translate-x-1/2 transform'
+ }],
+ defaultVariants: {
+ size: 'sm',
+ color: 'primary',
+ position: 'top-right'
+ }
+})
diff --git a/src/theme/collapsible.ts b/src/theme/collapsible.ts
new file mode 100644
index 00000000..2c029634
--- /dev/null
+++ b/src/theme/collapsible.ts
@@ -0,0 +1,6 @@
+export default {
+ slots: {
+ root: '',
+ content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-in-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-in-out] overflow-hidden'
+ }
+}
diff --git a/src/theme/container.ts b/src/theme/container.ts
new file mode 100644
index 00000000..95c953e7
--- /dev/null
+++ b/src/theme/container.ts
@@ -0,0 +1,3 @@
+export default {
+ base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
+}
diff --git a/src/theme/form.ts b/src/theme/form.ts
new file mode 100644
index 00000000..1e31c0cb
--- /dev/null
+++ b/src/theme/form.ts
@@ -0,0 +1,3 @@
+export default {
+ base: ''
+}
diff --git a/src/theme/formField.ts b/src/theme/formField.ts
new file mode 100644
index 00000000..b28e8712
--- /dev/null
+++ b/src/theme/formField.ts
@@ -0,0 +1,32 @@
+export default {
+ slots: {
+ root: '',
+ wrapper: '',
+ labelWrapper: 'flex content-center items-center justify-between',
+ label: 'block font-medium text-gray-700 dark:text-gray-200',
+ container: 'mt-1 relative',
+ description: 'text-gray-500 dark:text-gray-400',
+ error: 'mt-2 text-red-500 dark:text-red-400',
+ hint: 'text-gray-500 dark:text-gray-400',
+ help: 'mt-2 text-gray-500 dark:text-gray-400'
+ },
+ variants: {
+ size: {
+ '2xs': { root: 'text-xs' },
+ xs: { root: 'text-xs' },
+ sm: { root: 'text-sm' },
+ md: { root: 'text-sm' },
+ lg: { root: 'text-base' },
+ xl: { root: 'text-base' }
+ },
+ required: {
+ true: {
+ // eslint-disable-next-line quotes
+ label: `after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400`
+ }
+ }
+ },
+ defaultVariants: {
+ size: 'sm'
+ }
+}
diff --git a/src/theme/icons.ts b/src/theme/icons.ts
new file mode 100644
index 00000000..6e9290f8
--- /dev/null
+++ b/src/theme/icons.ts
@@ -0,0 +1,11 @@
+export default {
+ chevronDown: 'i-heroicons-chevron-down-20-solid',
+ chevronLeft: 'i-heroicons-chevron-left-20-solid',
+ chevronRight: 'i-heroicons-chevron-right-20-solid',
+ check: 'i-heroicons-check-20-solid',
+ close: 'i-heroicons-x-mark-20-solid',
+ empty: 'i-heroicons-circle-stack-20-solid',
+ loading: 'i-heroicons-arrow-path-20-solid',
+ minus: 'i-heroicons-minus-20-solid',
+ search: 'i-heroicons-magnifying-glass-20-solid'
+}
diff --git a/src/theme/index.ts b/src/theme/index.ts
new file mode 100644
index 00000000..44cdc181
--- /dev/null
+++ b/src/theme/index.ts
@@ -0,0 +1,24 @@
+export { default as accordion } from './accordion'
+export { default as avatar } from './avatar'
+export { default as badge } from './badge'
+export { default as button } from './button'
+export { default as card } from './card'
+export { default as checkbox } from './checkbox'
+export { default as chip } from './chip'
+export { default as collapsible } from './collapsible'
+export { default as container } from './container'
+export { default as form } from './form'
+export { default as formField } from './formField'
+export { default as icons } from './icons'
+export { default as input } from './input'
+export { default as kbd } from './kbd'
+export { default as link } from './link'
+export { default as modal } from './modal'
+export { default as navigationMenu } from './navigationMenu'
+export { default as popover } from './popover'
+export { default as skeleton } from './skeleton'
+export { default as slideover } from './slideover'
+export { default as switch } from './switch'
+export { default as tabs } from './tabs'
+export { default as tooltip } from './tooltip'
+export { default as textarea } from './textarea'
diff --git a/src/theme/input.ts b/src/theme/input.ts
new file mode 100644
index 00000000..a1d9ca46
--- /dev/null
+++ b/src/theme/input.ts
@@ -0,0 +1,155 @@
+export default (config: { colors: string[] }) => {
+ return {
+ slots: {
+ root: 'relative',
+ base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500',
+ leading: 'absolute inset-y-0 start-0 flex items-center',
+ leadingIcon: 'shrink-0 text-gray-400 dark:text-gray-500',
+ trailing: 'absolute inset-y-0 end-0 flex items-center',
+ trailingIcon: 'shrink-0 text-gray-400 dark:text-gray-500'
+ },
+ variants: {
+ size: {
+ '2xs': {
+ base: 'text-xs gap-x-1 px-2 py-1',
+ leading: 'px-2',
+ trailing: 'px-2',
+ leadingIcon: 'size-4',
+ trailingIcon: 'size-4'
+ },
+ xs: {
+ base: 'text-sm gap-x-1.5 px-2.5 py-1',
+ leading: 'px-2.5',
+ trailing: 'px-2.5',
+ leadingIcon: 'size-4',
+ trailingIcon: 'size-4'
+ },
+ sm: {
+ base: 'text-sm gap-x-1.5 px-2.5 py-1.5',
+ leading: 'px-2.5',
+ trailing: 'px-2.5',
+ leadingIcon: 'size-5',
+ trailingIcon: 'size-5'
+ },
+ md: {
+ base: 'text-sm gap-x-1.5 px-3 py-2',
+ leading: 'px-3',
+ trailing: 'px-3',
+ leadingIcon: 'size-5',
+ trailingIcon: 'size-5'
+ },
+ lg: {
+ base: 'text-sm gap-x-2.5 px-3.5 py-2.5',
+ leading: 'px-3.5',
+ trailing: 'px-3.5',
+ leadingIcon: 'size-5',
+ trailingIcon: 'size-5'
+ },
+ xl: {
+ base: 'text-base gap-x-2.5 px-3.5 py-2.5',
+ leading: 'px-3.5',
+ trailing: 'px-3.5',
+ leadingIcon: 'size-6',
+ trailingIcon: 'size-6'
+ }
+ },
+ variant: {
+ outline: '',
+ none: 'bg-transparent focus:ring-0 focus:shadow-none'
+ },
+ color: {
+ ...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
+ white: '',
+ gray: ''
+ },
+ leading: {
+ true: ''
+ },
+ trailing: {
+ true: ''
+ },
+ loading: {
+ true: ''
+ }
+ },
+ compoundVariants: [...config.colors.map((color: string) => ({
+ color,
+ variant: 'outline',
+ class: `shadow-sm bg-transparent text-gray-900 dark:text-white ring ring-inset ring-${color}-500 dark:ring-${color}-400 focus:ring-2 focus:ring-${color}-500 dark:focus:ring-${color}-400`
+ })), {
+ color: 'white',
+ variant: 'outline',
+ class: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
+ }, {
+ color: 'gray',
+ variant: 'outline',
+ class: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
+ }, {
+ leading: true,
+ size: '2xs',
+ class: 'ps-7'
+ }, {
+ leading: true,
+ size: 'xs',
+ class: 'ps-8'
+ }, {
+ leading: true,
+ size: 'sm',
+ class: 'ps-9'
+ }, {
+ leading: true,
+ size: 'md',
+ class: 'ps-10'
+ }, {
+ leading: true,
+ size: 'lg',
+ class: 'ps-11'
+ }, {
+ leading: true,
+ size: 'xl',
+ class: 'ps-12'
+ }, {
+ trailing: true,
+ size: '2xs',
+ class: 'pe-7'
+ }, {
+ trailing: true,
+ size: 'xs',
+ class: 'pe-8'
+ }, {
+ trailing: true,
+ size: 'sm',
+ class: 'pe-9'
+ }, {
+ trailing: true,
+ size: 'md',
+ class: 'pe-10'
+ }, {
+ trailing: true,
+ size: 'lg',
+ class: 'pe-11'
+ }, {
+ trailing: true,
+ size: 'xl',
+ class: 'pe-12'
+ }, {
+ loading: true,
+ leading: true,
+ class: {
+ leadingIcon: 'animate-spin'
+ }
+ }, {
+ loading: true,
+ leading: false,
+ trailing: true,
+ class: {
+ trailingIcon: 'animate-spin'
+ }
+ }],
+ defaultVariants: {
+ size: 'sm',
+ color: 'white',
+ variant: 'outline'
+ }
+ }
+}
diff --git a/src/theme/kbd.ts b/src/theme/kbd.ts
new file mode 100644
index 00000000..706a379e
--- /dev/null
+++ b/src/theme/kbd.ts
@@ -0,0 +1,13 @@
+export default {
+ base: 'inline-flex items-center justify-center text-gray-900 dark:text-white px-1 rounded font-medium font-sans bg-gray-50 dark:bg-gray-800 ring ring-gray-300 dark:ring-gray-700 ring-inset',
+ variants: {
+ size: {
+ xs: 'h-4 min-w-[16px] text-[10px]',
+ sm: 'h-5 min-w-[20px] text-[11px]',
+ md: 'h-6 min-w-[24px] text-[12px]'
+ }
+ },
+ defaultVariants: {
+ size: 'sm'
+ }
+}
diff --git a/src/theme/link.ts b/src/theme/link.ts
new file mode 100644
index 00000000..25255262
--- /dev/null
+++ b/src/theme/link.ts
@@ -0,0 +1,12 @@
+export default {
+ base: 'focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400',
+ variants: {
+ active: {
+ true: 'text-primary-500 dark:text-primary-400',
+ false: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
+ },
+ disabled: {
+ true: 'cursor-not-allowed opacity-75'
+ }
+ }
+}
diff --git a/src/theme/modal.ts b/src/theme/modal.ts
new file mode 100644
index 00000000..c1feafdb
--- /dev/null
+++ b/src/theme/modal.ts
@@ -0,0 +1,28 @@
+export default {
+ slots: {
+ overlay: 'fixed inset-0 z-30 bg-gray-200/75 dark:bg-gray-800/75',
+ content: 'fixed z-50 w-full h-dvh bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800 flex flex-col focus:outline-none',
+ header: 'px-4 py-5 sm:px-6',
+ body: 'flex-1 p-4 sm:p-6',
+ footer: 'flex items-center gap-x-1.5 p-4 sm:px-6',
+ title: 'text-gray-900 dark:text-white font-semibold',
+ description: 'mt-1 text-gray-500 dark:text-gray-400 text-sm',
+ close: 'absolute top-4 right-4'
+ },
+ variants: {
+ transition: {
+ true: {
+ overlay: 'data-[state=open]:animate-[modal-overlay-open_200ms_ease-out] data-[state=closed]:animate-[modal-overlay-closed_200ms_ease-in]',
+ content: 'data-[state=open]:animate-[modal-content-open_200ms_ease-out] data-[state=closed]:animate-[modal-content-closed_200ms_ease-in]'
+ }
+ },
+ fullscreen: {
+ true: {
+ content: 'inset-0'
+ },
+ false: {
+ content: 'top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] sm:max-w-lg sm:h-auto sm:my-8 sm:rounded-lg sm:shadow-lg sm:ring ring-gray-200 dark:ring-gray-800'
+ }
+ }
+ }
+}
diff --git a/src/theme/navigationMenu.ts b/src/theme/navigationMenu.ts
new file mode 100644
index 00000000..510a18cb
--- /dev/null
+++ b/src/theme/navigationMenu.ts
@@ -0,0 +1,49 @@
+export default {
+ slots: {
+ root: 'relative',
+ list: '',
+ item: '',
+ base: 'group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary-500 dark:focus-visible:before:ring-primary-400 disabled:cursor-not-allowed disabled:opacity-75',
+ icon: 'shrink-0 w-5 h-5 relative',
+ avatar: 'shrink-0 relative',
+ label: 'truncate relative',
+ badge: 'shrink-0 ms-auto relative rounded'
+ },
+ variants: {
+ orientation: {
+ horizontal: {
+ root: 'w-full flex items-center justify-between',
+ list: 'flex items-center min-w-0',
+ item: 'min-w-0',
+ base: 'px-2.5 py-3.5 before:inset-x-0 before:inset-y-2 hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 after:absolute after:bottom-0 after:inset-x-2.5 after:block after:h-[2px] after:mt-2 after:rounded-full'
+ },
+ vertical: {
+ root: 'flex flex-col *:py-1.5 first:*:pt-0 last:*:pb-0 divide-y divide-gray-200 dark:divide-gray-800',
+ base: 'px-2.5 py-1.5 before:inset-px'
+ }
+ },
+ active: {
+ true: {
+ base: 'text-gray-900 dark:text-white',
+ icon: 'text-gray-700 dark:text-gray-200'
+ },
+ false: {
+ base: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white',
+ icon: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-200'
+ }
+ }
+ },
+ compoundVariants: [{
+ orientation: 'horizontal',
+ active: true,
+ class: 'after:bg-primary-500 dark:after:bg-primary-400'
+ }, {
+ orientation: 'vertical',
+ active: true,
+ class: 'before:bg-gray-100 dark:before:bg-gray-800'
+ }, {
+ orientation: 'vertical',
+ active: false,
+ class: 'hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50'
+ }]
+}
diff --git a/src/theme/popover.ts b/src/theme/popover.ts
new file mode 100644
index 00000000..a2480d0e
--- /dev/null
+++ b/src/theme/popover.ts
@@ -0,0 +1,6 @@
+export default {
+ slots: {
+ content: 'bg-white dark:bg-gray-900 shadow-lg rounded-md ring ring-gray-200 dark:ring-gray-800 will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-[popover-down-open_200ms_ease-out] data-[state=closed]:data-[side=top]:animate-[popover-down-closed_200ms_ease-in] data-[state=open]:data-[side=right]:animate-[popover-left-open_200ms_ease-out] data-[state=closed]:data-[side=right]:animate-[popover-left-closed_200ms_ease-in] data-[state=open]:data-[side=left]:animate-[popover-right-open_200ms_ease-out] data-[state=closed]:data-[side=left]:animate-[popover-right-closed_200ms_ease-in] data-[state=open]:data-[side=bottom]:animate-[popover-up-open_200ms_ease-out] data-[state=closed]:data-[side=bottom]:animate-[popover-up-closed_200ms_ease-in]',
+ arrow: 'fill-gray-200 dark:fill-gray-800'
+ }
+}
diff --git a/src/theme/skeleton.ts b/src/theme/skeleton.ts
new file mode 100644
index 00000000..db36cfb1
--- /dev/null
+++ b/src/theme/skeleton.ts
@@ -0,0 +1,3 @@
+export default {
+ base: 'animate-pulse rounded-md bg-gray-100 dark:bg-gray-800'
+}
diff --git a/src/theme/slideover.ts b/src/theme/slideover.ts
new file mode 100644
index 00000000..9427ded5
--- /dev/null
+++ b/src/theme/slideover.ts
@@ -0,0 +1,34 @@
+export default {
+ slots: {
+ overlay: 'fixed inset-0 z-30 bg-gray-200/75 dark:bg-gray-800/75',
+ content: 'fixed z-50 bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800 sm:ring ring-gray-200 dark:ring-gray-800 sm:shadow-lg flex flex-col focus:outline-none',
+ header: 'px-4 py-5 sm:px-6',
+ body: 'flex-1 overflow-y-auto p-4 sm:p-6',
+ footer: 'flex items-center gap-x-1.5 p-4 sm:px-6',
+ title: 'text-gray-900 dark:text-white font-semibold',
+ description: 'mt-1 text-gray-500 dark:text-gray-400 text-sm',
+ close: 'absolute top-4 right-4'
+ },
+ variants: {
+ side: {
+ left: {
+ content: 'left-0 inset-y-0 w-full max-w-md'
+ },
+ right: {
+ content: 'right-0 inset-y-0 w-full max-w-md'
+ },
+ top: {
+ content: 'inset-x-0 top-0'
+ },
+ bottom: {
+ content: 'inset-x-0 bottom-0'
+ }
+ },
+ transition: {
+ true: {
+ overlay: 'data-[state=open]:animate-[slideover-overlay-open_200ms_ease-out] data-[state=closed]:animate-[slideover-overlay-closed_200ms_ease-in]',
+ content: 'data-[state=open]:data-[side=left]:animate-[slideover-content-left-open_200ms_ease-in-out] data-[state=closed]:data-[side=left]:animate-[slideover-content-left-closed_200ms_ease-in-out] data-[state=open]:data-[side=right]:animate-[slideover-content-right-open_200ms_ease-in-out] data-[state=closed]:data-[side=right]:animate-[slideover-content-right-closed_200ms_ease-in-out] data-[state=open]:data-[side=top]:animate-[slideover-content-top-open_200ms_ease-in-out] data-[state=closed]:data-[side=top]:animate-[slideover-content-top-closed_200ms_ease-in-out] data-[state=open]:data-[side=bottom]:animate-[slideover-content-bottom-open_200ms_ease-in-out] data-[state=closed]:data-[side=bottom]:animate-[slideover-content-bottom-closed_200ms_ease-in-out]'
+ }
+ }
+ }
+}
diff --git a/src/theme/switch.ts b/src/theme/switch.ts
new file mode 100644
index 00000000..071ed59a
--- /dev/null
+++ b/src/theme/switch.ts
@@ -0,0 +1,64 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ root: 'peer inline-flex shrink-0 items-center rounded-full border-2 border-transparent transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 disabled:cursor-not-allowed disabled:opacity-75 data-[state=unchecked]:bg-gray-200 dark:data-[state=unchecked]:bg-gray-700',
+ thumb: 'group pointer-events-none block rounded-full bg-white dark:bg-gray-900 shadow-lg ring-0 transition-transform duration-200 data-[state=unchecked]:translate-x-0 flex items-center justify-center',
+ icon: 'absolute shrink-0 group-data-[state=unchecked]:text-gray-400 dark:group-data-[state=unchecked]:text-gray-500 transition-[color,opacity] duration-200 opacity-0'
+ },
+ variants: {
+ color: Object.fromEntries(config.colors.map((color: string) => [color, {
+ root: `data-[state=checked]:bg-${color}-500 dark:data-[state=checked]:bg-${color}-400 focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`,
+ icon: `group-data-[state=checked]:text-${color}-500 dark:group-data-[state=checked]:text-${color}-400`
+ }])),
+ size: {
+ '2xs': {
+ root: 'h-3 w-5',
+ thumb: 'size-2 data-[state=checked]:translate-x-2',
+ icon: 'size-1'
+ },
+ xs: {
+ root: 'h-4 w-7',
+ thumb: 'size-3 data-[state=checked]:translate-x-3',
+ icon: 'size-2'
+ },
+ sm: {
+ root: 'h-5 w-9',
+ thumb: 'size-4 data-[state=checked]:translate-x-4',
+ icon: 'size-3'
+ },
+ md: {
+ root: 'h-6 w-11',
+ thumb: 'size-5 data-[state=checked]:translate-x-5',
+ icon: 'size-4'
+ },
+ lg: {
+ root: 'h-7 w-[52px]',
+ thumb: 'size-6 data-[state=checked]:translate-x-6',
+ icon: 'size-5'
+ },
+ xl: {
+ root: 'h-8 w-[60px]',
+ thumb: 'size-7 data-[state=checked]:translate-x-7',
+ icon: 'size-6'
+ }
+ },
+ checked: {
+ true: {
+ icon: 'group-data-[state=checked]:opacity-100'
+ }
+ },
+ unchecked: {
+ true: {
+ icon: 'group-data-[state=unchecked]:opacity-100'
+ }
+ },
+ loading: {
+ true: {
+ icon: 'animate-spin'
+ }
+ }
+ },
+ defaultVariants: {
+ color: 'primary',
+ size: 'sm'
+ }
+})
diff --git a/src/theme/tabs.ts b/src/theme/tabs.ts
new file mode 100644
index 00000000..bf3721e4
--- /dev/null
+++ b/src/theme/tabs.ts
@@ -0,0 +1,11 @@
+export default {
+ slots: {
+ root: 'flex data-[orientation=horizontal]:flex-col items-center gap-2',
+ list: 'relative w-full flex data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-center justify-center rounded-lg bg-gray-50 dark:bg-gray-800 data-[orientation=horizontal]:h-10 p-1 group',
+ // FIXME: Replace transition with `transition-[width,transform]` when available
+ indicator: 'absolute group-data-[orientation=horizontal]:left-0 group-data-[orientation=vertical]:top-0 group-data-[orientation=horizontal]:inset-y-1 group-data-[orientation=vertical]:inset-x-1 group-data-[orientation=horizontal]:w-[--radix-tabs-indicator-size] group-data-[orientation=vertical]:h-[--radix-tabs-indicator-size] group-data-[orientation=horizontal]:translate-x-[--radix-tabs-indicator-position] group-data-[orientation=vertical]:translate-y-[--radix-tabs-indicator-position] transition-transform duration-200 bg-white dark:bg-gray-900 rounded-md shadow-sm',
+ trigger: 'relative inline-flex items-center justify-center shrink-0 w-full h-8 text-gray-500 data-[state=active]:text-gray-900 dark:text-gray-400 dark:data-[state=active]:text-white px-3 text-sm font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75 transition-colors duration-200 ease-out focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:outline-0',
+ content: 'focus:outline-none',
+ label: 'truncate'
+ }
+}
diff --git a/src/theme/textarea.ts b/src/theme/textarea.ts
new file mode 100644
index 00000000..160ff4e1
--- /dev/null
+++ b/src/theme/textarea.ts
@@ -0,0 +1,63 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ root: 'relative',
+ base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500'
+ },
+
+ variants: {
+ size: {
+ '2xs': {
+ base: 'text-xs gap-x-1 px-2 py-1'
+ },
+ xs: {
+ base: 'text-sm gap-x-1.5 px-2.5 py-1'
+ },
+ sm: {
+ base: 'text-sm gap-x-1.5 px-2.5 py-1.5'
+ },
+ md: {
+ base: 'text-sm gap-x-1.5 px-3 py-2'
+ },
+ lg: {
+ base: 'text-sm gap-x-2.5 px-3.5 py-2.5'
+ },
+ xl: {
+ base: 'text-base gap-x-2.5 px-3.5 py-2.5'
+ }
+ },
+
+ variant: {
+ outline: '',
+ none: 'bg-transparent focus:ring-0 focus:shadow-none'
+ },
+
+ color: {
+ ...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
+ white: '',
+ gray: ''
+ }
+
+ },
+
+ compoundVariants: [
+ ...config.colors.map((color: string) => ({
+ color,
+ variant: 'outline',
+ class: `shadow-sm bg-transparent text-gray-900 dark:text-white ring ring-inset ring-${color}-500 dark:ring-${color}-400 focus:ring-2 focus:ring-${color}-500 dark:focus:ring-${color}-400`
+ })), {
+ color: 'white',
+ variant: 'outline',
+ class: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
+ }, {
+ color: 'gray',
+ variant: 'outline',
+ class: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
+ }
+ ],
+
+ defaultVariants: {
+ size: 'sm',
+ color: 'white',
+ variant: 'outline'
+ }
+})
diff --git a/src/theme/tooltip.ts b/src/theme/tooltip.ts
new file mode 100644
index 00000000..49cc492e
--- /dev/null
+++ b/src/theme/tooltip.ts
@@ -0,0 +1,9 @@
+export default {
+ slots: {
+ content: 'flex items-center gap-1 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow rounded ring ring-gray-200 dark:ring-gray-800 h-6 px-2 py-1 text-xs select-none will-change-[transform,opacity] data-[state=delayed-open]:data-[side=top]:animate-[tooltip-down_200ms_ease-out] data-[state=delayed-open]:data-[side=right]:animate-[tooltip-left_200ms_ease-out] data-[state=delayed-open]:data-[side=left]:animate-[tooltip-right_200ms_ease-out] data-[state=delayed-open]:data-[side=bottom]:animate-[tooltip-up_200ms_ease-out]',
+ arrow: 'fill-gray-200 dark:fill-gray-800',
+ text: 'truncate',
+ // eslint-disable-next-line quotes
+ shortcuts: `hidden lg:inline-flex items-center shrink-0 gap-0.5 before:content-['ยท'] before:mr-0.5`
+ }
+}
diff --git a/test/component-render.ts b/test/component-render.ts
new file mode 100644
index 00000000..ab1ec2be
--- /dev/null
+++ b/test/component-render.ts
@@ -0,0 +1,19 @@
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+import path from 'path'
+
+export default async function (nameOrHtml: string, options: any, component: any) {
+ let html: string
+ const name = component.__file ? path.parse(component.__file).name : undefined
+ if (options === undefined) {
+ const app = {
+ template: nameOrHtml,
+ components: { [`U${name}`]: component }
+ }
+ const result = await mountSuspended(app)
+ html = result.html()
+ } else {
+ const cResult = await mountSuspended(component, options)
+ html = cResult.html()
+ }
+ return html
+}
diff --git a/test/components/Accordion.spec.ts b/test/components/Accordion.spec.ts
new file mode 100644
index 00000000..acfc0c76
--- /dev/null
+++ b/test/components/Accordion.spec.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest'
+import Accordion, { type AccordionProps } from '../../src/runtime/components/Accordion.vue'
+import ComponentRender from '../component-render'
+
+const items = [{
+ label: 'Getting Started',
+ icon: 'i-heroicons-information-circle',
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
+}, {
+ label: 'Installation',
+ icon: 'i-heroicons-arrow-down-tray',
+ disabled: true,
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
+}, {
+ label: 'Theming',
+ icon: 'i-heroicons-eye-dropper',
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
+}, {
+ label: 'Layouts',
+ icon: 'i-heroicons-rectangle-group',
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
+}, {
+ label: 'Components',
+ icon: 'i-heroicons-square-3-stack-3d',
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
+}, {
+ label: 'Utilities',
+ slot: 'toto',
+ icon: 'i-heroicons-wrench-screwdriver',
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
+}]
+
+describe('Accordion', () => {
+ it.each([
+ ['basic case', { props: { items } }],
+ ['with class', { props: { items, class: 'w-96' } }],
+ ['with ui', { props: { ui: { items, item: 'border-gray-300 dark:border-gray-700' } } }],
+ ['with as', { props: { items, as: 'section' } }],
+ ['with type', { props: { items, type: 'multiple' as const } }],
+ ['with disabled', { props: { items, disabled: true } }],
+ ['with collapsible', { props: { items, collapsible: false } }],
+ ['with modelValue', { props: { items, modelValue: '1' } }],
+ ['with defaultValue', { props: { items, defaultValue: '1' } }],
+ ['with default slot', { props: { items }, slots: { default: () => 'Default slot' } }],
+ ['with content slot', { props: { items }, slots: { content: () => 'Content slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: AccordionProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Accordion)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Avatar.spec.ts b/test/components/Avatar.spec.ts
new file mode 100644
index 00000000..c493c9d7
--- /dev/null
+++ b/test/components/Avatar.spec.ts
@@ -0,0 +1,26 @@
+import { describe, it, expect } from 'vitest'
+import Avatar, { type AvatarProps } from '../../src/runtime/components/Avatar.vue'
+import ComponentRender from '../component-render'
+
+describe('Avatar', () => {
+ it.each([
+ ['with src', { props: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' } }],
+ ['with alt', { props: { alt: 'Benjamin Canac' } }],
+ ['with class', { props: { class: 'bg-white dark:bg-gray-900' } }],
+ ['with text', { props: { text: '+1' } }],
+ ['with icon', { props: { icon: 'i-heroicons-photo' } }],
+ ['with size 3xs', { props: { size: '3xs' as const } }],
+ ['with size 2xs', { props: { size: '2xs' as const } }],
+ ['with size xs', { props: { size: 'xs' as const } }],
+ ['with size sm', { props: { size: 'sm' as const } }],
+ ['with size md', { props: { size: 'md' as const } }],
+ ['with size lg', { props: { size: 'lg' as const } }],
+ ['with size xl', { props: { size: 'xl' as const } }],
+ ['with size 2xl', { props: { size: '2xl' as const } }],
+ ['with size 3xl', { props: { size: '3xl' as const } }],
+ ['with ui', { props: { ui: { fallback: 'font-bold' } } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: AvatarProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Avatar)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Badge.spec.ts b/test/components/Badge.spec.ts
new file mode 100644
index 00000000..727de921
--- /dev/null
+++ b/test/components/Badge.spec.ts
@@ -0,0 +1,26 @@
+import { describe, it, expect } from 'vitest'
+import Badge, { type BadgeProps } from '../../src/runtime/components/Badge.vue'
+import ComponentRender from '../component-render'
+
+describe('Badge', () => {
+ it.each([
+ ['with label', { props: { label: 'Badge' } }],
+ ['with as', { props: { label: 'Badge', as: 'div' } }],
+ ['with class', { props: { label: 'Badge', class: 'rounded-full font-bold' } }],
+ ['with size xs', { props: { label: 'Badge', size: 'xs' as const } }],
+ ['with size sm', { props: { label: 'Badge', size: 'sm' as const } }],
+ ['with size md', { props: { label: 'Badge', size: 'md' as const } }],
+ ['with size lg', { props: { label: 'Badge', size: 'lg' as const } }],
+ ['with color green', { props: { label: 'Badge', color: 'green' as const } }],
+ ['with color white', { props: { label: 'Badge', color: 'white' as const } }],
+ ['with color gray', { props: { label: 'Badge', color: 'gray' as const } }],
+ ['with color black', { props: { label: 'Badge', color: 'black' as const } }],
+ ['with variant outline', { props: { label: 'Badge', variant: 'outline' as const } }],
+ ['with variant soft', { props: { label: 'Badge', variant: 'soft' as const } }],
+ ['with variant link', { props: { label: 'Badge', variant: 'subtle' as const } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: BadgeProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Badge)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Button.spec.ts b/test/components/Button.spec.ts
new file mode 100644
index 00000000..858bff82
--- /dev/null
+++ b/test/components/Button.spec.ts
@@ -0,0 +1,38 @@
+import { describe, it, expect } from 'vitest'
+import Button, { type ButtonProps } from '../../src/runtime/components/Button.vue'
+import ComponentRender from '../component-render'
+
+describe('Button', () => {
+ it.each([
+ ['with label', { props: { label: 'Button' } }],
+ ['with class', { props: { class: 'rounded-full font-bold' } }],
+ ['with size 2xs', { props: { label: 'Button', size: '2xs' as const } }],
+ ['with size xs', { props: { label: 'Button', size: 'xs' as const } }],
+ ['with size sm', { props: { label: 'Button', size: 'sm' as const } }],
+ ['with size md', { props: { label: 'Button', size: 'md' as const } }],
+ ['with size lg', { props: { label: 'Button', size: 'lg' as const } }],
+ ['with size xl', { props: { label: 'Button', size: 'xl' as const } }],
+ ['with color', { props: { label: 'Button', color: 'red' as const } }],
+ ['with variant outline', { props: { label: 'Button', variant: 'outline' as const } }],
+ ['with variant soft', { props: { label: 'Button', variant: 'soft' as const } }],
+ ['with variant ghost', { props: { label: 'Button', variant: 'ghost' as const } }],
+ ['with variant link', { props: { label: 'Button', variant: 'link' as const } }],
+ ['with icon', { props: { icon: 'i-heroicons-rocket-launch' } }],
+ ['with leading and icon', { props: { leading: true, icon: 'i-heroicons-arrow-left' } }],
+ ['with leadingIcon', { props: { leadingIcon: 'i-heroicons-arrow-left' } }],
+ ['with trailing and icon', { props: { trailing: true, icon: 'i-heroicons-arrow-right' } }],
+ ['with trailingIcon', { props: { trailingIcon: 'i-heroicons-arrow-right' } }],
+ ['with loading', { props: { loading: true } }],
+ ['with loadingIcon', { props: { loading: true, loadingIcon: 'i-heroicons-sparkles' } }],
+ ['with disabled', { props: { label: 'Button', disabled: true } }],
+ ['with block', { props: { label: 'Button', block: true } }],
+ ['with square', { props: { label: 'Button', square: true } }],
+ ['with truncate', { props: { label: 'Button', truncate: true } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }],
+ ['with leading slot', { slots: { leading: () => 'Leading slot' } }],
+ ['with trailing slot', { slots: { trailing: () => 'Trailing slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ButtonProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Button)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Card.spec.ts b/test/components/Card.spec.ts
new file mode 100644
index 00000000..52d0b46f
--- /dev/null
+++ b/test/components/Card.spec.ts
@@ -0,0 +1,17 @@
+import { describe, it, expect } from 'vitest'
+import Card, { type CardProps } from '../../src/runtime/components/Card.vue'
+import ComponentRender from '../component-render'
+
+describe('Card', () => {
+ it.each([
+ ['basic case', {}],
+ ['with as', { props: { as: 'section' } }],
+ ['with class', { props: { class: 'rounded-xl' } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }],
+ ['with header slot', { slots: { header: () => 'Header slot' } }],
+ ['with footer slot', { slots: { footer: () => 'Footer slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: CardProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Card)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Checkbox.spec.ts b/test/components/Checkbox.spec.ts
new file mode 100644
index 00000000..0bcc7333
--- /dev/null
+++ b/test/components/Checkbox.spec.ts
@@ -0,0 +1,32 @@
+import { describe, it, expect } from 'vitest'
+import { UCheckbox } from '#components'
+import type { TypeOf } from 'zod'
+import ComponentRender from '../component-render'
+import { defu } from 'defu'
+
+describe('Checkbox', () => {
+ it.each([
+ ['basic case', {}],
+ ['with label', { props: { label: 'Label' } }],
+ ['with slot', { slots: { default: () => 'Label slot' } }],
+ ['with slot label', { slots: { label: () => 'Label slot' } }],
+ ['with custom id', { props: { id: 'custom-id' } }],
+ ['with custom value', { props: { value: 'custom-value' } }],
+ ['with custom name', { props: { name: 'custom-name' } }],
+ ['with disabled', { props: { disabled: true } }],
+ ['with indeterminate', { props: { indeterminate: true } }],
+ ['with help', { props: { label: 'Label', help: 'Help' } }],
+ ['with required', { props: { label: 'Label', required: true } }],
+ ['with custom color', { props: { label: 'Label', color: 'red' } }],
+ ['with size 2xs', { props: { size: '2xs' as const } }],
+ ['with size xs', { props: { size: 'xs' as const } }],
+ ['with size sm', { props: { size: 'sm' as const } }],
+ ['with size md', { props: { size: 'md' as const } }],
+ ['with size lg', { props: { size: 'lg' as const } }],
+ ['with size xl', { props: { size: 'xl' as const } }]
+ // @ts-ignore
+ ])('renders %s correctly', async (nameOrHtml: string, options: TypeOf) => {
+ const html = await ComponentRender(nameOrHtml, defu(options, { props: { id: 42 } }), UCheckbox)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Chip.spec.ts b/test/components/Chip.spec.ts
new file mode 100644
index 00000000..05f425d4
--- /dev/null
+++ b/test/components/Chip.spec.ts
@@ -0,0 +1,36 @@
+import { describe, it, expect } from 'vitest'
+import Chip, { type ChipProps } from '../../src/runtime/components/Chip.vue'
+import ComponentRender from '../component-render'
+
+describe('Chip', () => {
+ it.each([
+ ['basic case', {}],
+ ['with as', { props: { as: 'span' } }],
+ ['with class', { props: { class: 'mx-auto' } }],
+ ['with ui', { props: { ui: { base: 'text-gray-500 dark:text-gray-400' } } }],
+ ['with show', { props: { show: true } }],
+ ['with inset', { props: { inset: true } }],
+ ['with position top-right', { props: { show: true, position: 'top-right' as const } }],
+ ['with position bottom-right', { props: { show: true, position: 'bottom-right' as const } }],
+ ['with position top-left', { props: { show: true, position: 'top-left' as const } }],
+ ['with position bottom-left', { props: { show: true, position: 'bottom-left' as const } }],
+ ['with size 3xs', { props: { show: true, size: '3xs' as const } }],
+ ['with size 2xs', { props: { show: true, size: '2xs' as const } }],
+ ['with size xs', { props: { show: true, size: 'xs' as const } }],
+ ['with size sm', { props: { show: true, size: 'sm' as const } }],
+ ['with size md', { props: { show: true, size: 'md' as const } }],
+ ['with size lg', { props: { show: true, size: 'lg' as const } }],
+ ['with size xl', { props: { show: true, size: 'xl' as const } }],
+ ['with size 2xl', { props: { show: true, size: '2xl' as const } }],
+ ['with size 3xl', { props: { show: true, size: '3xl' as const } }],
+ ['with color green', { props: { show: true, color: 'green' as const } }],
+ ['with color white', { props: { show: true, color: 'white' as const } }],
+ ['with color gray', { props: { show: true, color: 'gray' as const } }],
+ ['with color black', { props: { show: true, color: 'black' as const } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }],
+ ['with content slot', { slots: { content: () => 'Content slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ChipProps & { show?: boolean }, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Chip)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Collapsible.spec.ts b/test/components/Collapsible.spec.ts
new file mode 100644
index 00000000..3d456308
--- /dev/null
+++ b/test/components/Collapsible.spec.ts
@@ -0,0 +1,12 @@
+import { describe, it, expect } from 'vitest'
+import Collapsible, { type CollapsibleProps } from '../../src/runtime/components/Collapsible.vue'
+import ComponentRender from '../component-render'
+
+describe('Collapsible', () => {
+ it.each([
+ ['basic case', { props: { open: true }, slots: { default: () => 'Click me', content: () => 'Collapsible content' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: CollapsibleProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Collapsible)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Container.spec.ts b/test/components/Container.spec.ts
new file mode 100644
index 00000000..c6adda9b
--- /dev/null
+++ b/test/components/Container.spec.ts
@@ -0,0 +1,15 @@
+import { describe, it, expect } from 'vitest'
+import Container, { type ContainerProps } from '../../src/runtime/components/Container.vue'
+import ComponentRender from '../component-render'
+
+describe('Container', () => {
+ it.each([
+ ['basic case', {}],
+ ['with as', { props: { as: 'article' } }],
+ ['with class', { props: { class: 'max-w-5xl' } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ContainerProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Container)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Form.spec.ts b/test/components/Form.spec.ts
new file mode 100644
index 00000000..5c910869
--- /dev/null
+++ b/test/components/Form.spec.ts
@@ -0,0 +1,680 @@
+import { reactive, ref, nextTick } from 'vue'
+import { describe, it, expect, test, beforeEach, vi } from 'vitest'
+import type { FormProps } from '../../src/runtime/components/Form.vue'
+import {
+ UForm,
+ UInput,
+ UFormField
+ // URadioGroup,
+ // UTextarea,
+ // UCheckbox,
+ // USelect,
+ // URadio,
+ // USelectMenu,
+ // UInputMenu,
+ // UToggle,
+ // URange
+} from '#components'
+import { DOMWrapper, flushPromises, VueWrapper } from '@vue/test-utils'
+import ComponentRender from '../component-render'
+
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+import { z } from 'zod'
+import * as yup from 'yup'
+import Joi from 'joi'
+import * as valibot from 'valibot'
+
+async function triggerEvent (
+ el: DOMWrapper | VueWrapper,
+ event: string
+) {
+ el.trigger(event)
+ return flushPromises()
+}
+
+async function setValue (
+ el: DOMWrapper | VueWrapper,
+ value: any
+) {
+ el.setValue(value)
+ return flushPromises()
+}
+
+async function renderForm (options: {
+ props: Partial>
+ slotVars?: object
+ slotComponents?: any
+ slotTemplate: string
+}) {
+ const state = reactive({})
+ return await mountSuspended(UForm, {
+ props: {
+ id: 42,
+ state,
+ ...options.props
+ },
+ slots: {
+ default: {
+ // @ts-ignore
+ setup () {
+ return { state, ...options.slotVars }
+ },
+ components: {
+ UFormField,
+ ...options.slotComponents
+ },
+ template: options.slotTemplate
+ }
+ }
+ })
+}
+
+describe('Form', () => {
+ it.each([
+ ['basic case', { props: { state: {} } }],
+ ['with default slot', { props: { state: {} }, slots: { default: 'Form slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props: FormProps }) => {
+ const html = await ComponentRender(nameOrHtml, options, UForm)
+ expect(html).toMatchSnapshot()
+ })
+
+ it.each([
+ ['zod', {
+ schema: z.object({
+ email: z.string(),
+ password: z.string().min(8, 'Must be at least 8 characters')
+ })
+ }
+ ],
+ ['yup', {
+ schema: yup.object({
+ email: yup.string(),
+ password: yup.string().min(8, 'Must be at least 8 characters')
+ })
+ }
+ ],
+ ['joi', {
+ schema: Joi.object({
+ email: Joi.string(),
+ password: Joi.string().min(8).messages({
+ 'string.min': 'Must be at least {#limit} characters'
+ })
+ })
+ }
+ ],
+ ['valibot', {
+ schema: valibot.objectAsync({
+ email: valibot.string(),
+ password: valibot.string([
+ valibot.minLength(8, 'Must be at least 8 characters')
+ ])
+ })
+ }
+ ],
+ ['custom', {
+ async validate (state: any) {
+ const errs = []
+ if (!state.email)
+ errs.push({ name: 'email', message: 'Email is required' })
+ if (state.password?.length < 8)
+ errs.push({
+ name: 'password',
+ message: 'Must be at least 8 characters'
+ })
+
+ return errs
+ }
+ }
+ ]
+ ])('%s validation works', async (_nameOrHtml: string, options: Partial>) => {
+ const wrapper = await renderForm({
+ props: options,
+ slotComponents: {
+ UFormField,
+ UInput
+ },
+ slotTemplate: `
+
+
+
+
+
+
+ `
+ })
+
+ const form = wrapper.find('form')
+ const emailInput = wrapper.find('#email')
+ const passwordInput = wrapper.find('#password')
+
+ await setValue(emailInput, 'bob@dylan.com')
+ await setValue(passwordInput, 'short')
+
+ await triggerEvent(form, 'submit.prevent')
+
+ // @ts-ignore
+ expect(wrapper.emitted('error')[0][0].errors).toMatchObject([
+ {
+ id: 'password',
+ name: 'password',
+ message: 'Must be at least 8 characters'
+ }
+ ])
+
+ expect(wrapper.html()).toMatchSnapshot('with error')
+
+ await setValue(passwordInput, 'validpassword')
+ await triggerEvent(form, 'submit.prevent')
+
+ expect(wrapper.emitted()).toHaveProperty('submit')
+ expect(wrapper.emitted('submit')![0][0]).toMatchObject({
+ data: { email: 'bob@dylan.com', password: 'validpassword' }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot('without error')
+ })
+
+ it.each([
+ ['input', UInput, {}, 'foo']
+ // ['textarea', UTextarea, {}, 'foo']
+ ])('%s validate on blur works', async (_name, InputComponent, inputProps, validInputValue) => {
+ const wrapper = await renderForm({
+ props: {
+ validateOn: ['blur'],
+ async validate (state: any) {
+ if (!state.value)
+ return [{ name: 'value', message: 'Error message' }]
+ return []
+ }
+ },
+
+ slotVars: {
+ inputProps
+ },
+ slotComponents: { InputComponent },
+ slotTemplate: `
+
+
+
+ `
+ })
+
+ const input = wrapper.find('#input')
+ await triggerEvent(input, 'blur')
+ expect(wrapper.text()).toContain('Error message')
+
+ await setValue(input, validInputValue)
+ await triggerEvent(input, 'blur')
+ expect(wrapper.text()).not.toContain('Error message')
+ })
+
+ it.each([
+ // ['checkbox', UCheckbox, {}, true],
+ // ['range', URange, {}, 2],
+ // ['select', USelect, { options: ['Option 1', 'Option 2'] }, 'Option 2']
+ ])('%s validate on change works', async (_name, InputComponent, inputProps, validInputValue) => {
+ const wrapper = await renderForm({
+ props: {
+ validateOn: ['change'],
+ async validate (state: any) {
+ if (!state.value)
+ return [{ name: 'value', message: 'Error message' }]
+ return []
+ }
+ },
+
+ slotVars: {
+ inputProps
+ },
+ slotComponents: {
+ InputComponent
+ },
+ slotTemplate: `
+
+
+
+ `
+ })
+
+ const input = wrapper.find('#input')
+
+ await triggerEvent(input, 'change')
+ expect(wrapper.text()).toContain('Error message')
+
+ await setValue(input, validInputValue)
+ await triggerEvent(input, 'change')
+ expect(wrapper.text()).not.toContain('Error message')
+ })
+
+ // test('radio group validate on change works', async () => {
+ // const wrapper = await renderForm({
+ // props: {
+ // validateOn: ['change'],
+ // validate (state: any) {
+ // if (state.value !== 'Option 2')
+ // return [{ name: 'value', message: 'Error message' }]
+ // return []
+ // }
+ // },
+ // slotVars: {
+ // inputProps: {
+ // options: ['Option 1', 'Option 2', 'Option 3']
+ // }
+ // },
+ // slotComponents: {
+ // UFormField,
+ // URadioGroup
+ // },
+ // slotTemplate: `
+ //
+ //
+ //
+ // `
+ // })
+ //
+ // const option1 = wrapper.find('[value="Option 1"]')
+ // await setValue(option1, true)
+ // expect(wrapper.text()).toContain('Error message')
+ //
+ // const option2 = wrapper.find('[value="Option 2"]')
+ // await setValue(option2, true)
+ // expect(wrapper.text()).not.toContain('Error message')
+ // })
+ //
+ // test('radio validate on change works', async () => {
+ // const wrapper = await renderForm({
+ // props: {
+ // validateOn: ['change'],
+ // validate (state: any) {
+ // if (state.value !== 'Option 2')
+ // return [{ name: 'value', message: 'Error message' }]
+ // return []
+ // }
+ // },
+ // slotComponents: {
+ // UFormField,
+ // URadio
+ // },
+ // slotTemplate: `
+ //
+ //
+ //
+ //
+ // `
+ // })
+ //
+ // const option1 = wrapper.find('#option-1')
+ // await setValue(option1, true)
+ // expect(wrapper.text()).toContain('Error message')
+ //
+ // const option2 = wrapper.find('#option-2')
+ // await setValue(option2, true)
+ // expect(wrapper.text()).not.toContain('Error message')
+ // })
+ //
+ // test('toggle validate on change', async () => {
+ // const wrapper = await renderForm({
+ // props: {
+ // validateOn: ['change'],
+ // validate (state: any) {
+ // if (state.value) return [{ name: 'value', message: 'Error message' }]
+ // return []
+ // }
+ // },
+ // slotComponents: {
+ // UFormField,
+ // UToggle
+ // },
+ // slotTemplate: `
+ //
+ //
+ //
+ // `
+ // })
+ //
+ // const input = wrapper.findComponent({ name: 'Switch' })
+ // await setValue(input, true)
+ // expect(wrapper.text()).toContain('Error message')
+ //
+ // await setValue(input, false)
+ // expect(wrapper.text()).not.toContain('Error message')
+ // })
+ //
+ // test('select menu validate on change', async () => {
+ // const wrapper = await renderForm({
+ // props: {
+ // validateOn: ['change'],
+ // validate (state: any) {
+ // if (state.value !== 'Option 2')
+ // return [{ name: 'value', message: 'Error message' }]
+ // return []
+ // }
+ // },
+ // slotVars: {
+ // inputProps: {
+ // options: ['Option 1', 'Option 2', 'Option 3']
+ // }
+ // },
+ // slotComponents: {
+ // UFormField,
+ // USelectMenu
+ // },
+ // slotTemplate: `
+ //
+ //
+ //
+ // `
+ // })
+ //
+ // const input = wrapper.findComponent({ name: 'Listbox' })
+ // await setValue(input, 'Option 1')
+ // expect(wrapper.text()).toContain('Error message')
+ //
+ // await setValue(input, 'Option 2')
+ // expect(wrapper.text()).not.toContain('Error message')
+ // })
+ //
+ // test('input menu validate on change', async () => {
+ // const wrapper = await renderForm({
+ // props: {
+ // validateOn: ['change'],
+ // validate (state: any) {
+ // if (state.value !== 'Option 2')
+ // return [{ name: 'value', message: 'Error message' }]
+ // return []
+ // }
+ // },
+ // slotVars: {
+ // inputProps: {
+ // options: ['Option 1', 'Option 2', 'Option 3']
+ // }
+ // },
+ // slotComponents: {
+ // UFormField,
+ // UInputMenu
+ // },
+ // slotTemplate: `
+ //
+ //
+ //
+ // `
+ // })
+ //
+ // const input = wrapper.findComponent({ name: 'Combobox' })
+ // await setValue(input, 'Option 1')
+ // expect(wrapper.text()).toContain('Error message')
+ //
+ // await setValue(input, 'Option 2')
+ // expect(wrapper.text()).not.toContain('Error message')
+ // })
+ //
+
+ it.each([
+ ['input', UInput, {}, 'foo']
+ // ['textarea', UTextarea, {}, 'foo']
+ ])('%s validate on input works', async (_name, InputComponent, inputProps, validInputValue) => {
+ const wrapper = await renderForm({
+ props: {
+ validateOn: ['input', 'blur'],
+ async validate (state: any) {
+ if (!state.value)
+ return [{ name: 'value', message: 'Error message' }]
+ return []
+ }
+ },
+ slotVars: {
+ inputProps
+ },
+ slotComponents: {
+ UFormField,
+ InputComponent
+ },
+ slotTemplate: `
+
+
+
+ `
+ })
+
+ const input = wrapper.find('#input')
+
+ // Validation @input is enabled only after a blur event
+ await triggerEvent(input, 'blur')
+
+ expect(wrapper.text()).toContain('Error message')
+
+ await setValue(input, validInputValue)
+ // Waiting because of the debounced validation on input event.
+ await new Promise((r) => setTimeout(r, 50))
+ expect(wrapper.text()).not.toContain('Error message')
+ })
+
+
+ describe('api', async () => {
+ let wrapper: any
+ let form: any
+ let state: any
+
+ beforeEach(async () => {
+ wrapper = await mountSuspended({
+ components: {
+ UFormField,
+ UForm,
+ UInput
+ },
+ setup () {
+ const form = ref()
+ const state = reactive({})
+ const schema = z.object({
+ email: z.string().email(),
+ password: z.string().min(8)
+ })
+
+ const onError = vi.fn()
+ const onSubmit = vi.fn()
+
+ return { state, schema, form, onSubmit, onError }
+ },
+ template: `
+
+
+
+
+
+
+
+
+ `
+ })
+ form = wrapper.setupState.form
+ state = wrapper.setupState.state
+ })
+
+ test('setErrors works', async () => {
+ form.value.setErrors([{
+ name: 'email',
+ message: 'this is an error'
+ }])
+
+ expect(form.value.errors).toMatchObject([{
+ id: 'emailInput',
+ name: 'email',
+ message: 'this is an error'
+ }])
+
+ await nextTick()
+
+ const emailField = wrapper.find('#emailField')
+ expect(emailField.text()).toBe('this is an error')
+
+ const passwordField = wrapper.find('#passwordField')
+ expect(passwordField.text()).toBe('')
+ })
+
+ test('clear works', async () => {
+ form.value.setErrors([{
+ id: 'emailInput',
+ name: 'email',
+ message: 'this is an error'
+ }])
+
+ form.value.clear()
+
+ expect(form.value.errors).toMatchObject([])
+
+ const emailField = wrapper.find('#emailField')
+ expect(emailField.text()).toBe('')
+
+ const passwordField = wrapper.find('#passwordField')
+ expect(passwordField.text()).toBe('')
+ })
+
+ test('submit error works', async () => {
+ await form.value.submit()
+
+ expect(form.value.errors).toMatchObject([
+ { id: 'emailInput', name: 'email', message: 'Required' },
+ { id: 'passwordInput', name: 'password', message: 'Required' }
+ ])
+
+ expect(wrapper.setupState.onSubmit).not.toHaveBeenCalled()
+ expect(wrapper.setupState.onError).toHaveBeenCalledTimes(1)
+ expect(wrapper.setupState.onError).toHaveBeenCalledWith(expect.objectContaining({
+ errors: [
+ { id: 'emailInput', name: 'email', message: 'Required' },
+ { id: 'passwordInput', name: 'password', message: 'Required' }
+ ]
+ }))
+
+ const emailField = wrapper.find('#emailField')
+ expect(emailField.text()).toBe('Required')
+
+ const passwordField = wrapper.find('#passwordField')
+ expect(passwordField.text()).toBe('Required')
+ })
+
+ test('valid submit works', async () => {
+ state.email = 'bob@dylan.com'
+ state.password = 'strongpassword'
+
+ await form.value.submit()
+
+ expect(wrapper.setupState.onSubmit).toHaveBeenCalledTimes(1)
+ expect(wrapper.setupState.onSubmit).toHaveBeenCalledWith(expect.objectContaining({
+ data: {
+ email: 'bob@dylan.com',
+ password: 'strongpassword'
+ }
+ }))
+
+ expect(wrapper.setupState.onError).toHaveBeenCalledTimes(0)
+ })
+
+ test('validate works', async () => {
+ await expect(form.value.validate).rejects.toThrow('Form validation exception')
+
+ state.email = 'bob@dylan.com'
+ state.password = 'strongpassword'
+
+ expect(await form.value.validate()).toMatchObject({
+ email: 'bob@dylan.com',
+ password: 'strongpassword'
+ })
+ })
+
+ test('getErrors works', async () => {
+ await form.value.submit()
+ const errors = form.value.getErrors()
+
+ expect(errors).toMatchObject([
+ { id: 'emailInput', name: 'email', message: 'Required' },
+ { id: 'passwordInput', name: 'password', message: 'Required' }
+ ])
+ })
+ })
+
+ describe('nested', async () => {
+ let wrapper: any
+ let form: any
+ let state: any
+
+ beforeEach(async () => {
+ wrapper = await mountSuspended({
+ components: {
+ UFormField,
+ UForm,
+ UInput
+ },
+ setup () {
+ const form = ref()
+ const state = reactive({ nested: {} })
+ const schema = z.object({
+ email: z.string().email(),
+ password: z.string().min(8)
+ })
+
+ const showNested = ref(true)
+ const nestedSchema = z.object({
+ field: z.string().min(1)
+ })
+
+
+ const onError = vi.fn()
+ const onSubmit = vi.fn()
+
+ return { state, schema, nestedSchema, form, onSubmit, onError, showNested }
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ })
+ form = wrapper.setupState.form
+ state = wrapper.setupState.state
+ })
+
+ test('submit error works', async () => {
+ await form.value.submit()
+
+ expect(wrapper.setupState.onSubmit).not.toHaveBeenCalled()
+ expect(wrapper.setupState.onError).toHaveBeenCalledTimes(1)
+ const onErrorCallArgs = wrapper.setupState.onError.mock.lastCall[0]
+ expect(onErrorCallArgs.childrens[0].errors).toMatchObject([ { id: 'nestedInput', name: 'field', message: 'Required' } ])
+ expect(onErrorCallArgs.errors).toMatchObject([
+ { id: 'emailInput', name: 'email', message: 'Required' },
+ { id: 'passwordInput', name: 'password', message: 'Required' }
+ ])
+
+ const nestedField = wrapper.find('#nestedField')
+ expect(nestedField.text()).toBe('Required')
+ })
+
+ test('submit works when child is disabled', async () => {
+ await form.value.submit()
+ expect(wrapper.setupState.onError).toHaveBeenCalledTimes(1)
+ vi.resetAllMocks()
+
+ wrapper.setupState.showNested.value = false
+ await nextTick()
+
+ state.email = 'bob@dylan.com'
+ state.password = 'strongpassword'
+
+ await form.value.submit()
+ expect(wrapper.setupState.onSubmit).toHaveBeenCalledTimes(1)
+ expect(wrapper.setupState.onError).toHaveBeenCalledTimes(0)
+ })
+ })
+})
diff --git a/test/components/FormField.spec.ts b/test/components/FormField.spec.ts
new file mode 100644
index 00000000..d47d8cb3
--- /dev/null
+++ b/test/components/FormField.spec.ts
@@ -0,0 +1,35 @@
+import { defineComponent } from 'vue'
+import { describe, it, expect } from 'vitest'
+import FormField, { type FormFieldProps } from '../../src/runtime/components/FormField.vue'
+import ComponentRender from '../component-render'
+
+// A wrapper component is needed here because of a conflict with the error prop / expose.
+// See: https://github.com/nuxt/test-utils/issues/684
+const FormFieldWrapper = defineComponent({
+ components: {
+ UFormField: FormField
+ },
+ template: ' '
+})
+
+describe('FormField', () => {
+ it.each([
+ ['with label and description', { props: { label: 'Username', description: 'Enter your username' } }],
+ ['with size', { props: { label: 'Username', description: 'Enter your username', size: 'xl' as const } }],
+ ['with required', { props: { label: 'Username', required: true } }],
+ ['with help', { props: { help: 'Username must be unique' } }],
+ ['with error', { props: { error: 'Username is already taken' } }],
+ ['with hint', { props: { hint: 'Use letters, numbers, and special characters' } }],
+ ['with class', { props: { class: 'relative' } }],
+ ['with ui', { props: { ui: { label: 'text-gray-900 dark:text-white' } } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }],
+ ['with label slot', { slots: { label: () => 'Label slot' } }],
+ ['with description slot', { slots: { description: () => 'Description slot' } }],
+ ['with error slot', { slots: { error: () => 'Error slot' } }],
+ ['with hint slot', { slots: { hint: () => 'Hint slot' } }],
+ ['with help slot', { slots: { help: () => 'Help slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: FormFieldProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, FormFieldWrapper)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Input.spec.ts b/test/components/Input.spec.ts
new file mode 100644
index 00000000..5793a616
--- /dev/null
+++ b/test/components/Input.spec.ts
@@ -0,0 +1,66 @@
+import { describe, it, expect, test } from 'vitest'
+import Input, { type InputProps } from '../../src/runtime/components/Input.vue'
+import ComponentRender from '../component-render'
+import { mount } from '@vue/test-utils'
+
+describe('Input', () => {
+ it.each([
+ ['basic case', {}],
+ ['with name', { props: { name: 'username' } }],
+ ['with type', { props: { type: 'password' } }],
+ ['with placeholder', { props: { placeholder: 'Enter your username' } }],
+ ['with disabled', { props: { disabled: true } }],
+ ['with required', { props: { required: true } }],
+ ['with icon', { props: { icon: 'i-heroicons-magnifying-glass' } }],
+ ['with leading and icon', { props: { leading: true, icon: 'i-heroicons-magnifying-glass' } }],
+ ['with leadingIcon', { props: { leadingIcon: 'i-heroicons-magnifying-glass' } }],
+ ['with trailing and icon', { props: { trailing: true, icon: 'i-heroicons-magnifying-glass' } }],
+ ['with trailingIcon', { props: { trailingIcon: 'i-heroicons-magnifying-glass' } }],
+ ['with loading', { props: { loading: true } }],
+ ['with loadingIcon', { props: { loading: true, loadingIcon: 'i-heroicons-sparkles' } }],
+ ['with size 2xs', { props: { size: '2xs' as const } }],
+ ['with size xs', { props: { size: 'xs' as const } }],
+ ['with size sm', { props: { size: 'sm' as const } }],
+ ['with size md', { props: { size: 'md' as const } }],
+ ['with size lg', { props: { size: 'lg' as const } }],
+ ['with size xl', { props: { size: 'xl' as const } }],
+ ['with color', { props: { color: 'red' as const } }],
+ ['with variant', { props: { variant: 'outline' as const } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }],
+ ['with leading slot', { slots: { leading: () => 'Leading slot' } }],
+ ['with trailing slot', { slots: { trailing: () => 'Trailing slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Input)
+ expect(html).toMatchSnapshot()
+ })
+
+ it.each([
+ ['with .trim modifier', { props: { modelModifiers: { trim: true } } }, { input: 'input ', expected: 'input' } ],
+ ['with .number modifier', { props: { modelModifiers: { number: true } } }, { input: '42', expected: 42 } ],
+ ['with .lazy modifier', { props: { modelModifiers: { lazy: true } } }, { input: 'input', expected: 'input' } ]
+ ])('%s works', async (_nameOrHtml: string, options: { props?: any, slots?: any }, spec: { input: any, expected: any }) => {
+ const wrapper = mount(Input, {
+ ...options
+ })
+
+ const input = wrapper.find('input')
+ await input.setValue(spec.input)
+
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[spec.expected]] })
+ })
+
+ test('with .lazy modifier updates on change only', async () => {
+ const wrapper = mount(Input, {
+ props: {
+ modelModifiers: { lazy: true }
+ }
+ })
+
+ const input = wrapper.find('input')
+ await input.trigger('update')
+ expect(wrapper.emitted()).toMatchObject({ })
+
+ await input.trigger('change')
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [['']] })
+ })
+})
diff --git a/test/components/Kbd.spec.ts b/test/components/Kbd.spec.ts
new file mode 100644
index 00000000..6487db9d
--- /dev/null
+++ b/test/components/Kbd.spec.ts
@@ -0,0 +1,18 @@
+import { describe, it, expect } from 'vitest'
+import Kbd, { type KbdProps } from '../../src/runtime/components/Kbd.vue'
+import ComponentRender from '../component-render'
+
+describe('Kbd', () => {
+ it.each([
+ ['with value', { props: { value: 'K' } }],
+ ['with as', { props: { value: 'K', as: 'span' } }],
+ ['with class', { props: { value: 'K', class: 'font-bold' } }],
+ ['with size xs', { props: { value: 'K', size: 'xs' as const } }],
+ ['with size sm', { props: { value: 'K', size: 'sm' as const } }],
+ ['with size md', { props: { value: 'K', size: 'md' as const } }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: KbdProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Kbd)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Link.spec.ts b/test/components/Link.spec.ts
new file mode 100644
index 00000000..c50218fa
--- /dev/null
+++ b/test/components/Link.spec.ts
@@ -0,0 +1,19 @@
+import { describe, it, expect } from 'vitest'
+import Link, { type LinkProps } from '../../src/runtime/components/Link.vue'
+import ComponentRender from '../component-render'
+
+describe('Link', () => {
+ it.each([
+ ['with as', { props: { as: 'div' } }],
+ ['with to', { props: { to: '/' } }],
+ ['with type', { props: { type: 'submit' as const } }],
+ ['with disabled', { props: { disabled: true } }],
+ ['with raw', { props: { raw: true } }],
+ ['with class', { props: { class: 'font-medium' } }],
+ ['with activeClass', { props: { active: true, activeClass: 'text-gray-900 dark:text-white' } }],
+ ['with inactiveClass', { props: { active: false, inactiveClass: 'hover:text-primary-500 dark:hover:text-primary-400' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props: LinkProps }) => {
+ const html = await ComponentRender(nameOrHtml, options, Link)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Modal.spec.ts b/test/components/Modal.spec.ts
new file mode 100644
index 00000000..e5b1a340
--- /dev/null
+++ b/test/components/Modal.spec.ts
@@ -0,0 +1,27 @@
+import { describe, it, expect } from 'vitest'
+import Modal, { type ModalProps } from '../../src/runtime/components/Modal.vue'
+import ComponentRender from '../component-render'
+
+describe('Modal', () => {
+ it.each([
+ ['basic case', { props: { open: true, portal: false } }],
+ ['with title', { props: { open: true, portal: false, title: 'Title' } }],
+ ['with description', { props: { open: true, portal: false, title: 'Title', description: 'Description' } }],
+ ['with fullscreen', { props: { open: true, portal: false, fullscreen: true, title: 'Title', description: 'Description' } }],
+ ['without overlay', { props: { open: true, portal: false, overlay: false, title: 'Title', description: 'Description' } }],
+ ['without transition', { props: { open: true, portal: false, transition: false, title: 'Title', description: 'Description' } }],
+ ['with class', { props: { open: true, portal: false, class: 'bg-gray-50 dark:bg-gray-800' } }],
+ ['with ui', { props: { open: true, portal: false, ui: { close: 'right-2' } } }],
+ ['with default slot', { props: { open: true, portal: false }, slots: { default: () => 'Default slot' } }],
+ ['with content slot', { props: { open: true, portal: false }, slots: { content: () => 'Content slot' } }],
+ ['with header slot', { props: { open: true, portal: false }, slots: { header: () => 'Header slot' } }],
+ ['with title slot', { props: { open: true, portal: false }, slots: { title: () => 'Title slot' } }],
+ ['with description slot', { props: { open: true, portal: false }, slots: { description: () => 'Description slot' } }],
+ ['with close slot', { props: { open: true, portal: false }, slots: { close: () => 'Close slot' } }],
+ ['with body slot', { props: { open: true, portal: false }, slots: { body: () => 'Body slot' } }],
+ ['with footer slot', { props: { open: true, portal: false }, slots: { footer: () => 'Footer slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ModalProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Modal)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/NavigationMenu.spec.ts b/test/components/NavigationMenu.spec.ts
new file mode 100644
index 00000000..feeb6cc3
--- /dev/null
+++ b/test/components/NavigationMenu.spec.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect } from 'vitest'
+import NavigationMenu, { type NavigationMenuProps } from '../../src/runtime/components/NavigationMenu.vue'
+import ComponentRender from '../component-render'
+
+const links = [{
+ label: 'Profile',
+ avatar: {
+ src: 'https://avatars.githubusercontent.com/u/739984?v=4'
+ },
+ badge: 100
+}, {
+ label: 'Modal',
+ icon: 'i-heroicons-home',
+ to: '/modal'
+}, {
+ label: 'NavigationMenu',
+ icon: 'i-heroicons-chart-bar',
+ to: '/navigation-menu'
+}, {
+ label: 'Popover',
+ icon: 'i-heroicons-command-line',
+ to: '/popover'
+}]
+
+describe('NavigationMenu', () => {
+ it.each([
+ ['basic case', { props: { links } }],
+ ['with vertical orientation', { props: { links, orientation: 'vertical' as const } }],
+ ['with class', { props: { links, class: 'w-48' } }],
+ ['with ui', { props: { links, ui: { icon: 'w-4 h-4' } } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: NavigationMenuProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, NavigationMenu)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Popover.spec.ts b/test/components/Popover.spec.ts
new file mode 100644
index 00000000..a17975e4
--- /dev/null
+++ b/test/components/Popover.spec.ts
@@ -0,0 +1,12 @@
+import { describe, it, expect } from 'vitest'
+import Popover, { type PopoverProps } from '../../src/runtime/components/Popover.vue'
+import ComponentRender from '../component-render'
+
+describe('Popover', () => {
+ it.each([
+ ['basic case', { props: { open: true, arrow: true }, slots: { default: () => 'Click me', content: () => 'Popover content' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: PopoverProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Popover)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Skeleton.spec.ts b/test/components/Skeleton.spec.ts
new file mode 100644
index 00000000..208f8938
--- /dev/null
+++ b/test/components/Skeleton.spec.ts
@@ -0,0 +1,14 @@
+import { describe, it, expect } from 'vitest'
+import Skeleton, { type SkeletonProps } from '../../src/runtime/components/Skeleton.vue'
+import ComponentRender from '../component-render'
+
+describe('Skeleton', () => {
+ it.each([
+ ['basic case', {}],
+ ['with as', { props: { as: 'span' } }],
+ ['with class', { props: { class: 'rounded-full size-12' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: SkeletonProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Skeleton)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Slideover.spec.ts b/test/components/Slideover.spec.ts
new file mode 100644
index 00000000..17272441
--- /dev/null
+++ b/test/components/Slideover.spec.ts
@@ -0,0 +1,29 @@
+import { describe, it, expect } from 'vitest'
+import Slideover, { type SlideoverProps } from '../../src/runtime/components/Slideover.vue'
+import ComponentRender from '../component-render'
+
+describe('Slideover', () => {
+ it.each([
+ ['basic case', { props: { open: true, portal: false } }],
+ ['with title', { props: { open: true, portal: false, title: 'Title' } }],
+ ['with description', { props: { open: true, portal: false, title: 'Title', description: 'Description' } }],
+ ['with left side', { props: { open: true, portal: false, side: 'left' as const, title: 'Title', description: 'Description' } }],
+ ['with top side', { props: { open: true, portal: false, side: 'top' as const, title: 'Title', description: 'Description' } }],
+ ['with bottom side', { props: { open: true, portal: false, side: 'bottom' as const, title: 'Title', description: 'Description' } }],
+ ['without overlay', { props: { open: true, portal: false, overlay: false, title: 'Title', description: 'Description' } }],
+ ['without transition', { props: { open: true, portal: false, transition: false, title: 'Title', description: 'Description' } }],
+ ['with class', { props: { open: true, portal: false, class: 'bg-gray-50 dark:bg-gray-800' } }],
+ ['with ui', { props: { open: true, portal: false, ui: { close: 'right-2' } } }],
+ ['with default slot', { props: { open: true, portal: false }, slots: { default: () => 'Default slot' } }],
+ ['with content slot', { props: { open: true, portal: false }, slots: { content: () => 'Content slot' } }],
+ ['with header slot', { props: { open: true, portal: false }, slots: { header: () => 'Header slot' } }],
+ ['with title slot', { props: { open: true, portal: false }, slots: { title: () => 'Title slot' } }],
+ ['with description slot', { props: { open: true, portal: false }, slots: { description: () => 'Description slot' } }],
+ ['with close slot', { props: { open: true, portal: false }, slots: { close: () => 'Close slot' } }],
+ ['with body slot', { props: { open: true, portal: false }, slots: { body: () => 'Body slot' } }],
+ ['with footer slot', { props: { open: true, portal: false }, slots: { footer: () => 'Footer slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: SlideoverProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Slideover)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Switch.spec.ts b/test/components/Switch.spec.ts
new file mode 100644
index 00000000..2b0d2a88
--- /dev/null
+++ b/test/components/Switch.spec.ts
@@ -0,0 +1,32 @@
+import { describe, it, expect } from 'vitest'
+import Switch, { type SwitchProps } from '../../src/runtime/components/Switch.vue'
+import ComponentRender from '../component-render'
+
+describe('Switch', () => {
+ it.each([
+ ['basic case', {}],
+ ['with class', { props: { class: '' } }],
+ ['with ui', { props: { ui: {} } }],
+ ['with as', { props: { as: 'section' } }],
+ ['with checked', { props: { checked: true } }],
+ ['with defaultChecked', { props: { defaultChecked: true } }],
+ ['with disabled', { props: { disabled: true } }],
+ ['with id', { props: { id: 'test' } }],
+ ['with name', { props: { name: 'test' } }],
+ ['with required', { props: { required: true } }],
+ ['with value', { props: { value: 'switch' } }],
+ ['with checkedIcon', { props: { checkedIcon: 'i-heroicons-check-20-solid' } }],
+ ['with uncheckedIcon', { props: { checkedIcon: 'i-heroicons-x-mark-20-solid' } }],
+ ['with loading', { props: { loading: true } }],
+ ['with loadingIcon', { props: { loading: true, loadingIcon: 'i-heroicons-sparkles' } }],
+ ['with size 2xs', { props: { size: '2xs' as const } }],
+ ['with size xs', { props: { size: 'xs' as const } }],
+ ['with size sm', { props: { size: 'sm' as const } }],
+ ['with size md', { props: { size: 'md' as const } }],
+ ['with size lg', { props: { size: 'lg' as const } }],
+ ['with size xl', { props: { size: 'xl' as const } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: SwitchProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Switch)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Tabs.spec.ts b/test/components/Tabs.spec.ts
new file mode 100644
index 00000000..52d1e217
--- /dev/null
+++ b/test/components/Tabs.spec.ts
@@ -0,0 +1,30 @@
+import { describe, it, expect } from 'vitest'
+import Tabs, { type TabsProps } from '../../src/runtime/components/Tabs.vue'
+import ComponentRender from '../component-render'
+
+const items = [{
+ label: 'Tab1',
+ content: 'This is the content shown for Tab1'
+}, {
+ label: 'Tab2',
+ content: 'And, this is the content for Tab2'
+}, {
+ label: 'Tab3',
+ content: 'Finally, this is the content for Tab3'
+}]
+
+describe('Tabs', () => {
+ it.each([
+ ['basic case', { props: { items } }],
+ ['with class', { props: { items, class: 'w-96' } }],
+ ['with ui', { props: { items, ui: { content: 'w-full ring ring-gray-200 dark:ring-gray-800' } } }],
+ ['with orientation', { props: { items, orientation: 'vertical' as const } }],
+ ['with modelValue', { props: { items, modelValue: '1' } }],
+ ['with defaultValue', { props: { items, defaultValue: '1' } }],
+ ['with default slot', { props: { items }, slots: { default: () => 'Default slot' } }],
+ ['with content slot', { props: { items }, slots: { content: () => 'Content slot' } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: TabsProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Tabs)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/Textarea.spec.ts b/test/components/Textarea.spec.ts
new file mode 100644
index 00000000..3a0dd437
--- /dev/null
+++ b/test/components/Textarea.spec.ts
@@ -0,0 +1,60 @@
+import { describe, it, expect, test } from 'vitest'
+import Textarea, { type TextareaProps } from '../../src/runtime/components/Textarea.vue'
+import ComponentRender from '../component-render'
+import { mount } from '@vue/test-utils'
+
+describe('Textarea', () => {
+ it.each([
+ ['basic case', {}],
+ ['with id', { props: { id: 'exampleId' } }],
+ ['with name', { props: { name: 'exampleName' } }],
+ ['with placeholder', { props: { placeholder: 'examplePlaceholder' } }],
+ ['with required', { props: { required: true } }],
+ ['with disabled', { props: { disabled: true } }],
+ ['with rows', { props: { rows: 5 } }],
+ ['with size', { props: { size: 'sm' } }],
+ ['with color', { props: { color: 'blue' } }],
+ ['with size 2xs', { props: { size: '2xs' as const } }],
+ ['with size xs', { props: { size: 'xs' as const } }],
+ ['with size sm', { props: { size: 'sm' as const } }],
+ ['with size md', { props: { size: 'md' as const } }],
+ ['with size lg', { props: { size: 'lg' as const } }],
+ ['with size xl', { props: { size: 'xl' as const } }],
+ ['with variant', { variant: 'outline' }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }]
+ // @ts-ignore
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props: TextareaProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Textarea)
+ expect(html).toMatchSnapshot()
+ })
+
+ it.each([
+ ['with .trim modifier', { props: { modelModifiers: { trim: true } } }, { input: 'input ', expected: 'input' } ],
+ ['with .number modifier', { props: { modelModifiers: { number: true } } }, { input: '42', expected: 42 } ],
+ ['with .lazy modifier', { props: { modelModifiers: { lazy: true } } }, { input: 'input', expected: 'input' } ]
+ ])('%s works', async (_nameOrHtml: string, options: { props?: any, slots?: any }, spec: { input: any, expected: any }) => {
+ const wrapper = await mount(Textarea, {
+ ...options
+ })
+
+ const input = wrapper.find('textarea')
+ await input.setValue(spec.input)
+
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[spec.expected]] })
+ })
+
+ test('with .lazy modifier updates on change only', async () => {
+ const wrapper = mount(Textarea, {
+ props: {
+ modelModifiers: { lazy: true }
+ }
+ })
+
+ const input = wrapper.find('textarea')
+ await input.trigger('update')
+ expect(wrapper.emitted()).toMatchObject({ })
+
+ await input.trigger('change')
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [['']] })
+ })
+})
diff --git a/test/components/Tooltip.spec.ts b/test/components/Tooltip.spec.ts
new file mode 100644
index 00000000..8b1988e1
--- /dev/null
+++ b/test/components/Tooltip.spec.ts
@@ -0,0 +1,25 @@
+import { defineComponent } from 'vue'
+import { describe, it, expect } from 'vitest'
+import Provider from '../../src/runtime/components/Provider.vue'
+import Tooltip, { type TooltipProps } from '../../src/runtime/components/Tooltip.vue'
+import ComponentRender from '../component-render'
+
+const TooltipWrapper = defineComponent({
+ components: {
+ UProvider: Provider,
+ UTooltip: Tooltip
+ },
+ inheritAttrs: false,
+ template: ' '
+})
+
+describe('Tooltip', () => {
+ it.each([
+ ['with text', { props: { text: 'Tooltip', open: true } }],
+ ['with arrow', { props: { text: 'Tooltip', arrow: true, open: true } }],
+ ['with shortcuts', { props: { text: 'Tooltip', shortcuts: ['โ', 'K'], open: true } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: TooltipProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, TooltipWrapper)
+ expect(html).toMatchSnapshot()
+ })
+})
diff --git a/test/components/__snapshots__/Accordion.spec.ts.snap b/test/components/__snapshots__/Accordion.spec.ts.snap
new file mode 100644
index 00000000..d5bfbcd2
--- /dev/null
+++ b/test/components/__snapshots__/Accordion.spec.ts.snap
@@ -0,0 +1,633 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Accordion > renders basic case correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with as correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+ "
+`;
+
+exports[`Accordion > renders with class correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with collapsible correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with content slot correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Content slot
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with default slot correctly 1`] = `
+"
+
+
+
+ Default slot
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Default slot
+
+
+
+
+
+
+
+
+
+ Default slot
+
+
+
+
+
+
+
+
+
+ Default slot
+
+
+
+
+
+
+
+
+
+ Default slot
+
+
+
+
+
+
+
+
+
+ Default slot
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with defaultValue correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
+
+
+
+
+
+
+ Installation
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with disabled correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with modelValue correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
+
+
+
+
+
+
+ Installation
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with type correctly 1`] = `
+"
+
+
+
+ Getting Started
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
+
+
+
+
+ Installation
+
+
+
+
+
+
+
+
+
+ Theming
+
+
+
+
+
+
+
+
+
+ Layouts
+
+
+
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+ Utilities
+
+
+
+
+
+
+
"
+`;
+
+exports[`Accordion > renders with ui correctly 1`] = `"
"`;
diff --git a/test/components/__snapshots__/Avatar.spec.ts.snap b/test/components/__snapshots__/Avatar.spec.ts.snap
new file mode 100644
index 00000000..7b40991e
--- /dev/null
+++ b/test/components/__snapshots__/Avatar.spec.ts.snap
@@ -0,0 +1,31 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Avatar > renders with alt correctly 1`] = `"BC "`;
+
+exports[`Avatar > renders with class correctly 1`] = `" "`;
+
+exports[`Avatar > renders with icon correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size 2xl correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size 2xs correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size 3xl correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size 3xs correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size lg correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size md correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size sm correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size xl correctly 1`] = `" "`;
+
+exports[`Avatar > renders with size xs correctly 1`] = `" "`;
+
+exports[`Avatar > renders with src correctly 1`] = `" "`;
+
+exports[`Avatar > renders with text correctly 1`] = `"+1 "`;
+
+exports[`Avatar > renders with ui correctly 1`] = `" "`;
diff --git a/test/components/__snapshots__/Badge.spec.ts.snap b/test/components/__snapshots__/Badge.spec.ts.snap
new file mode 100644
index 00000000..e51ed7ea
--- /dev/null
+++ b/test/components/__snapshots__/Badge.spec.ts.snap
@@ -0,0 +1,31 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Badge > renders with as correctly 1`] = `"Badge
"`;
+
+exports[`Badge > renders with class correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with color black correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with color gray correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with color green correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with color white correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with default slot correctly 1`] = `"Default slot "`;
+
+exports[`Badge > renders with label correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with size lg correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with size md correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with size sm correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with size xs correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with variant link correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with variant outline correctly 1`] = `"Badge "`;
+
+exports[`Badge > renders with variant soft correctly 1`] = `"Badge "`;
diff --git a/test/components/__snapshots__/Button.spec.ts.snap b/test/components/__snapshots__/Button.spec.ts.snap
new file mode 100644
index 00000000..c5475a17
--- /dev/null
+++ b/test/components/__snapshots__/Button.spec.ts.snap
@@ -0,0 +1,199 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Button > renders with block correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with class correctly 1`] = `" "`;
+
+exports[`Button > renders with color correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with default slot correctly 1`] = `
+"
+ Default slot
+
+ "
+`;
+
+exports[`Button > renders with disabled correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with icon correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with label correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with leading and icon correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with leading slot correctly 1`] = `
+"Leading slot
+
+
+ "
+`;
+
+exports[`Button > renders with leadingIcon correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with loading correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with loadingIcon correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with size 2xs correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with size lg correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with size md correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with size sm correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with size xl correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with size xs correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with square correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with trailing and icon correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with trailing slot correctly 1`] = `
+"
+
+ Trailing slot
+ "
+`;
+
+exports[`Button > renders with trailingIcon correctly 1`] = `
+"
+
+
+
+
+ "
+`;
+
+exports[`Button > renders with truncate correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with variant ghost correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with variant link correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with variant outline correctly 1`] = `
+"
+ Button
+
+ "
+`;
+
+exports[`Button > renders with variant soft correctly 1`] = `
+"
+ Button
+
+ "
+`;
diff --git a/test/components/__snapshots__/Card.spec.ts.snap b/test/components/__snapshots__/Card.spec.ts.snap
new file mode 100644
index 00000000..2ed819ad
--- /dev/null
+++ b/test/components/__snapshots__/Card.spec.ts.snap
@@ -0,0 +1,49 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Card > renders basic case correctly 1`] = `
+"
+
+
+
+
"
+`;
+
+exports[`Card > renders with as correctly 1`] = `
+""
+`;
+
+exports[`Card > renders with class correctly 1`] = `
+"
+
+
+
+
"
+`;
+
+exports[`Card > renders with default slot correctly 1`] = `
+""
+`;
+
+exports[`Card > renders with footer slot correctly 1`] = `
+""
+`;
+
+exports[`Card > renders with header slot correctly 1`] = `
+""
+`;
diff --git a/test/components/__snapshots__/Checkbox.spec.ts.snap b/test/components/__snapshots__/Checkbox.spec.ts.snap
new file mode 100644
index 00000000..59268fbb
--- /dev/null
+++ b/test/components/__snapshots__/Checkbox.spec.ts.snap
@@ -0,0 +1,207 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Checkbox > renders basic case correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with custom color correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with custom id correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with custom name correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with custom value correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with disabled correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with help correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with indeterminate correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with label correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with required correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with size 2xs correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with size lg correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with size md correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with size sm correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with size xl correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with size xs correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with slot correctly 1`] = `
+""
+`;
+
+exports[`Checkbox > renders with slot label correctly 1`] = `
+"
+
+
+
+
+
+
Label slot
+
+
+
"
+`;
diff --git a/test/components/__snapshots__/Chip.spec.ts.snap b/test/components/__snapshots__/Chip.spec.ts.snap
new file mode 100644
index 00000000..9a5c1da1
--- /dev/null
+++ b/test/components/__snapshots__/Chip.spec.ts.snap
@@ -0,0 +1,51 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Chip > renders basic case correctly 1`] = `"
"`;
+
+exports[`Chip > renders with as correctly 1`] = `" "`;
+
+exports[`Chip > renders with class correctly 1`] = `"
"`;
+
+exports[`Chip > renders with color black correctly 1`] = `"
"`;
+
+exports[`Chip > renders with color gray correctly 1`] = `"
"`;
+
+exports[`Chip > renders with color green correctly 1`] = `"
"`;
+
+exports[`Chip > renders with color white correctly 1`] = `"
"`;
+
+exports[`Chip > renders with content slot correctly 1`] = `"Content slot
"`;
+
+exports[`Chip > renders with default slot correctly 1`] = `"Default slot
"`;
+
+exports[`Chip > renders with inset correctly 1`] = `"
"`;
+
+exports[`Chip > renders with position bottom-left correctly 1`] = `"
"`;
+
+exports[`Chip > renders with position bottom-right correctly 1`] = `"
"`;
+
+exports[`Chip > renders with position top-left correctly 1`] = `"
"`;
+
+exports[`Chip > renders with position top-right correctly 1`] = `"
"`;
+
+exports[`Chip > renders with show correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size 2xl correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size 2xs correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size 3xl correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size 3xs correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size lg correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size md correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size sm correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size xl correctly 1`] = `"
"`;
+
+exports[`Chip > renders with size xs correctly 1`] = `"
"`;
+
+exports[`Chip > renders with ui correctly 1`] = `"
"`;
diff --git a/test/components/__snapshots__/Collapsible.spec.ts.snap b/test/components/__snapshots__/Collapsible.spec.ts.snap
new file mode 100644
index 00000000..15242589
--- /dev/null
+++ b/test/components/__snapshots__/Collapsible.spec.ts.snap
@@ -0,0 +1,6 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Collapsible > renders basic case correctly 1`] = `
+"Click me
Collapsible content
+
"
+`;
diff --git a/test/components/__snapshots__/Container.spec.ts.snap b/test/components/__snapshots__/Container.spec.ts.snap
new file mode 100644
index 00000000..8034d5ee
--- /dev/null
+++ b/test/components/__snapshots__/Container.spec.ts.snap
@@ -0,0 +1,9 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Container > renders basic case correctly 1`] = `"
"`;
+
+exports[`Container > renders with as correctly 1`] = `" "`;
+
+exports[`Container > renders with class correctly 1`] = `"
"`;
+
+exports[`Container > renders with default slot correctly 1`] = `"Default slot
"`;
diff --git a/test/components/__snapshots__/Form.spec.ts.snap b/test/components/__snapshots__/Form.spec.ts.snap
new file mode 100644
index 00000000..d90d8161
--- /dev/null
+++ b/test/components/__snapshots__/Form.spec.ts.snap
@@ -0,0 +1,315 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Form > custom validation works > with error 1`] = `
+""
+`;
+
+exports[`Form > custom validation works > without error 1`] = `
+""
+`;
+
+exports[`Form > joi validation works > with error 1`] = `
+""
+`;
+
+exports[`Form > joi validation works > without error 1`] = `
+""
+`;
+
+exports[`Form > renders basic case correctly 1`] = `""`;
+
+exports[`Form > renders with default slot correctly 1`] = `""`;
+
+exports[`Form > valibot validation works > with error 1`] = `
+""
+`;
+
+exports[`Form > valibot validation works > without error 1`] = `
+""
+`;
+
+exports[`Form > yup validation works > with error 1`] = `
+""
+`;
+
+exports[`Form > yup validation works > without error 1`] = `
+""
+`;
+
+exports[`Form > zod validation works > with error 1`] = `
+""
+`;
+
+exports[`Form > zod validation works > without error 1`] = `
+""
+`;
diff --git a/test/components/__snapshots__/FormField.spec.ts.snap b/test/components/__snapshots__/FormField.spec.ts.snap
new file mode 100644
index 00000000..91a08ec3
--- /dev/null
+++ b/test/components/__snapshots__/FormField.spec.ts.snap
@@ -0,0 +1,175 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`FormField > renders with class correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with default slot correctly 1`] = `
+"
+
+
+
+
+
Default slot
+
+
+
"
+`;
+
+exports[`FormField > renders with description slot correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with error correctly 1`] = `
+"
+
+
+
+
+
+
Username is already taken
+
+
"
+`;
+
+exports[`FormField > renders with error slot correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with help correctly 1`] = `
+"
+
+
+
+
+
+
Username must be unique
+
+
"
+`;
+
+exports[`FormField > renders with help slot correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with hint correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with hint slot correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with label and description correctly 1`] = `
+"
+
+
Username
+
+
+
Enter your username
+
+
+
+
+
"
+`;
+
+exports[`FormField > renders with label slot correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with required correctly 1`] = `
+""
+`;
+
+exports[`FormField > renders with size correctly 1`] = `
+"
+
+
Username
+
+
+
Enter your username
+
+
+
+
+
"
+`;
+
+exports[`FormField > renders with ui correctly 1`] = `
+""
+`;
diff --git a/test/components/__snapshots__/Input.spec.ts.snap b/test/components/__snapshots__/Input.spec.ts.snap
new file mode 100644
index 00000000..b46e1cee
--- /dev/null
+++ b/test/components/__snapshots__/Input.spec.ts.snap
@@ -0,0 +1,160 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Input > renders basic case correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with color correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with default slot correctly 1`] = `
+" Default slot
+
+
+
"
+`;
+
+exports[`Input > renders with disabled correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with icon correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with leading and icon correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with leading slot correctly 1`] = `
+"Leading slot
+
+
"
+`;
+
+exports[`Input > renders with leadingIcon correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with loading correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with loadingIcon correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with name correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with placeholder correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with required correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with size 2xs correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with size lg correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with size md correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with size sm correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with size xl correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with size xs correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with trailing and icon correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with trailing slot correctly 1`] = `
+"
+ Trailing slot
+
"
+`;
+
+exports[`Input > renders with trailingIcon correctly 1`] = `
+""
+`;
+
+exports[`Input > renders with type correctly 1`] = `
+"
+
+
+
"
+`;
+
+exports[`Input > renders with variant correctly 1`] = `
+"
+
+
+
"
+`;
diff --git a/test/components/__snapshots__/Kbd.spec.ts.snap b/test/components/__snapshots__/Kbd.spec.ts.snap
new file mode 100644
index 00000000..2eaca1dd
--- /dev/null
+++ b/test/components/__snapshots__/Kbd.spec.ts.snap
@@ -0,0 +1,15 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Kbd > renders with as correctly 1`] = `"K "`;
+
+exports[`Kbd > renders with class correctly 1`] = `"K "`;
+
+exports[`Kbd > renders with default slot correctly 1`] = `"Default slot "`;
+
+exports[`Kbd > renders with size md correctly 1`] = `"K "`;
+
+exports[`Kbd > renders with size sm correctly 1`] = `"K "`;
+
+exports[`Kbd > renders with size xs correctly 1`] = `"K "`;
+
+exports[`Kbd > renders with value correctly 1`] = `"K "`;
diff --git a/test/components/__snapshots__/Link.spec.ts.snap b/test/components/__snapshots__/Link.spec.ts.snap
new file mode 100644
index 00000000..e104bce3
--- /dev/null
+++ b/test/components/__snapshots__/Link.spec.ts.snap
@@ -0,0 +1,17 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Link > renders with activeClass correctly 1`] = `" "`;
+
+exports[`Link > renders with as correctly 1`] = `"
"`;
+
+exports[`Link > renders with class correctly 1`] = `" "`;
+
+exports[`Link > renders with disabled correctly 1`] = `" "`;
+
+exports[`Link > renders with inactiveClass correctly 1`] = `" "`;
+
+exports[`Link > renders with raw correctly 1`] = `" "`;
+
+exports[`Link > renders with to correctly 1`] = `" "`;
+
+exports[`Link > renders with type correctly 1`] = `" "`;
diff --git a/test/components/__snapshots__/Modal.spec.ts.snap b/test/components/__snapshots__/Modal.spec.ts.snap
new file mode 100644
index 00000000..bd0dc689
--- /dev/null
+++ b/test/components/__snapshots__/Modal.spec.ts.snap
@@ -0,0 +1,347 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Modal > renders basic case correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with body slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Body slot
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with class correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with close slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with content slot correctly 1`] = `
+"
+
+
+
+
+Content slot
+
+
+"
+`;
+
+exports[`Modal > renders with default slot correctly 1`] = `
+"Default slot
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with description correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with description slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+
Description slot
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with footer slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Footer slot
+
+
+
+"
+`;
+
+exports[`Modal > renders with fullscreen correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with header slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with title correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with title slot correctly 1`] = `
+"
+
+
+
+
+
+
+
Title slot
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders with ui correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders without overlay correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Modal > renders without transition correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
diff --git a/test/components/__snapshots__/NavigationMenu.spec.ts.snap b/test/components/__snapshots__/NavigationMenu.spec.ts.snap
new file mode 100644
index 00000000..3f2f6e92
--- /dev/null
+++ b/test/components/__snapshots__/NavigationMenu.spec.ts.snap
@@ -0,0 +1,101 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`NavigationMenu > renders basic case correctly 1`] = `
+"
+
+ "
+`;
+
+exports[`NavigationMenu > renders with class correctly 1`] = `
+"
+
+ "
+`;
+
+exports[`NavigationMenu > renders with ui correctly 1`] = `
+"
+
+ "
+`;
+
+exports[`NavigationMenu > renders with vertical orientation correctly 1`] = `
+"
+
+ "
+`;
diff --git a/test/components/__snapshots__/Popover.spec.ts.snap b/test/components/__snapshots__/Popover.spec.ts.snap
new file mode 100644
index 00000000..d00b4bf0
--- /dev/null
+++ b/test/components/__snapshots__/Popover.spec.ts.snap
@@ -0,0 +1,14 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Popover > renders basic case correctly 1`] = `
+"Click me
+
+
+
+
+
+
+"
+`;
diff --git a/test/components/__snapshots__/Skeleton.spec.ts.snap b/test/components/__snapshots__/Skeleton.spec.ts.snap
new file mode 100644
index 00000000..ab1f277b
--- /dev/null
+++ b/test/components/__snapshots__/Skeleton.spec.ts.snap
@@ -0,0 +1,7 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Skeleton > renders basic case correctly 1`] = `"
"`;
+
+exports[`Skeleton > renders with as correctly 1`] = `" "`;
+
+exports[`Skeleton > renders with class correctly 1`] = `"
"`;
diff --git a/test/components/__snapshots__/Slideover.spec.ts.snap b/test/components/__snapshots__/Slideover.spec.ts.snap
new file mode 100644
index 00000000..3603a3b7
--- /dev/null
+++ b/test/components/__snapshots__/Slideover.spec.ts.snap
@@ -0,0 +1,393 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Slideover > renders basic case correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with body slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Body slot
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with bottom side correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with class correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with close slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with content slot correctly 1`] = `
+"
+
+
+
+
+Content slot
+
+
+"
+`;
+
+exports[`Slideover > renders with default slot correctly 1`] = `
+"Default slot
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with description correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with description slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+
Description slot
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with footer slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Footer slot
+
+
+
+"
+`;
+
+exports[`Slideover > renders with header slot correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with left side correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with title correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with title slot correctly 1`] = `
+"
+
+
+
+
+
+
+
Title slot
+
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with top side correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders with ui correctly 1`] = `
+"
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders without overlay correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
+
+exports[`Slideover > renders without transition correctly 1`] = `
+"
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
+
+"
+`;
diff --git a/test/components/__snapshots__/Switch.spec.ts.snap b/test/components/__snapshots__/Switch.spec.ts.snap
new file mode 100644
index 00000000..3547b70a
--- /dev/null
+++ b/test/components/__snapshots__/Switch.spec.ts.snap
@@ -0,0 +1,106 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Switch > renders basic case correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with as correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with checked correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with checkedIcon correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with class correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with defaultChecked correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with disabled correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with id correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with loading correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with loadingIcon correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with name correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with required correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with size 2xs correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with size lg correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with size md correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with size sm correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with size xl correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with size xs correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with ui correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with uncheckedIcon correctly 1`] = `
+"
+"
+`;
+
+exports[`Switch > renders with value correctly 1`] = `
+"
+"
+`;
diff --git a/test/components/__snapshots__/Tabs.spec.ts.snap b/test/components/__snapshots__/Tabs.spec.ts.snap
new file mode 100644
index 00000000..6d3301c0
--- /dev/null
+++ b/test/components/__snapshots__/Tabs.spec.ts.snap
@@ -0,0 +1,89 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Tabs > renders basic case correctly 1`] = `
+"
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
+
+exports[`Tabs > renders with class correctly 1`] = `
+"
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
+
+exports[`Tabs > renders with content slot correctly 1`] = `
+"
+
+
Content slot
+
Content slot
+
Content slot
+
"
+`;
+
+exports[`Tabs > renders with default slot correctly 1`] = `
+"
+
+
Default slot Default slot Default slot
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
+
+exports[`Tabs > renders with defaultValue correctly 1`] = `
+"
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
+
+exports[`Tabs > renders with modelValue correctly 1`] = `
+"
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
+
+exports[`Tabs > renders with orientation correctly 1`] = `
+"
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
+
+exports[`Tabs > renders with ui correctly 1`] = `
+"
+
+
This is the content shown for Tab1
+
And, this is the content for Tab2
+
Finally, this is the content for Tab3
+
"
+`;
diff --git a/test/components/__snapshots__/Textarea.spec.ts.snap b/test/components/__snapshots__/Textarea.spec.ts.snap
new file mode 100644
index 00000000..69e2e687
--- /dev/null
+++ b/test/components/__snapshots__/Textarea.spec.ts.snap
@@ -0,0 +1,35 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Textarea > renders basic case correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with color correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with default slot correctly 1`] = `"Default slot
"`;
+
+exports[`Textarea > renders with disabled correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with id correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with name correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with placeholder correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with required correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with rows correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size 2xs correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size lg correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size md correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size sm correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size xl correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with size xs correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with variant correctly 1`] = `"
"`;
diff --git a/test/components/__snapshots__/Tooltip.spec.ts.snap b/test/components/__snapshots__/Tooltip.spec.ts.snap
new file mode 100644
index 00000000..350afc3b
--- /dev/null
+++ b/test/components/__snapshots__/Tooltip.spec.ts.snap
@@ -0,0 +1,44 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Tooltip > renders with arrow correctly 1`] = `
+"
+
+
+
+
+
+"
+`;
+
+exports[`Tooltip > renders with shortcuts correctly 1`] = `
+"
+
+
+
+
Tooltip โ K
+ Tooltipv-if
+
+
+
+
+"
+`;
+
+exports[`Tooltip > renders with text correctly 1`] = `
+"
+
+
+
+
Tooltip
+
+ Tooltipv-ifv-if
+
+
+
+
+"
+`;
diff --git a/test/nuxt/nuxt.config.ts b/test/nuxt/nuxt.config.ts
index f1e300d2..17c4a8e3 100644
--- a/test/nuxt/nuxt.config.ts
+++ b/test/nuxt/nuxt.config.ts
@@ -1,5 +1,3 @@
export default defineNuxtConfig({
- modules: [
- '../../src/module'
- ]
+ modules: ['../../src/module']
})
diff --git a/test/nuxt/setup.ts b/test/nuxt/setup.ts
new file mode 100644
index 00000000..a0439168
--- /dev/null
+++ b/test/nuxt/setup.ts
@@ -0,0 +1,9 @@
+import { mockNuxtImport } from '@nuxt/test-utils/runtime'
+
+let id = 0
+
+mockNuxtImport('useId', () => {
+ return () => {
+ return id++
+ }
+})
diff --git a/tsconfig.json b/tsconfig.json
index 49dd48dd..f537e877 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,8 +1,5 @@
{
+ // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
- "exclude": ["docs", "dist", "playground", "node_modules"],
- "compilerOptions": {
- "noImplicitAny": false,
- "strictNullChecks": false
- }
+ "exclude": ["cli", "docs", "dist", "playground", "node_modules"]
}
diff --git a/vitest.config.mts b/vitest.config.mts
index 989f4f40..bde28b2e 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -3,7 +3,6 @@ import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
- testTimeout: 20000,
globals: true,
silent: true,
environment: 'nuxt',
@@ -11,6 +10,7 @@ export default defineVitestConfig({
nuxt: {
rootDir: fileURLToPath(new URL('test/nuxt/', import.meta.url))
}
- }
+ },
+ setupFiles: './setup.ts'
}
})