@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,275 @@
1
+ # 🔽 Dropdown Component (`@owa/ui`)
2
+
3
+ Bộ component **Dropdown Menu** xây dựng theo mô hình **Compound Component** trên nền tảng **`@floating-ui/react`**, hỗ trợ **điều hướng bàn phím WAI-ARIA Menu hoàn chỉnh**, **tự động căn chỉnh vị trí thông minh (flip/shift/offset)**, **hiệu ứng chuyển động mượt mà**, và tương thích hoàn toàn với **React 19 / React Compiler**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Compound Component Pattern**: Cấu trúc module linh hoạt gồm `<Dropdown>`, `<DropdownTrigger>`, `<DropdownMenu>`, `<DropdownItem>`, `<DropdownHeader>`, `<DropdownGroup>`, `<DropdownSeparator>`.
10
+ - **Hỗ trợ React 19 & React Compiler**: Áp dụng mô hình `Slot` component giúp xử lý `ref` sạch sẽ qua JSX, không gây lỗi runtime hay cảnh báo `Cannot access refs during render`.
11
+ - **Hệ thống z-index đồng bộ**: Tích hợp với `DEFAULT_Z_INDEX.DROPDOWN` (mặc định `50`) từ hệ thống constant toàn cục.
12
+ - **Tự động định vị thông minh**: Tính toán vị trí nổi thông minh qua `@floating-ui/react` với các middleware `offset`, `flip`, `shift`.
13
+ - **Luôn nổi trên cùng (Floating Portal)**: Menu luôn được render qua `<FloatingPortal>` để không bị ảnh hưởng bởi CSS `overflow: hidden` hoặc `z-index` của cha.
14
+ - **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` giúp lấy an toàn `sizeConfig`, `radiusConfig`, `colorConfig`, ngăn ngừa crash giao diện khi nhận giá trị không hợp lệ.
15
+ - **WAI-ARIA Accessibility**:
16
+ - `role="menu"` cho dropdown menu container.
17
+ - `role="menuitem"` cho từng item lựa chọn.
18
+ - `role="group"` cho nhóm menu item.
19
+ - `aria-expanded`, `aria-haspopup="menu"`, `aria-disabled`.
20
+ - Hỗ trợ đầy đủ phím tắt bàn phím: `ArrowDown`, `ArrowUp`, `Home`, `End`, `Enter`, `Space`, `Escape`.
21
+ - **Nhiều tùy chọn kích thước & màu sắc**:
22
+ - 5 kích cỡ: `xs`, `sm`, `md` (*mặc định*), `lg`, `xl`.
23
+ - 7 chủ đề màu: `primary`, `secondary`, `neutral`, `error`, `success`, `warning`, `info`.
24
+ - 6 cấp độ bo góc: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
25
+
26
+ ---
27
+
28
+ ## 🚀 Cài đặt & Import
29
+
30
+ ```tsx
31
+ import {
32
+ Dropdown,
33
+ DropdownTrigger,
34
+ DropdownMenu,
35
+ DropdownItem,
36
+ DropdownHeader,
37
+ DropdownGroup,
38
+ DropdownSeparator,
39
+ } from "@owa/ui";
40
+ import type {
41
+ DropdownProps,
42
+ DropdownTriggerProps,
43
+ DropdownMenuProps,
44
+ DropdownItemProps,
45
+ DropdownHeaderProps,
46
+ DropdownGroupProps,
47
+ DropdownSeparatorProps,
48
+ DropdownPlacement,
49
+ DropdownSize,
50
+ DropdownRadius,
51
+ DropdownColor,
52
+ } from "@owa/ui";
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 📖 Hướng dẫn sử dụng
58
+
59
+ ### 1. Menu cơ bản
60
+
61
+ ```tsx
62
+ import {
63
+ Dropdown,
64
+ DropdownTrigger,
65
+ DropdownMenu,
66
+ DropdownItem,
67
+ Button,
68
+ } from "@owa/ui";
69
+
70
+ export function BasicDropdown() {
71
+ return (
72
+ <Dropdown>
73
+ <DropdownTrigger>
74
+ <Button>Mở Menu</Button>
75
+ </DropdownTrigger>
76
+ <DropdownMenu>
77
+ <DropdownItem onClick={() => console.log("Hồ sơ")}>Hồ sơ cá nhân</DropdownItem>
78
+ <DropdownItem onClick={() => console.log("Cài đặt")}>Cài đặt tài khoản</DropdownItem>
79
+ <DropdownItem isDanger onClick={() => console.log("Đăng xuất")}>Đăng xuất</DropdownItem>
80
+ </DropdownMenu>
81
+ </Dropdown>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ### 2. Menu đầy đủ tính năng (Header, Group, Icon, Shortcut, Danger)
89
+
90
+ ```tsx
91
+ import {
92
+ Dropdown,
93
+ DropdownTrigger,
94
+ DropdownMenu,
95
+ DropdownHeader,
96
+ DropdownGroup,
97
+ DropdownItem,
98
+ DropdownSeparator,
99
+ Button,
100
+ } from "@owa/ui";
101
+ import { UserIcon, SettingsIcon, LockIcon, TrashIcon } from "@/components/icons";
102
+
103
+ export function AdvancedDropdown() {
104
+ return (
105
+ <Dropdown placement="bottom-start" size="md">
106
+ <DropdownTrigger>
107
+ <Button variant="outline">Tài khoản của tôi</Button>
108
+ </DropdownTrigger>
109
+ <DropdownMenu minWidth={240}>
110
+ <DropdownHeader>
111
+ <div className="font-semibold text-neutral-900">Nguyễn Văn A</div>
112
+ <div className="text-xs text-neutral-500">vana@example.com</div>
113
+ </DropdownHeader>
114
+ <DropdownSeparator />
115
+
116
+ <DropdownGroup title="Quản lý">
117
+ <DropdownItem icon={<UserIcon />} shortcut="⌘P" onClick={() => {}}>
118
+ Hồ sơ cá nhân
119
+ </DropdownItem>
120
+ <DropdownItem icon={<SettingsIcon />} shortcut="⌘S" onClick={() => {}}>
121
+ Cài đặt
122
+ </DropdownItem>
123
+ </DropdownGroup>
124
+ <DropdownSeparator />
125
+
126
+ <DropdownGroup title="Bảo mật">
127
+ <DropdownItem icon={<LockIcon />} onClick={() => {}}>
128
+ Đổi mật khẩu
129
+ </DropdownItem>
130
+ <DropdownItem
131
+ icon={<TrashIcon />}
132
+ isDanger
133
+ shortcut="⌘⌫"
134
+ onClick={() => {}}
135
+ >
136
+ Xóa tài khoản
137
+ </DropdownItem>
138
+ </DropdownGroup>
139
+ </DropdownMenu>
140
+ </Dropdown>
141
+ );
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ### 3. Tùy chỉnh Trigger qua `asChild`
148
+
149
+ Khi bật `asChild` (hoặc truyền trực tiếp một phần tử con hợp lệ), `DropdownTrigger` sẽ truyền toàn bộ accessibility attributes và sự kiện vào phần tử con mà không bọc thêm thẻ `<button>` thừa:
150
+
151
+ ```tsx
152
+ import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, IconButton } from "@owa/ui";
153
+ import { MoreVerticalIcon } from "@/components/icons";
154
+
155
+ export function CustomTriggerDropdown() {
156
+ return (
157
+ <Dropdown placement="bottom-end">
158
+ <DropdownTrigger asChild>
159
+ <IconButton icon={<MoreVerticalIcon />} aria-label="Tùy chọn khác" variant="ghost" />
160
+ </DropdownTrigger>
161
+ <DropdownMenu>
162
+ <DropdownItem>Chỉnh sửa</DropdownItem>
163
+ <DropdownItem>Sao chép liên kết</DropdownItem>
164
+ <DropdownItem isDanger>Xóa mục này</DropdownItem>
165
+ </DropdownMenu>
166
+ </Dropdown>
167
+ );
168
+ }
169
+ ```
170
+
171
+ ---
172
+
173
+ ### 4. Kích hoạt bằng Hover (`trigger="hover"`)
174
+
175
+ ```tsx
176
+ <Dropdown trigger="hover" placement="bottom-start">
177
+ <DropdownTrigger>
178
+ <Button variant="soft">Rê chuột để mở</Button>
179
+ </DropdownTrigger>
180
+ <DropdownMenu>
181
+ <DropdownItem>Tùy chọn 1</DropdownItem>
182
+ <DropdownItem>Tùy chọn 2</DropdownItem>
183
+ </DropdownMenu>
184
+ </Dropdown>
185
+ ```
186
+
187
+ ---
188
+
189
+ ## ♿ Khả năng truy cập & Điều hướng bàn phím
190
+
191
+ | Phím bấm | Hành vi |
192
+ | :--- | :--- |
193
+ | `Enter` / `Space` / `ArrowDown` | Mở menu khi đang focus tại trigger và focus vào item đầu tiên. |
194
+ | `ArrowDown` | Di chuyển focus xuống item tiếp theo (tự động bỏ qua item bị `disabled` hoặc separator). |
195
+ | `ArrowUp` | Di chuyển focus lên item phía trên (hỗ trợ vòng lặp danh sách `loop: true`). |
196
+ | `Home` | Nhảy nhanh tới item đầu tiên trong menu. |
197
+ | `End` | Nhảy nhanh tới item cuối cùng trong menu. |
198
+ | `Escape` | Đóng menu và trả focus về lại trigger element. |
199
+ | `Enter` / `Space` | Kích hoạt sự kiện `onClick` của item đang chọn và đóng menu (nếu `closeOnSelect={true}`). |
200
+
201
+ ---
202
+
203
+ ## 📋 API Reference
204
+
205
+ ### `<Dropdown>` (Root Component)
206
+
207
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
208
+ | :--- | :--- | :--- | :--- |
209
+ | `children` | `ReactNode` | — | Các component con (`DropdownTrigger`, `DropdownMenu`). |
210
+ | `open` | `boolean` | — | Trạng thái mở menu (chế độ Controlled). |
211
+ | `defaultOpen` | `boolean` | `false` | Trạng thái mở ban đầu (chế độ Uncontrolled). |
212
+ | `onOpenChange` | `(open: boolean) => void` | — | Callback khi trạng thái đóng/mở thay đổi. |
213
+ | `trigger` | `'click' \| 'hover'` | `'click'` | Kiểu kích hoạt mở dropdown. |
214
+ | `placement` | `DropdownPlacement` | `'bottom-start'` | Vị trí hiển thị menu so với trigger. |
215
+ | `offset` | `number` | `4` | Khoảng cách (px) giữa trigger và menu. |
216
+ | `flip` | `boolean` | `true` | Tự động đảo hướng khi bị tràn mép màn hình. |
217
+ | `shift` | `boolean` | `true` | Tự động dịch chuyển menu để không bị che khuất. |
218
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích thước áp dụng cho menu và items. |
219
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Độ bo góc của khung menu. |
220
+ | `color` | `DropdownColor` | `'primary'` | Tông màu chủ đạo khi item được active/hover. |
221
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ Dropdown. |
222
+ | `animated` | `boolean` | `true` | Bật/tắt animation mở/đóng menu. |
223
+ | `animationDuration` | `number` | `150` | Thời lượng animation (ms). |
224
+ | `closeOnSelect` | `boolean` | `true` | Tự động đóng menu khi chọn một item. |
225
+ | `closeOnEsc` | `boolean` | `true` | Đóng menu khi nhấn phím `Escape`. |
226
+ | `closeOnClickOutside` | `boolean` | `true` | Đóng menu khi click ra ngoài. |
227
+
228
+ ---
229
+
230
+ ### `<DropdownTrigger>`
231
+
232
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
233
+ | :--- | :--- | :--- | :--- |
234
+ | `children` | `ReactNode` | — | Phần tử con làm trigger. |
235
+ | `asChild` | `boolean` | `false` | Sử dụng chính phần tử con thay vì bọc button mặc định. |
236
+ | `className` | `string` | `""` | Class CSS tùy biến bổ sung. |
237
+ | `ref` | `Ref<HTMLElement>` | — | React 19 Ref trực tiếp vào trigger element. |
238
+
239
+ ---
240
+
241
+ ### `<DropdownMenu>`
242
+
243
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
244
+ | :--- | :--- | :--- | :--- |
245
+ | `children` | `ReactNode` | — | Nội dung bên trong menu (Header, Group, Item, Separator). |
246
+ | `minWidth` | `string \| number` | — | Độ rộng tối thiểu của menu. |
247
+ | `zIndex` | `number` | `DEFAULT_Z_INDEX.DROPDOWN` (50) | Thứ tự z-index của menu. |
248
+ | `className` | `string` | `""` | Class CSS tùy biến bổ sung. |
249
+ | `style` | `CSSProperties` | — | Style inline tùy biến bổ sung. |
250
+ | `ref` | `Ref<HTMLDivElement>` | — | React 19 Ref trực tiếp vào menu container. |
251
+
252
+ ---
253
+
254
+ ### `<DropdownItem>`
255
+
256
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
257
+ | :--- | :--- | :--- | :--- |
258
+ | `children` | `ReactNode` | — | Nhãn hoặc nội dung của item. |
259
+ | `icon` | `ReactNode` | — | Icon hiển thị ở phía trước (bên trái). |
260
+ | `shortcut` | `string` | — | Ký hiệu phím tắt hiển thị ở bên phải (ví dụ: `"⌘K"`, `"Ctrl+S"`). |
261
+ | `color` | `DropdownColor` | — | Ghi đè màu sắc riêng cho item này. |
262
+ | `size` | `DropdownSize` | — | Ghi đè kích thước riêng cho item này. |
263
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa item (không thể hover hay click). |
264
+ | `isDanger` | `boolean` | `false` | Định dạng item theo phong cách cảnh báo/hành động nguy hiểm (chữ đỏ, hover nền đỏ nhạt). |
265
+ | `onClick` | `(e: MouseEvent) => void` | — | Callback khi người dùng click vào item. |
266
+ | `className` | `string` | `""` | Class CSS tùy biến bổ sung. |
267
+ | `ref` | `Ref<HTMLDivElement>` | — | React 19 Ref trực tiếp vào item. |
268
+
269
+ ---
270
+
271
+ ### `<DropdownHeader>`, `<DropdownGroup>`, `<DropdownSeparator>`
272
+
273
+ - `<DropdownHeader>`: Hiển thị thông tin tiêu đề/tài khoản đầu menu (`<div>` chuẩn WAI-ARIA menu).
274
+ - `<DropdownGroup title="Tiêu đề nhóm">`: Gom nhóm các item và hiển thị nhãn nhóm `role="group"`.
275
+ - `<DropdownSeparator>`: Đường kẻ ngang phân cách ngữ cảnh giữa các nhóm menu (thẻ `<hr>` ngữ nghĩa HTML5).
@@ -0,0 +1,200 @@
1
+ # 📭 Empty Component (`@openway/ui`)
2
+
3
+ Component **Empty** (Trạng thái rỗng) hiển thị khi một danh sách, bảng dữ liệu, tìm kiếm hoặc trang web không có dữ liệu để hiển thị. Được thiết kế chuẩn **Design System**, hỗ trợ đa dạng **Preset Illustrations**, tùy biến **Hình ảnh/Icon/URL**, căn chỉnh bố cục **Vertical / Horizontal**, tích hợp **Safe Config Fallback** (`getSafeConfig`) và tuân thủ đầy đủ tiêu chuẩn **WAI-ARIA Accessibility**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **5 Preset Illustrations tích hợp sẵn**:
10
+ - `default`: Minh họa hộp dữ liệu rỗng tiêu chuẩn.
11
+ - `search`: Kính lúp không tìm thấy kết quả.
12
+ - `error`: Lỗi tải dữ liệu hoặc mất kết nối mạng.
13
+ - `folder`: Thư mục rỗng.
14
+ - `simple`: Minh họa tối giản, gọn nhẹ.
15
+ - **Hỗ trợ đa dạng nguồn ảnh (`image`)**:
16
+ - Tên preset (`default`, `search`, `error`, `folder`, `simple`).
17
+ - Đường dẫn URL ảnh (tự động render qua `next/image` với tối ưu hóa hình ảnh).
18
+ - Custom JSX (`ReactNode`) như SVG Icon, Emoji hoặc component tùy biến.
19
+ - **3 Kích thước tiêu chuẩn (`size`)**:
20
+ - `sm`: Gọn nhẹ, thích hợp cho dropdown, popover, select menu, bảng nhỏ.
21
+ - `md` *(mặc định)*: Tiêu chuẩn, thích hợp cho section, thẻ card, modal dialog.
22
+ - `lg`: Kích thước lớn, thích hợp cho trang dashboard hoặc toàn màn hình.
23
+ - **2 Bố cục hiển thị (`layout`)**:
24
+ - `vertical` *(mặc định)*: Xếp dọc từ trên xuống (Ảnh -> Tiêu đề -> Mô tả -> Actions).
25
+ - `horizontal`: Bố cục hàng ngang (Ảnh bên trái, nội dung & actions bên phải), tối ưu khi diện tích theo chiều ngang rộng rãi.
26
+ - **Khu vực hành động linh hoạt (`actions`)**: Cung cấp slot chuyên biệt để chèn Button (Tạo mới, Thử lại, Tải lại...).
27
+ - **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` từ `@/utils/function` đảm bảo an toàn tuyệt đối, không crash giao diện khi nhận giá trị `size` hoặc `layout` không hợp lệ.
28
+ - **Trợ năng (Accessibility)**: Tự động gắn `role="status"` và `aria-live="polite"` giúp các trình đọc màn hình (Screen Reader) thông báo trạng thái rỗng một cách rõ ràng.
29
+
30
+ ---
31
+
32
+ ## 🚀 Cài đặt & Import
33
+
34
+ ```tsx
35
+ import { Empty, EmptyIllustration } from "@openway/ui";
36
+ import type { EmptyProps, EmptySize, EmptyLayout, EmptyPresetImage } from "@openway/ui";
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 📖 Hướng dẫn sử dụng
42
+
43
+ ### 1. Trạng thái rỗng cơ bản
44
+
45
+ ```tsx
46
+ import { Empty } from "@openway/ui";
47
+
48
+ export function BasicEmptyExample() {
49
+ return (
50
+ <Empty
51
+ title="Không có dữ liệu"
52
+ description="Hiện tại chưa có dữ liệu nào trong danh sách này."
53
+ />
54
+ );
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ ### 2. Các Presets minh họa (`image`)
61
+
62
+ ```tsx
63
+ import { Empty } from "@openway/ui";
64
+
65
+ export function PresetExamples() {
66
+ return (
67
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
68
+ {/* Tìm kiếm không thấy */}
69
+ <Empty
70
+ image="search"
71
+ title="Không tìm thấy kết quả"
72
+ description="Vui lòng thử lại với từ khóa khác."
73
+ />
74
+
75
+ {/* Lỗi kết nối */}
76
+ <Empty
77
+ image="error"
78
+ title="Tải thất bại"
79
+ description="Không thể kết nối đến máy chủ. Vui lòng thử lại."
80
+ />
81
+
82
+ {/* Thư mục rỗng */}
83
+ <Empty
84
+ image="folder"
85
+ title="Thư mục trống"
86
+ description="Chưa có tệp tin nào được tải lên thư mục này."
87
+ />
88
+ </div>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ### 3. Tùy chỉnh kích thước (`size`) & Kèm nút hành động (`actions`)
96
+
97
+ ```tsx
98
+ import { Empty, Button } from "@openway/ui";
99
+
100
+ export function ActionsExample() {
101
+ return (
102
+ <Empty
103
+ size="md"
104
+ image="default"
105
+ title="Chưa có dự án nào"
106
+ description="Hãy bắt đầu khởi tạo dự án đầu tiên của bạn để quản lý công việc hiệu quả."
107
+ actions={
108
+ <Button variant="filled" color="primary" onClick={() => console.log("Tạo mới")}>
109
+ Tạo dự án mới
110
+ </Button>
111
+ }
112
+ />
113
+ );
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ### 4. Bố cục ngang (`layout="horizontal"`)
120
+
121
+ ```tsx
122
+ import { Empty, Button } from "@openway/ui";
123
+
124
+ export function HorizontalEmptyExample() {
125
+ return (
126
+ <div className="border rounded-xl p-4">
127
+ <Empty
128
+ layout="horizontal"
129
+ size="sm"
130
+ image="folder"
131
+ title="Không tìm thấy tệp tin"
132
+ description="Thư mục hiện đang trống hoặc bạn không có quyền truy cập."
133
+ actions={
134
+ <Button size="xs" variant="outline" color="primary">
135
+ Tải tệp lên
136
+ </Button>
137
+ }
138
+ />
139
+ </div>
140
+ );
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ### 5. Dùng URL ảnh ngoài hoặc Custom ReactNode
147
+
148
+ ```tsx
149
+ import { Empty } from "@openway/ui";
150
+
151
+ export function CustomImageExample() {
152
+ return (
153
+ <Empty
154
+ image="https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=160&auto=format&fit=crop&q=60"
155
+ imageSize={120}
156
+ imageAlt="Custom Image"
157
+ title="Bộ sưu tập ảnh trống"
158
+ description="Hãy thêm các bức ảnh yêu thích của bạn."
159
+ />
160
+ );
161
+ }
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 🛡️ Safe Config Fallback
167
+
168
+ Component `Empty` tích hợp hàm tiện ích `getSafeConfig` từ `@/utils/function`:
169
+
170
+ ```tsx
171
+ import { getSafeConfig } from "@/utils/function";
172
+
173
+ const currentSize = getSafeConfig(size, emptySizeConfig, "md");
174
+ const currentLayout = getSafeConfig(layout, emptyLayoutConfig, "vertical");
175
+ ```
176
+
177
+ - Nếu `size` truyền vào không thuộc `"sm" | "md" | "lg"`, component tự động fallback về kích cỡ chuẩn `"md"`.
178
+ - Nếu `layout` truyền vào không hợp lệ, component tự động fallback về bố cục chuẩn `"vertical"`.
179
+ - Giúp ứng dụng hoạt động ổn định, loại bỏ hoàn toàn nguy cơ runtime error / crash giao diện khi nhận dữ liệu không mong muốn từ bên ngoài.
180
+
181
+ ---
182
+
183
+ ## 📋 Danh sách Props (`EmptyProps`)
184
+
185
+ | Tên Prop | Kiểu dữ liệu | Mặc định | Mô tả |
186
+ | :--- | :--- | :--- | :--- |
187
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Kích cỡ tổng thể của component |
188
+ | `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Bố cục xếp dọc hoặc dàn ngang |
189
+ | `image` | `EmptyPresetImage \| string \| ReactNode` | `'default'` | Preset minh họa, URL ảnh hoặc JSX node tùy biến |
190
+ | `imageSize` | `number \| string` | Theo `size` | Chiều rộng & chiều cao tùy chỉnh cho phần ảnh |
191
+ | `imageClassName`| `string` | `""` | Class CSS cho thẻ bao ngoài ảnh |
192
+ | `imageAlt` | `string` | `'Trống'` | Thuộc tính alt cho hình ảnh |
193
+ | `title` | `ReactNode` | `undefined` | Tiêu đề trạng thái rỗng |
194
+ | `titleClassName`| `string` | `""` | Class CSS tùy chỉnh tiêu đề |
195
+ | `description` | `ReactNode` | `'Không có dữ liệu'` | Nội dung mô tả chi tiết |
196
+ | `descriptionClassName` | `string` | `""` | Class CSS tùy chỉnh mô tả |
197
+ | `actions` | `ReactNode` | `undefined` | Khu vực chứa nút bấm hành động (CTA) |
198
+ | `actionsClassName` | `string` | `""` | Class CSS tùy chỉnh khu vực actions |
199
+ | `children` | `ReactNode` | `undefined` | Nội dung bổ sung tùy biến |
200
+ | `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref chuyển tiếp đến container chính |
@@ -0,0 +1,180 @@
1
+ # 📁 File Preview Components (`@owa/ui`)
2
+
3
+ Bộ component xem trước tệp tin hiệu năng cao, xây dựng trên nền tảng **HTML5 Native `<dialog>`** và kiến trúc **React Context + Compound Component**:
4
+
5
+ - **`<FileContainer>`**: Hộp thoại bọc ngoài Modal dialog quản lý Backdrop, Top layer breakout, ESC, Lock scroll và Header. Nhận prop `file` và chia sẻ `file` cùng `headerTitle` xuống dưới thông qua `FileContext`.
6
+ - **`<FilePreview>`**: Component xác định loại tệp (`image`, `pdf`, `video`, `audio`, `document`, `other`) và chuyển tiếp tới component hiển thị tương ứng. Tự động tiêu thụ `file` và `headerTitle` từ `useFileContext()`.
7
+ - **`<ImagePreview>`**: Khối hiển thị hình ảnh với đầy đủ tính năng Zoom, Rotate, Flip, Drag-to-pan, Reset và Tải ảnh.
8
+
9
+ ---
10
+
11
+ ## 🌟 Điểm nổi bật
12
+
13
+ - **Zero Global Store**: Hoàn toàn không phụ thuộc Zustand / Redux.
14
+ - **Context-driven Compound Architecture**:
15
+ - `FileContainer` nhận `file` (và tùy chọn `title`) rồi cung cấp qua `FileContext`.
16
+ - `FilePreview` tự động nhận `file` và `headerTitle` từ `useFileContext()`.
17
+ - **Lồng ghép linh hoạt**:
18
+ ```tsx
19
+ <FileContainer open={open} onClose={handleClose} file={selectedFile}>
20
+ <FilePreview imageProps={{ minZoom: 0.5, maxZoom: 3 }} />
21
+ </FileContainer>
22
+ ```
23
+ - **Tự động quản lý bộ nhớ**: Tự động giải phóng `ObjectURL` (`URL.revokeObjectURL`) khi truyền `File` object để tránh memory leak.
24
+
25
+ ---
26
+
27
+ ## 🚀 Cài đặt & Import
28
+
29
+ ```tsx
30
+ import {
31
+ FileContainer,
32
+ FilePreview,
33
+ ImagePreview,
34
+ ImagePreviewToolbar,
35
+ FileContext,
36
+ useFileContext,
37
+ getFileName,
38
+ getFileType,
39
+ getFileExtension,
40
+ normalizePreviewFile,
41
+ downloadFile,
42
+ } from "@owa/ui";
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 📖 Hướng dẫn sử dụng
48
+
49
+ ### 1. Sử dụng kết hợp qua Context (Khuyên dùng)
50
+
51
+ ```tsx
52
+ import { useState } from "react";
53
+ import { Button, FileContainer, FilePreview } from "@owa/ui";
54
+
55
+ export function Example() {
56
+ const [open, setOpen] = useState(false);
57
+ const [file, setFile] = useState<File | null>(null);
58
+
59
+ return (
60
+ <div>
61
+ <Button onClick={() => setOpen(true)}>Xem file</Button>
62
+
63
+ <FileContainer
64
+ open={open}
65
+ onClose={() => setOpen(false)}
66
+ file={file}
67
+ >
68
+ <FilePreview
69
+ imageProps={{
70
+ minZoom: 0.5,
71
+ maxZoom: 4,
72
+ toolbarProps: {
73
+ tools: { rotate: true, flip: true, download: true },
74
+ },
75
+ }}
76
+ />
77
+ </FileContainer>
78
+ </div>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ### 2. Sử dụng gọn nhẹ (Tự động render `<FilePreview />`)
86
+
87
+ ```tsx
88
+ <FileContainer
89
+ open={open}
90
+ onClose={() => setOpen(false)}
91
+ file={selectedFile}
92
+ />
93
+ ```
94
+
95
+ ---
96
+
97
+ ### 3. Sử dụng bọc trực tiếp `<ImagePreview>`
98
+
99
+ ```tsx
100
+ <FileContainer open={open} onClose={() => setOpen(false)} title="Xem ảnh">
101
+ <ImagePreview src="https://images.unsplash.com/photo-1579783902614-a3fb3927b675" name="artwork.jpg" />
102
+ </FileContainer>
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 📚 Bảng tra cứu Props (API Reference)
108
+
109
+ ### `<FileContainer>` (Hộp thoại Modal)
110
+
111
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
112
+ | :--- | :--- | :--- | :--- |
113
+ | `open` | `boolean` | `false` | Trạng thái hiển thị mở/đóng FileContainer |
114
+ | `onClose` | `() => void` | `undefined` | Callback khi đóng hộp thoại |
115
+ | `file` | `File \| ServerFile` | `undefined` | Dữ liệu file truyền vào để chia sẻ qua Context |
116
+ | `title` | `ReactNode` | `undefined` | Tiêu đề tùy chỉnh cho phần header (mặc định lấy tên file) |
117
+ | `description` | `ReactNode` | `undefined` | Đoạn văn bản mô tả phụ bên dưới tiêu đề header |
118
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'lg'` | Kích thước chiều rộng của hộp thoại Modal |
119
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'xl'` | Mức độ bo góc của hộp thoại Modal |
120
+ | `showCloseButton` | `boolean` | `true` | Hiển thị nút đóng (X) ở header |
121
+ | `closeOnOverlayClick` | `boolean` | `true` | Cho phép đóng khi click ra ngoài backdrop |
122
+ | `closeOnEsc` | `boolean` | `true` | Cho phép đóng khi nhấn phím ESC |
123
+ | `lockScroll` | `boolean` | `true` | Khóa cuộn trang khi đang hiển thị |
124
+ | `className` | `string` | `""` | Class CSS tùy biến cho hộp thoại |
125
+ | `overlayClassName`| `string` | `""` | Class CSS tùy biến cho backdrop |
126
+ | `children` | `ReactNode` | `undefined` | Nội dung bên trong (mặc định tự động render `<FilePreview />`) |
127
+
128
+ ---
129
+
130
+ ### `<FilePreview>` (Xác định loại file & Điều hướng Viewer)
131
+
132
+ > **Lưu ý**: `<FilePreview>` tự động lấy `file` và `headerTitle` từ `FileContainer` thông qua `useFileContext()`.
133
+
134
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
135
+ | :--- | :--- | :--- | :--- |
136
+ | `onDownload` | `(file: PreviewFile) => void` | `undefined` | Callback tùy biến khi bấm nút tải file xuống |
137
+ | `imageProps` | `Partial<ImagePreviewProps>` | `undefined` | Cấu hình chi tiết cho phần hiển thị ảnh (`minZoom`, `maxZoom`, `toolbarProps`, ...) |
138
+ | `className` | `string` | `""` | Class CSS tùy biến cho container nội dung |
139
+
140
+ ---
141
+
142
+ ### `<ImagePreview>` (Hiển thị và tương tác ảnh)
143
+
144
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
145
+ | :--- | :--- | :--- | :--- |
146
+ | `src` | `string` | **Bắt buộc** | Đường dẫn ảnh hoặc data URL |
147
+ | `name` | `string` | `undefined` | Tên file ảnh hiển thị khi tải xuống |
148
+ | `alt` | `string` | `undefined` | Thẻ alt mô tả cho ảnh |
149
+ | `minZoom` | `number` | `0.2` | Mức độ thu nhỏ tối thiểu (20%) |
150
+ | `maxZoom` | `number` | `5` | Mức độ phóng to tối đa (500%) |
151
+ | `showToolbar` | `boolean` | `true` | Hiển thị thanh công cụ điều khiển phía dưới |
152
+ | `toolbarProps` | `Partial<ImagePreviewToolbarProps>` | `undefined` | Cấu hình chi tiết các nút bấm trên toolbar (`tools`) |
153
+ | `className` | `string` | `""` | Class CSS tùy biến cho container bao ngoài |
154
+ | `children` | `ReactNode` | `undefined` | Phần tử React con bổ sung |
155
+
156
+ ### `<ImagePreviewToolbar>` (Thanh công cụ điều khiển ảnh)
157
+
158
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
159
+ | :--- | :--- | :--- | :--- |
160
+ | `zoom` | `number` | `1` | Mức độ phóng to hiện tại |
161
+ | `minZoom` | `number` | `0.2` | Mức thu nhỏ tối thiểu |
162
+ | `maxZoom` | `number` | `5` | Mức phóng to tối đa |
163
+ | `step` | `number` | `0.05` | Bước nhảy zoom khi kéo thanh trượt Slider |
164
+ | `onZoomChange` | `(zoom: number) => void` | `undefined` | Callback khi zoom thay đổi qua Slider |
165
+ | `sliderProps` | `Partial<SliderProps>` | `undefined` | Tùy biến props cho component Slider bên trong |
166
+ | `tools` | `ToolbarToolsConfig` | `{}` | Cấu hình bật/tắt các nút và công cụ |
167
+
168
+ ---
169
+
170
+ ### `ToolbarToolsConfig` (Cấu hình nút trên thanh công cụ)
171
+
172
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
173
+ | :--- | :--- | :--- | :--- |
174
+ | `zoomIn` | `boolean` | `true` | Nút phóng to (+) |
175
+ | `zoomOut` | `boolean` | `true` | Nút thu nhỏ (-) |
176
+ | `zoomSlider` | `boolean` | `true` | Thanh trượt Slider điều chỉnh zoom trực tiếp |
177
+ | `reset` | `boolean` | `true` | Nút hiển thị % và reset tỉ lệ (1:1) |
178
+ | `rotate` | `boolean` | `true` | Cặp nút xoay ảnh theo & ngược chiều kim đồng hồ |
179
+ | `flip` | `boolean` | `true` | Nút lật ảnh theo chiều ngang |
180
+ | `download` | `boolean` | `true` | Nút tải ảnh xuống máy tính |