@vetc-miniapp/ui-react 0.0.23 → 0.0.24

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.
Files changed (56) hide show
  1. package/README.md +375 -56
  2. package/package.json +17 -5
  3. package/src/dist/ui-react/index.js +1 -1
  4. package/src/ui-react/components/avatar/Avatar.tsx +88 -0
  5. package/src/ui-react/components/avatar/index.ts +2 -0
  6. package/src/ui-react/components/bottom-sheet/BottomSheet.tsx +149 -0
  7. package/src/ui-react/components/bottom-sheet/index.ts +2 -0
  8. package/src/ui-react/components/button/Button.tsx +246 -0
  9. package/src/ui-react/components/button/index.ts +2 -0
  10. package/src/ui-react/components/button-group/ButtonGroup.tsx +108 -0
  11. package/src/ui-react/components/button-group/index.ts +2 -0
  12. package/src/ui-react/components/card/Card.tsx +77 -0
  13. package/src/ui-react/components/card/index.ts +2 -0
  14. package/src/ui-react/components/checkbox/Checkbox.tsx +232 -0
  15. package/src/ui-react/components/checkbox/index.ts +2 -0
  16. package/src/ui-react/components/chip/Chip.tsx +137 -0
  17. package/src/ui-react/components/chip/index.ts +2 -0
  18. package/src/ui-react/components/dialog/Dialog.tsx +135 -0
  19. package/src/ui-react/components/dialog/index.ts +2 -0
  20. package/src/ui-react/components/divider/Divider.tsx +54 -0
  21. package/src/ui-react/components/divider/index.ts +2 -0
  22. package/src/ui-react/components/input/Input.tsx +195 -0
  23. package/src/ui-react/components/input/index.ts +2 -0
  24. package/src/ui-react/components/list/List.tsx +180 -0
  25. package/src/ui-react/components/list/index.ts +2 -0
  26. package/src/ui-react/components/loading/Loading.tsx +121 -0
  27. package/src/ui-react/components/loading/index.ts +2 -0
  28. package/src/ui-react/components/modal/Modal.tsx +116 -0
  29. package/src/ui-react/components/modal/index.ts +2 -0
  30. package/src/ui-react/components/navigation-bar/NavigationBar.tsx +188 -0
  31. package/src/ui-react/components/navigation-bar/index.ts +2 -0
  32. package/src/ui-react/components/radio/Radio.tsx +216 -0
  33. package/src/ui-react/components/radio/index.ts +2 -0
  34. package/src/ui-react/components/select/Select.tsx +109 -0
  35. package/src/ui-react/components/select/index.ts +2 -0
  36. package/src/ui-react/components/switch/Switch.tsx +164 -0
  37. package/src/ui-react/components/switch/index.ts +2 -0
  38. package/src/ui-react/components/tab-bar/TabBar.tsx +137 -0
  39. package/src/ui-react/components/tab-bar/index.ts +2 -0
  40. package/src/ui-react/components/textarea/Textarea.tsx +109 -0
  41. package/src/ui-react/components/textarea/index.ts +2 -0
  42. package/src/ui-react/components/toast/Toast.ts +98 -0
  43. package/src/ui-react/components/toast/index.ts +2 -0
  44. package/src/ui-react/components/typography/Typography.tsx +201 -0
  45. package/src/ui-react/components/typography/index.ts +2 -0
  46. package/src/ui-react/hooks/use-tap-app-bar.js +26 -0
  47. package/src/ui-react/hooks/use-tap-app-bar.ts +34 -0
  48. package/src/ui-react/index.js +1 -0
  49. package/src/ui-react/index.ts +79 -3
  50. package/src/ui-react/styles/VETCProvider.tsx +152 -0
  51. package/src/ui-react/styles/tokens.css +427 -0
  52. package/src/ui-react/tokens/colors.ts +91 -0
  53. package/src/ui-react/tokens/index.ts +3 -0
  54. package/src/ui-react/tokens/spacing.ts +59 -0
  55. package/src/ui-react/tokens/typography.ts +63 -0
  56. package/src/ui-react/tokens_vetc.json +1517 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * VETC BottomSheet — tokenized
3
+ * Figma: Sheet page — 16px top radius, handle bar, slide-up animation
4
+ */
5
+ import React, { useEffect } from 'react';
6
+
7
+ export interface BottomSheetProps {
8
+ open: boolean;
9
+ title?: React.ReactNode;
10
+ children?: React.ReactNode;
11
+ maskClosable?: boolean;
12
+ showHandle?: boolean;
13
+ maxHeight?: string;
14
+ onClose?: () => void;
15
+ className?: string;
16
+ style?: React.CSSProperties;
17
+ id?: string;
18
+ }
19
+
20
+ export function BottomSheet({
21
+ open,
22
+ title,
23
+ children,
24
+ maskClosable = true,
25
+ showHandle = true,
26
+ maxHeight = '90vh',
27
+ onClose,
28
+ className = '',
29
+ style,
30
+ id,
31
+ }: BottomSheetProps) {
32
+ // Lock body scroll when open
33
+ useEffect(() => {
34
+ document.body.style.overflow = open ? 'hidden' : '';
35
+ return () => { document.body.style.overflow = ''; };
36
+ }, [open]);
37
+
38
+ if (!open) return null;
39
+
40
+ return (
41
+ <>
42
+ {/* Overlay */}
43
+ <div
44
+ className="vetc-bottom-sheet-overlay"
45
+ aria-hidden="true"
46
+ onClick={() => maskClosable && onClose?.()}
47
+ style={{
48
+ position: 'fixed',
49
+ inset: 0,
50
+ backgroundColor: 'var(--vetc-sheet-overlay)',
51
+ zIndex: 1000,
52
+ animation: 'vetc-fade-in var(--vetc-transition-base)',
53
+ }}
54
+ />
55
+
56
+ {/* Sheet panel */}
57
+ <div
58
+ id={id}
59
+ role="dialog"
60
+ aria-modal="true"
61
+ className={`vetc-bottom-sheet ${className}`}
62
+ style={{
63
+ position: 'fixed',
64
+ bottom: 0,
65
+ left: 0,
66
+ right: 0,
67
+ backgroundColor: 'var(--vetc-sheet-bg)',
68
+ borderRadius: `var(--vetc-sheet-radius) var(--vetc-sheet-radius) 0 0`,
69
+ maxHeight,
70
+ overflowY: 'auto',
71
+ zIndex: 1001,
72
+ fontFamily: 'var(--vetc-font-family)',
73
+ animation: 'vetc-slide-up var(--vetc-transition-slow) cubic-bezier(0.32, 0.72, 0, 1)',
74
+ ...style,
75
+ }}
76
+ >
77
+ {/* Handle bar */}
78
+ {showHandle && (
79
+ <div style={{ display: 'flex', justifyContent: 'center', paddingTop: 'var(--vetc-space-8)', paddingBottom: 'var(--vetc-space-4)' }}>
80
+ <div style={{
81
+ width: 'var(--vetc-sheet-handle-width)',
82
+ height: 'var(--vetc-sheet-handle-height)',
83
+ borderRadius: 'var(--vetc-radius-pill)',
84
+ backgroundColor: 'var(--vetc-sheet-handle-color)',
85
+ }} />
86
+ </div>
87
+ )}
88
+
89
+ {/* Header */}
90
+ {title && (
91
+ <div style={{
92
+ padding: `var(--vetc-space-12) var(--vetc-sheet-padding)`,
93
+ borderBottom: `1px solid var(--vetc-sheet-border)`,
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ justifyContent: 'space-between',
97
+ }}>
98
+ <span style={{
99
+ fontSize: 'var(--vetc-sheet-title-size)',
100
+ fontWeight: 'var(--vetc-sheet-title-weight)' as any,
101
+ color: 'var(--vetc-sheet-title-color)',
102
+ lineHeight: 'var(--vetc-line-height-relaxed)',
103
+ }}>
104
+ {title}
105
+ </span>
106
+ {onClose && (
107
+ <button
108
+ onClick={onClose}
109
+ aria-label="Đóng"
110
+ style={{
111
+ background: 'none',
112
+ border: 'none',
113
+ cursor: 'pointer',
114
+ padding: 'var(--vetc-space-4)',
115
+ display: 'flex',
116
+ color: 'var(--vetc-color-icon-muted)',
117
+ borderRadius:'var(--vetc-radius-pill)',
118
+ transition: 'background-color var(--vetc-transition-fast)',
119
+ }}
120
+ >
121
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
122
+ <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
123
+ </svg>
124
+ </button>
125
+ )}
126
+ </div>
127
+ )}
128
+
129
+ {/* Body */}
130
+ <div style={{ padding: 'var(--vetc-sheet-padding)' }}>
131
+ {children}
132
+ </div>
133
+ </div>
134
+
135
+ <style>{`
136
+ @keyframes vetc-fade-in {
137
+ from { opacity: 0 }
138
+ to { opacity: 1 }
139
+ }
140
+ @keyframes vetc-slide-up {
141
+ from { transform: translateY(100%) }
142
+ to { transform: translateY(0) }
143
+ }
144
+ `}</style>
145
+ </>
146
+ );
147
+ }
148
+
149
+ export default BottomSheet;
@@ -0,0 +1,2 @@
1
+ export { BottomSheet } from './BottomSheet';
2
+ export type { BottomSheetProps } from './BottomSheet';
@@ -0,0 +1,246 @@
1
+ /**
2
+ * VETC Button Component
3
+ * Figma: filled/outlined/elevated/ghost styles
4
+ * Large: h=48px, radius=12px, font=16px/600
5
+ * Small: h=32px, radius=8px, font=14px/600
6
+ * style: filled | outlined | elevated | ghost | danger | danger-outlined
7
+ * variant: brand (green) | neutral (gray-90)
8
+ */
9
+ import React, { useState } from 'react';
10
+
11
+ export type ButtonStyle = 'filled' | 'outlined' | 'elevated' | 'ghost' | 'danger' | 'danger-outlined';
12
+ export type ButtonVariant = 'brand' | 'neutral';
13
+ export type ButtonSize = 'lg' | 'sm';
14
+ export type ButtonShape = 'rounded' | 'pill';
15
+
16
+ export interface ButtonProps {
17
+ style?: ButtonStyle;
18
+ variant?: ButtonVariant;
19
+ size?: ButtonSize;
20
+ shape?: ButtonShape;
21
+ block?: boolean;
22
+ disabled?: boolean;
23
+ loading?: boolean;
24
+ icon?: React.ReactNode;
25
+ iconRight?: React.ReactNode;
26
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
27
+ children?: React.ReactNode;
28
+ htmlType?: 'button' | 'submit' | 'reset';
29
+ className?: string;
30
+ css?: React.CSSProperties;
31
+ id?: string;
32
+ }
33
+
34
+ type Tokens = {
35
+ bg: string; bgHover: string; bgActive: string;
36
+ text: string; border: string; shadow: string;
37
+ };
38
+
39
+ const BRAND: Record<ButtonStyle, Tokens> = {
40
+ filled: {
41
+ bg: 'var(--vetc-color-brand)',
42
+ bgHover: 'var(--vetc-color-brand-hover)',
43
+ bgActive: 'var(--vetc-color-brand-active)',
44
+ text: 'var(--vetc-white)',
45
+ border: 'transparent',
46
+ shadow: 'none',
47
+ },
48
+ outlined: {
49
+ bg: 'transparent',
50
+ bgHover: 'var(--vetc-green-02)',
51
+ bgActive: 'var(--vetc-green-05)',
52
+ text: 'var(--vetc-color-brand)',
53
+ border: 'var(--vetc-color-brand)',
54
+ shadow: 'none',
55
+ },
56
+ elevated: {
57
+ bg: 'var(--vetc-color-bg)',
58
+ bgHover: 'var(--vetc-color-bg-hover)',
59
+ bgActive: 'var(--vetc-color-bg-pressed)',
60
+ text: 'var(--vetc-color-brand)',
61
+ border: 'transparent',
62
+ shadow: 'var(--vetc-shadow-md)',
63
+ },
64
+ ghost: {
65
+ bg: 'transparent',
66
+ bgHover: 'var(--vetc-color-bg-hover)',
67
+ bgActive: 'var(--vetc-color-bg-pressed)',
68
+ text: 'var(--vetc-color-brand)',
69
+ border: 'transparent',
70
+ shadow: 'none',
71
+ },
72
+ danger: {
73
+ bg: 'var(--vetc-color-negative)',
74
+ bgHover: 'var(--vetc-red-60)',
75
+ bgActive: 'var(--vetc-red-60)',
76
+ text: 'var(--vetc-white)',
77
+ border: 'transparent',
78
+ shadow: 'none',
79
+ },
80
+ 'danger-outlined': {
81
+ bg: 'transparent',
82
+ bgHover: 'var(--vetc-red-02)',
83
+ bgActive: 'var(--vetc-red-05)',
84
+ text: 'var(--vetc-color-negative)',
85
+ border: 'var(--vetc-color-negative)',
86
+ shadow: 'none',
87
+ },
88
+ };
89
+
90
+ const NEUTRAL: Record<ButtonStyle, Tokens> = {
91
+ filled: {
92
+ bg: 'var(--vetc-gray-90)',
93
+ bgHover: 'var(--vetc-gray-80)',
94
+ bgActive: 'var(--vetc-gray-70)',
95
+ text: 'var(--vetc-white)',
96
+ border: 'transparent',
97
+ shadow: 'none',
98
+ },
99
+ outlined: {
100
+ bg: 'transparent',
101
+ bgHover: 'var(--vetc-gray-05)',
102
+ bgActive: 'var(--vetc-gray-10)',
103
+ text: 'var(--vetc-gray-90)',
104
+ border: 'var(--vetc-gray-90)',
105
+ shadow: 'none',
106
+ },
107
+ elevated: {
108
+ bg: 'var(--vetc-color-bg)',
109
+ bgHover: 'var(--vetc-color-bg-hover)',
110
+ bgActive: 'var(--vetc-color-bg-pressed)',
111
+ text: 'var(--vetc-color-text-primary)',
112
+ border: 'transparent',
113
+ shadow: 'var(--vetc-shadow-md)',
114
+ },
115
+ ghost: {
116
+ bg: 'transparent',
117
+ bgHover: 'var(--vetc-color-bg-hover)',
118
+ bgActive: 'var(--vetc-color-bg-pressed)',
119
+ text: 'var(--vetc-color-text-primary)',
120
+ border: 'transparent',
121
+ shadow: 'none',
122
+ },
123
+ // danger variants are semantic — variant (brand/neutral) has no effect
124
+ danger: {
125
+ bg: 'var(--vetc-color-negative)',
126
+ bgHover: 'var(--vetc-red-60)',
127
+ bgActive: 'var(--vetc-red-60)',
128
+ text: 'var(--vetc-white)',
129
+ border: 'transparent',
130
+ shadow: 'none',
131
+ },
132
+ 'danger-outlined': {
133
+ bg: 'transparent',
134
+ bgHover: 'var(--vetc-red-02)',
135
+ bgActive: 'var(--vetc-red-05)',
136
+ text: 'var(--vetc-color-negative)',
137
+ border: 'var(--vetc-color-negative)',
138
+ shadow: 'none',
139
+ },
140
+ };
141
+
142
+ const LoadingSpinner = ({ color }: { color: string }) => (
143
+ <svg
144
+ width="16" height="16" viewBox="0 0 16 16" fill="none"
145
+ style={{ animation: 'vetc-btn-spin 0.6s linear infinite', flexShrink: 0 }}
146
+ aria-hidden="true"
147
+ >
148
+ <circle cx="8" cy="8" r="6" stroke={color} strokeOpacity="0.25" strokeWidth="2" />
149
+ <path d="M8 2a6 6 0 016 6" stroke={color} strokeWidth="2" strokeLinecap="round" />
150
+ </svg>
151
+ );
152
+
153
+ export function Button({
154
+ style = 'filled',
155
+ variant = 'brand',
156
+ size = 'lg',
157
+ shape = 'rounded',
158
+ block = false,
159
+ disabled = false,
160
+ loading = false,
161
+ icon,
162
+ iconRight,
163
+ onClick,
164
+ children,
165
+ htmlType = 'button',
166
+ className = '',
167
+ css,
168
+ id,
169
+ }: ButtonProps) {
170
+ const [pressed, setPressed] = useState(false);
171
+ const [hovered, setHovered] = useState(false);
172
+
173
+ const tokens = (variant === 'neutral' ? NEUTRAL : BRAND)[style];
174
+ const isDisabled = disabled || loading;
175
+
176
+ const height = size === 'lg' ? 'var(--vetc-btn-height-lg)' : 'var(--vetc-btn-height-sm)';
177
+ const fontSize = size === 'lg' ? 'var(--vetc-btn-font-size-lg)' : 'var(--vetc-btn-font-size-sm)';
178
+ const paddingX = size === 'lg' ? 'var(--vetc-btn-padding-x-lg)' : 'var(--vetc-btn-padding-x-sm)';
179
+ const radius = shape === 'pill' ? 'var(--vetc-btn-radius-pill)'
180
+ : size === 'lg' ? 'var(--vetc-btn-radius-lg)'
181
+ : 'var(--vetc-btn-radius-sm)';
182
+
183
+ let bg = tokens.bg;
184
+ let border = tokens.border;
185
+ let shadow = tokens.shadow;
186
+
187
+ if (isDisabled) {
188
+ bg = 'var(--vetc-btn-disabled-bg)';
189
+ border = 'var(--vetc-btn-disabled-border)';
190
+ shadow = 'none';
191
+ } else if (pressed) {
192
+ bg = tokens.bgActive;
193
+ } else if (hovered) {
194
+ bg = tokens.bgHover;
195
+ }
196
+
197
+ const textColor = isDisabled ? 'var(--vetc-btn-disabled-text)' : tokens.text;
198
+ const spinnerColor = isDisabled ? 'var(--vetc-gray-30)' : tokens.text;
199
+
200
+ return (
201
+ <>
202
+ <button
203
+ id={id}
204
+ type={htmlType}
205
+ disabled={isDisabled}
206
+ onClick={onClick}
207
+ onMouseEnter={() => setHovered(true)}
208
+ onMouseLeave={() => { setHovered(false); setPressed(false); }}
209
+ onMouseDown={() => setPressed(true)}
210
+ onMouseUp={() => setPressed(false)}
211
+ className={`vetc-btn vetc-btn--${style} vetc-btn--${variant} vetc-btn--${size} ${className}`}
212
+ style={{
213
+ display: 'inline-flex',
214
+ alignItems: 'center',
215
+ justifyContent: 'center',
216
+ gap: 'var(--vetc-space-8)',
217
+ height,
218
+ width: block ? '100%' : undefined,
219
+ paddingInline: paddingX,
220
+ borderRadius: radius,
221
+ border: `1px solid ${border}`,
222
+ backgroundColor: bg,
223
+ boxShadow: shadow,
224
+ color: textColor,
225
+ fontSize,
226
+ fontWeight: 'var(--vetc-btn-font-weight)' as any,
227
+ fontFamily: 'var(--vetc-font-family)',
228
+ lineHeight: 'var(--vetc-line-height-relaxed)',
229
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
230
+ transition: 'background-color var(--vetc-transition-fast), box-shadow var(--vetc-transition-fast)',
231
+ userSelect: 'none',
232
+ outline: 'none',
233
+ whiteSpace: 'nowrap',
234
+ ...css,
235
+ }}
236
+ >
237
+ {loading ? <LoadingSpinner color={spinnerColor} /> : icon && <span style={{ display: 'inline-flex', flexShrink: 0 }}>{icon}</span>}
238
+ {children && <span>{children}</span>}
239
+ {iconRight && !loading && <span style={{ display: 'inline-flex', flexShrink: 0 }}>{iconRight}</span>}
240
+ </button>
241
+ <style>{`@keyframes vetc-btn-spin { to { transform: rotate(360deg); } }`}</style>
242
+ </>
243
+ );
244
+ }
245
+
246
+ export default Button;
@@ -0,0 +1,2 @@
1
+ export { Button } from './Button';
2
+ export type { ButtonProps, ButtonStyle, ButtonVariant, ButtonSize, ButtonShape } from './Button';
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import { Button } from '../button';
3
+
4
+ export type ButtonGroupLayout = 'inline' | 'stacked';
5
+ export type ButtonGroupVariant = 'bounding' | 'none';
6
+
7
+ export interface ButtonGroupAction {
8
+ label: React.ReactNode;
9
+ onClick?: () => void;
10
+ loading?: boolean;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ export interface ButtonGroupProps {
15
+ /** Primary action — brand filled, rightmost (inline) or bottom (stacked). Required. */
16
+ primary: ButtonGroupAction & {
17
+ /** danger = red filled style */
18
+ danger?: boolean;
19
+ /** Override primary button color. Default: brand */
20
+ variant?: 'brand' | 'neutral';
21
+ };
22
+ /** Secondary actions — neutral outlined. Max 2. */
23
+ secondary?: ButtonGroupAction[];
24
+ /** Optional content rendered above the button row */
25
+ content?: React.ReactNode;
26
+ /** inline: buttons side by side | stacked: buttons stacked vertically */
27
+ layout?: ButtonGroupLayout;
28
+ /** bounding: top border + background | none: no container styling */
29
+ variant?: ButtonGroupVariant;
30
+ }
31
+
32
+ export function ButtonGroup({
33
+ primary,
34
+ secondary = [],
35
+ content,
36
+ layout = 'inline',
37
+ variant = 'none',
38
+ }: ButtonGroupProps) {
39
+ const isInline = layout === 'inline';
40
+ const isBounding = variant === 'bounding';
41
+
42
+ const containerStyle: React.CSSProperties = isBounding
43
+ ? {
44
+ backgroundColor: 'var(--vetc-btn-group-bg)',
45
+ borderTop: `1px solid var(--vetc-btn-group-border-color)`,
46
+ padding: 'var(--vetc-btn-group-padding)',
47
+ fontFamily: 'var(--vetc-font-family)',
48
+ }
49
+ : { fontFamily: 'var(--vetc-font-family)' };
50
+
51
+ const rowStyle: React.CSSProperties = isInline
52
+ ? { display: 'flex', gap: 'var(--vetc-btn-group-gap)', alignItems: 'stretch' }
53
+ : { display: 'flex', flexDirection: 'column', gap: 'var(--vetc-btn-group-gap)' };
54
+
55
+ const secondaryBtns = secondary.map((action, i) => (
56
+ <Button
57
+ key={i}
58
+ style="outlined"
59
+ variant="neutral"
60
+ block={!isInline}
61
+ loading={action.loading}
62
+ disabled={action.disabled}
63
+ onClick={action.onClick}
64
+ css={isInline ? { flex: 1 } : undefined}
65
+ >
66
+ {action.label}
67
+ </Button>
68
+ ));
69
+
70
+ const primaryBtn = (
71
+ <Button
72
+ style={primary.danger ? 'danger' : 'filled'}
73
+ variant={primary.variant ?? 'brand'}
74
+ block={!isInline}
75
+ loading={primary.loading}
76
+ disabled={primary.disabled}
77
+ onClick={primary.onClick}
78
+ css={isInline ? { flex: 1 } : undefined}
79
+ >
80
+ {primary.label}
81
+ </Button>
82
+ );
83
+
84
+ return (
85
+ <div style={containerStyle}>
86
+ {content && (
87
+ <div style={{ marginBottom: 'var(--vetc-btn-group-padding)' }}>
88
+ {content}
89
+ </div>
90
+ )}
91
+ <div style={rowStyle}>
92
+ {isInline ? (
93
+ <>
94
+ {secondaryBtns}
95
+ {primaryBtn}
96
+ </>
97
+ ) : (
98
+ <>
99
+ {primaryBtn}
100
+ {secondaryBtns}
101
+ </>
102
+ )}
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ export default ButtonGroup;
@@ -0,0 +1,2 @@
1
+ export { ButtonGroup } from './ButtonGroup';
2
+ export type { ButtonGroupProps, ButtonGroupAction, ButtonGroupLayout, ButtonGroupVariant } from './ButtonGroup';
@@ -0,0 +1,77 @@
1
+ /**
2
+ * VETC Card — tokenized
3
+ */
4
+ import React from 'react';
5
+ import { Card as AntCard } from 'antd';
6
+
7
+ export type CardElevation = 'flat' | 'raised' | 'outlined';
8
+
9
+ export interface CardProps {
10
+ elevation?: CardElevation;
11
+ title?: React.ReactNode;
12
+ extra?: React.ReactNode;
13
+ children?: React.ReactNode;
14
+ onClick?: () => void;
15
+ className?: string;
16
+ style?: React.CSSProperties;
17
+ bodyStyle?: React.CSSProperties;
18
+ id?: string;
19
+ }
20
+
21
+ export function Card({
22
+ elevation = 'outlined',
23
+ title,
24
+ extra,
25
+ children,
26
+ onClick,
27
+ className = '',
28
+ style,
29
+ bodyStyle,
30
+ id,
31
+ }: CardProps) {
32
+ const shadowMap: Record<CardElevation, string> = {
33
+ flat: 'var(--vetc-shadow-none)',
34
+ raised: 'var(--vetc-card-shadow-raised)',
35
+ outlined: 'var(--vetc-shadow-none)',
36
+ };
37
+ const borderMap: Record<CardElevation, string> = {
38
+ flat: 'none',
39
+ raised: 'none',
40
+ outlined: `1px solid var(--vetc-card-border)`,
41
+ };
42
+
43
+ return (
44
+ <AntCard
45
+ id={id}
46
+ title={title}
47
+ extra={extra}
48
+ onClick={onClick}
49
+ className={`vetc-card vetc-card--${elevation} ${className}`}
50
+ styles={{
51
+ body: {
52
+ padding: 'var(--vetc-card-padding)',
53
+ fontFamily: 'var(--vetc-font-family)',
54
+ ...bodyStyle,
55
+ },
56
+ header: title
57
+ ? { borderBottom: `1px solid var(--vetc-card-border)`, padding: `var(--vetc-space-12) var(--vetc-card-padding)` }
58
+ : undefined,
59
+ }}
60
+ style={{
61
+ borderRadius: 'var(--vetc-card-radius)',
62
+ border: borderMap[elevation],
63
+ boxShadow: shadowMap[elevation],
64
+ cursor: onClick ? 'pointer' : 'default',
65
+ overflow: 'hidden',
66
+ backgroundColor: 'var(--vetc-card-bg)',
67
+ fontFamily: 'var(--vetc-font-family)',
68
+ transition: `box-shadow var(--vetc-transition-fast)`,
69
+ ...style,
70
+ }}
71
+ >
72
+ {children}
73
+ </AntCard>
74
+ );
75
+ }
76
+
77
+ export default Card;
@@ -0,0 +1,2 @@
1
+ export { Card } from './Card';
2
+ export type { CardProps, CardElevation } from './Card';