@beydesign/storybook 0.3.1 → 0.4.1

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/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export * from './stories/UI';
2
2
  export * from './stories/Agent';
3
3
  export * from './stories/EntityCard';
4
- export * from './stories/VideoCard';
5
4
  export * from './stories/PlanCard';
6
5
  export * from './stories/Form';
7
6
  export * from './stories/Buttons';
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export * from './stories/UI';
2
2
  export * from './stories/Agent';
3
3
  export * from './stories/EntityCard';
4
- export * from './stories/VideoCard';
5
4
  export * from './stories/PlanCard';
6
5
  export * from './stories/Form';
7
6
  export * from './stories/Buttons';
@@ -1,29 +1,65 @@
1
1
  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
+ import { keyframes } from '@emotion/react';
5
+ import { UserCircle, FilmSlate, ArrowsClockwise, Warning, } from '@phosphor-icons/react';
4
6
  import { Typography, TypographySize, TypographyWeight } from '../Typography';
5
7
  import { Menu, Badge, BadgeColor, BadgeSize } from '../UI';
6
8
  import { MainButton, ButtonHierarchy, ButtonSize, ButtonDisplayMode, } from '../Buttons';
7
- import { UserCircle } from '@phosphor-icons/react';
8
- export const EntityCard = ({ view = 'avatar', name, image, description = '', showDescription = false, onPreview, previewButtonText, previewIcon = 'Play', showStatusBadge = false, statusBadgeText = '', statusBadgeColor = BadgeColor.Green, menuItems = [], showMenu = false, width, }) => {
9
+ /** Default card width per view variant. */
10
+ const WIDTH_BY_VIEW = {
11
+ avatar: '208px',
12
+ agent: '280px',
13
+ video: '340px',
14
+ };
15
+ /** Default image aspect ratio per view variant. */
16
+ const ASPECT_RATIO_BY_VIEW = {
17
+ avatar: '203 / 264',
18
+ agent: '1 / 1',
19
+ video: '16 / 9',
20
+ };
21
+ const spin = keyframes `
22
+ from { transform: rotate(0deg); }
23
+ to { transform: rotate(360deg); }
24
+ `;
25
+ /** A continuously rotating refresh icon, matching the AgentCard processing spinner. */
26
+ const Spinner = () => (_jsx(Box, { display: 'flex', sx: { animation: `${spin} 1.5s linear infinite` }, children: _jsx(ArrowsClockwise, { size: 24, color: 'var(--color-text-text-white)' }) }));
27
+ export const EntityCard = ({ view = 'avatar', name, image, description = '', showDescription = false, date, aspectRatio, processing = false, processingText = 'Please wait', error = false, errorText = 'Something went wrong', onPreview, previewButtonText, previewIcon = 'Play', showStatusBadge = false, statusBadgeText = '', statusBadgeColor = BadgeColor.Green, menuItems = [], showMenu = false, width, testId, }) => {
9
28
  const [hovered, setHovered] = useState(false);
10
29
  const [imageError, setImageError] = useState(false);
11
- const isAgent = view === 'agent';
12
- const cardWidth = width ?? (isAgent ? '280px' : '208px');
13
- const imageAspectRatio = isAgent ? '1 / 1' : '203 / 264';
30
+ const isVideo = view === 'video';
31
+ const cardWidth = width ?? WIDTH_BY_VIEW[view];
32
+ const imageAspectRatio = aspectRatio ?? ASPECT_RATIO_BY_VIEW[view];
14
33
  const entityKey = name?.toLowerCase()?.replace(/\s+/g, '-');
15
34
  const hasMenu = showMenu && menuItems.length > 0;
16
- const showActions = hovered && (!!onPreview || hasMenu);
35
+ const isProcessing = processing && !error;
36
+ const canPreview = !!onPreview && !isProcessing && !error;
37
+ const showActions = hovered && (canPreview || hasMenu);
17
38
  const showBadge = showStatusBadge && !!statusBadgeText;
39
+ const showImage = !!image && !imageError;
40
+ const resolvedTestId = testId ?? `${entityKey}-${isVideo ? 'video' : 'entity'}-card`;
18
41
  return (_jsxs(Box, { width: cardWidth, display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', bgcolor: hovered
19
42
  ? 'var(--color-background-bg-card-highlight-hover)'
20
- : 'var(--color-background-bg-card-highlight)', border: '1px solid var(--color-border-border-subtle)', boxSizing: 'border-box', overflow: 'hidden', onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), sx: { transition: 'background-color 0.15s ease-in-out' }, "data-testid": `${entityKey}-entity-card`, children: [_jsxs(Box, { position: 'relative', width: '100%', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', overflow: 'hidden', sx: { aspectRatio: imageAspectRatio }, children: [image && !imageError ? (_jsx("img", { src: image, alt: name, width: '100%', height: '100%', style: { objectFit: 'cover', height: '100%', width: '100%' }, onError: () => setImageError(true) })) : (_jsx(Box, { width: '100%', height: '100%', bgcolor: 'var(--color-background-bg-element)', display: 'flex', justifyContent: 'center', alignItems: 'center', children: _jsx(UserCircle, { size: 74, weight: 'thin', color: 'var(--color-text-text-subtle)' }) })), _jsx(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', sx: {
43
+ : 'var(--color-background-bg-card-highlight)', border: '1px solid var(--color-border-border-subtle)', boxSizing: 'border-box', overflow: 'hidden', onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), sx: { transition: 'background-color 0.15s ease-in-out' }, "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: [showImage ? (_jsx("img", { src: image, alt: name, width: '100%', height: '100%', style: { objectFit: 'cover', height: '100%', width: '100%' }, onError: () => setImageError(true) })) : (_jsx(Box, { width: '100%', height: '100%', bgcolor: 'var(--color-background-bg-element)', display: 'flex', justifyContent: 'center', alignItems: 'center', children: isVideo ? (_jsx(FilmSlate, { size: 64, weight: 'thin', color: 'var(--color-text-text-subtle)' })) : (_jsx(UserCircle, { size: 74, weight: 'thin', color: 'var(--color-text-text-subtle)' })) })), _jsx(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', sx: {
21
44
  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%)',
22
- opacity: hovered ? 1 : 0,
45
+ opacity: hovered && !isProcessing && !error ? 1 : 0,
23
46
  transition: 'opacity 0.15s ease-in-out',
24
47
  pointerEvents: 'none',
25
- } }), _jsx(Box, { position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg) var(--spacing-tokens-size-in-px-spacing-spacing-lg)', sx: {
48
+ } }), 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 })] })), error && (_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-error-solid)', sx: { zIndex: 3, opacity: 0.7 }, children: [_jsx(Warning, { size: 74, color: 'var(--color-text-text-white)' }), _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', children: errorText })] })), 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)', iconColor: 'var(--color-foreground-fg-accent-success)' }) })), showActions && (_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', flexDirection: 'row', alignItems: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', sx: { zIndex: 4 }, children: [canPreview &&
49
+ (previewButtonText ? (_jsx(MainButton, { text: previewButtonText, showLeadingIcon: true, leadingIcon: previewIcon, iconWeight: 'fill', hierarchy: ButtonHierarchy.SecondaryTransparent, size: ButtonSize.sm, onClick: onPreview, testId: `${entityKey}-preview-button` })) : (_jsx(MainButton, { displayIcon: previewIcon, displayIconMode: ButtonDisplayMode.Only, hierarchy: ButtonHierarchy.SecondaryTransparent, size: ButtonSize.sm, onClick: onPreview, testId: `${entityKey}-preview-button` }))), hasMenu && (_jsx(Menu, { items: menuItems, testKey: entityKey, size: ButtonSize.sm }))] })), isVideo ? (_jsxs(Box, { position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg) var(--spacing-tokens-size-in-px-spacing-spacing-lg)', sx: {
50
+ zIndex: 2,
51
+ pointerEvents: 'none',
52
+ backgroundImage: 'linear-gradient(rgba(45, 38, 55, 0) 0%, rgba(45, 38, 55, 0.16) 34%, rgba(45, 38, 55, 0.64) 100%)',
53
+ }, children: [_jsx(Box, { minWidth: 0, flex: 1, children: _jsx(Typography, { text: name, title: name, size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', textStyle: {
54
+ display: 'block',
55
+ overflow: 'hidden',
56
+ textOverflow: 'ellipsis',
57
+ whiteSpace: 'nowrap',
58
+ } }) }), date && (_jsx(Typography, { text: date, size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-white)', textStyle: {
59
+ whiteSpace: 'nowrap',
60
+ flexShrink: 0,
61
+ opacity: 0.8,
62
+ } }))] })) : (_jsx(Box, { position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg) var(--spacing-tokens-size-in-px-spacing-spacing-lg)', sx: {
26
63
  backgroundImage: 'linear-gradient(rgba(45, 38, 55, 0) 0%, rgba(45, 38, 55, 0.16) 34%, rgba(45, 38, 55, 0.64) 100%)',
27
- }, children: _jsx(Typography, { size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', textStyle: { wordBreak: 'break-word' }, children: name }) }), 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)', iconColor: 'var(--color-foreground-fg-accent-success)' }) })), showActions && (_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', flexDirection: 'row', alignItems: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', sx: { zIndex: 2 }, children: [onPreview &&
28
- (previewButtonText ? (_jsx(MainButton, { text: previewButtonText, showLeadingIcon: true, leadingIcon: previewIcon, iconWeight: 'fill', hierarchy: ButtonHierarchy.SecondaryTransparent, size: ButtonSize.sm, onClick: onPreview, testId: `${entityKey}-preview-button` })) : (_jsx(MainButton, { displayIcon: previewIcon, displayIconMode: ButtonDisplayMode.Only, hierarchy: ButtonHierarchy.SecondaryTransparent, size: ButtonSize.sm, onClick: onPreview, testId: `${entityKey}-preview-button` }))), hasMenu && (_jsx(Menu, { items: menuItems, testKey: entityKey, size: ButtonSize.sm }))] }))] }), showDescription && description && (_jsx(Box, { display: 'flex', alignItems: 'center', padding: '0 var(--spacing-tokens-size-in-px-spacing-spacing-lg)', width: '100%', boxSizing: 'border-box', children: _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-label)', textStyle: { wordBreak: 'break-word' }, children: description }) }))] }));
64
+ }, children: _jsx(Typography, { size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', textStyle: { wordBreak: 'break-word' }, children: name }) }))] }), showDescription && description && (_jsx(Box, { display: 'flex', alignItems: 'center', padding: '0 var(--spacing-tokens-size-in-px-spacing-spacing-lg)', width: '100%', boxSizing: 'border-box', children: _jsx(Typography, { size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-label)', textStyle: { wordBreak: 'break-word' }, children: description }) }))] }));
29
65
  };
@@ -8,3 +8,9 @@ export declare const AgentView: Story;
8
8
  export declare const WithDescription: Story;
9
9
  export declare const NoActions: Story;
10
10
  export declare const NoImage: Story;
11
+ export declare const VideoView: Story;
12
+ export declare const VideoProcessing: Story;
13
+ export declare const VideoNoImage: Story;
14
+ export declare const ErrorState: Story;
15
+ export declare const VideoError: Story;
16
+ export declare const Processing: Story;
@@ -14,10 +14,10 @@ const meta = {
14
14
  argTypes: {
15
15
  view: {
16
16
  control: 'select',
17
- options: ['avatar', 'agent'],
18
- description: 'View variant — drives default width and image aspect ratio',
17
+ options: ['avatar', 'agent', 'video'],
18
+ description: 'View variant — drives default width, image aspect ratio and fallback icon',
19
19
  table: {
20
- type: { summary: "'avatar' | 'agent'" },
20
+ type: { summary: "'avatar' | 'agent' | 'video'" },
21
21
  defaultValue: { summary: 'avatar' },
22
22
  },
23
23
  },
@@ -41,6 +41,45 @@ const meta = {
41
41
  description: 'Whether to show the description',
42
42
  table: { type: { summary: 'boolean' } },
43
43
  },
44
+ date: {
45
+ control: 'text',
46
+ description: 'Secondary text shown to the right of the name (video view, e.g. created date)',
47
+ table: { type: { summary: 'string' } },
48
+ },
49
+ aspectRatio: {
50
+ control: 'text',
51
+ description: 'Aspect ratio of the image — overrides the per-view default',
52
+ table: {
53
+ type: { summary: 'string' },
54
+ defaultValue: { summary: '203 / 264 · 1 / 1 · 16 / 9' },
55
+ },
56
+ },
57
+ processing: {
58
+ control: 'boolean',
59
+ description: 'Show the processing overlay (spinner + label)',
60
+ table: { type: { summary: 'boolean' } },
61
+ },
62
+ processingText: {
63
+ control: 'text',
64
+ description: 'Processing overlay label',
65
+ table: {
66
+ type: { summary: 'string' },
67
+ defaultValue: { summary: 'Please wait' },
68
+ },
69
+ },
70
+ error: {
71
+ control: 'boolean',
72
+ description: 'Show the error overlay (warning icon + label)',
73
+ table: { type: { summary: 'boolean' } },
74
+ },
75
+ errorText: {
76
+ control: 'text',
77
+ description: 'Error overlay label',
78
+ table: {
79
+ type: { summary: 'string' },
80
+ defaultValue: { summary: 'Something went wrong' },
81
+ },
82
+ },
44
83
  showMenu: {
45
84
  control: 'boolean',
46
85
  description: 'Whether to show the menu on hover',
@@ -91,7 +130,7 @@ const meta = {
91
130
  description: 'Width of the card',
92
131
  table: {
93
132
  type: { summary: 'string | number' },
94
- defaultValue: { summary: '208px' },
133
+ defaultValue: { summary: '208px · 280px · 340px' },
95
134
  },
96
135
  },
97
136
  },
@@ -185,3 +224,119 @@ export const NoImage = {
185
224
  },
186
225
  },
187
226
  };
227
+ const videoThumbnail = 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?fm=jpg&q=60&w=800&ixlib=rb-4.0.3';
228
+ const videoMenuItems = [
229
+ {
230
+ label: 'Download Video',
231
+ icon: 'DownloadSimple',
232
+ onClick: () => { },
233
+ },
234
+ { label: 'Copy Video ID', icon: 'Copy', onClick: () => { } },
235
+ {
236
+ label: 'Delete Video',
237
+ icon: 'Trash',
238
+ onClick: () => { },
239
+ destructive: true,
240
+ },
241
+ ];
242
+ export const VideoView = {
243
+ args: {
244
+ view: 'video',
245
+ name: 'Product launch announcement',
246
+ image: videoThumbnail,
247
+ date: 'Jul 6, 2026',
248
+ onPreview: () => { },
249
+ previewButtonText: 'Preview',
250
+ showMenu: true,
251
+ menuItems: videoMenuItems,
252
+ },
253
+ parameters: {
254
+ docs: {
255
+ description: {
256
+ story: 'The video view — a landscape (16:9) media tile with the name and date overlaid on the bottom gradient. Hover to reveal the preview + menu actions.',
257
+ },
258
+ },
259
+ },
260
+ };
261
+ export const VideoProcessing = {
262
+ args: {
263
+ view: 'video',
264
+ name: 'Onboarding walkthrough',
265
+ image: videoThumbnail,
266
+ date: 'Jul 6, 2026',
267
+ processing: true,
268
+ showMenu: true,
269
+ menuItems: [videoMenuItems[2]],
270
+ },
271
+ parameters: {
272
+ docs: {
273
+ description: {
274
+ story: 'While a video renders, a spinning indicator overlays the thumbnail and the preview action is disabled. The menu stays available.',
275
+ },
276
+ },
277
+ },
278
+ };
279
+ export const VideoNoImage = {
280
+ args: {
281
+ view: 'video',
282
+ name: 'Product launch announcement',
283
+ date: 'Jul 6, 2026',
284
+ onPreview: () => { },
285
+ previewButtonText: 'Preview',
286
+ showMenu: true,
287
+ menuItems: videoMenuItems,
288
+ },
289
+ parameters: {
290
+ docs: {
291
+ description: {
292
+ story: 'Video view fallback placeholder (FilmSlate) shown when no thumbnail is provided or it fails to load.',
293
+ },
294
+ },
295
+ },
296
+ };
297
+ export const ErrorState = {
298
+ args: {
299
+ ...Default.args,
300
+ name: 'Nelly',
301
+ error: true,
302
+ },
303
+ parameters: {
304
+ docs: {
305
+ description: {
306
+ story: 'An error overlay (warning icon + label) covers the image while the preview action is suppressed. The menu stays accessible so the item can still be retried or removed. Works across every view.',
307
+ },
308
+ },
309
+ },
310
+ };
311
+ export const VideoError = {
312
+ args: {
313
+ view: 'video',
314
+ name: 'Product launch announcement',
315
+ image: videoThumbnail,
316
+ date: 'Jul 6, 2026',
317
+ error: true,
318
+ showMenu: true,
319
+ menuItems: videoMenuItems,
320
+ },
321
+ parameters: {
322
+ docs: {
323
+ description: {
324
+ story: 'Video view in an error state — e.g. a render that failed.',
325
+ },
326
+ },
327
+ },
328
+ };
329
+ export const Processing = {
330
+ args: {
331
+ ...Default.args,
332
+ name: 'Nelly',
333
+ processing: true,
334
+ },
335
+ parameters: {
336
+ docs: {
337
+ description: {
338
+ story: 'A processing overlay (spinner + label) covers the image while the preview action is suppressed. The menu stays accessible. Works across every view.',
339
+ },
340
+ },
341
+ },
342
+ };
@@ -1,9 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from '@mui/material';
3
3
  import { SkeletonLoader } from '../UI';
4
- export const EntityCardSkeleton = ({ view = 'avatar', width, showDescription = false, style, }) => {
5
- const isAgent = view === 'agent';
6
- const cardWidth = width ?? (isAgent ? '280px' : '208px');
7
- const imageAspectRatio = isAgent ? '1 / 1' : '203 / 264';
4
+ /** Default card width per view variant. */
5
+ const WIDTH_BY_VIEW = {
6
+ avatar: '208px',
7
+ agent: '280px',
8
+ video: '340px',
9
+ };
10
+ /** Default image aspect ratio per view variant. */
11
+ const ASPECT_RATIO_BY_VIEW = {
12
+ avatar: '203 / 264',
13
+ agent: '1 / 1',
14
+ video: '16 / 9',
15
+ };
16
+ export const EntityCardSkeleton = ({ view = 'avatar', aspectRatio, width, showDescription = false, style, }) => {
17
+ const cardWidth = width ?? WIDTH_BY_VIEW[view];
18
+ const imageAspectRatio = aspectRatio ?? ASPECT_RATIO_BY_VIEW[view];
8
19
  return (_jsxs(Box, { width: cardWidth, display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', bgcolor: 'var(--color-background-bg-card-highlight)', border: '1px solid var(--color-border-border-subtle)', boxSizing: 'border-box', overflow: 'hidden', style: style, children: [_jsx(Box, { width: '100%', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', overflow: 'hidden', sx: { aspectRatio: imageAspectRatio }, children: _jsx(SkeletonLoader, {}) }), showDescription && (_jsxs(Box, { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', padding: '0 var(--spacing-tokens-size-in-px-spacing-spacing-lg)', width: '100%', boxSizing: 'border-box', children: [_jsx(SkeletonLoader, { width: '100%', height: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)' }), _jsx(SkeletonLoader, { width: '70%', height: 'var(--spacing-tokens-size-in-px-spacing-spacing-xl)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)' })] }))] }));
9
20
  };
@@ -6,5 +6,6 @@ type Story = StoryObj<typeof EntityCardSkeleton>;
6
6
  export declare const Avatar: Story;
7
7
  export declare const AvatarWithDescription: Story;
8
8
  export declare const Agent: Story;
9
+ export declare const Video: Story;
9
10
  export declare const Grid: Story;
10
11
  export declare const LoadingState: Story;
@@ -11,19 +11,26 @@ const meta = {
11
11
  argTypes: {
12
12
  view: {
13
13
  control: 'select',
14
- options: ['avatar', 'agent'],
14
+ options: ['avatar', 'agent', 'video'],
15
15
  description: 'View variant — drives default width and image aspect ratio',
16
16
  table: {
17
- type: { summary: "'avatar' | 'agent'" },
17
+ type: { summary: "'avatar' | 'agent' | 'video'" },
18
18
  defaultValue: { summary: 'avatar' },
19
19
  },
20
20
  },
21
+ aspectRatio: {
22
+ control: 'text',
23
+ description: 'Aspect ratio of the placeholder — overrides the per-view default',
24
+ table: { type: { summary: 'string' } },
25
+ },
21
26
  width: {
22
27
  control: 'text',
23
28
  description: 'Width of the card (overrides the view default)',
24
29
  table: {
25
30
  type: { summary: 'string | number' },
26
- defaultValue: { summary: '208px (avatar) / 280px (agent)' },
31
+ defaultValue: {
32
+ summary: '208px (avatar) / 280px (agent) / 340px (video)',
33
+ },
27
34
  },
28
35
  },
29
36
  showDescription: {
@@ -61,8 +68,13 @@ export const Agent = {
61
68
  view: 'agent',
62
69
  },
63
70
  };
71
+ export const Video = {
72
+ args: {
73
+ view: 'video',
74
+ },
75
+ };
64
76
  export const Grid = {
65
- render: () => (_jsxs(Box, { display: "flex", flexWrap: "wrap", gap: 2, children: [_jsx(EntityCardSkeleton, { view: "avatar" }), _jsx(EntityCardSkeleton, { view: "avatar", showDescription: true }), _jsx(EntityCardSkeleton, { view: "agent" })] })),
77
+ render: () => (_jsxs(Box, { display: "flex", flexWrap: "wrap", gap: 2, children: [_jsx(EntityCardSkeleton, { view: "avatar" }), _jsx(EntityCardSkeleton, { view: "avatar", showDescription: true }), _jsx(EntityCardSkeleton, { view: "agent" }), _jsx(EntityCardSkeleton, { view: "video" })] })),
66
78
  };
67
79
  export const LoadingState = {
68
80
  render: () => (_jsxs(Box, { display: "flex", flexDirection: "column", gap: 3, children: [_jsxs(Box, { children: [_jsx("h3", { children: "Loading state example" }), _jsx("p", { children: "How a row of entity cards appears while data is loading." })] }), _jsx(Box, { display: "flex", flexWrap: "wrap", gap: 2, children: Array(5)
@@ -4,13 +4,14 @@ import { IconName } from '../../utils';
4
4
  * Entity card view variants.
5
5
  * - `avatar`: portrait avatar tile (icon-only preview action)
6
6
  * - `agent`: wider, square agent tile (labelled action + status badge)
7
+ * - `video`: landscape media tile (name + date footer, optional processing overlay)
7
8
  */
8
- export type EntityCardView = 'avatar' | 'agent';
9
+ export type EntityCardView = 'avatar' | 'agent' | 'video';
9
10
  /**
10
11
  * Entity card component props
11
12
  */
12
13
  export interface EntityCardProps {
13
- /** View variant — drives default width and image aspect ratio */
14
+ /** View variant — drives default width, image aspect ratio and fallback icon */
14
15
  view?: EntityCardView;
15
16
  /** Display name shown over the image */
16
17
  name: string;
@@ -20,6 +21,18 @@ export interface EntityCardProps {
20
21
  description?: string;
21
22
  /** Whether to show the description */
22
23
  showDescription?: boolean;
24
+ /** Secondary text shown to the right of the name (`video` view, e.g. formatted created date) */
25
+ date?: string;
26
+ /** Aspect ratio of the image — overrides the per-view default (`203 / 264` avatar, `1 / 1` agent, `16 / 9` video) */
27
+ aspectRatio?: string;
28
+ /** Show the processing overlay (spinner + label) over the image */
29
+ processing?: boolean;
30
+ /** Processing overlay label. Defaults to "Please wait" */
31
+ processingText?: string;
32
+ /** Show the error overlay (warning icon + label) over the image */
33
+ error?: boolean;
34
+ /** Error overlay label. Defaults to "Something went wrong" */
35
+ errorText?: string;
23
36
  /** Preview handler — renders the action button on hover */
24
37
  onPreview?: () => void;
25
38
  /** Label for the action button; when set it renders text + icon, otherwise icon-only */
@@ -38,6 +51,8 @@ export interface EntityCardProps {
38
51
  showMenu?: boolean;
39
52
  /** Width of the card (overrides the view default) */
40
53
  width?: string | number;
54
+ /** Test id (overrides the derived default) */
55
+ testId?: string;
41
56
  }
42
57
  /**
43
58
  * Entity card skeleton props
@@ -45,6 +60,8 @@ export interface EntityCardProps {
45
60
  export interface EntityCardSkeletonProps {
46
61
  /** View variant — drives default width and image aspect ratio */
47
62
  view?: EntityCardView;
63
+ /** Aspect ratio of the placeholder — overrides the per-view default */
64
+ aspectRatio?: string;
48
65
  /** Width of the card (overrides the view default) */
49
66
  width?: string | number;
50
67
  /** Show the description placeholder */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beydesign/storybook",
3
3
  "private": false,
4
- "version": "0.3.1",
4
+ "version": "0.4.1",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -1,3 +0,0 @@
1
- import React from 'react';
2
- import { VideoCardProps } from './types';
3
- export declare const VideoCard: React.FC<VideoCardProps>;
@@ -1,47 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Box } from '@mui/material';
4
- import { keyframes } from '@emotion/react';
5
- import { FilmSlate } from '@phosphor-icons/react';
6
- import { Typography, TypographySize, TypographyWeight } from '../Typography';
7
- import { Menu } from '../UI';
8
- import { MainButton, ButtonHierarchy, ButtonSize, ButtonDisplayMode, } from '../Buttons';
9
- const spin = keyframes `
10
- from { transform: rotate(0deg); }
11
- to { transform: rotate(360deg); }
12
- `;
13
- const SPINNER_SIZE = 28;
14
- const SPINNER_BARS = 12;
15
- /** A spoked activity spinner that rotates continuously. */
16
- const Spinner = () => (_jsx(Box, { position: 'relative', width: `${SPINNER_SIZE}px`, height: `${SPINNER_SIZE}px`, sx: { animation: `${spin} 1s steps(${SPINNER_BARS}, end) infinite` }, children: Array.from({ length: SPINNER_BARS }).map((_, i) => (_jsx(Box, { position: 'absolute', top: 0, left: '50%', width: '2px', height: `${SPINNER_SIZE / 2}px`, borderRadius: '2px', bgcolor: 'var(--color-text-text-white)', sx: {
17
- transform: `translateX(-50%) rotate(${i * (360 / SPINNER_BARS)}deg)`,
18
- transformOrigin: `50% ${SPINNER_SIZE / 2}px`,
19
- opacity: 0.15 + (0.85 * i) / (SPINNER_BARS - 1),
20
- } }, i))) }));
21
- export const VideoCard = ({ name, image, date, aspectRatio = '16 / 9', processing = false, processingText = 'Processing…', onPreview, previewButtonText, previewIcon = 'Play', menuItems = [], showMenu = false, width, testId, }) => {
22
- const [hovered, setHovered] = useState(false);
23
- const [imageError, setImageError] = useState(false);
24
- const entityKey = name?.toLowerCase()?.replace(/\s+/g, '-');
25
- const hasMenu = showMenu && menuItems.length > 0;
26
- const canPreview = !!onPreview && !processing;
27
- const showActions = hovered && (canPreview || hasMenu);
28
- const showImage = !!image && !imageError;
29
- return (_jsx(Box, { width: width ?? '340px', display: 'flex', flexDirection: 'column', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', bgcolor: hovered
30
- ? 'var(--color-background-bg-card-highlight-hover)'
31
- : 'var(--color-background-bg-card-highlight)', border: '1px solid var(--color-border-border-subtle)', boxSizing: 'border-box', overflow: 'hidden', onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), sx: { transition: 'background-color 0.15s ease-in-out' }, "data-testid": testId ?? `${entityKey}-video-card`, children: _jsxs(Box, { position: 'relative', width: '100%', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', overflow: 'hidden', sx: { aspectRatio }, children: [showImage ? (_jsx("img", { src: image, alt: name, width: '100%', height: '100%', style: { objectFit: 'cover', height: '100%', width: '100%' }, onError: () => setImageError(true) })) : (_jsx(Box, { width: '100%', height: '100%', bgcolor: 'var(--color-background-bg-element)', display: 'flex', justifyContent: 'center', alignItems: 'center', children: _jsx(FilmSlate, { size: 64, weight: 'thin', color: 'var(--color-text-text-subtle)' }) })), _jsx(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, sx: {
32
- 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%)',
33
- opacity: hovered && !processing ? 1 : 0,
34
- transition: 'opacity 0.15s ease-in-out',
35
- pointerEvents: 'none',
36
- } }), processing && (_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 })] })), showActions && (_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', flexDirection: 'row', alignItems: 'center', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', sx: { zIndex: 3 }, children: [canPreview &&
37
- (previewButtonText ? (_jsx(MainButton, { text: previewButtonText, showLeadingIcon: true, leadingIcon: previewIcon, iconWeight: 'fill', hierarchy: ButtonHierarchy.SecondaryTransparent, size: ButtonSize.sm, onClick: onPreview, testId: `${entityKey}-preview-button` })) : (_jsx(MainButton, { displayIcon: previewIcon, displayIconMode: ButtonDisplayMode.Only, hierarchy: ButtonHierarchy.SecondaryTransparent, size: ButtonSize.sm, onClick: onPreview, testId: `${entityKey}-preview-button` }))), hasMenu && (_jsx(Menu, { items: menuItems, testKey: entityKey, size: ButtonSize.sm }))] })), _jsxs(Box, { position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between', gap: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-3xl) var(--spacing-tokens-size-in-px-spacing-spacing-lg) var(--spacing-tokens-size-in-px-spacing-spacing-lg)', sx: {
38
- zIndex: 2,
39
- pointerEvents: 'none',
40
- backgroundImage: 'linear-gradient(rgba(45, 38, 55, 0) 0%, rgba(45, 38, 55, 0.16) 34%, rgba(45, 38, 55, 0.64) 100%)',
41
- }, children: [_jsx(Box, { minWidth: 0, flex: 1, children: _jsx(Typography, { text: name, title: name, size: TypographySize.HeadingXS, weight: TypographyWeight.SemiBold, color: 'var(--color-text-text-white)', textStyle: {
42
- display: 'block',
43
- overflow: 'hidden',
44
- textOverflow: 'ellipsis',
45
- whiteSpace: 'nowrap',
46
- } }) }), date && (_jsx(Typography, { text: date, size: TypographySize.TextS, weight: TypographyWeight.Regular, color: 'var(--color-text-text-white)', textStyle: { whiteSpace: 'nowrap', flexShrink: 0, opacity: 0.8 } }))] })] }) }));
47
- };
@@ -1,9 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { VideoCard } from './VideoCard';
3
- declare const meta: Meta<typeof VideoCard>;
4
- export default meta;
5
- type Story = StoryObj<typeof VideoCard>;
6
- export declare const Ready: Story;
7
- export declare const Processing: Story;
8
- export declare const NoImage: Story;
9
- export declare const NoActions: Story;
@@ -1,156 +0,0 @@
1
- import { VideoCard } from './VideoCard';
2
- const meta = {
3
- title: 'Design System/Components/VideoCard/VideoCard',
4
- component: VideoCard,
5
- parameters: {
6
- layout: 'centered',
7
- },
8
- tags: ['autodocs'],
9
- argTypes: {
10
- name: {
11
- control: 'text',
12
- description: 'Video name shown over the bottom of the thumbnail',
13
- table: { type: { summary: 'string' } },
14
- },
15
- image: {
16
- control: 'text',
17
- description: 'Thumbnail url (video preview frame, or the avatar image as a fallback)',
18
- table: { type: { summary: 'string' } },
19
- },
20
- date: {
21
- control: 'text',
22
- description: 'Secondary text shown to the right of the name (e.g. created date)',
23
- table: { type: { summary: 'string' } },
24
- },
25
- aspectRatio: {
26
- control: 'text',
27
- description: 'Aspect ratio of the thumbnail',
28
- table: {
29
- type: { summary: 'string' },
30
- defaultValue: { summary: '16 / 9' },
31
- },
32
- },
33
- processing: {
34
- control: 'boolean',
35
- description: 'Show the processing overlay (spinner + label)',
36
- table: { type: { summary: 'boolean' } },
37
- },
38
- processingText: {
39
- control: 'text',
40
- description: 'Processing overlay label',
41
- table: {
42
- type: { summary: 'string' },
43
- defaultValue: { summary: 'Processing…' },
44
- },
45
- },
46
- onPreview: {
47
- description: 'Preview handler — reveals the action button on hover and makes the thumbnail clickable',
48
- table: { type: { summary: 'function' } },
49
- },
50
- previewButtonText: {
51
- control: 'text',
52
- description: 'Label for the preview action; when set it renders text + icon, otherwise icon-only',
53
- table: { type: { summary: 'string' } },
54
- },
55
- previewIcon: {
56
- control: 'text',
57
- description: 'Icon for the preview action',
58
- table: {
59
- type: { summary: 'IconName' },
60
- defaultValue: { summary: 'Play' },
61
- },
62
- },
63
- showMenu: {
64
- control: 'boolean',
65
- description: 'Whether to show the menu on hover',
66
- table: { type: { summary: 'boolean' } },
67
- },
68
- menuItems: {
69
- description: 'Menu items for the dropdown',
70
- table: { type: { summary: 'array' } },
71
- },
72
- width: {
73
- control: 'text',
74
- description: 'Width of the card',
75
- table: {
76
- type: { summary: 'string | number' },
77
- defaultValue: { summary: '340px' },
78
- },
79
- },
80
- },
81
- };
82
- export default meta;
83
- const videoThumbnail = 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?fm=jpg&q=60&w=800&ixlib=rb-4.0.3';
84
- const menuItems = [
85
- {
86
- label: 'Download Video',
87
- icon: 'DownloadSimple',
88
- onClick: () => { },
89
- },
90
- { label: 'Copy Video ID', icon: 'Copy', onClick: () => { } },
91
- {
92
- label: 'Delete Video',
93
- icon: 'Trash',
94
- onClick: () => { },
95
- destructive: true,
96
- },
97
- ];
98
- export const Ready = {
99
- args: {
100
- name: 'Product launch announcement',
101
- image: videoThumbnail,
102
- date: 'Jul 6, 2026',
103
- onPreview: () => { },
104
- previewButtonText: 'Preview',
105
- showMenu: true,
106
- menuItems,
107
- },
108
- };
109
- export const Processing = {
110
- args: {
111
- name: 'Onboarding walkthrough',
112
- image: videoThumbnail,
113
- date: 'Jul 6, 2026',
114
- processing: true,
115
- showMenu: true,
116
- menuItems: [menuItems[2]],
117
- },
118
- parameters: {
119
- docs: {
120
- description: {
121
- story: 'While a video renders, a spinning indicator overlays the thumbnail and the preview action is disabled. The menu stays available.',
122
- },
123
- },
124
- },
125
- };
126
- export const NoImage = {
127
- args: {
128
- name: 'Product launch announcement',
129
- date: 'Jul 6, 2026',
130
- onPreview: () => { },
131
- previewButtonText: 'Preview',
132
- showMenu: true,
133
- menuItems,
134
- },
135
- parameters: {
136
- docs: {
137
- description: {
138
- story: 'Fallback placeholder shown when no thumbnail is provided or it fails to load.',
139
- },
140
- },
141
- },
142
- };
143
- export const NoActions = {
144
- args: {
145
- name: 'Product launch announcement',
146
- image: videoThumbnail,
147
- date: 'Jul 6, 2026',
148
- },
149
- parameters: {
150
- docs: {
151
- description: {
152
- story: 'A read-only card without preview or menu actions on hover.',
153
- },
154
- },
155
- },
156
- };
@@ -1,3 +0,0 @@
1
- import React from 'react';
2
- import { VideoCardSkeletonProps } from './types';
3
- export declare const VideoCardSkeleton: React.FC<VideoCardSkeletonProps>;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Box } from '@mui/material';
3
- import { SkeletonLoader } from '../UI';
4
- export const VideoCardSkeleton = ({ aspectRatio = '16 / 9', width, style, }) => {
5
- return (_jsx(Box, { width: width ?? '340px', display: 'flex', flexDirection: 'column', padding: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', bgcolor: 'var(--color-background-bg-card-highlight)', border: '1px solid var(--color-border-border-subtle)', boxSizing: 'border-box', overflow: 'hidden', style: style, children: _jsx(Box, { width: '100%', borderRadius: 'var(--spacing-tokens-size-in-px-spacing-spacing-sm)', overflow: 'hidden', sx: { aspectRatio }, children: _jsx(SkeletonLoader, {}) }) }));
6
- };
@@ -1,6 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { VideoCardSkeleton } from './VideoCardSkeleton';
3
- declare const meta: Meta<typeof VideoCardSkeleton>;
4
- export default meta;
5
- type Story = StoryObj<typeof VideoCardSkeleton>;
6
- export declare const Default: Story;
@@ -1,31 +0,0 @@
1
- import { VideoCardSkeleton } from './VideoCardSkeleton';
2
- const meta = {
3
- title: 'Design System/Components/VideoCard/VideoCardSkeleton',
4
- component: VideoCardSkeleton,
5
- parameters: {
6
- layout: 'centered',
7
- },
8
- tags: ['autodocs'],
9
- argTypes: {
10
- aspectRatio: {
11
- control: 'text',
12
- description: 'Aspect ratio of the thumbnail placeholder',
13
- table: {
14
- type: { summary: 'string' },
15
- defaultValue: { summary: '16 / 9' },
16
- },
17
- },
18
- width: {
19
- control: 'text',
20
- description: 'Width of the card',
21
- table: {
22
- type: { summary: 'string | number' },
23
- defaultValue: { summary: '340px' },
24
- },
25
- },
26
- },
27
- };
28
- export default meta;
29
- export const Default = {
30
- args: {},
31
- };
@@ -1,3 +0,0 @@
1
- export * from './VideoCard';
2
- export * from './VideoCardSkeleton';
3
- export * from './types';
@@ -1,3 +0,0 @@
1
- export * from './VideoCard';
2
- export * from './VideoCardSkeleton';
3
- export * from './types';
@@ -1,47 +0,0 @@
1
- import { MenuItemProps } from '../UI';
2
- import { IconName } from '../../utils';
3
- /**
4
- * VideoCard component props — a landscape (16:9 by default) media tile for a
5
- * generated video. Shows a thumbnail with the video name and date overlaid on
6
- * the bottom gradient, a hover reveal of the preview + menu actions, and an
7
- * optional processing overlay.
8
- */
9
- export interface VideoCardProps {
10
- /** Video name shown in the footer */
11
- name: string;
12
- /** Thumbnail url (video preview frame, or the avatar image as a fallback) */
13
- image?: string;
14
- /** Secondary text shown to the right of the name (e.g. formatted created date) */
15
- date?: string;
16
- /** Aspect ratio of the thumbnail. Defaults to `16 / 9` */
17
- aspectRatio?: string;
18
- /** Show the processing overlay (spinner + label) over the thumbnail */
19
- processing?: boolean;
20
- /** Processing overlay label. Defaults to "Processing…" */
21
- processingText?: string;
22
- /** Preview handler — reveals the action button on hover and makes the thumbnail clickable */
23
- onPreview?: () => void;
24
- /** Label for the preview action; when set it renders text + icon, otherwise icon-only */
25
- previewButtonText?: string;
26
- /** Icon for the preview action (defaults to `Play`) */
27
- previewIcon?: IconName;
28
- /** Menu items for the dropdown — renders the menu button on hover */
29
- menuItems?: MenuItemProps[];
30
- /** Whether to show the menu */
31
- showMenu?: boolean;
32
- /** Width of the card (overrides the default) */
33
- width?: string | number;
34
- /** Test id */
35
- testId?: string;
36
- }
37
- /**
38
- * VideoCard skeleton props
39
- */
40
- export interface VideoCardSkeletonProps {
41
- /** Aspect ratio of the thumbnail placeholder. Defaults to `16 / 9` */
42
- aspectRatio?: string;
43
- /** Width of the card (overrides the default) */
44
- width?: string | number;
45
- /** Custom style for the skeleton */
46
- style?: React.CSSProperties;
47
- }
@@ -1 +0,0 @@
1
- export {};