@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,293 @@
1
+ # 📦 Modal Component (`@owa/ui`)
2
+
3
+ Bộ component **Modal** hiệu năng cao, chuẩn **HTML5 Native `<dialog>`** và thiết kế theo mô hình **Pure Compound Pattern** linh hoạt.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Native Top Layer**: Sử dụng thẻ `<dialog>` kết hợp phương thức `dialog.showModal()` của trình duyệt. Modal tự động nổi lên tầng cao nhất (`#top-layer`), không bao giờ bị cắt xén bởi phần tử cha có `overflow: hidden`, `overflow: auto` hay xung đột `z-index`.
10
+ - **Zero Global Store**: Loại bỏ hoàn toàn Zustand / Redux / React Portal. Bạn có thể render Modal ở bất kỳ đâu trong cây JSX.
11
+ - **Mượt mà với Exit Animation**: Tích hợp sẵn hiệu ứng mở và thoát (**Exit Animation 250ms**) cho cả hộp thoại lẫn nền mờ backdrop.
12
+ - **Pure Compound Architecture**: Tách rời hoàn toàn giữa tầng Container (`<ModalContainer>`) và tầng giao diện (`<Modal>`, `<ModalHeader>`, `<ModalBody>`, `<ModalFooter>`, `<ModalClose>`).
13
+ - **Tự động liên kết Context**: Nút đóng `(X)` trong `<ModalHeader>` và component `<ModalClose>` tự động kích hoạt đóng kèm Exit Animation mà không cần truyền lặp lại state `open/onClose`.
14
+ - **Hỗ trợ Loading an toàn (`isLoading`)**: Tự động khóa đóng modal (chặn click backdrop, chặn ESC, tự động disable nút đóng và nút bọc `<ModalClose>`) khi đang trong tiến trình xử lý API/tải dữ liệu.
15
+ - **Hỗ trợ đầy đủ Accessibility (a11y)**: Tự động khóa cuộn trang (`body scroll lock`), bắt phím `ESC`, focus trap của trình duyệt, liên kết `aria-labelledby` và `aria-describedby`.
16
+
17
+ ---
18
+
19
+ ## 🚀 Cài đặt & Import
20
+
21
+ ```tsx
22
+ import {
23
+ ModalContainer,
24
+ Modal,
25
+ ModalHeader,
26
+ ModalBody,
27
+ ModalFooter,
28
+ ModalClose,
29
+ useModalContext,
30
+ } from "@owa/ui";
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 📖 Hướng dẫn sử dụng
36
+
37
+ ### 1. Cách sử dụng cơ bản (Basic Modal)
38
+
39
+ ```tsx
40
+ import { useState } from "react";
41
+ import {
42
+ Button,
43
+ ModalContainer,
44
+ Modal,
45
+ ModalHeader,
46
+ ModalBody,
47
+ ModalFooter,
48
+ ModalClose,
49
+ } from "@owa/ui";
50
+
51
+ export function BasicModalExample() {
52
+ const [open, setOpen] = useState(false);
53
+
54
+ return (
55
+ <div>
56
+ <Button onClick={() => setOpen(true)}>Mở Modal</Button>
57
+
58
+ <ModalContainer open={open} onClose={() => setOpen(false)}>
59
+ <Modal size="md" radius="xl">
60
+ <ModalHeader
61
+ title="Xác nhận cập nhật"
62
+ description="Vui lòng kiểm tra lại thông tin trước khi tiếp tục."
63
+ />
64
+ <ModalBody>
65
+ <p className="text-sm text-neutral-600">
66
+ Hệ thống sẽ lưu các thay đổi của bạn vào cơ sở dữ liệu.
67
+ </p>
68
+ </ModalBody>
69
+ <ModalFooter>
70
+ <ModalClose>
71
+ <Button variant="outline">Hủy bỏ</Button>
72
+ </ModalClose>
73
+ <ModalClose>
74
+ <Button color="primary">Đồng ý</Button>
75
+ </ModalClose>
76
+ </ModalFooter>
77
+ </Modal>
78
+ </ModalContainer>
79
+ </div>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ### 2. Form nhập liệu phức tạp & Xử lý Loading (`isLoading`)
87
+
88
+ Khi submit form và gọi API bất đồng bộ, chỉ cần truyền prop `isLoading={loading}` vào `<ModalContainer>`. Hệ thống sẽ tự động:
89
+ - Chặn người dùng đóng Modal khi bấm phím `ESC` hoặc click ra ngoài vùng nền mờ `backdrop`.
90
+ - Tự động `disabled` nút đóng `(X)` ở Header và nút bọc bên trong `<ModalClose>`.
91
+
92
+ ```tsx
93
+ export function CreateUserModal({ open, onClose, onSuccess }) {
94
+ const [loading, setLoading] = useState(false);
95
+
96
+ const handleSubmit = async (e: React.FormEvent) => {
97
+ e.preventDefault();
98
+ setLoading(true);
99
+ try {
100
+ // 1. Gọi API lưu dữ liệu
101
+ await apiCreateUser();
102
+ onSuccess?.();
103
+ // 2. Đóng modal sau khi hoàn thành
104
+ onClose();
105
+ } finally {
106
+ setLoading(false);
107
+ }
108
+ };
109
+
110
+ return (
111
+ <ModalContainer open={open} isLoading={loading} onClose={onClose}>
112
+ <Modal size="lg">
113
+ <ModalHeader
114
+ title="Tạo người dùng mới"
115
+ description="Điền thông tin chi tiết của thành viên mới"
116
+ />
117
+ <form onSubmit={handleSubmit}>
118
+ <ModalBody className="space-y-4">
119
+ <div>
120
+ <label className="block text-sm font-medium mb-1">Họ và tên</label>
121
+ <input required className="w-full border rounded-md px-3 py-2 text-sm" />
122
+ </div>
123
+ <div>
124
+ <label className="block text-sm font-medium mb-1">Email</label>
125
+ <input type="email" required className="w-full border rounded-md px-3 py-2 text-sm" />
126
+ </div>
127
+ </ModalBody>
128
+ <ModalFooter>
129
+ {/* Tự động disabled khi isLoading = true */}
130
+ <ModalClose>
131
+ <Button variant="outline" type="button">Hủy</Button>
132
+ </ModalClose>
133
+ <Button color="primary" type="submit" loading={loading}>
134
+ Lưu thành viên
135
+ </Button>
136
+ </ModalFooter>
137
+ </form>
138
+ </Modal>
139
+ </ModalContainer>
140
+ );
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ### 3. Đóng Modal lập trình qua Hook (`useModalContext`)
147
+
148
+ Nếu cần thực hiện logic bất đồng bộ (validate, fetch) trong các component con nằm sâu bên trong Modal:
149
+
150
+ ```tsx
151
+ import { useModalContext, Button } from "@owa/ui";
152
+
153
+ function CustomAction() {
154
+ const { onClose, isLoading } = useModalContext();
155
+
156
+ const handleSaveAndClose = async () => {
157
+ await saveApi();
158
+ onClose?.(); // Kích hoạt animation thoát 250ms của ModalContainer
159
+ };
160
+
161
+ return (
162
+ <Button onClick={handleSaveAndClose} disabled={isLoading}>
163
+ Lưu và Đóng
164
+ </Button>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ### 4. Tùy biến kích thước (`size`) & Bo góc (`radius`)
172
+
173
+ Modal hỗ trợ 6 kích thước chiều rộng và 6 mức độ bo góc. Bạn có thể truyền `size` trực tiếp vào `<ModalContainer size="lg">` để tự động đồng bộ kích thước xuống toàn bộ các component con (`Modal`, `ModalHeader`, `ModalBody`, `ModalFooter`):
174
+
175
+ > **Độ ưu tiên kích thước:** `Prop truyền trực tiếp vào component con` > `Prop size từ ModalContainer qua Context` > `Mặc định ("md")`.
176
+
177
+ ```tsx
178
+ // Đồng bộ toàn bộ Modal, Header, Body, Footer thành kích thước "lg"
179
+ <ModalContainer open={open} size="lg" onClose={() => setOpen(false)}>
180
+ <Modal radius="full">
181
+ <ModalHeader title="Modal Kích Thước Lớn (LG)" />
182
+ <ModalBody>...</ModalBody>
183
+ <ModalFooter>...</ModalFooter>
184
+ </Modal>
185
+ </ModalContainer>
186
+ ```
187
+
188
+ | Kích thước (`size`) | Chiều rộng (`max-w`) | Mục đích sử dụng |
189
+ | :--- | :--- | :--- |
190
+ | `xs` | `320px` | Thông báo ngắn gọn, prompt đơn giản |
191
+ | `sm` | `400px` | Hộp thoại xác nhận, form 1-2 trường nhập |
192
+ | `md` *(mặc định)* | `540px` | Form nhập liệu tiêu chuẩn |
193
+ | `lg` | `720px` | Form nhiều cột, cắt ảnh (avatar crop) |
194
+ | `xl` | `960px` | Xem tài liệu, bảng dữ liệu biểu mẫu |
195
+ | `full` | `100vw` | Trải nghiệm toàn màn hình |
196
+
197
+ ---
198
+
199
+ ## 📚 Bảng tra cứu Props (API Reference)
200
+
201
+ ### `<ModalContainer>`
202
+
203
+ Tầng Wrapper quản lý Overlay Backdrop, Native Dialog, Animation thoát và phím ESC.
204
+
205
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
206
+ | :--- | :--- | :--- | :--- |
207
+ | `open` | `boolean` | `false` | Trạng thái hiển thị mở/đóng Modal |
208
+ | `onClose` | `() => void` | `undefined` | Callback khi Modal đóng (click backdrop, bấm ESC, bấm nút X) |
209
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Kích thước tổng thể đồng bộ xuống Context cho `Modal`, `ModalHeader`, `ModalBody`, `ModalFooter` |
210
+ | `isLoading` | `boolean` | `false` | Trạng thái đang tải/xử lý. Khi `true`, sẽ chặn đóng Modal (qua click backdrop, phím ESC, nút X, `<ModalClose>`) và tự động `disabled` các nút đóng |
211
+ | `closeOnOverlayClick` | `boolean` | `true` | Cho phép đóng Modal khi click vào lớp nền backdrop bên ngoài (bị chặn khi `isLoading = true`) |
212
+ | `closeOnEsc` | `boolean` | `true` | Cho phép đóng Modal khi nhấn phím `ESC` trên bàn phím (bị chặn khi `isLoading = true`) |
213
+ | `lockScroll` | `boolean` | `true` | Tự động khóa cuộn trang (`body overflow: hidden`) khi mở Modal |
214
+ | `overlayClassName` | `string` | `""` | Class CSS tùy biến cho lớp nền backdrop mờ toàn màn hình |
215
+ | `className` | `string` | `""` | Class CSS cho container bao ngoài dialog |
216
+ | `children` | `ReactNode` | — | Nội dung bên trong container (thường là `<Modal>`) |
217
+
218
+ ---
219
+
220
+ ### `<Modal>`
221
+
222
+ Khung giao diện hộp thoại (Dialog Box).
223
+
224
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
225
+ | :--- | :--- | :--- | :--- |
226
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Kích thước chiều rộng của hộp thoại |
227
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'rounded-lg'` | Mức độ bo góc của hộp thoại |
228
+ | `className` | `string` | `""` | Class CSS tùy biến cho hộp thoại |
229
+ | `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref trỏ trực tiếp đến thẻ div hộp thoại |
230
+ | `children` | `ReactNode` | — | Các component con (`<ModalHeader>`, `<ModalBody>`, `<ModalFooter>`) |
231
+
232
+ ---
233
+
234
+ ### `<ModalHeader>`
235
+
236
+ Phần đầu của hộp thoại Modal.
237
+
238
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
239
+ | :--- | :--- | :--- | :--- |
240
+ | `title` | `ReactNode` | `undefined` | Tiêu đề chính của Modal |
241
+ | `description` | `ReactNode` | `undefined` | Đoạn văn bản mô tả phụ bên dưới tiêu đề |
242
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Kích thước áp dụng cho tiêu đề và padding |
243
+ | `showCloseButton` | `boolean` | `true` | Hiển thị nút đóng `(X)` ở góc trên bên phải (tự động `disabled` khi `isLoading = true`) |
244
+ | `onClose` | `() => void` | `undefined` | Callback tùy biến khi click nút `(X)` (vẫn luôn chạy kèm animation thoát của ModalContainer) |
245
+ | `titleClassName` | `string` | `""` | Class CSS riêng cho text tiêu đề |
246
+ | `descriptionClassName`| `string` | `""` | Class CSS riêng cho text mô tả |
247
+ | `closeButtonClassName`| `string` | `""` | Class CSS tùy biến nút đóng `(X)` |
248
+ | `className` | `string` | `""` | Class CSS cho toàn bộ khối header |
249
+
250
+ ---
251
+
252
+ ### `<ModalBody>`
253
+
254
+ Phần thân chứa nội dung chính (tự động bật thanh cuộn dọc khi nội dung dài).
255
+
256
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
257
+ | :--- | :--- | :--- | :--- |
258
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Kích thước padding và font size (tự động kế thừa từ ModalContainer) |
259
+ | `className` | `string` | `""` | Class CSS tùy biến cho khối body |
260
+ | `children` | `ReactNode` | — | Nội dung form, bảng biểu, danh sách... |
261
+
262
+ ---
263
+
264
+ ### `<ModalFooter>`
265
+
266
+ Phần chân trang chứa các nút hành động xác nhận hoặc hủy.
267
+
268
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
269
+ | :--- | :--- | :--- | :--- |
270
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Kích thước padding và khoảng cách gap giữa các nút |
271
+ | `className` | `string` | `""` | Class CSS cho toàn bộ khối footer |
272
+ | `children` | `ReactNode` | — | Các nút bấm (`<Button>`, `<ModalClose>`) |
273
+
274
+ ---
275
+
276
+ ### `<ModalClose>`
277
+
278
+ Wrapper bọc quanh bất kỳ nút bấm hoặc phần tử nào bên trong Modal (ví dụ: nút Hủy bỏ trong Footer). Khi click, `<ModalClose>` sẽ tự động kích hoạt hiệu ứng đóng mượt mà (**Exit Animation 250ms**) trước khi unmount (tự động `disabled` khi `isLoading = true`).
279
+
280
+ ```tsx
281
+ <ModalFooter>
282
+ {/* asChild=true (mặc định) sẽ ghép thẳng sự kiện vào Button mà không sinh thêm thẻ div thừa */}
283
+ <ModalClose>
284
+ <Button variant="outline">Hủy bỏ</Button>
285
+ </ModalClose>
286
+ <Button color="primary" onClick={handleSave}>Lưu thay đổi</Button>
287
+ </ModalFooter>
288
+ ```
289
+
290
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
291
+ | :--- | :--- | :--- | :--- |
292
+ | `children` | `ReactNode` | — | Nút bấm hoặc phần tử con cần kích hoạt sự kiện đóng |
293
+ | `asChild` | `boolean` | `true` | Truyền thẳng sự kiện `onClick` và prop `disabled` vào phần tử con thay vì bọc ngoài bằng thẻ `div` |
@@ -0,0 +1,255 @@
1
+ # 💬 Popover Component (`@owa/ui`)
2
+
3
+ Bộ component **Popover** (hộp thoại nổi tương tác) xây dựng theo mô hình **Compound Component** trên nền tảng **`@floating-ui/react`**, hỗ trợ **chuẩn WAI-ARIA Dialog**, **quản lý tiêu điểm thông minh (FloatingFocusManager)**, **tự động căn vị trí thông minh (flip/shift/offset)**, 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 rõ ràng gồm `<Popover>`, `<PopoverTrigger>`, `<PopoverContent>`, `<PopoverHeader>`, `<PopoverBody>`, `<PopoverFooter>`, `<PopoverClose>`.
10
+ - **Hỗ trợ React 19 & React Compiler**: Áp dụng mô hình `Slot` component trên cả `PopoverTrigger` và `PopoverClose`, đảm bảo xử lý `ref` sạch sẽ và an toàn tuyệt đối khi render.
11
+ - **Quản lý tiêu điểm (Focus Management)**: Tích hợp `FloatingFocusManager`, tự động bẫy/khóa focus bên trong popover khi ở chế độ `modal={true}` và khôi phục focus về trigger khi đóng.
12
+ - **Hệ thống z-index đồng bộ**: Sử dụng `DEFAULT_Z_INDEX.POPOVER` (mặc định `50`) từ hệ thống constants của `@owa/ui`.
13
+ - **Luôn nổi trên cùng (Floating Portal)**: Nội dung popover luôn được gắn vào `<FloatingPortal>`, không bị ảnh hưởng bởi layout cha hay `overflow: hidden`.
14
+ - **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` giúp các subcomponents (`PopoverContent`, `PopoverHeader`, `PopoverBody`, `PopoverFooter`) lấy an toàn cấu hình `sizeConfig` và `radiusConfig`.
15
+ - **WAI-ARIA Accessibility**:
16
+ - `role="dialog"` cho popover container.
17
+ - `aria-expanded`, `aria-haspopup="dialog"`.
18
+ - Tự động đóng khi nhấn phím `Escape` (`closeOnEsc`) hoặc click ra ngoài (`closeOnClickOutside`).
19
+ - **Tùy biến linh hoạt**:
20
+ - 5 kích cỡ: `xs`, `sm`, `md` (*mặc định*), `lg`, `xl`.
21
+ - 6 cấp độ bo góc: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
22
+ - 2 cơ chế kích hoạt: `click` (*mặc định*) hoặc `hover` (hỗ trợ `safePolygon` giúp di chuột mượt mà sang popover).
23
+
24
+ ---
25
+
26
+ ## 🚀 Cài đặt & Import
27
+
28
+ ```tsx
29
+ import {
30
+ Popover,
31
+ PopoverTrigger,
32
+ PopoverContent,
33
+ PopoverHeader,
34
+ PopoverBody,
35
+ PopoverFooter,
36
+ PopoverClose,
37
+ } from "@owa/ui";
38
+ import type {
39
+ PopoverProps,
40
+ PopoverTriggerProps,
41
+ PopoverContentProps,
42
+ PopoverHeaderProps,
43
+ PopoverBodyProps,
44
+ PopoverFooterProps,
45
+ PopoverCloseProps,
46
+ PopoverPlacement,
47
+ PopoverTriggerType,
48
+ PopoverSize,
49
+ PopoverRadius,
50
+ PopoverColor,
51
+ } from "@owa/ui";
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 📖 Hướng dẫn sử dụng
57
+
58
+ ### 1. Popover cơ bản (Form nhập liệu / Bộ lọc)
59
+
60
+ ```tsx
61
+ import {
62
+ Popover,
63
+ PopoverTrigger,
64
+ PopoverContent,
65
+ PopoverHeader,
66
+ PopoverBody,
67
+ PopoverFooter,
68
+ PopoverClose,
69
+ Button,
70
+ } from "@owa/ui";
71
+
72
+ export function BasicPopover() {
73
+ return (
74
+ <Popover placement="bottom-start" size="md">
75
+ <PopoverTrigger>
76
+ <Button variant="outline">Mở bộ lọc</Button>
77
+ </PopoverTrigger>
78
+ <PopoverContent minWidth={280}>
79
+ <PopoverHeader>Bộ lọc nâng cao</PopoverHeader>
80
+ <PopoverBody>
81
+ <div className="space-y-3">
82
+ <div>
83
+ <label className="block text-xs font-medium text-neutral-600 mb-1">Từ khóa</label>
84
+ <input
85
+ type="text"
86
+ placeholder="Nhập từ khóa..."
87
+ className="w-full px-2.5 py-1.5 border border-neutral-300 rounded text-sm outline-none focus:border-primary-500"
88
+ />
89
+ </div>
90
+ <div>
91
+ <label className="block text-xs font-medium text-neutral-600 mb-1">Trạng thái</label>
92
+ <select className="w-full px-2.5 py-1.5 border border-neutral-300 rounded text-sm outline-none focus:border-primary-500">
93
+ <option value="all">Tất cả</option>
94
+ <option value="active">Đang hoạt động</option>
95
+ <option value="inactive">Đã tạm dừng</option>
96
+ </select>
97
+ </div>
98
+ </div>
99
+ </PopoverBody>
100
+ <PopoverFooter className="flex justify-end gap-2">
101
+ <PopoverClose asChild>
102
+ <Button variant="ghost" size="sm">Hủy</Button>
103
+ </PopoverClose>
104
+ <Button size="sm">Áp dụng</Button>
105
+ </PopoverFooter>
106
+ </PopoverContent>
107
+ </Popover>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ### 2. Tùy biến Trigger với `asChild`
115
+
116
+ ```tsx
117
+ import { Popover, PopoverTrigger, PopoverContent, PopoverBody, IconButton } from "@owa/ui";
118
+ import { InfoIcon } from "@/components/icons";
119
+
120
+ export function InfoPopover() {
121
+ return (
122
+ <Popover trigger="hover" placement="top">
123
+ <PopoverTrigger asChild>
124
+ <IconButton icon={<InfoIcon />} aria-label="Xem giải thích" variant="ghost" size="sm" />
125
+ </PopoverTrigger>
126
+ <PopoverContent maxWidth={320}>
127
+ <PopoverBody>
128
+ <p className="text-xs text-neutral-600 leading-relaxed">
129
+ Dữ liệu này được tự động đồng bộ từ máy chủ mỗi 5 phút một lần.
130
+ </p>
131
+ </PopoverBody>
132
+ </PopoverContent>
133
+ </Popover>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ### 3. Kích hoạt bằng Hover (`trigger="hover"`)
141
+
142
+ Với chế độ `hover`, Popover tích hợp thuật toán `safePolygon()` của Floating UI, cho phép người dùng di chuyển con trỏ chuột theo đường chéo từ trigger sang khung popover mà không bị tắt đột ngột:
143
+
144
+ ```tsx
145
+ <Popover trigger="hover" placement="bottom">
146
+ <PopoverTrigger>
147
+ <Button variant="soft">Rê chuột xem chi tiết</Button>
148
+ </PopoverTrigger>
149
+ <PopoverContent>
150
+ <PopoverBody>
151
+ <p className="text-sm">Nội dung chi tiết được hiển thị khi hover!</p>
152
+ </PopoverBody>
153
+ </PopoverContent>
154
+ </Popover>
155
+ ```
156
+
157
+ ---
158
+
159
+ ### 4. Chế độ Modal Popover (`modal={true}`)
160
+
161
+ Khi bật `modal={true}`, Popover sẽ khóa toàn bộ tương tác bên ngoài và bẫy phím `Tab` chỉ tuần hoàn bên trong nội dung của Popover:
162
+
163
+ ```tsx
164
+ <Popover modal={true} placement="bottom-start">
165
+ <PopoverTrigger>
166
+ <Button color="error">Xóa dữ liệu quan trọng</Button>
167
+ </PopoverTrigger>
168
+ <PopoverContent minWidth={300}>
169
+ <PopoverHeader>Xác nhận xóa</PopoverHeader>
170
+ <PopoverBody>
171
+ <p className="text-sm text-neutral-700">
172
+ Bạn có chắc chắn muốn xóa bản ghi này? Hành động này không thể hoàn tác.
173
+ </p>
174
+ </PopoverBody>
175
+ <PopoverFooter className="flex justify-end gap-2">
176
+ <PopoverClose asChild>
177
+ <Button variant="outline" size="sm">Hủy</Button>
178
+ </PopoverClose>
179
+ <Button color="error" size="sm">Đồng ý xóa</Button>
180
+ </PopoverFooter>
181
+ </PopoverContent>
182
+ </Popover>
183
+ ```
184
+
185
+ ---
186
+
187
+ ## ♿ Khả năng truy cập & Điều hướng bàn phím
188
+
189
+ | Phím bấm | Hành vi |
190
+ | :--- | :--- |
191
+ | `Enter` / `Space` | Kích hoạt mở hoặc đóng popover khi đang focus tại trigger. |
192
+ | `Tab` | Di chuyển focus tuần tự qua các phần tử tương tác (inputs, buttons) bên trong popover. |
193
+ | `Shift + Tab` | Di chuyển ngược lại phần tử tương tác trước đó trong popover. |
194
+ | `Escape` | Đóng popover và tự động khôi phục focus về lại trigger element. |
195
+
196
+ ---
197
+
198
+ ## 📋 API Reference
199
+
200
+ ### `<Popover>` (Root Component)
201
+
202
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
203
+ | :--- | :--- | :--- | :--- |
204
+ | `children` | `ReactNode` | — | Các subcomponents (`PopoverTrigger`, `PopoverContent`). |
205
+ | `open` | `boolean` | — | Trạng thái mở popover (chế độ Controlled). |
206
+ | `defaultOpen` | `boolean` | `false` | Trạng thái mở ban đầu (chế độ Uncontrolled). |
207
+ | `onOpenChange` | `(open: boolean) => void` | — | Callback khi trạng thái mở/đóng thay đổi. |
208
+ | `trigger` | `'click' \| 'hover'` | `'click'` | Kiểu kích hoạt mở popover. |
209
+ | `placement` | `PopoverPlacement` | `'bottom'` | Hướng hiển thị của popover so với trigger. |
210
+ | `offset` | `number` | `8` | Khoảng cách (px) giữa trigger và popover. |
211
+ | `flip` | `boolean` | `true` | Tự động đảo hướng khi popover bị tràn mép màn hình. |
212
+ | `shift` | `boolean` | `true` | Tự động dịch chuyển popover để không bị che khuất. |
213
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích thước áp dụng cho padding/font của popover. |
214
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Độ bo góc của khung popover. |
215
+ | `color` | `PopoverColor` | `'neutral'` | Tông màu chủ đề của popover. |
216
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ Popover. |
217
+ | `animated` | `boolean` | `true` | Bật/tắt hiệu ứng chuyển động khi mở/đóng. |
218
+ | `animationDuration` | `number` | `150` | Thời lượng hiệu ứng (ms). |
219
+ | `modal` | `boolean` | `false` | Khóa tiêu điểm bên trong popover và ngăn tương tác nền ngoài. |
220
+ | `closeOnEsc` | `boolean` | `true` | Đóng popover khi nhấn phím `Escape`. |
221
+ | `closeOnClickOutside` | `boolean` | `true` | Đóng popover khi click ra ngoài. |
222
+
223
+ ---
224
+
225
+ ### `<PopoverTrigger>`
226
+
227
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
228
+ | :--- | :--- | :--- | :--- |
229
+ | `children` | `ReactNode` | — | Phần tử con làm trigger. |
230
+ | `asChild` | `boolean` | `false` | Truyền trực tiếp props/events vào phần tử con thay vì bọc button mặc định. |
231
+ | `className` | `string` | `""` | Class CSS tùy biến bổ sung. |
232
+ | `ref` | `Ref<HTMLElement>` | — | React 19 Ref trực tiếp vào trigger element. |
233
+
234
+ ---
235
+
236
+ ### `<PopoverContent>`
237
+
238
+ | Prop | Kiểu dữ liệu | Mặc định | Mô tả |
239
+ | :--- | :--- | :--- | :--- |
240
+ | `children` | `ReactNode` | — | Nội dung bên trong popover. |
241
+ | `minWidth` | `string \| number` | — | Độ rộng tối thiểu của popover. |
242
+ | `maxWidth` | `string \| number` | — | Độ rộng tối đa của popover. |
243
+ | `zIndex` | `number` | `DEFAULT_Z_INDEX.POPOVER` (50) | Thứ tự z-index của popover nổi. |
244
+ | `className` | `string` | `""` | Class CSS tùy biến bổ sung. |
245
+ | `style` | `CSSProperties` | — | Style inline tùy biến bổ sung. |
246
+ | `ref` | `Ref<HTMLDivElement>` | — | React 19 Ref trực tiếp vào content container. |
247
+
248
+ ---
249
+
250
+ ### `<PopoverHeader>`, `<PopoverBody>`, `<PopoverFooter>`, `<PopoverClose>`
251
+
252
+ - `<PopoverHeader>`: Tiêu đề của popover (chuẩn WAI-ARIA `<h2>` ngữ nghĩa dialog).
253
+ - `<PopoverBody>`: Phần thân chính chứa văn bản, inputs hoặc controls.
254
+ - `<PopoverFooter>`: Chân trang chứa các nút hành động (Cancel, Submit).
255
+ - `<PopoverClose>`: Nút kích hoạt đóng popover (hỗ trợ `asChild` để gắn vào bất kỳ component Button tùy ý).