diff --git a/docs/app/components/content/examples/empty/EmptySlotsExample.vue b/docs/app/components/content/examples/empty/EmptySlotsExample.vue new file mode 100644 index 0000000000..ee134d31bc --- /dev/null +++ b/docs/app/components/content/examples/empty/EmptySlotsExample.vue @@ -0,0 +1,86 @@ + + + diff --git a/docs/content/docs/2.components/empty.md b/docs/content/docs/2.components/empty.md new file mode 100644 index 0000000000..4d68cc3e86 --- /dev/null +++ b/docs/content/docs/2.components/empty.md @@ -0,0 +1,199 @@ +--- +title: Empty +description: 'A component to display an empty state.' +category: data +links: + - label: GitHub + icon: i-simple-icons-github + to: https://github.com/nuxt/ui/blob/v4/src/runtime/components/Empty.vue +navigation.badge: Soon +--- + +## Usage + +::code-preview + +:::u-empty +--- +icon: i-lucide-file +title: No projects found +description: It looks like you haven't added any projects. Create one to get started. +actions: + - icon: i-lucide-plus + label: Create new + - icon: i-lucide-refresh-cw + label: Refresh + color: neutral + variant: subtle +--- +::: + +:: + +### Title + +Use the `title` prop to set the title of the empty state. + +::component-code +--- +props: + title: No projects found +--- +:: + +### Description + +Use the `description` prop to set the description of the empty state. + +::component-code +--- +prettier: true +ignore: + - title +props: + title: No projects found + description: It looks like you haven't added any projects. Create one to get started. +--- +:: + +### Icon + +Use the `icon` prop to set the icon of the empty state. + +::component-code +--- +prettier: true +ignore: + - title + - description +props: + icon: i-lucide-file + title: No projects found + description: It looks like you haven't added any projects. Create one to get started. +--- +:: + +### Avatar + +Use the `avatar` prop to set the avatar of the empty state. + +::component-code +--- +prettier: true +ignore: + - icon + - title + - description +props: + avatar.src: 'https://github.com/nuxt.png' + title: No projects found + description: It looks like you haven't added any projects. Create one to get started. +--- +:: + +### Actions + +Use the `actions` prop to add some [Button](/docs/components/button) actions to the empty state. + +::component-code +--- +prettier: true +ignore: + - icon + - title + - description + - actions +props: + icon: i-lucide-file + title: No projects found + description: It looks like you haven't added any projects. Create one to get started. + actions: + - icon: i-lucide-plus + label: Create new + - icon: i-lucide-refresh-cw + label: Refresh + color: neutral + variant: subtle +--- +:: + +### Variant + +Use the `variant` prop to change the variant of the empty state. + +::component-code +--- +prettier: true +ignore: + - icon + - title + - description + - actions +props: + variant: naked + icon: i-lucide-bell + title: No notifications + description: You're all caught up. New notifications will appear here. + actions: + - icon: i-lucide-refresh-cw + label: Refresh + color: neutral + variant: subtle +--- +:: + +### Size + +Use the `size` prop to change the size of the empty state. + +::component-code +--- +prettier: true +ignore: + - icon + - title + - description + - actions +props: + size: xl + icon: i-lucide-bell + title: No notifications + description: You're all caught up. New notifications will appear here. + actions: + - icon: i-lucide-refresh-cw + label: Refresh + color: neutral + variant: subtle +--- +:: + +## Examples + +### With slots + +Use the available slots to create a more complex empty state. + +::component-example +--- +collapse: true +name: 'empty-slots-example' +--- +:: + +## API + +### Props + +:component-props + +### Slots + +:component-slots + +## Theme + +:component-theme + +## Changelog + +:component-changelog diff --git a/docs/content/docs/2.components/form.md b/docs/content/docs/2.components/form.md index 7f361980ee..626fd5dbf3 100644 --- a/docs/content/docs/2.components/form.md +++ b/docs/content/docs/2.components/form.md @@ -159,7 +159,7 @@ props: Use the `nested` prop to nest multiple Form components and link their validation functions. In this case, validating the parent form will automatically validate all the other forms inside it. -Nested forms directly inherit their parent's state, so you don’t need to define a separate state for them. You can use the `name` prop to target a nested attribute within the parent's state. +Nested forms directly inherit their parent's state, so you don't need to define a separate state for them. You can use the `name` prop to target a nested attribute within the parent's state. It can be used to dynamically add fields based on user's input: diff --git a/docs/public/components/dark/empty.png b/docs/public/components/dark/empty.png new file mode 100644 index 0000000000..058aab93ec Binary files /dev/null and b/docs/public/components/dark/empty.png differ diff --git a/docs/public/components/light/empty.png b/docs/public/components/light/empty.png new file mode 100644 index 0000000000..776bc0e90a Binary files /dev/null and b/docs/public/components/light/empty.png differ diff --git a/package.json b/package.json index b4ff50e1ce..9950f4b3f7 100644 --- a/package.json +++ b/package.json @@ -106,8 +106,8 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "typecheck": "vue-tsc --noEmit && nuxt typecheck playgrounds/nuxt && nuxt typecheck docs && cd playgrounds/vue && vue-tsc --noEmit", - "test": "vitest", - "test:vue": "vitest -c vitest.vue.config.ts", + "test": "TZ=UTC vitest", + "test:vue": "TZ=UTC vitest -c vitest.vue.config.ts", "release": "release-it" }, "dependencies": { diff --git a/playgrounds/nuxt/app/composables/useNavigation.ts b/playgrounds/nuxt/app/composables/useNavigation.ts index b09aed56e9..6248b38e9c 100644 --- a/playgrounds/nuxt/app/composables/useNavigation.ts +++ b/playgrounds/nuxt/app/composables/useNavigation.ts @@ -27,6 +27,7 @@ const components = [ 'context-menu', 'drawer', 'dropdown-menu', + 'empty', 'error', 'field-group', 'file-upload', diff --git a/playgrounds/nuxt/app/pages/components/empty.vue b/playgrounds/nuxt/app/pages/components/empty.vue new file mode 100644 index 0000000000..128fe26b05 --- /dev/null +++ b/playgrounds/nuxt/app/pages/components/empty.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/runtime/components/Empty.vue b/src/runtime/components/Empty.vue new file mode 100644 index 0000000000..fde3dea1a1 --- /dev/null +++ b/src/runtime/components/Empty.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/runtime/components/prose/Field.vue b/src/runtime/components/prose/Field.vue index 4c62aa1aa6..2bcc38b04b 100644 --- a/src/runtime/components/prose/Field.vue +++ b/src/runtime/components/prose/Field.vue @@ -16,7 +16,7 @@ export interface ProseFieldProps { */ name?: string /** - * Expected type of the field’s value + * Expected type of the field's value */ type?: string /** diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index 85108d4498..015f48fc9e 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -40,6 +40,7 @@ export * from '../components/DashboardSidebarToggle.vue' export * from '../components/DashboardToolbar.vue' export * from '../components/Drawer.vue' export * from '../components/DropdownMenu.vue' +export * from '../components/Empty.vue' export * from '../components/Error.vue' export * from '../components/FieldGroup.vue' export * from '../components/FileUpload.vue' @@ -76,10 +77,10 @@ export * from '../components/PageLogos.vue' export * from '../components/PageSection.vue' export * from '../components/Pagination.vue' export * from '../components/PinInput.vue' +export * from '../components/Popover.vue' export * from '../components/PricingPlan.vue' export * from '../components/PricingPlans.vue' export * from '../components/PricingTable.vue' -export * from '../components/Popover.vue' export * from '../components/Progress.vue' export * from '../components/RadioGroup.vue' export * from '../components/Select.vue' diff --git a/src/theme/empty.ts b/src/theme/empty.ts new file mode 100644 index 0000000000..f366a6875a --- /dev/null +++ b/src/theme/empty.ts @@ -0,0 +1,67 @@ +export default { + slots: { + root: 'relative flex flex-col items-center justify-center gap-4 rounded-lg p-4 sm:p-6 lg:p-8 min-w-0', + header: 'flex flex-col items-center gap-2 max-w-sm text-center', + avatar: 'shrink-0 mb-2', + title: 'text-highlighted text-pretty font-medium', + description: 'text-balance text-center', + body: 'flex flex-col items-center gap-4 max-w-sm', + actions: 'flex flex-wrap justify-center gap-2 shrink-0', + footer: 'flex flex-col items-center gap-2 max-w-sm' + }, + variants: { + size: { + xs: { + avatar: 'size-8 text-base', + title: 'text-sm', + description: 'text-xs' + }, + sm: { + avatar: 'size-9 text-lg', + title: 'text-sm', + description: 'text-xs' + }, + md: { + avatar: 'size-10 text-xl', + title: 'text-base', + description: 'text-sm' + }, + lg: { + avatar: 'size-11 text-[22px]', + title: 'text-base', + description: 'text-sm' + }, + xl: { + avatar: 'size-12 text-2xl', + title: 'text-lg', + description: 'text-base' + } + }, + variant: { + solid: { + root: 'bg-inverted', + title: 'text-inverted', + description: 'text-dimmed' + }, + outline: { + root: 'bg-default ring ring-default', + description: 'text-muted' + }, + soft: { + root: 'bg-elevated/50', + description: 'text-toned' + }, + subtle: { + root: 'bg-elevated/50 ring ring-default', + description: 'text-toned' + }, + naked: { + description: 'text-muted' + } + } + }, + defaultVariants: { + variant: 'outline', + size: 'md' + } +} diff --git a/src/theme/index.ts b/src/theme/index.ts index f27b4d0414..2d40b33607 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -39,6 +39,7 @@ export { default as dashboardSidebarToggle } from './dashboard-sidebar-toggle' export { default as dashboardToolbar } from './dashboard-toolbar' export { default as drawer } from './drawer' export { default as dropdownMenu } from './dropdown-menu' +export { default as empty } from './empty' export { default as error } from './error' export { default as fieldGroup } from './field-group' export { default as fileUpload } from './file-upload' diff --git a/test/components/Empty.spec.ts b/test/components/Empty.spec.ts new file mode 100644 index 0000000000..27aed4169a --- /dev/null +++ b/test/components/Empty.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest' +import Empty from '../../src/runtime/components/Empty.vue' +import type { EmptyProps, EmptySlots } from '../../src/runtime/components/Empty.vue' +import ComponentRender from '../component-render' +import theme from '#build/ui/empty' + +describe('Empty', () => { + const variants = Object.keys(theme.variants.variant) as any + const sizes = Object.keys(theme.variants.size) as any + + const props = { + icon: 'i-lucide-file', + title: 'Title', + description: 'Description', + actions: [{ icon: 'i-lucide-plus', label: 'Add' }] + } + + it.each([ + // Props + ['with as', { props: { as: 'section' } }], + ['with icon', { props: { icon: 'i-lucide-file' } }], + ['with avatar', { props: { avatar: { src: 'https://github.com/benjamincanac.png' } } }], + ['with title', { props: { icon: 'i-lucide-file', title: 'Title' } }], + ['with description', { props: { icon: 'i-lucide-file', title: 'Title', description: 'Description' } }], + ['with actions', { props: { icon: 'i-lucide-file', title: 'Title', description: 'Description', actions: [{ icon: 'i-lucide-plus', label: 'Add' }] } }], + ...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant } }]), + ...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]), + ['with class', { props: { ...props, class: 'gap-6' } }], + ['with ui', { props: { ...props, ui: {} } }], + // Slots + ['with header slot', { props, slots: { header: () => 'Header slot' } }], + ['with leading slot', { props, slots: { leading: () => 'Leading slot' } }], + ['with title slot', { props, slots: { title: () => 'Title slot' } }], + ['with description slot', { props, slots: { description: () => 'Description slot' } }], + ['with body slot', { props, slots: { body: () => 'Body slot' } }], + ['with actions slot', { props, slots: { actions: () => 'Actions slot' } }], + ['with footer slot', { props, slots: { footer: () => 'Footer slot' } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: EmptyProps, slots?: Partial }) => { + const html = await ComponentRender(nameOrHtml, options, Empty) + expect(html).toMatchSnapshot() + }) +}) diff --git a/test/components/__snapshots__/Empty-vue.spec.ts.snap b/test/components/__snapshots__/Empty-vue.spec.ts.snap new file mode 100644 index 0000000000..d7a31c4334 --- /dev/null +++ b/test/components/__snapshots__/Empty-vue.spec.ts.snap @@ -0,0 +1,343 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Empty > renders with actions correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with actions slot correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
Actions slot
+
+ +
" +`; + +exports[`Empty > renders with as correctly 1`] = ` +"
+ + + +
" +`; + +exports[`Empty > renders with avatar correctly 1`] = ` +"
+
+ + +
+ + +
" +`; + +exports[`Empty > renders with body slot correctly 1`] = ` +"
+
+

Title

+
Description
+
+
Body slot
+ +
" +`; + +exports[`Empty > renders with class correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with description correctly 1`] = ` +"
+
+

Title

+
Description
+
+ + +
" +`; + +exports[`Empty > renders with description slot correctly 1`] = ` +"
+
+

Title

+
Description slot
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with footer slot correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+
Footer slot
+
" +`; + +exports[`Empty > renders with header slot correctly 1`] = ` +"
+
Header slot
+
+
+
+ +
" +`; + +exports[`Empty > renders with icon correctly 1`] = ` +"
+
+ + +
+ + +
" +`; + +exports[`Empty > renders with leading slot correctly 1`] = ` +"
+
Leading slot

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant naked correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant outline correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant soft correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant solid correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant subtle correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size lg correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size md correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size sm correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size xl correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size xs correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with title correctly 1`] = ` +"
+
+

Title

+ +
+ + +
" +`; + +exports[`Empty > renders with title slot correctly 1`] = ` +"
+
+

Title slot

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with ui correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; diff --git a/test/components/__snapshots__/Empty.spec.ts.snap b/test/components/__snapshots__/Empty.spec.ts.snap new file mode 100644 index 0000000000..372bef4e53 --- /dev/null +++ b/test/components/__snapshots__/Empty.spec.ts.snap @@ -0,0 +1,343 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Empty > renders with actions correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with actions slot correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
Actions slot
+
+ +
" +`; + +exports[`Empty > renders with as correctly 1`] = ` +"
+ + + +
" +`; + +exports[`Empty > renders with avatar correctly 1`] = ` +"
+
+ + +
+ + +
" +`; + +exports[`Empty > renders with body slot correctly 1`] = ` +"
+
+

Title

+
Description
+
+
Body slot
+ +
" +`; + +exports[`Empty > renders with class correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with description correctly 1`] = ` +"
+
+

Title

+
Description
+
+ + +
" +`; + +exports[`Empty > renders with description slot correctly 1`] = ` +"
+
+

Title

+
Description slot
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with footer slot correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+
Footer slot
+
" +`; + +exports[`Empty > renders with header slot correctly 1`] = ` +"
+
Header slot
+
+
+
+ +
" +`; + +exports[`Empty > renders with icon correctly 1`] = ` +"
+
+ + +
+ + +
" +`; + +exports[`Empty > renders with leading slot correctly 1`] = ` +"
+
Leading slot

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant naked correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant outline correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant soft correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant solid correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with primary variant subtle correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size lg correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size md correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size sm correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size xl correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with size xs correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with title correctly 1`] = ` +"
+
+

Title

+ +
+ + +
" +`; + +exports[`Empty > renders with title slot correctly 1`] = ` +"
+
+

Title slot

+
Description
+
+
+
+
+ +
" +`; + +exports[`Empty > renders with ui correctly 1`] = ` +"
+
+

Title

+
Description
+
+
+
+
+ +
" +`;