React MyUI - Card

1. Design

2. Implementation (css and tsx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
.myui-card {
display: flex;
flex-direction: column;
background-color: var(--color-background);
border-radius: 8px;
overflow: hidden;
transition: all 0.2s ease-in-out;
}

/* Card variants */
.myui-card--default {
border: 1px solid var(--color-border);
}

.myui-card--elevated {
border: none;
box-shadow: var(--shadow-default);
}

.myui-card--outlined {
border: 1px solid var(--color-border);
background-color: transparent;
}

.myui-card--flat {
border: none;
background-color: var(--color-background-secondary, #f8f9fa);
}

/* Card with hover effect */
.myui-card--hover:hover {
transform: translateY(-4px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
}

/* Card with interactive behavior */
.myui-card--interactive {
cursor: pointer;
}

.myui-card--interactive:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.myui-card--interactive:active {
transform: translateY(1px);
}

/* Card sections */
.myui-card__image {
width: 100%;
height: auto;
display: block;
}

.myui-card__image--cover {
object-fit: cover;
}

.myui-card__header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-border);
}

.myui-card__body {
padding: 1.25rem;
flex: 1;
}

.myui-card__footer {
padding: 1rem 1.25rem;
border-top: 1px solid var(--color-border);
}

/* Card title and subtitle */
.myui-card__title {
margin: 0 0 0.25rem 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
}

.myui-card__subtitle {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-secondary, #666666);
}

/* Padding variants */
.myui-card--compact .myui-card__header,
.myui-card--compact .myui-card__footer {
padding: 0.75rem 1rem;
}

.myui-card--compact .myui-card__body {
padding: 1rem;
}

.myui-card--spacious .myui-card__header,
.myui-card--spacious .myui-card__footer {
padding: 1.5rem 1.75rem;
}

.myui-card--spacious .myui-card__body {
padding: 1.75rem;
}

/* Horizontal card layout */
.myui-card--horizontal {
flex-direction: row;
}

.myui-card--horizontal .myui-card__image-container {
flex: 0 0 auto;
max-width: 40%;
}

.myui-card--horizontal .myui-card__content {
flex: 1;
display: flex;
flex-direction: column;
}

.myui-card--horizontal .myui-card__image {
height: 100%;
object-fit: cover;
}

/* Card states */
.myui-card--disabled {
opacity: 0.7;
pointer-events: none;
}

/* Color variants */
.myui-card--primary {
border-top: 4px solid var(--color-primary);
}

.myui-card--secondary {
border-top: 4px solid var(--color-secondary);
}

.myui-card--accent {
border-top: 4px solid var(--color-accent);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import React from 'react';
import './Card.css';

export type CardVariant = 'default' | 'elevated' | 'outlined' | 'flat';
export type CardPadding = 'normal' | 'compact' | 'spacious';
export type CardColorAccent = 'none' | 'primary' | 'secondary' | 'accent';

export interface CardProps {
/**
* Card children content
*/
children: React.ReactNode;

/**
* Card variant
* @default 'default'
*/
variant?: CardVariant;

/**
* Card padding size
* @default 'normal'
*/
padding?: CardPadding;

/**
* Color accent at the top of card
* @default 'none'
*/
colorAccent?: CardColorAccent;

/**
* Card header content
*/
header?: React.ReactNode;

/**
* Card footer content
*/
footer?: React.ReactNode;

/**
* Card image (at the top or left side)
*/
image?: {
src: string;
alt: string;
height?: number | string;
cover?: boolean;
};

/**
* Card title
*/
title?: React.ReactNode;

/**
* Card subtitle
*/
subtitle?: React.ReactNode;

/**
* Whether the card has hover effect
* @default false
*/
hover?: boolean;

/**
* Whether the card is interactive (clickable)
* @default false
*/
interactive?: boolean;

/**
* Whether the card layout is horizontal
* @default false
*/
horizontal?: boolean;

/**
* Whether the card is disabled
* @default false
*/
disabled?: boolean;

/**
* Additional class name
*/
className?: string;

/**
* Click handler for interactive cards
*/
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
}

export const Card: React.FC<CardProps> = ({
children,
variant = 'default',
padding = 'normal',
colorAccent = 'none',
header,
footer,
image,
title,
subtitle,
hover = false,
interactive = false,
horizontal = false,
disabled = false,
className = '',
onClick,
...rest
}) => {
const paddingClass = padding !== 'normal' ? `myui-card--${padding}` : '';
const colorAccentClass = colorAccent !== 'none' ? `myui-card--${colorAccent}` : '';

const cardClasses = [
'myui-card',
`myui-card--${variant}`,
paddingClass,
colorAccentClass,
hover ? 'myui-card--hover' : '',
interactive ? 'myui-card--interactive' : '',
horizontal ? 'myui-card--horizontal' : '',
disabled ? 'myui-card--disabled' : '',
className
].filter(Boolean).join(' ');

const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (interactive && onClick && !disabled) {
onClick(event);
}
};

// Create title/subtitle header if provided
const titleHeader = (title || subtitle) && !header ? (
<div className="myui-card__header">
{title && <h3 className="myui-card__title">{title}</h3>}
{subtitle && <div className="myui-card__subtitle">{subtitle}</div>}
</div>
) : null;

const renderContent = () => (
<>
{header && <div className="myui-card__header">{header}</div>}
{titleHeader}
<div className="myui-card__body">{children}</div>
{footer && <div className="myui-card__footer">{footer}</div>}
</>
);

return (
<div
className={cardClasses}
onClick={handleClick}
role={interactive ? 'button' : undefined}
tabIndex={interactive ? 0 : undefined}
{...rest}
>
{horizontal ? (
<>
{image && (
<div className="myui-card__image-container">
<img
src={image.src}
alt={image.alt}
className={`myui-card__image ${image.cover ? 'myui-card__image--cover' : ''}`}
style={{ height: image.height }}
/>
</div>
)}
<div className="myui-card__content">
{renderContent()}
</div>
</>
) : (
<>
{image && (
<img
src={image.src}
alt={image.alt}
className={`myui-card__image ${image.cover ? 'myui-card__image--cover' : ''}`}
style={{ height: image.height }}
/>
)}
{renderContent()}
</>
)}
</div>
);
};

export default Card;

3. Export Config

package/core/src/components/Card/index.ts

1
2
export * from './Card'
export { default } from './Card'

packages/core/src/index.ts
Add

1
export * from './components/Card';

4. StoryBook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Card } from '@hardyhu-ui/core';
import { Button } from '@hardyhu-ui/core';

const meta: Meta<typeof Card> = {
title: 'Components/Card',
component: Card,
argTypes: {
variant: {
control: { type: 'select' },
options: ['default', 'elevated', 'outlined', 'flat'],
description: 'Card style variant',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'default' },
},
},
padding: {
control: { type: 'select' },
options: ['normal', 'compact', 'spacious'],
description: 'Card padding size',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'normal' },
},
},
colorAccent: {
control: { type: 'select' },
options: ['none', 'primary', 'secondary', 'accent'],
description: 'Color accent on top of card',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'none' },
},
},
hover: {
control: 'boolean',
description: 'Enables hover effect',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
interactive: {
control: 'boolean',
description: 'Makes card clickable',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
horizontal: {
control: 'boolean',
description: 'Makes card layout horizontal',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
disabled: {
control: 'boolean',
description: 'Disables the card',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
onClick: { action: 'clicked' },
},
parameters: {
docs: {
description: {
component: 'A versatile card component with various styles and layouts.',
},
},
},
};

export default meta;
type Story = StoryObj<typeof Card>;

export const Default: Story = {
args: {
title: 'Card Title',
subtitle: 'Card Subtitle',
children: (
<p>This is a simple card with title and subtitle. Cards can be used to group related content and actions.</p>
),
},
};

export const WithFooter: Story = {
args: {
title: 'Card with Footer',
children: (
<p>This card has a footer with action buttons.</p>
),
footer: (
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
<Button variant="outline" size="small">Cancel</Button>
<Button size="small">Save</Button>
</div>
),
},
};

export const WithImage: Story = {
args: {
title: 'Card with Image',
subtitle: 'Feature image at the top',
image: {
src: 'https://picsum.photos/800/400',
alt: 'Feature image',
cover: true,
},
children: (
<p>This card includes an image at the top. Images help provide visual context to your content.</p>
),
},
};

export const Variants: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Card {...args} variant="default" title="Default Card">
<p>Standard card with border</p>
</Card>

<Card {...args} variant="elevated" title="Elevated Card">
<p>Card with shadow</p>
</Card>

<Card {...args} variant="outlined" title="Outlined Card">
<p>Transparent with border</p>
</Card>

<Card {...args} variant="flat" title="Flat Card">
<p>Card with background color</p>
</Card>
</div>
),
};

export const ColorAccents: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Card {...args} colorAccent="primary" title="Primary Accent">
<p>Card with primary color accent</p>
</Card>

<Card {...args} colorAccent="secondary" title="Secondary Accent">
<p>Card with secondary color accent</p>
</Card>

<Card {...args} colorAccent="accent" title="Accent Color">
<p>Card with accent color</p>
</Card>
</div>
),
};

export const Interactive: Story = {
args: {
title: 'Interactive Card',
subtitle: 'Click me',
children: (
<p>This card is interactive and can be clicked like a button.</p>
),
interactive: true,
hover: true,
onClick: () => alert('Card clicked!'),
},
};

export const Horizontal: Story = {
args: {
title: 'Horizontal Layout',
subtitle: 'Image on the left side',
children: (
<p>This card uses a horizontal layout with the image on the left side. This layout is useful for list items or content where the image should be displayed alongside text.</p>
),
image: {
src: 'https://picsum.photos/400/300',
alt: 'Card image',
cover: true,
},
horizontal: true,
variant: 'elevated',
},
};

export const CustomContent: Story = {
render: () => (
<Card variant="elevated" hover>
<div style={{ padding: '1rem' }}>
<div style={{ marginBottom: '1rem' }}>
<h3 style={{ margin: '0 0 0.5rem 0', fontSize: '1.5rem' }}>Custom Content</h3>
<p style={{ margin: '0', color: '#666' }}>Card with custom styled content</p>
</div>

<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
<div style={{ backgroundColor: '#f0f0f0', padding: '1rem', borderRadius: '4px', flex: 1 }}>
<h4 style={{ margin: '0 0 0.5rem 0' }}>Feature 1</h4>
<p style={{ margin: '0', fontSize: '0.875rem' }}>Description of feature one.</p>
</div>
<div style={{ backgroundColor: '#f0f0f0', padding: '1rem', borderRadius: '4px', flex: 1 }}>
<h4 style={{ margin: '0 0 0.5rem 0' }}>Feature 2</h4>
<p style={{ margin: '0', fontSize: '0.875rem' }}>Description of feature two.</p>
</div>
</div>

<Button fullWidth>Learn More</Button>
</div>
</Card>
),
};

export const GridLayout: Story = {
render: () => (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '1rem' }}>
{Array(6).fill(0).map((_, i) => (
<Card
key={i}
variant="elevated"
hover
interactive
image={{
src: `https://picsum.photos/300/${200 + i * 10}`,
alt: `Card ${i + 1}`,
cover: true,
height: 150,
}}
onClick={() => alert(`Card ${i + 1} clicked!`)}
>
<h3 style={{ margin: '0 0 0.5rem 0' }}>Card {i + 1}</h3>
<p style={{ margin: '0 0 1rem 0' }}>This is a grid of interactive cards with images.</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.875rem', color: '#666' }}>Item #{i + 1}</span>
<Button size="small" variant="outline">View</Button>
</div>
</Card>
))}
</div>
),
};

5. Jest

1


React MyUI - Card
https://www.hardyhu.cn/2025/02/16/React-MyUI-Card/
Author
John Doe
Posted on
February 16, 2025
Licensed under