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: `
Open menu
`,
}),
}
/**
* 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: `
Open menu
`,
}),
}
/**
* 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: `
Open drawerTitle
`,
}),
}
/**
* 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: `
Open menu
`,
}),
}
/**
* 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