@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,195 @@
1
+ /**
2
+ * VETC Input / TextField & PasswordInput Components
3
+ * All style values use CSS custom properties from tokens.css
4
+ * Figma: Text field page — height 48px, radius 8px, padding-x 12px
5
+ */
6
+ import React from 'react';
7
+ import { Input as AntInput } from 'antd';
8
+
9
+ export type InputStatus = 'default' | 'error' | 'success';
10
+
11
+ // ── Shared label/helper sub-components ───────────────────────────────────────
12
+
13
+ function FieldLabel({ htmlFor, required, disabled, children }: {
14
+ htmlFor?: string;
15
+ required?: boolean;
16
+ disabled?: boolean;
17
+ children: React.ReactNode;
18
+ }) {
19
+ return (
20
+ <label
21
+ htmlFor={htmlFor}
22
+ style={{
23
+ fontFamily: 'var(--vetc-font-family)',
24
+ fontSize: 'var(--vetc-input-label-font-size)',
25
+ fontWeight: 'var(--vetc-input-label-font-weight)' as any,
26
+ lineHeight: 'var(--vetc-line-height-relaxed)',
27
+ color: disabled
28
+ ? 'var(--vetc-color-text-disabled)'
29
+ : 'var(--vetc-input-label-color)',
30
+ display: 'block',
31
+ }}
32
+ >
33
+ {children}
34
+ {required && (
35
+ <span style={{ color: 'var(--vetc-color-negative)', marginLeft: 'var(--vetc-space-2)' }}>*</span>
36
+ )}
37
+ </label>
38
+ );
39
+ }
40
+
41
+ function HelperText({ color, children }: { color: string; children: React.ReactNode }) {
42
+ return (
43
+ <span
44
+ style={{
45
+ fontFamily: 'var(--vetc-font-family)',
46
+ fontSize: 'var(--vetc-input-helper-font-size)',
47
+ lineHeight: 'var(--vetc-line-height-relaxed)',
48
+ color,
49
+ }}
50
+ >
51
+ {children}
52
+ </span>
53
+ );
54
+ }
55
+
56
+ const inputFieldStyle: React.CSSProperties = {
57
+ height: 'var(--vetc-input-height)',
58
+ borderRadius: 'var(--vetc-input-radius)',
59
+ paddingInline:'var(--vetc-input-padding-x)',
60
+ fontSize: 'var(--vetc-input-font-size)',
61
+ fontFamily: 'var(--vetc-font-family)',
62
+ };
63
+
64
+ // ── Input ─────────────────────────────────────────────────────────────────────
65
+
66
+ export interface InputProps {
67
+ label?: string;
68
+ placeholder?: string;
69
+ value?: string;
70
+ defaultValue?: string;
71
+ helperText?: string;
72
+ /** Status affecting border color */
73
+ status?: InputStatus;
74
+ /** Error message — sets status=error automatically */
75
+ error?: string;
76
+ disabled?: boolean;
77
+ readOnly?: boolean;
78
+ maxLength?: number;
79
+ showCount?: boolean;
80
+ prefix?: React.ReactNode;
81
+ suffix?: React.ReactNode;
82
+ allowClear?: boolean;
83
+ onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void;
84
+ onFocus?: React.FocusEventHandler<HTMLInputElement>;
85
+ onBlur?: React.FocusEventHandler<HTMLInputElement>;
86
+ onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>;
87
+ type?: string;
88
+ id?: string;
89
+ name?: string;
90
+ className?: string;
91
+ style?: React.CSSProperties;
92
+ required?: boolean;
93
+ }
94
+
95
+ export function Input({
96
+ label, placeholder, value, defaultValue,
97
+ helperText, status = 'default', error,
98
+ disabled = false, readOnly = false,
99
+ maxLength, showCount = false,
100
+ prefix, suffix, allowClear = false,
101
+ onChange, onFocus, onBlur, onPressEnter,
102
+ type = 'text', id, name,
103
+ className = '', style, required = false,
104
+ }: InputProps) {
105
+ const hasError = !!error;
106
+ const resolvedStatus: InputStatus = hasError ? 'error' : status;
107
+ const antStatus = resolvedStatus === 'error' ? 'error' : undefined;
108
+
109
+ const helperMsg = error ?? helperText;
110
+ const helperColor = resolvedStatus === 'error'
111
+ ? 'var(--vetc-input-helper-color-error)'
112
+ : 'var(--vetc-input-helper-color)';
113
+
114
+ return (
115
+ <div
116
+ className={`vetc-input-wrapper ${className}`}
117
+ style={{ display: 'flex', flexDirection: 'column', gap: 'var(--vetc-input-gap)', ...style }}
118
+ >
119
+ {label && <FieldLabel htmlFor={id} required={required} disabled={disabled}>{label}</FieldLabel>}
120
+
121
+ <AntInput
122
+ id={id}
123
+ name={name}
124
+ type={type}
125
+ value={value}
126
+ defaultValue={defaultValue}
127
+ placeholder={placeholder}
128
+ disabled={disabled}
129
+ readOnly={readOnly}
130
+ maxLength={maxLength}
131
+ showCount={showCount}
132
+ prefix={prefix}
133
+ suffix={suffix}
134
+ allowClear={allowClear}
135
+ status={antStatus}
136
+ onChange={(e) => onChange?.(e.target.value, e)}
137
+ onFocus={onFocus}
138
+ onBlur={onBlur}
139
+ onPressEnter={onPressEnter}
140
+ style={inputFieldStyle}
141
+ />
142
+
143
+ {helperMsg && <HelperText color={helperColor}>{helperMsg}</HelperText>}
144
+ </div>
145
+ );
146
+ }
147
+
148
+ // ── Password Input ────────────────────────────────────────────────────────────
149
+
150
+ export interface PasswordInputProps extends Omit<InputProps, 'type' | 'suffix' | 'allowClear'> {}
151
+
152
+ export function PasswordInput({
153
+ label, placeholder, value, defaultValue,
154
+ helperText, status = 'default', error,
155
+ disabled = false, readOnly = false, maxLength,
156
+ onChange, onFocus, onBlur,
157
+ id, name, className = '', style, required = false,
158
+ }: PasswordInputProps) {
159
+ const hasError = !!error;
160
+ const resolvedStatus: InputStatus = hasError ? 'error' : status;
161
+ const antStatus = resolvedStatus === 'error' ? 'error' : undefined;
162
+ const helperMsg = error ?? helperText;
163
+ const helperColor = hasError
164
+ ? 'var(--vetc-input-helper-color-error)'
165
+ : 'var(--vetc-input-helper-color)';
166
+
167
+ return (
168
+ <div
169
+ className={`vetc-input-wrapper ${className ?? ''}`}
170
+ style={{ display: 'flex', flexDirection: 'column', gap: 'var(--vetc-input-gap)', ...style }}
171
+ >
172
+ {label && <FieldLabel htmlFor={id} required={required} disabled={disabled}>{label}</FieldLabel>}
173
+
174
+ <AntInput.Password
175
+ id={id}
176
+ name={name}
177
+ value={value}
178
+ defaultValue={defaultValue}
179
+ placeholder={placeholder}
180
+ disabled={disabled}
181
+ readOnly={readOnly}
182
+ maxLength={maxLength}
183
+ status={antStatus}
184
+ onChange={(e) => onChange?.(e.target.value, e as any)}
185
+ onFocus={onFocus as any}
186
+ onBlur={onBlur as any}
187
+ style={inputFieldStyle}
188
+ />
189
+
190
+ {helperMsg && <HelperText color={helperColor}>{helperMsg}</HelperText>}
191
+ </div>
192
+ );
193
+ }
194
+
195
+ export default Input;
@@ -0,0 +1,2 @@
1
+ export { Input, PasswordInput } from './Input';
2
+ export type { InputProps, PasswordInputProps, InputStatus } from './Input';
@@ -0,0 +1,180 @@
1
+ /**
2
+ * VETC List & ListItem — tokenized
3
+ * Figma: List item page — height 56px, padding-x 16px
4
+ */
5
+ import React from 'react';
6
+
7
+ // ── ChevronRight Icon ─────────────────────────────────────────────────────────
8
+ function ChevronRightIcon({ disabled }: { disabled?: boolean }) {
9
+ return (
10
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
11
+ <path
12
+ d="M7.5 5L12.5 10L7.5 15"
13
+ stroke={disabled ? 'var(--vetc-color-icon-disabled)' : 'var(--vetc-color-icon-muted)'}
14
+ strokeWidth="1.5"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ // ── ListItem ──────────────────────────────────────────────────────────────────
23
+ export interface ListItemProps {
24
+ leading?: React.ReactNode;
25
+ title: React.ReactNode;
26
+ description?: React.ReactNode;
27
+ trailing?: React.ReactNode;
28
+ /** Show right chevron */
29
+ arrow?: boolean;
30
+ /** Bottom divider line */
31
+ divider?: boolean;
32
+ onClick?: () => void;
33
+ disabled?: boolean;
34
+ className?: string;
35
+ style?: React.CSSProperties;
36
+ id?: string;
37
+ }
38
+
39
+ export function ListItem({
40
+ leading,
41
+ title,
42
+ description,
43
+ trailing,
44
+ arrow = false,
45
+ divider = true,
46
+ onClick,
47
+ disabled = false,
48
+ className = '',
49
+ style,
50
+ id,
51
+ }: ListItemProps) {
52
+ const isClickable = !!onClick && !disabled;
53
+
54
+ return (
55
+ <div
56
+ id={id}
57
+ role={isClickable ? 'button' : undefined}
58
+ tabIndex={isClickable ? 0 : undefined}
59
+ className={`vetc-list-item ${className}`}
60
+ onClick={!disabled ? onClick : undefined}
61
+ onKeyDown={isClickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') onClick?.(); } : undefined}
62
+ style={{
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ minHeight: 'var(--vetc-list-item-height)',
66
+ padding: `0 var(--vetc-list-item-padding-x)`,
67
+ gap: 'var(--vetc-list-item-gap)',
68
+ cursor: isClickable ? 'pointer' : 'default',
69
+ borderBottom: divider ? `1px solid var(--vetc-list-item-border)` : 'none',
70
+ backgroundColor: 'var(--vetc-list-item-bg)',
71
+ opacity: disabled ? 0.5 : 1,
72
+ fontFamily: 'var(--vetc-font-family)',
73
+ transition: `background-color var(--vetc-transition-fast)`,
74
+ outline: 'none',
75
+ ...style,
76
+ }}
77
+ onMouseEnter={(e) => {
78
+ if (isClickable) e.currentTarget.style.backgroundColor = 'var(--vetc-list-item-bg-hover)';
79
+ }}
80
+ onMouseLeave={(e) => {
81
+ if (isClickable) e.currentTarget.style.backgroundColor = 'var(--vetc-list-item-bg)';
82
+ }}
83
+ >
84
+ {/* Leading slot */}
85
+ {leading && (
86
+ <div style={{
87
+ flexShrink: 0,
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ color: 'var(--vetc-list-item-icon-color)',
91
+ }}>
92
+ {leading}
93
+ </div>
94
+ )}
95
+
96
+ {/* Content */}
97
+ <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 'var(--vetc-space-2)' }}>
98
+ <span style={{
99
+ fontSize: 'var(--vetc-list-item-title-size)',
100
+ fontWeight: 'var(--vetc-list-item-title-weight)' as any,
101
+ color: disabled ? 'var(--vetc-color-text-disabled)' : 'var(--vetc-list-item-title-color)',
102
+ lineHeight: 'var(--vetc-line-height-relaxed)',
103
+ overflow: 'hidden',
104
+ textOverflow: 'ellipsis',
105
+ whiteSpace: 'nowrap',
106
+ }}>
107
+ {title}
108
+ </span>
109
+ {description && (
110
+ <span style={{
111
+ fontSize: 'var(--vetc-list-item-desc-size)',
112
+ fontWeight: 'var(--vetc-font-weight-regular)' as any,
113
+ color: disabled ? 'var(--vetc-color-text-disabled)' : 'var(--vetc-list-item-desc-color)',
114
+ lineHeight: 'var(--vetc-line-height-relaxed)',
115
+ overflow: 'hidden',
116
+ textOverflow: 'ellipsis',
117
+ whiteSpace: 'nowrap',
118
+ }}>
119
+ {description}
120
+ </span>
121
+ )}
122
+ </div>
123
+
124
+ {/* Trailing slot */}
125
+ {(trailing || arrow) && (
126
+ <div style={{
127
+ flexShrink: 0,
128
+ display: 'flex',
129
+ alignItems: 'center',
130
+ gap: 'var(--vetc-space-8)',
131
+ color: 'var(--vetc-list-item-icon-color)',
132
+ }}>
133
+ {trailing}
134
+ {arrow && <ChevronRightIcon disabled={disabled} />}
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ // ── List ──────────────────────────────────────────────────────────────────────
142
+ export interface ListProps<T = any> {
143
+ items: T[];
144
+ renderItem: (item: T, index: number) => React.ReactNode;
145
+ bordered?: boolean;
146
+ className?: string;
147
+ style?: React.CSSProperties;
148
+ id?: string;
149
+ }
150
+
151
+ export function List<T = any>({
152
+ items,
153
+ renderItem,
154
+ bordered = false,
155
+ className = '',
156
+ style,
157
+ id,
158
+ }: ListProps<T>) {
159
+ return (
160
+ <div
161
+ id={id}
162
+ className={`vetc-list ${className}`}
163
+ style={{
164
+ border: bordered ? `1px solid var(--vetc-color-border-variant)` : 'none',
165
+ borderRadius: bordered ? 'var(--vetc-card-radius)' : 0,
166
+ overflow: 'hidden',
167
+ backgroundColor: 'var(--vetc-color-bg)',
168
+ ...style,
169
+ }}
170
+ >
171
+ {items.map((item, index) => (
172
+ <React.Fragment key={index}>
173
+ {renderItem(item, index)}
174
+ </React.Fragment>
175
+ ))}
176
+ </div>
177
+ );
178
+ }
179
+
180
+ export default ListItem;
@@ -0,0 +1,2 @@
1
+ export { List, ListItem } from './List';
2
+ export type { ListProps, ListItemProps } from './List';
@@ -0,0 +1,121 @@
1
+ /**
2
+ * VETC Loading — tokenized
3
+ */
4
+ import React from 'react';
5
+ import { Spin, Skeleton } from 'antd';
6
+ import { LoadingOutlined } from '@ant-design/icons';
7
+
8
+ export type SpinnerSize = 'sm' | 'md' | 'lg';
9
+
10
+ const spinnerSizeVarMap: Record<SpinnerSize, string> = {
11
+ sm: 'var(--vetc-spinner-size-sm)',
12
+ md: 'var(--vetc-spinner-size-md)',
13
+ lg: 'var(--vetc-spinner-size-lg)',
14
+ };
15
+ // antd icon needs numeric fontSize — CSS var won't work directly
16
+ const spinnerPxMap: Record<SpinnerSize, number> = { sm: 16, md: 24, lg: 40 };
17
+
18
+ export interface SpinnerProps {
19
+ size?: SpinnerSize;
20
+ color?: string;
21
+ fullscreen?: boolean;
22
+ tip?: string;
23
+ children?: React.ReactNode;
24
+ spinning?: boolean;
25
+ className?: string;
26
+ style?: React.CSSProperties;
27
+ }
28
+
29
+ export function Spinner({
30
+ size = 'md',
31
+ color,
32
+ fullscreen = false,
33
+ tip,
34
+ children,
35
+ spinning = true,
36
+ className = '',
37
+ style,
38
+ }: SpinnerProps) {
39
+ const icon = (
40
+ <LoadingOutlined
41
+ style={{ fontSize: spinnerPxMap[size], color: color ?? 'var(--vetc-spinner-color)' }}
42
+ spin
43
+ />
44
+ );
45
+
46
+ if (children) {
47
+ return (
48
+ <Spin spinning={spinning} indicator={icon} tip={tip} className={`vetc-spinner ${className}`} style={style}>
49
+ {children}
50
+ </Spin>
51
+ );
52
+ }
53
+
54
+ if (fullscreen) {
55
+ return (
56
+ <div
57
+ className={`vetc-spinner-overlay ${className}`}
58
+ style={{
59
+ position: 'fixed',
60
+ inset: 0,
61
+ display: 'flex',
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ backgroundColor: 'var(--vetc-color-overlay)',
65
+ zIndex: 9999,
66
+ ...style,
67
+ }}
68
+ >
69
+ <Spin spinning={spinning} indicator={icon} tip={tip} />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <div
76
+ className={`vetc-spinner ${className}`}
77
+ style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', ...style }}
78
+ >
79
+ <Spin spinning={spinning} indicator={icon} tip={tip} />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ // ── Skeleton ──────────────────────────────────────────────────────────────────
85
+ export interface SkeletonProps {
86
+ rows?: number;
87
+ avatar?: boolean;
88
+ avatarSize?: number;
89
+ title?: boolean;
90
+ loading?: boolean;
91
+ className?: string;
92
+ style?: React.CSSProperties;
93
+ children?: React.ReactNode;
94
+ }
95
+
96
+ export function SkeletonLoader({
97
+ rows = 3,
98
+ avatar = false,
99
+ avatarSize = 40,
100
+ title = true,
101
+ loading = true,
102
+ className = '',
103
+ style,
104
+ children,
105
+ }: SkeletonProps) {
106
+ return (
107
+ <Skeleton
108
+ active
109
+ loading={loading}
110
+ avatar={avatar ? { size: avatarSize, shape: 'circle' } : false}
111
+ title={title}
112
+ paragraph={{ rows }}
113
+ className={`vetc-skeleton ${className}`}
114
+ style={style}
115
+ >
116
+ {children}
117
+ </Skeleton>
118
+ );
119
+ }
120
+
121
+ export default Spinner;
@@ -0,0 +1,2 @@
1
+ export { Spinner, SkeletonLoader } from './Loading';
2
+ export type { SpinnerProps, SkeletonProps, SpinnerSize } from './Loading';
@@ -0,0 +1,116 @@
1
+ /**
2
+ * VETC Modal / Dialog — tokenized
3
+ * Figma: Dialog page — width 344px, radius 16px, overlay rgba(0,0,0,0.4)
4
+ */
5
+ import React from 'react';
6
+ import { Modal as AntModal } from 'antd';
7
+
8
+ export interface ModalProps {
9
+ open: boolean;
10
+ title?: React.ReactNode;
11
+ children?: React.ReactNode;
12
+ /** null = hide footer, undefined = default buttons, ReactNode = custom */
13
+ footer?: React.ReactNode | null;
14
+ okText?: string;
15
+ cancelText?: string;
16
+ okDisabled?: boolean;
17
+ okLoading?: boolean;
18
+ okDanger?: boolean;
19
+ onOk?: () => void;
20
+ onCancel?: () => void;
21
+ maskClosable?: boolean;
22
+ closable?: boolean;
23
+ width?: number | string;
24
+ className?: string;
25
+ style?: React.CSSProperties;
26
+ id?: string;
27
+ }
28
+
29
+ /** Shared button style for modal actions */
30
+ const modalBtnStyle: React.CSSProperties = {
31
+ height: 'var(--vetc-btn-height-lg)',
32
+ borderRadius: 'var(--vetc-btn-radius-lg)',
33
+ fontWeight: 'var(--vetc-btn-font-weight)' as any,
34
+ fontSize: 'var(--vetc-btn-font-size-lg)',
35
+ fontFamily: 'var(--vetc-font-family)',
36
+ };
37
+
38
+ export function Modal({
39
+ open, title, children, footer,
40
+ okText = 'Xác nhận', cancelText = 'Hủy',
41
+ okDisabled = false, okLoading = false, okDanger = false,
42
+ onOk, onCancel,
43
+ maskClosable = true, closable = true,
44
+ width,
45
+ className = '', style, id,
46
+ }: ModalProps) {
47
+ return (
48
+ <AntModal
49
+ open={open}
50
+ title={title}
51
+ okText={okText}
52
+ cancelText={cancelText}
53
+ okButtonProps={{
54
+ disabled: okDisabled,
55
+ loading: okLoading,
56
+ danger: okDanger,
57
+ style: modalBtnStyle,
58
+ }}
59
+ cancelButtonProps={{ style: modalBtnStyle }}
60
+ footer={footer === null ? null : footer}
61
+ onOk={onOk}
62
+ onCancel={onCancel}
63
+ maskClosable={maskClosable}
64
+ closable={closable}
65
+ width={width ?? 'var(--vetc-modal-width)'}
66
+ centered
67
+ className={`vetc-modal ${className}`}
68
+ style={{ fontFamily: 'var(--vetc-font-family)', ...style }}
69
+ styles={{
70
+ header: {
71
+ padding: `var(--vetc-modal-padding) var(--vetc-modal-padding) 0`,
72
+ borderBottom: 'none',
73
+ },
74
+ body: {
75
+ padding: `var(--vetc-space-8) var(--vetc-modal-padding) var(--vetc-modal-padding)`,
76
+ },
77
+ footer: {
78
+ padding: `0 var(--vetc-modal-padding) var(--vetc-modal-padding)`,
79
+ borderTop: 'none',
80
+ },
81
+ mask: { backgroundColor: 'var(--vetc-modal-overlay)' },
82
+ } as any}
83
+ >
84
+ {children}
85
+ </AntModal>
86
+ );
87
+ }
88
+
89
+ // ── Confirm hook ──────────────────────────────────────────────────────────────
90
+ export function useConfirm() {
91
+ const confirm = (opts: {
92
+ title: string;
93
+ content?: React.ReactNode;
94
+ okText?: string;
95
+ cancelText?: string;
96
+ okDanger?: boolean;
97
+ onOk?: () => void | Promise<void>;
98
+ onCancel?: () => void;
99
+ }) => {
100
+ AntModal.confirm({
101
+ title: opts.title,
102
+ content: opts.content,
103
+ okText: opts.okText ?? 'Xác nhận',
104
+ cancelText: opts.cancelText ?? 'Hủy',
105
+ okButtonProps: { danger: opts.okDanger, style: { ...modalBtnStyle, height: 40 } },
106
+ cancelButtonProps: { style: { ...modalBtnStyle, height: 40 } },
107
+ centered: true,
108
+ onOk: opts.onOk,
109
+ onCancel: opts.onCancel,
110
+ styles: { mask: { backgroundColor: 'var(--vetc-modal-overlay)' } } as any,
111
+ });
112
+ };
113
+ return { confirm };
114
+ }
115
+
116
+ export default Modal;
@@ -0,0 +1,2 @@
1
+ export { Modal, useConfirm } from './Modal';
2
+ export type { ModalProps } from './Modal';