@openway/ui 1.0.0 → 1.0.2

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 (58) hide show
  1. package/README.md +203 -181
  2. package/dist/chunk-X4LIYOS5.js +3 -0
  3. package/dist/chunk-X4LIYOS5.js.map +1 -0
  4. package/dist/index.cjs +5 -1
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +2740 -1473
  7. package/dist/index.d.ts +2740 -1473
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/query.cjs +3 -0
  11. package/dist/query.cjs.map +1 -0
  12. package/dist/query.d.cts +431 -0
  13. package/dist/query.d.ts +431 -0
  14. package/dist/query.js +3 -0
  15. package/dist/query.js.map +1 -0
  16. package/dist/useInfiniteScroll-D9AW6cQV.d.cts +598 -0
  17. package/dist/useInfiniteScroll-D9AW6cQV.d.ts +598 -0
  18. package/docs/AGENTS.md +144 -0
  19. package/docs/README.md +150 -0
  20. package/docs/SKILL.md +54 -0
  21. package/docs/components/alert.md +246 -0
  22. package/docs/components/badge.md +238 -0
  23. package/docs/components/button.md +262 -0
  24. package/docs/components/carousel.md +354 -0
  25. package/docs/components/checkbox.md +252 -0
  26. package/docs/components/collapse.md +318 -0
  27. package/docs/components/confirm.md +322 -0
  28. package/docs/components/datepicker.md +259 -0
  29. package/docs/components/daterangepicker.md +260 -0
  30. package/docs/components/datetimepicker.md +226 -0
  31. package/docs/components/datetimerangepicker.md +222 -0
  32. package/docs/components/dropdown.md +275 -0
  33. package/docs/components/empty.md +200 -0
  34. package/docs/components/file-preview.md +180 -0
  35. package/docs/components/input.md +559 -0
  36. package/docs/components/modal.md +293 -0
  37. package/docs/components/popover.md +255 -0
  38. package/docs/components/radio.md +245 -0
  39. package/docs/components/select.md +254 -0
  40. package/docs/components/skeleton.md +150 -0
  41. package/docs/components/slider.md +346 -0
  42. package/docs/components/table.md +316 -0
  43. package/docs/components/tabs.md +432 -0
  44. package/docs/components/textarea.md +193 -0
  45. package/docs/components/timepicker.md +242 -0
  46. package/docs/components/timerangepicker.md +210 -0
  47. package/docs/components/toast.md +282 -0
  48. package/docs/components/toggle.md +211 -0
  49. package/docs/components/tooltip.md +213 -0
  50. package/docs/components/upload-avatar.md +318 -0
  51. package/docs/components/upload-file.md +245 -0
  52. package/docs/components/upload-image.md +126 -0
  53. package/docs/hooks/useDebounce.md +92 -0
  54. package/docs/hooks/useInfiniteScroll.md +95 -0
  55. package/docs/hooks/useMutationApp.md +242 -0
  56. package/docs/hooks/useSelectInfiniteQuery.md +123 -0
  57. package/docs/hooks/useTableQuery.md +124 -0
  58. package/package.json +31 -12
@@ -0,0 +1,213 @@
1
+ # 💡 Tooltip Component (`@owa/ui`)
2
+
3
+ Component **Tooltip** (chú giải ngữ cảnh nhanh) xây dựng trên nền tảng **`@floating-ui/react`**, hỗ trợ **chuẩn WAI-ARIA Tooltip**, **tự động căn vị trí thông minh (flip/shift/offset)**, **mũi tên động (FloatingArrow)**, **tích hợp Slot pattern React 19**, và tương thích với hệ thống **Safe Config Fallback**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Tích hợp Slot & React 19**: Tự động gán trực tiếp event listeners và ref vào phần tử con thông qua mô hình `Slot` component mà không cần bọc thêm thẻ `<span>` thừa, bảo toàn 100% layout flexbox/grid ban đầu.
10
+ - **Hệ thống z-index đồng bộ**: Sử dụng `DEFAULT_Z_INDEX.TOOLTIP` (mặc định `60`) từ hệ sinh thái constant toàn cục của `@owa/ui`, luôn nổi trên Dropdown và Popover.
11
+ - **Mũi tên chỉ hướng thông minh (FloatingArrow)**: Mũi tên SVG tự động xoay và định vị chính xác theo góc lật của tooltip, tự đổi màu viền và nền đồng bộ theo `variant` và `color`.
12
+ - **Tự động định vị & chống tràn màn hình**:
13
+ - `offset`: Tự động duy trì khoảng cách tiêu chuẩn với trigger.
14
+ - `flip`: Tự động đảo hướng đối xứng khi chạm viền màn hình (ví dụ: `top` -> `bottom`).
15
+ - `shift`: Tự động dịch chuyển ngang/dọc trong viewport để không bị cắt xén nội dung.
16
+ - **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` đảm bảo an toàn tuyệt đối, không crash ứng dụng khi nhận giá trị prop kích thước hoặc màu sắc không hợp lệ.
17
+ - **WAI-ARIA Accessibility**:
18
+ - `role="tooltip"` tự động gắn cho hộp chú giải.
19
+ - Tự động kích hoạt khi `hover` chuột hoặc khi nhận tiêu điểm bàn phím (`focus`).
20
+ - Tự động ẩn khi rê chuột ra ngoài (`mouseleave`), mất tiêu điểm (`blur`) hoặc khi nhấn phím `Escape`.
21
+ - **Đa dạng biến thể & tùy biến**:
22
+ - 4 biến thể (`variant`): `filled` (*mặc định*), `soft`, `outline`, `other`.
23
+ - 7 chủ đề màu sắc (`color`): `neutral` (*mặc định*), `primary`, `secondary`, `error`, `success`, `warning`, `info`.
24
+ - 5 kích cỡ (`size`): `xs`, `sm`, `md` (*mặc định*), `lg`, `xl`.
25
+ - 6 cấp độ bo góc (`radius`): `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
26
+
27
+ ---
28
+
29
+ ## 🚀 Cài đặt & Import
30
+
31
+ ```tsx
32
+ import { Tooltip } from "@owa/ui";
33
+ import type {
34
+ TooltipProps,
35
+ TooltipPlacement,
36
+ TooltipVariant,
37
+ TooltipColor,
38
+ TooltipSize,
39
+ TooltipRadius,
40
+ } from "@owa/ui";
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 📖 Hướng dẫn sử dụng
46
+
47
+ ### 1. Tooltip cơ bản
48
+
49
+ ```tsx
50
+ import { Tooltip, Button } from "@owa/ui";
51
+
52
+ export function BasicTooltip() {
53
+ return (
54
+ <div className="flex gap-4 items-center">
55
+ <Tooltip content="Lưu dữ liệu hiện tại">
56
+ <Button>Lưu</Button>
57
+ </Tooltip>
58
+
59
+ <Tooltip content="Hành động này không thể hoàn tác" color="error">
60
+ <Button color="error" variant="soft">Xóa</Button>
61
+ </Tooltip>
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ---
68
+
69
+ ### 2. Các hướng hiển thị (`placement`)
70
+
71
+ Hỗ trợ 12 hướng hiển thị phong phú:
72
+
73
+ ```tsx
74
+ <div className="grid grid-cols-3 gap-3">
75
+ <Tooltip content="Top Start" placement="top-start">
76
+ <Button variant="outline">Top Start</Button>
77
+ </Tooltip>
78
+ <Tooltip content="Top Center" placement="top">
79
+ <Button variant="outline">Top</Button>
80
+ </Tooltip>
81
+ <Tooltip content="Top End" placement="top-end">
82
+ <Button variant="outline">Top End</Button>
83
+ </Tooltip>
84
+
85
+ <Tooltip content="Bottom Start" placement="bottom-start">
86
+ <Button variant="outline">Bottom Start</Button>
87
+ </Tooltip>
88
+ <Tooltip content="Bottom Center" placement="bottom">
89
+ <Button variant="outline">Bottom</Button>
90
+ </Tooltip>
91
+ <Tooltip content="Bottom End" placement="bottom-end">
92
+ <Button variant="outline">Bottom End</Button>
93
+ </Tooltip>
94
+ </div>
95
+ ```
96
+
97
+ ---
98
+
99
+ ### 3. Các biến thể (`variant`) & Màu sắc (`color`)
100
+
101
+ ```tsx
102
+ // 1. Filled (Mặc định - Nền đậm tương phản cao)
103
+ <Tooltip content="Filled Neutral" variant="filled" color="neutral">
104
+ <Button>Neutral</Button>
105
+ </Tooltip>
106
+ <Tooltip content="Filled Primary" variant="filled" color="primary">
107
+ <Button color="primary">Primary</Button>
108
+ </Tooltip>
109
+
110
+ // 2. Soft (Nền pastel nhẹ nhàng)
111
+ <Tooltip content="Soft Info" variant="soft" color="info">
112
+ <Button color="info" variant="soft">Info</Button>
113
+ </Tooltip>
114
+ <Tooltip content="Soft Success" variant="soft" color="success">
115
+ <Button color="success" variant="soft">Success</Button>
116
+ </Tooltip>
117
+
118
+ // 3. Outline (Nền trắng viền màu sắc nét)
119
+ <Tooltip content="Outline Warning" variant="outline" color="warning">
120
+ <Button color="warning" variant="outline">Warning</Button>
121
+ </Tooltip>
122
+
123
+ // 4. Other (Tự do tùy biến 100% qua className)
124
+ <Tooltip
125
+ content="Custom Gradient"
126
+ variant="other"
127
+ className="bg-linear-to-r from-purple-600 to-pink-500 text-white font-bold shadow-lg"
128
+ >
129
+ <Button>VIP</Button>
130
+ </Tooltip>
131
+ ```
132
+
133
+ ---
134
+
135
+ ### 4. Tùy chỉnh độ trễ hiển thị (`delay`)
136
+
137
+ ```tsx
138
+ // Xuất hiện ngay lập tức (không có độ trễ)
139
+ <Tooltip content="Hiện ngay lập tức" delay={0}>
140
+ <Button>Instant Tooltip</Button>
141
+ </Tooltip>
142
+
143
+ // Tùy chỉnh độ trễ mở và đóng riêng biệt
144
+ <Tooltip content="Mở sau 500ms, đóng sau 100ms" delay={{ open: 500, close: 100 }}>
145
+ <Button>Custom Delay</Button>
146
+ </Tooltip>
147
+ ```
148
+
149
+ ---
150
+
151
+ ### 5. Kết hợp với IconButton & Trạng thái Disabled
152
+
153
+ ```tsx
154
+ import { Tooltip, IconButton } from "@owa/ui";
155
+ import { EditIcon, TrashIcon } from "@/components/icons";
156
+
157
+ export function IconButtonsWithTooltip() {
158
+ return (
159
+ <div className="flex gap-2">
160
+ <Tooltip content="Chỉnh sửa thông tin">
161
+ <IconButton icon={<EditIcon />} aria-label="Chỉnh sửa" variant="ghost" />
162
+ </Tooltip>
163
+
164
+ {/* Tooltip bị vô hiệu hóa khi không muốn hiển thị */}
165
+ <Tooltip content="Nút này bị khóa" disabled>
166
+ <IconButton icon={<TrashIcon />} aria-label="Xóa" disabled variant="ghost" />
167
+ </Tooltip>
168
+ </div>
169
+ );
170
+ }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## ♿ Khả năng truy cập & Điều hướng bàn phím
176
+
177
+ | Thao tác | Hành vi |
178
+ | :--- | :--- |
179
+ | `Hover chuột` | Hiển thị tooltip sau khoảng thời gian trễ `delay.open`. |
180
+ | `Rời chuột` | Ẩn tooltip sau khoảng thời gian trễ `delay.close`. |
181
+ | `Tab` (Focus bàn phím) | Tự động kích hoạt hiển thị tooltip khi phần tử con nhận focus. |
182
+ | `Shift + Tab` / `Blur` | Tự động ẩn tooltip khi phần tử con mất focus. |
183
+ | `Escape` | Đóng tooltip ngay lập tức mà không làm mất tiêu điểm của phần tử. |
184
+
185
+ ---
186
+
187
+ ## 📋 API Reference
188
+
189
+ ### `<Tooltip>`
190
+
191
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
192
+ | :--- | :--- | :--- | :--- |
193
+ | `content` | `ReactNode` | *(Bắt buộc)* | Nội dung văn bản hoặc JSX hiển thị bên trong hộp tooltip. |
194
+ | `children` | `ReactElement` | *(Bắt buộc)* | Phần tử con kích hoạt tooltip khi hover/focus. |
195
+ | `placement` | `TooltipPlacement` | `'top'` | Hướng hiển thị (`'top'`, `'bottom'`, `'left'`, `'right'`, và các biến thể `-start`, `-end`). |
196
+ | `variant` | `'filled' \| 'soft' \| 'outline' \| 'other'` | `'filled'` | Biến thể phong cách giao diện của tooltip. |
197
+ | `color` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | `'neutral'` | Chủ đề màu sắc của tooltip. |
198
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích cỡ padding, font-size và kích thước mũi tên. |
199
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Độ bo cong của các góc khung tooltip. |
200
+ | `hasArrow` | `boolean` | `true` | Bật/tắt mũi tên định vị chỉ về phía trigger. |
201
+ | `offset` | `number` | `8` | Khoảng cách (px) giữa trigger và khung tooltip. |
202
+ | `flip` | `boolean` | `true` | Tự động đảo hướng khi bị chạm mép màn hình. |
203
+ | `shift` | `boolean` | `true` | Tự động dịch chuyển để tooltip nằm trọn trong viewport. |
204
+ | `delay` | `number \| { open?: number; close?: number }` | `{ open: 200, close: 150 }` | Thời gian trễ khi mở / đóng tooltip (ms). |
205
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa không cho phép hiển thị tooltip. |
206
+ | `open` | `boolean` | — | Trạng thái mở/đóng ở chế độ Controlled. |
207
+ | `defaultOpen` | `boolean` | `false` | Trạng thái mở ban đầu ở chế độ Uncontrolled. |
208
+ | `onOpenChange` | `(open: boolean) => void` | — | Callback khi trạng thái hiển thị thay đổi. |
209
+ | `animated` | `boolean` | `true` | Bật/tắt hiệu ứng chuyển động mượt khi xuất hiện/biến mất. |
210
+ | `animationDuration` | `number` | `150` | Thời lượng hiệu ứng chuyển động (ms). |
211
+ | `zIndex` | `number` | `DEFAULT_Z_INDEX.TOOLTIP` (60) | Thứ tự z-index của tooltip. |
212
+ | `className` | `string` | `""` | Class CSS tùy biến cho khung tooltip. |
213
+ | `arrowClassName` | `string` | `""` | Class CSS tùy biến cho mũi tên định vị. |
@@ -0,0 +1,318 @@
1
+ # 👤 Upload Avatar Component Suite (`@owa/ui`)
2
+
3
+ Component **UploadAvatar** toàn diện, linh hoạt và tương tác cao cho phép người dùng tải lên, kéo thả (Drag & Drop), cắt xén (Crop), xem trước (Lightbox Preview) và quản lý ảnh đại diện. Thiết kế đồng bộ hoàn hảo với hệ thống **Design System**, hỗ trợ **Safe Config Fallback**, **Quản lý bộ nhớ tự động (Lifecycle Cleanup)** và tuân thủ tiêu chuẩn **WAI-ARIA Accessibility**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Tích hợp quy trình xử lý ảnh trọn gói**:
10
+ - 🖱️ **Kéo thả & Chọn tệp**: Hỗ trợ kéo thả ảnh trực quan nhờ `react-dropzone`, tự động kiểm tra định dạng MIME (`accept`) và giới hạn dung lượng (`maxSize`).
11
+ - ✂️ **Cắt xén ảnh tích hợp (`UploadAvatarCropModal`)**: Tích hợp sẵn modal cắt ảnh trực quan hỗ trợ Zoom, Xoay (Rotate 90°), Lật ảnh (Flip), Reset và Chọn tệp khác ngay trong modal.
12
+ - 🔍 **Xem trước phóng to (Lightbox Preview)**: Tích hợp với `<FileContainer>` và `<FilePreview>` để phóng to, xoay ảnh và tải xuống ảnh chất lượng gốc.
13
+ - 🗑️ **Xóa nhanh & Thao tác tiện lợi**: Menu hành động hiển thị mượt mà khi hover (hoặc focus) cho phép Xem trước, Cắt lại và Xóa ảnh.
14
+ - **Quản lý bộ nhớ tối ưu (Zero Memory Leak)**:
15
+ - Ứng dụng mô hình **Lifecycle Cleanup**: Tự động giải phóng Blob URL (`URL.revokeObjectURL`) bên trong `useEffect cleanup` khi đóng modal, đổi ảnh hoặc khi component unmount.
16
+ - **5 Kích thước tiêu chuẩn (`size`)**:
17
+ - `xs`: 48x48px (`size-12`)
18
+ - `sm`: 64x64px (`size-16`)
19
+ - `md`: 80x80px (`size-20` - *mặc định*)
20
+ - `lg`: 96x96px (`size-24`)
21
+ - `xl`: 128x128px (`size-32`)
22
+ - **3 Biến thể giao diện (`variant`)**:
23
+ - `outline` *(mặc định)*: Viền nét rõ ràng quanh khung avatar, hover/focus đổi màu viền chủ đề.
24
+ - `filled`: Nền pastel nhạt (`bg-{color}-50/60`), viền đồng điệu.
25
+ - `ghost`: Nền trong suốt, viền mờ tối giản.
26
+ - `other`: Bỏ qua các style mặc định, tự do tùy biến qua `avatarClassName`.
27
+ - **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `neutral`, `error`, `success`, `warning`, `info`.
28
+ - **2 Hình dạng linh hoạt (`shape`)**:
29
+ - `circle` *(mặc định)*: Hình tròn hoàn hảo (`rounded-full`).
30
+ - `square`: Hình vuông với tùy chọn bo góc `radius` (`none`, `sm`, `md`, `lg`, `xl`, `full`).
31
+ - **Icon Avatar mặc định tinh tế**: Sử dụng silhouette `<AvatarIcon />` sắc nét làm icon giữ chỗ (placeholder) thay thế cho icon đám mây truyền thống.
32
+ - **Đồng bộ cấu hình Form & A11y như `Input`**:
33
+ - Nhóm các cờ boolean vào prop `config`: `isRequired`, `isInvalid`, `isLoading`, `showSpinner`, `isClearable`, `isFullWidth`.
34
+ - Hỗ trợ `labelPlacement` (`top` hoặc `left`) cùng văn bản trợ giúp `helperText` và thông báo lỗi `errorMessage`.
35
+ - Tương thích bàn phím: <kbd>Space</kbd> / <kbd>Enter</kbd> để chọn ảnh, <kbd>Delete</kbd> / <kbd>Backspace</kbd> để xóa ảnh.
36
+ - Hỗ trợ Screen Reader với vùng thông báo động `aria-live`.
37
+
38
+ ---
39
+
40
+ ## 🚀 Cài đặt & Import
41
+
42
+ ```tsx
43
+ import {
44
+ UploadAvatar,
45
+ UploadAvatarCropModal,
46
+ UploadAvatarCropContent,
47
+ getCroppedImage,
48
+ createImage,
49
+ rotateSize,
50
+ } from "@owa/ui";
51
+
52
+ import type {
53
+ UploadAvatarProps,
54
+ UploadAvatarConfig,
55
+ UploadAvatarCropOptions,
56
+ UploadAvatarCropModalProps,
57
+ UploadAvatarCropContentProps,
58
+ UploadAvatarSize,
59
+ UploadAvatarVariant,
60
+ UploadAvatarColor,
61
+ UploadAvatarShape,
62
+ UploadAvatarRadius,
63
+ UploadAvatarLabelPlacement,
64
+ PreviewFile,
65
+ ServerFile,
66
+ PixelCrop,
67
+ } from "@owa/ui";
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 📖 Hướng dẫn sử dụng
73
+
74
+ ### 1. Sử dụng cơ bản (Uncontrolled & Controlled)
75
+
76
+ #### Cách 1: Tự quản lý (Uncontrolled với `defaultValue`)
77
+ ```tsx
78
+ import { UploadAvatar } from "@owa/ui";
79
+
80
+ export function UncontrolledExample() {
81
+ return (
82
+ <UploadAvatar
83
+ defaultValue="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300"
84
+ label="Ảnh đại diện"
85
+ helperText="Hỗ trợ JPG, PNG, WEBP dưới 5MB"
86
+ onChange={(item) => console.log("Avatar changed:", item)}
87
+ />
88
+ );
89
+ }
90
+ ```
91
+
92
+ #### Cách 2: Quản lý trạng thái (Controlled với `value`)
93
+ ```tsx
94
+ import { useState } from "react";
95
+ import { UploadAvatar, PreviewFile } from "@owa/ui";
96
+
97
+ export function ControlledExample() {
98
+ const [avatar, setAvatar] = useState<PreviewFile | string | null>(null);
99
+
100
+ return (
101
+ <div className="flex flex-col gap-4">
102
+ <UploadAvatar
103
+ value={avatar}
104
+ onChange={setAvatar}
105
+ label="Ảnh đại diện hồ sơ"
106
+ helperText="Click để tải ảnh lên và cắt xén"
107
+ />
108
+ <p className="text-xs text-neutral-500">
109
+ Đã chọn: {avatar ? (typeof avatar === "string" ? avatar : avatar.name) : "Chưa có ảnh"}
110
+ </p>
111
+ </div>
112
+ );
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ### 2. Tùy chỉnh tính năng Cắt ảnh (`crop`)
119
+
120
+ Bạn có thể cấu hình modal cắt ảnh chi tiết hoặc tắt hoàn toàn tính năng này:
121
+
122
+ ```tsx
123
+ import { UploadAvatar } from "@owa/ui";
124
+
125
+ // 1. Tùy chỉnh modal cắt ảnh
126
+ <UploadAvatar
127
+ crop={{
128
+ aspectRatio: 1, // Tỷ lệ 1:1
129
+ cropShape: "round", // Vùng cắt tròn "round" hoặc vuông "rect"
130
+ showGrid: true, // Hiển thị lưới căn chỉnh
131
+ minZoom: 1,
132
+ maxZoom: 5,
133
+ modalTitle: "Tùy chỉnh góc chụp ảnh đại diện",
134
+ }}
135
+ />
136
+
137
+ // 2. Tắt hoàn toàn modal cắt ảnh (Nhận file trực tiếp sau khi chọn)
138
+ <UploadAvatar crop={false} />
139
+ ```
140
+
141
+ ---
142
+
143
+ ### 3. Kích thước (`size`) & Hình dạng (`shape`)
144
+
145
+ ```tsx
146
+ import { UploadAvatar } from "@owa/ui";
147
+
148
+ export function SizesAndShapesExample() {
149
+ return (
150
+ <div className="flex flex-wrap items-center gap-6">
151
+ {/* Các kích thước */}
152
+ <UploadAvatar size="xs" label="XS (48px)" />
153
+ <UploadAvatar size="sm" label="SM (64px)" />
154
+ <UploadAvatar size="md" label="MD (80px)" />
155
+ <UploadAvatar size="lg" label="LG (96px)" />
156
+ <UploadAvatar size="xl" label="XL (128px)" />
157
+
158
+ {/* Hình vuông bo góc */}
159
+ <UploadAvatar
160
+ shape="square"
161
+ radius="lg"
162
+ size="lg"
163
+ crop={{ cropShape: "rect" }}
164
+ label="Logo công ty"
165
+ />
166
+ </div>
167
+ );
168
+ }
169
+ ```
170
+
171
+ ---
172
+
173
+ ### 4. Biến thể (`variant`) & Màu sắc (`color`)
174
+
175
+ ```tsx
176
+ import { UploadAvatar } from "@owa/ui";
177
+
178
+ export function VariantsAndColorsExample() {
179
+ return (
180
+ <div className="flex gap-6">
181
+ <UploadAvatar variant="outline" color="primary" label="Outline Primary" />
182
+ <UploadAvatar variant="filled" color="secondary" label="Filled Secondary" />
183
+ <UploadAvatar variant="ghost" color="neutral" label="Ghost Neutral" />
184
+ <UploadAvatar config={{ isInvalid: true }} errorMessage="Ảnh không hợp lệ" />
185
+ </div>
186
+ );
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ### 5. Tích hợp React Hook Form
193
+
194
+ ```tsx
195
+ import { useForm, Controller } from "react-hook-form";
196
+ import { UploadAvatar, Button } from "@owa/ui";
197
+
198
+ interface ProfileFormData {
199
+ avatar: File | null;
200
+ }
201
+
202
+ export function ProfileForm() {
203
+ const { control, handleSubmit, formState: { errors, isSubmitting } } = useForm<ProfileFormData>({
204
+ defaultValues: { avatar: null },
205
+ });
206
+
207
+ const onSubmit = (data: ProfileFormData) => {
208
+ console.log("Form submitted with file:", data.avatar);
209
+ };
210
+
211
+ return (
212
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-sm">
213
+ <Controller
214
+ name="avatar"
215
+ control={control}
216
+ rules={{ required: "Vui lòng tải lên ảnh đại diện của bạn" }}
217
+ render={({ field: { value, onChange, ref } }) => (
218
+ <UploadAvatar
219
+ ref={ref}
220
+ value={value}
221
+ onChange={onChange}
222
+ label="Ảnh đại diện"
223
+ helperText="Kích thước tối đa 2MB"
224
+ maxSize={2 * 1024 * 1024}
225
+ config={{
226
+ isRequired: true,
227
+ isInvalid: Boolean(errors.avatar),
228
+ isLoading: isSubmitting,
229
+ showSpinner: isSubmitting,
230
+ }}
231
+ errorMessage={errors.avatar?.message}
232
+ />
233
+ )}
234
+ />
235
+ <Button type="submit" disabled={isSubmitting}>Lưu thông tin</Button>
236
+ </form>
237
+ );
238
+ }
239
+ ```
240
+
241
+ ---
242
+
243
+ ## ⚙️ Bảng thuộc tính Props
244
+
245
+ ### `UploadAvatarProps`
246
+
247
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
248
+ | :--- | :--- | :--- | :--- |
249
+ | `value` | `PreviewFile \| string \| null` | `undefined` | Giá trị ảnh avatar hiện tại (dùng ở chế độ Controlled). |
250
+ | `defaultValue` | `PreviewFile \| string \| null` | `null` | Giá trị ảnh avatar khởi tạo ban đầu (dùng ở chế độ Uncontrolled). |
251
+ | `onChange` | `(item: PreviewFile \| null) => void` | `undefined` | Callback khi ảnh avatar thay đổi hoặc bị xóa (`null`). |
252
+ | `onRemove` | `(item: PreviewFile) => void` | `undefined` | Callback khi người dùng nhấn nút xóa ảnh. |
253
+ | `onPreview` | `(item: PreviewFile) => void` | `undefined` | Callback khi người dùng mở modal xem trước phóng to (lightbox). |
254
+ | `onClear` | `() => void` | `undefined` | Callback kích hoạt khi nút xóa nhanh được gọi. |
255
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích thước avatar (48px, 64px, 80px, 96px, 128px). |
256
+ | `variant` | `'outline' \| 'filled' \| 'ghost' \| 'other'` | `'outline'` | Kiểu biến thể hiển thị khung viền avatar. |
257
+ | `color` | `'primary' \| 'secondary' \| 'neutral' \| 'error' \| 'success' \| 'warning' \| 'info'` | `'primary'` | Chủ đề bảng màu sắc hiển thị. |
258
+ | `shape` | `'circle' \| 'square'` | `'circle'` | Hình dạng avatar (`circle`: tròn hoàn toàn; `square`: vuông). |
259
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | Theo `size` | Tùy biến bo góc khi `shape="square"`. |
260
+ | `crop` | `boolean \| UploadAvatarCropOptions` | `true` | Bật/tắt hoặc cấu hình modal cắt xén ảnh trước khi tải lên. |
261
+ | `maxSize` | `number` | `undefined` | Kích thước tệp tối đa cho phép (đơn vị: bytes). |
262
+ | `accept` | `string \| Accept` | `"image/*"` | Các định dạng MIME type được phép tải lên. |
263
+ | `config` | `UploadAvatarConfig` | `{}` | Gom nhóm các cờ boolean trạng thái và tính năng (xem bảng bên dưới). |
264
+ | `label` | `ReactNode` | `undefined` | Nhãn hiển thị tiêu đề cho trường avatar. |
265
+ | `labelPlacement`| `'top' \| 'left'` | `'top'` | Vị trí đặt nhãn so với avatar. |
266
+ | `helperText` | `ReactNode` | `undefined` | Đoạn văn bản hướng dẫn/chú thích bên dưới avatar. |
267
+ | `errorMessage` | `ReactNode` | `undefined` | Thông báo lỗi hiển thị bên dưới avatar (tự động kích hoạt viền đỏ). |
268
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ tương tác tải lên. |
269
+ | `readOnly` | `boolean` | `false` | Chế độ chỉ xem, không cho phép thay đổi hay xóa. |
270
+ | `icon` | `ReactNode` | `<AvatarIcon />` | Tùy biến icon placeholder khi chưa có ảnh. |
271
+ | `name` | `string` | `undefined` | Tên của trường input file trong form HTML. |
272
+ | `id` | `string` | Tự động sinh | ID của phần tử input (dùng cho liên kết label & a11y). |
273
+ | `ref` | `Ref<HTMLInputElement>` | `undefined` | Ref chuyển tiếp đến thẻ input file ẩn bên trong. |
274
+ | `className` | `string` | `""` | Tùy biến class container ngoài cùng. |
275
+ | `wrapperClassName` | `string` | `""` | Alias của `className`. |
276
+ | `avatarClassName` | `string` | `""` | Tùy biến class áp dụng riêng cho khung viền của avatar. |
277
+ | `labelClassName` | `string` | `""` | Tùy biến class cho nhãn `<label>`. |
278
+ | `helperClassName` | `string` | `""` | Tùy biến class cho text hướng dẫn / thông báo lỗi. |
279
+
280
+ ---
281
+
282
+ ### `UploadAvatarConfig`
283
+
284
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
285
+ | :--- | :--- | :--- | :--- |
286
+ | `isRequired` | `boolean` | `false` | Đánh dấu bắt buộc nhập (hiển thị dấu `*` đỏ cạnh label). |
287
+ | `isInvalid` | `boolean` | `false` | Đánh dấu trường không hợp lệ (kích hoạt viền đỏ và `aria-invalid`). |
288
+ | `isLoading` | `boolean` | `false` | Đang tải tệp, khóa tương tác và kích hoạt `aria-busy`. |
289
+ | `showSpinner` | `boolean` | `false` | Hiển thị biểu tượng xoay spinner overlay khi `isLoading=true`. |
290
+ | `isClearable` | `boolean` | `false` | Cho phép hiển thị nút xóa nhanh avatar. |
291
+ | `isFullWidth` | `boolean` | `false` | Mở rộng container bao ngoài chiếm 100% chiều rộng khung cha. |
292
+
293
+ ---
294
+
295
+ ### `UploadAvatarCropOptions`
296
+
297
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
298
+ | :--- | :--- | :--- | :--- |
299
+ | `aspectRatio` | `number` | `1` | Tỷ lệ khung hình cắt (mặc định 1:1 cho avatar). |
300
+ | `cropShape` | `'round' \| 'rect'` | `'round'` cho circle, `'rect'` cho square | Hình dạng mặt nạ cắt xén ảnh trong modal. |
301
+ | `showGrid` | `boolean` | `true` | Hiển thị đường lưới tỷ lệ 3x3 khi cắt ảnh. |
302
+ | `minZoom` | `number` | `1` | Mức độ thu nhỏ tối thiểu. |
303
+ | `maxZoom` | `number` | `4` | Mức độ phóng to tối đa. |
304
+ | `modalTitle` | `string` | `"Cắt ảnh đại diện"` | Tiêu đề thanh header của modal cắt ảnh. |
305
+
306
+ ---
307
+
308
+ ## 🛡️ Khả năng tiếp cận (Accessibility)
309
+
310
+ - **WAI-ARIA & Keyboard Navigation**:
311
+ - Phần tử avatar được gán `role="button"`, `tabIndex={0}`, có thể điều hướng bằng phím <kbd>Tab</kbd>.
312
+ - Nhấn <kbd>Space</kbd> hoặc <kbd>Enter</kbd> để kích hoạt hộp thoại chọn tệp tin.
313
+ - Khi đã có ảnh, nhấn <kbd>Delete</kbd> hoặc <kbd>Backspace</kbd> sẽ xóa ảnh ngay lập tức.
314
+ - **Screen Reader Support**:
315
+ - Tích hợp vùng chứa `aria-live="polite"` (`sr-only`) tự động thông báo trạng thái cập nhật hoặc xóa ảnh cho người khiếm thị bằng tiếng Việt chuẩn.
316
+ - Liên kết tự động giữa label, helperText, errorMessage với input thông qua `aria-describedby` và `htmlFor`.
317
+ - **Trạng thái vô hiệu hóa**:
318
+ - Khi `disabled={true}` hoặc `isLoading={true}`, phần tử tự động thiết lập `aria-disabled="true"`, `aria-busy="true"` và ngăn chặn toàn bộ sự kiện click, kéo thả.