@vetc-miniapp/ui-react 0.0.22 → 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.
- package/README.md +375 -56
- package/package.json +17 -5
- package/src/dist/ui-react/index.js +1 -1
- package/src/ui-react/components/avatar/Avatar.tsx +88 -0
- package/src/ui-react/components/avatar/index.ts +2 -0
- package/src/ui-react/components/bottom-sheet/BottomSheet.tsx +149 -0
- package/src/ui-react/components/bottom-sheet/index.ts +2 -0
- package/src/ui-react/components/button/Button.tsx +246 -0
- package/src/ui-react/components/button/index.ts +2 -0
- package/src/ui-react/components/button-group/ButtonGroup.tsx +108 -0
- package/src/ui-react/components/button-group/index.ts +2 -0
- package/src/ui-react/components/card/Card.tsx +77 -0
- package/src/ui-react/components/card/index.ts +2 -0
- package/src/ui-react/components/checkbox/Checkbox.tsx +232 -0
- package/src/ui-react/components/checkbox/index.ts +2 -0
- package/src/ui-react/components/chip/Chip.tsx +137 -0
- package/src/ui-react/components/chip/index.ts +2 -0
- package/src/ui-react/components/dialog/Dialog.tsx +135 -0
- package/src/ui-react/components/dialog/index.ts +2 -0
- package/src/ui-react/components/divider/Divider.tsx +54 -0
- package/src/ui-react/components/divider/index.ts +2 -0
- package/src/ui-react/components/input/Input.tsx +195 -0
- package/src/ui-react/components/input/index.ts +2 -0
- package/src/ui-react/components/list/List.tsx +180 -0
- package/src/ui-react/components/list/index.ts +2 -0
- package/src/ui-react/components/loading/Loading.tsx +121 -0
- package/src/ui-react/components/loading/index.ts +2 -0
- package/src/ui-react/components/modal/Modal.tsx +116 -0
- package/src/ui-react/components/modal/index.ts +2 -0
- package/src/ui-react/components/navigation-bar/NavigationBar.tsx +188 -0
- package/src/ui-react/components/navigation-bar/index.ts +2 -0
- package/src/ui-react/components/radio/Radio.tsx +216 -0
- package/src/ui-react/components/radio/index.ts +2 -0
- package/src/ui-react/components/select/Select.tsx +109 -0
- package/src/ui-react/components/select/index.ts +2 -0
- package/src/ui-react/components/switch/Switch.tsx +164 -0
- package/src/ui-react/components/switch/index.ts +2 -0
- package/src/ui-react/components/tab-bar/TabBar.tsx +137 -0
- package/src/ui-react/components/tab-bar/index.ts +2 -0
- package/src/ui-react/components/textarea/Textarea.tsx +109 -0
- package/src/ui-react/components/textarea/index.ts +2 -0
- package/src/ui-react/components/toast/Toast.ts +98 -0
- package/src/ui-react/components/toast/index.ts +2 -0
- package/src/ui-react/components/typography/Typography.tsx +201 -0
- package/src/ui-react/components/typography/index.ts +2 -0
- package/src/ui-react/hooks/use-did-show.js +1 -0
- package/src/ui-react/hooks/use-tap-app-bar.js +26 -0
- package/src/ui-react/hooks/use-tap-app-bar.ts +34 -0
- package/src/ui-react/index.js +1 -0
- package/src/ui-react/index.ts +79 -3
- package/src/ui-react/styles/VETCProvider.tsx +152 -0
- package/src/ui-react/styles/tokens.css +427 -0
- package/src/ui-react/tokens/colors.ts +91 -0
- package/src/ui-react/tokens/index.ts +3 -0
- package/src/ui-react/tokens/spacing.ts +59 -0
- package/src/ui-react/tokens/typography.ts +63 -0
- package/src/ui-react/tokens_vetc.json +1517 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VETC TabBar (Bottom Navigation)
|
|
3
|
+
* Figma: h=56px, label=12px/semibold, icon=24px
|
|
4
|
+
* variant: neutral (gray-90 active) | brand (green-40 active)
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
export interface TabBarItem {
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon: React.ReactNode;
|
|
12
|
+
activeIcon?: React.ReactNode;
|
|
13
|
+
badge?: number | string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TabBarVariant = 'neutral' | 'brand';
|
|
18
|
+
|
|
19
|
+
export interface TabBarProps {
|
|
20
|
+
items: TabBarItem[];
|
|
21
|
+
activeKey?: string;
|
|
22
|
+
onChange?: (key: string) => void;
|
|
23
|
+
variant?: TabBarVariant;
|
|
24
|
+
backgroundColor?: string;
|
|
25
|
+
divider?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
style?: React.CSSProperties;
|
|
28
|
+
id?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function TabBar({
|
|
32
|
+
items,
|
|
33
|
+
activeKey,
|
|
34
|
+
onChange,
|
|
35
|
+
variant = 'brand',
|
|
36
|
+
backgroundColor,
|
|
37
|
+
divider = true,
|
|
38
|
+
className = '',
|
|
39
|
+
style,
|
|
40
|
+
id,
|
|
41
|
+
}: TabBarProps) {
|
|
42
|
+
const activeColor = variant === 'brand'
|
|
43
|
+
? 'var(--vetc-tab-color-active)'
|
|
44
|
+
: 'var(--vetc-color-text-primary)';
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<nav
|
|
48
|
+
id={id}
|
|
49
|
+
className={`vetc-tab-bar ${className}`}
|
|
50
|
+
style={{
|
|
51
|
+
display: 'flex',
|
|
52
|
+
alignItems: 'stretch',
|
|
53
|
+
height: 'var(--vetc-tab-height)',
|
|
54
|
+
backgroundColor: backgroundColor ?? 'var(--vetc-tab-bg)',
|
|
55
|
+
borderTop: divider ? `1px solid var(--vetc-tab-border)` : 'none',
|
|
56
|
+
fontFamily: 'var(--vetc-font-family)',
|
|
57
|
+
...style,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{items.map((item) => {
|
|
61
|
+
const isActive = item.key === activeKey;
|
|
62
|
+
const color = item.disabled
|
|
63
|
+
? 'var(--vetc-tab-color-disabled)'
|
|
64
|
+
: isActive
|
|
65
|
+
? activeColor
|
|
66
|
+
: 'var(--vetc-tab-color-inactive)';
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
key={item.key}
|
|
71
|
+
disabled={item.disabled}
|
|
72
|
+
onClick={() => !item.disabled && onChange?.(item.key)}
|
|
73
|
+
aria-current={isActive ? 'page' : undefined}
|
|
74
|
+
aria-label={item.label}
|
|
75
|
+
className={`vetc-tab-bar__item${isActive ? ' vetc-tab-bar__item--active' : ''}`}
|
|
76
|
+
style={{
|
|
77
|
+
flex: 1,
|
|
78
|
+
display: 'flex',
|
|
79
|
+
flexDirection: 'column',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
gap: 'var(--vetc-space-2)',
|
|
83
|
+
background: 'none',
|
|
84
|
+
border: 'none',
|
|
85
|
+
cursor: item.disabled ? 'not-allowed' : 'pointer',
|
|
86
|
+
padding: `var(--vetc-space-6) 0`,
|
|
87
|
+
position: 'relative',
|
|
88
|
+
opacity: item.disabled ? 0.5 : 1,
|
|
89
|
+
transition: `color var(--vetc-transition-fast)`,
|
|
90
|
+
color,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{/* Icon + Badge */}
|
|
94
|
+
<div style={{ position: 'relative', display: 'inline-flex' }}>
|
|
95
|
+
<span style={{ color, display: 'flex', fontSize: '24px', lineHeight: 1 }}>
|
|
96
|
+
{isActive && item.activeIcon ? item.activeIcon : item.icon}
|
|
97
|
+
</span>
|
|
98
|
+
{item.badge !== undefined && (
|
|
99
|
+
<span style={{
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
top: 'calc(-1 * var(--vetc-space-4))',
|
|
102
|
+
right: 'calc(-1 * var(--vetc-space-8))',
|
|
103
|
+
minWidth: 'var(--vetc-tab-badge-size)',
|
|
104
|
+
height: 'var(--vetc-tab-badge-size)',
|
|
105
|
+
backgroundColor: 'var(--vetc-tab-badge-bg)',
|
|
106
|
+
borderRadius: 'var(--vetc-radius-pill)',
|
|
107
|
+
fontSize: 'var(--vetc-tab-badge-font-size)',
|
|
108
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
109
|
+
color: 'var(--vetc-tab-badge-text)',
|
|
110
|
+
display: 'flex',
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
padding: `0 var(--vetc-space-4)`,
|
|
114
|
+
lineHeight: 1,
|
|
115
|
+
}}>
|
|
116
|
+
{item.badge}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Label */}
|
|
122
|
+
<span style={{
|
|
123
|
+
fontSize: 'var(--vetc-tab-label-size)',
|
|
124
|
+
fontWeight: 'var(--vetc-tab-label-weight)' as any,
|
|
125
|
+
color,
|
|
126
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
127
|
+
}}>
|
|
128
|
+
{item.label}
|
|
129
|
+
</span>
|
|
130
|
+
</button>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</nav>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default TabBar;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VETC Textarea — tokenized
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Input as AntInput } from 'antd';
|
|
6
|
+
|
|
7
|
+
const { TextArea: AntTextArea } = AntInput;
|
|
8
|
+
|
|
9
|
+
function FieldLabel({ htmlFor, required, disabled, children }: {
|
|
10
|
+
htmlFor?: string; required?: boolean; disabled?: boolean; children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<label htmlFor={htmlFor} style={{
|
|
14
|
+
fontFamily: 'var(--vetc-font-family)',
|
|
15
|
+
fontSize: 'var(--vetc-input-label-font-size)',
|
|
16
|
+
fontWeight: 'var(--vetc-input-label-font-weight)' as any,
|
|
17
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
18
|
+
color: disabled ? 'var(--vetc-color-text-disabled)' : 'var(--vetc-input-label-color)',
|
|
19
|
+
display: 'block',
|
|
20
|
+
}}>
|
|
21
|
+
{children}
|
|
22
|
+
{required && <span style={{ color: 'var(--vetc-color-negative)', marginLeft: 'var(--vetc-space-2)' }}>*</span>}
|
|
23
|
+
</label>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TextareaProps {
|
|
28
|
+
label?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
value?: string;
|
|
31
|
+
defaultValue?: string;
|
|
32
|
+
helperText?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
maxLength?: number;
|
|
37
|
+
showCount?: boolean;
|
|
38
|
+
rows?: number;
|
|
39
|
+
autoSize?: boolean | { minRows?: number; maxRows?: number };
|
|
40
|
+
onChange?: (value: string) => void;
|
|
41
|
+
onFocus?: React.FocusEventHandler<HTMLTextAreaElement>;
|
|
42
|
+
onBlur?: React.FocusEventHandler<HTMLTextAreaElement>;
|
|
43
|
+
id?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
style?: React.CSSProperties;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function Textarea({
|
|
51
|
+
label, placeholder, value, defaultValue,
|
|
52
|
+
helperText, error, disabled = false, readOnly = false,
|
|
53
|
+
maxLength, showCount = false, rows = 4, autoSize,
|
|
54
|
+
onChange, onFocus, onBlur,
|
|
55
|
+
id, name, className = '', style, required = false,
|
|
56
|
+
}: TextareaProps) {
|
|
57
|
+
const hasError = !!error;
|
|
58
|
+
const helperMsg = error ?? helperText;
|
|
59
|
+
const helperColor = hasError
|
|
60
|
+
? 'var(--vetc-input-helper-color-error)'
|
|
61
|
+
: 'var(--vetc-input-helper-color)';
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={`vetc-textarea-wrapper ${className}`}
|
|
66
|
+
style={{ display: 'flex', flexDirection: 'column', gap: 'var(--vetc-input-gap)', ...style }}
|
|
67
|
+
>
|
|
68
|
+
{label && <FieldLabel htmlFor={id} required={required} disabled={disabled}>{label}</FieldLabel>}
|
|
69
|
+
|
|
70
|
+
<AntTextArea
|
|
71
|
+
id={id}
|
|
72
|
+
name={name}
|
|
73
|
+
value={value}
|
|
74
|
+
defaultValue={defaultValue}
|
|
75
|
+
placeholder={placeholder}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
readOnly={readOnly}
|
|
78
|
+
maxLength={maxLength}
|
|
79
|
+
showCount={showCount}
|
|
80
|
+
rows={rows}
|
|
81
|
+
autoSize={autoSize}
|
|
82
|
+
status={hasError ? 'error' : undefined}
|
|
83
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
84
|
+
onFocus={onFocus}
|
|
85
|
+
onBlur={onBlur}
|
|
86
|
+
style={{
|
|
87
|
+
borderRadius: 'var(--vetc-input-radius)',
|
|
88
|
+
padding: 'var(--vetc-input-padding-x)',
|
|
89
|
+
fontSize: 'var(--vetc-input-font-size)',
|
|
90
|
+
fontFamily: 'var(--vetc-font-family)',
|
|
91
|
+
resize: 'vertical',
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
{helperMsg && (
|
|
96
|
+
<span style={{
|
|
97
|
+
fontFamily: 'var(--vetc-font-family)',
|
|
98
|
+
fontSize: 'var(--vetc-input-helper-font-size)',
|
|
99
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
100
|
+
color: helperColor,
|
|
101
|
+
}}>
|
|
102
|
+
{helperMsg}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default Textarea;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VETC Toast — tokenized
|
|
3
|
+
* Figma: Toast page (7 variants)
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { notification, message } from 'antd';
|
|
7
|
+
|
|
8
|
+
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info';
|
|
9
|
+
|
|
10
|
+
export interface ToastOptions {
|
|
11
|
+
message: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
type?: ToastType;
|
|
14
|
+
placement?: 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
|
|
15
|
+
/** Duration in seconds (0 = persistent) */
|
|
16
|
+
duration?: number;
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Shared notification style — references CSS vars */
|
|
22
|
+
const toastStyle: React.CSSProperties = {
|
|
23
|
+
borderRadius: 'var(--vetc-toast-radius)',
|
|
24
|
+
fontFamily: 'var(--vetc-font-family)',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Static programmatic toast API
|
|
29
|
+
* Usage: toast.success('Message')
|
|
30
|
+
*/
|
|
31
|
+
export const toast = {
|
|
32
|
+
show(opts: ToastOptions) {
|
|
33
|
+
const {
|
|
34
|
+
type = 'default', message: msg, description,
|
|
35
|
+
duration = 3, placement = 'top', icon, onClose,
|
|
36
|
+
} = opts;
|
|
37
|
+
|
|
38
|
+
if (type === 'default') {
|
|
39
|
+
message.open({ type: 'info', content: msg, duration, onClose });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
notification[type]({
|
|
44
|
+
message: msg,
|
|
45
|
+
description,
|
|
46
|
+
duration,
|
|
47
|
+
placement,
|
|
48
|
+
icon,
|
|
49
|
+
onClose,
|
|
50
|
+
style: toastStyle,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
success: (msg: string, description?: string) =>
|
|
54
|
+
toast.show({ message: msg, description, type: 'success' }),
|
|
55
|
+
error: (msg: string, description?: string) =>
|
|
56
|
+
toast.show({ message: msg, description, type: 'error' }),
|
|
57
|
+
warning: (msg: string, description?: string) =>
|
|
58
|
+
toast.show({ message: msg, description, type: 'warning' }),
|
|
59
|
+
info: (msg: string, description?: string) =>
|
|
60
|
+
toast.show({ message: msg, description, type: 'info' }),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hook-based toast — requires VETCProvider (antd App wrapper)
|
|
65
|
+
*/
|
|
66
|
+
export function useToast() {
|
|
67
|
+
const [api, contextHolder] = notification.useNotification();
|
|
68
|
+
|
|
69
|
+
const show = (opts: ToastOptions) => {
|
|
70
|
+
const {
|
|
71
|
+
type = 'info', message: msg, description,
|
|
72
|
+
duration = 3, placement = 'top', icon, onClose,
|
|
73
|
+
} = opts;
|
|
74
|
+
|
|
75
|
+
api[type === 'default' ? 'info' : type]({
|
|
76
|
+
message: msg,
|
|
77
|
+
description,
|
|
78
|
+
duration,
|
|
79
|
+
placement,
|
|
80
|
+
icon,
|
|
81
|
+
onClose,
|
|
82
|
+
style: toastStyle,
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
contextHolder,
|
|
88
|
+
toast: {
|
|
89
|
+
show,
|
|
90
|
+
success: (msg: string, description?: string) => show({ message: msg, description, type: 'success' }),
|
|
91
|
+
error: (msg: string, description?: string) => show({ message: msg, description, type: 'error' }),
|
|
92
|
+
warning: (msg: string, description?: string) => show({ message: msg, description, type: 'warning' }),
|
|
93
|
+
info: (msg: string, description?: string) => show({ message: msg, description, type: 'info' }),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default toast;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VETC Typography Components
|
|
3
|
+
* All style values reference CSS custom properties from tokens.css
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
// ── Typography style map — dùng CSS vars thay vì hardcode ──────────────────
|
|
8
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
9
|
+
display4xl: {
|
|
10
|
+
fontSize: 'var(--vetc-font-size-4xl)',
|
|
11
|
+
fontWeight: 'var(--vetc-font-weight-bold)' as any,
|
|
12
|
+
lineHeight: 'var(--vetc-line-height-tight)',
|
|
13
|
+
},
|
|
14
|
+
display3xl: {
|
|
15
|
+
fontSize: 'var(--vetc-font-size-3xl)',
|
|
16
|
+
fontWeight: 'var(--vetc-font-weight-bold)' as any,
|
|
17
|
+
lineHeight: 'var(--vetc-line-height-tight)',
|
|
18
|
+
},
|
|
19
|
+
display2xl: {
|
|
20
|
+
fontSize: 'var(--vetc-font-size-2xl)',
|
|
21
|
+
fontWeight: 'var(--vetc-font-weight-bold)' as any,
|
|
22
|
+
lineHeight: 'var(--vetc-line-height-tight)',
|
|
23
|
+
},
|
|
24
|
+
headlineXl: {
|
|
25
|
+
fontSize: 'var(--vetc-font-size-xl)',
|
|
26
|
+
fontWeight: 'var(--vetc-font-weight-bold)' as any,
|
|
27
|
+
lineHeight: 'var(--vetc-line-height-normal)',
|
|
28
|
+
},
|
|
29
|
+
headlineLg: {
|
|
30
|
+
fontSize: 'var(--vetc-font-size-lg)',
|
|
31
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
32
|
+
lineHeight: 'var(--vetc-line-height-normal)',
|
|
33
|
+
},
|
|
34
|
+
titleBase: {
|
|
35
|
+
fontSize: 'var(--vetc-font-size-base)',
|
|
36
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
37
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
38
|
+
letterSpacing: 'var(--vetc-letter-spacing-sm)',
|
|
39
|
+
},
|
|
40
|
+
titleSm: {
|
|
41
|
+
fontSize: 'var(--vetc-font-size-sm)',
|
|
42
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
43
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
44
|
+
letterSpacing: 'var(--vetc-letter-spacing-sm)',
|
|
45
|
+
},
|
|
46
|
+
labelBase: {
|
|
47
|
+
fontSize: 'var(--vetc-font-size-base)',
|
|
48
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
49
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
50
|
+
},
|
|
51
|
+
labelSm: {
|
|
52
|
+
fontSize: 'var(--vetc-font-size-sm)',
|
|
53
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
54
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
55
|
+
letterSpacing: 'var(--vetc-letter-spacing-lg)',
|
|
56
|
+
},
|
|
57
|
+
labelXs: {
|
|
58
|
+
fontSize: 'var(--vetc-font-size-xs)',
|
|
59
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
60
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
61
|
+
letterSpacing: 'var(--vetc-letter-spacing-lg)',
|
|
62
|
+
},
|
|
63
|
+
labelXxs: {
|
|
64
|
+
fontSize: 'var(--vetc-font-size-2xs)',
|
|
65
|
+
fontWeight: 'var(--vetc-font-weight-semibold)' as any,
|
|
66
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
67
|
+
letterSpacing: 'var(--vetc-letter-spacing-lg)',
|
|
68
|
+
},
|
|
69
|
+
bodyBase: {
|
|
70
|
+
fontSize: 'var(--vetc-font-size-base)',
|
|
71
|
+
fontWeight: 'var(--vetc-font-weight-regular)' as any,
|
|
72
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
73
|
+
letterSpacing: 'var(--vetc-letter-spacing-sm)',
|
|
74
|
+
},
|
|
75
|
+
bodySm: {
|
|
76
|
+
fontSize: 'var(--vetc-font-size-sm)',
|
|
77
|
+
fontWeight: 'var(--vetc-font-weight-regular)' as any,
|
|
78
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
79
|
+
letterSpacing: 'var(--vetc-letter-spacing-md)',
|
|
80
|
+
},
|
|
81
|
+
bodyXs: {
|
|
82
|
+
fontSize: 'var(--vetc-font-size-xs)',
|
|
83
|
+
fontWeight: 'var(--vetc-font-weight-regular)' as any,
|
|
84
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
85
|
+
letterSpacing: 'var(--vetc-letter-spacing-lg)',
|
|
86
|
+
},
|
|
87
|
+
body2xs: {
|
|
88
|
+
fontSize: 'var(--vetc-font-size-2xs)',
|
|
89
|
+
fontWeight: 'var(--vetc-font-weight-regular)' as any,
|
|
90
|
+
lineHeight: 'var(--vetc-line-height-relaxed)',
|
|
91
|
+
letterSpacing: 'var(--vetc-letter-spacing-lg)',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type TypographyVariant = keyof typeof styles;
|
|
96
|
+
export type TextColor =
|
|
97
|
+
| 'primary' | 'secondary' | 'tertiary' | 'disabled'
|
|
98
|
+
| 'brand' | 'error' | 'warning' | 'success' | 'inherit';
|
|
99
|
+
|
|
100
|
+
const colorMap: Record<TextColor, string> = {
|
|
101
|
+
primary: 'var(--vetc-color-text-primary)',
|
|
102
|
+
secondary: 'var(--vetc-color-text-secondary)',
|
|
103
|
+
tertiary: 'var(--vetc-color-text-tertiary)',
|
|
104
|
+
disabled: 'var(--vetc-color-text-disabled)',
|
|
105
|
+
brand: 'var(--vetc-color-brand)',
|
|
106
|
+
error: 'var(--vetc-color-text-error)',
|
|
107
|
+
warning: 'var(--vetc-color-warning)',
|
|
108
|
+
success: 'var(--vetc-color-positive)',
|
|
109
|
+
inherit: 'inherit',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export interface TypographyProps {
|
|
113
|
+
/** Typography style variant */
|
|
114
|
+
variant?: TypographyVariant;
|
|
115
|
+
/** Text color */
|
|
116
|
+
color?: TextColor;
|
|
117
|
+
/** Strike-through */
|
|
118
|
+
strikethrough?: boolean;
|
|
119
|
+
/** Underline */
|
|
120
|
+
underline?: boolean;
|
|
121
|
+
/** Single-line truncate */
|
|
122
|
+
truncate?: boolean;
|
|
123
|
+
/** Clamp to N lines */
|
|
124
|
+
lines?: number;
|
|
125
|
+
/** Render tag */
|
|
126
|
+
as?: keyof React.JSX.IntrinsicElements;
|
|
127
|
+
children?: React.ReactNode;
|
|
128
|
+
className?: string;
|
|
129
|
+
style?: React.CSSProperties;
|
|
130
|
+
id?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function Typography({
|
|
134
|
+
variant = 'bodyBase',
|
|
135
|
+
color = 'primary',
|
|
136
|
+
strikethrough = false,
|
|
137
|
+
underline = false,
|
|
138
|
+
truncate = false,
|
|
139
|
+
lines,
|
|
140
|
+
as: Tag = 'span',
|
|
141
|
+
children,
|
|
142
|
+
className = '',
|
|
143
|
+
style,
|
|
144
|
+
id,
|
|
145
|
+
}: TypographyProps) {
|
|
146
|
+
const variantStyle = styles[variant] ?? styles.bodyBase;
|
|
147
|
+
const colorValue = colorMap[color];
|
|
148
|
+
|
|
149
|
+
const combinedStyle: React.CSSProperties = {
|
|
150
|
+
fontFamily: 'var(--vetc-font-family)',
|
|
151
|
+
margin: 0,
|
|
152
|
+
padding: 0,
|
|
153
|
+
...variantStyle,
|
|
154
|
+
color: colorValue,
|
|
155
|
+
textDecoration: strikethrough ? 'line-through' : underline ? 'underline' : 'none',
|
|
156
|
+
...(truncate && !lines
|
|
157
|
+
? { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
|
|
158
|
+
: {}),
|
|
159
|
+
...(lines
|
|
160
|
+
? {
|
|
161
|
+
display: '-webkit-box',
|
|
162
|
+
WebkitLineClamp: lines,
|
|
163
|
+
WebkitBoxOrient: 'vertical' as const,
|
|
164
|
+
overflow: 'hidden',
|
|
165
|
+
}
|
|
166
|
+
: {}),
|
|
167
|
+
...style,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Tag id={id} className={`vetc-text vetc-text--${variant} ${className}`} style={combinedStyle}>
|
|
172
|
+
{children}
|
|
173
|
+
</Tag>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Convenience shorthands ──────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export function Display({ level = '2xl', ...props }: Omit<TypographyProps, 'variant'> & { level?: '4xl' | '3xl' | '2xl' }) {
|
|
180
|
+
return <Typography {...props} variant={`display${level}` as TypographyVariant} as={props.as ?? 'h1'} />;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function Headline({ level = 'xl', ...props }: Omit<TypographyProps, 'variant'> & { level?: 'xl' | 'lg' }) {
|
|
184
|
+
return <Typography {...props} variant={level === 'xl' ? 'headlineXl' : 'headlineLg'} as={props.as ?? 'h2'} />;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function Title({ size = 'base', ...props }: Omit<TypographyProps, 'variant'> & { size?: 'base' | 'sm' }) {
|
|
188
|
+
return <Typography {...props} variant={size === 'base' ? 'titleBase' : 'titleSm'} as={props.as ?? 'h3'} />;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function Label({ size = 'base', ...props }: Omit<TypographyProps, 'variant'> & { size?: 'base' | 'sm' | 'xs' | 'xxs' }) {
|
|
192
|
+
const variantMap = { base: 'labelBase', sm: 'labelSm', xs: 'labelXs', xxs: 'labelXxs' } as const;
|
|
193
|
+
return <Typography {...props} variant={variantMap[size]} as={props.as ?? 'span'} />;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function Body({ size = 'base', ...props }: Omit<TypographyProps, 'variant'> & { size?: 'base' | 'sm' | 'xs' | '2xs' }) {
|
|
197
|
+
const variantMap = { base: 'bodyBase', sm: 'bodySm', xs: 'bodyXs', '2xs': 'body2xs' } as const;
|
|
198
|
+
return <Typography {...props} variant={variantMap[size]} as={props.as ?? 'p'} />;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default Typography;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export function useTapAppBar(route, callback) {
|
|
4
|
+
const saved = useRef(callback);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
saved.current = callback;
|
|
8
|
+
}, [callback]);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!window.MiniApp) return;
|
|
12
|
+
|
|
13
|
+
const handler = (data) => {
|
|
14
|
+
if (data?.route === route) {
|
|
15
|
+
console.log("onTapAppBar", data)
|
|
16
|
+
saved.current?.(data);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
window.MiniApp.on("onTapAppBar", handler);
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
window.MiniApp.off("onTapAppBar", handler);
|
|
24
|
+
};
|
|
25
|
+
}, [route]);
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
type TapAppBarPayload = {
|
|
4
|
+
route?: string;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function useTapAppBar(
|
|
9
|
+
route: string,
|
|
10
|
+
callback: (data?: TapAppBarPayload) => void
|
|
11
|
+
): void {
|
|
12
|
+
const saved = useRef(callback);
|
|
13
|
+
|
|
14
|
+
// luôn giữ callback mới nhất
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
saved.current = callback;
|
|
17
|
+
}, [callback]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (typeof window === "undefined" || !window.MiniApp) return;
|
|
21
|
+
|
|
22
|
+
const handler = (data?: TapAppBarPayload) => {
|
|
23
|
+
if (data?.route === route) {
|
|
24
|
+
saved.current?.(data);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
window.MiniApp.on("onTapAppBar", handler);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
window.MiniApp.off("onTapAppBar", handler);
|
|
32
|
+
};
|
|
33
|
+
}, [route]);
|
|
34
|
+
}
|
package/src/ui-react/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export * from './hooks/use-app-pause.js';
|
|
|
2
2
|
export * from './hooks/use-app-resume.js';
|
|
3
3
|
export * from './hooks/use-did-show.js';
|
|
4
4
|
export * from './hooks/use-did-hide.js';
|
|
5
|
+
export * from './hooks/use-tap-app-bar.js';
|
|
5
6
|
export * from './hooks/use-navigate.js';
|
|
6
7
|
export * from './hooks/use-app-state.js';
|
|
7
8
|
export * from './hooks/use-listener-scan-qr.js';
|