@goplusvn/core 0.1.19 → 0.1.21

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.19",
4
+ "version": "0.1.21",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -480,7 +480,7 @@ export function DataTable<TData extends Record<string, unknown>>({
480
480
 
481
481
  {/* Pagination Section */}
482
482
  {pagination && (
483
- <div className="border-t border-border dark:border-slate-800 bg-card px-4 sm:px-6 py-2 shrink-0">
483
+ <div className="border-t border-border dark:border-slate-800 bg-card px-2 sm:px-4 py-1 shrink-0">
484
484
  <MemoizedPagination
485
485
  table={table}
486
486
  currentPage={pagination.page}
@@ -65,21 +65,21 @@ export function DataTablePagination<TData>({
65
65
  };
66
66
 
67
67
  return (
68
- <div className="flex flex-col sm:flex-row items-center justify-between gap-1 sm:gap-2 py-0.5 px-1 sm:px-2">
69
- <div className="hidden sm:block flex-1 text-xs text-left text-muted-foreground w-full">
68
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-1 px-1 sm:px-2">
69
+ <div className="hidden sm:block flex-1 text-[11px] text-left text-muted-foreground w-full">
70
70
  Hiển thị {startItem} - {endItem} trong tổng số {total} mục
71
71
  </div>
72
72
 
73
- <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto pb-1 sm:pb-0">
74
- <div className="flex items-center gap-1 sm:gap-2 px-1 sm:px-2 border-r pr-1 sm:pr-2">
75
- <p className="hidden sm:block text-xs font-medium whitespace-nowrap">
73
+ <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto">
74
+ <div className="flex items-center gap-1 gap-1 px-1 border-r pr-1 sm:pr-1.5">
75
+ <p className="hidden sm:block text-[11px] font-medium whitespace-nowrap">
76
76
  Số dòng
77
77
  </p>
78
78
  <Select
79
79
  value={`${effPageSize}`}
80
80
  onValueChange={(value) => table.setPageSize(Number(value))}
81
81
  >
82
- <SelectTrigger className="h-6 w-[65px] sm:w-[75px] text-xs focus:ring-inset focus:ring-offset-0">
82
+ <SelectTrigger className="!h-6 !min-h-0 py-0 w-[60px] sm:w-[65px] text-[11px] px-1.5 focus:ring-inset focus:ring-offset-0">
83
83
  <SelectValue placeholder={effPageSize} />
84
84
  </SelectTrigger>
85
85
  <SelectContent side="top">
@@ -113,11 +113,11 @@ export function DataTablePagination<TData>({
113
113
  </Button>
114
114
 
115
115
  <div className="flex items-center gap-1 sm:gap-2 px-1">
116
- <span className="hidden sm:inline text-xs font-medium whitespace-nowrap">
116
+ <span className="hidden sm:inline text-[11px] font-medium whitespace-nowrap">
117
117
  Trang
118
118
  </span>
119
119
  <input
120
- className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-xs text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
120
+ className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-[11px] text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
121
121
  value={inputPage}
122
122
  onChange={(e) => setInputPage(e.target.value)}
123
123
  onBlur={handlePageInputBlur}
@@ -126,7 +126,7 @@ export function DataTablePagination<TData>({
126
126
  }}
127
127
  disabled={total === 0}
128
128
  />
129
- <span className="text-sm font-medium text-muted-foreground whitespace-nowrap">
129
+ <span className="text-[11px] font-medium text-muted-foreground whitespace-nowrap">
130
130
  / {pageCount}
131
131
  </span>
132
132
  </div>
@@ -0,0 +1,178 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
5
+ import {
6
+ Button,
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "../primitives"
13
+ import {
14
+ ChevronLeft,
15
+ ChevronRight,
16
+ ChevronsLeft,
17
+ ChevronsRight,
18
+ } from "lucide-react"
19
+
20
+ interface PaginationProps {
21
+ totalItems: number
22
+ currentPage: number
23
+ pageSize: number
24
+ disabled?: boolean
25
+ }
26
+
27
+ export function UrlPagination({
28
+ totalItems,
29
+ currentPage,
30
+ pageSize,
31
+ disabled,
32
+ }: PaginationProps) {
33
+ const router = useRouter()
34
+ const pathname = usePathname()
35
+ const searchParams = useSearchParams()
36
+
37
+ const pageCount = Math.ceil(totalItems / pageSize)
38
+ const startItem = (currentPage - 1) * pageSize + 1
39
+ const endItem = Math.min(currentPage * pageSize, totalItems)
40
+
41
+ // Create a new URLSearchParams instance to update params
42
+ const createQueryString = React.useCallback(
43
+ (params: Record<string, string | number | null>) => {
44
+ const newSearchParams = new URLSearchParams(searchParams?.toString())
45
+
46
+ for (const [key, value] of Object.entries(params)) {
47
+ if (value === null) {
48
+ newSearchParams.delete(key)
49
+ } else {
50
+ newSearchParams.set(key, String(value))
51
+ }
52
+ }
53
+
54
+ return newSearchParams.toString()
55
+ },
56
+ [searchParams]
57
+ )
58
+
59
+ const handlePageChange = (page: number) => {
60
+ if (page < 1 || page > Math.max(1, pageCount)) return
61
+ router.push(`${pathname}?${createQueryString({ page })}`)
62
+ }
63
+
64
+ const handlePageSizeChange = (value: string) => {
65
+ const newPageSize = Number(value)
66
+ // When changing page size, reset to page 1 to avoid out of bounds
67
+ router.push(
68
+ `${pathname}?${createQueryString({ pageSize: newPageSize, page: 1 })}`
69
+ )
70
+ }
71
+
72
+ const [inputPage, setInputPage] = React.useState(String(currentPage))
73
+
74
+ React.useEffect(() => {
75
+ setInputPage(String(currentPage))
76
+ }, [currentPage])
77
+
78
+ const handlePageInputBlur = () => {
79
+ const pageNum = Number(inputPage)
80
+ if (isNaN(pageNum) || pageNum < 1 || pageNum > pageCount) {
81
+ setInputPage(String(currentPage))
82
+ } else if (pageNum !== currentPage) {
83
+ handlePageChange(pageNum)
84
+ }
85
+ }
86
+
87
+ const handlePageInputKeyDown = (e: React.KeyboardEvent) => {
88
+ if (e.key === "Enter") {
89
+ handlePageInputBlur()
90
+ }
91
+ }
92
+
93
+ return (
94
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-1 sm:gap-2 px-1 sm:px-2">
95
+ {/* Ẩn text "Hiển thị" trên mobile, chỉ hiện trên màn hình sm trở lên */}
96
+ <div className="hidden sm:block flex-1 text-[11px] text-left text-muted-foreground w-full">
97
+ Hiển thị {totalItems > 0 ? startItem : 0} -{" "}
98
+ {totalItems > 0 ? endItem : 0} trong tổng số {totalItems} mục
99
+ </div>
100
+
101
+ <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto">
102
+ <div className="flex items-center gap-1 gap-1 px-1 border-r pr-1 sm:pr-1.5">
103
+ <p className="hidden sm:block text-[11px] font-medium whitespace-nowrap">Số dòng</p>
104
+ <Select
105
+ value={`${pageSize}`}
106
+ onValueChange={handlePageSizeChange}
107
+ disabled={disabled}
108
+ >
109
+ <SelectTrigger className="!h-6 !min-h-0 py-0 w-[60px] sm:w-[65px] text-[11px] px-1.5 focus:ring-inset focus:ring-offset-0">
110
+ <SelectValue placeholder={pageSize} />
111
+ </SelectTrigger>
112
+ <SelectContent side="top">
113
+ {[10, 20, 50, 100, 500, 1000].map((size) => (
114
+ <SelectItem key={size} value={`${size}`}>
115
+ {size}
116
+ </SelectItem>
117
+ ))}
118
+ </SelectContent>
119
+ </Select>
120
+ </div>
121
+ <div className="flex items-center gap-1 pl-1">
122
+ <Button
123
+ variant="outline"
124
+ className="hidden h-6 w-6 p-0 lg:flex"
125
+ onClick={() => handlePageChange(1)}
126
+ disabled={currentPage <= 1 || disabled || totalItems === 0}
127
+ >
128
+ <span className="sr-only">Trang đầu</span>
129
+ <ChevronsLeft className="h-3 w-3 sm:h-3 sm:w-3" />
130
+ </Button>
131
+ <Button
132
+ variant="outline"
133
+ className="h-6 w-6 p-0"
134
+ onClick={() => handlePageChange(currentPage - 1)}
135
+ disabled={currentPage <= 1 || disabled || totalItems === 0}
136
+ >
137
+ <span className="sr-only">Trang trước</span>
138
+ <ChevronLeft className="h-3 w-3 sm:h-3 sm:w-3" />
139
+ </Button>
140
+
141
+ <div className="flex items-center gap-1 sm:gap-2 px-1">
142
+ <span className="hidden sm:inline text-[11px] font-medium whitespace-nowrap">Trang</span>
143
+ <input
144
+ className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-[11px] text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
145
+ value={inputPage}
146
+ onChange={(e) => setInputPage(e.target.value)}
147
+ onBlur={handlePageInputBlur}
148
+ onKeyDown={handlePageInputKeyDown}
149
+ disabled={disabled || totalItems === 0}
150
+ />
151
+ <span className="text-sm font-medium text-muted-foreground whitespace-nowrap">
152
+ / {Math.max(1, pageCount)}
153
+ </span>
154
+ </div>
155
+
156
+ <Button
157
+ variant="outline"
158
+ className="h-6 w-6 p-0"
159
+ onClick={() => handlePageChange(currentPage + 1)}
160
+ disabled={currentPage >= pageCount || disabled || totalItems === 0}
161
+ >
162
+ <span className="sr-only">Trang sau</span>
163
+ <ChevronRight className="h-3 w-3 sm:h-3 sm:w-3" />
164
+ </Button>
165
+ <Button
166
+ variant="outline"
167
+ className="hidden h-6 w-6 p-0 lg:flex"
168
+ onClick={() => handlePageChange(pageCount)}
169
+ disabled={currentPage >= pageCount || disabled || totalItems === 0}
170
+ >
171
+ <span className="sr-only">Trang cuối</span>
172
+ <ChevronsRight className="h-3 w-3 sm:h-3 sm:w-3" />
173
+ </Button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ )
178
+ }
@@ -16,6 +16,8 @@ export interface MultiSelectOption {
16
16
  disabled?: boolean;
17
17
  /** Optional icon component to display alongside the option */
18
18
  icon?: React.ComponentType<{ className?: string }>;
19
+ /** Optional count to display on the right side of the option */
20
+ count?: number;
19
21
  }
20
22
 
21
23
  interface MultiSelectProps {
@@ -191,11 +193,20 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
191
193
  if (!searchValue.trim()) {
192
194
  return options;
193
195
  }
194
- const searchLower = searchValue.toLowerCase();
196
+ const removeAccents = (str: string) => {
197
+ return str ? str.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : '';
198
+ };
199
+
200
+ const searchLower = removeAccents(searchValue.toLowerCase());
201
+
195
202
  return options.filter(
196
- (option) =>
197
- option.label.toLowerCase().includes(searchLower) ||
198
- String(option.value).toLowerCase().includes(searchLower),
203
+ (option) => {
204
+ const labelLower = option.label ? removeAccents(option.label.toLowerCase()) : '';
205
+ const valueLower = option.value !== undefined && option.value !== null
206
+ ? removeAccents(String(option.value).toLowerCase())
207
+ : '';
208
+ return labelLower.includes(searchLower) || valueLower.includes(searchLower);
209
+ }
199
210
  );
200
211
  }, [options, searchValue]);
201
212
 
@@ -391,78 +402,44 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
391
402
  setOpen(!open);
392
403
  }}
393
404
  >
394
- <div className="flex flex-wrap gap-1">
395
- {selectedOptions.length > 0 ? (
396
- <>
397
- {selectedOptions
398
- .slice(0, responsiveSettings.maxCount)
399
- .map((option, index) => (
400
- <Badge
401
- key={String(option.value)}
402
- className={cn(
403
- "mr-1 mb-1",
404
- getBadgeColor(index),
405
- responsiveSettings.compactMode &&
406
- "text-xs px-1.5 py-0.5",
407
- )}
408
- onClick={(e) => handleRemove(option.value, e)}
409
- >
410
- {option.icon && !responsiveSettings.compactMode && (
411
- <option.icon className="mr-2 h-4 w-4" />
412
- )}
413
- {option.label}
414
- <div
415
- role="button"
416
- tabIndex={0}
417
- className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
418
- onKeyDown={(e) => {
419
- if (e.key === "Enter") {
420
- handleRemove(option.value, e as any);
421
- }
422
- }}
423
- onMouseDown={(e) => {
424
- e.preventDefault();
425
- e.stopPropagation();
426
- }}
427
- onClick={(e) => handleRemove(option.value, e)}
428
- >
429
- <X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
430
- </div>
431
- </Badge>
432
- ))}
433
- {value.length > responsiveSettings.maxCount && (
434
- <Badge
435
- className={cn(
436
- "mr-1 mb-1 bg-muted text-muted-foreground hover:bg-muted",
437
- responsiveSettings.compactMode && "text-xs px-1.5 py-0.5",
438
- )}
439
- >
440
- +{value.length - responsiveSettings.maxCount} more
441
- <div
442
- role="button"
443
- tabIndex={0}
444
- className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
445
- onClick={(e) => {
446
- e.stopPropagation();
447
- clearExtraOptions();
448
- }}
449
- >
450
- <X className="h-3 w-3" />
451
- </div>
452
- </Badge>
453
- )}
454
- </>
455
- ) : (
405
+ <div className="flex flex-wrap gap-1 items-center">
406
+ {value.length === 0 ? (
456
407
  <span className="text-muted-foreground">{placeholder}</span>
408
+ ) : value.length === 1 ? (
409
+ <Badge variant="secondary" className="mr-1 bg-secondary hover:bg-secondary border-transparent font-normal truncate max-w-[150px]">
410
+ {options.find((o) => o.value === value[0])?.label}
411
+ </Badge>
412
+ ) : value.length === options.length ? (
413
+ <Badge variant="secondary" className="mr-1 bg-secondary hover:bg-secondary border-transparent font-normal">
414
+ Tất cả lựa chọn
415
+ </Badge>
416
+ ) : (
417
+ <Badge className="mr-1 bg-primary text-primary-foreground hover:bg-primary font-medium">
418
+ {value.length} đã chọn
419
+ </Badge>
457
420
  )}
458
421
  </div>
459
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
422
+ <div className="flex items-center space-x-1 ml-2">
423
+ {value.length > 0 && (
424
+ <div
425
+ role="button"
426
+ className="h-5 w-5 rounded-md flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
427
+ onClick={(e) => {
428
+ e.stopPropagation();
429
+ handleClearAll();
430
+ }}
431
+ >
432
+ <X className="h-3 w-3" />
433
+ </div>
434
+ )}
435
+ <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
436
+ </div>
460
437
  </Button>
461
438
 
462
439
  {open && (
463
440
  <div
464
441
  ref={dropdownRef}
465
- className="absolute z-[100] mt-1 w-full rounded-md border bg-popover text-popover-foreground shadow-md"
442
+ className="absolute z-[100] mt-1 w-full rounded-md border bg-popover/95 backdrop-blur-md text-popover-foreground shadow-lg shadow-black/5 animate-in fade-in-0 zoom-in-95 slide-in-from-top-1"
466
443
  style={{
467
444
  top: "100%",
468
445
  left: 0,
@@ -516,34 +493,48 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
516
493
 
517
494
  {/* Action Buttons */}
518
495
  <div className="flex items-center justify-between border-b p-1">
496
+ <div className="flex items-center">
497
+ <Button
498
+ type="button"
499
+ variant="ghost"
500
+ size="sm"
501
+ className="h-8 px-2 text-xs"
502
+ onClick={(e) => {
503
+ e.stopPropagation();
504
+ handleSelectAll();
505
+ }}
506
+ >
507
+ Select All
508
+ </Button>
509
+ <Button
510
+ type="button"
511
+ variant="ghost"
512
+ size="sm"
513
+ className="h-8 px-2 text-xs"
514
+ onClick={(e) => {
515
+ e.stopPropagation();
516
+ handleClearAll();
517
+ }}
518
+ >
519
+ Clear All
520
+ </Button>
521
+ </div>
519
522
  <Button
520
523
  type="button"
521
524
  variant="ghost"
522
525
  size="sm"
523
- className="h-8 px-2 text-xs"
524
- onClick={(e) => {
525
- e.stopPropagation();
526
- handleSelectAll();
527
- }}
528
- >
529
- Select All
530
- </Button>
531
- <Button
532
- type="button"
533
- variant="ghost"
534
- size="sm"
535
- className="h-8 px-2 text-xs"
526
+ className="h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
536
527
  onClick={(e) => {
537
528
  e.stopPropagation();
538
- handleClearAll();
529
+ setOpen(false);
539
530
  }}
540
531
  >
541
- Clear All
532
+ Đóng
542
533
  </Button>
543
534
  </div>
544
535
 
545
536
  {/* Options List */}
546
- <ScrollArea className="max-h-[300px]">
537
+ <div className="max-h-[250px] overflow-y-auto overflow-x-hidden">
547
538
  {filteredOptions.length === 0 ? (
548
539
  <div className="py-6 text-center text-sm text-muted-foreground">
549
540
  {emptyText}
@@ -560,13 +551,13 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
560
551
  aria-selected={isSelected}
561
552
  aria-disabled={isDisabled}
562
553
  className={cn(
563
- "relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
554
+ "relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-150",
564
555
  isDisabled
565
556
  ? "cursor-not-allowed opacity-50"
566
- : "cursor-pointer hover:bg-accent hover:text-accent-foreground",
557
+ : "cursor-pointer hover:bg-accent/80 hover:text-accent-foreground",
567
558
  isSelected &&
568
559
  !isDisabled &&
569
- "bg-accent text-accent-foreground",
560
+ "bg-accent/50 text-accent-foreground",
570
561
  )}
571
562
  onMouseDown={(e) => {
572
563
  e.preventDefault();
@@ -582,13 +573,13 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
582
573
  >
583
574
  <div
584
575
  className={cn(
585
- "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
576
+ "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border transition-all duration-200",
586
577
  isSelected
587
- ? "bg-primary text-primary-foreground"
588
- : "opacity-50 [&_svg]:invisible",
578
+ ? "bg-primary border-primary text-primary-foreground"
579
+ : "border-input opacity-50",
589
580
  )}
590
581
  >
591
- <Check className={cn("h-4 w-4")} />
582
+ <Check className={cn("h-3 w-3 transition-opacity", isSelected ? "opacity-100" : "opacity-0")} />
592
583
  </div>
593
584
  {option.icon && (
594
585
  <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
@@ -596,28 +587,19 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
596
587
  <span className="flex-1 truncate">
597
588
  {option.label}
598
589
  </span>
590
+ {option.count !== undefined && (
591
+ <span className="ml-2 text-xs text-muted-foreground">
592
+ {option.count}
593
+ </span>
594
+ )}
599
595
  </div>
600
596
  );
601
597
  })}
602
598
  </div>
603
599
  )}
604
- </ScrollArea>
605
-
606
- {/* Close Button */}
607
- <div className="border-t p-2">
608
- <Button
609
- type="button"
610
- variant="outline"
611
- size="sm"
612
- className="w-full h-8 text-xs"
613
- onClick={(e) => {
614
- e.stopPropagation();
615
- setOpen(false);
616
- }}
617
- >
618
- Close
619
- </Button>
620
600
  </div>
601
+
602
+
621
603
  </div>
622
604
  </div>
623
605
  )}
@@ -0,0 +1,125 @@
1
+ import re
2
+
3
+ filepath = "/Users/thongle/Downloads/REPO/goerp/goerp-core/packages/core/src/ui/forms/multi-select.tsx"
4
+
5
+ with open(filepath, 'r', encoding='utf-8') as f:
6
+ content = f.read()
7
+
8
+ # 1. Thay thế Action Buttons và thêm Close icon
9
+ old_action_buttons = """ {/* Action Buttons */}
10
+ <div className="flex items-center justify-between border-b p-1">
11
+ <Button
12
+ type="button"
13
+ variant="ghost"
14
+ size="sm"
15
+ className="h-8 px-2 text-xs"
16
+ onClick={(e) => {
17
+ e.stopPropagation();
18
+ handleSelectAll();
19
+ }}
20
+ >
21
+ Select All
22
+ </Button>
23
+ <Button
24
+ type="button"
25
+ variant="ghost"
26
+ size="sm"
27
+ className="h-8 px-2 text-xs"
28
+ onClick={(e) => {
29
+ e.stopPropagation();
30
+ handleClearAll();
31
+ }}
32
+ >
33
+ Clear All
34
+ </Button>
35
+ </div>"""
36
+
37
+ new_action_buttons = """ {/* Action Buttons */}
38
+ <div className="flex items-center justify-between border-b p-1">
39
+ <div className="flex items-center">
40
+ <Button
41
+ type="button"
42
+ variant="ghost"
43
+ size="sm"
44
+ className="h-8 px-2 text-xs"
45
+ onClick={(e) => {
46
+ e.stopPropagation();
47
+ handleSelectAll();
48
+ }}
49
+ >
50
+ Select All
51
+ </Button>
52
+ <Button
53
+ type="button"
54
+ variant="ghost"
55
+ size="sm"
56
+ className="h-8 px-2 text-xs"
57
+ onClick={(e) => {
58
+ e.stopPropagation();
59
+ handleClearAll();
60
+ }}
61
+ >
62
+ Clear All
63
+ </Button>
64
+ </div>
65
+ <Button
66
+ type="button"
67
+ variant="ghost"
68
+ size="sm"
69
+ className="h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
70
+ onClick={(e) => {
71
+ e.stopPropagation();
72
+ setOpen(false);
73
+ }}
74
+ >
75
+ Đóng
76
+ </Button>
77
+ </div>"""
78
+
79
+ content = content.replace(old_action_buttons, new_action_buttons)
80
+
81
+ # 2. Xóa Close button cũ dưới cùng
82
+ old_close_btn = """ {/* Close Button */}
83
+ <div className="border-t p-2">
84
+ <Button
85
+ type="button"
86
+ variant="outline"
87
+ size="sm"
88
+ className="w-full h-8 text-xs"
89
+ onClick={(e) => {
90
+ e.stopPropagation();
91
+ setOpen(false);
92
+ }}
93
+ >
94
+ Close
95
+ </Button>
96
+ </div>"""
97
+
98
+ content = content.replace(old_close_btn, "")
99
+
100
+ # 3. Thay ScrollArea bằng native div cuộn
101
+ old_scroll_start = """ {/* Options List */}
102
+ <ScrollArea className="max-h-[300px]">
103
+ {filteredOptions.length === 0 ? ("""
104
+
105
+ new_scroll_start = """ {/* Options List */}
106
+ <div className="max-h-[250px] overflow-y-auto overflow-x-hidden">
107
+ {filteredOptions.length === 0 ? ("""
108
+
109
+ content = content.replace(old_scroll_start, new_scroll_start)
110
+
111
+ # The end tag for ScrollArea
112
+ old_scroll_end = """ </div>
113
+ )}
114
+ </ScrollArea>"""
115
+
116
+ new_scroll_end = """ </div>
117
+ )}
118
+ </div>"""
119
+
120
+ content = content.replace(old_scroll_end, new_scroll_end)
121
+
122
+ with open(filepath, 'w', encoding='utf-8') as f:
123
+ f.write(content)
124
+
125
+ print("Updated multiselect component")
@@ -0,0 +1,48 @@
1
+ import re
2
+
3
+ filepath = "/Users/thongle/Downloads/REPO/goerp/goerp-core/packages/core/src/ui/forms/multi-select.tsx"
4
+
5
+ with open(filepath, 'r', encoding='utf-8') as f:
6
+ content = f.read()
7
+
8
+ old_search_logic = """ // Filter options based on search value
9
+ const filteredOptions = useMemo(() => {
10
+ if (!searchValue.trim()) {
11
+ return options;
12
+ }
13
+ const searchLower = searchValue.toLowerCase();
14
+ return options.filter(
15
+ (option) =>
16
+ option.label.toLowerCase().includes(searchLower) ||
17
+ String(option.value).toLowerCase().includes(searchLower),
18
+ );
19
+ }, [options, searchValue]);"""
20
+
21
+ new_search_logic = """ // Filter options based on search value
22
+ const filteredOptions = useMemo(() => {
23
+ if (!searchValue.trim()) {
24
+ return options;
25
+ }
26
+ const removeAccents = (str: string) => {
27
+ return str ? str.normalize('NFD').replace(/[\\u0300-\\u036f]/g, '') : '';
28
+ };
29
+
30
+ const searchLower = removeAccents(searchValue.toLowerCase());
31
+
32
+ return options.filter(
33
+ (option) => {
34
+ const labelLower = option.label ? removeAccents(option.label.toLowerCase()) : '';
35
+ const valueLower = option.value !== undefined && option.value !== null
36
+ ? removeAccents(String(option.value).toLowerCase())
37
+ : '';
38
+ return labelLower.includes(searchLower) || valueLower.includes(searchLower);
39
+ }
40
+ );
41
+ }, [options, searchValue]);"""
42
+
43
+ content = content.replace(old_search_logic, new_search_logic)
44
+
45
+ with open(filepath, 'w', encoding='utf-8') as f:
46
+ f.write(content)
47
+
48
+ print("Updated multiselect search logic")
@@ -0,0 +1,119 @@
1
+ import re
2
+
3
+ filepath = "/Users/thongle/Downloads/REPO/goerp/goerp-core/packages/core/src/ui/forms/multi-select.tsx"
4
+
5
+ with open(filepath, 'r', encoding='utf-8') as f:
6
+ content = f.read()
7
+
8
+ # 1. Update MultiSelectOption interface
9
+ old_interface = """ /** Optional icon component to display alongside the option */
10
+ icon?: React.ComponentType<{ className?: string }>;
11
+ }"""
12
+
13
+ new_interface = """ /** Optional icon component to display alongside the option */
14
+ icon?: React.ComponentType<{ className?: string }>;
15
+ /** Optional count to display on the right side of the option */
16
+ count?: number;
17
+ }"""
18
+
19
+ content = content.replace(old_interface, new_interface)
20
+
21
+ # 2. Update trigger button (Compact Badge & Clear Icon)
22
+ # We need to replace the content of `<div className="flex flex-wrap gap-1">`
23
+ # and the ChevronsUpDown area.
24
+ old_trigger_area = r'<div className="flex flex-wrap gap-1">[\s\S]*?<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />'
25
+
26
+ new_trigger_area = """<div className="flex flex-wrap gap-1 items-center">
27
+ {value.length === 0 ? (
28
+ <span className="text-muted-foreground">{placeholder}</span>
29
+ ) : value.length === 1 ? (
30
+ <Badge variant="secondary" className="mr-1 bg-secondary hover:bg-secondary border-transparent font-normal truncate max-w-[150px]">
31
+ {options.find((o) => o.value === value[0])?.label}
32
+ </Badge>
33
+ ) : value.length === options.length ? (
34
+ <Badge variant="secondary" className="mr-1 bg-secondary hover:bg-secondary border-transparent font-normal">
35
+ Tất cả lựa chọn
36
+ </Badge>
37
+ ) : (
38
+ <Badge className="mr-1 bg-primary text-primary-foreground hover:bg-primary font-medium">
39
+ {value.length} đã chọn
40
+ </Badge>
41
+ )}
42
+ </div>
43
+ <div className="flex items-center space-x-1 ml-2">
44
+ {value.length > 0 && (
45
+ <div
46
+ role="button"
47
+ className="h-5 w-5 rounded-md flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
48
+ onClick={(e) => {
49
+ e.stopPropagation();
50
+ handleClearAll();
51
+ }}
52
+ >
53
+ <X className="h-3 w-3" />
54
+ </div>
55
+ )}
56
+ <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
57
+ </div>"""
58
+
59
+ content = re.sub(old_trigger_area, new_trigger_area, content)
60
+
61
+
62
+ # 3. Update Popover Container for glassmorphism and animation
63
+ old_popover = """className="absolute z-[100] mt-1 w-full rounded-md border bg-popover text-popover-foreground shadow-md\""""
64
+ new_popover = """className="absolute z-[100] mt-1 w-full rounded-md border bg-popover/95 backdrop-blur-md text-popover-foreground shadow-lg shadow-black/5 animate-in fade-in-0 zoom-in-95 slide-in-from-top-1\""""
65
+ content = content.replace(old_popover, new_popover)
66
+
67
+
68
+ # 4. Update Option Checkbox and Count
69
+ old_option_render = r"""<div\s+className=\{cn\(\s*"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",\s*isSelected\s*\?\s*"bg-primary text-primary-foreground"\s*:\s*"opacity-50 \[\&_svg\]:invisible",\s*\)\}\s*>\s*<Check className=\{cn\("h-4 w-4"\)\} />\s*</div>\s*\{option\.icon && \(\s*<option\.icon className="mr-2 h-4 w-4 text-muted-foreground" />\s*\)\}\s*<span className="flex-1 truncate">\s*\{option\.label\}\s*</span>"""
70
+
71
+ new_option_render = """<div
72
+ className={cn(
73
+ "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border transition-all duration-200",
74
+ isSelected
75
+ ? "bg-primary border-primary text-primary-foreground"
76
+ : "border-input opacity-50",
77
+ )}
78
+ >
79
+ <Check className={cn("h-3 w-3 transition-opacity", isSelected ? "opacity-100" : "opacity-0")} />
80
+ </div>
81
+ {option.icon && (
82
+ <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
83
+ )}
84
+ <span className="flex-1 truncate">
85
+ {option.label}
86
+ </span>
87
+ {option.count !== undefined && (
88
+ <span className="ml-2 text-xs text-muted-foreground">
89
+ {option.count}
90
+ </span>
91
+ )}"""
92
+
93
+ content = re.sub(old_option_render, new_option_render, content)
94
+
95
+ # 5. Option Hover Animation
96
+ old_option_wrapper = """className={cn(
97
+ "relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
98
+ isDisabled
99
+ ? "cursor-not-allowed opacity-50"
100
+ : "cursor-pointer hover:bg-accent hover:text-accent-foreground",
101
+ isSelected &&
102
+ !isDisabled &&
103
+ "bg-accent text-accent-foreground",
104
+ )}"""
105
+ new_option_wrapper = """className={cn(
106
+ "relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-150",
107
+ isDisabled
108
+ ? "cursor-not-allowed opacity-50"
109
+ : "cursor-pointer hover:bg-accent/80 hover:text-accent-foreground",
110
+ isSelected &&
111
+ !isDisabled &&
112
+ "bg-accent/50 text-accent-foreground",
113
+ )}"""
114
+ content = content.replace(old_option_wrapper, new_option_wrapper)
115
+
116
+ with open(filepath, 'w', encoding='utf-8') as f:
117
+ f.write(content)
118
+
119
+ print("UI Upgrade Successful")
package/src/ui/index.tsx CHANGED
@@ -16,3 +16,5 @@ export * from "./auth";
16
16
  export * from "./management";
17
17
  export * from "./pages/not-found";
18
18
  export * from "./shared";
19
+
20
+ export { UrlPagination } from "./data-display/url-pagination";
@@ -144,14 +144,14 @@ export function Combobox({
144
144
  role="combobox"
145
145
  aria-expanded={open}
146
146
  disabled={disabled}
147
- className={cn("w-full justify-between", className)}
147
+ className={cn("w-full justify-between h-auto min-h-9 text-left whitespace-normal gap-2", className)}
148
148
  id={id}
149
149
  onClick={(e) => {
150
150
  e.stopPropagation();
151
151
  setOpen(!open);
152
152
  }}
153
153
  >
154
- <span className="truncate">
154
+ <span className="break-words min-w-0">
155
155
  {selectedOption ? selectedOption.label : placeholder}
156
156
  </span>
157
157
  <div className="ml-2 flex items-center gap-1 shrink-0">
@@ -37,7 +37,7 @@ export function SelectTrigger({
37
37
  <SelectPrimitive.Trigger
38
38
  data-slot="select-trigger"
39
39
  className={cn(
40
- "cursor-pointer flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
40
+ "cursor-pointer flex min-h-9 h-auto w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm text-left ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&_span]:line-clamp-none [&_span]:whitespace-normal [&_span]:break-words [&_span]:min-w-0",
41
41
  className,
42
42
  )}
43
43
  {...props}