Skip to content

Commit c9ab6cd

Browse files
feat: ContentCard
1 parent a5e53aa commit c9ab6cd

File tree

16 files changed

+1483
-0
lines changed

16 files changed

+1483
-0
lines changed

apps/mobile-app/scripts/utils/routes.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export const routes = [
3838
getComponent: () =>
3939
require('@coinbase/cds-mobile/overlays/__stories__/AlertVerticalActions.stories').default,
4040
},
41+
{
42+
key: 'AlphaContentCard',
43+
getComponent: () =>
44+
require('@coinbase/cds-mobile/alpha/content-card/__stories__/AlphaContentCard.stories')
45+
.default,
46+
},
4147
{
4248
key: 'AlphaSelect',
4349
getComponent: () =>

apps/mobile-app/src/routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export const routes = [
3838
getComponent: () =>
3939
require('@coinbase/cds-mobile/overlays/__stories__/AlertVerticalActions.stories').default,
4040
},
41+
{
42+
key: 'AlphaContentCard',
43+
getComponent: () =>
44+
require('@coinbase/cds-mobile/alpha/content-card/__stories__/AlphaContentCard.stories')
45+
.default,
46+
},
4147
{
4248
key: 'AlphaSelect',
4349
getComponent: () =>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, { forwardRef, memo } from 'react';
2+
import type { View } from 'react-native';
3+
4+
import { VStack } from '../../layout';
5+
import { Pressable, type PressableProps } from '../../system';
6+
7+
export type ContentCardBaseProps = {
8+
children: React.ReactNode;
9+
renderAsPressable?: boolean;
10+
};
11+
12+
export type ContentCardProps = ContentCardBaseProps & PressableProps;
13+
14+
export const ContentCard = memo(
15+
forwardRef<View, ContentCardProps>(({ children, renderAsPressable, ...props }, ref) => {
16+
const Component = renderAsPressable ? Pressable : VStack;
17+
return (
18+
<Component ref={ref} borderRadius={500} {...props}>
19+
{children}
20+
</Component>
21+
);
22+
}),
23+
);
24+
25+
ContentCard.displayName = 'ContentCard';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { forwardRef, memo, useMemo } from 'react';
2+
import type { StyleProp, View, ViewStyle } from 'react-native';
3+
4+
import { HStack } from '../../layout';
5+
import { Box, type BoxBaseProps } from '../../layout/Box';
6+
import { VStack, type VStackProps } from '../../layout/VStack';
7+
import { Text } from '../../typography';
8+
9+
export type ContentCardBodyBaseProps = BoxBaseProps & {
10+
title: React.ReactNode;
11+
description?: React.ReactNode;
12+
media?: React.ReactNode;
13+
mediaPlacement?: 'top' | 'bottom' | 'start' | 'end';
14+
styles?: {
15+
root: StyleProp<ViewStyle>;
16+
contentContainer: StyleProp<ViewStyle>;
17+
mediaContainer: StyleProp<ViewStyle>;
18+
};
19+
};
20+
21+
export type ContentCardBodyProps = ContentCardBodyBaseProps;
22+
23+
export const ContentCardBody = memo(
24+
forwardRef<View, ContentCardBodyProps>(
25+
(
26+
{ title, description, media, mediaPlacement = 'top', style, styles, padding = 2, ...props },
27+
ref,
28+
) => {
29+
const hasMedia = !!media;
30+
const isHorizontal = hasMedia && (mediaPlacement === 'start' || mediaPlacement === 'end');
31+
const isMediaFirst = hasMedia && (mediaPlacement === 'top' || mediaPlacement === 'start');
32+
33+
const titleNode = useMemo(() => {
34+
if (typeof title === 'string') {
35+
return (
36+
<Text font="headline" numberOfLines={2}>
37+
{title}
38+
</Text>
39+
);
40+
}
41+
return title;
42+
}, [title]);
43+
44+
const descriptionNode = useMemo(() => {
45+
if (typeof description === 'string') {
46+
return (
47+
<Text color="fgMuted" font="label2" numberOfLines={3}>
48+
{description}
49+
</Text>
50+
);
51+
}
52+
return description;
53+
}, [description]);
54+
55+
const contentNode = useMemo(() => {
56+
return (
57+
<VStack
58+
flexGrow={1}
59+
flexShrink={1}
60+
gap={isHorizontal ? 1 : 0}
61+
style={styles?.contentContainer}
62+
>
63+
{titleNode}
64+
{descriptionNode}
65+
</VStack>
66+
);
67+
}, [isHorizontal, styles?.contentContainer, titleNode, descriptionNode]);
68+
69+
const mediaNode = useMemo(() => {
70+
if (!hasMedia) return null;
71+
return (
72+
<HStack
73+
borderRadius={500}
74+
flexShrink={0}
75+
overflow="hidden"
76+
style={styles?.mediaContainer}
77+
>
78+
{media}
79+
</HStack>
80+
);
81+
}, [hasMedia, media, styles?.mediaContainer]);
82+
83+
return (
84+
<Box
85+
ref={ref}
86+
alignItems="flex-start"
87+
flexDirection={isHorizontal ? 'row' : 'column'}
88+
gap={isHorizontal ? 2 : 1}
89+
padding={padding}
90+
style={[styles?.root, style]}
91+
{...props}
92+
>
93+
{isMediaFirst ? mediaNode : contentNode}
94+
{isMediaFirst ? contentNode : mediaNode}
95+
</Box>
96+
);
97+
},
98+
),
99+
);
100+
101+
ContentCardBody.displayName = 'ContentCardBody';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React, { forwardRef, memo } from 'react';
2+
import type { View } from 'react-native';
3+
4+
import { HStack, type HStackProps } from '../../layout/HStack';
5+
6+
export type ContentCardFooterBaseProps = HStackProps;
7+
8+
export type ContentCardFooterProps = ContentCardFooterBaseProps;
9+
10+
export const ContentCardFooter = memo(
11+
forwardRef<View, ContentCardFooterProps>(({ paddingX = 2, paddingBottom = 2, ...props }, ref) => {
12+
return <HStack ref={ref} paddingBottom={paddingBottom} paddingX={paddingX} {...props} />;
13+
}),
14+
);
15+
16+
ContentCardFooter.displayName = 'ContentCardFooter';
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { forwardRef, memo, useMemo } from 'react';
2+
import type { StyleProp, View, ViewStyle } from 'react-native';
3+
4+
import { type BoxBaseProps } from '../../layout/Box';
5+
import { HStack } from '../../layout/HStack';
6+
import { VStack } from '../../layout/VStack';
7+
import { Text } from '../../typography';
8+
9+
export type ContentCardHeaderBaseProps = BoxBaseProps & {
10+
title: React.ReactNode;
11+
subtitle: React.ReactNode;
12+
thumbnail: React.ReactNode;
13+
action: React.ReactNode;
14+
styles?: {
15+
root: StyleProp<ViewStyle>;
16+
contentContainer: StyleProp<ViewStyle>;
17+
};
18+
};
19+
20+
export type ContentCardHeaderProps = ContentCardHeaderBaseProps;
21+
22+
export const ContentCardHeader = memo(
23+
forwardRef<View, ContentCardHeaderProps>(
24+
(
25+
{
26+
title,
27+
subtitle,
28+
thumbnail,
29+
styles,
30+
action,
31+
gap = 1.5,
32+
paddingX = 2,
33+
paddingTop = 2,
34+
style,
35+
...props
36+
},
37+
ref,
38+
) => {
39+
const titleNode = useMemo(() => {
40+
if (typeof title === 'string') {
41+
return (
42+
<Text font="label1" numberOfLines={1}>
43+
{title}
44+
</Text>
45+
);
46+
}
47+
return title;
48+
}, [title]);
49+
50+
const subtitleNode = useMemo(() => {
51+
if (typeof subtitle === 'string') {
52+
return (
53+
<Text color="fgMuted" font="legal" numberOfLines={1}>
54+
{subtitle}
55+
</Text>
56+
);
57+
}
58+
return subtitle;
59+
}, [subtitle]);
60+
61+
return (
62+
<HStack
63+
ref={ref}
64+
alignItems="center"
65+
gap={gap}
66+
paddingTop={paddingTop}
67+
paddingX={paddingX}
68+
style={[styles?.root, style]}
69+
{...props}
70+
>
71+
{thumbnail}
72+
<VStack
73+
flexGrow={1}
74+
flexShrink={1}
75+
justifyContent="flex-start"
76+
style={styles?.contentContainer}
77+
>
78+
{titleNode}
79+
{subtitleNode}
80+
</VStack>
81+
{action}
82+
</HStack>
83+
);
84+
},
85+
),
86+
);
87+
88+
ContentCardHeader.displayName = 'ContentCardHeader';

0 commit comments

Comments
 (0)