import { ReactElement, useState } from 'react';
import { Meta, StoryObj } from '@storybook/react-webpack5';
import { fn } from 'storybook/test';
import { Bank, Star, Travel, Briefcase } from '@transferwise/icons';
import { lorem10, lorem20 } from '../../test-utils';
import Button from '../../button';
import Title from '../../title';
import { ActionPrompt, type ActionPromptProps } from './ActionPrompt';
const withComponentGrid =
({ maxWidth = 'auto', gap = '1rem' } = {}) =>
(Story: () => ReactElement) => (
);
const meta: Meta = {
title: 'Prompts/ActionPrompt',
component: ActionPrompt,
tags: ['new'],
decorators: [withComponentGrid()],
args: {
title: 'Action Required',
description: 'Please complete the following action to continue.',
action: { label: 'Continue', onClick: fn() },
},
argTypes: {
sentiment: {
control: 'select',
options: ['success', 'negative', 'neutral', 'warning', 'proposition'],
},
title: {
control: 'text',
table: {
type: { summary: 'ReactNode' },
},
},
description: {
control: 'text',
table: {
type: { summary: 'ReactNode' },
},
},
media: {
control: false,
table: {
type: {
summary: 'ActionPromptMedia',
detail:
'{ imgSrc?: string; avatar?: { imgSrc?: string; profileName?: string; profileType?: string; asset?: ReactNode; badge?: { flagCode: string } }; aria-label?: string }',
},
},
},
action: {
control: false,
table: {
type: {
summary: 'ActionPromptAction',
detail: '{ label: ReactNode; onClick?: () => void; href?: string; target?: string }',
},
},
},
actionSecondary: {
control: false,
table: {
type: {
summary: 'ActionPromptActionSecondary',
detail: '{ label: ReactNode; onClick?: () => void; href?: string; target?: string }',
},
},
},
},
};
export default meta;
type Story = StoryObj;
/**
* Convenience controls for previewing rich markup,
* not otherwise possible via Storybook
*/
type PreviewStoryArgs = ActionPromptProps & {
previewMedia: ActionPromptProps['media'];
previewOnDismiss: boolean;
previewSecondaryAction: boolean;
};
const previewArgGroup = {
category: 'Storybook Preview options',
type: {
summary: undefined,
},
};
const MEDIA_OPTIONS: Record = {
undefined,
'Custom image (no badge)': { imgSrc: '../../wise-card.svg' },
'Avatar: Profile image': { avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } },
'Avatar: Business (GB badge)': {
avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } },
},
'Avatar: Personal (EU badge)': {
avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } },
},
'Avatar: Icon (Bank)': { avatar: { asset: } },
'Avatar: Icon (Star)': { avatar: { asset: } },
'Avatar: Icon (Travel)': { avatar: { asset: } },
'Avatar: Icon (Briefcase)': { avatar: { asset: } },
'Avatar: Initials': { avatar: { profileName: 'John Doe' } },
};
const previewArgTypes = {
previewMedia: {
name: 'Preview with `media`',
control: 'select',
options: [
'undefined',
'Custom image (no badge)',
'Avatar: Profile image',
'Avatar: Business (GB badge)',
'Avatar: Personal (EU badge)',
'Avatar: Icon (Bank)',
'Avatar: Icon (Star)',
'Avatar: Icon (Travel)',
'Avatar: Icon (Briefcase)',
'Avatar: Initials',
],
mapping: MEDIA_OPTIONS,
table: previewArgGroup,
},
previewOnDismiss: {
name: 'Preview with `onDismiss`',
control: 'boolean',
table: previewArgGroup,
},
previewSecondaryAction: {
name: 'Preview with `actionSecondary`',
control: 'boolean',
table: previewArgGroup,
},
} as const;
const getPropsForPreview = (
args: PreviewStoryArgs,
): [ActionPromptProps, Partial] => {
const { previewMedia, previewOnDismiss, previewSecondaryAction, ...props } = args;
return [
props,
{
media: previewMedia,
onDismiss: previewOnDismiss ? fn() : undefined,
actionSecondary: previewSecondaryAction ? { label: 'Cancel', onClick: fn() } : undefined,
},
];
};
export const Playground: StoryObj = {
argTypes: {
sentiment: {
control: 'radio',
options: ['success', 'negative', 'neutral', 'warning', 'proposition'],
},
...previewArgTypes,
},
args: {
previewMedia: { avatar: undefined },
previewOnDismiss: true,
previewSecondaryAction: true,
},
render: (args: PreviewStoryArgs) => {
const [props, previewProps] = getPropsForPreview(args);
return ;
},
};
/**
* ActionPrompt supports multiple sentiments to communicate different types of messages:
* - `neutral` (default): General prompts
* - `success`: Success confirmations
* - `negative`: Error or critical actions
* - `warning`: Warning messages
* - `proposition`: Promotional or feature prompts
*/
export const Sentiments: Story = {
render: (args) => (
<>
{(['success', 'warning', 'negative', 'neutral', 'proposition'] as const).map((sentiment) => (
))}
>
),
};
/**
* ActionPrompt can have just a primary action, or both primary and secondary actions.
*/
export const Actions: Story = {
render: () => (
<>
>
),
};
/**
* ActionPrompt supports various media types to enhance visual communication.
*
* **Default**: Each sentiment has a default status icon that appears when no media is provided.
*
* **Icon Overrides**: Replace default icons with custom images, avatars with images, avatars with initials,
* avatars with custom icons, or avatars with profile types and badges.
*/
export const MediaTypes: Story = {
render: () => {
return (
<>
Default
Icon Overrides
} }}
action={{ label: 'Connect bank', onClick: fn() }}
/>
>
);
},
};
/**
* When `onDismiss` is provided, a close button appears allowing users to dismiss the prompt.
* Note: The component itself is not automatically removed - you must handle state management.
*/
export const Dismissible: Story = {
render: (args) => {
const [showPrompt, setShowPrompt] = useState(true);
return (
<>
{showPrompt ? (
setShowPrompt(false)}
/>
) : (
)}
>
);
},
};
/**
* While the component itself will stretch to the full available width, the text container will be
* capped at `480px` to ensure optimal readability.
*
* [Visit wise.design](https://wise.design/components/info-prompt#writing-an-info-prompt) for guidance on writing effective prompt messages that are concise and easy to understand.
*/
export const ParagraphWidth: Story = {
parameters: {
docs: {
canvas: {
sourceState: 'hidden',
},
},
},
args: {
title: lorem10,
description: lorem20,
sentiment: 'success',
action: { label: 'View details', onClick: fn() },
onDismiss: () => {},
},
};