@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,559 @@
1
+ # 🔤 Input Component Suite (`@owa/ui`)
2
+
3
+ Bộ component **Input** toàn diện, linh hoạt và tương tác cao, thiết kế chuẩn **Design System**, hỗ trợ **Safe Config Fallback**, **Floating Labels**, **Loading & Spinners**, và tuân thủ đầy đủ tiêu chuẩn **WAI-ARIA Accessibility**.
4
+
5
+ ---
6
+
7
+ ## 🌟 Điểm nổi bật
8
+
9
+ - **Đa dạng biến thể Input chuyên dụng**:
10
+ - `<Input>`: Ô nhập liệu văn bản tiêu chuẩn (text, email, url, tel, search...).
11
+ - `<PasswordInput>`: Ô nhập mật khẩu tích hợp nút ẩn/hiện mật khẩu bảo mật.
12
+ - `<NumberInput>`: Ô nhập số hỗ trợ định dạng số, phân tách hàng nghìn, tiền tệ và giới hạn min/max.
13
+ - `<OtpInput>`: Ô nhập mã OTP chia từng ô (slot) hỗ trợ dán tự động (paste) và chuyển focus thông minh.
14
+ - `<MultiInput>`: Ô nhập nhiều giá trị (tags / chip list) hỗ trợ xóa nhanh, badge preview.
15
+ - **5 Kích thước tiêu chuẩn (`size`)**: `xs` (24px), `sm` (32px), `md` (40px - *mặc định*), `lg` (48px), `xl` (56px) với font chữ, padding, kích thước icon và label được tính toán theo tỷ lệ chuẩn.
16
+ - **3 Biến thể giao diện (`variant`)**:
17
+ - `outline` *(mặc định)*: Viền nét rõ ràng quanh khung nhập liệu, hover/focus đổi màu viền chủ đề.
18
+ - `filled`: Nền pastel nhạt (`bg-{color}-50/60`), viền đồng điệu.
19
+ - `ghost`: Nền trong suốt, tinh giản, chỉ nổi bật khi hover/focus.
20
+ - `other`: Bỏ qua các class màu mặc định, tự do áp dụng custom style qua `inputWrapperClassName`.
21
+ - **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `error`, `success`, `warning`, `info`, `neutral`.
22
+ - **6 Mức độ bo góc (`radius`)**: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
23
+ - **3 Vị trí đặt nhãn (`labelPlacement`)**:
24
+ - `floating` *(mặc định)*: Nhãn lơ lửng, nằm cố định chính giữa viền trên khung input.
25
+ - `top`: Nhãn nằm phía trên ô input.
26
+ - `left`: Nhãn nằm ngang bên trái ô input.
27
+ - **Trạng thái Loading & Xoay Spinner (`isLoading` & `showSpinner`)**:
28
+ - `isLoading={true}`: Tự động vô hiệu hóa tương tác (`disabled`), kích hoạt `aria-busy="true"` và `aria-disabled="true"`.
29
+ - `showSpinner`: Mặc định là `false`. Đặt `showSpinner={true}` khi muốn hiển thị spinner xoay vòng ở bên phải.
30
+ - **Slots linh hoạt**: Hỗ trợ `leftIcon`, `rightIcon`, `leftAddon`, `rightAddon` và nút xóa nhanh `isClearable`.
31
+ - **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` đảm bảo component hoạt động an toàn, không bị crash kể cả khi truyền prop không hợp lệ.
32
+
33
+ ---
34
+
35
+ ## 🚀 Cài đặt & Import
36
+
37
+ ```tsx
38
+ import {
39
+ Input,
40
+ PasswordInput,
41
+ NumberInput,
42
+ OtpInput,
43
+ MultiInput,
44
+ splitTagsFromText,
45
+ isValidOtpChar,
46
+ sanitizeOtpString,
47
+ } from "@owa/ui";
48
+ import type {
49
+ InputProps,
50
+ InputConfig,
51
+ PasswordInputProps,
52
+ NumberInputProps,
53
+ OtpInputProps,
54
+ OtpInputConfig,
55
+ OtpInputRef,
56
+ OtpInputType,
57
+ MultiInputProps,
58
+ MultiInputConfig,
59
+ InputSize,
60
+ InputVariant,
61
+ InputColor,
62
+ InputRadius,
63
+ InputLabelPlacement,
64
+ } from "@owa/ui";
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 📖 Hướng dẫn sử dụng
70
+
71
+ ### 1. Cách sử dụng cơ bản
72
+
73
+ ```tsx
74
+ import { Input } from "@owa/ui";
75
+
76
+ export function BasicInputExample() {
77
+ return (
78
+ <div className="flex flex-col gap-4 max-w-sm">
79
+ <Input label="Họ và tên" placeholder="Nguyễn Văn A" />
80
+ <Input label="Email" type="email" placeholder="example@domain.com" />
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ### 2. Vị trí đặt nhãn (`labelPlacement`)
89
+
90
+ ```tsx
91
+ // 1. Floating (Mặc định)
92
+ <Input label="Nhãn lơ lửng" labelPlacement="floating" placeholder="Nhập văn bản..." />
93
+
94
+ // 2. Top (Phía trên)
95
+ <Input label="Nhãn phía trên" labelPlacement="top" placeholder="Nhập văn bản..." />
96
+
97
+ // 3. Left (Ngang bên trái)
98
+ <Input label="Nhãn bên trái" labelPlacement="left" placeholder="Nhập văn bản..." />
99
+ ```
100
+
101
+ ---
102
+
103
+ ### 3. Icon & Addons
104
+
105
+ ```tsx
106
+ <Input
107
+ label="Website"
108
+ leftAddon="https://"
109
+ rightAddon=".com"
110
+ placeholder="mywebsite"
111
+ />
112
+
113
+ <Input
114
+ label="Tìm kiếm"
115
+ leftIcon={<SearchIcon />}
116
+ isClearable={true}
117
+ placeholder="Nhập từ khóa tìm kiếm..."
118
+ />
119
+ ```
120
+
121
+ ---
122
+
123
+ ### 4. Trạng thái Loading & Xóa nhanh
124
+
125
+ ```tsx
126
+ <Input
127
+ label="Đang tải dữ liệu"
128
+ isLoading={true}
129
+ showSpinner={true}
130
+ defaultValue="Đang đồng bộ..."
131
+ />
132
+ ```
133
+
134
+ ---
135
+
136
+ ### 5. Trạng thái Báo lỗi & Hướng dẫn
137
+
138
+ ```tsx
139
+ <Input
140
+ label="Mật khẩu"
141
+ isRequired={true}
142
+ errorMessage="Mật khẩu tối thiểu 8 ký tự!"
143
+ isInvalid={true}
144
+ />
145
+
146
+ <Input
147
+ label="Tên đăng nhập"
148
+ helperText="Chỉ bao gồm chữ cái viết thường và số."
149
+ />
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 🛠 Bảng thông số Props (`InputProps`)
155
+
156
+ | Tên Prop | Kiểu dữ liệu | Giá trị mặc định | Mô tả |
157
+ | :--- | :--- | :---: | :--- |
158
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích cỡ giao diện (font chữ, chiều cao, padding, kích thước icon/label). |
159
+ | `variant` | `'outline' \| 'filled' \| 'ghost' \| 'other'` | `'outline'` | Biến thể hiển thị giao diện. |
160
+ | `color` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | `'primary'` | Chủ đề màu sắc theo Design System. |
161
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | — | Độ bo góc của khung viền input (mặc định theo từng size). |
162
+ | `label` | `ReactNode` | — | Nhãn tiêu đề hiển thị cho ô nhập liệu. |
163
+ | `labelPlacement` | `'floating' \| 'top' \| 'left'` | `'floating'` | Vị trí hiển thị của nhãn. |
164
+ | `config` | `InputConfig` | — | Cấu hình tập trung các cờ trạng thái / tính năng (`isRequired`, `isInvalid`, `isLoading`, `showSpinner`, `isClearable`, `isFullWidth`). |
165
+ | `isRequired` | `boolean` | `false` | Hiển thị dấu sao đỏ `*` và đánh dấu `aria-required="true"`. |
166
+ | `helperText` | `ReactNode` | — | Đoạn văn bản hướng dẫn/trợ giúp bên dưới ô. |
167
+ | `errorMessage` | `ReactNode` | — | Thông báo lỗi khi nhập sai (tự kích hoạt trạng thái báo lỗi). |
168
+ | `isInvalid` | `boolean` | `false` | Bật trạng thái viền đỏ báo lỗi và `aria-invalid="true"`. |
169
+ | `isLoading` | `boolean` | `false` | Khóa tương tác, bật `aria-busy="true"` và `aria-disabled="true"`. |
170
+ | `showSpinner` | `boolean` | `false` | Hiển thị biểu tượng xoay spinner khi `isLoading={true}`. |
171
+ | `leftIcon` | `ReactNode` | — | Icon hiển thị ở đầu ô input. |
172
+ | `rightIcon` | `ReactNode` | — | Icon hiển thị ở cuối ô input. |
173
+ | `leftAddon` | `ReactNode` | — | Addon / prefix cố định ở đầu ô input (vd: `'https://'`). |
174
+ | `rightAddon` | `ReactNode` | — | Addon / suffix cố định ở cuối ô input (vd: `'.com'`). |
175
+ | `isClearable` | `boolean` | `false` | Hiển thị nút xóa nhanh nội dung khi có văn bản. |
176
+ | `onClear` | `() => void` | — | Callback được gọi khi bấm nút xóa nhanh. |
177
+ | `isFullWidth` | `boolean` | `false` | Mở rộng chiếm toàn bộ 100% chiều ngang container cha. |
178
+ | `ref` | `Ref<HTMLInputElement>` | — | Ref chuyển tiếp đến thẻ `<input>` HTML bên dưới. |
179
+
180
+ ---
181
+
182
+ ## 💵 NumberInput Component
183
+
184
+ Component **`NumberInput`** chuyên dụng cho việc nhập số, tiền tệ, khối lượng, tỷ giá với khả năng **tự động phân cách hàng nghìn/thập phân trong thời gian thực**, **giữ vị trí con trỏ chuột không bị nhảy**, **tối ưu bàn phím di động (`inputMode`)**, và **giới hạn min/max thông minh**.
185
+
186
+ ### 🌟 Điểm nổi bật của NumberInput
187
+ - **Định dạng số theo thời gian thực**: Tự động chèn dấu phân cách khi gõ (ví dụ `1000000` -> `1.000.000`).
188
+ - **Giữ vị trí con trỏ chuẩn xác**: Tính toán vị trí con trỏ chuột khi chèn/xóa dấu phân cách, người dùng thoải mái sửa số ở giữa mà không bị nhảy con trỏ về cuối.
189
+ - **Tối ưu Bàn phím Mobile**: Tự động bật bàn phím số `inputMode="numeric"` hoặc `inputMode="decimal"` khi có số thập phân.
190
+ - **Hỗ trợ đa định dạng quốc tế**: Dễ dàng tùy biến `thousandSeparator` và `decimalSeparator` (chuẩn VN `.` / `,` hoặc chuẩn US `,` / `.`).
191
+ - **Giới hạn Min / Max thông minh**: Clamp `max` tức thì để chặn nhập vượt ngưỡng, đồng thời cho phép gõ từng chữ số với `min` dương và tự động clamp về `min` khi `onBlur`.
192
+ - **Hỗ trợ `value` linh hoạt**: Nhận cả số nguyên (`value={1000000}`) hoặc chuỗi (`value="1.000.000"`).
193
+
194
+ ### 📖 Ví dụ sử dụng `NumberInput`
195
+
196
+ #### 1. Nhập số tiền cơ bản (Chuẩn Việt Nam)
197
+ ```tsx
198
+ import { NumberInput } from "@owa/ui";
199
+
200
+ export function CurrencyExample() {
201
+ const [amount, setAmount] = useState<string | number>("");
202
+
203
+ return (
204
+ <NumberInput
205
+ label="Số tiền thanh toán"
206
+ rightAddon="VNĐ"
207
+ value={amount}
208
+ onChange={(e) => setAmount(e.target.value)}
209
+ placeholder="0"
210
+ />
211
+ );
212
+ }
213
+ ```
214
+
215
+ #### 2. Nhập số thập phân (Ví dụ: Khối lượng kg hoặc USD)
216
+ ```tsx
217
+ <NumberInput
218
+ label="Trọng lượng"
219
+ rightAddon="kg"
220
+ maxDecimalDigits={2}
221
+ decimalSeparator=","
222
+ thousandSeparator="."
223
+ placeholder="0,00"
224
+ />
225
+
226
+ <NumberInput
227
+ label="Số tiền USD"
228
+ leftAddon="$"
229
+ maxDecimalDigits={2}
230
+ decimalSeparator="."
231
+ thousandSeparator=","
232
+ placeholder="0.00"
233
+ />
234
+ ```
235
+
236
+ #### 3. Giới hạn Min & Max
237
+ ```tsx
238
+ <NumberInput
239
+ label="Số lượng vé"
240
+ min={1}
241
+ max={10}
242
+ helperText="Tối thiểu 1 vé, tối đa 10 vé mỗi lượt mua."
243
+ />
244
+ ```
245
+
246
+ ---
247
+
248
+ ### 🛠 Bảng thông số Props (`NumberInputProps`)
249
+
250
+ | Tên Prop | Kiểu dữ liệu | Giá trị mặc định | Mô tả |
251
+ | :--- | :--- | :---: | :--- |
252
+ | `value` | `string \| number \| null` | — | Giá trị số hoặc chuỗi đã format (Controlled mode). |
253
+ | `defaultValue` | `string \| number \| null` | — | Giá trị mặc định ban đầu (Uncontrolled mode). |
254
+ | `min` | `number` | — | Giá trị nhỏ nhất cho phép (clamp khi onBlur hoặc số âm vượt ngưỡng). |
255
+ | `max` | `number` | — | Giá trị lớn nhất cho phép (clamp thời gian thực khi gõ). |
256
+ | `thousandSeparator` | `string` | `'.'` | Ký tự phân cách hàng nghìn. |
257
+ | `decimalSeparator` | `string` | `','` | Ký tự phân cách phần thập phân. |
258
+ | `maxDecimalDigits` | `number` | `0` | Số chữ số thập phân tối đa (`0` là số nguyên). |
259
+ | `allowNegative` | `boolean` | — | Cho phép nhập số âm (mặc định `true` nếu `min < 0` hoặc chưa đặt `min`). |
260
+ | `onChange` | `(e: ChangeEvent<HTMLInputElement>) => void` | — | Callback khi giá trị input thay đổi. |
261
+ | `onBlur` | `(e: FocusEvent<HTMLInputElement>) => void` | — | Callback khi blur ra ngoài (tự động clamp `min`). |
262
+ | `...props` | `InputProps` | — | Thừa hưởng toàn bộ props của component `Input` (`size`, `variant`, `color`, `radius`, `label`, `config`, `leftIcon`, `rightAddon`,...). |
263
+
264
+ ---
265
+
266
+ ## 🔢 OtpInput Component
267
+
268
+ Component **`OtpInput`** chuyên dụng cho việc nhập mã xác thực OTP (One-Time Password) hoặc mã PIN với từng ô ký tự riêng biệt, hỗ trợ **Mobile SMS Autofill**, **Smart Paste**, **Keyboard Navigation**, **Tự động bôi đen để gõ đè**, và **Tích hợp HTML Form**.
269
+
270
+ ### 🌟 Điểm nổi bật của OtpInput
271
+ - **Tự động chuyển Focus**: Tự động chuyển con trỏ sang ô tiếp theo khi gõ, lùi lại ô trước khi bấm `Backspace`.
272
+ - **Hỗ trợ Mobile SMS Autofill & Smart Paste**: Bắt cả sự kiện `onPaste` trực tiếp và `onChange` khi iOS/Android tự động điền mã OTP từ tin nhắn SMS.
273
+ - **Tự động bôi đen khi Focus (`onFocus select`)**: Cho phép người dùng click vào bất kỳ ô nào và gõ số mới để đè lên số cũ mà không cần xóa thủ công.
274
+ - **Điều khiển trực tiếp qua Ref (`OtpInputRef`)**: Hỗ trợ gọi `ref.current.getValue()`, `ref.current.clear()`, và `ref.current.focus(index)`.
275
+ - **Tích hợp Form HTML chuẩn**: Tự động render `<input type="hidden" name={name} value={...}>` giúp gửi toàn bộ chuỗi OTP khi submit `<form>` hoặc dùng `FormData`.
276
+ - **Phân nhóm linh hoạt**: Hỗ trợ `groupSize` và `separator` (ví dụ chia cụm `3-3`: `123 - 456`).
277
+ - **Bảo mật & Che giấu mã**: Hỗ trợ `mask={true}` hoặc `type="password"`.
278
+
279
+ ### 📖 Ví dụ sử dụng `OtpInput`
280
+
281
+ #### 1. Sử dụng cơ bản & Lắng nghe hoàn thành
282
+ ```tsx
283
+ import { OtpInput } from "@owa/ui";
284
+
285
+ export function OtpBasicExample() {
286
+ return (
287
+ <OtpInput
288
+ length={6}
289
+ label="Mã xác thực OTP"
290
+ helperText="Nhập 6 chữ số được gửi tới số điện thoại của bạn."
291
+ onChange={(val) => console.log("Đang nhập:", val)}
292
+ onComplete={(val) => console.log("Đã điền đủ 6 số:", val)}
293
+ />
294
+ );
295
+ }
296
+ ```
297
+
298
+ #### 2. Điều khiển bằng `ref` (`OtpInputRef`)
299
+ ```tsx
300
+ import { useRef } from "react";
301
+ import { OtpInput, type OtpInputRef } from "@owa/ui";
302
+
303
+ export function OtpRefExample() {
304
+ const otpRef = useRef<OtpInputRef>(null);
305
+
306
+ const handleResend = () => {
307
+ // Xóa trắng toàn bộ các ô và tự động focus lại ô đầu tiên
308
+ otpRef.current?.clear();
309
+ };
310
+
311
+ const handleCheck = () => {
312
+ const code = otpRef.current?.getValue();
313
+ alert(`Mã hiện tại: ${code}`);
314
+ };
315
+
316
+ return (
317
+ <div className="flex flex-col gap-3">
318
+ <OtpInput ref={otpRef} length={4} />
319
+ <div className="flex gap-2">
320
+ <button onClick={handleResend}>Gửi lại mã</button>
321
+ <button onClick={handleCheck}>Kiểm tra mã</button>
322
+ </div>
323
+ </div>
324
+ );
325
+ }
326
+ ```
327
+
328
+ #### 3. Phân nhóm & Che giấu mã (Masked)
329
+ ```tsx
330
+ <OtpInput
331
+ length={6}
332
+ groupSize={3}
333
+ separator="-"
334
+ mask={true}
335
+ label="Mã PIN giao dịch"
336
+ />
337
+ ```
338
+
339
+ #### 4. Cấu hình trạng thái qua `config` (`OtpInputConfig`)
340
+ ```tsx
341
+ <OtpInput
342
+ length={6}
343
+ name="otp_token"
344
+ config={{
345
+ isRequired: true,
346
+ isLoading: isSubmitting,
347
+ showSpinner: true,
348
+ isInvalid: hasError,
349
+ }}
350
+ errorMessage={hasError ? "Mã OTP không chính xác hoặc đã hết hạn!" : undefined}
351
+ />
352
+ ```
353
+
354
+ ---
355
+
356
+ ### 🛠 Bảng thông số Props (`OtpInputProps`)
357
+
358
+ | Tên Prop | Kiểu dữ liệu | Giá trị mặc định | Mô tả |
359
+ | :--- | :--- | :---: | :--- |
360
+ | `length` | `number` | `6` | Số lượng ô ký tự OTP cần nhập. |
361
+ | `value` | `string` | — | Giá trị chuỗi OTP (Controlled mode). |
362
+ | `defaultValue` | `string` | `""` | Giá trị mặc định ban đầu (Uncontrolled mode). |
363
+ | `onChange` | `(value: string) => void` | — | Callback kích hoạt mỗi khi chuỗi OTP thay đổi. |
364
+ | `onComplete` | `(value: string) => void` | — | Callback kích hoạt khi người dùng đã điền đủ tất cả các ô. |
365
+ | `type` | `'numeric' \| 'alphanumeric' \| 'password'` | `'numeric'` | Kiểu ký tự cho phép nhập. |
366
+ | `mask` | `boolean \| string` | `false` | Ẩn ký tự đã nhập (dạng chấm tròn mật khẩu). |
367
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích thước từng ô slot (`xs`: 24px, `sm`: 32px, `md`: 40px, `lg`: 48px, `xl`: 56px). |
368
+ | `variant` | `'outline' \| 'filled' \| 'ghost' \| 'other'` | `'outline'` | Biến thể hiển thị giao diện của từng ô slot. |
369
+ | `color` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | `'primary'` | Chủ đề màu sắc viền/nền khi focus. |
370
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | — | Độ bo góc của từng ô slot. |
371
+ | `config` | `OtpInputConfig` | — | Cấu hình tập trung các cờ trạng thái (`isRequired`, `isInvalid`, `isLoading`, `showSpinner`). |
372
+ | `id` | `string` | — | ID tùy biến (tự sinh `${id}-slot-${index}`). |
373
+ | `name` | `string` | — | Tên trường form (tự render hidden input chứa toàn bộ chuỗi OTP gộp lại). |
374
+ | `autoFocus` | `boolean` | `false` | Tự động focus vào ô trống đầu tiên khi mount. |
375
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa tương tác toàn bộ các ô. |
376
+ | `readOnly` | `boolean` | `false` | Chế độ chỉ đọc, không cho phép chỉnh sửa. |
377
+ | `groupSize` | `number` | — | Số lượng ô trong mỗi nhóm (ví dụ `3` cho cụm `3-3`). |
378
+ | `separator` | `ReactNode` | `'-'` | Ký tự hoặc icon phân cách giữa các nhóm. |
379
+ | `label` | `ReactNode` | — | Nhãn mô tả cho trường nhập OTP. |
380
+ | `labelPlacement` | `'top' \| 'left'` | `'top'` | Vị trí đặt nhãn. |
381
+ | `helperText` | `ReactNode` | — | Văn bản hướng dẫn hiển thị bên dưới. |
382
+ | `errorMessage` | `ReactNode` | — | Thông báo lỗi (tự chuyển trạng thái sang error). |
383
+ | `allowOneTimeCode` | `boolean` | `true` | Cho phép hệ điều hành tự động điền mã OTP từ tin nhắn SMS (`autoComplete="one-time-code"`). |
384
+ | `getSlotAriaLabel` | `(index: number, length: number) => string` | — | Tùy biến `aria-label` cho từng ô slot (hỗ trợ đa ngôn ngữ i18n). |
385
+ | `ref` | `Ref<OtpInputRef>` | — | Ref cung cấp các phương thức điều khiển: `getValue()`, `clear()`, `focus(index)`. |
386
+
387
+ ---
388
+
389
+ ### ⚙️ Cấu hình `OtpInputConfig`
390
+
391
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
392
+ | :--- | :--- | :---: | :--- |
393
+ | `isRequired` | `boolean` | `false` | Hiển thị dấu `*` đỏ và đánh dấu `aria-required="true"`. |
394
+ | `isInvalid` | `boolean` | `false` | Kích hoạt giao diện báo lỗi và `aria-invalid="true"`. |
395
+ | `isLoading` | `boolean` | `false` | Khóa tương tác (`aria-busy="true"`). |
396
+ | `showSpinner` | `boolean` | `false` | Hiển thị biểu tượng xoay spinner cạnh nhóm ô OTP khi `isLoading = true`. |
397
+
398
+ ---
399
+
400
+ ### 🎛️ Phương thức `OtpInputRef`
401
+
402
+ | Phương thức | Tham số | Giá trị trả về | Mô tả |
403
+ | :--- | :--- | :--- | :--- |
404
+ | `getValue()` | — | `string` | Lấy chuỗi OTP hoàn chỉnh hiện tại (ví dụ: `"123456"`). |
405
+ | `clear()` | — | `void` | Xóa sạch toàn bộ các ô về rỗng, trigger `onChange("")` và tự động focus về ô đầu tiên. |
406
+ | `focus(index?)` | `index?: number` | `void` | Focus vào ô chỉ định (mặc định là ô đầu tiên `index = 0`). |
407
+
408
+ ---
409
+
410
+ ## 🏷️ MultiInput Component
411
+
412
+ Component **`MultiInput`** chuyên dụng cho việc nhập nhiều giá trị (tags, keywords, chips, danh sách email...) với khả năng **phân tách tự động khi gõ hoặc Paste**, **ngăn chặn trùng lặp**, **giới hạn số lượng tag**, **thu gọn hiển thị `+N`**, và **tích hợp HTML Form Submission dạng mảng (`name[]`)**.
413
+
414
+ ### 🌟 Điểm nổi bật của MultiInput
415
+ - **Tạo tag linh hoạt**: Nhấn `Enter`, `Comma (,)`, `Tab`, `Space` (hoặc tùy biến qua prop `delimiters`).
416
+ - **Phân tách thông minh khi Paste**: Tự động nhận diện chuỗi copy nhiều giá trị (ví dụ: `"React, Vue; Angular\nSvelte"`) và phân tách thành từng tag độc lập.
417
+ - **Quản lý giới hạn & Xác thực**: Hỗ trợ chặn tag trùng (`allowDuplicates={false}`), giới hạn tổng tag (`maxTags`), độ dài tối đa (`maxTagLength`), và hàm validate tùy biến (`validateTag`).
418
+ - **Thu gọn tag hiển thị (`+N`)**: Giữ giao diện gọn gàng với `maxTagCount` (ví dụ hiển thị 3 tag đầu, tag còn lại gom thành badge `+5`).
419
+ - **Tích hợp Form HTML chuẩn**: Khi có prop `name="tags"`, tự động render các thẻ hidden `<input type="hidden" name="tags[]" value="..." />` tương thích hoàn hảo với `FormData` và backend server.
420
+ - **Tùy biến giao diện toàn diện**: Hỗ trợ `leftIcon`, `leftAddon`, `rightAddon`, nút thêm nhanh `showAddButton` (`+`), và tùy biến từng tag qua `renderTag`.
421
+
422
+ ### 📖 Ví dụ sử dụng `MultiInput`
423
+
424
+ #### 1. Sử dụng cơ bản với phím Enter & Dấu phẩy
425
+ ```tsx
426
+ import { MultiInput } from "@owa/ui";
427
+
428
+ export function BasicTagsExample() {
429
+ return (
430
+ <MultiInput
431
+ label="Kỹ năng chuyên môn"
432
+ placeholder="Nhập kỹ năng và bấm Enter..."
433
+ delimiters={["Enter", ","]}
434
+ defaultValue={["React", "TypeScript"]}
435
+ onChange={(tags) => console.log("Danh sách tags:", tags)}
436
+ />
437
+ );
438
+ }
439
+ ```
440
+
441
+ #### 2. Nút Thêm nhanh & Giới hạn số lượng
442
+ ```tsx
443
+ <MultiInput
444
+ label="Từ khóa tìm kiếm"
445
+ showAddButton={true}
446
+ maxTags={5}
447
+ onMaxTagsReached={(tag) => alert(`Đã đạt tối đa 5 tags!`)}
448
+ placeholder="Nhập từ khóa..."
449
+ />
450
+ ```
451
+
452
+ #### 3. Thu gọn hiển thị (`maxTagCount`) & Kiểm tra trùng lặp
453
+ ```tsx
454
+ <MultiInput
455
+ label="Danh mục sản phẩm"
456
+ maxTagCount={3}
457
+ allowDuplicates={false}
458
+ onDuplicate={(tag) => alert(`Tag "${tag}" đã tồn tại!`)}
459
+ defaultValue={["Điện tử", "Gia dụng", "Thời trang", "Mỹ phẩm", "Sách"]}
460
+ />
461
+ ```
462
+
463
+ #### 4. Cấu hình trạng thái qua `config` (`MultiInputConfig`)
464
+ ```tsx
465
+ <MultiInput
466
+ name="user_skills"
467
+ label="Kỹ năng bắt buộc"
468
+ config={{
469
+ isRequired: true,
470
+ isClearable: true,
471
+ isFullWidth: true,
472
+ isLoading: isSubmitting,
473
+ showSpinner: true,
474
+ }}
475
+ />
476
+ ```
477
+
478
+ ---
479
+
480
+ ### 🛠 Bảng thông số Props (`MultiInputProps`)
481
+
482
+ | Tên Prop | Kiểu dữ liệu | Giá trị mặc định | Mô tả |
483
+ | :--- | :--- | :---: | :--- |
484
+ | `value` | `string[]` | — | Mảng danh sách tags (Controlled mode). |
485
+ | `defaultValue` | `string[]` | `[]` | Danh sách tags mặc định ban đầu (Uncontrolled mode). |
486
+ | `onChange` | `(values: string[]) => void` | — | Callback kích hoạt mỗi khi danh sách tags thay đổi. |
487
+ | `inputValue` | `string` | — | Giá trị chuỗi text đang gõ dở trong ô input (Controlled). |
488
+ | `onInputValueChange` | `(value: string) => void` | — | Callback khi text đang gõ dở thay đổi. |
489
+ | `delimiters` | `string[]` | `['Enter']` | Mảng các phím kích hoạt tạo tag (`'Enter'`, `','`, `'Tab'`, `'Space'`). |
490
+ | `showAddButton` | `boolean` | `false` | Hiển thị nút (+) ở cuối ô input để click tạo tag. |
491
+ | `renderAddButton` | `(props: { onAdd: () => void; disabled?: boolean }) => ReactNode` | — | Tùy biến render nút (+) thêm tag. |
492
+ | `addOnBlur` | `boolean` | `false` | Tự động tạo tag từ chuỗi đang gõ dở khi blur ra ngoài. |
493
+ | `addOnPaste` | `boolean` | `true` | Tự động phân tách chuỗi khi Paste vào ô input. |
494
+ | `pasteSplitRegex` | `RegExp` | `/[\r\n,;\t]+/` | Regex dùng để phân tách chuỗi khi Paste. |
495
+ | `trimValues` | `boolean` | `true` | Tự động cắt bỏ khoảng trắng thừa đầu và cuối mỗi tag. |
496
+ | `allowDuplicates` | `boolean` | `false` | Cho phép nhập các tag trùng lặp hay không. |
497
+ | `onDuplicate` | `(value: string) => void` | — | Callback khi người dùng cố gắng thêm tag đã tồn tại. |
498
+ | `maxTags` | `number` | — | Số lượng tag tối đa cho phép thêm vào danh sách. |
499
+ | `onMaxTagsReached` | `(value: string) => void` | — | Callback kích hoạt khi đạt giới hạn `maxTags`. |
500
+ | `maxTagLength` | `number` | — | Giới hạn số ký tự tối đa của 1 tag. |
501
+ | `validateTag` | `(tag: string) => boolean \| string` | — | Hàm kiểm tra hợp lệ của tag (trả về `false` hoặc `string` báo lỗi). |
502
+ | `onValidateError` | `(tag: string, error?: string) => void` | — | Callback khi tag không vượt qua kiểm tra validate. |
503
+ | `maxTagCount` | `number` | — | Số lượng tag tối đa hiển thị trước khi thu gọn thành `+N`. |
504
+ | `renderTag` | `(props: TagRenderProps) => ReactNode` | — | Tùy biến hiển thị badge tag hoàn toàn. |
505
+ | `tagVariant` | `BadgeVariant` | `'soft'` | Biến thể hiển thị giao diện của Badge tag. |
506
+ | `tagColor` | `BadgeColor` | `'primary'` | Chủ đề màu sắc của Badge tag. |
507
+ | `tagRadius` | `BadgeRadius` | — | Độ bo góc của Badge tag (tự đồng bộ theo Input nếu không truyền). |
508
+ | `tagSize` | `BadgeSize` | — | Kích thước của Badge tag (tự đồng bộ theo `size` nếu không truyền). |
509
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích cỡ tổng thể của MultiInput. |
510
+ | `variant` | `'outline' \| 'filled' \| 'ghost' \| 'other'` | `'outline'` | Biến thể hiển thị giao diện khung input. |
511
+ | `color` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | `'primary'` | Chủ đề màu sắc viền và viền focus. |
512
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | — | Độ bo góc của khung input. |
513
+ | `label` | `ReactNode` | — | Nhãn tiêu đề của MultiInput. |
514
+ | `labelPlacement` | `'floating' \| 'top' \| 'left'` | `'floating'` | Vị trí đặt nhãn. |
515
+ | `config` | `MultiInputConfig` | — | Cấu hình tập trung các cờ trạng thái (`isRequired`, `isInvalid`, `isLoading`, `showSpinner`, `isClearable`, `isFullWidth`). |
516
+ | `id` | `string` | — | ID tùy biến của thẻ input. |
517
+ | `name` | `string` | — | Tên trường form (tự động render danh sách hidden input `name[]` chứa các tag). |
518
+ | `placeholder` | `string` | `'Type and press Enter...'` | Placeholder khi danh sách tag rỗng. |
519
+ | `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ tương tác. |
520
+ | `readOnly` | `boolean` | `false` | Chế độ chỉ đọc, không cho thêm/xóa tag. |
521
+ | `autoFocus` | `boolean` | `false` | Tự động focus vào ô input khi mount. |
522
+ | `leftIcon` | `ReactNode` | — | Icon hiển thị ở đầu khung input. |
523
+ | `leftAddon` | `ReactNode` | — | Khung addon cố định ở bên trái. |
524
+ | `rightIcon` | `ReactNode` | — | Icon hiển thị ở cuối khung input. |
525
+ | `rightAddon` | `ReactNode` | — | Khung addon cố định ở bên phải. |
526
+ | `helperText` | `ReactNode` | — | Đoạn văn bản hướng dẫn bên dưới. |
527
+ | `errorMessage` | `ReactNode` | — | Thông báo lỗi hiển thị bên dưới. |
528
+ | `onClear` | `() => void` | — | Callback khi nhấn nút xóa toàn bộ tags. |
529
+ | `ref` | `Ref<HTMLInputElement>` | — | Ref chuyển tiếp đến thẻ `<input>` gõ text bên trong. |
530
+
531
+ ---
532
+
533
+ ### ⚙️ Cấu hình `MultiInputConfig`
534
+
535
+ | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
536
+ | :--- | :--- | :---: | :--- |
537
+ | `isRequired` | `boolean` | `false` | Hiển thị dấu `*` đỏ và đánh dấu `aria-required="true"`. |
538
+ | `isInvalid` | `boolean` | `false` | Bật viền đỏ báo lỗi và `aria-invalid="true"`. |
539
+ | `isLoading` | `boolean` | `false` | Vô hiệu hóa tương tác và bật `aria-busy="true"`. |
540
+ | `showSpinner` | `boolean` | `false` | Hiển thị biểu tượng xoay spinner khi `isLoading = true`. |
541
+ | `isClearable` | `boolean` | `false` | Hiển thị nút xóa nhanh toàn bộ tags khi có ít nhất 1 tag hoặc text. |
542
+ | `isFullWidth` | `boolean` | `false` | Mở rộng chiếm toàn bộ 100% chiều ngang container cha. |
543
+
544
+ ---
545
+
546
+ ## 🛠️ Các hàm tiện ích (Utility Functions)
547
+
548
+ Gói thư viện xuất khẩu các hàm helper tiện ích chuyên dụng cho việc xử lý số, OTP, và phân tách tags:
549
+
550
+ | Tên hàm | Tham số | Giá trị trả về | Mô tả |
551
+ | :--- | :--- | :--- | :--- |
552
+ | `splitTagsFromText(text, splitRegex?, trim?)` | `text: string, splitRegex?: RegExp, trim?: boolean` | `string[]` | Phân tách chuỗi văn bản (khi gõ hoặc Paste) thành danh sách các tag độc lập theo regex phân cách và cắt bỏ khoảng trắng thừa. |
553
+ | `isValidOtpChar(char, type)` | `char: string, type: OtpInputType` | `boolean` | Kiểm tra xem 1 ký tự nhập vào có hợp lệ theo kiểu OTP chỉ định (`numeric`, `alphanumeric`, `alpha`) hay không. |
554
+ | `sanitizeOtpString(text, type)` | `text: string, type: OtpInputType` | `string` | Làm sạch toàn bộ chuỗi text, chỉ giữ lại các ký tự hợp lệ theo kiểu OTP chỉ định. |
555
+ | `formatNumberString(...)` | `value: string, options?: NumberFormatOptions` | `string` | Định dạng chuỗi số với dấu phân cách hàng nghìn và phần thập phân. |
556
+ | `parseRawNumberString(value)` | `value: string` | `string` | Chuyển đổi chuỗi số hiển thị về dạng số thô tiêu chuẩn. |
557
+
558
+
559
+