@beydesign/storybook 0.4.5 → 0.4.7
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.
- package/dist/stories/EntityCard/AvatarSilhouette.d.ts +8 -0
- package/dist/stories/EntityCard/AvatarSilhouette.js +8 -0
- package/dist/stories/EntityCard/EntityCard.js +13 -5
- package/dist/stories/EntityCard/EntityCard.stories.js +3 -3
- package/dist/stories/EntityCard/types.d.ts +2 -2
- package/dist/stories/Form/TextArea.js +3 -1
- package/dist/stories/Form/TextInput.js +3 -1
- package/dist/stories/UI/ButtonCard.js +4 -1
- package/dist/stories/UI/ConversationalAvatarsBanner.js +1 -1
- package/dist/stories/UI/ConversationalVoiceAgentsBanner.js +2 -2
- package/dist/stories/UI/Popover.js +13 -2
- package/dist/stories/UI/SelectAvatarCard.js +2 -2
- package/dist/stories/UI/TabMenu.js +114 -9
- package/dist/stories/UI/TabMenu.stories.d.ts +6 -0
- package/dist/stories/UI/TabMenu.stories.js +23 -1
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The avatar bust silhouette shown as the empty-state placeholder (no image),
|
|
4
|
+
* mirroring the Figma "Empty" state. Fills its container and is anchored to the
|
|
5
|
+
* bottom, so the shoulders bleed off the card edge. Uses `currentColor` so the
|
|
6
|
+
* fill can be driven by a design token from the parent.
|
|
7
|
+
*/
|
|
8
|
+
export declare const AvatarSilhouette: React.FC;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The avatar bust silhouette shown as the empty-state placeholder (no image),
|
|
4
|
+
* mirroring the Figma "Empty" state. Fills its container and is anchored to the
|
|
5
|
+
* bottom, so the shoulders bleed off the card edge. Uses `currentColor` so the
|
|
6
|
+
* fill can be driven by a design token from the parent.
|
|
7
|
+
*/
|
|
8
|
+
export const AvatarSilhouette = () => (_jsx("svg", { viewBox: '0 0 206 224', width: '100%', height: '100%', preserveAspectRatio: 'none', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', style: { display: 'block' }, "aria-hidden": 'true', focusable: 'false', children: _jsx("path", { d: 'M99.3964 0.0456982C113.077 -0.455929 125.202 3.14652 135.525 12.1236C148.651 23.5376 147.776 38.5586 145.834 54.3326C145.588 56.3287 145.199 58.4767 144.991 60.4808C149.483 60.7505 150.277 64.6019 149.882 68.4009C149.333 73.6891 147.427 80.6122 143.514 84.4206C142.672 85.1879 140.346 86.2549 140.073 86.8043C139.095 88.7733 138.467 91.5773 137.716 93.6661C135.881 98.7729 133.492 103.4 129.918 107.52C128.811 108.796 127.72 109.764 126.634 110.938C125.107 116.921 126.767 127.746 129.952 133.133C134.695 141.159 144.185 143.686 152.658 146.232C158.101 147.813 163.51 149.514 168.878 151.329C178.708 154.699 189.396 158.77 194.713 168.238C197.991 174.074 199.3 179.931 200.599 186.435C201.789 192.393 202.659 198.373 203.549 204.377C204.13 208.372 204.663 212.376 205.146 216.386C205.449 218.83 205.845 221.556 206 224H0L0.0225427 223.877C0.483937 221.262 0.77993 216.99 1.11962 214.171C1.77914 208.761 2.57988 203.368 3.5213 198C4.96167 189.348 6.76585 179.033 10.363 171.015C12.2654 166.773 15.904 162.193 19.855 159.538C29.5517 153.12 41.2795 150.131 52.2751 146.656C59.2908 144.439 66.4763 142.872 72.352 138.147C79.5078 132.395 81.5098 119.718 79.8167 111.052L79.7425 110.962C78.8337 109.888 77.6063 108.78 76.5931 107.611C73.4593 103.966 71.0023 99.7957 69.3371 95.2959C68.78 93.7779 68.2501 92.2503 67.7476 90.7137C66.0041 85.3616 66.4977 87.0427 62.1073 84.1196C56.9385 80.6782 51.0027 60.6731 61.2827 60.467C61.0959 57.2634 60.4807 53.6568 60.1419 50.3502C58.9309 38.5311 59.2264 24.9218 67.4846 15.453C70.7115 11.7529 73.9771 10.0768 78.4209 8.59052C79.0166 8.39123 81.4647 5.42845 82.4273 5.1527C87.3821 1.81482 93.4351 0.264287 99.3964 0.0456982Z', fill: 'currentColor' }) }));
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box } from '@mui/material';
|
|
4
4
|
import { keyframes } from '@emotion/react';
|
|
5
|
-
import {
|
|
5
|
+
import { ArrowsClockwise, Warning } from '@phosphor-icons/react';
|
|
6
|
+
import { AvatarSilhouette } from './AvatarSilhouette';
|
|
6
7
|
import { Typography, TypographySize, TypographyWeight } from '../Typography';
|
|
7
8
|
import { Menu, Badge, BadgeColor, BadgeSize } from '../UI';
|
|
8
9
|
import { MainButton, ButtonHierarchy, ButtonSize, ButtonDisplayMode, } from '../Buttons';
|
|
@@ -37,8 +38,8 @@ export const EntityCard = ({ view = 'avatar', name, image, description = '', sho
|
|
|
37
38
|
const isProcessing = processing && !error;
|
|
38
39
|
const canPreview = !!onPreview && !isProcessing && !error;
|
|
39
40
|
const showActions = canPreview || hasMenu;
|
|
40
|
-
const showBadge = showStatusBadge && !!statusBadgeText;
|
|
41
|
-
const showImage = !!image && !imageError;
|
|
41
|
+
const showBadge = showStatusBadge && !!statusBadgeText && !error;
|
|
42
|
+
const showImage = !!image && !imageError && !error;
|
|
42
43
|
const resolvedTestId = testId ?? `${entityKey}-${isVideo ? 'video' : 'entity'}-card`;
|
|
43
44
|
const handlePreviewButtonClick = (event) => {
|
|
44
45
|
event.stopPropagation();
|
|
@@ -60,12 +61,19 @@ export const EntityCard = ({ view = 'avatar', name, image, description = '', sho
|
|
|
60
61
|
opacity: 1,
|
|
61
62
|
pointerEvents: 'auto',
|
|
62
63
|
},
|
|
63
|
-
}, "data-testid": resolvedTestId, children: [_jsxs(Box, { position: 'relative', width: '100%', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', overflow: 'hidden', sx: { aspectRatio: imageAspectRatio }, children: [
|
|
64
|
+
}, "data-testid": resolvedTestId, children: [_jsxs(Box, { position: 'relative', width: '100%', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', overflow: 'hidden', sx: { aspectRatio: imageAspectRatio }, children: [error ? (_jsx(Box, { width: '100%', height: '100%', bgcolor: 'var(--color-background-bg-badge-accent-red)', display: 'flex', justifyContent: 'center', alignItems: 'center', role: 'img', "aria-label": errorText, title: errorText, children: _jsx(Warning, { size: 50, color: 'var(--color-foreground-fg-error)' }) })) : showImage ? (_jsx("img", { src: image, alt: name, width: '100%', height: '100%', style: { objectFit: 'cover', height: '100%', width: '100%' }, onError: () => setImageError(true) })) : (_jsx(Box, { position: 'relative', width: '100%', height: '100%', overflow: 'hidden', bgcolor: 'var(--color-background-bg-button-disabled)', children: _jsx(Box, { position: 'absolute', bottom: 0, left: '50%', sx: {
|
|
65
|
+
transform: 'translateX(-50%)',
|
|
66
|
+
color: 'var(--color-border-border-subtle)',
|
|
67
|
+
aspectRatio: '206 / 224',
|
|
68
|
+
...(isVideo
|
|
69
|
+
? { height: '100%', maxWidth: '100%' }
|
|
70
|
+
: { width: '206px', height: '224px' }),
|
|
71
|
+
}, children: _jsx(AvatarSilhouette, {}) }) })), _jsx(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', sx: {
|
|
64
72
|
backgroundImage: 'linear-gradient(180deg, rgba(21, 0, 73, 0) 0%, rgba(72, 29, 180, 0.152) 16.76%, rgba(32, 13, 82, 0.36) 100%)',
|
|
65
73
|
opacity: hovered && !isProcessing && !error ? 1 : 0,
|
|
66
74
|
transition: 'opacity 0.15s ease-in-out',
|
|
67
75
|
pointerEvents: 'none',
|
|
68
|
-
} }), isProcessing && (_jsxs(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', bgcolor: 'var(--color-background-bg-overlay-2)', sx: { zIndex: 1 }, children: [_jsx(Spinner, {}), _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', children: processingText })] })),
|
|
76
|
+
} }), isProcessing && (_jsxs(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', bgcolor: 'var(--color-background-bg-overlay-2)', sx: { zIndex: 1 }, children: [_jsx(Spinner, {}), _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', children: processingText })] })), showBadge && (_jsx(Box, { position: 'absolute', top: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', left: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', sx: { zIndex: 2 }, children: _jsx(Badge, { text: statusBadgeText, color: statusBadgeColor, size: BadgeSize.md, showIcon: true, icon: 'Circle', iconWeight: 'fill', iconSize: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)' }) })), showActions && (_jsxs(Box, { className: ACTIONS_CLASS, position: 'absolute', top: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', right: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', sx: {
|
|
69
77
|
zIndex: 4,
|
|
70
78
|
transition: 'opacity 0.15s ease-in-out',
|
|
71
79
|
'@media (hover: hover)': {
|
|
@@ -69,12 +69,12 @@ const meta = {
|
|
|
69
69
|
},
|
|
70
70
|
error: {
|
|
71
71
|
control: 'boolean',
|
|
72
|
-
description: 'Show the error
|
|
72
|
+
description: 'Show the error state (tinted media area + warning icon)',
|
|
73
73
|
table: { type: { summary: 'boolean' } },
|
|
74
74
|
},
|
|
75
75
|
errorText: {
|
|
76
76
|
control: 'text',
|
|
77
|
-
description: '
|
|
77
|
+
description: "Accessible label for the error state (the warning icon's aria-label/title)",
|
|
78
78
|
table: {
|
|
79
79
|
type: { summary: 'string' },
|
|
80
80
|
defaultValue: { summary: 'Something went wrong' },
|
|
@@ -303,7 +303,7 @@ export const ErrorState = {
|
|
|
303
303
|
parameters: {
|
|
304
304
|
docs: {
|
|
305
305
|
description: {
|
|
306
|
-
story: '
|
|
306
|
+
story: 'The media area is replaced by a tinted error surface with a warning icon; the name stays visible and the preview action is suppressed. The menu stays accessible so the item can still be retried or removed. Works across every view.',
|
|
307
307
|
},
|
|
308
308
|
},
|
|
309
309
|
},
|
|
@@ -29,9 +29,9 @@ export interface EntityCardProps {
|
|
|
29
29
|
processing?: boolean;
|
|
30
30
|
/** Processing overlay label. Defaults to "Please wait" */
|
|
31
31
|
processingText?: string;
|
|
32
|
-
/** Show the error
|
|
32
|
+
/** Show the error state (tinted media area + warning icon) in place of the image */
|
|
33
33
|
error?: boolean;
|
|
34
|
-
/**
|
|
34
|
+
/** Accessible label for the error state — exposed as the warning icon's aria-label/title. Defaults to "Something went wrong" */
|
|
35
35
|
errorText?: string;
|
|
36
36
|
/** Preview handler — renders the action button on hover */
|
|
37
37
|
onPreview?: () => void;
|
|
@@ -5,6 +5,8 @@ import { Typography, TypographySize, TypographyWeight } from '../Typography';
|
|
|
5
5
|
import { HintBubble, HintBubbleSize } from '../UI';
|
|
6
6
|
export const TextArea = ({ label, value, onChange, placeholder, hintText, errorText, disabled, allowMaxLength = false, maxLength = 256, showMaxLengthIndicator = false, numberOfRows = 5, style, hintBubbleProps, hintBubbleText, required, labelTrailingElement, labelContainerStyle, }) => {
|
|
7
7
|
const [focused, setFocused] = useState(false);
|
|
8
|
+
const showMaxLength = Boolean(showMaxLengthIndicator && allowMaxLength && maxLength);
|
|
9
|
+
const showFooter = Boolean(errorText || hintText) || showMaxLength;
|
|
8
10
|
return (_jsx(Box, { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', sx: style, children: _jsxs(Box, { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)', minWidth: '260px', children: [label && (_jsxs(Box, { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', sx: labelContainerStyle, children: [_jsx(Typography, { size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, required: required, children: label }), labelTrailingElement] })), hintBubbleText && (_jsx(HintBubble, { text: hintBubbleText, size: HintBubbleSize.sm, ...hintBubbleProps })), _jsx(Box, { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', bgcolor: 'var(--color-background-bg-component)', border: `1px solid ${errorText
|
|
9
11
|
? 'var(--color-border-border-error)'
|
|
10
12
|
: focused
|
|
@@ -19,5 +21,5 @@ export const TextArea = ({ label, value, onChange, placeholder, hintText, errorT
|
|
|
19
21
|
: 'var(--color-text-text-title)',
|
|
20
22
|
backgroundColor: 'transparent',
|
|
21
23
|
resize: 'none',
|
|
22
|
-
}, maxLength: allowMaxLength ? maxLength : undefined, onFocus: () => setFocused(true), onBlur: () => setFocused(false), required: required }) }), _jsxs(Box, { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', children: [errorText ? (_jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-error)', children: errorText })) : hintText ? (_jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-subtle)', children: hintText })) : (_jsx(Box, { style: { flex: 1 } })),
|
|
24
|
+
}, maxLength: allowMaxLength ? maxLength : undefined, onFocus: () => setFocused(true), onBlur: () => setFocused(false), required: required }) }), showFooter && (_jsxs(Box, { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', children: [errorText ? (_jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-error)', children: errorText })) : hintText ? (_jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-subtle)', children: hintText })) : (_jsx(Box, { style: { flex: 1 } })), showMaxLength && (_jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-subtle)', children: `${maxLength - value.length}` }))] }))] }) }));
|
|
23
25
|
};
|
|
@@ -26,7 +26,9 @@ export const TextInput = ({ label, value, onChange, placeholder, type = 'text',
|
|
|
26
26
|
: 'var(--color-foreground-fg-secondary)';
|
|
27
27
|
return (_jsx(Box, { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', width: fullWidth ? '100%' : undefined, minWidth: fullWidth ? 0 : undefined, sx: style, children: _jsxs(Box, { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)', width: fullWidth ? '100%' : undefined, minWidth: fullWidth ? 0 : '260px', children: [label && (_jsxs(Box, { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', sx: labelContainerStyle, children: [_jsx(Typography, { size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, required: required, children: label }), labelTrailingElement] })), hintBubbleText && (_jsx(HintBubble, { text: hintBubbleText, size: HintBubbleSize.sm, ...hintBubbleProps })), _jsxs(Box, { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: '40px', bgcolor: 'var(--color-background-bg-component)', border: errorText
|
|
28
28
|
? 'none'
|
|
29
|
-
: '1px solid var(--color-border-border-component)', overflow: 'hidden', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)', sx: { boxSizing: 'border-box' }, children: [_jsxs(Box, { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', padding: '0 var(--spacing-tokens-size-in-px-spacing-spacing-lg)', height: '100%', width: '100%', sx: { boxSizing: 'border-box' }, borderRadius:
|
|
29
|
+
: '1px solid var(--color-border-border-component)', overflow: 'hidden', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)', sx: { boxSizing: 'border-box' }, children: [_jsxs(Box, { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', padding: '0 var(--spacing-tokens-size-in-px-spacing-spacing-lg)', height: '100%', width: '100%', sx: { boxSizing: 'border-box' }, borderRadius: trailingText
|
|
30
|
+
? 'var(--spacing-tokens-size-in-px-spacing-spacing-xs) 0 0 var(--spacing-tokens-size-in-px-spacing-spacing-xs)'
|
|
31
|
+
: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)', border: `${errorText
|
|
30
32
|
? '1px solid var(--color-border-border-error)'
|
|
31
33
|
: focused
|
|
32
34
|
? '1px solid var(--color-border-border-component-active)'
|
|
@@ -11,7 +11,10 @@ import { getIconComponent } from '../../utils';
|
|
|
11
11
|
export const ButtonCard = ({ title, description, icon = 'UserFocus', iconColor = 'var(--color-foreground-fg-accent-yellow)', iconBackgroundColor = 'var(--color-background-bg-badge-accent-yellow)', onClick, showActionButton = false, actionButtonText = 'Contact Us', onActionClick, disabled = false, width = '294px', style, testId, }) => {
|
|
12
12
|
const cardKey = title?.toLowerCase()?.replace(/\s+/g, '-');
|
|
13
13
|
const isInteractive = !disabled && !!onClick;
|
|
14
|
-
return (_jsxs(Box, { width: width, display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl)', paddingY:
|
|
14
|
+
return (_jsxs(Box, { width: width, display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl)', paddingY: {
|
|
15
|
+
xs: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl)',
|
|
16
|
+
sm: 'var(--spacing-tokens-size-in-px-spacing-spacing-4xl)',
|
|
17
|
+
}, borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', bgcolor: 'var(--color-background-bg-component)', boxSizing: 'border-box', position: 'relative', onClick: isInteractive ? onClick : undefined, style: style, "data-testid": testId ?? `${cardKey}-button-card`, sx: {
|
|
15
18
|
opacity: disabled ? 0.5 : 1,
|
|
16
19
|
cursor: isInteractive ? 'pointer' : 'default',
|
|
17
20
|
pointerEvents: disabled ? 'none' : 'auto',
|
|
@@ -25,7 +25,7 @@ const IMAGE_COLUMN = [
|
|
|
25
25
|
BANNER_AVATARS.yuruo,
|
|
26
26
|
];
|
|
27
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.
|
|
28
|
+
const renderAction = (action, index) => (_jsx(MainButton, { text: action.text, size: ButtonSize.xxs, hierarchy: action.primary
|
|
29
29
|
? ButtonHierarchy.Primary
|
|
30
30
|
: ButtonHierarchy.SecondaryColor, onClick: action.onClick, testId: testId ? `${testId}-action-${index}` : undefined }, `${action.text}-${index}`));
|
|
31
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: {
|
|
@@ -11,7 +11,7 @@ const MOBILE = '@media (max-width: 640px)';
|
|
|
11
11
|
const PREVIEW_WIDTH = 296;
|
|
12
12
|
const PREVIEW_HEIGHT = 150;
|
|
13
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.
|
|
14
|
+
const renderAction = (action, index) => (_jsx(MainButton, { text: action.text, size: ButtonSize.xxs, hierarchy: action.primary
|
|
15
15
|
? ButtonHierarchy.Primary
|
|
16
16
|
: ButtonHierarchy.SecondaryColor, onClick: action.onClick, testId: testId ? `${testId}-action-${index}` : undefined }, `${action.text}-${index}`));
|
|
17
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: {
|
|
@@ -37,5 +37,5 @@ export const ConversationalVoiceAgentsBanner = ({ title, description, actions =
|
|
|
37
37
|
height: 'auto',
|
|
38
38
|
aspectRatio: `${PREVIEW_WIDTH} / ${PREVIEW_HEIGHT}`,
|
|
39
39
|
},
|
|
40
|
-
}, 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.
|
|
40
|
+
}, 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.xxs, 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.xxs, hierarchy: ButtonHierarchy.SecondaryTransparent, onClick: onPreviewCtaClick, style: { border: 'none' }, testId: testId ? `${testId}-preview-cta` : undefined }) }))] })] }));
|
|
41
41
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React, { useState } from 'react';
|
|
2
|
+
import React, { useRef, useState } from 'react';
|
|
3
3
|
import { Tooltip, ClickAwayListener } from '@mui/material';
|
|
4
4
|
import { PopoverVariant } from './types';
|
|
5
5
|
/** Composed from the semantic `shadow-sm` design token. */
|
|
@@ -21,6 +21,7 @@ const VARIANT_COLOR = {
|
|
|
21
21
|
*/
|
|
22
22
|
export const Popover = ({ content, children, variant = PopoverVariant.Dark, placement = 'top', trigger = 'hover', arrow = true, maxWidth = 280, padding = 'var(--spacing-tokens-size-in-px-spacing-spacing-lg)', open, onOpenChange, disableInteractive = false, className, }) => {
|
|
23
23
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
24
|
+
const popperRef = useRef(null);
|
|
24
25
|
const isControlled = open !== undefined;
|
|
25
26
|
const isOpen = isControlled ? open : internalOpen;
|
|
26
27
|
const setOpen = (next) => {
|
|
@@ -53,7 +54,17 @@ export const Popover = ({ content, children, variant = PopoverVariant.Dark, plac
|
|
|
53
54
|
setOpen(!isOpen);
|
|
54
55
|
},
|
|
55
56
|
});
|
|
56
|
-
return (_jsx(ClickAwayListener, { onClickAway: () =>
|
|
57
|
+
return (_jsx(ClickAwayListener, { onClickAway: (event) => {
|
|
58
|
+
// The panel renders in a portal, so it is not a descendant of the
|
|
59
|
+
// trigger that ClickAwayListener watches — without this check a tap
|
|
60
|
+
// inside the panel counts as "away". That matters on touch, where
|
|
61
|
+
// ClickAwayListener fires on touchend: the panel would unmount before
|
|
62
|
+
// the click reached the item, closing the menu without running its
|
|
63
|
+
// action. Mouse clicks are unaffected either way.
|
|
64
|
+
if (popperRef.current?.contains(event.target))
|
|
65
|
+
return;
|
|
66
|
+
setOpen(false);
|
|
67
|
+
}, children: _jsx(Tooltip, { open: isOpen, placement: placement, arrow: arrow, title: content, disableHoverListener: true, disableFocusListener: true, disableTouchListener: true, slotProps: { ...slotProps, popper: { ref: popperRef } }, children: triggerEl }) }));
|
|
57
68
|
}
|
|
58
69
|
const controlledProps = isControlled
|
|
59
70
|
? {
|
|
@@ -41,7 +41,7 @@ export const SelectAvatarCard = ({ state = SelectAvatarCardState.Filled, showHea
|
|
|
41
41
|
opacity: 0,
|
|
42
42
|
visibility: 'hidden',
|
|
43
43
|
transition: 'opacity 0.15s ease-in-out',
|
|
44
|
-
}, children: _jsx(MainButton, { text: previewButtonText, size: ButtonSize.
|
|
44
|
+
}, children: _jsx(MainButton, { text: previewButtonText, size: ButtonSize.xxs, hierarchy: ButtonHierarchy.SecondaryTransparent, showLeadingIcon: true, leadingIcon: "Play", onClick: onPreview, testId: testId ? `${testId}-preview` : undefined }) })), showChangeButton && (_jsx(MainButton, { text: changeButtonText, size: ButtonSize.xxs, 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
45
|
flex: '1 0 0',
|
|
46
46
|
minWidth: 0,
|
|
47
47
|
wordBreak: 'break-word',
|
|
@@ -49,7 +49,7 @@ export const SelectAvatarCard = ({ state = SelectAvatarCardState.Filled, showHea
|
|
|
49
49
|
top: '50%',
|
|
50
50
|
left: '50%',
|
|
51
51
|
transform: 'translate(-50%, -50%)',
|
|
52
|
-
}, children: _jsx(MainButton, { text: selectButtonText, hierarchy: ButtonHierarchy.PrimaryLemon, size: ButtonSize.
|
|
52
|
+
}, children: _jsx(MainButton, { text: selectButtonText, hierarchy: ButtonHierarchy.PrimaryLemon, size: ButtonSize.xxs, shape: ButtonShape.Square, onClick: onSelect, testId: testId ? `${testId}-select` : undefined }) })), isError && (_jsx(Box, { position: "absolute", display: "flex", alignItems: "center", justifyContent: "center", sx: {
|
|
53
53
|
top: '50%',
|
|
54
54
|
left: '50%',
|
|
55
55
|
transform: 'translate(-50%, -50%)',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Box } from '@mui/material';
|
|
4
4
|
import { Typography, TypographySize, TypographyWeight } from '../Typography';
|
|
5
5
|
import { TabMenuSize } from './types';
|
|
@@ -17,7 +17,7 @@ const SIZE_TOKENS = {
|
|
|
17
17
|
[TabMenuSize.sm]: {
|
|
18
18
|
containerGap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xxs)',
|
|
19
19
|
containerPadding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xxs)',
|
|
20
|
-
tabPaddingY: '
|
|
20
|
+
tabPaddingY: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)',
|
|
21
21
|
tabRadius: 'var(--spacing-tokens-size-in-px-radius-radius-sm)',
|
|
22
22
|
tabHeight: 'auto',
|
|
23
23
|
dividerHeight: '20px',
|
|
@@ -50,10 +50,38 @@ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.m
|
|
|
50
50
|
height: tab.offsetHeight,
|
|
51
51
|
});
|
|
52
52
|
};
|
|
53
|
-
|
|
53
|
+
// When the bar is scrolled (narrow viewports), bring the active tab into
|
|
54
|
+
// view rather than leaving it off-screen.
|
|
55
|
+
const revealActiveTab = () => {
|
|
56
|
+
const tab = tabRefs.current[activeTab];
|
|
57
|
+
if (!tab)
|
|
58
|
+
return;
|
|
59
|
+
// offsetLeft and scrollLeft share an origin — the container's padding box
|
|
60
|
+
// — so the visible window is simply [scrollLeft, scrollLeft + clientWidth].
|
|
61
|
+
const visibleStart = container.scrollLeft;
|
|
62
|
+
const visibleEnd = visibleStart + container.clientWidth;
|
|
63
|
+
// Reveal the container's padding alongside the tab, so an edge tab ends
|
|
64
|
+
// up inset from the border rather than flush against it. Overshooting is
|
|
65
|
+
// safe — the browser clamps scrollLeft to the scrollable range.
|
|
66
|
+
const { paddingLeft, paddingRight } = getComputedStyle(container);
|
|
67
|
+
const tabStart = tab.offsetLeft - (parseFloat(paddingLeft) || 0);
|
|
68
|
+
const tabEnd = tab.offsetLeft + tab.offsetWidth + (parseFloat(paddingRight) || 0);
|
|
69
|
+
const behavior = animated ? 'smooth' : 'auto';
|
|
70
|
+
if (tabStart < visibleStart) {
|
|
71
|
+
container.scrollTo({ left: tabStart, behavior });
|
|
72
|
+
}
|
|
73
|
+
else if (tabEnd > visibleEnd) {
|
|
74
|
+
container.scrollTo({ left: tabEnd - container.clientWidth, behavior });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const update = () => {
|
|
78
|
+
measure();
|
|
79
|
+
revealActiveTab();
|
|
80
|
+
};
|
|
81
|
+
update();
|
|
54
82
|
// Re-measure whenever the container or a tab changes size (e.g. responsive
|
|
55
83
|
// reflow) so the highlight tracks the active tab.
|
|
56
|
-
const resizeObserver = new ResizeObserver(
|
|
84
|
+
const resizeObserver = new ResizeObserver(update);
|
|
57
85
|
resizeObserver.observe(container);
|
|
58
86
|
tabRefs.current.forEach((tab) => tab && resizeObserver.observe(tab));
|
|
59
87
|
// Text metrics — and therefore tab widths — change when the fallback font is
|
|
@@ -61,14 +89,83 @@ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.m
|
|
|
61
89
|
let cancelled = false;
|
|
62
90
|
document.fonts?.ready.then(() => {
|
|
63
91
|
if (!cancelled)
|
|
64
|
-
|
|
92
|
+
update();
|
|
65
93
|
});
|
|
66
94
|
return () => {
|
|
67
95
|
cancelled = true;
|
|
68
96
|
resizeObserver.disconnect();
|
|
69
97
|
};
|
|
70
|
-
}, [activeTab, size, showDividers, tabs]);
|
|
71
|
-
|
|
98
|
+
}, [activeTab, animated, size, showDividers, tabs]);
|
|
99
|
+
// A mouse wheel only emits deltaY, which a horizontal-only scroller ignores —
|
|
100
|
+
// and with the scrollbar hidden that leaves mouse users unable to reach tabs
|
|
101
|
+
// that are off-screen. Translate vertical wheel into horizontal scroll, but
|
|
102
|
+
// only while the bar overflows and only while it can still move that way, so
|
|
103
|
+
// the page keeps scrolling normally at the ends.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const container = containerRef.current;
|
|
106
|
+
if (!container)
|
|
107
|
+
return;
|
|
108
|
+
const handleWheel = (event) => {
|
|
109
|
+
const maxScroll = container.scrollWidth - container.clientWidth;
|
|
110
|
+
if (maxScroll <= 0)
|
|
111
|
+
return;
|
|
112
|
+
// Leave genuine horizontal gestures (trackpad swipe) to the browser.
|
|
113
|
+
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX))
|
|
114
|
+
return;
|
|
115
|
+
const atStart = container.scrollLeft <= 0;
|
|
116
|
+
const atEnd = container.scrollLeft >= maxScroll - 1;
|
|
117
|
+
if ((event.deltaY < 0 && atStart) || (event.deltaY > 0 && atEnd))
|
|
118
|
+
return;
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
container.scrollLeft += event.deltaY;
|
|
121
|
+
};
|
|
122
|
+
// Not passive: we need preventDefault to stop the page scrolling with us.
|
|
123
|
+
container.addEventListener('wheel', handleWheel, { passive: false });
|
|
124
|
+
return () => container.removeEventListener('wheel', handleWheel);
|
|
125
|
+
}, []);
|
|
126
|
+
// Arrow keys move between tabs and select as they go (automatic activation),
|
|
127
|
+
// Home/End jump to the ends. Selection wraps at both edges. Focus is moved
|
|
128
|
+
// with preventScroll so it doesn't fight the smooth scroll in the effect.
|
|
129
|
+
const handleKeyDown = (event) => {
|
|
130
|
+
if (!onTabChange)
|
|
131
|
+
return;
|
|
132
|
+
const lastIndex = tabs.length - 1;
|
|
133
|
+
let nextIndex;
|
|
134
|
+
switch (event.key) {
|
|
135
|
+
case 'ArrowRight':
|
|
136
|
+
nextIndex = activeTab >= lastIndex ? 0 : activeTab + 1;
|
|
137
|
+
break;
|
|
138
|
+
case 'ArrowLeft':
|
|
139
|
+
nextIndex = activeTab <= 0 ? lastIndex : activeTab - 1;
|
|
140
|
+
break;
|
|
141
|
+
case 'Home':
|
|
142
|
+
nextIndex = 0;
|
|
143
|
+
break;
|
|
144
|
+
case 'End':
|
|
145
|
+
nextIndex = lastIndex;
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
onTabChange(nextIndex);
|
|
152
|
+
tabRefs.current[nextIndex]?.focus({ preventScroll: true });
|
|
153
|
+
};
|
|
154
|
+
return (_jsxs(Box, { ref: containerRef, "data-testid": testId, role: 'tablist', onKeyDown: handleKeyDown, position: 'relative', display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: tokens.containerGap, padding: tokens.containerPadding, bgcolor: 'var(--color-background-bg-navigate)', border: '1px solid var(--color-border-border-component)', borderRadius: 'var(--spacing-tokens-size-in-px-radius-radius-md)', sx: {
|
|
155
|
+
boxSizing: 'border-box',
|
|
156
|
+
// Tabs keep their natural width (they never shrink), so on narrow
|
|
157
|
+
// viewports the bar scrolls sideways instead of overflowing its parent.
|
|
158
|
+
maxWidth: '100%',
|
|
159
|
+
overflowX: 'auto',
|
|
160
|
+
// Don't let a horizontal swipe past the end trigger back-navigation.
|
|
161
|
+
overscrollBehaviorX: 'contain',
|
|
162
|
+
// This reads as a segmented control, not a scroll region, so the
|
|
163
|
+
// scrollbar chrome is hidden — touch, trackpad and the
|
|
164
|
+
// keep-the-active-tab-visible logic below still scroll it.
|
|
165
|
+
scrollbarWidth: 'none',
|
|
166
|
+
'&::-webkit-scrollbar': { display: 'none' },
|
|
167
|
+
...style,
|
|
168
|
+
}, children: [indicator && (_jsx(Box, { "aria-hidden": true, position: 'absolute', bgcolor: 'var(--color-background-bg-brand-transparent)', borderRadius: tokens.tabRadius, sx: {
|
|
72
169
|
left: 0,
|
|
73
170
|
top: 0,
|
|
74
171
|
width: indicator.width,
|
|
@@ -84,10 +181,18 @@ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.m
|
|
|
84
181
|
const isActive = index === activeTab;
|
|
85
182
|
return (_jsxs(React.Fragment, { children: [showDividers && index > 0 && (_jsx(Box, { width: '1px', height: tokens.dividerHeight, bgcolor: 'var(--color-border-border-divider)', flexShrink: 0, sx: { zIndex: 1 } })), _jsx(Box, { ref: (el) => {
|
|
86
183
|
tabRefs.current[index] = el;
|
|
87
|
-
}, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative', flexShrink: 0, height: tokens.tabHeight, paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-4xl)', paddingY: tokens.tabPaddingY, borderRadius: tokens.tabRadius,
|
|
184
|
+
}, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative', flexShrink: 0, height: tokens.tabHeight, paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-4xl)', paddingY: tokens.tabPaddingY, borderRadius: tokens.tabRadius, role: 'tab', "aria-selected": isActive,
|
|
185
|
+
// Roving tabindex: Tab reaches the active tab, arrow keys move
|
|
186
|
+
// within the list. Non-interactive menus stay out of the tab order.
|
|
187
|
+
tabIndex: onTabChange ? (isActive ? 0 : -1) : undefined, onClick: onTabChange ? () => onTabChange(index) : undefined, sx: {
|
|
88
188
|
boxSizing: 'border-box',
|
|
89
189
|
zIndex: 1,
|
|
90
190
|
cursor: onTabChange ? 'pointer' : 'default',
|
|
191
|
+
// Inset so the ring isn't clipped by the scroll container.
|
|
192
|
+
'&:focus-visible': {
|
|
193
|
+
outline: '2px solid var(--color-border-border-component-active)',
|
|
194
|
+
outlineOffset: '-2px',
|
|
195
|
+
},
|
|
91
196
|
'&:hover': onTabChange && !isActive
|
|
92
197
|
? {
|
|
93
198
|
bgcolor: 'var(--color-background-bg-brand-transparent-hover)',
|
|
@@ -97,7 +202,7 @@ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.m
|
|
|
97
202
|
...tabStyle,
|
|
98
203
|
}, children: _jsx(Typography, { size: tokens.textSize, weight: tokens.textWeight, color: isActive
|
|
99
204
|
? 'var(--color-text-text-clicable)'
|
|
100
|
-
: 'var(--color-text-text-
|
|
205
|
+
: 'var(--color-text-text-label)', textStyle: {
|
|
101
206
|
transition: animated ? 'color 0.25s ease' : 'none',
|
|
102
207
|
}, children: label }) })] }, index));
|
|
103
208
|
})] }));
|
|
@@ -7,3 +7,9 @@ export declare const Medium: Story;
|
|
|
7
7
|
export declare const Small: Story;
|
|
8
8
|
export declare const MediumStatic: Story;
|
|
9
9
|
export declare const SmallStatic: Story;
|
|
10
|
+
/**
|
|
11
|
+
* Five tabs need far more width than a 320px phone offers, so the bar scrolls
|
|
12
|
+
* sideways rather than overflowing its container. Selecting a tab that sits
|
|
13
|
+
* off-screen scrolls it back into view.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MobileScrolling: Story;
|
|
@@ -70,7 +70,13 @@ const meta = {
|
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
export default meta;
|
|
73
|
-
const TABS = [
|
|
73
|
+
const TABS = [
|
|
74
|
+
'Tab name 1',
|
|
75
|
+
'Tab name 2',
|
|
76
|
+
'Tab name 3',
|
|
77
|
+
'Tab name 4',
|
|
78
|
+
'Tab name 5',
|
|
79
|
+
];
|
|
74
80
|
const InteractiveTabMenu = (args) => {
|
|
75
81
|
const [activeTab, setActiveTab] = useState(args.activeTab ?? 0);
|
|
76
82
|
return _jsx(TabMenu, { ...args, activeTab: activeTab, onTabChange: setActiveTab });
|
|
@@ -108,3 +114,19 @@ export const SmallStatic = {
|
|
|
108
114
|
animated: false,
|
|
109
115
|
},
|
|
110
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* Five tabs need far more width than a 320px phone offers, so the bar scrolls
|
|
119
|
+
* sideways rather than overflowing its container. Selecting a tab that sits
|
|
120
|
+
* off-screen scrolls it back into view.
|
|
121
|
+
*/
|
|
122
|
+
export const MobileScrolling = {
|
|
123
|
+
render: (args) => _jsx(InteractiveTabMenu, { ...args }),
|
|
124
|
+
args: {
|
|
125
|
+
...Medium.args,
|
|
126
|
+
size: TabMenuSize.sm,
|
|
127
|
+
},
|
|
128
|
+
parameters: {
|
|
129
|
+
layout: 'padded',
|
|
130
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
131
|
+
},
|
|
132
|
+
};
|