@beydesign/storybook 0.2.17 → 0.2.19

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
  }
@@ -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
+ };
@@ -32,25 +32,43 @@ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.m
32
32
  const tabRefs = useRef([]);
33
33
  const [indicator, setIndicator] = useState(null);
34
34
  useLayoutEffect(() => {
35
+ const container = containerRef.current;
36
+ if (!container)
37
+ return;
35
38
  const measure = () => {
36
- const container = containerRef.current;
37
39
  const tab = tabRefs.current[activeTab];
38
- if (!container || !tab)
40
+ if (!tab)
39
41
  return;
40
- const containerRect = container.getBoundingClientRect();
41
- const tabRect = tab.getBoundingClientRect();
42
+ // Use offset* rather than getBoundingClientRect deltas: they are relative
43
+ // to the container's padding box, which is exactly the origin an absolutely
44
+ // positioned child (the indicator) is laid out from. Bounding-rect deltas
45
+ // would include the container's border and push the highlight 1px off.
42
46
  setIndicator({
43
- left: tabRect.left - containerRect.left,
44
- top: tabRect.top - containerRect.top,
45
- width: tabRect.width,
46
- height: tabRect.height,
47
+ left: tab.offsetLeft,
48
+ top: tab.offsetTop,
49
+ width: tab.offsetWidth,
50
+ height: tab.offsetHeight,
47
51
  });
48
52
  };
49
53
  measure();
50
- window.addEventListener('resize', measure);
51
- return () => window.removeEventListener('resize', measure);
54
+ // Re-measure whenever the container or a tab changes size (e.g. responsive
55
+ // reflow) so the highlight tracks the active tab.
56
+ const resizeObserver = new ResizeObserver(measure);
57
+ resizeObserver.observe(container);
58
+ tabRefs.current.forEach((tab) => tab && resizeObserver.observe(tab));
59
+ // Text metrics — and therefore tab widths — change when the fallback font is
60
+ // swapped for Figtree, so re-measure once web fonts have finished loading.
61
+ let cancelled = false;
62
+ document.fonts?.ready.then(() => {
63
+ if (!cancelled)
64
+ measure();
65
+ });
66
+ return () => {
67
+ cancelled = true;
68
+ resizeObserver.disconnect();
69
+ };
52
70
  }, [activeTab, size, showDividers, tabs]);
53
- return (_jsxs(Box, { ref: containerRef, "data-testid": testId, position: 'relative', display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: tokens.containerGap, padding: tokens.containerPadding, bgcolor: 'var(--color-background-bg-component)', border: '1px solid var(--color-border-border-component)', borderRadius: 'var(--spacing-tokens-size-in-px-radius-radius-md)', sx: style, children: [indicator && (_jsx(Box, { "aria-hidden": true, position: 'absolute', bgcolor: 'var(--color-background-bg-brand-transparent)', borderRadius: tokens.tabRadius, sx: {
71
+ return (_jsxs(Box, { ref: containerRef, "data-testid": testId, position: 'relative', display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: tokens.containerGap, padding: tokens.containerPadding, bgcolor: 'var(--color-background-bg-component)', border: '1px solid var(--color-border-border-component)', borderRadius: 'var(--spacing-tokens-size-in-px-radius-radius-md)', sx: { boxSizing: 'border-box', ...style }, children: [indicator && (_jsx(Box, { "aria-hidden": true, position: 'absolute', bgcolor: 'var(--color-background-bg-brand-transparent)', borderRadius: tokens.tabRadius, sx: {
54
72
  left: 0,
55
73
  top: 0,
56
74
  width: indicator.width,
@@ -67,6 +85,7 @@ export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.m
67
85
  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) => {
68
86
  tabRefs.current[index] = el;
69
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, onClick: onTabChange ? () => onTabChange(index) : undefined, sx: {
88
+ boxSizing: 'border-box',
70
89
  zIndex: 1,
71
90
  cursor: onTabChange ? 'pointer' : 'default',
72
91
  '&:hover': onTabChange && !isActive
@@ -10,6 +10,7 @@ export * from './Menu';
10
10
  export * from './Popover';
11
11
  export * from './ProgressIndicator';
12
12
  export * from './PromoBanner';
13
+ export * from './SelectAvatarCard';
13
14
  export * from './SkeletonLoader';
14
15
  export * from './Topbar';
15
16
  export * from './types';
@@ -10,6 +10,7 @@ export * from './Menu';
10
10
  export * from './Popover';
11
11
  export * from './ProgressIndicator';
12
12
  export * from './PromoBanner';
13
+ export * from './SelectAvatarCard';
13
14
  export * from './SkeletonLoader';
14
15
  export * from './Topbar';
15
16
  export * from './types';
@@ -500,6 +500,59 @@ export interface TabMenuProps {
500
500
  /** Test ID of the tab menu. */
501
501
  testId?: string;
502
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
+ }
503
556
  /**
504
557
  * Button card component props — a compact, clickable card with a leading icon,
505
558
  * title, description and an optional corner action button.
@@ -118,3 +118,16 @@ export var TabMenuSize;
118
118
  TabMenuSize["md"] = "md";
119
119
  TabMenuSize["sm"] = "sm";
120
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.17",
4
+ "version": "0.2.19",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",