@goplusvn/core 0.1.19 → 0.1.20
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 +1 -1
- package/src/ui/forms/multi-select.tsx +89 -107
- package/src/ui/forms/update_multiselect.py +125 -0
- package/src/ui/forms/update_search.py +48 -0
- package/src/ui/forms/upgrade_multiselect.py +119 -0
- package/src/ui/primitives/combobox.tsx +2 -2
- package/src/ui/primitives/select.tsx +1 -1
package/package.json
CHANGED
|
@@ -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
|
|
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()
|
|
198
|
-
|
|
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
|
-
{
|
|
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
|
-
<
|
|
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-
|
|
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
|
-
|
|
529
|
+
setOpen(false);
|
|
539
530
|
}}
|
|
540
531
|
>
|
|
541
|
-
|
|
532
|
+
Đóng
|
|
542
533
|
</Button>
|
|
543
534
|
</div>
|
|
544
535
|
|
|
545
536
|
{/* Options List */}
|
|
546
|
-
<
|
|
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
|
|
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
|
|
578
|
+
? "bg-primary border-primary text-primary-foreground"
|
|
579
|
+
: "border-input opacity-50",
|
|
589
580
|
)}
|
|
590
581
|
>
|
|
591
|
-
<Check className={cn("h-
|
|
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")
|
|
@@ -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="
|
|
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
|
|
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}
|