import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { fn } from 'storybook/test'
import { Arrow01RightIcon, ColorsIcon, PlusCircleIcon, PlayCircleIcon, CheckCircleIcon, PenEditIcon, CalendarDaysIcon, VialIcon, TerminalIcon, TrashIcon, ComputerMouseIcon, FolderIcon } from '../components/icons-v2-generated'
import { PageActions } from '../components/ui/page-actions'
const meta = {
title: 'UI/PageActions',
component: PageActions,
parameters: {
layout: 'padded',
docs: {
description: {
component: `
PageActions component for displaying action buttons in page headers.
**Three variants:**
1. \`icon-buttons\` - Buttons with icons, collapses to dropdown menu on mobile
2. \`primary-buttons\` - Primary + outline buttons, fixed to bottom on mobile
3. \`menu-primary\` - ActionsMenuDropdown ("...") + primary button on desktop, all actions expand to fixed bottom bar on mobile
Use with \`PageContainer\` headerActions prop for consistent page layouts.
`,
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['icon-buttons', 'primary-buttons', 'menu-primary'],
description: 'Layout variant for the action buttons',
},
actions: {
description: 'Array of action buttons to display',
},
className: {
control: 'text',
description: 'Additional CSS classes for the container',
},
},
decorators: [
(Story) => (
),
],
} satisfies Meta
export default meta
type Story = StoryObj
/**
* Icon buttons variant - used for list pages like Scripts.
* Shows buttons with icons on desktop, collapses to ActionsMenuDropdown on mobile.
*/
export const IconButtons: Story = {
args: {
variant: 'icon-buttons',
actions: [
{
label: 'Edit Categories',
onClick: fn(),
icon: ,
},
{
label: 'Add Script',
onClick: fn(),
icon: ,
},
],
},
}
/**
* Primary buttons variant - used for form/detail pages.
* Shows primary + outline buttons on desktop, fixed bottom bar on mobile.
*/
export const PrimaryButtons: Story = {
args: {
variant: 'primary-buttons',
actions: [
{
label: 'Save Script',
onClick: fn(),
variant: 'accent',
},
{
label: 'Test Script',
onClick: fn(),
variant: 'outline',
},
],
},
}
/**
* Primary buttons with icons
*/
export const PrimaryButtonsWithIcons: Story = {
args: {
variant: 'primary-buttons',
actions: [
{
label: 'Save Changes',
onClick: fn(),
variant: 'accent',
icon: ,
},
{
label: 'Preview',
onClick: fn(),
variant: 'outline',
icon: ,
},
],
},
}
/**
* Single primary button
*/
export const SinglePrimaryButton: Story = {
args: {
variant: 'primary-buttons',
actions: [
{
label: 'Create New',
onClick: fn(),
variant: 'accent',
icon: ,
},
],
},
}
/**
* Single icon button - shows as icon on mobile instead of collapsing to menu
*/
export const SingleIconButton: Story = {
args: {
variant: 'icon-buttons',
actions: [
{
label: 'Add Script',
onClick: fn(),
icon: ,
},
],
},
parameters: {
docs: {
description: {
story: 'When there is only one action in icon-buttons variant, it displays as an icon button on mobile instead of collapsing to an ActionsMenuDropdown.',
},
},
},
}
/**
* Icon buttons with three actions
*/
export const ThreeIconButtons: Story = {
args: {
variant: 'icon-buttons',
actions: [
{
label: 'Edit',
onClick: fn(),
icon: ,
},
{
label: 'Preview',
onClick: fn(),
icon: ,
},
{
label: 'Add',
onClick: fn(),
icon: ,
},
],
},
}
/**
* With disabled and loading states
*/
export const WithStates: Story = {
args: {
variant: 'primary-buttons',
actions: [
{
label: 'Saving...',
onClick: fn(),
variant: 'accent',
loading: true,
},
{
label: 'Cancel',
onClick: fn(),
variant: 'outline',
disabled: true,
},
],
},
}
/**
* Menu + primary variant - shows ActionsMenuDropdown ("...") + primary button on desktop.
* On mobile all actions expand into a fixed bottom bar.
*/
export const MenuPrimary: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Run Script',
onClick: fn(),
variant: 'accent',
icon: ,
},
],
menuActions: [
{
items: [
{
id: 'edit-script',
label: 'Edit Script',
onClick: fn(),
icon: ,
},
{
id: 'schedule-script',
label: 'Schedule Script',
onClick: fn(),
icon: ,
},
{
id: 'test-script',
label: 'Test Script',
onClick: fn(),
icon: ,
},
],
},
],
},
}
/**
* Menu + primary with a single menu action
*/
export const MenuPrimarySingleAction: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Save Changes',
onClick: fn(),
variant: 'accent',
icon: ,
},
],
menuActions: [
{
items: [
{
id: 'delete',
label: 'Delete',
onClick: fn(),
},
],
},
],
},
}
/**
* Menu + primary with loading state on the primary button
*/
export const MenuPrimaryLoading: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Running...',
onClick: fn(),
variant: 'accent',
loading: true,
},
],
menuActions: [
{
items: [
{
id: 'edit-script',
label: 'Edit Script',
onClick: fn(),
icon: ,
},
{
id: 'schedule-script',
label: 'Schedule Script',
onClick: fn(),
icon: ,
},
],
},
],
},
}
/**
* Mobile-only actions with `showOnlyMobile` — some actions appear only on mobile.
* "Edit Categories" is hidden on desktop and only shows in the mobile menu.
*/
export const MobileOnlyActions: Story = {
args: {
variant: 'icon-buttons',
actions: [
{
label: 'Add Script',
onClick: fn(),
icon: ,
},
{
label: 'Edit Categories',
onClick: fn(),
icon: ,
showOnlyMobile: true,
},
{
label: 'Preview',
onClick: fn(),
icon: ,
},
],
},
parameters: {
docs: {
description: {
story:
'Use the `showOnlyMobile` prop to hide specific actions on desktop. They will still appear in the mobile menu or bottom bar.',
},
},
},
}
/**
* Link actions — any action can render as a Next.js `` by providing `href`.
* The `` element lands in the DOM (crawler-visible) and clicking routes
* client-side. Mix freely with onClick-only actions; use `openInNewTab` for
* external links.
*/
export const LinkActions: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Open Docs',
href: '/docs',
variant: 'accent',
icon: ,
},
],
menuActions: [
{
items: [
{
id: 'github',
label: 'GitHub',
href: 'https://github.com/flamingo',
icon: ,
},
{
id: 'changelog',
label: 'Changelog',
href: '/changelog',
icon: ,
},
{
id: 'run-diagnostics',
label: 'Run Diagnostics',
onClick: fn(),
icon: ,
},
],
},
],
},
parameters: {
docs: {
description: {
story:
'Primary action navigates to `/docs` via Next Link. Menu has two link actions (internal + external in new tab) and one onClick-only action. Inspect the DOM — link actions render as real `` inside the dropdown menu.',
},
},
},
}
/**
* Split button — a primary action with a chevron that opens a dropdown of
* alternatives. Matches the Figma device-page pattern where "Remote Shell"
* sits next to a "..." menu, and the chevron reveals CMD / Power Shell.
*
* On mobile everything collapses into a single "..." menu.
*/
export const SplitButtonWithMenu: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Remote Shell',
variant: 'outline',
submenu: [
{
id: 'cmd',
label: 'CMD',
onClick: fn(),
icon: ,
},
{
id: 'power-shell',
label: 'Power Shell',
onClick: fn(),
icon: ,
},
],
},
],
menuActions: [
{
items: [
{
id: 'edit-device',
label: 'Edit Device',
onClick: fn(),
icon: ,
},
{
id: 'delete-device',
label: 'Delete Device',
onClick: fn(),
icon: ,
},
],
},
],
},
parameters: {
docs: {
description: {
story:
'Provide `submenu` on an action to render a split button with a chevron that opens a dropdown of alternatives. On mobile the submenu items are flattened into the "..." menu alongside `menuActions`.',
},
},
},
}
/**
* Two-target SplitButton — primary action on the left half, secondary action
* (e.g. open in new tab) on the right half. Each half is independently clickable.
*/
export const SplitButtonTwoActions: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Open Docs',
onClick: fn(),
variant: 'accent',
icon: ,
iconAction: {
icon: ,
'aria-label': 'Open docs in new tab',
href: 'https://example.com/docs',
openInNewTab: true,
},
},
],
menuActions: [
{
items: [
{
id: 'edit',
label: 'Edit',
onClick: fn(),
icon: ,
},
],
},
],
},
parameters: {
docs: {
description: {
story:
'Provide `iconAction` on an action to render it as a `SplitButton` (two independent click targets). Main half runs the primary `onClick`/`href`; the icon half runs its own action — typical use case is "open in new tab".',
},
},
},
}
/**
* Menu items with a secondary action — each row in the dropdown can have its
* own 40px icon button on the right (with vertical divider). Matches the Figma
* device-page menu where rows like "Remote Control" have an arrow that opens
* the app in a new tab while the main row navigates inside the current page.
*/
export const MenuItemsWithIconAction: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Save Changes',
onClick: fn(),
variant: 'accent',
icon: ,
},
],
menuActions: [
{
items: [
{
id: 'remote-control',
label: 'Remote Control',
href: '/devices/remote-control',
icon: ,
iconAction: {
icon: ,
'aria-label': 'Open Remote Control in new tab',
href: '/devices/remote-control',
openInNewTab: true,
},
},
{
id: 'file-manager',
label: 'File Manager',
href: '/devices/file-manager',
icon: ,
iconAction: {
icon: ,
'aria-label': 'Open File Manager in new tab',
href: '/devices/file-manager',
openInNewTab: true,
},
},
{
id: 'archive-device',
label: 'Archive Device',
onClick: fn(),
},
{
id: 'delete-device',
label: 'Delete Device',
onClick: fn(),
icon: ,
},
],
},
],
},
parameters: {
docs: {
description: {
story:
'Add `iconAction` to any `ActionsMenuItem` to render an independent secondary button on the right of the row, separated by a vertical divider. Clicking the secondary action does not trigger the main row.',
},
},
},
}
/**
* Two SplitButton actions + a menu where some items have a secondary action.
* Shows the full Figma device-page pattern in a single layout.
*/
export const DoubleActionsAndMixedMenu: Story = {
args: {
variant: 'menu-primary',
actions: [
{
label: 'Open Docs',
onClick: fn(),
variant: 'accent',
icon: ,
iconAction: {
icon: ,
'aria-label': 'Open docs in new tab',
href: 'https://example.com/docs',
openInNewTab: true,
},
},
{
label: 'Run Script',
onClick: fn(),
variant: 'outline',
icon: ,
// Whole button disabled — both halves locked.
disabled: true,
iconAction: {
icon: ,
'aria-label': 'Run script in new window',
onClick: fn(),
},
},
{
label: 'Test Script',
onClick: fn(),
variant: 'outline',
icon: ,
// Only the main half disabled — icon-half stays clickable.
mainDisabled: true,
iconAction: {
icon: ,
'aria-label': 'Test script in new window',
onClick: fn(),
},
},
{
label: 'Schedule',
onClick: fn(),
variant: 'outline',
icon: ,
// Only the icon half disabled — main stays clickable.
iconAction: {
icon: ,
'aria-label': 'Open scheduler in new window',
onClick: fn(),
disabled: true,
},
},
],
menuActions: [
{
items: [
{
id: 'remote-control',
label: 'Remote Control',
href: '/devices/remote-control',
icon: ,
iconAction: {
icon: ,
'aria-label': 'Open Remote Control in new tab',
href: '/devices/remote-control',
openInNewTab: true,
},
},
{
id: 'delete-device',
label: 'Delete Device',
onClick: fn(),
icon: ,
},
],
},
],
},
parameters: {
docs: {
description: {
story:
'Combined story: two SplitButton page-actions on desktop (each with its own secondary action), plus a "..." menu where "Remote Control" has a secondary "open in new tab" button on the right and "Delete Device" is a single-target row.',
},
},
},
}
/**
* Primary buttons with a mobile-only action.
*/
export const PrimaryButtonsMobileOnly: Story = {
args: {
variant: 'primary-buttons',
actions: [
{
label: 'Save Changes',
onClick: fn(),
variant: 'accent',
icon: ,
},
{
label: 'Delete',
onClick: fn(),
variant: 'outline',
showOnlyMobile: true,
},
],
},
parameters: {
docs: {
description: {
story:
'The "Delete" button only appears in the mobile bottom bar. On desktop only "Save Changes" is shown.',
},
},
},
}