@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,245 @@
1
+ # 📄 Upload File Component Suite (`@openway/ui`)
2
+
3
+ Bộ component **UploadFile** toàn diện, linh hoạt và hiệu năng cao dành cho việc tải lên, kéo thả (Drag & Drop), quản lý danh sách, xem trước (Preview) và kiểm tra các loại tệp tin tài liệu (PDF, Word, Excel, PowerPoint, ZIP, Media, Code, v.v.).
4
+
5
+ Component được xây dựng đồng bộ 100% với hệ thống **Design System** của `@openway/ui`, tương đương cấu trúc với `UploadImage`, sử dụng chung kiểu dữ liệu `PreviewFile` và tích hợp sâu với `<FileContainer>` để xem trước tệp tin tức thì.
6
+
7
+ ---
8
+
9
+ ## 🌟 Điểm nổi bật
10
+
11
+ - **Kiểu dữ liệu đồng nhất toàn hệ sinh thái**:
12
+ - Tái sử dụng trực tiếp kiểu dữ liệu `PreviewFile` (`File | ServerFile | string`) từ `file-preview` tương thích hoàn toàn với `UploadImage` và `FileContainer`.
13
+ - **Hệ thống nhận diện `FileIcon` thông minh**:
14
+ - Tự động phân tích phần mở rộng và MIME type để hiển thị icon tài liệu và huy hiệu màu sắc đặc trưng:
15
+ - 🔴 **PDF**: Huy hiệu đỏ (`PDF`)
16
+ - 🔵 **Word / Docs** (`doc`, `docx`): Huy hiệu xanh dương (`DOC`)
17
+ - 🟢 **Excel / Sheets** (`xls`, `xlsx`, `csv`): Huy hiệu xanh lá (`XLS`)
18
+ - 🟠 **PowerPoint** (`ppt`, `pptx`): Huy hiệu cam (`PPT`)
19
+ - 🟡 **Archive / ZIP** (`zip`, `rar`, `7z`, `tar`): Huy hiệu vàng/hổ phách (`ZIP`)
20
+ - 🟣 **Audio**: Huy hiệu tím (`AUD`)
21
+ - 🔷 **Video**: Huy hiệu xanh ngọc (`VID`)
22
+ - ⬛ **Code**: Huy hiệu chàm (`DEV`)
23
+ - ⚪ **Khác**: Huy hiệu trung tính (`FILE`)
24
+ - **3 Chế độ hiển thị (`viewMode`)**:
25
+ - `dropzone` *(mặc định)*: Khung viền nét đứt lớn hỗ trợ kéo thả và nhấn chọn tệp. Danh sách các tệp tin đã tải lên luôn hiển thị trực quan ngay bên dưới.
26
+ - `button`: Nút bấm kích hoạt tải tệp gọn gàng kèm icon, thích hợp cho toolbar hoặc khu vực hẹp.
27
+ - `compact`: Khung kéo thả thu gọn dạng thanh ngang 1 dòng tiết kiệm diện tích.
28
+ - **2 Kiểu hiển thị danh sách (`listType`)**:
29
+ - `list` *(mặc định)*: Dạng hàng ngang chi tiết với icon, tên file, dung lượng format (`formatBytes`), thanh tiến trình và các nút hành động.
30
+ - `grid`: Bố cục dạng thẻ (card) trên lưới responsive.
31
+ - **Theo dõi tiến trình & Trạng thái lỗi**:
32
+ - Hỗ trợ hiển thị thanh tiến trình mini động khi `status: "uploading"` kèm tỷ lệ `%` hoàn thành.
33
+ - Hiển thị thông báo lỗi chi tiết khi `status: "error"` và nút **Thử lại** (`onRetry`).
34
+ - **Tích hợp sẵn xem trước & Tải xuống**:
35
+ - Click vào tên tệp hoặc nút Xem trước (`EyeIcon`) tự động kích hoạt modal `<FileContainer>` đa năng.
36
+ - Hỗ trợ tải trực tiếp tệp về máy tính qua `downloadFile` hoặc callback `onDownload`.
37
+ - **Chuẩn Design System**:
38
+ - 5 kích thước: `xs`, `sm`, `md`, `lg`, `xl`.
39
+ - 7 bảng màu: `primary`, `secondary`, `neutral`, `error`, `success`, `warning`, `info`.
40
+ - 4 biến thể: `outline`, `filled`, `ghost`, `other`.
41
+ - Bo góc: `none`, `sm`, `md`, `lg`, `xl`, `full`.
42
+ - Safe Config Fallback với `getSafeConfig`.
43
+ - Nhãn form hỗ trợ `isRequired` (dấu sao đỏ) và đổi màu khi focus (`group-focus-within/field`).
44
+ - **Hỗ trợ tiếp cận (A11y)**:
45
+ - Tương thích bàn phím: <kbd>Tab</kbd>, <kbd>Enter</kbd> / <kbd>Space</kbd> để chọn file, <kbd>Delete</kbd> / <kbd>Backspace</kbd> để xóa tệp.
46
+ - Vùng thông báo động Screen Reader qua `aria-live="polite"`.
47
+
48
+ ---
49
+
50
+ ## 🚀 Cài đặt & Import
51
+
52
+ ```tsx
53
+ import {
54
+ UploadFile,
55
+ UploadFileDropzone,
56
+ UploadFileList,
57
+ UploadFileItemRow,
58
+ FileIcon,
59
+ formatBytes,
60
+ getFileCategory,
61
+ } from "@openway/ui";
62
+
63
+ import type {
64
+ UploadFileProps,
65
+ UploadFileConfig,
66
+ UploadFileViewMode,
67
+ UploadFileListType,
68
+ UploadFileSize,
69
+ UploadFileColor,
70
+ UploadFileVariant,
71
+ UploadFileRadius,
72
+ PreviewFile,
73
+ ServerFile,
74
+ } from "@openway/ui";
75
+ ```
76
+
77
+ ---
78
+
79
+ ## 📖 Hướng dẫn sử dụng
80
+
81
+ ### 1. Chế độ Dropzone mặc định (1 tệp hoặc nhiều tệp)
82
+
83
+ ```tsx
84
+ import { useState } from "react";
85
+ import { UploadFile, PreviewFile } from "@openway/ui";
86
+
87
+ export function BasicExample() {
88
+ const [files, setFiles] = useState<PreviewFile[]>([]);
89
+
90
+ return (
91
+ <UploadFile
92
+ value={files}
93
+ onChange={setFiles}
94
+ label="Tài liệu đính kèm"
95
+ config={{ isRequired: true }}
96
+ helperText="Hỗ trợ PDF, DOCX, XLSX tối đa 10MB"
97
+ maxSize={10 * 1024 * 1024}
98
+ />
99
+ );
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ### 2. Bố cục danh sách dạng Grid (`listType="grid"`)
106
+
107
+ ```tsx
108
+ <UploadFile
109
+ listType="grid"
110
+ config={{ multiple: true }}
111
+ label="Hồ sơ nghiệm thu dự án"
112
+ defaultValue={[
113
+ { id: "1", name: "bien-ban-nghiem-thu.pdf", size: 1450000 },
114
+ { id: "2", name: "phu-luc-khoi-luong.xlsx", size: 850000 },
115
+ ]}
116
+ />
117
+ ```
118
+
119
+ ---
120
+
121
+ ### 3. Chế độ Button Trigger
122
+
123
+ ```tsx
124
+ <UploadFile
125
+ viewMode="button"
126
+ buttonText="Chọn tài liệu"
127
+ config={{ multiple: true }}
128
+ label="Hồ sơ đính kèm"
129
+ onChange={(items) => console.log("Danh sách tệp:", items)}
130
+ />
131
+ ```
132
+
133
+ ---
134
+
135
+ ### 4. Chế độ Compact Dropzone (Thanh ngang 1 dòng)
136
+
137
+ ```tsx
138
+ <UploadFile
139
+ viewMode="compact"
140
+ label="Bản sao chứng minh thư"
141
+ dropzoneTitle="Kéo thả CMND/CCCD hoặc nhấn để duyệt"
142
+ />
143
+ ```
144
+
145
+ ---
146
+
147
+ ### 5. Quản lý trạng thái Uploading & Lỗi
148
+
149
+ ```tsx
150
+ const fileList: ServerFile[] = [
151
+ {
152
+ id: "1",
153
+ name: "bao-cao-tai-chinh.xlsx",
154
+ size: 2500000,
155
+ status: "uploading",
156
+ progress: 75,
157
+ },
158
+ {
159
+ id: "2",
160
+ name: "ho-so-loi.zip",
161
+ size: 15000000,
162
+ status: "error",
163
+ error: "Dung lượng vượt quá cấu hình máy chủ",
164
+ },
165
+ ];
166
+
167
+ <UploadFile
168
+ value={fileList}
169
+ onRetry={(item, index) => {
170
+ console.log("Thử lại tải tệp:", item);
171
+ }}
172
+ />
173
+ ```
174
+
175
+ ---
176
+
177
+ ### 6. Sử dụng độc lập `FileIcon`
178
+
179
+ ```tsx
180
+ import { FileIcon } from "@openway/ui";
181
+
182
+ <div className="flex items-center gap-3">
183
+ <FileIcon fileName="bao-cao.pdf" size="md" />
184
+ <FileIcon fileName="bang-luong.xlsx" size="md" />
185
+ <FileIcon fileName="du-an.zip" size="md" />
186
+ <FileIcon fileName="source-code.ts" size="md" />
187
+ </div>
188
+ ```
189
+
190
+ ---
191
+
192
+ ## ⚙️ Bảng Props `UploadFileProps`
193
+
194
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
195
+ | :--- | :--- | :--- | :--- |
196
+ | `config` | `UploadFileConfig` | `undefined` | Cấu hình tập trung các cờ boolean (`multiple`, `isRequired`, `isInvalid`, `isLoading`, `showFileList`...) |
197
+ | `value` | `PreviewFile[] \| PreviewFile \| null` | `undefined` | Danh sách tệp tin (Controlled mode) |
198
+ | `defaultValue` | `PreviewFile[] \| PreviewFile \| null` | `undefined` | Giá trị ban đầu (Uncontrolled mode) |
199
+ | `onChange` | `(items: PreviewFile[]) => void` | `undefined` | Callback khi danh sách tệp thay đổi |
200
+ | `onRemove` | `(item: PreviewFile, index: number) => void` | `undefined` | Callback khi xóa 1 tệp tin |
201
+ | `onPreview` | `(item: PreviewFile) => void` | `undefined` | Callback khi bấm xem trước |
202
+ | `onDownload` | `(item: PreviewFile) => void` | `undefined` | Callback khi bấm tải xuống |
203
+ | `onRetry` | `(item: PreviewFile, index: number) => void` | `undefined` | Callback khi bấm nút thử lại |
204
+ | `viewMode` | `'dropzone' \| 'button' \| 'compact'` | `'dropzone'` | Chế độ hiển thị giao diện tải |
205
+ | `listType` | `'list' \| 'grid'` | `'list'` | Kiểu bố cục danh sách tệp |
206
+ | `shape` | `'rectangle' \| 'square'` | `'rectangle'` | Hình dạng khung Dropzone |
207
+ | `maxCount` | `number` | `1` (khi !multiple) | Số lượng tệp tối đa |
208
+ | `maxSize` | `number` | `undefined` | Dung lượng tệp tối đa (bytes) |
209
+ | `minSize` | `number` | `undefined` | Dung lượng tệp tối thiểu (bytes) |
210
+ | `accept` | `string \| Accept` | `undefined` | Định dạng cho phép (ví dụ: `".pdf,.docx"`) |
211
+ | `beforeUpload` | `(file: File) => boolean \| string \| Promise<...>` | `undefined` | Hook kiểm tra tệp trước khi nạp |
212
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích thước component |
213
+ | `color` | `'primary' \| 'secondary' \| 'neutral' \| ...` | `'primary'` | Bảng màu chủ đề |
214
+ | `variant` | `'outline' \| 'filled' \| 'ghost' \| 'other'` | `'outline'` | Biến thể đường viền/nền |
215
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Độ bo góc |
216
+ | `label` | `ReactNode` | `undefined` | Tiêu đề nhãn của trường form |
217
+ | `labelClassName` | `string` | `""` | Lớp CSS tùy biến cho nhãn |
218
+ | `helperText` | `ReactNode` | `undefined` | Văn bản gợi ý |
219
+ | `errorMessage`| `ReactNode` | `undefined` | Thông báo lỗi |
220
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa tương tác |
221
+ | `readOnly` | `boolean` | `false` | Chế độ chỉ đọc |
222
+ | `renderItem` | `(item, index, actions) => ReactNode` | `undefined` | Tùy biến render từng mục tệp |
223
+
224
+ ### Cấu trúc `UploadFileConfig`
225
+
226
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
227
+ | :--- | :--- | :--- | :--- |
228
+ | `multiple` | `boolean` | `false` | Cho phép tải lên nhiều tệp tin cùng lúc |
229
+ | `isRequired` | `boolean` | `false` | Đánh dấu trường bắt buộc nhập (dấu * đỏ) |
230
+ | `isInvalid` | `boolean` | `false` | Đánh dấu trạng thái lỗi |
231
+ | `isLoading` | `boolean` | `false` | Trạng thái đang tải / xử lý |
232
+ | `showSpinner` | `boolean` | `false` | Hiển thị biểu tượng xoay spinner khi đang loading |
233
+ | `showFileList`| `boolean` | `true` | Hiển thị danh sách tệp bên dưới |
234
+ | `showPreviewButton` | `boolean` | `true` | Hiển thị nút xem trước trên từng mục tệp |
235
+ | `showDownloadButton`| `boolean` | `true` | Hiển thị nút tải xuống trên từng mục tệp |
236
+ | `showRemoveButton` | `boolean` | `true` | Hiển thị nút xóa trên từng mục tệp |
237
+
238
+ ---
239
+
240
+ ## ⌨️ Phím tắt & Trợ năng (Accessibility)
241
+
242
+ - <kbd>Tab</kbd>: Di chuyển tiêu điểm bàn phím vào vùng Dropzone, nút Tải hoặc từng mục tệp tin.
243
+ - <kbd>Enter</kbd> hoặc <kbd>Space</kbd>: Mở hộp thoại chọn tệp của hệ thống hoặc kích hoạt xem trước.
244
+ - <kbd>Delete</kbd> hoặc <kbd>Backspace</kbd>: Xóa tệp đang được focus khỏi danh sách (trừ chế độ `readOnly` hoặc `disabled`).
245
+ - Tự động phát âm thanh/thông báo trợ năng qua vùng `<div aria-live="polite" className="sr-only">`.
@@ -0,0 +1,126 @@
1
+ # 🖼️ UploadImage Component (`@openway/ui`)
2
+
3
+ Component tải lên hình ảnh chuyên nghiệp, hỗ trợ kéo thả (`drag-and-drop`), xem trước ảnh tức thời (`preview`), crop/cắt ảnh trực quan, hiển thị dạng danh sách hoặc lưới thẻ (`picture wall`), và tuân thủ các quy chuẩn **Design System** & **Accessibility**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **3 Chế độ hiển thị (`viewMode`)**:
10
+ - `dropzone` *(mặc định)*: Khung kéo thả lớn với icon, tiêu đề và mô tả trực quan.
11
+ - `card-grid`: Lưới thẻ ảnh dạng thumbnail vuông (picture wall), nút thêm ảnh hiển thị như 1 ô trong lưới.
12
+ - `button`: Nút bấm kích hoạt mở hộp thoại chọn ảnh gọn gàng.
13
+ - **Tích hợp cắt ảnh linh hoạt (`enableCrop`)**:
14
+ - Hỗ trợ modal crop ảnh trực quan (`react-easy-crop`) với tỷ lệ tùy biến (`cropAspectRatio`).
15
+ - Hỗ trợ xoay, zoom và kiểm soát chất lượng ảnh sau crop.
16
+ - **Xem trước ảnh tiện lợi (`enablePreview`)**:
17
+ - Tích hợp modal phóng to ảnh đầy đủ màn hình, xem chi tiết kích thước và tên tệp.
18
+ - **Kiểm soát dung lượng & Định dạng tệp**:
19
+ - Giới hạn dung lượng tối đa (`maxSize`), tối thiểu (`minSize`).
20
+ - Giới hạn số lượng tệp tải lên (`maxFiles`).
21
+ - Tùy chỉnh danh sách định dạng ảnh cho phép (`accept`, ví dụ: `image/jpeg`, `image/png`, `image/webp`).
22
+ - **Phản hồi lỗi & Trạng thái tải trực quan**:
23
+ - Báo lỗi tự động khi vượt quá dung lượng hoặc sai loại tệp.
24
+ - Thanh tiến trình tải lên (`progress`) và hiệu ứng hover sinh động.
25
+
26
+ ---
27
+
28
+ ## 🚀 Cài đặt & Import
29
+
30
+ ```tsx
31
+ import { UploadImage } from "@openway/ui";
32
+ import type { UploadImageProps, UploadImageFileItem } from "@openway/ui";
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 📖 Hướng dẫn sử dụng
38
+
39
+ ### 1. UploadImage cơ bản dạng Dropzone
40
+
41
+ ```tsx
42
+ import { useState } from "react";
43
+ import { UploadImage, UploadImageFileItem } from "@openway/ui";
44
+
45
+ export function BasicUploadImageExample() {
46
+ const [files, setFiles] = useState<UploadImageFileItem[]>([]);
47
+
48
+ return (
49
+ <div className="max-w-md">
50
+ <UploadImage
51
+ label="Ảnh đại diện bài viết"
52
+ value={files}
53
+ onChange={(newFiles) => setFiles(newFiles)}
54
+ maxFiles={1}
55
+ maxSize={5 * 1024 * 1024} // 5MB
56
+ helperText="Định dạng PNG, JPG, WEBP tối đa 5MB"
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ---
64
+
65
+ ### 2. Dạng Lưới thẻ (Picture Wall - `card-grid`)
66
+
67
+ Thích hợp cho tải album ảnh sản phẩm hoặc thư viện ảnh:
68
+
69
+ ```tsx
70
+ import { useState } from "react";
71
+ import { UploadImage, UploadImageFileItem } from "@openway/ui";
72
+
73
+ export function PictureWallExample() {
74
+ const [gallery, setGallery] = useState<UploadImageFileItem[]>([]);
75
+
76
+ return (
77
+ <div className="max-w-xl">
78
+ <UploadImage
79
+ viewMode="card-grid"
80
+ shape="square"
81
+ maxFiles={8}
82
+ value={gallery}
83
+ onChange={(newFiles) => setGallery(newFiles)}
84
+ label="Bộ sưu tập ảnh sản phẩm"
85
+ enablePreview
86
+ />
87
+ </div>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ### 3. Kích hoạt tính năng Crop ảnh (`enableCrop`)
95
+
96
+ ```tsx
97
+ <UploadImage
98
+ label="Tải ảnh bìa (Tỷ lệ 16:9)"
99
+ maxFiles={1}
100
+ enableCrop
101
+ cropAspectRatio={16 / 9}
102
+ value={files}
103
+ onChange={setFiles}
104
+ />
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 🎛️ Bảng Props Chi tiết
110
+
111
+ | Tên Prop | Kiểu dữ liệu | Mặc định | Mô tả |
112
+ | :--- | :--- | :--- | :--- |
113
+ | `value` | `UploadImageFileItem[]` | `[]` | Danh sách tệp ảnh hiện tại. |
114
+ | `onChange` | `(files: UploadImageFileItem[]) => void` | `undefined` | Callback khi danh sách ảnh thay đổi (thêm, xóa, crop). |
115
+ | `viewMode` | `"dropzone" \| "card-grid" \| "button"` | `"dropzone"` | Kiểu giao diện tải ảnh. |
116
+ | `shape` | `"rectangle" \| "square"` | `"rectangle"` | Hình dạng khung hiển thị ảnh. |
117
+ | `maxFiles` | `number` | `1` | Số lượng tệp ảnh tối đa được phép tải. |
118
+ | `maxSize` | `number` | `10 * 1024 * 1024` | Kích thước tối đa mỗi tệp (bytes). |
119
+ | `accept` | `Record<string, string[]>` | Ảnh thông dụng | Đối tượng định dạng MIME types cho phép. |
120
+ | `enableCrop` | `boolean` | `false` | Mở hộp thoại crop ảnh trước khi thêm vào danh sách. |
121
+ | `cropAspectRatio` | `number` | `1` | Tỷ lệ khung hình khi crop (ví dụ: `1` cho ảnh vuông, `16/9` cho banner). |
122
+ | `enablePreview` | `boolean` | `true` | Cho phép nhấn vào ảnh để mở modal xem ảnh kích thước lớn. |
123
+ | `disabled` | `boolean` | `false` | Khóa chức năng tải ảnh. |
124
+ | `color` | `ThemeColor` | `"primary"` | Chủ đề màu sắc theo Design System. |
125
+ | `size` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Kích thước khung tải ảnh. |
126
+ | `radius` | `Radius` | `"md"` | Độ bo góc của khung tải và thumbnail. |
@@ -0,0 +1,92 @@
1
+ # ⏱️ Hooks `useDebounce` & `useDebouncedCallback` (`@openway/ui`)
2
+
3
+ Bộ đôi hook tối ưu hiệu năng giúp hoãn thực thi và chống spam request khi người dùng nhập dữ liệu hoặc thao tác liên tục.
4
+
5
+ ---
6
+
7
+ ## 🌟 Phân biệt 2 Hook
8
+
9
+ - **`useDebounce<T>(value, delay)`**: Dùng để debounce một **giá trị (value)** (ví dụ: chuỗi tìm kiếm text input, giá trị thanh trượt slider). Khi người dùng ngừng thay đổi sau khoảng thời gian `delay`, giá trị mới được cập nhật.
10
+ - **`useDebouncedCallback(callback, delay)`**: Dùng để debounce một **hàm callback** (ví dụ: hàm gọi API, resize window, autosave form). Trả về hàm `{ debounced, cancel }`. Luôn giữ tham chiếu callback mới nhất mà không gây re-render dư thừa.
11
+
12
+ ---
13
+
14
+ ## 🚀 Import
15
+
16
+ ```tsx
17
+ import { useDebounce, useDebouncedCallback } from "@openway/ui";
18
+ ```
19
+
20
+ ---
21
+
22
+ ## 📖 Hướng dẫn sử dụng
23
+
24
+ ### 1. Sử dụng `useDebounce` với giá trị
25
+
26
+ ```tsx
27
+ import { useState, useEffect } from "react";
28
+ import { Input, useDebounce } from "@openway/ui";
29
+
30
+ export function SearchFilter() {
31
+ const [keyword, setKeyword] = useState("");
32
+ // debouncedKeyword chỉ thay đổi sau khi ngừng gõ 400ms
33
+ const debouncedKeyword = useDebounce(keyword, 400);
34
+
35
+ useEffect(() => {
36
+ if (debouncedKeyword) {
37
+ console.log("Tìm kiếm với từ khóa:", debouncedKeyword);
38
+ }
39
+ }, [debouncedKeyword]);
40
+
41
+ return (
42
+ <Input
43
+ label="Tìm kiếm"
44
+ value={keyword}
45
+ onChange={(e) => setKeyword(e.target.value)}
46
+ placeholder="Gõ từ khóa..."
47
+ />
48
+ );
49
+ }
50
+ ```
51
+
52
+ ---
53
+
54
+ ### 2. Sử dụng `useDebouncedCallback` với hàm xử lý
55
+
56
+ ```tsx
57
+ import { useDebouncedCallback, Button } from "@openway/ui";
58
+
59
+ export function AutoSaveForm() {
60
+ const { debounced: handleAutoSave, cancel } = useDebouncedCallback((formData: Record<string, unknown>) => {
61
+ console.log("Tự động lưu dữ liệu lên server:", formData);
62
+ }, 500);
63
+
64
+ return (
65
+ <div className="space-y-2">
66
+ <Button onClick={() => handleAutoSave({ title: "Bản nháp mới" })}>
67
+ Cập nhật dữ liệu
68
+ </Button>
69
+ <Button variant="ghost" color="error" onClick={cancel}>
70
+ Hủy lưu tự động
71
+ </Button>
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## 🎛️ Bảng Tham số
80
+
81
+ ### `useDebounce<T>(value: T, delay?: number): T`
82
+ | Tham số | Kiểu dữ liệu | Mặc định | Mô tả |
83
+ | :--- | :--- | :--- | :--- |
84
+ | `value` | `T` | **Bắt buộc** | Giá trị cần debounce. |
85
+ | `delay` | `number` | `300` | Thời gian hoãn tính theo mili-giây (ms). |
86
+
87
+ ### `useDebouncedCallback(callback, delay?: number)`
88
+ | Tham số | Kiểu dữ liệu | Mặc định | Mô tả |
89
+ | :--- | :--- | :--- | :--- |
90
+ | `callback` | `(...args: Args) => R` | **Bắt buộc** | Hàm cần debounce. |
91
+ | `delay` | `number` | `300` | Thời gian hoãn tính theo mili-giây (ms). |
92
+ - Trả về object: `{ debounced: (...args) => void, cancel: () => void }`.
@@ -0,0 +1,95 @@
1
+ # ♾️ Hook `useInfiniteScroll` (`@openway/ui`)
2
+
3
+ Hook React độc lập, hiệu năng cao phục vụ cơ chế tải dữ liệu vô tận (Infinite Scroll) dựa trên 100% **IntersectionObserver** nguyên bản của trình duyệt.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Zero Main-Thread Overhead**: Hoàn toàn không gắn event listener `scroll` thủ công trên container, loại bỏ hiện tượng giật lag khung hình khi cuộn nhanh.
10
+ - **Pure React 19 Compliant**: Không sử dụng ref dư thừa để lưu primitives (`hasMore`, `isLoading`, `disabled`), hoàn toàn tuân thủ quy chuẩn render của React 19.
11
+ - **Chống Request Trùng lặp (Mutex Lock)**: Sử dụng duy nhất một cờ khóa an toàn `isTriggeringRef` để ngăn chặn các lượt gọi API async liên tiếp trong cùng một chu kỳ render.
12
+ - **Tự động kích hoạt khi hoàn tất tải**: Nếu dữ liệu trang trước đã tải xong (`isLoading: false`) mà phần tử sentinel vẫn đang nằm trong tầm quan sát, hook sẽ tự động kích hoạt tải tiếp trang kế tiếp một cách mượt mà.
13
+
14
+ ---
15
+
16
+ ## 🚀 Import
17
+
18
+ ```tsx
19
+ import { useInfiniteScroll } from "@openway/ui";
20
+ import type { UseInfiniteScrollOptions, UseInfiniteScrollReturn } from "@openway/ui";
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 📖 Hướng dẫn sử dụng
26
+
27
+ ### Sử dụng với Danh sách thẻ (Card List / Feed)
28
+
29
+ ```tsx
30
+ import { useState } from "react";
31
+ import { useInfiniteScroll, Skeleton } from "@openway/ui";
32
+
33
+ export function PostFeed() {
34
+ const [posts, setPosts] = useState<string[]>(["Bài viết 1", "Bài viết 2"]);
35
+ const [hasMore, setHasMore] = useState(true);
36
+ const [isLoading, setIsLoading] = useState(false);
37
+
38
+ const loadMorePosts = async () => {
39
+ setIsLoading(true);
40
+ // Giả lập gọi API
41
+ await new Promise((resolve) => setTimeout(resolve, 800));
42
+ setPosts((prev) => [
43
+ ...prev,
44
+ `Bài viết ${prev.length + 1}`,
45
+ `Bài viết ${prev.length + 2}`,
46
+ ]);
47
+ if (posts.length > 20) setHasMore(false);
48
+ setIsLoading(false);
49
+ };
50
+
51
+ const { sentinelRef } = useInfiniteScroll({
52
+ onLoadMore: loadMorePosts,
53
+ hasMore,
54
+ isLoading,
55
+ rootMargin: "150px", // Bắt đầu tải trước khi chạm đáy 150px
56
+ });
57
+
58
+ return (
59
+ <div className="max-w-md mx-auto space-y-3">
60
+ {posts.map((post, idx) => (
61
+ <div key={idx} className="p-4 border rounded-lg shadow-sm bg-white dark:bg-neutral-900">
62
+ {post}
63
+ </div>
64
+ ))}
65
+
66
+ {/* Phần tử Sentinel để IntersectionObserver theo dõi */}
67
+ <div ref={sentinelRef} className="py-2">
68
+ {isLoading && (
69
+ <Skeleton lines={2} height="2.5rem" className="w-full" />
70
+ )}
71
+ </div>
72
+
73
+ {!hasMore && (
74
+ <p className="text-center text-xs text-neutral-400 py-2">
75
+ Đã tải hết toàn bộ bài viết
76
+ </p>
77
+ )}
78
+ </div>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 🎛️ Bảng Options
86
+
87
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
88
+ | :--- | :--- | :--- | :--- |
89
+ | `onLoadMore` | `() => void \| Promise<void>` | **Bắt buộc** | Callback kích hoạt khi sentinel xuất hiện trong tầm nhìn. |
90
+ | `hasMore` | `boolean` | `true` | Còn dữ liệu trang sau hay không. Nếu `false`, hook không kích hoạt nữa. |
91
+ | `isLoading` | `boolean` | `false` | Trạng thái đang tải dữ liệu. Ngăn chặn kích hoạt lượt tải mới khi lượt cũ chưa hoàn tất. |
92
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ cơ chế theo dõi. |
93
+ | `rootMargin` | `string` | `"100px"` | Khoảng cách biên quan sát trước khi người dùng chạm tới đáy. |
94
+ | `threshold` | `number \| number[]` | `0` | Ngưỡng hiển thị của phần tử sentinel (từ `0` đến `1`). |
95
+ | `root` | `Element \| null \| RefObject` | `null` | Khung cuộn gốc. Mặc định là viewport trình duyệt. |