@beydesign/storybook 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,7 +51,9 @@ const StyledButton = styled.button `
51
51
  case ButtonHierarchy.WhiteText:
52
52
  if (props.$state === ButtonState.Disabled || props.$disabled)
53
53
  return 'var(--color-background-bg-button-disabled)';
54
- return 'var(--color-background-bg-overlay-1)';
54
+ // Figma "White-text" fill is the Primary/Black/40 primitive
55
+ // (rgba(0,0,0,0.4)) — a dark scrim over media, not an overlay token.
56
+ return 'var(--primitives-white-primary-black-40)';
55
57
  default:
56
58
  return 'var(--color-background-bg-brand-primary)';
57
59
  }
@@ -183,7 +185,8 @@ const StyledButton = styled.button `
183
185
  case ButtonHierarchy.TertiaryColor:
184
186
  return 'var(--color-background-bg-card-highlight-hover)';
185
187
  case ButtonHierarchy.WhiteText:
186
- return 'var(--color-background-bg-overlay-2)';
188
+ // Darken the black-40 scrim one step on hover (no black-50 exists).
189
+ return 'var(--primitives-white-primary-black-60)';
187
190
  default:
188
191
  return 'var(--color-background-bg-brand-primary-hover)';
189
192
  }
@@ -23,7 +23,12 @@ export const ButtonCard = ({ title, description, icon = 'UserFocus', iconColor =
23
23
  boxShadow: `var(--global-shadows-shadow-xs-offset-x) var(--global-shadows-shadow-xs-offset-y) var(--global-shadows-shadow-xs-blur) var(--global-shadows-shadow-xs-spread) var(--global-shadows-shadow-xs-color)`,
24
24
  },
25
25
  }),
26
- }, children: [_jsxs(Box, { display: 'flex', flex: '1 0 0', minWidth: 0, alignItems: 'flex-start', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)', children: [_jsx(Box, { display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, width: '34px', height: '34px', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', bgcolor: iconBackgroundColor, children: getIconComponent(icon, { size: 24, color: iconColor }) }), _jsxs(Box, { display: 'flex', flex: '1 0 0', minWidth: 0, flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)', alignItems: 'flex-start', sx: { wordBreak: 'break-word' }, children: [_jsx(Typography, { size: TypographySize.HeadingS, weight: TypographyWeight.Bold, color: 'var(--color-text-text-title)', textStyle: { textAlign: 'left' }, children: title }), description && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-paragraph)', textStyle: { textAlign: 'left' }, children: description }))] })] }), showActionButton && (_jsx(Box, { position: 'absolute', top: '6px', right: '8px', children: _jsx(MainButton, { text: actionButtonText, hierarchy: ButtonHierarchy.TertiaryColor, size: ButtonSize.xxs, onClick: (e) => {
26
+ }, children: [_jsxs(Box, { display: 'flex', flex: '1 0 0', minWidth: 0, alignItems: 'flex-start', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)', children: [_jsx(Box, { display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, width: '34px', height: '34px', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', bgcolor: iconBackgroundColor,
27
+ // Without border-box the 34px tile grows to 50px (34 + 8px padding
28
+ // each side); Figma sizes the tile at a fixed 34×34. The 24px icon
29
+ // must not shrink to fit the 18px content box — like Figma it stays
30
+ // 24px and overflows the nominal padding, centered in the tile.
31
+ boxSizing: 'border-box', sx: { '& svg': { flexShrink: 0 } }, children: getIconComponent(icon, { size: 24, color: iconColor }) }), _jsxs(Box, { display: 'flex', flex: '1 0 0', minWidth: 0, flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)', alignItems: 'flex-start', sx: { wordBreak: 'break-word' }, children: [_jsx(Typography, { size: TypographySize.HeadingS, weight: TypographyWeight.Bold, color: 'var(--color-text-text-title)', textStyle: { textAlign: 'left' }, children: title }), description && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-paragraph)', textStyle: { textAlign: 'left' }, children: description }))] })] }), showActionButton && (_jsx(Box, { position: 'absolute', top: '6px', right: '8px', children: _jsx(MainButton, { text: actionButtonText, hierarchy: ButtonHierarchy.TertiaryColor, size: ButtonSize.xxs, onClick: (e) => {
27
32
  e.stopPropagation();
28
33
  onActionClick?.();
29
34
  }, testId: `${cardKey}-action-button` }) }))] }));
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import { ConversationalAvatarsBannerProps } from './types';
3
+ export declare const ConversationalAvatarsBanner: React.FC<ConversationalAvatarsBannerProps>;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from '@mui/material';
3
+ import { Typography, TypographySize, TypographyWeight } from '../Typography';
4
+ import { MainButton, ButtonHierarchy, ButtonSize } from '../Buttons';
5
+ import { BANNER_AVATARS, AVATAR_TILE_BACKGROUND, AVATAR_TILE_WIDTH, } from './bannerAvatars';
6
+ const DEFAULT_BACKGROUND = 'linear-gradient(180deg, rgba(194, 171, 252, 0.08) 0%, rgba(128, 96, 198, 0.2) 100%)';
7
+ // The avatar column stays on the right at every breakpoint (unlike FeatureBanner
8
+ // which stacks its media), so the banner only reduces its padding/gap on mobile.
9
+ const MOBILE = '@media (max-width: 640px)';
10
+ // Height the card holds so the avatar collage shows the hero tile plus a sliver
11
+ // of the tiles above/below.
12
+ const BANNER_MIN_HEIGHT = 198;
13
+ // Figma tile size — 130×150; the 198px card centers the middle (hero) tile and
14
+ // clips the tiles above/below to slivers.
15
+ const TILE_HEIGHT = 150;
16
+ // On mobile the tighter card can't fit a full tile, so the collage bleeds this
17
+ // many extra pixels past the right edge — the tile's right side (and rounded
18
+ // corner) is clipped, matching the Figma mobile layout.
19
+ const MOBILE_TILE_CUTOFF = 15;
20
+ // Avatars shown in the collage column (reused from the PromoBanner set). The
21
+ // middle tile is the fully-visible hero; the rest are clipped by the height.
22
+ const IMAGE_COLUMN = [
23
+ BANNER_AVATARS.fjolla,
24
+ BANNER_AVATARS.micheal,
25
+ BANNER_AVATARS.yuruo,
26
+ ];
27
+ export const ConversationalAvatarsBanner = ({ title, description, actions = [], background = DEFAULT_BACKGROUND, style: customStyle, testId, }) => {
28
+ const renderAction = (action, index) => (_jsx(MainButton, { text: action.text, size: ButtonSize.md, hierarchy: action.primary
29
+ ? ButtonHierarchy.Primary
30
+ : ButtonHierarchy.SecondaryColor, onClick: action.onClick, testId: testId ? `${testId}-action-${index}` : undefined }, `${action.text}-${index}`));
31
+ return (_jsxs(Box, { display: "flex", alignItems: "center", gap: "40px", width: "100%", minHeight: `${BANNER_MIN_HEIGHT}px`, overflow: "hidden", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-lg)", padding: "var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-2xl)", sx: {
32
+ background,
33
+ boxSizing: 'border-box',
34
+ [MOBILE]: {
35
+ gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
36
+ padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
37
+ },
38
+ ...customStyle,
39
+ }, "data-testid": testId, children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-xl)", flex: 1, minWidth: 0, children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: [title && (_jsx(Typography, { size: TypographySize.HeadingM, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", children: title })), description && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-label)", children: description }))] }), actions.length > 0 && (_jsx(Box, { display: "flex", alignItems: "center", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: actions.map(renderAction) }))] }), _jsx(Box, { flexShrink: 0, alignSelf: "stretch", position: "relative", overflow: "hidden", width: `${AVATAR_TILE_WIDTH}px`, sx: {
40
+ marginY: 'calc(-1 * var(--spacing-tokens-size-in-px-spacing-spacing-3xl))',
41
+ [MOBILE]: {
42
+ marginY: 'calc(-1 * var(--spacing-tokens-size-in-px-spacing-spacing-xl))',
43
+ // Push the strip past the right edge so the tile is clipped there.
44
+ marginRight: `calc(-1 * var(--spacing-tokens-size-in-px-spacing-spacing-lg) - ${MOBILE_TILE_CUTOFF}px)`,
45
+ },
46
+ }, children: _jsx(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", position: "absolute", left: 0, right: 0, top: "50%", sx: { transform: 'translateY(-50%)' }, children: IMAGE_COLUMN.map((image, index) => (_jsx(Box, { component: "img", src: image, alt: "", width: "100%", height: `${TILE_HEIGHT}px`, flexShrink: 0, sx: {
47
+ objectFit: 'cover',
48
+ borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
49
+ background: AVATAR_TILE_BACKGROUND,
50
+ display: 'block',
51
+ } }, index))) }) })] }));
52
+ };
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ConversationalAvatarsBanner } from './ConversationalAvatarsBanner';
3
+ declare const meta: Meta<typeof ConversationalAvatarsBanner>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof ConversationalAvatarsBanner>;
6
+ export declare const Default: Story;
7
+ export declare const Mobile: Story;
@@ -0,0 +1,76 @@
1
+ import { ConversationalAvatarsBanner } from './ConversationalAvatarsBanner';
2
+ const meta = {
3
+ title: 'Design System/Components/UI/ConversationalAvatarsBanner',
4
+ component: ConversationalAvatarsBanner,
5
+ parameters: {
6
+ layout: 'padded',
7
+ design: {
8
+ type: 'figspec',
9
+ url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=7081-24780',
10
+ },
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ title: {
15
+ control: 'text',
16
+ description: 'Title of the banner',
17
+ table: { type: { summary: 'string' } },
18
+ },
19
+ description: {
20
+ control: 'text',
21
+ description: 'Description shown below the title',
22
+ table: { type: { summary: 'string' } },
23
+ },
24
+ actions: {
25
+ control: 'object',
26
+ description: 'Action buttons — set primary: true for the brand (violet) button',
27
+ table: { type: { summary: 'BannerAction[]' } },
28
+ },
29
+ background: {
30
+ control: 'text',
31
+ description: 'Background of the banner',
32
+ table: { type: { summary: 'string' } },
33
+ },
34
+ style: {
35
+ control: 'object',
36
+ description: 'Custom style for the banner',
37
+ table: { type: { summary: 'object' } },
38
+ },
39
+ },
40
+ };
41
+ export default meta;
42
+ export const Default = {
43
+ args: {
44
+ title: 'Introducing Conversational Avatars',
45
+ description: 'Bring your ideas to life with intelligent conversational avatars.',
46
+ actions: [{ text: 'Create New Agent' }],
47
+ },
48
+ };
49
+ const FIGMA_MOBILE_VIEWPORT = {
50
+ viewport: {
51
+ viewports: {
52
+ figmaMobile: {
53
+ name: 'Figma Mobile (375px)',
54
+ styles: { width: '375px', height: '812px' },
55
+ },
56
+ },
57
+ defaultViewport: 'figmaMobile',
58
+ },
59
+ };
60
+ export const Mobile = {
61
+ args: {
62
+ ...Default.args,
63
+ },
64
+ parameters: {
65
+ layout: 'padded',
66
+ ...FIGMA_MOBILE_VIEWPORT,
67
+ docs: {
68
+ description: {
69
+ story: 'The avatar banner on the mobile Home screen — unlike the feature ' +
70
+ 'banner it keeps the avatar collage as a narrow strip on the right ' +
71
+ 'instead of stacking. Best viewed in the Canvas with the preset ' +
72
+ 'mobile viewport.',
73
+ },
74
+ },
75
+ },
76
+ };
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import { ConversationalVoiceAgentsBannerProps } from './types';
3
+ export declare const ConversationalVoiceAgentsBanner: React.FC<ConversationalVoiceAgentsBannerProps>;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from '@mui/material';
3
+ import { Typography, TypographySize, TypographyWeight } from '../Typography';
4
+ import { MainButton, ButtonHierarchy, ButtonSize, ButtonDisplayMode, } from '../Buttons';
5
+ import { BANNER_VIDEO_PREVIEW } from './bannerAvatars';
6
+ const PINK_GRADIENT = 'linear-gradient(90deg, rgba(235, 205, 240, 0.05) 0%, rgba(202, 59, 224, 0.15) 100%)';
7
+ // Below this width the banner stacks vertically to match the Figma mobile
8
+ // layout (text + actions on top, video preview full-width below).
9
+ const MOBILE = '@media (max-width: 640px)';
10
+ // The video preview keeps the Figma 296×150 tile on desktop.
11
+ const PREVIEW_WIDTH = 296;
12
+ const PREVIEW_HEIGHT = 150;
13
+ export const ConversationalVoiceAgentsBanner = ({ title, description, actions = [], previewImage = BANNER_VIDEO_PREVIEW, onPlayClick, previewCtaText, onPreviewCtaClick, background = PINK_GRADIENT, style: customStyle, testId, }) => {
14
+ const renderAction = (action, index) => (_jsx(MainButton, { text: action.text, size: ButtonSize.md, hierarchy: action.primary
15
+ ? ButtonHierarchy.Primary
16
+ : ButtonHierarchy.SecondaryColor, onClick: action.onClick, testId: testId ? `${testId}-action-${index}` : undefined }, `${action.text}-${index}`));
17
+ return (_jsxs(Box, { display: "flex", alignItems: "center", gap: "40px", width: "100%", overflow: "hidden", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-lg)", padding: "var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-2xl)", sx: {
18
+ background,
19
+ boxSizing: 'border-box',
20
+ [MOBILE]: {
21
+ flexDirection: 'column',
22
+ alignItems: 'stretch',
23
+ gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-3xl)',
24
+ padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
25
+ },
26
+ ...customStyle,
27
+ }, "data-testid": testId, children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-xl)", flex: 1, minWidth: 0, children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: [title && (_jsx(Typography, { size: TypographySize.HeadingM, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", children: title })), description && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-label)", children: description }))] }), actions.length > 0 && (_jsx(Box, { display: "flex", alignItems: "center", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", sx: {
28
+ // On mobile the two actions split the row into equal-width
29
+ // buttons, matching the Figma mobile layout.
30
+ [MOBILE]: {
31
+ width: '100%',
32
+ '& > *': { flex: 1, minWidth: 0 },
33
+ '& button': { width: '100%' },
34
+ },
35
+ }, children: actions.map(renderAction) }))] }), _jsxs(Box, { position: "relative", flexShrink: 0, overflow: "hidden", width: `${PREVIEW_WIDTH}px`, height: `${PREVIEW_HEIGHT}px`, borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-sm)", sx: {
36
+ [MOBILE]: {
37
+ width: '100%',
38
+ height: 'auto',
39
+ aspectRatio: `${PREVIEW_WIDTH} / ${PREVIEW_HEIGHT}`,
40
+ },
41
+ }, children: [_jsx(Box, { component: "img", src: previewImage, alt: "", width: "100%", height: "100%", sx: { objectFit: 'cover', display: 'block' } }), _jsx(Box, { position: "absolute", left: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", bottom: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: _jsx(MainButton, { size: ButtonSize.md, hierarchy: ButtonHierarchy.SecondaryTransparent, displayIconMode: ButtonDisplayMode.Only, displayIcon: "Play", onClick: onPlayClick, style: { border: 'none' }, testId: testId ? `${testId}-play` : undefined }) }), previewCtaText && (_jsx(Box, { position: "absolute", right: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", bottom: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: _jsx(MainButton, { text: previewCtaText, size: ButtonSize.md, hierarchy: ButtonHierarchy.SecondaryTransparent, onClick: onPreviewCtaClick, style: { border: 'none' }, testId: testId ? `${testId}-preview-cta` : undefined }) }))] })] }));
42
+ };
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ConversationalVoiceAgentsBanner } from './ConversationalVoiceAgentsBanner';
3
+ declare const meta: Meta<typeof ConversationalVoiceAgentsBanner>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof ConversationalVoiceAgentsBanner>;
6
+ export declare const Default: Story;
7
+ export declare const Mobile: Story;
@@ -0,0 +1,104 @@
1
+ import { ConversationalVoiceAgentsBanner } from './ConversationalVoiceAgentsBanner';
2
+ const meta = {
3
+ title: 'Design System/Components/UI/ConversationalVoiceAgentsBanner',
4
+ component: ConversationalVoiceAgentsBanner,
5
+ parameters: {
6
+ layout: 'padded',
7
+ design: {
8
+ type: 'figspec',
9
+ url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=6938-20061',
10
+ },
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ title: {
15
+ control: 'text',
16
+ description: 'Title of the banner',
17
+ table: { type: { summary: 'string' } },
18
+ },
19
+ description: {
20
+ control: 'text',
21
+ description: 'Description shown below the title',
22
+ table: { type: { summary: 'string' } },
23
+ },
24
+ actions: {
25
+ control: 'object',
26
+ description: 'Action buttons — set primary: true for the brand (violet) button',
27
+ table: { type: { summary: 'BannerAction[]' } },
28
+ },
29
+ previewImage: {
30
+ control: 'text',
31
+ description: 'Thumbnail/poster image URL for the video preview',
32
+ table: { type: { summary: 'string' } },
33
+ },
34
+ onPlayClick: {
35
+ action: 'play clicked',
36
+ description: 'Called when the play button on the video preview is clicked',
37
+ table: { type: { summary: 'function' } },
38
+ },
39
+ previewCtaText: {
40
+ control: 'text',
41
+ description: 'CTA text overlaid on the video preview',
42
+ table: { type: { summary: 'string' } },
43
+ },
44
+ onPreviewCtaClick: {
45
+ action: 'preview cta clicked',
46
+ description: 'Called when the preview CTA is clicked',
47
+ table: { type: { summary: 'function' } },
48
+ },
49
+ background: {
50
+ control: 'text',
51
+ description: 'Background of the banner',
52
+ table: { type: { summary: 'string' } },
53
+ },
54
+ style: {
55
+ control: 'object',
56
+ description: 'Custom style for the banner',
57
+ table: { type: { summary: 'object' } },
58
+ },
59
+ },
60
+ };
61
+ export default meta;
62
+ export const Default = {
63
+ args: {
64
+ title: 'Add a Human Face to your Conversational Voice Agents',
65
+ description: 'Bring your ideas to life with intelligent conversational avatars.',
66
+ actions: [
67
+ { text: 'Create API Key' },
68
+ { text: 'Go to Docs', primary: true },
69
+ ],
70
+ previewCtaText: 'How to Get Started',
71
+ },
72
+ };
73
+ const FIGMA_MOBILE_VIEWPORT = {
74
+ viewport: {
75
+ viewports: {
76
+ figmaMobile: {
77
+ name: 'Figma Mobile (375px)',
78
+ styles: { width: '375px', height: '812px' },
79
+ },
80
+ },
81
+ defaultViewport: 'figmaMobile',
82
+ },
83
+ };
84
+ export const Mobile = {
85
+ args: {
86
+ ...Default.args,
87
+ },
88
+ parameters: {
89
+ layout: 'padded',
90
+ ...FIGMA_MOBILE_VIEWPORT,
91
+ design: {
92
+ type: 'figspec',
93
+ url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=7081-24780',
94
+ },
95
+ docs: {
96
+ description: {
97
+ story: 'The conversational agents banner on the mobile Home screen — text ' +
98
+ 'and actions stack on top, the video preview spans full-width below. ' +
99
+ 'Below 640px the component reflows automatically; best viewed in the ' +
100
+ 'Canvas with the preset mobile viewport.',
101
+ },
102
+ },
103
+ },
104
+ };
@@ -1,31 +1,65 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box } from '@mui/material';
2
+ import { Box, useMediaQuery } from '@mui/material';
3
3
  import { Typography, TypographySize, TypographyWeight } from '../Typography';
4
4
  import { MainButton, ButtonHierarchy, ButtonSize } from '../Buttons';
5
+ import { BANNER_AVATARS as AVATARS, AVATAR_TILE_BACKGROUND, } from './bannerAvatars';
5
6
  const DEFAULT_BACKGROUND = 'linear-gradient(180deg, rgba(194, 171, 252, 0.08) 0%, rgba(128, 96, 198, 0.2) 100%)';
6
- // Below this width the banner stacks vertically to match the Figma mobile
7
- // layout (side images hidden, promo code + CTA stacked full-width).
8
- const MOBILE = '@media (max-width: 640px)';
7
+ // Below this width the banner switches to the Figma mobile variant: text
8
+ // centered, features/footer hidden, promo code + CTA stacked, and the mobile*
9
+ // content overrides applied.
10
+ const MOBILE_QUERY = '(max-width: 640px)';
11
+ const MOBILE = `@media ${MOBILE_QUERY}`;
12
+ // The side collages only fit alongside the 458px content column at the Figma
13
+ // frame width (~1122px). Below that they would crush the content and overflow
14
+ // the buttons, so hide them and let the content center on its own.
15
+ const HIDE_COLLAGE = '@media (max-width: 1121px)';
16
+ // Width of the Figma text/content column. Capping the middle content keeps the
17
+ // side collages adjacent to the text on wide (e.g. Full HD) viewports instead
18
+ // of stretching apart; the row stays centered so extra space becomes gutters.
19
+ const CONTENT_MAX_WIDTH = '458px';
20
+ // The middle tile of each column is the fully-visible "hero"; the first and
21
+ // last tiles are clipped to slivers above/below by the banner height. Columns
22
+ // are centered in the banner, then `offset` shifts them vertically (negative =
23
+ // up) so the two columns of each side interlock, matching the Figma layout.
24
+ const LEFT_COLUMNS = [
25
+ { offset: 0, images: [AVATARS.jerome, AVATARS.micheal, AVATARS.jerome] },
26
+ { offset: -40, images: [AVATARS.laura, AVATARS.nelly, AVATARS.fjolla] },
27
+ ];
28
+ const RIGHT_COLUMNS = [
29
+ { offset: 0, images: [AVATARS.jerome, AVATARS.fjolla, AVATARS.yuruo] },
30
+ { offset: -20, images: [AVATARS.micheal, AVATARS.laura, AVATARS.nelly] },
31
+ ];
9
32
  export const PromoBanner = ({ title = 'Real-time AI avatars for every conversation.', description = 'Trusted by developers, startups, educators, and enterprises to build engaging AI experiences at scale:', features = [
10
33
  'Real-Time',
11
34
  'Multilingual',
12
35
  'API-first',
13
36
  'Enterprise-ready',
14
37
  'GDPR Compliant',
15
- ], promoCode, promoCodeLabel = 'YOUR PROMO CODE', onCopyPromoCode, ctaText, onCtaClick, footerText, leftImage, rightImage, showImages = true, background = DEFAULT_BACKGROUND, style: customStyle, testId, }) => {
38
+ ], promoCode, promoCodeLabel = 'YOUR PROMO CODE', onCopyPromoCode, ctaText, onCtaClick, footerText, mobileTitle, mobileDescription, mobileCtaText, showImages = true, background = DEFAULT_BACKGROUND, style: customStyle, testId, }) => {
39
+ // Below 640px the banner renders the Figma mobile variant, swapping to any
40
+ // provided mobile* copy and falling back to the desktop content otherwise.
41
+ const isMobile = useMediaQuery(MOBILE_QUERY);
42
+ const resolvedTitle = isMobile && mobileTitle ? mobileTitle : title;
43
+ const resolvedDescription = isMobile && mobileDescription ? mobileDescription : description;
44
+ const resolvedCtaText = isMobile && mobileCtaText ? mobileCtaText : ctaText;
16
45
  const handleCopyPromoCode = () => {
17
46
  if (!promoCode)
18
47
  return;
19
48
  navigator.clipboard?.writeText(promoCode);
20
49
  onCopyPromoCode?.(promoCode);
21
50
  };
22
- const featuresText = features?.length
51
+ // Features are part of the desktop-only layout (hidden on the mobile variant).
52
+ const featuresText = !isMobile && features?.length
23
53
  ? features.map((feature) => `✓ ${feature}`).join(', ') + '.'
24
54
  : '';
25
- const renderSideImage = (image) => {
26
- if (!showImages || !image)
55
+ const renderAvatarMatrix = (columns) => {
56
+ if (!showImages)
27
57
  return null;
28
- return (_jsx(Box, { component: "img", src: image, alt: "", height: "100%", flexShrink: 0, sx: { objectFit: 'contain', [MOBILE]: { display: 'none' } } }));
58
+ return (_jsx(Box, { display: "flex", alignItems: "center", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-lg)", height: "100%", flexShrink: 0, overflow: "hidden", sx: { [HIDE_COLLAGE]: { display: 'none' } }, children: columns.map((column, columnIndex) => (_jsx(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", flexShrink: 0, sx: { transform: `translateY(${column.offset}px)` }, children: column.images.map((image, imageIndex) => (_jsx(Box, { component: "img", src: image, alt: "", width: "130px", height: "150px", flexShrink: 0, sx: {
59
+ objectFit: 'cover',
60
+ borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
61
+ background: AVATAR_TILE_BACKGROUND,
62
+ } }, imageIndex))) }, columnIndex))) }));
29
63
  };
30
64
  return (_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "center", gap: "40px", height: "220px", width: "100%", overflow: "hidden", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-lg)", paddingX: "var(--spacing-tokens-size-in-px-spacing-spacing-2xl)", sx: {
31
65
  background,
@@ -36,17 +70,21 @@ export const PromoBanner = ({ title = 'Real-time AI avatars for every conversati
36
70
  paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
37
71
  },
38
72
  ...customStyle,
39
- }, "data-testid": testId, children: [renderSideImage(leftImage), _jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "center", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-2xl)", flex: 1, minWidth: 0, paddingY: "var(--spacing-tokens-size-in-px-spacing-spacing-3xl)", children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", width: "100%", children: [title && (_jsx(Typography, { size: TypographySize.HeadingM, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", textStyle: { textAlign: 'center' }, children: title })), (description || featuresText) && (_jsxs(Box, { display: "flex", flexDirection: "column", children: [description && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-title)", children: description })), featuresText && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", children: featuresText }))] }))] }), (promoCode || ctaText) && (_jsxs(Box, { display: "flex", justifyContent: "center", alignItems: "stretch", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", sx: {
73
+ }, "data-testid": testId, children: [renderAvatarMatrix(LEFT_COLUMNS), _jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "center", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-2xl)", flex: 1, minWidth: 0, maxWidth: CONTENT_MAX_WIDTH, paddingY: "var(--spacing-tokens-size-in-px-spacing-spacing-3xl)", children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", width: "100%",
74
+ // Text is centered on the mobile variant (inherited by the children).
75
+ sx: { [MOBILE]: { textAlign: 'center' } }, children: [resolvedTitle && (_jsx(Typography, { size: TypographySize.HeadingM, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", textStyle: { textAlign: 'center' }, children: resolvedTitle })), (resolvedDescription || featuresText) && (_jsxs(Box, { display: "flex", flexDirection: "column", children: [resolvedDescription && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-title)", children: resolvedDescription })), featuresText && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", children: featuresText }))] }))] }), (promoCode || resolvedCtaText) && (_jsxs(Box, { display: "flex", justifyContent: "center", alignItems: "stretch", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", sx: {
76
+ // On mobile the promo code + CTA stack and share the wider CTA's
77
+ // width (centered), matching Figma — not stretched full-width.
40
78
  [MOBILE]: {
41
79
  flexDirection: 'column',
42
- width: '100%',
80
+ width: 'fit-content',
43
81
  '& button': { width: '100%' },
44
82
  },
45
83
  }, children: [promoCode && (_jsxs(Box, { position: "relative", display: "flex", children: [_jsx(MainButton, { text: promoCode, size: ButtonSize.md, hierarchy: ButtonHierarchy.SecondaryColor, showTrailingIcon: true, trailingIcon: "Copy", onClick: handleCopyPromoCode, style: {
46
- border: '1px dashed var(--color-border-border-component)',
84
+ border: '1px dashed var(--color-border-border-secondary-action)',
47
85
  height: '100%',
48
86
  }, testId: testId ? `${testId}-promo-code` : undefined }), promoCodeLabel && (_jsx(Box, { position: "absolute", top: "-13px", left: "-8px", bgcolor: "var(--color-background-bg-icon-badge)", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-xs)", padding: "var(--spacing-tokens-size-in-px-spacing-spacing-xxs) var(--spacing-tokens-size-in-px-spacing-spacing-sm)", whiteSpace: "nowrap", zIndex: 1, children: _jsx(Typography, { size: TypographySize.Text2XS, weight: TypographyWeight.SemiBold,
49
87
  // Invariant violet label (bg-icon-badge) -> pin to the
50
88
  // invariant dark text so it stays legible in both themes.
51
- color: "var(--color-text-text-dark)", children: promoCodeLabel }) }))] })), ctaText && (_jsx(MainButton, { text: ctaText, size: ButtonSize.md, hierarchy: ButtonHierarchy.Primary, onClick: onCtaClick, testId: testId ? `${testId}-cta` : undefined }))] })), footerText && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-title)", textStyle: { width: '100%' }, children: footerText }))] }), renderSideImage(rightImage)] }));
89
+ color: "var(--color-text-text-dark)", children: promoCodeLabel }) }))] })), resolvedCtaText && (_jsx(MainButton, { text: resolvedCtaText, size: ButtonSize.md, hierarchy: ButtonHierarchy.Primary, onClick: onCtaClick, testId: testId ? `${testId}-cta` : undefined }))] })), !isMobile && footerText && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-title)", textStyle: { width: '100%' }, children: footerText }))] }), renderAvatarMatrix(RIGHT_COLUMNS)] }));
52
90
  };
@@ -1,6 +1,4 @@
1
1
  import { PromoBanner } from './PromoBanner';
2
- import bannerAvatarsLeft from '../assets/banner/banner-avatars-left.png';
3
- import bannerAvatarsRight from '../assets/banner/banner-avatars-right.png';
4
2
  const meta = {
5
3
  title: 'Design System/Components/UI/PromoBanner',
6
4
  component: PromoBanner,
@@ -76,23 +74,30 @@ const meta = {
76
74
  type: { summary: 'string' },
77
75
  },
78
76
  },
79
- leftImage: {
77
+ mobileTitle: {
80
78
  control: 'text',
81
- description: 'Image URL of the avatar collage shown on the left side',
79
+ description: 'Title override used on the mobile variant (< 640px)',
82
80
  table: {
83
81
  type: { summary: 'string' },
84
82
  },
85
83
  },
86
- rightImage: {
84
+ mobileDescription: {
87
85
  control: 'text',
88
- description: 'Image URL of the avatar collage shown on the right side',
86
+ description: 'Description override used on the mobile variant (< 640px)',
87
+ table: {
88
+ type: { summary: 'string' },
89
+ },
90
+ },
91
+ mobileCtaText: {
92
+ control: 'text',
93
+ description: 'CTA text override used on the mobile variant (< 640px)',
89
94
  table: {
90
95
  type: { summary: 'string' },
91
96
  },
92
97
  },
93
98
  showImages: {
94
99
  control: 'boolean',
95
- description: 'Whether to show the avatar images',
100
+ description: 'Whether to show the avatar collage on both sides',
96
101
  table: {
97
102
  type: { summary: 'boolean' },
98
103
  },
@@ -129,8 +134,10 @@ export const Default = {
129
134
  promoCodeLabel: 'YOUR PROMO CODE',
130
135
  ctaText: 'Enjoy 1 month free on Starter',
131
136
  footerText: 'Use Promo code at checkout and get 90 End-to-End API minutes or 180 Audio-to-Video API minutes free.',
132
- leftImage: bannerAvatarsLeft,
133
- rightImage: bannerAvatarsRight,
137
+ // Below 640px the banner renders the Figma mobile variant with this copy.
138
+ mobileTitle: 'Welcome! Get your free Starter plan for 1 month',
139
+ mobileDescription: "For a limited time, enjoy 1 month free on our starter plan (worth $50). That's 9k credits for 90 End-to-End API minutes or 180 Audio-to-Video API minutes. Just apply the code below at checkout.",
140
+ mobileCtaText: 'Activate My Free Starter Plan',
134
141
  },
135
142
  };
136
143
  export const WithoutImages = {
@@ -153,8 +160,6 @@ export const TitleAndCtaOnly = {
153
160
  description: undefined,
154
161
  features: [],
155
162
  ctaText: 'Get Started',
156
- leftImage: bannerAvatarsLeft,
157
- rightImage: bannerAvatarsRight,
158
163
  },
159
164
  };
160
165
  const FIGMA_MOBILE_VIEWPORT = {
@@ -169,15 +174,10 @@ const FIGMA_MOBILE_VIEWPORT = {
169
174
  },
170
175
  };
171
176
  export const Mobile = {
177
+ // Same props as Default — below 640px the component swaps to the mobile*
178
+ // content overrides and the Figma mobile layout automatically.
172
179
  args: {
173
- title: 'Welcome! Get your free Starter plan for 1 month',
174
- description: "For a limited time, enjoy 1 month free on our starter plan (worth $50). That's 9k credits for 90 End-to-End API minutes or 180 Audio-to-Video API minutes. Just apply the code below at checkout.",
175
- features: [],
176
- promoCode: 'PKAJ2HGO',
177
- promoCodeLabel: 'YOUR PROMO CODE',
178
- ctaText: 'Activate My Free Starter Plan',
179
- footerText: undefined,
180
- showImages: false,
180
+ ...Default.args,
181
181
  },
182
182
  parameters: {
183
183
  layout: 'padded',
@@ -188,10 +188,11 @@ export const Mobile = {
188
188
  },
189
189
  docs: {
190
190
  description: {
191
- story: 'The promo banner on the mobile Home screen side images hidden, ' +
192
- 'auto height, promo code and CTA stacked full-width. Below 640px the ' +
193
- 'component reflows automatically; best viewed in the Canvas with the ' +
194
- 'preset mobile viewport.',
191
+ story: 'The same PromoBanner as Default, viewed below 640px. The component ' +
192
+ 'renders the Figma mobile variant: side collages and features/footer ' +
193
+ 'hidden, text centered, promo code and CTA stacked full-width, and the ' +
194
+ 'mobileTitle / mobileDescription / mobileCtaText copy applied. Best ' +
195
+ 'viewed in the Canvas with the preset mobile viewport.',
195
196
  },
196
197
  },
197
198
  },
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { SelectAvatarCardProps } from './types';
3
+ /**
4
+ * SelectAvatarCard — a media card for the currently selected avatar. Mirrors the
5
+ * Figma "Selected Avatar" component with its Filled (Default + Hover), Empty and
6
+ * Error states. In the Filled state the Change action is always visible and the
7
+ * Preview action is revealed on hover.
8
+ */
9
+ export declare const SelectAvatarCard: React.FC<SelectAvatarCardProps>;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box } from '@mui/material';
3
+ import { SelectAvatarCardState } from './types';
4
+ import { Typography, TypographySize, TypographyWeight } from '../Typography';
5
+ import { MainButton, ButtonHierarchy, ButtonSize, ButtonShape, } from '../Buttons';
6
+ import { getIconComponent } from '../../utils';
7
+ // Scrim behind the avatar name, matching the Figma "name section" gradient
8
+ // (transparent at the top, fading to ~64% #2d2637 at the bottom).
9
+ const NAME_GRADIENT = 'linear-gradient(180deg, rgba(45, 38, 55, 0) 0%, rgba(45, 38, 55, 0.047) 11%, rgba(45, 38, 55, 0.094) 20%, rgba(45, 38, 55, 0.16) 34%, rgba(45, 38, 55, 0.64) 100%)';
10
+ /**
11
+ * SelectAvatarCard — a media card for the currently selected avatar. Mirrors the
12
+ * Figma "Selected Avatar" component with its Filled (Default + Hover), Empty and
13
+ * Error states. In the Filled state the Change action is always visible and the
14
+ * Preview action is revealed on hover.
15
+ */
16
+ export const SelectAvatarCard = ({ state = SelectAvatarCardState.Filled, showHeader = true, headerTitle = 'Selected Avatar', avatarImage, avatarName, showChangeButton = true, changeButtonText = 'Change', onChange, showPreviewButton = true, previewButtonText = 'Preview', onPreview, selectButtonText = 'Select Avatar', onSelect, width = '552px', height = '344px', style, testId, }) => {
17
+ const isFilled = state === SelectAvatarCardState.Filled;
18
+ const isEmpty = state === SelectAvatarCardState.Empty;
19
+ const isError = state === SelectAvatarCardState.Error;
20
+ const mediaBackground = isEmpty
21
+ ? 'var(--color-background-bg-element)'
22
+ : isError
23
+ ? 'var(--color-background-bg-button-disabled)'
24
+ : 'transparent';
25
+ return (_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", width: width, height: height, boxSizing: "border-box", padding: "var(--spacing-tokens-size-in-px-spacing-spacing-xl)", borderRadius: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", bgcolor: "var(--color-background-bg-page-contrast)", sx: {
26
+ border: '1px solid var(--color-border-border-subtle)',
27
+ boxShadow: '0px 2px 4px 0px var(--global-shadows-shadow-xs-color)',
28
+ // Reveal the Preview pill when the card is hovered.
29
+ '&:hover .select-avatar-card__preview': {
30
+ opacity: 1,
31
+ visibility: 'visible',
32
+ },
33
+ ...style,
34
+ }, "data-testid": testId ?? 'select-avatar-card', children: [showHeader && (_jsx(Box, { display: "flex", alignItems: "center", width: "100%", children: _jsx(Typography, { size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, color: "var(--color-text-text-title)", textStyle: { flex: '1 0 0', minWidth: 0, wordBreak: 'break-word' }, children: headerTitle }) })), _jsxs(Box, { position: "relative", flex: "1 0 0", minHeight: 0, width: "100%", overflow: "hidden", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-sm)", bgcolor: mediaBackground, children: [isFilled && (_jsxs(_Fragment, { children: [avatarImage && (_jsx(Box, { component: "img", src: avatarImage, alt: avatarName ?? '', position: "absolute", sx: {
35
+ inset: 0,
36
+ width: '100%',
37
+ height: '100%',
38
+ objectFit: 'cover',
39
+ pointerEvents: 'none',
40
+ } })), _jsxs(Box, { position: "absolute", top: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", right: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", display: "flex", alignItems: "flex-start", justifyContent: "flex-end", gap: "10px", children: [showPreviewButton && (_jsx(Box, { className: "select-avatar-card__preview", sx: {
41
+ opacity: 0,
42
+ visibility: 'hidden',
43
+ transition: 'opacity 0.15s ease-in-out',
44
+ }, children: _jsx(MainButton, { text: previewButtonText, size: ButtonSize.md, hierarchy: ButtonHierarchy.SecondaryTransparent, showLeadingIcon: true, leadingIcon: "Play", onClick: onPreview, testId: testId ? `${testId}-preview` : undefined }) })), showChangeButton && (_jsx(MainButton, { text: changeButtonText, size: ButtonSize.md, hierarchy: ButtonHierarchy.WhiteText, showLeadingIcon: true, leadingIcon: "ArrowsLeftRight", onClick: onChange, testId: testId ? `${testId}-change` : undefined }))] }), avatarName && (_jsx(Box, { position: "absolute", left: 0, right: 0, bottom: 0, display: "flex", alignItems: "center", padding: "24px 12px 12px", sx: { background: NAME_GRADIENT }, children: _jsx(Typography, { size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, color: "var(--color-text-text-white)", textStyle: {
45
+ flex: '1 0 0',
46
+ minWidth: 0,
47
+ wordBreak: 'break-word',
48
+ }, children: avatarName }) }))] })), isEmpty && (_jsx(Box, { position: "absolute", sx: {
49
+ top: '50%',
50
+ left: '50%',
51
+ transform: 'translate(-50%, -50%)',
52
+ }, children: _jsx(MainButton, { text: selectButtonText, hierarchy: ButtonHierarchy.PrimaryLemon, size: ButtonSize.xs, shape: ButtonShape.Square, onClick: onSelect, testId: testId ? `${testId}-select` : undefined }) })), isError && (_jsx(Box, { position: "absolute", display: "flex", alignItems: "center", justifyContent: "center", sx: {
53
+ top: '50%',
54
+ left: '50%',
55
+ transform: 'translate(-50%, -50%)',
56
+ }, children: getIconComponent('Warning', {
57
+ size: 182,
58
+ weight: 'regular',
59
+ color: 'var(--color-foreground-fg-disabled)',
60
+ }) }))] })] }));
61
+ };
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SelectAvatarCard } from './SelectAvatarCard';
3
+ declare const meta: Meta<typeof SelectAvatarCard>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof SelectAvatarCard>;
6
+ /**
7
+ * An avatar is selected. The Change action is always visible; hover the card to
8
+ * reveal the Preview action.
9
+ */
10
+ export declare const Default: Story;
11
+ /** No avatar picked yet — a centered call to action to select one. */
12
+ export declare const Empty: Story;
13
+ /** The preview could not be loaded — a centered warning icon. */
14
+ export declare const Error: Story;
15
+ /** The Filled card without its header, e.g. when embedded under another label. */
16
+ export declare const WithoutHeader: Story;
17
+ /**
18
+ * All four states side by side, matching the Figma frame. Hover the first card
19
+ * to reveal its Preview action.
20
+ */
21
+ export declare const AllStates: Story;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SelectAvatarCard } from './SelectAvatarCard';
3
+ import { SelectAvatarCardState } from './types';
4
+ import { BANNER_AVATARS } from './bannerAvatars';
5
+ const SAMPLE_AVATAR = BANNER_AVATARS.nelly;
6
+ const meta = {
7
+ title: 'Design System/Components/UI/SelectAvatarCard',
8
+ component: SelectAvatarCard,
9
+ parameters: {
10
+ layout: 'centered',
11
+ design: {
12
+ type: 'figspec',
13
+ url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=4644-15058',
14
+ },
15
+ },
16
+ tags: ['autodocs'],
17
+ argTypes: {
18
+ state: {
19
+ control: 'select',
20
+ options: Object.values(SelectAvatarCardState),
21
+ description: 'Visual state of the card',
22
+ },
23
+ showHeader: { control: 'boolean' },
24
+ headerTitle: { control: 'text' },
25
+ avatarImage: { control: 'text', description: 'Preview image URL (Filled)' },
26
+ avatarName: { control: 'text', description: 'Avatar name (Filled)' },
27
+ showChangeButton: { control: 'boolean' },
28
+ changeButtonText: { control: 'text' },
29
+ showPreviewButton: {
30
+ control: 'boolean',
31
+ description: 'Reveal the Preview action on hover (Filled)',
32
+ },
33
+ previewButtonText: { control: 'text' },
34
+ selectButtonText: { control: 'text' },
35
+ width: { control: 'text', table: { defaultValue: { summary: '552px' } } },
36
+ height: { control: 'text', table: { defaultValue: { summary: '344px' } } },
37
+ onChange: { action: 'change clicked' },
38
+ onPreview: { action: 'preview clicked' },
39
+ onSelect: { action: 'select clicked' },
40
+ },
41
+ };
42
+ export default meta;
43
+ /**
44
+ * An avatar is selected. The Change action is always visible; hover the card to
45
+ * reveal the Preview action.
46
+ */
47
+ export const Default = {
48
+ args: {
49
+ state: SelectAvatarCardState.Filled,
50
+ avatarImage: SAMPLE_AVATAR,
51
+ avatarName: 'Anna',
52
+ },
53
+ };
54
+ /** No avatar picked yet — a centered call to action to select one. */
55
+ export const Empty = {
56
+ args: {
57
+ state: SelectAvatarCardState.Empty,
58
+ },
59
+ };
60
+ /** The preview could not be loaded — a centered warning icon. */
61
+ export const Error = {
62
+ args: {
63
+ state: SelectAvatarCardState.Error,
64
+ },
65
+ };
66
+ /** The Filled card without its header, e.g. when embedded under another label. */
67
+ export const WithoutHeader = {
68
+ args: {
69
+ ...Default.args,
70
+ showHeader: false,
71
+ },
72
+ };
73
+ /**
74
+ * All four states side by side, matching the Figma frame. Hover the first card
75
+ * to reveal its Preview action.
76
+ */
77
+ export const AllStates = {
78
+ parameters: { layout: 'padded' },
79
+ render: () => (_jsxs("div", { style: {
80
+ display: 'flex',
81
+ flexWrap: 'wrap',
82
+ gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
83
+ alignItems: 'flex-start',
84
+ }, children: [_jsx(SelectAvatarCard, { state: SelectAvatarCardState.Filled, avatarImage: SAMPLE_AVATAR, avatarName: "Anna", onChange: () => { }, onPreview: () => { } }), _jsx(SelectAvatarCard, { state: SelectAvatarCardState.Empty, onSelect: () => { } }), _jsx(SelectAvatarCard, { state: SelectAvatarCardState.Error })] })),
85
+ };
@@ -0,0 +1,12 @@
1
+ export declare const BANNER_AVATARS: {
2
+ nelly: string;
3
+ micheal: string;
4
+ fjolla: string;
5
+ laura: string;
6
+ yuruo: string;
7
+ jerome: string;
8
+ };
9
+ export declare const AVATAR_TILE_BACKGROUND = "linear-gradient(180deg, #e9e9e9 0%, #beb1df 100%)";
10
+ export declare const BANNER_VIDEO_PREVIEW = "https://storage.googleapis.com/bey-dev-public/yuruo-preview.png";
11
+ export declare const AVATAR_TILE_WIDTH = 130;
12
+ export declare const AVATAR_TILE_HEIGHT = 150;
@@ -0,0 +1,20 @@
1
+ // Shared avatar imagery used by the banner collages (PromoBanner side columns
2
+ // and ConversationalAvatarsBanner collage column). Keeping them here lets both components reuse
3
+ // the same set of avatars.
4
+ const AVATAR_BASE = 'https://storage.googleapis.com/bey-dev-public';
5
+ export const BANNER_AVATARS = {
6
+ nelly: `${AVATAR_BASE}/nelly.jpg`,
7
+ micheal: `${AVATAR_BASE}/micheal.jpg`,
8
+ fjolla: `${AVATAR_BASE}/fjolla.png`,
9
+ laura: `${AVATAR_BASE}/laura.png`,
10
+ yuruo: `${AVATAR_BASE}/yuruo.png`,
11
+ jerome: `${AVATAR_BASE}/jerome.png`,
12
+ };
13
+ // Placeholder gradient shown behind avatars with transparent areas so every
14
+ // tile has a consistent backdrop (matches the Figma image tile fill).
15
+ export const AVATAR_TILE_BACKGROUND = 'linear-gradient(180deg, #e9e9e9 0%, #beb1df 100%)';
16
+ // Poster/thumbnail shown in the ConversationalVoiceAgentsBanner video preview.
17
+ export const BANNER_VIDEO_PREVIEW = `${AVATAR_BASE}/yuruo-preview.png`;
18
+ // Size of a single avatar tile in the collages.
19
+ export const AVATAR_TILE_WIDTH = 130;
20
+ export const AVATAR_TILE_HEIGHT = 150;
@@ -1,14 +1,16 @@
1
1
  export * from './Badge';
2
2
  export * from './ButtonCard';
3
3
  export * from './Avatar';
4
+ export * from './ConversationalAvatarsBanner';
4
5
  export * from './CheckIcon';
5
- export * from './FeatureBanner';
6
+ export * from './ConversationalVoiceAgentsBanner';
6
7
  export * from './HintBubble';
7
8
  export * from './Logo';
8
9
  export * from './Menu';
9
10
  export * from './Popover';
10
11
  export * from './ProgressIndicator';
11
12
  export * from './PromoBanner';
13
+ export * from './SelectAvatarCard';
12
14
  export * from './SkeletonLoader';
13
15
  export * from './Topbar';
14
16
  export * from './types';
@@ -1,14 +1,16 @@
1
1
  export * from './Badge';
2
2
  export * from './ButtonCard';
3
3
  export * from './Avatar';
4
+ export * from './ConversationalAvatarsBanner';
4
5
  export * from './CheckIcon';
5
- export * from './FeatureBanner';
6
+ export * from './ConversationalVoiceAgentsBanner';
6
7
  export * from './HintBubble';
7
8
  export * from './Logo';
8
9
  export * from './Menu';
9
10
  export * from './Popover';
10
11
  export * from './ProgressIndicator';
11
12
  export * from './PromoBanner';
13
+ export * from './SelectAvatarCard';
12
14
  export * from './SkeletonLoader';
13
15
  export * from './Topbar';
14
16
  export * from './types';
@@ -354,13 +354,15 @@ export interface PromoBannerProps {
354
354
  ctaText?: string;
355
355
  /** Called when the call-to-action button is clicked */
356
356
  onCtaClick?: () => void;
357
- /** Footer note shown below the actions */
357
+ /** Footer note shown below the actions (desktop only) */
358
358
  footerText?: string;
359
- /** Image URL of the avatar collage shown on the left side */
360
- leftImage?: string;
361
- /** Image URL of the avatar collage shown on the right side */
362
- rightImage?: string;
363
- /** Whether to show the avatar images */
359
+ /** Title override used on the mobile variant (falls back to `title`) */
360
+ mobileTitle?: string;
361
+ /** Description override used on the mobile variant (falls back to `description`) */
362
+ mobileDescription?: string;
363
+ /** CTA text override used on the mobile variant (falls back to `ctaText`) */
364
+ mobileCtaText?: string;
365
+ /** Whether to show the avatar collage on both sides of the banner */
364
366
  showImages?: boolean;
365
367
  /** Background of the banner */
366
368
  background?: string;
@@ -370,17 +372,10 @@ export interface PromoBannerProps {
370
372
  testId?: string;
371
373
  }
372
374
  /**
373
- * FeatureBanner media variants
375
+ * Shared banner action button, used by ConversationalAvatarsBanner and
376
+ * ConversationalVoiceAgentsBanner.
374
377
  */
375
- export declare enum FeatureBannerMedia {
376
- Video = "Video",
377
- Image = "Image",
378
- None = "None"
379
- }
380
- /**
381
- * FeatureBanner action button
382
- */
383
- export interface FeatureBannerAction {
378
+ export interface BannerAction {
384
379
  /** Button text */
385
380
  text: string;
386
381
  /** Called when the button is clicked */
@@ -389,25 +384,44 @@ export interface FeatureBannerAction {
389
384
  primary?: boolean;
390
385
  }
391
386
  /**
392
- * FeatureBanner component props
387
+ * ConversationalAvatarsBanner component props — a feature card whose media is a collage column
388
+ * of avatars (reused from the PromoBanner set) that stays on the right at all
389
+ * breakpoints.
393
390
  */
394
- export interface FeatureBannerProps {
391
+ export interface ConversationalAvatarsBannerProps {
395
392
  /** Title of the banner */
396
393
  title?: string;
397
394
  /** Description shown below the title */
398
395
  description?: string;
399
396
  /** Action buttons */
400
- actions?: FeatureBannerAction[];
401
- /** Media shown on the right side of the banner */
402
- media?: FeatureBannerMedia;
403
- /** Image URL for the media (video thumbnail or image column) */
404
- mediaImage?: string;
397
+ actions?: BannerAction[];
398
+ /** Background of the banner */
399
+ background?: string;
400
+ /** Custom style of the banner */
401
+ style?: SxProps<Theme>;
402
+ /** Test ID of the banner */
403
+ testId?: string;
404
+ }
405
+ /**
406
+ * ConversationalVoiceAgentsBanner component props — a feature card whose media is a
407
+ * video preview (thumbnail with a play button and an optional CTA pill). Text
408
+ * and actions sit on the left on desktop and stack above the preview on mobile.
409
+ */
410
+ export interface ConversationalVoiceAgentsBannerProps {
411
+ /** Title of the banner */
412
+ title?: string;
413
+ /** Description shown below the title */
414
+ description?: string;
415
+ /** Action buttons */
416
+ actions?: BannerAction[];
417
+ /** Thumbnail/poster image URL for the video preview */
418
+ previewImage?: string;
405
419
  /** Called when the play button on the video preview is clicked */
406
420
  onPlayClick?: () => void;
407
421
  /** CTA text overlaid on the video preview */
408
- mediaCtaText?: string;
409
- /** Called when the media CTA is clicked */
410
- onMediaCtaClick?: () => void;
422
+ previewCtaText?: string;
423
+ /** Called when the preview CTA is clicked */
424
+ onPreviewCtaClick?: () => void;
411
425
  /** Background of the banner */
412
426
  background?: string;
413
427
  /** Custom style of the banner */
@@ -486,6 +500,59 @@ export interface TabMenuProps {
486
500
  /** Test ID of the tab menu. */
487
501
  testId?: string;
488
502
  }
503
+ /**
504
+ * Visual state of the {@link SelectAvatarCard}.
505
+ * - `Filled`: an avatar is selected — shows the preview image, its name and the
506
+ * Change action (Preview appears on hover).
507
+ * - `Empty`: no avatar yet — shows a centered "Select Avatar" button.
508
+ * - `Error`: the preview failed to load — shows a centered warning icon.
509
+ */
510
+ export declare enum SelectAvatarCardState {
511
+ Filled = "Filled",
512
+ Empty = "Empty",
513
+ Error = "Error"
514
+ }
515
+ /**
516
+ * SelectAvatarCard component props — a media card that shows the currently
517
+ * selected avatar (with Change / Preview actions), an empty state prompting the
518
+ * user to pick one, or an error state.
519
+ */
520
+ export interface SelectAvatarCardProps {
521
+ /** Visual state of the card. Defaults to `Filled`. */
522
+ state?: SelectAvatarCardState;
523
+ /** Show the card header. Defaults to `true`. */
524
+ showHeader?: boolean;
525
+ /** Header title. Defaults to "Selected Avatar". */
526
+ headerTitle?: string;
527
+ /** Avatar preview image URL (used in the `Filled` state). */
528
+ avatarImage?: string;
529
+ /** Avatar name shown over the bottom gradient (used in the `Filled` state). */
530
+ avatarName?: string;
531
+ /** Show the Change action in the `Filled` state. Defaults to `true`. */
532
+ showChangeButton?: boolean;
533
+ /** Change button label. Defaults to "Change". */
534
+ changeButtonText?: string;
535
+ /** Called when the Change button is clicked. */
536
+ onChange?: () => void;
537
+ /** Reveal the Preview action on hover in the `Filled` state. Defaults to `true`. */
538
+ showPreviewButton?: boolean;
539
+ /** Preview button label. Defaults to "Preview". */
540
+ previewButtonText?: string;
541
+ /** Called when the Preview button is clicked. */
542
+ onPreview?: () => void;
543
+ /** Empty-state button label. Defaults to "Select Avatar". */
544
+ selectButtonText?: string;
545
+ /** Called when the empty-state Select Avatar button is clicked. */
546
+ onSelect?: () => void;
547
+ /** Width of the card. Defaults to "552px". */
548
+ width?: string | number;
549
+ /** Height of the card. Defaults to "344px". */
550
+ height?: string | number;
551
+ /** Custom style. */
552
+ style?: SxProps<Theme>;
553
+ /** Test id. */
554
+ testId?: string;
555
+ }
489
556
  /**
490
557
  * Button card component props — a compact, clickable card with a leading icon,
491
558
  * title, description and an optional corner action button.
@@ -110,15 +110,6 @@ export var ProgressIndicatorMode;
110
110
  /** Renders only the bar, with no accompanying value text. */
111
111
  ProgressIndicatorMode["Bar"] = "Bar";
112
112
  })(ProgressIndicatorMode || (ProgressIndicatorMode = {}));
113
- /**
114
- * FeatureBanner media variants
115
- */
116
- export var FeatureBannerMedia;
117
- (function (FeatureBannerMedia) {
118
- FeatureBannerMedia["Video"] = "Video";
119
- FeatureBannerMedia["Image"] = "Image";
120
- FeatureBannerMedia["None"] = "None";
121
- })(FeatureBannerMedia || (FeatureBannerMedia = {}));
122
113
  /**
123
114
  * TabMenu size variants
124
115
  */
@@ -127,3 +118,16 @@ export var TabMenuSize;
127
118
  TabMenuSize["md"] = "md";
128
119
  TabMenuSize["sm"] = "sm";
129
120
  })(TabMenuSize || (TabMenuSize = {}));
121
+ /**
122
+ * Visual state of the {@link SelectAvatarCard}.
123
+ * - `Filled`: an avatar is selected — shows the preview image, its name and the
124
+ * Change action (Preview appears on hover).
125
+ * - `Empty`: no avatar yet — shows a centered "Select Avatar" button.
126
+ * - `Error`: the preview failed to load — shows a centered warning icon.
127
+ */
128
+ export var SelectAvatarCardState;
129
+ (function (SelectAvatarCardState) {
130
+ SelectAvatarCardState["Filled"] = "Filled";
131
+ SelectAvatarCardState["Empty"] = "Empty";
132
+ SelectAvatarCardState["Error"] = "Error";
133
+ })(SelectAvatarCardState || (SelectAvatarCardState = {}));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beydesign/storybook",
3
3
  "private": false,
4
- "version": "0.2.16",
4
+ "version": "0.2.18",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -1,3 +0,0 @@
1
- import React from 'react';
2
- import { FeatureBannerProps } from './types';
3
- export declare const FeatureBanner: React.FC<FeatureBannerProps>;
@@ -1,50 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box } from '@mui/material';
3
- import { FeatureBannerMedia, } from './types';
4
- import { Typography, TypographySize, TypographyWeight } from '../Typography';
5
- import { MainButton, ButtonHierarchy, ButtonSize, ButtonDisplayMode, } from '../Buttons';
6
- const VIOLET_GRADIENT = 'linear-gradient(180deg, rgba(194, 171, 252, 0.08) 0%, rgba(128, 96, 198, 0.2) 100%)';
7
- const PINK_GRADIENT = 'linear-gradient(90deg, rgba(235, 205, 240, 0.05) 0%, rgba(202, 59, 224, 0.15) 100%)';
8
- // Below this width the banner stacks vertically to match the Figma mobile
9
- // layout (text + actions on top, media full-width below).
10
- const MOBILE = '@media (max-width: 640px)';
11
- export const FeatureBanner = ({ title, description, actions = [], media = FeatureBannerMedia.None, mediaImage, onPlayClick, mediaCtaText, onMediaCtaClick, background, style: customStyle, testId, }) => {
12
- const bannerBackground = background ||
13
- (media === FeatureBannerMedia.Video ? PINK_GRADIENT : VIOLET_GRADIENT);
14
- const renderAction = (action, index) => (_jsx(MainButton, { text: action.text, size: ButtonSize.md, hierarchy: action.primary
15
- ? ButtonHierarchy.Primary
16
- : ButtonHierarchy.SecondaryColor, onClick: action.onClick, style: action.primary
17
- ? undefined
18
- : {
19
- border: '1px solid var(--color-border-border-component)',
20
- }, testId: testId ? `${testId}-action-${index}` : undefined }, `${action.text}-${index}`));
21
- const renderMedia = () => {
22
- if (media === FeatureBannerMedia.None || !mediaImage)
23
- return null;
24
- if (media === FeatureBannerMedia.Video) {
25
- return (_jsxs(Box, { position: "relative", width: "296px", height: "150px", flexShrink: 0, overflow: "hidden", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-sm)", sx: {
26
- [MOBILE]: {
27
- width: '100%',
28
- height: 'auto',
29
- aspectRatio: '296 / 150',
30
- },
31
- }, children: [_jsx(Box, { component: "img", src: mediaImage, alt: "", width: "100%", height: "100%", sx: { objectFit: 'cover', display: 'block' } }), _jsx(Box, { position: "absolute", left: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", bottom: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: _jsx(MainButton, { size: ButtonSize.md, hierarchy: ButtonHierarchy.SecondaryTransparent, displayIconMode: ButtonDisplayMode.Only, displayIcon: "Play", onClick: onPlayClick, style: { border: 'none' }, testId: testId ? `${testId}-play` : undefined }) }), mediaCtaText && (_jsx(Box, { position: "absolute", right: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", bottom: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: _jsx(MainButton, { text: mediaCtaText, size: ButtonSize.md, hierarchy: ButtonHierarchy.SecondaryTransparent, onClick: onMediaCtaClick, style: { border: 'none' }, testId: testId ? `${testId}-media-cta` : undefined }) }))] }));
32
- }
33
- // Image media spans the full banner height by escaping the vertical padding
34
- return (_jsx(Box, { component: "img", src: mediaImage, alt: "", flexShrink: 0, sx: {
35
- marginY: 'calc(-1 * var(--spacing-tokens-size-in-px-spacing-spacing-3xl))',
36
- [MOBILE]: { marginY: 0, maxWidth: '100%' },
37
- } }));
38
- };
39
- return (_jsxs(Box, { display: "flex", alignItems: "center", gap: "40px", width: "100%", overflow: "hidden", borderRadius: "var(--spacing-tokens-size-in-px-radius-radius-lg)", padding: "var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-2xl)", sx: {
40
- background: bannerBackground,
41
- boxSizing: 'border-box',
42
- [MOBILE]: {
43
- flexDirection: 'column',
44
- alignItems: 'stretch',
45
- gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-3xl)',
46
- padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg)',
47
- },
48
- ...customStyle,
49
- }, "data-testid": testId, children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-xl)", flex: 1, minWidth: 0, children: [_jsxs(Box, { display: "flex", flexDirection: "column", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: [title && (_jsx(Typography, { size: TypographySize.HeadingM, weight: TypographyWeight.Bold, color: "var(--color-text-text-title)", children: title })), description && (_jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-label)", children: description }))] }), actions.length > 0 && (_jsx(Box, { display: "flex", alignItems: "center", gap: "var(--spacing-tokens-size-in-px-spacing-spacing-md)", children: actions.map(renderAction) }))] }), renderMedia()] }));
50
- };
@@ -1,10 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { FeatureBanner } from './FeatureBanner';
3
- declare const meta: Meta<typeof FeatureBanner>;
4
- export default meta;
5
- type Story = StoryObj<typeof FeatureBanner>;
6
- export declare const Video: Story;
7
- export declare const Image: Story;
8
- export declare const TextOnly: Story;
9
- export declare const Mobile: Story;
10
- export declare const HomeLayout: Story;
@@ -1,163 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { FeatureBanner } from './FeatureBanner';
3
- import { FeatureBannerMedia } from './types';
4
- import videoPreview from '../assets/banner/video-preview.png';
5
- import avatarsColumn from '../assets/banner/banner-avatars-column.png';
6
- const meta = {
7
- title: 'Design System/Components/UI/FeatureBanner',
8
- component: FeatureBanner,
9
- parameters: {
10
- layout: 'padded',
11
- design: {
12
- type: 'figspec',
13
- url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=7011-27024',
14
- },
15
- },
16
- tags: ['autodocs'],
17
- argTypes: {
18
- title: {
19
- control: 'text',
20
- description: 'Title of the banner',
21
- table: {
22
- type: { summary: 'string' },
23
- },
24
- },
25
- description: {
26
- control: 'text',
27
- description: 'Description shown below the title',
28
- table: {
29
- type: { summary: 'string' },
30
- },
31
- },
32
- actions: {
33
- control: 'object',
34
- description: 'Action buttons — set primary: true for the brand (violet) button',
35
- table: {
36
- type: { summary: 'FeatureBannerAction[]' },
37
- },
38
- },
39
- media: {
40
- control: 'select',
41
- description: 'Media shown on the right side of the banner',
42
- options: Object.values(FeatureBannerMedia),
43
- table: {
44
- type: { summary: 'string' },
45
- },
46
- },
47
- mediaImage: {
48
- control: 'text',
49
- description: 'Image URL for the media (video thumbnail or image column)',
50
- table: {
51
- type: { summary: 'string' },
52
- },
53
- },
54
- onPlayClick: {
55
- action: 'play clicked',
56
- description: 'Called when the play button on the video preview is clicked',
57
- table: {
58
- type: { summary: 'function' },
59
- },
60
- },
61
- mediaCtaText: {
62
- control: 'text',
63
- description: 'CTA text overlaid on the video preview',
64
- table: {
65
- type: { summary: 'string' },
66
- },
67
- },
68
- onMediaCtaClick: {
69
- action: 'media cta clicked',
70
- description: 'Called when the media CTA is clicked',
71
- table: {
72
- type: { summary: 'function' },
73
- },
74
- },
75
- background: {
76
- control: 'text',
77
- description: 'Background of the banner',
78
- table: {
79
- type: { summary: 'string' },
80
- },
81
- },
82
- style: {
83
- control: 'object',
84
- description: 'Custom style for the banner',
85
- table: {
86
- type: { summary: 'object' },
87
- },
88
- },
89
- },
90
- };
91
- export default meta;
92
- export const Video = {
93
- args: {
94
- title: 'Add a Human Face to your Conversational Voice Agents',
95
- description: 'Bring your ideas to life with intelligent conversational avatars.',
96
- actions: [
97
- { text: 'Create API Key' },
98
- { text: 'Go to Docs', primary: true },
99
- ],
100
- media: FeatureBannerMedia.Video,
101
- mediaImage: videoPreview,
102
- mediaCtaText: 'How to Get Started',
103
- },
104
- };
105
- export const Image = {
106
- args: {
107
- title: 'Introducing Conversational Avatars',
108
- description: 'Bring your ideas to life with intelligent conversational avatars.',
109
- actions: [{ text: 'Create New Agent' }],
110
- media: FeatureBannerMedia.Image,
111
- mediaImage: avatarsColumn,
112
- },
113
- };
114
- export const TextOnly = {
115
- args: {
116
- title: 'Introducing Conversational Avatars',
117
- description: 'Bring your ideas to life with intelligent conversational avatars.',
118
- actions: [{ text: 'Create New Agent' }],
119
- media: FeatureBannerMedia.None,
120
- },
121
- };
122
- const FIGMA_MOBILE_VIEWPORT = {
123
- viewport: {
124
- viewports: {
125
- figmaMobile: {
126
- name: 'Figma Mobile (375px)',
127
- styles: { width: '375px', height: '812px' },
128
- },
129
- },
130
- defaultViewport: 'figmaMobile',
131
- },
132
- };
133
- export const Mobile = {
134
- args: {
135
- ...Video.args,
136
- },
137
- parameters: {
138
- layout: 'padded',
139
- ...FIGMA_MOBILE_VIEWPORT,
140
- design: {
141
- type: 'figspec',
142
- url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=7081-24780',
143
- },
144
- docs: {
145
- description: {
146
- story: 'The feature banner on the mobile Home screen — text and actions ' +
147
- 'stack on top, media spans full-width below. Below 640px the component ' +
148
- 'reflows automatically; best viewed in the Canvas with the preset ' +
149
- 'mobile viewport.',
150
- },
151
- },
152
- },
153
- };
154
- export const HomeLayout = {
155
- parameters: {
156
- docs: {
157
- description: {
158
- story: 'Both banners side by side as they appear on the Home screen of the app.',
159
- },
160
- },
161
- },
162
- render: () => (_jsxs("div", { style: { display: 'flex', gap: '12px', alignItems: 'stretch' }, children: [_jsx("div", { style: { flex: 3, minWidth: 0, display: 'flex' }, children: _jsx(FeatureBanner, { ...Video.args }) }), _jsx("div", { style: { flex: 2, minWidth: 0, display: 'flex' }, children: _jsx(FeatureBanner, { ...Image.args }) })] })),
163
- };