import type { Meta, StoryObj } from '@storybook/vue3-vite' import { computed, ref } from 'vue' import CpButton from '@/components/CpButton.vue' import CpMenu from '@/components/CpMenu.vue' const meta = { title: 'Molecules/CpMenu', component: CpMenu, parameters: { docs: { description: { component: 'A popover-based menu built on top of `primevue/popover`. The trigger is provided through the `trigger` slot — clicking it toggles the menu, which is anchored to the trigger element. Items can be passed through the `items` prop (with icons, separators, async commands, critical styling) or rendered via the default slot for arbitrary content.', }, }, }, argTypes: { items: { control: 'object', description: 'The menu items to display. Each item is a PrimeVue `MenuItem`.', table: { type: { summary: 'MenuItem[]' }, defaultValue: { summary: '[]' }, }, }, forcePopover: { control: 'boolean', description: 'Disable the bottom-drawer behavior on mobile and keep the popover anchored to the trigger.', table: { defaultValue: { summary: 'false' } }, }, isFullHeightDrawer: { control: 'boolean', description: 'When enabled, the mobile drawer takes the full available height.', table: { defaultValue: { summary: 'false' } }, }, keepOpenOnClick: { control: 'boolean', description: 'Keep the menu open when clicking an item.', table: { defaultValue: { summary: 'false' } }, }, placement: { control: 'select', options: ['start', 'end'], description: 'Horizontal alignment of the popover relative to the trigger. `start`/`end` align the popover edge to the matching trigger edge; the menu auto-flips horizontally when there is not enough room.', table: { defaultValue: { summary: 'start' } }, }, }, decorators: [() => ({ template: '
' })], } satisfies Meta export default meta type Story = StoryObj /** * Click the trigger to toggle a menu featuring an async "Download" action, * a separator and a critical "Delete" action. */ export const Default: Story = { args: { forcePopover: true, placement: 'start', }, render: (args) => ({ components: { CpButton, CpMenu }, setup() { const isLoading = ref(false) const items = computed(() => [ { label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked'), }, { label: 'Download', leadingIcon: 'download', isLoading: isLoading.value, isAsync: true, command: async () => { isLoading.value = true await new Promise((resolve) => setTimeout(resolve, 2000)) isLoading.value = false }, }, { separator: true }, { label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked'), }, ]) return { args, items } }, template: ` `, }), } /** * With `forcePopover`, the menu stays anchored to its trigger even on * mobile viewports — the bottom-drawer behavior (slide-up, backdrop, * swipe-to-dismiss) is disabled. Use it when the popover content is * compact and you don't want a fullscreen-ish drawer. */ export const ForcePopoverOnMobile: Story = { args: { forcePopover: true, }, render: (args) => ({ components: { CpButton, CpMenu }, setup() { const items = [ { label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') }, { label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') }, { separator: true }, { label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') }, ] return { args, items } }, template: ` `, }), } /** * In mobile viewports, enable `fullHeightDrawer` to make the bottom drawer * fill all available height. */ export const FullHeightDrawer: Story = { args: { isFullHeightDrawer: true, }, globals: { viewport: { value: 'mobile1', isRotated: false }, }, render: (args) => ({ components: { CpButton, CpMenu }, setup() { const items = [ { label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') }, { label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') }, { label: 'Move', leadingIcon: 'arrow-right-left', command: () => alert('Move clicked') }, { label: 'Archive', leadingIcon: 'archive', command: () => alert('Archive clicked') }, { separator: true }, { label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') }, ] return { args, items } }, template: ` `, }), } /** * Use `placement` to control the popover's horizontal alignment. Switch the * `placement` control to compare `start` and `end`: each aligns the popover's * matching edge to the trigger's. The popover auto-flips horizontally when it * would overflow. The trigger is centered here so the difference is easy to see. */ export const Placement: Story = { args: { forcePopover: true, placement: 'end', }, render: (args) => ({ components: { CpButton, CpMenu }, setup() { const items = [ { label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') }, { label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') }, { separator: true }, { label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') }, ] return { args, items } }, template: `
`, }), } /** * Use the default slot to render arbitrary content inside the popover — * useful for forms, share panels, filters, etc. */ export const CustomContent: Story = { render: () => ({ components: { CpButton, CpMenu }, template: `
Share this document Copy link
`, }), }