@openway/ui 1.0.2 → 1.0.3

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.
@@ -0,0 +1,424 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --color-primary-50: #f4f8fb;
5
+ --color-primary-100: #e9f0f5;
6
+ --color-primary-200: #cedfe9;
7
+ --color-primary-300: #a2c4d7;
8
+ --color-primary-400: #6ba1bd;
9
+ --color-primary-500: #4e8aa9;
10
+ --color-primary-600: #3c6f8d;
11
+ --color-primary-700: #325972;
12
+ --color-primary-800: #2c4c60;
13
+ --color-primary-900: #294151;
14
+ --color-primary-950: #1b2a36;
15
+
16
+ --color-secondary-50: #faf6ec;
17
+ --color-secondary-100: #f4e8cd;
18
+ --color-secondary-200: #ead09e;
19
+ --color-secondary-300: #deb066;
20
+ --color-secondary-400: #d4943e;
21
+ --color-secondary-500: #c47e2e;
22
+ --color-secondary-600: #a96125;
23
+ --color-secondary-700: #874621;
24
+ --color-secondary-800: #713a22;
25
+ --color-secondary-900: #613222;
26
+ --color-secondary-950: #381810;
27
+
28
+ --color-neutral-white: #fcfdfd;
29
+ --color-neutral-50: #f9fbfb;
30
+ --color-neutral-100: #f3f5f6;
31
+ --color-neutral-200: #e5e9eb;
32
+ --color-neutral-300: #d2d8da;
33
+ --color-neutral-400: #9daaae;
34
+ --color-neutral-500: #7d8b90;
35
+ --color-neutral-600: #4c5e62;
36
+ --color-neutral-700: #394a4f;
37
+ --color-neutral-800: #203236;
38
+ --color-neutral-900: #122026;
39
+ --color-neutral-950: #040d11;
40
+ --color-neutral-black: #020709;
41
+
42
+ --color-error-50: #fef2f2;
43
+ --color-error-100: #fee2e2;
44
+ --color-error-200: #fecaca;
45
+ --color-error-300: #fca5a5;
46
+ --color-error-400: #f87171;
47
+ --color-error-500: #ef4444;
48
+ --color-error-600: #dc2626;
49
+ --color-error-700: #b91c1c;
50
+ --color-error-800: #991b1b;
51
+ --color-error-900: #7f1d1d;
52
+ --color-error-950: #450a0a;
53
+
54
+ --color-success-50: #ecfdf6;
55
+ --color-success-100: #d2f9e6;
56
+ --color-success-200: #a9f1d2;
57
+ --color-success-300: #71e4ba;
58
+ --color-success-400: #38cf9d;
59
+ --color-success-500: #12a176;
60
+ --color-success-600: #09926c;
61
+ --color-success-700: #077559;
62
+ --color-success-800: #085d48;
63
+ --color-success-900: #084c3c;
64
+ --color-success-950: #032b23;
65
+
66
+ --color-warning-50: #fefce8;
67
+ --color-warning-100: #fef9c3;
68
+ --color-warning-200: #fef08a;
69
+ --color-warning-300: #fde047;
70
+ --color-warning-400: #facc15;
71
+ --color-warning-500: #eab308;
72
+ --color-warning-600: #ca8a04;
73
+ --color-warning-700: #a16207;
74
+ --color-warning-800: #854d0e;
75
+ --color-warning-900: #713f12;
76
+ --color-warning-950: #422006;
77
+
78
+ --color-info-50: #f0fafb;
79
+ --color-info-100: #d9f1f4;
80
+ --color-info-200: #b7e3ea;
81
+ --color-info-300: #85cedb;
82
+ --color-info-400: #4cafc4;
83
+ --color-info-500: #349eb6;
84
+ --color-info-600: #2b788f;
85
+ --color-info-700: #296275;
86
+ --color-info-800: #295161;
87
+ --color-info-900: #264553;
88
+ --color-info-950: #142c38;
89
+ }
90
+
91
+ @keyframes errorSlideIn {
92
+ 0% {
93
+ opacity: 0;
94
+ transform: translateY(-4px);
95
+ }
96
+ 100% {
97
+ opacity: 1;
98
+ transform: translateY(0);
99
+ }
100
+ }
101
+
102
+ @keyframes errorSlideOut {
103
+ 0% {
104
+ opacity: 1;
105
+ transform: translateY(0);
106
+ }
107
+ 100% {
108
+ opacity: 0;
109
+ transform: translateY(-4px);
110
+ }
111
+ }
112
+
113
+ .animate-error-in {
114
+ animation: errorSlideIn 0.35s cubic-bezier(0.25, 1, 0.5, 1) forwards;
115
+ }
116
+
117
+ .animate-error-out {
118
+ animation: errorSlideOut 0.25s cubic-bezier(0.7, 0, 0.84, 0) forwards;
119
+ }
120
+
121
+ @keyframes alertSlideInDown {
122
+ 0% {
123
+ opacity: 0;
124
+ transform: translateY(-16px) scale(0.96);
125
+ }
126
+ 100% {
127
+ opacity: 1;
128
+ transform: translateY(0) scale(1);
129
+ }
130
+ }
131
+
132
+ @keyframes alertSlideInUp {
133
+ 0% {
134
+ opacity: 0;
135
+ transform: translateY(16px) scale(0.96);
136
+ }
137
+ 100% {
138
+ opacity: 1;
139
+ transform: translateY(0) scale(1);
140
+ }
141
+ }
142
+
143
+ @keyframes alertSlideOut {
144
+ 0% {
145
+ opacity: 1;
146
+ transform: scale(1);
147
+ }
148
+ 100% {
149
+ opacity: 0;
150
+ transform: scale(0.95);
151
+ }
152
+ }
153
+
154
+ @keyframes alertSlideOutTop {
155
+ 0% {
156
+ opacity: 1;
157
+ transform: translateY(0) scale(1);
158
+ max-height: 200px;
159
+ margin-bottom: 8px;
160
+ }
161
+ 100% {
162
+ opacity: 0;
163
+ transform: translateY(-16px) scale(0.96);
164
+ max-height: 0;
165
+ margin-bottom: 0;
166
+ padding-top: 0;
167
+ padding-bottom: 0;
168
+ }
169
+ }
170
+
171
+ @keyframes alertSlideOutBottom {
172
+ 0% {
173
+ opacity: 1;
174
+ transform: translateY(0) scale(1);
175
+ max-height: 200px;
176
+ margin-bottom: 8px;
177
+ }
178
+ 100% {
179
+ opacity: 0;
180
+ transform: translateY(16px) scale(0.96);
181
+ max-height: 0;
182
+ margin-bottom: 0;
183
+ padding-top: 0;
184
+ padding-bottom: 0;
185
+ }
186
+ }
187
+
188
+ .animate-alert-in-top {
189
+ animation: alertSlideInDown 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
190
+ }
191
+
192
+ .animate-alert-in-bottom {
193
+ animation: alertSlideInUp 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
194
+ }
195
+
196
+ .animate-alert-out-top {
197
+ animation: alertSlideOutTop 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
198
+ }
199
+
200
+ .animate-alert-out-bottom {
201
+ animation: alertSlideOutBottom 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
202
+ }
203
+
204
+ .animate-alert-out {
205
+ animation: alertSlideOutTop 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
206
+ }
207
+
208
+ /* Confirm Dialog Animations */
209
+ @keyframes confirmBackdropFadeIn {
210
+ 0% {
211
+ opacity: 0;
212
+ }
213
+ 100% {
214
+ opacity: 1;
215
+ }
216
+ }
217
+
218
+ @keyframes confirmBackdropFadeOut {
219
+ 0% {
220
+ opacity: 1;
221
+ }
222
+ 100% {
223
+ opacity: 0;
224
+ }
225
+ }
226
+
227
+ @keyframes confirmZoomIn {
228
+ 0% {
229
+ opacity: 0;
230
+ transform: scale(0.94) translateY(6px);
231
+ }
232
+ 100% {
233
+ opacity: 1;
234
+ transform: scale(1) translateY(0);
235
+ }
236
+ }
237
+
238
+ @keyframes confirmZoomOut {
239
+ 0% {
240
+ opacity: 1;
241
+ transform: scale(1) translateY(0);
242
+ }
243
+ 100% {
244
+ opacity: 0;
245
+ transform: scale(0.94) translateY(6px);
246
+ }
247
+ }
248
+
249
+ .animate-confirm-backdrop-in {
250
+ animation: confirmBackdropFadeIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) forwards;
251
+ }
252
+
253
+ .animate-confirm-backdrop-out {
254
+ animation: confirmBackdropFadeOut 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
255
+ }
256
+
257
+ .animate-confirm-in {
258
+ animation: confirmZoomIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) forwards;
259
+ }
260
+
261
+ .animate-confirm-out {
262
+ animation: confirmZoomOut 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
263
+ }
264
+
265
+ /* Modal Dialog Animations */
266
+ @keyframes modalBackdropFadeIn {
267
+ 0% {
268
+ opacity: 0;
269
+ }
270
+ 100% {
271
+ opacity: 1;
272
+ }
273
+ }
274
+
275
+ @keyframes modalBackdropFadeOut {
276
+ 0% {
277
+ opacity: 1;
278
+ }
279
+ 100% {
280
+ opacity: 0;
281
+ }
282
+ }
283
+
284
+ @keyframes modalZoomIn {
285
+ 0% {
286
+ opacity: 0;
287
+ transform: scale(0.95) translateY(6px);
288
+ }
289
+ 100% {
290
+ opacity: 1;
291
+ transform: scale(1) translateY(0);
292
+ }
293
+ }
294
+
295
+ @keyframes modalZoomOut {
296
+ 0% {
297
+ opacity: 1;
298
+ transform: scale(1) translateY(0);
299
+ }
300
+ 100% {
301
+ opacity: 0;
302
+ transform: scale(0.95) translateY(6px);
303
+ }
304
+ }
305
+
306
+ .animate-modal-backdrop-in {
307
+ animation: modalBackdropFadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
308
+ }
309
+
310
+ .animate-modal-backdrop-out {
311
+ animation: modalBackdropFadeOut 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
312
+ }
313
+
314
+ .animate-modal-in {
315
+ animation: modalZoomIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
316
+ }
317
+
318
+ .animate-modal-out {
319
+ animation: modalZoomOut 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
320
+ }
321
+
322
+ /* Reset native HTML dialog backdrop to prevent browser grey flash */
323
+ dialog::backdrop {
324
+ background: transparent;
325
+ }
326
+
327
+ /* Custom Thin Modern Scrollbars for UI components */
328
+ .ui-scrollbar {
329
+ scrollbar-width: thin;
330
+ scrollbar-color: rgba(156, 163, 175, 0.4) transparent;
331
+ }
332
+
333
+ .ui-scrollbar::-webkit-scrollbar {
334
+ width: 5px;
335
+ height: 5px;
336
+ }
337
+
338
+ .ui-scrollbar::-webkit-scrollbar-track {
339
+ background: transparent;
340
+ }
341
+
342
+ .ui-scrollbar::-webkit-scrollbar-thumb {
343
+ background-color: rgba(156, 163, 175, 0.4);
344
+ border-radius: 9999px;
345
+ }
346
+
347
+ .ui-scrollbar::-webkit-scrollbar-thumb:hover {
348
+ background-color: rgba(107, 114, 128, 0.7);
349
+ }
350
+
351
+ /* ==================== Skeleton Wave Animation ==================== */
352
+ @keyframes skeleton-shimmer {
353
+ 0% {
354
+ background-position: -200% -200%;
355
+ }
356
+ 100% {
357
+ background-position: 200% 200%;
358
+ }
359
+ }
360
+
361
+ .skeleton-wave {
362
+ background: linear-gradient(
363
+ 135deg,
364
+ var(--color-neutral-200) 25%,
365
+ var(--color-neutral-100) 50%,
366
+ var(--color-neutral-200) 75%
367
+ );
368
+ background-size: 200% 200%;
369
+ animation: skeleton-shimmer 2.2s ease-in-out infinite;
370
+ }
371
+
372
+ /* ==================== Table Expanding Animations ==================== */
373
+ @keyframes tableRowExpand {
374
+ 0% {
375
+ grid-template-rows: 0fr;
376
+ opacity: 0;
377
+ }
378
+ 100% {
379
+ grid-template-rows: 1fr;
380
+ opacity: 1;
381
+ }
382
+ }
383
+
384
+ @keyframes tableRowContentSlideDown {
385
+ 0% {
386
+ opacity: 0;
387
+ transform: translateY(-8px);
388
+ }
389
+ 100% {
390
+ opacity: 1;
391
+ transform: translateY(0);
392
+ }
393
+ }
394
+
395
+ .animate-table-expand-wrapper {
396
+ display: grid;
397
+ grid-template-rows: 1fr;
398
+ animation: tableRowExpand 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
399
+ }
400
+
401
+ .animate-table-expand-content {
402
+ overflow: hidden;
403
+ animation: tableRowContentSlideDown 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
404
+ }
405
+
406
+ @keyframes tableSubrowFadeIn {
407
+ 0% {
408
+ opacity: 0;
409
+ transform: translateY(-4px);
410
+ background-color: var(--color-primary-50);
411
+ }
412
+ 50% {
413
+ opacity: 1;
414
+ background-color: var(--color-primary-50);
415
+ }
416
+ 100% {
417
+ transform: translateY(0);
418
+ background-color: transparent;
419
+ }
420
+ }
421
+
422
+ .animate-table-subrow-in {
423
+ animation: tableSubrowFadeIn 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
424
+ }
@@ -1,5 +1,5 @@
1
1
  import { HTMLAttributes, Ref, ReactNode, RefObject } from 'react';
2
- import { ToastT, ToasterProps as ToasterProps$1 } from 'sonner';
2
+ import { ToasterProps as ToasterProps$1, ToastT } from 'sonner';
3
3
  import { Placement } from '@floating-ui/react';
4
4
 
5
5
  /**
@@ -394,9 +394,43 @@ interface SelectFilterField<TValue = unknown> {
394
394
  onChange: (val: TValue) => void;
395
395
  }) => ReactNode;
396
396
  }
397
+ interface SelectConfig {
398
+ /**
399
+ * Đánh dấu trường bắt buộc nhập (hiển thị dấu * đỏ cạnh label)
400
+ * @default false
401
+ */
402
+ isRequired?: boolean;
403
+ /**
404
+ * Trạng thái báo lỗi (viền đỏ)
405
+ * @default false
406
+ */
407
+ isInvalid?: boolean;
408
+ /**
409
+ * Trạng thái đang tải dữ liệu
410
+ * @default false
411
+ */
412
+ isLoading?: boolean;
413
+ /**
414
+ * Hiển thị biểu tượng xoay spinner khi đang ở trạng thái loading
415
+ * @default false
416
+ */
417
+ showSpinner?: boolean;
418
+ /**
419
+ * Hiển thị nút xóa nhanh nội dung khi select có giá trị
420
+ * @default false
421
+ */
422
+ isClearable?: boolean;
423
+ /**
424
+ * Mở rộng chiều rộng 100% của container chứa
425
+ * @default true
426
+ */
427
+ isFullWidth?: boolean;
428
+ }
397
429
  interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unknown> = Record<string, unknown>> {
398
430
  /** Ref chuyển tiếp tới root wrapper element */
399
431
  ref?: Ref<HTMLDivElement>;
432
+ /** Cấu hình tập trung các cờ trạng thái / tính năng */
433
+ config?: SelectConfig;
400
434
  /** Kích thước: xs, sm, md, lg, xl. Mặc định 'md' */
401
435
  size?: SelectSize;
402
436
  /** Biến thể giao diện: outline, filled, ghost, other. Mặc định 'outline' */
@@ -411,7 +445,7 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
411
445
  id?: string;
412
446
  /** Nhãn tiêu đề của Select */
413
447
  label?: ReactNode;
414
- /** Vị trí nhãn: top, left, floating. Mặc định 'top' */
448
+ /** Vị trí nhãn: top, left, floating. Mặc định 'floating' */
415
449
  labelPlacement?: LabelPlacement;
416
450
  /** ClassName tùy biến cho nhãn */
417
451
  labelClassName?: string;
@@ -421,10 +455,6 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
421
455
  helperText?: ReactNode;
422
456
  /** Thông báo lỗi khi không hợp lệ */
423
457
  errorMessage?: ReactNode;
424
- /** Trạng thái không hợp lệ */
425
- isInvalid?: boolean;
426
- /** Bắt buộc chọn */
427
- isRequired?: boolean;
428
458
  /** Vô hiệu hóa Select */
429
459
  isDisabled?: boolean;
430
460
  /** Chỉ đọc, không cho phép thay đổi */
@@ -471,16 +501,14 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
471
501
  debounceMs?: number;
472
502
  /** Hàm custom predicate lọc options ở chế độ client */
473
503
  filterFn?: (option: SelectOptionItem<TData>, query: string, filters: TFilters) => boolean;
474
- /** Trạng thái đang tải dữ liệu */
475
- isLoading?: boolean;
476
504
  /** Văn bản hiển thị khi không có dữ liệu */
477
505
  emptyText?: ReactNode;
478
506
  /** Props tùy biến chuyển tiếp đến component Empty */
479
507
  emptyProps?: Partial<EmptyProps>;
480
- /** Hiển thị nút xoá nhanh toàn bộ giá trị đã chọn */
481
- clearable?: boolean;
482
508
  /** Render menu qua Portal để tránh bị che bởi overflow hidden. Mặc định true */
483
509
  portal?: boolean;
510
+ /** Container để gắn portal menu vào (mặc định document.body, tự động nhận diện `<dialog>` nếu Select nằm trong Modal/Confirm) */
511
+ portalRoot?: HTMLElement | null | React.RefObject<HTMLElement | null>;
484
512
  /** Vị trí hiển thị của popup dropdown. Mặc định 'bottom-start' */
485
513
  placement?: Placement;
486
514
  /** Chiều cao tối đa của vùng cuộn menu. Mặc định 280px */
@@ -504,6 +532,8 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
504
532
  triggerClassName?: string;
505
533
  /** ClassName cho popup dropdown menu */
506
534
  menuClassName?: string;
535
+ /** ClassName cho phần thông báo helper/error text */
536
+ helperClassName?: string;
507
537
  }
508
538
  /** Props cho component Select (Single Select) */
509
539
  interface SelectProps<TData = unknown, TFilters extends Record<string, unknown> = Record<string, unknown>> extends BaseSelectProps<TData, TFilters> {
@@ -1,5 +1,5 @@
1
1
  import { HTMLAttributes, Ref, ReactNode, RefObject } from 'react';
2
- import { ToastT, ToasterProps as ToasterProps$1 } from 'sonner';
2
+ import { ToasterProps as ToasterProps$1, ToastT } from 'sonner';
3
3
  import { Placement } from '@floating-ui/react';
4
4
 
5
5
  /**
@@ -394,9 +394,43 @@ interface SelectFilterField<TValue = unknown> {
394
394
  onChange: (val: TValue) => void;
395
395
  }) => ReactNode;
396
396
  }
397
+ interface SelectConfig {
398
+ /**
399
+ * Đánh dấu trường bắt buộc nhập (hiển thị dấu * đỏ cạnh label)
400
+ * @default false
401
+ */
402
+ isRequired?: boolean;
403
+ /**
404
+ * Trạng thái báo lỗi (viền đỏ)
405
+ * @default false
406
+ */
407
+ isInvalid?: boolean;
408
+ /**
409
+ * Trạng thái đang tải dữ liệu
410
+ * @default false
411
+ */
412
+ isLoading?: boolean;
413
+ /**
414
+ * Hiển thị biểu tượng xoay spinner khi đang ở trạng thái loading
415
+ * @default false
416
+ */
417
+ showSpinner?: boolean;
418
+ /**
419
+ * Hiển thị nút xóa nhanh nội dung khi select có giá trị
420
+ * @default false
421
+ */
422
+ isClearable?: boolean;
423
+ /**
424
+ * Mở rộng chiều rộng 100% của container chứa
425
+ * @default true
426
+ */
427
+ isFullWidth?: boolean;
428
+ }
397
429
  interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unknown> = Record<string, unknown>> {
398
430
  /** Ref chuyển tiếp tới root wrapper element */
399
431
  ref?: Ref<HTMLDivElement>;
432
+ /** Cấu hình tập trung các cờ trạng thái / tính năng */
433
+ config?: SelectConfig;
400
434
  /** Kích thước: xs, sm, md, lg, xl. Mặc định 'md' */
401
435
  size?: SelectSize;
402
436
  /** Biến thể giao diện: outline, filled, ghost, other. Mặc định 'outline' */
@@ -411,7 +445,7 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
411
445
  id?: string;
412
446
  /** Nhãn tiêu đề của Select */
413
447
  label?: ReactNode;
414
- /** Vị trí nhãn: top, left, floating. Mặc định 'top' */
448
+ /** Vị trí nhãn: top, left, floating. Mặc định 'floating' */
415
449
  labelPlacement?: LabelPlacement;
416
450
  /** ClassName tùy biến cho nhãn */
417
451
  labelClassName?: string;
@@ -421,10 +455,6 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
421
455
  helperText?: ReactNode;
422
456
  /** Thông báo lỗi khi không hợp lệ */
423
457
  errorMessage?: ReactNode;
424
- /** Trạng thái không hợp lệ */
425
- isInvalid?: boolean;
426
- /** Bắt buộc chọn */
427
- isRequired?: boolean;
428
458
  /** Vô hiệu hóa Select */
429
459
  isDisabled?: boolean;
430
460
  /** Chỉ đọc, không cho phép thay đổi */
@@ -471,16 +501,14 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
471
501
  debounceMs?: number;
472
502
  /** Hàm custom predicate lọc options ở chế độ client */
473
503
  filterFn?: (option: SelectOptionItem<TData>, query: string, filters: TFilters) => boolean;
474
- /** Trạng thái đang tải dữ liệu */
475
- isLoading?: boolean;
476
504
  /** Văn bản hiển thị khi không có dữ liệu */
477
505
  emptyText?: ReactNode;
478
506
  /** Props tùy biến chuyển tiếp đến component Empty */
479
507
  emptyProps?: Partial<EmptyProps>;
480
- /** Hiển thị nút xoá nhanh toàn bộ giá trị đã chọn */
481
- clearable?: boolean;
482
508
  /** Render menu qua Portal để tránh bị che bởi overflow hidden. Mặc định true */
483
509
  portal?: boolean;
510
+ /** Container để gắn portal menu vào (mặc định document.body, tự động nhận diện `<dialog>` nếu Select nằm trong Modal/Confirm) */
511
+ portalRoot?: HTMLElement | null | React.RefObject<HTMLElement | null>;
484
512
  /** Vị trí hiển thị của popup dropdown. Mặc định 'bottom-start' */
485
513
  placement?: Placement;
486
514
  /** Chiều cao tối đa của vùng cuộn menu. Mặc định 280px */
@@ -504,6 +532,8 @@ interface BaseSelectProps<TData = unknown, TFilters extends Record<string, unkno
504
532
  triggerClassName?: string;
505
533
  /** ClassName cho popup dropdown menu */
506
534
  menuClassName?: string;
535
+ /** ClassName cho phần thông báo helper/error text */
536
+ helperClassName?: string;
507
537
  }
508
538
  /** Props cho component Select (Single Select) */
509
539
  interface SelectProps<TData = unknown, TFilters extends Record<string, unknown> = Record<string, unknown>> extends BaseSelectProps<TData, TFilters> {
package/docs/AGENTS.md CHANGED
@@ -131,8 +131,6 @@ Tất cả tài liệu chi tiết của từng component được lưu trữ t
131
131
  - `xs` (24px), `sm` (32px), `md` (40px - chuẩn cho form input/nút), `lg` (48px), `xl` (56px).
132
132
  3. **Bo góc (`radius`)**:
133
133
  - `none` (0px), `sm` (4px), `md` (6px), `lg` (8px - chuẩn mặc định), `xl` (12px), `full` (tròn).
134
- 4. **Hỗ trợ chế độ Tối (Dark Mode)**:
135
- - Tất cả component tự động thích ứng qua class Tailwind `dark:`.
136
134
 
137
135
  ---
138
136
 
package/docs/README.md CHANGED
@@ -147,4 +147,3 @@ Tài liệu chi tiết của các hooks được lưu trữ tại thư mục [`h
147
147
  1. **Zero `any`**: 100% các component và hook đều được khai báo TypeScript chặt chẽ, type-safe.
148
148
  2. **5 Kích thước Chuẩn (`size`)**: `xs` (24px), `sm` (32px), `md` (40px - chuẩn form), `lg` (48px), `xl` (56px).
149
149
  3. **Độ Bo Góc Chuẩn (`radius`)**: `none` (0px), `sm` (4px), `md` (6px), `lg` (8px - chuẩn mặc định), `xl` (12px), `full` (tròn).
150
- 4. **Dark Mode**: Toàn bộ component hỗ trợ tự động thích ứng qua selector `dark:`.
@@ -1,4 +1,4 @@
1
- # 🎠 Carousel Component (`@owa/ui`)
1
+ # 🎠 Carousel Component (`@owa/ui`)
2
2
 
3
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
4
 
@@ -12,7 +12,7 @@ Component **Carousel** (băng chuyền / slider) hiện đại, hiệu năng cao
12
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
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
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.
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`.
16
16
  - **Định vị mặc định thông minh**:
17
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
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`).
@@ -151,9 +151,9 @@ export function BasicCarousel() {
151
151
  <CarouselContent>
152
152
  {products.map((product) => (
153
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">
154
+ <div className="p-4 border border-neutral-200 rounded-xl bg-white shadow-sm">
155
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>
156
+ <h3 className="mt-2 font-semibold text-neutral-900">{product.name}</h3>
157
157
  <p className="text-primary-600 font-bold">{product.price}</p>
158
158
  </div>
159
159
  </CarouselSlide>
@@ -205,7 +205,7 @@ Hỗ trợ 4 biến thể giao diện: `glass` (*mặc định*), `filled`, `out
205
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
206
 
207
207
  ```tsx
208
- <Carousel defaultIndex={1} className="w-full max-w-md border border-neutral-200 dark:border-neutral-700 p-2 rounded-xl">
208
+ <Carousel defaultIndex={1} className="w-full max-w-md border border-neutral-200 p-2 rounded-xl">
209
209
  <CarouselContent>
210
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
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>
@@ -162,19 +162,13 @@ export function BasicInputExample() {
162
162
  | `label` | `ReactNode` | — | Nhãn tiêu đề hiển thị cho ô nhập liệu. |
163
163
  | `labelPlacement` | `'floating' \| 'top' \| 'left'` | `'floating'` | Vị trí hiển thị của nhãn. |
164
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
165
  | `helperText` | `ReactNode` | — | Đoạn văn bản hướng dẫn/trợ giúp bên dưới ô. |
167
166
  | `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
167
  | `leftIcon` | `ReactNode` | — | Icon hiển thị ở đầu ô input. |
172
168
  | `rightIcon` | `ReactNode` | — | Icon hiển thị ở cuối ô input. |
173
169
  | `leftAddon` | `ReactNode` | — | Addon / prefix cố định ở đầu ô input (vd: `'https://'`). |
174
170
  | `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
171
  | `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
172
  | `ref` | `Ref<HTMLInputElement>` | — | Ref chuyển tiếp đến thẻ `<input>` HTML bên dưới. |
179
173
 
180
174
  ---