@openway/ui 1.0.0 → 1.0.1

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 (57) hide show
  1. package/README.md +203 -181
  2. package/dist/chunk-CJQNI3QX.js +3 -0
  3. package/dist/chunk-CJQNI3QX.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 +2642 -1103
  7. package/dist/index.d.ts +2642 -1103
  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 +309 -0
  13. package/dist/query.d.ts +309 -0
  14. package/dist/query.js +3 -0
  15. package/dist/query.js.map +1 -0
  16. package/dist/useInfiniteScroll-DAlgjUN4.d.cts +325 -0
  17. package/dist/useInfiniteScroll-DAlgjUN4.d.ts +325 -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/useSelectInfiniteQuery.md +123 -0
  56. package/docs/hooks/useTableQuery.md +124 -0
  57. package/package.json +31 -12
@@ -0,0 +1,354 @@
1
+ # 🎠 Carousel Component (`@owa/ui`)
2
+
3
+ Component **Carousel** (băng chuyền / slider) hiện đại, hiệu năng cao, thiết kế chuẩn **Compound Components Pattern** (`<Carousel>`, `<CarouselContent>`, `<CarouselSlide>`, `<CarouselPrevious>`, `<CarouselNext>`, `<CarouselPagination>`), hỗ trợ **Pointer Drag/Touch Gestures**, **Autoplay thông minh**, **Infinite Looping**, **Glassmorphism Design**, **Dynamic Slide Registration**, **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
+ - **Compound Components Pattern chuẩn chỉ**: Tách biệt rõ ràng các thành phần `<Carousel>`, `<CarouselContent>`, `<CarouselSlide>`, `<CarouselPrevious>`, `<CarouselNext>`, `<CarouselPagination>`, mang lại khả năng tùy biến bố cục không giới hạn.
10
+ - **Dynamic Slide Registration (`registerSlide` / `unregisterSlide`)**: Tự động đếm và đồng bộ số lượng slide (`totalSlides`) theo thời gian thực dựa trên vòng đời của từng `<CarouselSlide>`, không phụ thuộc vào thứ tự hay cách thức render children.
11
+ - **Tương tác kéo vuốt mượt mà (Touch & Pointer Gestures)**: Hỗ trợ kéo vuốt cả chuột và màn hình cảm ứng mượt mà qua Pointer Events (`setPointerCapture`), có hiệu ứng lực cản (drag resistance) khi kéo quá mép đầu/cuối.
12
+ - **Autoplay thông minh**: Tự động chuyển slide theo chu kỳ `interval`, tự động tạm dừng khi rê chuột (`pauseOnHover`), khi focus (`pauseOnFocus`), hoặc khi đang kéo chuột/vuốt màn hình.
13
+ - **Vòng lặp vô hạn (Infinite Loop)**: Tự động tính toán chuyển động vòng tròn liền mạch giữa slide đầu và slide cuối.
14
+ - **Hiển thị nhiều slide cùng lúc (Multi-slides)**: Hỗ trợ `slidesToShow`, `slidesToScroll`, và khoảng cách gap `spacing` linh hoạt (nhận `number` theo px hoặc chuỗi CSS).
15
+ - **Thiết kế Kính Mờ (Frosted Glass / Glassmorphism)**: Phong cách nút và thanh phân trang kính mờ hiện đại với `backdrop-blur-md`, tự động thích ứng hoàn hảo cho cả Light Mode và Dark Mode.
16
+ - **Định vị mặc định thông minh**:
17
+ - `CarouselPrevious`: Nút lùi kính mờ đặt sát mép trái (`absolute left-2 top-1/2 -translate-y-1/2`).
18
+ - `CarouselNext`: Nút tiến kính mờ đặt sát mép phải (`absolute right-2 top-1/2 -translate-y-1/2`).
19
+ - `CarouselPagination`: Thanh phân trang đặt tại đáy giữa (`absolute bottom-2.5 left-1/2 -translate-x-1/2`).
20
+ - Dễ dàng ghi đè hoặc đặt vào custom toolbar/slot bằng cách truyền `className`.
21
+ - **4 Biến thể nút điều hướng (`variant`)**: `glass` (*mặc định*), `filled`, `outline`, `ghost`.
22
+ - **3 Kiểu dáng phân trang (`type`)**: `dots` (*mặc định*), `line` (thanh ngang co giãn), `fraction` (tỉ lệ dạng `1 / 4`).
23
+ - **3 Kích thước tiêu chuẩn (`size`)**: `sm`, `md` (*mặc định*), `lg`.
24
+ - **6 Kiểu bo góc (`radius`)**: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
25
+ - **Chế độ Controlled & Uncontrolled**: Hỗ trợ đầy đủ `currentIndex` + `onIndexChange` (Controlled) và `defaultIndex` (Uncontrolled).
26
+ - **Điều hướng bàn phím & Trợ năng (A11y)**: Hỗ trợ `role="region"`, `role="tablist"`, `role="tab"`, `role="group"`, `aria-roledescription="carousel"`, phím `ArrowLeft`, `ArrowRight`, `Home`, `End`.
27
+ - **Safe Config Fallback**: Tích hợp `getSafeConfig` từ `@/utils/function` giúp component luôn an toàn, không bị crash kể cả khi truyền prop không hợp lệ.
28
+
29
+ ---
30
+
31
+ ## 🚀 Cài đặt & Import
32
+
33
+ ```tsx
34
+ import {
35
+ Carousel,
36
+ CarouselContent,
37
+ CarouselSlide,
38
+ CarouselPrevious,
39
+ CarouselNext,
40
+ CarouselPagination,
41
+ useCarousel,
42
+ CarouselContext,
43
+ useCarouselContext,
44
+ carouselSizeConfig,
45
+ carouselRadiusConfig,
46
+ carouselNavVariantConfig,
47
+ carouselDotStyleConfig,
48
+ carouselGlassPaginationWrapper,
49
+ } from "@owa/ui";
50
+
51
+ import type {
52
+ CarouselProps,
53
+ CarouselContentProps,
54
+ CarouselSlideProps,
55
+ CarouselNavigationProps,
56
+ CarouselPaginationProps,
57
+ CarouselSize,
58
+ CarouselRadius,
59
+ CarouselPaginationType,
60
+ CarouselArrowPosition,
61
+ CarouselNavigationVariant,
62
+ CarouselContextValue,
63
+ } from "@owa/ui";
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 📖 Hướng dẫn sử dụng
69
+
70
+ ### 1. Sử dụng cơ bản (Compound Components)
71
+
72
+ ```tsx
73
+ import {
74
+ Carousel,
75
+ CarouselContent,
76
+ CarouselSlide,
77
+ CarouselPrevious,
78
+ CarouselNext,
79
+ CarouselPagination,
80
+ } from "@owa/ui";
81
+
82
+ export function BasicCarousel() {
83
+ return (
84
+ <Carousel loop className="w-full max-w-lg shadow-lg">
85
+ <CarouselContent>
86
+ <CarouselSlide>
87
+ <div className="h-56 bg-primary-600 text-white flex items-center justify-center text-xl font-bold">
88
+ Slide 1: Khám Phá Công Nghệ
89
+ </div>
90
+ </CarouselSlide>
91
+ <CarouselSlide>
92
+ <div className="h-56 bg-primary-700 text-white flex items-center justify-center text-xl font-bold">
93
+ Slide 2: Trải Nghiệm Tối Ưu
94
+ </div>
95
+ </CarouselSlide>
96
+ <CarouselSlide>
97
+ <div className="h-56 bg-primary-800 text-white flex items-center justify-center text-xl font-bold">
98
+ Slide 3: Thiết Kế Hiện Đại
99
+ </div>
100
+ </CarouselSlide>
101
+ </CarouselContent>
102
+
103
+ {/* Nút điều hướng kính mờ tự động đặt sát 2 mép */}
104
+ <CarouselPrevious />
105
+ <CarouselNext />
106
+
107
+ {/* Phân trang tự động đặt ở đáy giữa */}
108
+ <CarouselPagination type="dots" />
109
+ </Carousel>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ### 2. Tự động chuyển slide (Autoplay) & Vòng lặp vô hạn (Loop)
117
+
118
+ ```tsx
119
+ <Carousel
120
+ autoPlay={true}
121
+ interval={3500}
122
+ transitionDuration={650}
123
+ loop={true}
124
+ pauseOnHover={true}
125
+ pauseOnFocus={true}
126
+ className="w-full max-w-xl"
127
+ >
128
+ <CarouselContent>
129
+ <CarouselSlide>
130
+ <img src="/banner-1.jpg" alt="Banner 1" className="w-full h-64 object-cover" />
131
+ </CarouselSlide>
132
+ <CarouselSlide>
133
+ <img src="/banner-2.jpg" alt="Banner 2" className="w-full h-64 object-cover" />
134
+ </CarouselSlide>
135
+ <CarouselSlide>
136
+ <img src="/banner-3.jpg" alt="Banner 3" className="w-full h-64 object-cover" />
137
+ </CarouselSlide>
138
+ </CarouselContent>
139
+ <CarouselPrevious />
140
+ <CarouselNext />
141
+ <CarouselPagination type="line" />
142
+ </Carousel>
143
+ ```
144
+
145
+ ---
146
+
147
+ ### 3. Hiển thị nhiều slide cùng lúc (Multi-slides Grid)
148
+
149
+ ```tsx
150
+ <Carousel slidesToShow={3} slidesToScroll={1} spacing={16} loop className="w-full max-w-4xl">
151
+ <CarouselContent>
152
+ {products.map((product) => (
153
+ <CarouselSlide key={product.id}>
154
+ <div className="p-4 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 shadow-sm">
155
+ <img src={product.image} alt={product.name} className="h-40 w-full object-cover rounded-lg" />
156
+ <h3 className="mt-2 font-semibold text-neutral-900 dark:text-white">{product.name}</h3>
157
+ <p className="text-primary-600 font-bold">{product.price}</p>
158
+ </div>
159
+ </CarouselSlide>
160
+ ))}
161
+ </CarouselContent>
162
+ <CarouselPrevious />
163
+ <CarouselNext />
164
+ </Carousel>
165
+ ```
166
+
167
+ ---
168
+
169
+ ### 4. Các kiểu phân trang (Pagination Types)
170
+
171
+ Hỗ trợ 3 kiểu phân trang:
172
+
173
+ ```tsx
174
+ {/* 1. Dạng chấm tròn tinh gọn (mặc định) */}
175
+ <CarouselPagination type="dots" />
176
+
177
+ {/* 2. Dạng thanh gạch ngang co giãn khi active */}
178
+ <CarouselPagination type="line" />
179
+
180
+ {/* 3. Dạng phân số tỉ lệ (ví dụ: 1 / 4) */}
181
+ <CarouselPagination type="fraction" />
182
+ ```
183
+
184
+ ---
185
+
186
+ ### 5. Biến thể nút điều hướng (Navigation Variants)
187
+
188
+ Hỗ trợ 4 biến thể giao diện: `glass` (*mặc định*), `filled`, `outline`, `ghost`:
189
+
190
+ ```tsx
191
+ <Carousel className="w-full max-w-lg shadow">
192
+ <CarouselContent>
193
+ <CarouselSlide><div className="h-44 bg-primary-600 text-white flex items-center justify-center">Filled Buttons</div></CarouselSlide>
194
+ <CarouselSlide><div className="h-44 bg-primary-700 text-white flex items-center justify-center">Filled Buttons</div></CarouselSlide>
195
+ </CarouselContent>
196
+ <CarouselPrevious variant="filled" />
197
+ <CarouselNext variant="filled" />
198
+ </Carousel>
199
+ ```
200
+
201
+ ---
202
+
203
+ ### 6. Tùy biến vị trí tự do (Custom Slot / Bottom Toolbar)
204
+
205
+ Bạn có thể dễ dàng đặt nút điều hướng và phân trang vào trong một thanh công cụ tùy biến dưới đáy bằng cách thêm `className="static"` hoặc class định vị mong muốn:
206
+
207
+ ```tsx
208
+ <Carousel defaultIndex={1} className="w-full max-w-md border border-neutral-200 dark:border-neutral-700 p-2 rounded-xl">
209
+ <CarouselContent>
210
+ <CarouselSlide><div className="h-36 bg-linear-to-r from-primary-600 to-primary-800 text-white rounded-lg flex items-center justify-center font-semibold">Slide 1</div></CarouselSlide>
211
+ <CarouselSlide><div className="h-36 bg-linear-to-r from-primary-700 to-primary-900 text-white rounded-lg flex items-center justify-center font-semibold">Slide 2</div></CarouselSlide>
212
+ <CarouselSlide><div className="h-36 bg-linear-to-r from-primary-800 to-primary-950 text-white rounded-lg flex items-center justify-center font-semibold">Slide 3</div></CarouselSlide>
213
+ </CarouselContent>
214
+
215
+ {/* Custom Bottom Toolbar */}
216
+ <div className="flex justify-between items-center mt-3 px-2">
217
+ <CarouselPrevious className="static translate-y-0" variant="glass" />
218
+ <CarouselPagination className="static translate-x-0" type="dots" />
219
+ <CarouselNext className="static translate-y-0" variant="glass" />
220
+ </div>
221
+ </Carousel>
222
+ ```
223
+
224
+ ---
225
+
226
+ ### 7. Chế độ Controlled Mode (Quản lý State chủ động)
227
+
228
+ ```tsx
229
+ import { useState } from "react";
230
+ import { Carousel, CarouselContent, CarouselSlide, CarouselPrevious, CarouselNext, CarouselPagination } from "@owa/ui";
231
+
232
+ export function ControlledDemo() {
233
+ const [index, setIndex] = useState(0);
234
+
235
+ return (
236
+ <div className="space-y-4">
237
+ <div className="flex items-center gap-3">
238
+ <span className="text-sm font-semibold">Slide hiện tại: {index + 1}</span>
239
+ <button
240
+ type="button"
241
+ onClick={() => setIndex(2)}
242
+ className="px-3 py-1 bg-primary-600 text-white text-xs rounded-md shadow hover:bg-primary-700"
243
+ >
244
+ Nhảy tới Slide 3
245
+ </button>
246
+ </div>
247
+
248
+ <Carousel
249
+ currentIndex={index}
250
+ onIndexChange={setIndex}
251
+ className="w-full max-w-md shadow"
252
+ >
253
+ <CarouselContent>
254
+ <CarouselSlide><div className="h-36 bg-blue-600 text-white flex items-center justify-center">Slide 1</div></CarouselSlide>
255
+ <CarouselSlide><div className="h-36 bg-indigo-600 text-white flex items-center justify-center">Slide 2</div></CarouselSlide>
256
+ <CarouselSlide><div className="h-36 bg-purple-600 text-white flex items-center justify-center">Slide 3</div></CarouselSlide>
257
+ </CarouselContent>
258
+ <CarouselPrevious />
259
+ <CarouselNext />
260
+ <CarouselPagination />
261
+ </Carousel>
262
+ </div>
263
+ );
264
+ }
265
+ ```
266
+
267
+ ---
268
+
269
+ ## 📊 Bảng thuộc tính Props
270
+
271
+ ### `<Carousel>`
272
+
273
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
274
+ | :--- | :--- | :--- | :--- |
275
+ | `currentIndex` | `number` | - | Chỉ số slide đang active (sử dụng trong Controlled mode) |
276
+ | `defaultIndex` | `number` | `0` | Chỉ số slide mặc định khi khởi tạo (Uncontrolled mode) |
277
+ | `onIndexChange` | `(index: number) => void` | - | Callback kích hoạt khi chỉ số slide thay đổi |
278
+ | `loop` | `boolean` | `false` | Cho phép cuộn vòng tròn vô hạn |
279
+ | `slidesToShow` | `number` | `1` | Số lượng slide hiển thị đồng thời trên một khung nhìn |
280
+ | `slidesToScroll` | `number` | `1` | Số lượng slide di chuyển mỗi lần cuộn |
281
+ | `spacing` | `number \| string` | `0` | Khoảng cách gap giữa các slide (`16` hoặc `"1rem"`) |
282
+ | `autoPlay` | `boolean` | `true` | Tự động chuyển slide định kỳ |
283
+ | `interval` | `number` | `3000` | Thời gian chờ giữa mỗi lần tự động chuyển slide (ms) |
284
+ | `transitionDuration` | `number` | `650` | Thời gian hiệu ứng chuyển đổi giữa các slide (ms) |
285
+ | `pauseOnHover` | `boolean` | `true` | Tạm dừng autoplay khi rê chuột vào carousel |
286
+ | `pauseOnFocus` | `boolean` | `true` | Tạm dừng autoplay khi focus vào carousel |
287
+ | `draggable` | `boolean` | `true` | Cho phép kéo/vuốt bằng chuột hoặc ngón tay cảm ứng |
288
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Kích thước chung của carousel |
289
+ | `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Bo góc của container carousel |
290
+ | `children` | `ReactNode` | - | Các component con bên trong Carousel |
291
+
292
+ ---
293
+
294
+ ### `<CarouselContent>`
295
+
296
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
297
+ | :--- | :--- | :--- | :--- |
298
+ | `ref` | `Ref<HTMLDivElement>` | - | Ref đến dải trượt track |
299
+ | `children` | `ReactNode` | - | Danh sách các `<CarouselSlide>` |
300
+ | `className` | `string` | `""` | Tùy biến class cho container track |
301
+
302
+ ---
303
+
304
+ ### `<CarouselSlide>`
305
+
306
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
307
+ | :--- | :--- | :--- | :--- |
308
+ | `ref` | `Ref<HTMLDivElement>` | - | Ref đến phần tử DOM của slide |
309
+ | `index` | `number` | - | Chỉ số vị trí của slide |
310
+ | `children` | `ReactNode` | - | Nội dung bên trong slide |
311
+ | `className` | `string` | `""` | Class tùy biến cho slide |
312
+
313
+ ---
314
+
315
+ ### `<CarouselPrevious>` & `<CarouselNext>`
316
+
317
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
318
+ | :--- | :--- | :--- | :--- |
319
+ | `ref` | `Ref<HTMLButtonElement>` | - | Ref đến phần tử button |
320
+ | `variant` | `"glass" \| "filled" \| "outline" \| "ghost"` | `"glass"` | Biến thể hiển thị giao diện nút |
321
+ | `icon` | `ReactNode` | `<ChevronIcon />` | Biểu tượng icon tùy biến |
322
+ | `disabled` | `boolean` | - | Trạng thái vô hiệu hóa (tự động tính nếu không truyền) |
323
+ | `className` | `string` | `""` | Class tùy biến (có thể dùng `static` để gỡ bỏ định vị absolute) |
324
+
325
+ ---
326
+
327
+ ### `<CarouselPagination>`
328
+
329
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
330
+ | :--- | :--- | :--- | :--- |
331
+ | `ref` | `Ref<HTMLDivElement>` | - | Ref đến container phân trang |
332
+ | `type` | `"dots" \| "line" \| "fraction" \| "none"` | `"dots"` | Kiểu dáng hiển thị phân trang |
333
+ | `clickable` | `boolean` | `true` | Cho phép người dùng click vào chấm/thanh để chuyển slide |
334
+ | `className` | `string` | `""` | Class tùy biến |
335
+
336
+ ---
337
+
338
+ ## ⌨️ Phím tắt & Trợ năng (Accessibility)
339
+
340
+ - **`role="region"` & `aria-roledescription="carousel"`**: Báo hiệu với Screen Reader đây là một vùng băng chuyền nội dung.
341
+ - **`role="tablist"` & `role="tab"`**: Đánh dấu thanh phân trang và từng slide chỉ số chuẩn ngữ nghĩa.
342
+ - **Phím `ArrowLeft` / `ArrowRight`**: Chuyển đổi slide trước / sau.
343
+ - **Phím `Home` / `End`**: Nhảy nhanh về slide đầu tiên hoặc slide cuối cùng.
344
+ - **Tập trung bàn phím (Focus Ring)**: Có viền sáng `focus-visible:ring-2 focus-visible:ring-primary-500/50` rõ ràng khi dùng phím `Tab`.
345
+
346
+ ---
347
+
348
+ ## 🧪 Kiểm thử Component (Cypress Testing)
349
+
350
+ Component được kiểm thử 100% bằng **Cypress Component Testing** tại [`Carousel.cy.tsx`](Carousel.cy.tsx):
351
+
352
+ ```bash
353
+ pnpm --filter @owa/ui cypress:run --spec "src/components/carousel/Carousel.cy.tsx"
354
+ ```
@@ -0,0 +1,252 @@
1
+ # ☑️ Checkbox & CheckboxGroup Component (`@owa/ui`)
2
+
3
+ Bộ đôi component **Checkbox** và **CheckboxGroup** chuyên nghiệp, thiết kế theo kiến trúc **Data-driven thuần túy (Pure Data-driven)**, không sử dụng React Context, loại bỏ hoàn toàn `useEffect` gây cascading render, tích hợp **Live Search (Client & Server modes)** thông qua component `Input` variant `outline`, hỗ trợ **bảo lưu mục đã chọn (Preserve Selected)** và tuân thủ chặt chẽ tiêu chuẩn **WAI-ARIA Accessibility**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ ### 1. Checkbox
10
+ - **Độc lập & Tối ưu**: Không phụ thuộc vào Context, nhận props trực tiếp, hỗ trợ chuyển tiếp `ref` chuẩn React 19 mà không cần wrapper trung gian.
11
+ - **5 Kích thước tiêu chuẩn (`size`)**: `xs`, `sm`, `md` (*mặc định*), `lg`, `xl` đồng bộ tỉ lệ ô tick, icon SVG, nhãn và văn bản chú thích.
12
+ - **4 Biến thể giao diện (`variant`)**:
13
+ - `filled` (*mặc định*): Nền màu chủ đề khi được chọn.
14
+ - `outline`: Viền nét màu chủ đề, nền trong suốt.
15
+ - `soft`: Nền pastel dịu mắt (`bg-{color}-100`).
16
+ - `other`: Tự do tùy biến style qua `boxClassName`.
17
+ - **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `error`, `success`, `warning`, `info`, `neutral`.
18
+ - **6 Mức độ bo góc (`radius`)**: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
19
+ - **Trạng thái gạch ngang (`indeterminate`)**: Hỗ trợ trạng thái chọn một phần với thuộc tính chuẩn `aria-checked="mixed"` và icon trừ (`MinusIcon`).
20
+ - **2 Vị trí đặt nhãn (`labelPlacement`)**: `right` (*mặc định*) và `left` (căn đều hai bên).
21
+ - **Safe Config Fallback**: Sử dụng `getSafeConfig` đảm bảo component luôn an toàn, không bao giờ bị lỗi hiển thị khi nhận giá trị không hợp lệ.
22
+
23
+ ### 2. CheckboxGroup
24
+ - **Kiến trúc Data-driven thuần túy**: Nhận danh sách lựa chọn qua prop `options: CheckboxOptionItem<TData>[]`. Không còn cấu trúc compound component cồng kềnh, không tốn chi phí Context Provider.
25
+ - **Tích hợp tìm kiếm trực quan (`searchable`)**:
26
+ - Sử dụng component `Input` variant `outline` với icon tìm kiếm (`SearchIcon`) và nút xóa nhanh (`isClearable`).
27
+ - **Không làm mất focus**: Trạng thái tải ngầm hiển thị spinner ở góc phải qua `rightIcon`, không disable input trong khi người dùng đang gõ phím.
28
+ - **2 Chế độ tìm kiếm linh hoạt (`searchMode`)**:
29
+ - `client` (*mặc định*): Tìm kiếm thông minh bằng thuật toán fuzzy ranking của `@tanstack/match-sorter-utils`. Hỗ trợ tìm theo đa trường dữ liệu (`searchField`) hoặc custom function (`filterFn`).
30
+ - `server`: Tìm kiếm qua API máy chủ với `onSearch` và bộ đệm thời gian `debounceMs`.
31
+ - **Bảo lưu mục đã chọn (`preserveSelected`)**: Tự động ghim lại các mục đã chọn lên đầu danh sách khi chuyển đổi từ khóa tìm kiếm mới, quản lý bộ nhớ thông minh (chỉ lưu các mục thực sự được tick chọn).
32
+ - **2 Bố cục sắp xếp (`orientation`)**: `vertical` (*mặc định*) và `horizontal`.
33
+
34
+ ---
35
+
36
+ ## 🚀 Cài đặt & Import
37
+
38
+ ```tsx
39
+ import { Checkbox, CheckboxGroup } from "@owa/ui";
40
+ import type {
41
+ CheckboxProps,
42
+ CheckboxGroupProps,
43
+ CheckboxOptionItem,
44
+ CheckboxSize,
45
+ CheckboxVariant,
46
+ CheckboxColor,
47
+ CheckboxRadius,
48
+ CheckboxLabelPlacement,
49
+ CheckboxSearchMode,
50
+ } from "@owa/ui";
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 📖 Hướng dẫn sử dụng
56
+
57
+ ### 1. Sử dụng Checkbox đơn lẻ
58
+
59
+ ```tsx
60
+ import { useState } from "react";
61
+ import { Checkbox } from "@owa/ui";
62
+
63
+ export function SingleCheckboxExample() {
64
+ const [agree, setAgree] = useState(false);
65
+
66
+ return (
67
+ <Checkbox
68
+ checked={agree}
69
+ onChange={(e) => setAgree(e.target.checked)}
70
+ label="Tôi đồng ý với các điều khoản và chính sách dịch vụ"
71
+ helperText="Vui lòng đọc kỹ trước khi tiếp tục"
72
+ config={{ isRequired: true }}
73
+ />
74
+ );
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ### 2. CheckboxGroup cơ bản (Data-driven)
81
+
82
+ ```tsx
83
+ import { useState } from "react";
84
+ import { CheckboxGroup } from "@owa/ui";
85
+
86
+ export function BasicGroupExample() {
87
+ const [selected, setSelected] = useState<string[]>(["react"]);
88
+
89
+ return (
90
+ <CheckboxGroup
91
+ label="Kỹ năng công nghệ"
92
+ helperText="Chọn các kỹ năng bạn có kinh nghiệm làm việc"
93
+ value={selected}
94
+ onChange={setSelected}
95
+ color="primary"
96
+ orientation="horizontal"
97
+ options={[
98
+ { value: "react", label: "React 19" },
99
+ { value: "vue", label: "Vue.js 3" },
100
+ { value: "tailwind", label: "Tailwind CSS v4" },
101
+ { value: "typescript", label: "TypeScript" },
102
+ ]}
103
+ />
104
+ );
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ### 3. Tìm kiếm Client Mode (Fuzzy Search & Đa trường)
111
+
112
+ ```tsx
113
+ import { useState } from "react";
114
+ import { CheckboxGroup } from "@owa/ui";
115
+
116
+ const frameworks = [
117
+ { value: "react", label: "React JS", code: "FE-01", description: "Facebook library" },
118
+ { value: "vue", label: "Vue JS", code: "FE-02", description: "Progressive framework" },
119
+ { value: "angular", label: "Angular", code: "FE-03", description: "Google platform" },
120
+ { value: "svelte", label: "Svelte", code: "FE-04", description: "Cybernetically enhanced" },
121
+ ];
122
+
123
+ export function ClientSearchExample() {
124
+ const [selected, setSelected] = useState<string[]>(["react"]);
125
+
126
+ return (
127
+ <CheckboxGroup
128
+ label="Tìm kiếm Framework"
129
+ searchable
130
+ searchField={["label", "code", "description"]}
131
+ searchPlaceholder="Tìm theo tên, mã code hoặc mô tả..."
132
+ preserveSelected
133
+ value={selected}
134
+ onChange={setSelected}
135
+ options={frameworks}
136
+ />
137
+ );
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ### 4. Tìm kiếm Server Mode (Gọi API với Debounce & Bảo lưu mục đã chọn)
144
+
145
+ ```tsx
146
+ import { useState } from "react";
147
+ import { CheckboxGroup } from "@owa/ui";
148
+
149
+ export function ServerSearchExample() {
150
+ const [selectedProducts, setSelectedProducts] = useState<string[]>([]);
151
+
152
+ const handleSearchProducts = async (query: string) => {
153
+ const res = await fetch(
154
+ `https://dummyjson.com/products/search?q=${encodeURIComponent(query)}&limit=5`
155
+ );
156
+ if (!res.ok) throw new Error("API request failed");
157
+ const data = await res.json();
158
+
159
+ return data.products.map((p: any) => ({
160
+ value: String(p.id),
161
+ label: p.title,
162
+ description: `$${p.price} - ${p.category}`,
163
+ }));
164
+ };
165
+
166
+ return (
167
+ <CheckboxGroup
168
+ label="Chọn sản phẩm"
169
+ searchable
170
+ searchMode="server"
171
+ debounceMs={300}
172
+ onSearch={handleSearchProducts}
173
+ searchPlaceholder="Tìm kiếm sản phẩm từ máy chủ..."
174
+ preserveSelected
175
+ value={selectedProducts}
176
+ onChange={setSelectedProducts}
177
+ color="info"
178
+ />
179
+ );
180
+ }
181
+ ```
182
+
183
+ ---
184
+
185
+ ## 🎛️ Bảng Props & API Reference
186
+
187
+ ### `CheckboxProps`
188
+
189
+ Kế thừa `Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type">`:
190
+
191
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
192
+ | :--- | :--- | :--- | :--- |
193
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích cỡ ô chọn, icon và nhãn |
194
+ | `variant` | `'filled' \| 'outline' \| 'soft' \| 'other'` | `'filled'` | Kiểu dáng hiển thị của box |
195
+ | `color` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | `'primary'` | Chủ đề màu sắc |
196
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Mức độ bo góc |
197
+ | `config` | `CheckboxConfig` | `undefined` | Cấu hình cờ trạng thái (`isRequired`, `indeterminate`, `isLoading`, `isInvalid`,...) |
198
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa tương tác |
199
+ | `readOnly` | `boolean` | `false` | Chế độ chỉ đọc |
200
+ | `labelPlacement` | `'right' \| 'left'` | `'right'` | Vị trí hiển thị nhãn |
201
+ | `label` | `ReactNode` | `undefined` | Nhãn văn bản cạnh ô checkbox |
202
+ | `helperText` | `ReactNode` | `undefined` | Chú thích bên dưới |
203
+ | `errorMessage` | `ReactNode` | `undefined` | Thông báo lỗi |
204
+ | `icon` | `ReactNode` | `CheckIcon` | Icon tùy biến khi checked |
205
+ | `indeterminateIcon` | `ReactNode` | `MinusIcon` | Icon tùy biến khi indeterminate |
206
+
207
+ ---
208
+
209
+ ### `CheckboxGroupProps<TData = unknown>`
210
+
211
+ Kế thừa `Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue" | "children">`:
212
+
213
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
214
+ | :--- | :--- | :--- | :--- |
215
+ | `options` | `CheckboxOptionItem<TData>[]` | `[]` | Mảng dữ liệu các lựa chọn (Data-driven) |
216
+ | `value` | `string[]` | `undefined` | Danh sách giá trị đã chọn (Controlled) |
217
+ | `defaultValue` | `string[]` | `[]` | Danh sách giá trị mặc định (Uncontrolled) |
218
+ | `onChange` | `(values: string[]) => void` | `undefined` | Callback khi danh sách chọn thay đổi |
219
+ | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Bố cục sắp xếp các mục |
220
+ | `searchable` | `boolean` | `false` | Bật/tắt thanh tìm kiếm |
221
+ | `searchMode` | `'client' \| 'server'` | `'client'` | Chế độ tìm kiếm phía client hoặc gọi API server |
222
+ | `searchField` | `string \| string[]` | `'label'` | Các trường dữ liệu dùng để tìm kiếm |
223
+ | `filterFn` | `(item: CheckboxOptionItem<TData>, query: string) => boolean` | `undefined` | Hàm lọc tùy biến phía client |
224
+ | `onSearch` | `(query: string) => Promise<CheckboxOptionItem<TData>[]>` | `undefined` | Hàm gọi API tìm kiếm phía server |
225
+ | `debounceMs` | `number` | `300` | Thời gian trễ gọi hàm `onSearch` (ms) |
226
+ | `preserveSelected` | `boolean` | `true` | Bảo lưu các mục đã chọn khi từ khóa tìm kiếm thay đổi |
227
+ | `emptyText` | `ReactNode` | `'Không tìm thấy kết quả'` | Thông báo khi không có kết quả |
228
+ | `size` | `CheckboxSize` | `'md'` | Kích cỡ truyền xuống toàn bộ checkbox con |
229
+ | `color` | `CheckboxColor` | `'primary'` | Màu sắc truyền xuống toàn bộ checkbox con |
230
+ | `variant` | `CheckboxVariant` | `'filled'` | Biến thể truyền xuống toàn bộ checkbox con |
231
+ | `radius` | `CheckboxRadius` | `undefined` | Bo góc truyền xuống toàn bộ checkbox con |
232
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ nhóm |
233
+ | `isReadOnly` | `boolean` | `false` | Chế độ chỉ đọc cho toàn bộ nhóm |
234
+ | `isLoading` | `boolean` | `false` | Trạng thái đang tải của nhóm |
235
+ | `label` | `ReactNode` | `undefined` | Tiêu đề của nhóm |
236
+ | `helperText` | `ReactNode` | `undefined` | Chú thích của nhóm |
237
+ | `errorMessage` | `ReactNode` | `undefined` | Thông báo lỗi của nhóm |
238
+
239
+ ---
240
+
241
+ ### `CheckboxOptionItem<TData = unknown>`
242
+
243
+ | Trường | Kiểu dữ liệu | Mô tả |
244
+ | :--- | :--- | :--- |
245
+ | `value` | `string` | Giá trị định danh duy nhất của ô chọn |
246
+ | `label` | `ReactNode` | Nhãn hiển thị chính |
247
+ | `description` | `ReactNode` | Đoạn chú thích/mô tả phụ bên dưới nhãn |
248
+ | `disabled` | `boolean` | Vô hiệu hóa ô chọn này |
249
+ | `isReadOnly` | `boolean` | Chế độ chỉ đọc cho ô chọn này |
250
+ | `indeterminate`| `boolean` | Trạng thái gạch ngang cho ô chọn này |
251
+ | `data` | `TData` | Đối tượng dữ liệu gốc đính kèm |
252
+ | `[key: string]` | `unknown` | Mở rộng các trường tùy ý (phục vụ lọc theo `searchField`) |