diff --git a/playground/pages/navigation-menu.vue b/playground/pages/navigation-menu.vue index 81a18daa..2d1359bd 100644 --- a/playground/pages/navigation-menu.vue +++ b/playground/pages/navigation-menu.vue @@ -1,33 +1,75 @@ diff --git a/src/runtime/components/NavigationMenu.vue b/src/runtime/components/NavigationMenu.vue index 62fa2871..fae6ea7a 100644 --- a/src/runtime/components/NavigationMenu.vue +++ b/src/runtime/components/NavigationMenu.vue @@ -1,28 +1,47 @@ @@ -61,41 +88,83 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0] + +
+ +
+ + + +
diff --git a/src/theme/navigation-menu.ts b/src/theme/navigation-menu.ts index b16c0a3e..027291d9 100644 --- a/src/theme/navigation-menu.ts +++ b/src/theme/navigation-menu.ts @@ -1,75 +1,191 @@ -export default { +export default (config: { colors: string[] }) => ({ slots: { root: 'relative flex gap-1.5', list: 'isolate min-w-0', - itemWrapper: 'min-w-0', - item: 'group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] 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', - itemLeadingIcon: 'shrink-0 size-5', - itemLeadingAvatar: 'shrink-0', - itemLabel: 'truncate', - itemTrailing: 'ms-auto', - itemTrailingBadge: 'shrink-0 rounded', - separator: 'px-2' + item: 'min-w-0', + link: 'group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] 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', + linkLeadingIcon: 'shrink-0 size-5', + linkLeadingAvatar: 'shrink-0', + linkTrailing: 'ms-auto inline-flex', + linkTrailingBadge: 'shrink-0 rounded', + linkTrailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-[state=open]:rotate-180', + linkLabel: 'truncate', + linkLabelExternalIcon: 'size-3 align-top text-gray-400 dark:text-gray-500', + childList: 'grid grid-cols-2 gap-2 p-2', + childItem: '', + childLink: 'group size-full px-3 py-2 rounded-md flex items-start gap-2 text-left', + childLinkWrapper: 'flex flex-col items-start', + childLinkLabel: 'font-semibold text-sm relative inline-flex', + childLinkDescription: 'text-sm text-gray-500 dark:text-gray-400', + childLinkIcon: 'size-5 shrink-0', + childLinkExternalIcon: 'size-3 align-top text-gray-400 dark:text-gray-500', + separator: 'px-2 h-px bg-gray-200 dark:bg-gray-800', + viewportWrapper: 'absolute top-full inset-x-0 flex w-full', + // FIXME: add `sm:w-[var(--radix-navigation-menu-viewport-width)]` / `transition-[width,height]` / `origin-[top_center]` once position is based on trigger + viewport: 'relative overflow-hidden mt-2.5 bg-white dark:bg-gray-900 shadow-lg rounded-md ring ring-gray-200 dark:ring-gray-800 will-change-[transform,opacity] h-[--radix-navigation-menu-viewport-height] w-full data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]', + content: 'absolute top-0 left-0 w-full data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]', + indicator: 'data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] top-full z-[1] flex h-2.5 items-end justify-center overflow-hidden transition-transform duration-200 ease-out', + arrow: 'relative top-[50%] size-2.5 rotate-45 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 z-[1] rounded-sm' }, variants: { + color: { + ...Object.fromEntries(config.colors.map((color: string) => [color, ''])), + black: '' + }, + variant: { + pill: '', + line: '', + link: '' + }, orientation: { horizontal: { root: 'w-full items-center justify-between', list: 'flex items-center', - item: 'px-2.5 py-3.5 before:inset-x-0 before:inset-y-2 after:absolute after:bottom-0 after:inset-x-2.5 after:block after:h-0.5 after:mt-2 after:rounded-full' + link: 'px-2.5 py-1.5 before:inset-x-px before:inset-y-0' }, vertical: { root: 'flex-col', - item: 'px-2.5 py-1.5 before:inset-px' + link: 'px-2.5 py-1.5 before:inset-y-px before:inset-x-0' } }, active: { true: { - item: 'text-gray-900 dark:text-white', - itemLeadingIcon: 'text-gray-700 dark:text-gray-200' + link: 'text-gray-900 dark:text-white', + linkLeadingIcon: 'text-gray-700 dark:text-gray-200', + childLink: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white', + childLinkIcon: 'text-gray-700 dark:text-gray-200' + }, false: { - item: 'text-gray-500 dark:text-gray-400', - itemLeadingIcon: 'text-gray-400 dark:text-gray-500' + link: 'text-gray-500 dark:text-gray-400', + linkLeadingIcon: 'text-gray-400 dark:text-gray-500', + childLink: 'hover:bg-gray-50 dark:hover:bg-gray-800/50 text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white transition-colors', + childLinkIcon: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors' } }, disabled: { true: { - item: 'cursor-not-allowed opacity-75' + link: 'cursor-not-allowed opacity-75' } } }, compoundVariants: [{ + variant: 'line', + orientation: 'horizontal', + class: { + link: 'after:absolute after:-bottom-2 after:inset-x-2.5 after:block after:h-0.5 after:rounded-full' + } + }, { + variant: 'line', + orientation: 'vertical', + class: { + link: 'after:absolute after:-left-2 after:inset-y-0.5 after:block after:w-0.5 after:rounded-full' + } + }, { disabled: false, active: false, + variant: 'pill', class: { - item: 'hover:text-gray-900 dark:hover:text-white transition-colors', - itemLeadingIcon: 'group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors' + link: 'hover:text-gray-900 dark:hover:text-white transition-colors hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 before:transition-colors', + linkLeadingIcon: 'group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors' } - }, { - orientation: 'horizontal', + }, ...config.colors.map((color: string) => ({ + color, + variant: 'pill', active: true, class: { - item: 'after:bg-primary-500 dark:after:bg-primary-400' + link: `text-${color}-500 dark:text-${color}-400 before:bg-gray-100 dark:before:bg-gray-800`, + linkLeadingIcon: `text-${color}-500 dark:text-${color}-400` + } + })), { + color: 'black', + variant: 'pill', + active: true, + class: { + link: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800', + linkLeadingIcon: 'text-gray-900 dark:text-white' } }, { - orientation: 'horizontal', disabled: false, + variant: 'line', class: { - item: 'hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 before:transition-colors' + link: 'hover:text-gray-900 dark:hover:text-white transition-colors hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 before:transition-colors', + linkLeadingIcon: 'group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors' } - }, { - orientation: 'vertical', + }, ...config.colors.map((color: string) => ({ + color, + variant: 'line', active: true, class: { - item: 'before:bg-gray-100 dark:before:bg-gray-800' + link: `after:bg-${color}-500 dark:after:bg-${color}-400` + } + })), { + color: 'black', + variant: 'line', + active: true, + class: { + link: 'after:bg-gray-900 dark:after:bg-white' } }, { - orientation: 'vertical', + disabled: false, active: false, - disabled: false, + variant: 'link', class: { - item: 'hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 before:transition-colors' + link: 'hover:text-gray-900 dark:hover:text-white transition-colors', + linkLeadingIcon: 'group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors' } - }] -} + }, ...config.colors.map((color: string) => ({ + color, + variant: 'link', + active: true, + class: { + link: `text-${color}-500 dark:text-${color}-400`, + linkLeadingIcon: `text-${color}-500 dark:text-${color}-400` + } + })), { + color: 'black', + variant: 'link', + active: true, + class: { + link: 'text-gray-900 dark:text-white', + linkLeadingIcon: 'text-gray-900 dark:text-white' + } + }], + // compoundVariants: [{ + // disabled: false, + // active: false, + // class: { + // item: 'hover:text-gray-900 dark:hover:text-white transition-colors', + // itemLeadingIcon: 'group-hover:text-gray-700 dark:group-hover:text-gray-200 transition-colors' + // } + // }, { + // orientation: 'horizontal', + // active: true, + // class: { + // item: 'after:bg-primary-500 dark:after:bg-primary-400' + // } + // }, { + // orientation: 'horizontal', + // disabled: false, + // class: { + // item: 'hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 before:transition-colors' + // } + // }, { + // orientation: 'vertical', + // active: true, + // class: { + // item: 'before:bg-gray-100 dark:before:bg-gray-800' + // } + // }, { + // orientation: 'vertical', + // active: false, + // disabled: false, + // class: { + // item: 'hover:before:bg-gray-50 dark:hover:before:bg-gray-800/50 before:transition-colors' + // } + // }], + defaultVariants: { + color: 'primary', + variant: 'pill' + } +}) diff --git a/test/components/NavigationMenu.spec.ts b/test/components/NavigationMenu.spec.ts index 0b3f2845..63aaea31 100644 --- a/test/components/NavigationMenu.spec.ts +++ b/test/components/NavigationMenu.spec.ts @@ -1,37 +1,73 @@ import { describe, it, expect } from 'vitest' import NavigationMenu, { type NavigationMenuProps, type NavigationMenuSlots } from '../../src/runtime/components/NavigationMenu.vue' import ComponentRender from '../component-render' +import theme from '#build/ui/navigation-menu' describe('NavigationMenu', () => { + const colors = Object.keys(theme.variants.color) as any + const variants = Object.keys(theme.variants.variant) as any + const items = [ [{ - label: 'Profile', + label: 'Documentation', + icon: 'i-heroicons-book-open', + children: [{ + label: 'Introduction', + description: 'Fully styled and customizable components for Nuxt.', + icon: 'i-heroicons-home' + }, { + label: 'Installation', + description: 'Learn how to install and configure Nuxt UI in your application.', + icon: 'i-heroicons-cloud-arrow-down' + }, { + label: 'Theming', + description: 'Learn how to customize the look and feel of the components.', + icon: 'i-heroicons-swatch' + }, { + label: 'Shortcuts', + description: 'Learn how to display and define keyboard shortcuts in your app.', + icon: 'i-heroicons-computer-desktop' + }] + }, { + label: 'Components', + icon: 'i-heroicons-cube-transparent', active: true, - avatar: { - src: 'https://avatars.githubusercontent.com/u/739984?v=4' - }, - badge: 100, - select() { - console.log('Profile clicked') - } - }, { - 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' + children: [{ + label: 'Link', + icon: 'i-heroicons-document', + description: 'Use NuxtLink with superpowers.', + to: '/link' + }, { + label: 'Modal', + icon: 'i-heroicons-document', + description: 'Display a modal within your application.', + to: '/modal' + }, { + label: 'NavigationMenu', + icon: 'i-heroicons-document', + description: 'Display a list of links.', + to: '/navigation-menu' + }, { + label: 'Pagination', + icon: 'i-heroicons-document', + description: 'Display a list of pages.', + to: '/pagination' + }, { + label: 'Popover', + icon: 'i-heroicons-document', + description: 'Display a non-modal dialog that floats around a trigger element.', + to: '/popover' + }, { + label: 'Progress', + icon: 'i-heroicons-document', + description: 'Show a horizontal bar to indicate task progression.', + to: '/progress' + }] }], [{ - label: 'Examples', - icon: 'i-heroicons-light-bulb', - to: 'https://ui.nuxt.com', - target: '_blank', - slot: 'custom' + label: 'GitHub', + icon: 'i-simple-icons-github', + to: 'https://github.com/nuxt/ui', + target: '_blank' }, { label: 'Help', icon: 'i-heroicons-question-mark-circle', @@ -44,7 +80,11 @@ describe('NavigationMenu', () => { it.each([ // Props ['with items', { props }], + ['with arrow', { props: { ...props, arrow: true } }], ['with orientation vertical', { props: { ...props, orientation: 'vertical' as const } }], + ...colors.map((color: string) => [`with color ${color}`, { props: { ...props, color } }]), + ...variants.map((variant: string) => [`with variant ${variant}`, { props: { ...props, variant } }]), + ['with trailingIcon', { props: { ...props, trailingIcon: 'i-heroicons-plus' } }], ['with class', { props: { ...props, class: 'w-48' } }], ['with ui', { props: { items, ui: { itemLeadingIcon: 'size-4' } } }], // Slots diff --git a/test/components/__snapshots__/NavigationMenu.spec.ts.snap b/test/components/__snapshots__/NavigationMenu.spec.ts.snap index 228e5185..2c838da4 100644 --- a/test/components/__snapshots__/NavigationMenu.spec.ts.snap +++ b/test/components/__snapshots__/NavigationMenu.spec.ts.snap @@ -1,33 +1,250 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`NavigationMenu > renders with arrow correctly 1`] = ` +"" +`; + exports[`NavigationMenu > renders with class correctly 1`] = ` "" +`; + +exports[`NavigationMenu > renders with color black correctly 1`] = ` +"" +`; + +exports[`NavigationMenu > renders with color green correctly 1`] = ` +"" +`; + +exports[`NavigationMenu > renders with color primary correctly 1`] = ` +"" +`; + +exports[`NavigationMenu > renders with color red correctly 1`] = ` +"" `; @@ -35,28 +252,40 @@ exports[`NavigationMenu > renders with custom slot correctly 1`] = ` "" `; @@ -64,22 +293,36 @@ exports[`NavigationMenu > renders with item slot correctly 1`] = ` "" `; @@ -87,30 +330,40 @@ exports[`NavigationMenu > renders with item-label slot correctly 1`] = ` "" `; @@ -118,30 +371,40 @@ exports[`NavigationMenu > renders with item-leading slot correctly 1`] = ` "" `; @@ -149,22 +412,36 @@ exports[`NavigationMenu > renders with item-trailing slot correctly 1`] = ` "" `; @@ -172,30 +449,40 @@ exports[`NavigationMenu > renders with items correctly 1`] = ` "" `; @@ -203,33 +490,78 @@ exports[`NavigationMenu > renders with orientation vertical correctly 1`] = ` "" +`; + +exports[`NavigationMenu > renders with trailingIcon correctly 1`] = ` +"" `; @@ -237,29 +569,162 @@ exports[`NavigationMenu > renders with ui correctly 1`] = ` "" +`; + +exports[`NavigationMenu > renders with variant line correctly 1`] = ` +"" +`; + +exports[`NavigationMenu > renders with variant link correctly 1`] = ` +"" +`; + +exports[`NavigationMenu > renders with variant pill correctly 1`] = ` +"" `;