Skip to content

Commit 3ef89b5

Browse files
authored
refactor(app): create storybooks for media cards in desktop app and odd (#20129)
# Overview Create components for media cards on ODD and App ## Test Plan and Hands on Testing - smoke tested to ensure previous behavior has not been lost <img width="923" height="356" alt="Screenshot 2025-11-12 at 1 23 00 PM" src="https://github.com/user-attachments/assets/92b8a24f-8f4a-472d-9460-32f68ac262a4" /> **Storybooks** <img width="685" height="463" alt="Screenshot 2025-11-17 at 11 11 51 AM" src="https://github.com/user-attachments/assets/ed60cd6a-8734-4ef5-b543-e93e2235e5 <img width="516" height="566" alt="Screenshot 2025-11-17 at 11 12 36 AM" src="https://github.com/user-attachments/assets/196eb49f-7ef7-41e4-8141-3e87df1dd14c" /> d6" /> ## Changelog - Created molecules & storybooks for media card ## Risk assessment medium - changes avenue for how media cards are displayed
1 parent 5525881 commit 3ef89b5

File tree

10 files changed

+560
-250
lines changed

10 files changed

+560
-250
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { VIEWPORT } from '@opentrons/components'
2+
3+
import { MediaContainerContent } from './index'
4+
5+
import type { Meta, StoryObj } from '@storybook/react'
6+
7+
const meta: Meta<typeof MediaContainerContent> = {
8+
title: 'App/Molecules/MediaContainer',
9+
component: MediaContainerContent,
10+
parameters: VIEWPORT.touchScreenViewport,
11+
argTypes: {
12+
state: {
13+
control: {
14+
type: 'radio',
15+
},
16+
options: ['loading', 'error', 'neutral'],
17+
},
18+
overflowMenuActions: {
19+
control: 'object',
20+
description: 'dictionary of menu actions with label and handler',
21+
},
22+
},
23+
}
24+
25+
export default meta
26+
27+
type Story = StoryObj<typeof MediaContainerContent>
28+
29+
export const MediaContainerContentComponent: Story = {
30+
args: {
31+
mediaContent: (
32+
<img
33+
src="https://via.placeholder.com/300x200.png?text=Sample+Image"
34+
alt="Sample"
35+
style={{
36+
width: '100%',
37+
height: '100%',
38+
objectFit: 'cover',
39+
borderRadius: '0.5rem',
40+
}}
41+
/>
42+
),
43+
centerPrimaryText: 'Example Image Title',
44+
centerSecondaryText: 'Taken during experiment',
45+
rightPrimaryText: '2:45 PM',
46+
state: 'loading',
47+
overflowMenu: true,
48+
overflowMenuActions: [{ label: 'view media', onClick: () => {} }],
49+
mediaContentOnClick: () => {},
50+
hoverText: 'Click to view image',
51+
},
52+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { useTranslation } from 'react-i18next'
2+
3+
import {
4+
Chip,
5+
MenuItem,
6+
OverflowBtn,
7+
StyledText,
8+
useMenuHandleClickOutside,
9+
} from '@opentrons/components'
10+
11+
import { Skeleton } from '/app/atoms/Skeleton'
12+
13+
import styles from './media.module.css'
14+
15+
import type { ReactNode } from 'react'
16+
17+
export interface OverflowAction {
18+
label: string
19+
onClick: () => void
20+
}
21+
22+
export interface MediaContainerContentProps {
23+
mediaContent: ReactNode
24+
centerPrimaryText: string
25+
centerSecondaryText: string
26+
rightPrimaryText: string
27+
overflowMenu: boolean
28+
overflowMenuActions?: OverflowAction[]
29+
mediaContentOnClick?: () => void
30+
state: 'loading' | 'error' | null
31+
hoverText: string | null
32+
}
33+
34+
export function MediaContainerContent(
35+
props: MediaContainerContentProps
36+
): JSX.Element {
37+
const {
38+
mediaContent,
39+
centerPrimaryText,
40+
centerSecondaryText,
41+
rightPrimaryText,
42+
state,
43+
hoverText,
44+
overflowMenu,
45+
overflowMenuActions,
46+
mediaContentOnClick,
47+
} = props
48+
49+
const { t } = useTranslation(['run_details', 'branded'])
50+
const isLoading = state === 'loading'
51+
const isError = state === 'error'
52+
53+
const {
54+
handleOverflowClick,
55+
showOverflowMenu,
56+
menuOverlay,
57+
setShowOverflowMenu,
58+
} = useMenuHandleClickOutside()
59+
60+
return (
61+
<div className={styles.media_card}>
62+
<div
63+
className={styles.media_card_thumbnail}
64+
onClick={() => {
65+
if (!isLoading && mediaContentOnClick) mediaContentOnClick()
66+
}}
67+
>
68+
{isLoading ? (
69+
<Skeleton width="100%" height="100%" backgroundSize="47rem" />
70+
) : (
71+
mediaContent
72+
)}
73+
{!isLoading && hoverText != null && (
74+
<div className={styles.media_img_overlay}>
75+
<StyledText
76+
desktopStyle="bodyDefaultRegular"
77+
className={styles.media_overlay_text}
78+
>
79+
{hoverText}
80+
</StyledText>
81+
</div>
82+
)}
83+
</div>
84+
<div className={styles.media_card_cmd_txt_container}>
85+
{isError && (
86+
<Chip
87+
text={t('error_event')}
88+
type="error"
89+
width="fit-content"
90+
chipSize="small"
91+
/>
92+
)}
93+
{isLoading ? (
94+
<Skeleton width="100%" height="1.25rem" backgroundSize="47rem" />
95+
) : (
96+
<StyledText desktopStyle="bodyDefaultRegular">
97+
{centerPrimaryText}
98+
</StyledText>
99+
)}
100+
{isLoading ? (
101+
<Skeleton width="80%" height="1rem" backgroundSize="47rem" />
102+
) : (
103+
<StyledText
104+
desktopStyle="bodyDefaultRegular"
105+
className={styles.media_cmd_txt_subtext}
106+
>
107+
{centerSecondaryText}
108+
</StyledText>
109+
)}
110+
</div>
111+
<div className={styles.media_card_timestamp}>
112+
{isLoading ? (
113+
<Skeleton width="80%" height="1rem" backgroundSize="47rem" />
114+
) : (
115+
<StyledText desktopStyle="bodyDefaultRegular">
116+
{rightPrimaryText}
117+
</StyledText>
118+
)}
119+
</div>
120+
{overflowMenu && overflowMenuActions && (
121+
<div className={styles.overflow_container}>
122+
<OverflowBtn onClick={handleOverflowClick} />
123+
{showOverflowMenu && (
124+
<div className={styles.overflow_menu_container}>
125+
{overflowMenuActions.map(({ label, onClick }) => (
126+
<MenuItem
127+
key={label}
128+
onClick={() => {
129+
onClick()
130+
setShowOverflowMenu(false)
131+
}}
132+
>
133+
<div className={styles.overflow_menu_item}>{t(label)}</div>
134+
</MenuItem>
135+
))}
136+
</div>
137+
)}
138+
</div>
139+
)}
140+
141+
{menuOverlay}
142+
</div>
143+
)
144+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
.media_card {
2+
position: relative;
3+
display: flex;
4+
padding: var(--spacing-12);
5+
border-radius: var(--border-radius-4);
6+
background-color: var(--grey-20);
7+
gap: var(--spacing-24);
8+
}
9+
10+
.media_card_thumbnail {
11+
position: relative;
12+
display: flex;
13+
width: 7.5625rem;
14+
height: 4.25rem;
15+
flex: 0 0 7.5625rem;
16+
align-items: center;
17+
cursor: pointer;
18+
}
19+
20+
.media_img_overlay {
21+
position: absolute;
22+
top: 0;
23+
left: 0;
24+
display: flex;
25+
width: 100%;
26+
height: 100%;
27+
align-items: center;
28+
justify-content: center;
29+
border-radius: 3.97px;
30+
background: var(--transparent-black-60);
31+
opacity: 0;
32+
transition: opacity 0.2s ease;
33+
}
34+
35+
.media_card_thumbnail:hover .media_img_overlay {
36+
opacity: 1;
37+
}
38+
39+
.media_overlay_text {
40+
color: white;
41+
}
42+
43+
.media_card_cmd_txt_container {
44+
display: flex;
45+
flex: 1;
46+
flex-direction: column;
47+
justify-content: center;
48+
gap: var(--spacing-2);
49+
}
50+
51+
.media_card_timestamp {
52+
display: flex;
53+
flex: 0 0 9rem;
54+
align-items: center;
55+
}
56+
57+
.media_cmd_txt_subtext {
58+
color: var(--grey-60);
59+
}
60+
61+
.overflow_container {
62+
position: absolute;
63+
top: 0.2rem;
64+
right: 0.2rem;
65+
display: flex;
66+
flex-direction: column;
67+
}
68+
69+
.overflow_menu_container {
70+
position: absolute;
71+
z-index: 10;
72+
top: 2.3rem;
73+
right: 0;
74+
display: flex;
75+
flex-direction: column;
76+
border-radius: var(--border-radius-8);
77+
background-color: var(--white);
78+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
79+
white-space: nowrap;
80+
}
81+
82+
.overflow_menu_item {
83+
display: flex;
84+
align-items: center;
85+
gap: var(--spacing-8);
86+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { VIEWPORT } from '@opentrons/components'
2+
3+
import { ODDMediaContainerContent } from './index'
4+
5+
import type { Meta, StoryObj } from '@storybook/react'
6+
7+
const meta: Meta<typeof ODDMediaContainerContent> = {
8+
title: 'App/Molecules/ODDMediaContainer',
9+
component: ODDMediaContainerContent,
10+
parameters: VIEWPORT.touchScreenViewport,
11+
argTypes: {
12+
state: {
13+
control: {
14+
type: 'radio',
15+
},
16+
options: ['loading', 'error', 'neutral'],
17+
},
18+
},
19+
}
20+
21+
export default meta
22+
23+
type Story = StoryObj<typeof ODDMediaContainerContent>
24+
25+
export const MediaContainerContentComponent: Story = {
26+
args: {
27+
leftPrimaryText: 'timestamp',
28+
centerPrimaryText: 'current command step',
29+
centerSecondaryText: 'previous command',
30+
rightButtonOnClick: () => {},
31+
rightButtonText: 'view image',
32+
state: 'loading',
33+
},
34+
}

0 commit comments

Comments
 (0)