@beydesign/storybook 0.4.4 → 0.4.6
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/Form/TextArea.js +3 -1
- package/dist/stories/Form/TextInput.js +3 -1
- package/dist/stories/Navigation/Drawer/Drawer.js +19 -1
- package/dist/stories/Navigation/Drawer/DrawerProfile.d.ts +4 -3
- package/dist/stories/Navigation/Drawer/DrawerProfile.js +57 -53
- package/dist/stories/Navigation/Drawer/types.d.ts +2 -2
- package/dist/stories/Navigation/internal/NavRow.js +7 -2
- 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
|
@@ -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)'
|
|
@@ -7,6 +7,9 @@ import { NavPanel } from '../internal';
|
|
|
7
7
|
import { getIconComponent } from '../../../utils';
|
|
8
8
|
const EXPANDED_WIDTH = '238px';
|
|
9
9
|
const COLLAPSED_WIDTH = '68px';
|
|
10
|
+
/** Mirrors the horizontal padding `NavPanel` applies around its content. */
|
|
11
|
+
const PANEL_PADDING_X = 'var(--spacing-tokens-size-in-px-spacing-spacing-xl, 16px)';
|
|
12
|
+
const contentWidth = (panelWidth) => `calc(${panelWidth} - 2 * ${PANEL_PADDING_X})`;
|
|
10
13
|
/** Composed from the semantic `shadow-sm` design token. */
|
|
11
14
|
const SHADOW_SM = 'var(--global-shadows-shadow-sm-offset-x) var(--global-shadows-shadow-sm-offset-y) var(--global-shadows-shadow-sm-blur) var(--global-shadows-shadow-sm-spread) var(--global-shadows-shadow-sm-color)';
|
|
12
15
|
/**
|
|
@@ -21,6 +24,21 @@ const Shell = styled.aside `
|
|
|
21
24
|
width: ${({ $collapsed }) => ($collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH)};
|
|
22
25
|
transition: width 0.3s ease-in-out;
|
|
23
26
|
`;
|
|
27
|
+
/**
|
|
28
|
+
* Holds the body at the width its state will settle at, rather than letting it
|
|
29
|
+
* fill the shell mid-animation. Without this the expanded layout reflows inside
|
|
30
|
+
* a shell that is still collapsed — labels squeeze to an ellipsis and the
|
|
31
|
+
* credits caption wraps down the card — before snapping into place. The panel
|
|
32
|
+
* clips the overhang (`overflow-x: hidden`) and reveals it as the width catches
|
|
33
|
+
* up.
|
|
34
|
+
*/
|
|
35
|
+
const Content = styled.div `
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
flex: 1;
|
|
39
|
+
min-height: 0;
|
|
40
|
+
width: ${({ $collapsed }) => contentWidth($collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH)};
|
|
41
|
+
`;
|
|
24
42
|
const ToggleButton = styled.button `
|
|
25
43
|
appearance: none;
|
|
26
44
|
position: absolute;
|
|
@@ -77,5 +95,5 @@ export const Drawer = ({ navItems, activeKey, collapsed, defaultCollapsed = fals
|
|
|
77
95
|
setInternalCollapsed(next);
|
|
78
96
|
onToggle?.(next);
|
|
79
97
|
};
|
|
80
|
-
return (_jsxs(Shell, { "$collapsed": isCollapsed, className: className, style: style, "data-testid": testId, children: [_jsx(NavPanel, { children: _jsx(DrawerBody, { navItems: navItems, activeKey: activeKey, size: drawerSize, credits: credits, creditsLoading: creditsLoading, profile: profile, profileLoading: profileLoading, logo: logo }) }), showToggle && (_jsx(ToggleButton, { type: "button", onClick: handleToggle, "aria-label": isCollapsed ? 'Expand sidebar' : 'Collapse sidebar', children: _jsx(ToggleIcon, { "$collapsed": isCollapsed, children: getIconComponent('CaretLeft', { size: 16, weight: 'bold' }) }) }))] }));
|
|
98
|
+
return (_jsxs(Shell, { "$collapsed": isCollapsed, className: className, style: style, "data-testid": testId, children: [_jsx(NavPanel, { children: _jsx(Content, { "$collapsed": isCollapsed, children: _jsx(DrawerBody, { navItems: navItems, activeKey: activeKey, size: drawerSize, credits: credits, creditsLoading: creditsLoading, profile: profile, profileLoading: profileLoading, logo: logo }) }) }), showToggle && (_jsx(ToggleButton, { type: "button", onClick: handleToggle, "aria-label": isCollapsed ? 'Expand sidebar' : 'Collapse sidebar', children: _jsx(ToggleIcon, { "$collapsed": isCollapsed, children: getIconComponent('CaretLeft', { size: 16, weight: 'bold' }) }) }))] }));
|
|
81
99
|
};
|
|
@@ -3,8 +3,9 @@ import { DrawerProfileProps } from './types';
|
|
|
3
3
|
/**
|
|
4
4
|
* User profile footer for the {@link Drawer}: avatar plus name and email.
|
|
5
5
|
* Collapses to just the avatar. When `menuItems` are supplied, clicking the
|
|
6
|
-
* profile
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* profile opens a popover menu anchored to the row (e.g. a "Log out" action)
|
|
7
|
+
* rather than expanding the drawer footer, so the drawer's layout never shifts.
|
|
8
|
+
* The menu always shows its labels — including when the drawer is collapsed and
|
|
9
|
+
* the popover opens to the side of the avatar.
|
|
9
10
|
*/
|
|
10
11
|
export declare const DrawerProfile: React.FC<DrawerProfileProps>;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import styled from '@emotion/styled';
|
|
4
4
|
import { DrawerSize } from './types';
|
|
5
|
-
import { Avatar, AvatarSize, AvatarStyle } from '../../UI';
|
|
5
|
+
import { Avatar, AvatarSize, AvatarStyle, Popover, PopoverVariant, } from '../../UI';
|
|
6
6
|
import { Typography, TypographySize, TypographyWeight } from '../../Typography';
|
|
7
7
|
import { getIconComponent } from '../../../utils';
|
|
8
8
|
const Container = styled.div `
|
|
@@ -54,31 +54,31 @@ const Chevron = styled.span `
|
|
|
54
54
|
transition: none;
|
|
55
55
|
}
|
|
56
56
|
`;
|
|
57
|
+
/** The popover's border sits outside this list, on the surface around it. */
|
|
58
|
+
const POPOVER_BORDER = 2;
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
60
|
+
* MUI puts a 2px margin on each side of the tooltip surface, which pushed the
|
|
61
|
+
* menu 2px right of the profile row. Drop the side margins so the two line up;
|
|
62
|
+
* the bottom margin is the gap to the row, so it stays.
|
|
61
63
|
*/
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@media (prefers-reduced-motion: reduce) {
|
|
68
|
-
transition: none;
|
|
64
|
+
const MenuPopover = styled(Popover) `
|
|
65
|
+
&& {
|
|
66
|
+
margin-left: 0;
|
|
67
|
+
margin-right: 0;
|
|
69
68
|
}
|
|
70
69
|
`;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Menu surface rendered inside the popover. Owns its own padding — the popover
|
|
72
|
+
* is handed `padding="0"` — so that pinning its width lines the menu up with
|
|
73
|
+
* the profile row exactly, with no surface padding left to widen it.
|
|
74
|
+
*/
|
|
75
75
|
const MenuList = styled.div `
|
|
76
76
|
display: flex;
|
|
77
77
|
flex-direction: column;
|
|
78
78
|
gap: var(--spacing-tokens-size-in-px-spacing-spacing-xxs, 2px);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
padding: var(--spacing-tokens-size-in-px-spacing-spacing-xs, 4px);
|
|
81
|
+
min-width: 200px;
|
|
82
82
|
`;
|
|
83
83
|
/** Menu rows reuse the nav-item look so they read as part of the drawer. */
|
|
84
84
|
const MenuItemButton = styled.button `
|
|
@@ -90,7 +90,7 @@ const MenuItemButton = styled.button `
|
|
|
90
90
|
gap: var(--spacing-tokens-size-in-px-spacing-spacing-md, 8px);
|
|
91
91
|
width: 100%;
|
|
92
92
|
box-sizing: border-box;
|
|
93
|
-
justify-content:
|
|
93
|
+
justify-content: flex-start;
|
|
94
94
|
padding: var(--spacing-tokens-size-in-px-spacing-spacing-md, 8px);
|
|
95
95
|
border-radius: var(--spacing-tokens-size-in-px-radius-radius-xs);
|
|
96
96
|
font: inherit;
|
|
@@ -113,58 +113,62 @@ const MenuItemButton = styled.button `
|
|
|
113
113
|
/**
|
|
114
114
|
* User profile footer for the {@link Drawer}: avatar plus name and email.
|
|
115
115
|
* Collapses to just the avatar. When `menuItems` are supplied, clicking the
|
|
116
|
-
* profile
|
|
117
|
-
*
|
|
118
|
-
*
|
|
116
|
+
* profile opens a popover menu anchored to the row (e.g. a "Log out" action)
|
|
117
|
+
* rather than expanding the drawer footer, so the drawer's layout never shifts.
|
|
118
|
+
* The menu always shows its labels — including when the drawer is collapsed and
|
|
119
|
+
* the popover opens to the side of the avatar.
|
|
119
120
|
*/
|
|
120
121
|
export const DrawerProfile = ({ size = DrawerSize.Expanded, name, email, avatarImage, avatarText, menuItems, onClick, className, style, testId, }) => {
|
|
121
122
|
const isCollapsed = size === DrawerSize.Collapsed;
|
|
122
123
|
const hasMenu = Boolean(menuItems && menuItems.length > 0);
|
|
123
124
|
const [open, setOpen] = useState(false);
|
|
124
125
|
const containerRef = useRef(null);
|
|
125
|
-
|
|
126
|
+
const [menuWidth, setMenuWidth] = useState();
|
|
127
|
+
// Match the menu to the profile row so the two line up. Only when expanded —
|
|
128
|
+
// collapsed, the row is just the avatar and the menu sizes to its content.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!open || isCollapsed)
|
|
131
|
+
return;
|
|
132
|
+
const width = containerRef.current?.offsetWidth;
|
|
133
|
+
if (width)
|
|
134
|
+
setMenuWidth(width - POPOVER_BORDER);
|
|
135
|
+
}, [open, isCollapsed]);
|
|
136
|
+
// Outside clicks are handled by the Popover itself; Escape is not.
|
|
126
137
|
useEffect(() => {
|
|
127
138
|
if (!open)
|
|
128
139
|
return;
|
|
129
|
-
const handlePointer = (event) => {
|
|
130
|
-
if (!containerRef.current?.contains(event.target)) {
|
|
131
|
-
setOpen(false);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
140
|
const handleKey = (event) => {
|
|
135
141
|
if (event.key === 'Escape')
|
|
136
142
|
setOpen(false);
|
|
137
143
|
};
|
|
138
|
-
document.addEventListener('mousedown', handlePointer);
|
|
139
144
|
document.addEventListener('keydown', handleKey);
|
|
140
|
-
return () =>
|
|
141
|
-
document.removeEventListener('mousedown', handlePointer);
|
|
142
|
-
document.removeEventListener('keydown', handleKey);
|
|
143
|
-
};
|
|
145
|
+
return () => document.removeEventListener('keydown', handleKey);
|
|
144
146
|
}, [open]);
|
|
145
147
|
const clickable = hasMenu || Boolean(onClick);
|
|
146
|
-
const handleRowClick = () => {
|
|
147
|
-
if (hasMenu) {
|
|
148
|
-
setOpen((prev) => !prev);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
onClick?.();
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
148
|
const handleItemClick = (item) => {
|
|
155
149
|
setOpen(false);
|
|
156
150
|
item.onClick?.();
|
|
157
151
|
};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
152
|
+
const row = (_jsxs(Row, { "$collapsed": isCollapsed, "$clickable": clickable, "$active": hasMenu && open,
|
|
153
|
+
// With a menu the Popover owns the click; otherwise fire the row handler.
|
|
154
|
+
onClick: hasMenu ? undefined : onClick, role: clickable ? 'button' : undefined, tabIndex: clickable ? 0 : undefined, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? open : undefined, onKeyDown: clickable
|
|
155
|
+
? (event) => {
|
|
156
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
if (hasMenu)
|
|
159
|
+
setOpen((prev) => !prev);
|
|
160
|
+
else
|
|
161
|
+
onClick?.();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
: undefined, children: [_jsx(Avatar, { size: AvatarSize.sm, border: true, avatarStyle: avatarImage ? AvatarStyle.Picture : AvatarStyle.Letters, image: avatarImage, text: avatarText ?? name }), !isCollapsed && (_jsxs(_Fragment, { children: [_jsxs(TextColumn, { children: [_jsx(TextLine, { children: _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.SemiBold, color: "var(--color-text-text-title)", children: name }) }), email && (_jsx(TextLine, { children: _jsx(Typography, { size: TypographySize.TextXS, weight: TypographyWeight.Regular, color: "var(--color-text-text-paragraph)", children: email }) }))] }), hasMenu && (_jsx(Chevron, { "$open": open, children: getIconComponent('CaretDown', { size: 16, weight: 'bold' }) }))] }))] }));
|
|
165
|
+
const menu = (_jsx(MenuList, { role: "menu", style: isCollapsed ? undefined : { width: menuWidth, minWidth: 0 }, children: menuItems?.map((item, index) => (_jsxs(MenuItemButton, { type: "button", role: "menuitem", "$destructive": Boolean(item.destructive), onClick: () => handleItemClick(item), children: [item.icon &&
|
|
166
|
+
getIconComponent(item.icon, {
|
|
167
|
+
size: 20,
|
|
168
|
+
color: 'currentColor',
|
|
169
|
+
}), _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: "inherit", children: item.label })] }, index))) }));
|
|
170
|
+
return (_jsx(Container, { ref: containerRef, className: className, style: style, "data-testid": testId, children: hasMenu ? (_jsx(MenuPopover, { content: menu, variant: PopoverVariant.Light,
|
|
171
|
+
// Anchored above the row so the menu never covers the profile; to the
|
|
172
|
+
// side when collapsed, where there is no room above the avatar.
|
|
173
|
+
placement: isCollapsed ? 'right-end' : 'top-start', trigger: "click", arrow: false, maxWidth: isCollapsed ? 260 : menuWidth, padding: "0", open: open, onOpenChange: setOpen, children: row })) : (row) }));
|
|
170
174
|
};
|
|
@@ -100,8 +100,8 @@ export interface DrawerProfileProps {
|
|
|
100
100
|
/** Overrides the initials source (defaults to `name`). */
|
|
101
101
|
avatarText?: string;
|
|
102
102
|
/**
|
|
103
|
-
* When provided, clicking the profile
|
|
104
|
-
* (e.g. a "Log out" action)
|
|
103
|
+
* When provided, clicking the profile opens a popover menu with these items
|
|
104
|
+
* (e.g. a "Log out" action) anchored to the profile row instead of firing
|
|
105
105
|
* `onClick`.
|
|
106
106
|
*/
|
|
107
107
|
menuItems?: DrawerProfileMenuItem[];
|
|
@@ -37,10 +37,15 @@ const StyledNavRow = styled.button `
|
|
|
37
37
|
background-color: ${({ $state }) => background($state)};
|
|
38
38
|
opacity: ${({ $state }) => ($state === NavItemState.Disabled ? 0.5 : 1)};
|
|
39
39
|
cursor: ${({ $state }) => $state === NavItemState.Disabled ? 'not-allowed' : 'pointer'};
|
|
40
|
+
/*
|
|
41
|
+
* Padding deliberately does not transition. The icon lands on the same x in
|
|
42
|
+
* both states, so animating it gained nothing visually — but since the row is
|
|
43
|
+
* auto height once expanded, growing the padding from 0 pumped every row from
|
|
44
|
+
* the bare icon height back up to full, shifting the list as it went.
|
|
45
|
+
*/
|
|
40
46
|
transition:
|
|
41
47
|
background-color 0.2s ease-in-out,
|
|
42
|
-
color 0.2s ease-in-out
|
|
43
|
-
padding 0.3s ease-in-out;
|
|
48
|
+
color 0.2s ease-in-out;
|
|
44
49
|
|
|
45
50
|
&:hover {
|
|
46
51
|
${({ $state }) => $state === NavItemState.Inactive
|
|
@@ -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
|
+
};
|