import React, { useState } from 'react';
import { Meta, StoryObj } from '@storybook/react-vite';
import { FlexContainer, iconList } from '@pantheon-systems/pds-toolkit-react';
import { IconButton } from './IconButton';
const SIZES = ['xs', 's', 'm'] as const;
const meta: Meta<typeof IconButton> = {
tags: ['ready-for-dev'],
title: 'Component Proposals/Buttons/Icon Button',
component: IconButton,
parameters: {
docs: {
description: {
component:
'Icon-only button implementation, with two extra variants: `agentic` (the AI gradient border, for an action the agent does) and `primary` (filled, for Send).',
},
},
},
argTypes: {
ariaLabel: {
control: 'text',
table: { category: 'Content' },
},
iconName: {
control: 'select',
options: iconList,
table: { category: 'Component configuration' },
},
buttonType: {
control: 'select',
table: { category: 'Base attributes' },
},
disabled: {
control: 'boolean',
table: { category: 'Base attributes' },
},
onClick: {
table: { category: 'Functions' },
},
className: {
table: {
category: 'Base attributes',
},
},
hasBorder: {
control: 'boolean',
table: { category: 'Component configuration' },
},
hasTooltip: {
control: 'boolean',
table: { category: 'Component configuration' },
},
size: {
control: 'select',
options: SIZES,
table: { category: 'Component configuration' },
},
variant: {
control: 'select',
table: { category: 'Component configuration' },
},
icon2Name: {
control: 'select',
options: iconList,
table: { category: 'Component configuration' },
},
isPressed: {
control: 'boolean',
table: { category: 'State' },
},
isWorking: {
control: 'boolean',
table: { category: 'State' },
},
pressedIconName: {
control: 'select',
options: iconList,
table: { category: 'Component configuration' },
},
},
args: {
ariaLabel: 'Ask Zappy',
buttonType: 'button',
disabled: false,
iconName: 'zappy',
hasBorder: false,
onClick: () => {
console.log('Icon button clicked');
},
size: 'm',
hasTooltip: true,
variant: 'agentic',
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Sizes: Story = {
parameters: {
docs: {
description: {
story:
'All available `size` values, smallest to largest: `xs`, `s`, `m`. The rows are the standard variant borderless (the default), the standard variant with a border, and the `agentic` variant (the AI gradient border).',
},
},
},
render: (args) => {
const looks = [
{ hasBorder: false, name: 'Borderless', variant: 'standard' },
{ hasBorder: true, name: 'With border', variant: 'standard' },
{ hasBorder: false, name: 'Agentic', variant: 'agentic' },
] as const;
return (
<FlexContainer flexDirection='column' gap='m'>
{looks.map(({ hasBorder, name, variant }) => (
<FlexContainer alignItems='center' gap='m' key={name}>
<span style={{ width: '8rem' }}>{name}</span>
{SIZES.map((size) => (
<IconButton
key={size}
{...args}
hasBorder={hasBorder}
size={size}
variant={variant}
/>
))}
</FlexContainer>
))}
</FlexContainer>
);
},
};
export const WithBorder: Story = {
args: {
hasBorder: true,
variant: 'standard',
},
};
export const Pressed: Story = {
parameters: {
docs: {
description: {
story:
'Demonstrates the `isPressed` toggle pattern. Pressed fills the button, and is announced to screen readers through `aria-pressed`. Pass `pressedIconName` to swap the icon as well.',
},
},
},
render: ({ variant }) => {
const [isPressed, setIsPressed] = useState(false);
return (
<IconButton
ariaLabel='Ask Zappy'
hasBorder={variant === 'standard'}
iconName='zappy'
isPressed={isPressed}
onClick={() => setIsPressed((v) => !v)}
variant={variant}
/>
);
},
};
const SEND_STATES = [
{
ariaLabel: 'Send message',
iconName: 'paperPlane',
isWorking: false,
name: 'Send',
},
{
ariaLabel: 'Sending message',
iconName: 'paperPlane',
isWorking: true,
name: 'Working',
},
{
ariaLabel: 'Stop generating',
iconName: 'stop',
isWorking: false,
name: 'Stop',
},
] as const;
export const PrimaryAllStates: Story = {
parameters: {
docs: {
description: {
story:
'`variant="primary"` is filled like a primary Button, for the main action of a toolbar. As a Send button it has three states, made from the props the button already has: `iconName="paperPlane"` to send, `isWorking` while the request is in flight (a spinner, disabled, `aria-busy`), and `iconName="stop"` to stop a running response. Update `ariaLabel` with the state. The border option does not apply to `primary`.',
},
},
},
render: (args) => (
<FlexContainer flexDirection='column' gap='m'>
{SEND_STATES.map(({ ariaLabel, iconName, isWorking, name }) => (
<FlexContainer alignItems='center' gap='m' key={name}>
<span style={{ width: '8rem' }}>{name}</span>
{SIZES.map((size) => (
<IconButton
{...args}
ariaLabel={ariaLabel}
hasBorder={false}
iconName={iconName}
isWorking={isWorking}
key={size}
size={size}
variant='primary'
/>
))}
</FlexContainer>
))}
</FlexContainer>
),
};
export const PrimarySend: Story = {
parameters: {
docs: {
description: {
story:
'How the three states chain in a chat input: click to send, it shows the working state until the request starts, then the stop icon while the response streams. Clicking stop returns to send. The screen owns the state and updates `ariaLabel` with it.',
},
},
},
render: (args) => {
const [state, setState] = useState<'send' | 'stop' | 'working'>('send');
const current = SEND_STATES.find((s) => s.name.toLowerCase() === state);
const handleClick = () => {
if (state === 'send') {
setState('working');
setTimeout(() => setState('stop'), 400);
} else if (state === 'stop') {
setState('send');
}
};
return (
<IconButton
{...args}
ariaLabel={current?.ariaLabel ?? ''}
hasBorder={false}
iconName={current?.iconName ?? 'paperPlane'}
isWorking={state === 'working'}
onClick={handleClick}
variant='primary'
/>
);
},
};
Icon Button
Starting point for an IconButton proposal.
Extends a PDS componentOpen in Storybook