@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.
Files changed (57) 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-did-show.js +1 -0
  47. package/src/ui-react/hooks/use-tap-app-bar.js +26 -0
  48. package/src/ui-react/hooks/use-tap-app-bar.ts +34 -0
  49. package/src/ui-react/index.js +1 -0
  50. package/src/ui-react/index.ts +79 -3
  51. package/src/ui-react/styles/VETCProvider.tsx +152 -0
  52. package/src/ui-react/styles/tokens.css +427 -0
  53. package/src/ui-react/tokens/colors.ts +91 -0
  54. package/src/ui-react/tokens/index.ts +3 -0
  55. package/src/ui-react/tokens/spacing.ts +59 -0
  56. package/src/ui-react/tokens/typography.ts +63 -0
  57. 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,2 @@
1
+ export { TabBar } from './TabBar';
2
+ export type { TabBarProps, TabBarItem, TabBarVariant } from './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,2 @@
1
+ export { Textarea } from './Textarea';
2
+ export type { TextareaProps } from './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,2 @@
1
+ export { toast, useToast } from './Toast';
2
+ export type { ToastOptions, ToastType } from './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,2 @@
1
+ export { Typography, Display, Headline, Title, Label, Body } from './Typography';
2
+ export type { TypographyProps, TypographyVariant, TextColor } from './Typography';
@@ -12,6 +12,7 @@ export function useDidShow(route, callback) {
12
12
 
13
13
  const handler = (data) => {
14
14
  if (data?.route === route) {
15
+ console.log("didShow", data)
15
16
  saved.current?.(data);
16
17
  }
17
18
  };
@@ -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
+ }
@@ -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';