@comitor/ui 0.1.0
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/LICENSE +21 -0
- package/README.md +458 -0
- package/dist/chart/chart.d.ts +88 -0
- package/dist/chart/chart.js +199 -0
- package/dist/chart/chart.js.map +1 -0
- package/dist/chart/comitor-chart.d.ts +109 -0
- package/dist/chart/comitor-chart.js +189 -0
- package/dist/chart/comitor-chart.js.map +1 -0
- package/dist/chart/index.d.ts +4 -0
- package/dist/chart/index.js +3 -0
- package/dist/chart/index.js.map +1 -0
- package/dist/components/composite/avatar-group.d.ts +33 -0
- package/dist/components/composite/avatar-group.js +86 -0
- package/dist/components/composite/avatar-group.js.map +1 -0
- package/dist/components/composite/combobox.d.ts +70 -0
- package/dist/components/composite/combobox.js +291 -0
- package/dist/components/composite/combobox.js.map +1 -0
- package/dist/components/composite/command-palette.d.ts +56 -0
- package/dist/components/composite/command-palette.js +131 -0
- package/dist/components/composite/command-palette.js.map +1 -0
- package/dist/components/composite/confirm-dialog.d.ts +40 -0
- package/dist/components/composite/confirm-dialog.js +96 -0
- package/dist/components/composite/confirm-dialog.js.map +1 -0
- package/dist/components/composite/copy-button.d.ts +27 -0
- package/dist/components/composite/copy-button.js +80 -0
- package/dist/components/composite/copy-button.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +136 -0
- package/dist/components/composite/data-table.js +300 -0
- package/dist/components/composite/data-table.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +77 -0
- package/dist/components/composite/date-picker.js +268 -0
- package/dist/components/composite/date-picker.js.map +1 -0
- package/dist/components/composite/empty-state.d.ts +38 -0
- package/dist/components/composite/empty-state.js +35 -0
- package/dist/components/composite/empty-state.js.map +1 -0
- package/dist/components/composite/filter-chips.d.ts +39 -0
- package/dist/components/composite/filter-chips.js +79 -0
- package/dist/components/composite/filter-chips.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +58 -0
- package/dist/components/composite/form-field.js +81 -0
- package/dist/components/composite/form-field.js.map +1 -0
- package/dist/components/composite/form-section.d.ts +33 -0
- package/dist/components/composite/form-section.js +85 -0
- package/dist/components/composite/form-section.js.map +1 -0
- package/dist/components/composite/inline-edit.d.ts +38 -0
- package/dist/components/composite/inline-edit.js +165 -0
- package/dist/components/composite/inline-edit.js.map +1 -0
- package/dist/components/composite/keyboard-hint.d.ts +31 -0
- package/dist/components/composite/keyboard-hint.js +72 -0
- package/dist/components/composite/keyboard-hint.js.map +1 -0
- package/dist/components/composite/letter-avatar.d.ts +47 -0
- package/dist/components/composite/letter-avatar.js +66 -0
- package/dist/components/composite/letter-avatar.js.map +1 -0
- package/dist/components/composite/page-container.d.ts +25 -0
- package/dist/components/composite/page-container.js +23 -0
- package/dist/components/composite/page-container.js.map +1 -0
- package/dist/components/composite/page-header.d.ts +49 -0
- package/dist/components/composite/page-header.js +75 -0
- package/dist/components/composite/page-header.js.map +1 -0
- package/dist/components/composite/relative-time.d.ts +28 -0
- package/dist/components/composite/relative-time.js +44 -0
- package/dist/components/composite/relative-time.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +38 -0
- package/dist/components/composite/search-filter-bar.js +135 -0
- package/dist/components/composite/search-filter-bar.js.map +1 -0
- package/dist/components/composite/stat-card.d.ts +48 -0
- package/dist/components/composite/stat-card.js +76 -0
- package/dist/components/composite/stat-card.js.map +1 -0
- package/dist/components/composite/status-config.d.ts +85 -0
- package/dist/components/composite/status-config.js +41 -0
- package/dist/components/composite/status-config.js.map +1 -0
- package/dist/components/composite/status-pill.d.ts +26 -0
- package/dist/components/composite/status-pill.js +59 -0
- package/dist/components/composite/status-pill.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +31 -0
- package/dist/components/composite/table-pagination.js +103 -0
- package/dist/components/composite/table-pagination.js.map +1 -0
- package/dist/components/composite/text-utils.d.ts +38 -0
- package/dist/components/composite/text-utils.js +40 -0
- package/dist/components/composite/text-utils.js.map +1 -0
- package/dist/components/composite/toast.d.ts +56 -0
- package/dist/components/composite/toast.js +63 -0
- package/dist/components/composite/toast.js.map +1 -0
- package/dist/components/ui/accordion.d.ts +17 -0
- package/dist/components/ui/accordion.js +53 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +34 -0
- package/dist/components/ui/alert-dialog.js +103 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/alert.d.ts +16 -0
- package/dist/components/ui/alert.js +50 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +7 -0
- package/dist/components/ui/aspect-ratio.js +10 -0
- package/dist/components/ui/aspect-ratio.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +33 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +15 -0
- package/dist/components/ui/badge.js +37 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +30 -0
- package/dist/components/ui/breadcrumb.js +89 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button-group.d.ts +21 -0
- package/dist/components/ui/button-group.js +62 -0
- package/dist/components/ui/button-group.js.map +1 -0
- package/dist/components/ui/button.d.ts +21 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +26 -0
- package/dist/components/ui/calendar.js +148 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +33 -0
- package/dist/components/ui/card.js +70 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/carousel.d.ts +46 -0
- package/dist/components/ui/carousel.js +173 -0
- package/dist/components/ui/carousel.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +17 -0
- package/dist/components/ui/checkbox.js +67 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +17 -0
- package/dist/components/ui/collapsible.js +18 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/command.d.ts +41 -0
- package/dist/components/ui/command.js +122 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +51 -0
- package/dist/components/ui/context-menu.js +166 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +48 -0
- package/dist/components/ui/dialog.js +131 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +32 -0
- package/dist/components/ui/drawer.js +102 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +52 -0
- package/dist/components/ui/dropdown-menu.js +167 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty.d.ts +22 -0
- package/dist/components/ui/empty.js +87 -0
- package/dist/components/ui/empty.js.map +1 -0
- package/dist/components/ui/field.d.ts +46 -0
- package/dist/components/ui/field.js +197 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/form.d.ts +50 -0
- package/dist/components/ui/form.js +100 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +17 -0
- package/dist/components/ui/hover-card.js +31 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +26 -0
- package/dist/components/ui/input-group.js +148 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-otp.d.ts +23 -0
- package/dist/components/ui/input-otp.js +54 -0
- package/dist/components/ui/input-otp.js.map +1 -0
- package/dist/components/ui/input.d.ts +21 -0
- package/dist/components/ui/input.js +62 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/item.d.ts +39 -0
- package/dist/components/ui/item.js +142 -0
- package/dist/components/ui/item.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +8 -0
- package/dist/components/ui/kbd.js +25 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/label.d.ts +7 -0
- package/dist/components/ui/label.js +21 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/menubar.d.ts +51 -0
- package/dist/components/ui/menubar.js +196 -0
- package/dist/components/ui/menubar.js.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +39 -0
- package/dist/components/ui/navigation-menu.js +123 -0
- package/dist/components/ui/navigation-menu.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +40 -0
- package/dist/components/ui/pagination.js +95 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +19 -0
- package/dist/components/ui/popover.js +35 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +12 -0
- package/dist/components/ui/progress.js +27 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +9 -0
- package/dist/components/ui/radio-group.js +34 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +19 -0
- package/dist/components/ui/resizable.js +38 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +13 -0
- package/dist/components/ui/scroll-area.js +46 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +33 -0
- package/dist/components/ui/select.js +133 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +7 -0
- package/dist/components/ui/separator.js +23 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +38 -0
- package/dist/components/ui/sheet.js +96 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +6 -0
- package/dist/components/ui/skeleton.js +9 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +12 -0
- package/dist/components/ui/slider.js +54 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +9 -0
- package/dist/components/ui/sonner.js +25 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +6 -0
- package/dist/components/ui/spinner.js +10 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/switch.d.ts +14 -0
- package/dist/components/ui/switch.js +65 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +30 -0
- package/dist/components/ui/table.js +87 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +35 -0
- package/dist/components/ui/tabs.js +54 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +6 -0
- package/dist/components/ui/textarea.js +19 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +18 -0
- package/dist/components/ui/toggle-group.js +52 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +15 -0
- package/dist/components/ui/toggle.js +33 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +21 -0
- package/dist/components/ui/tooltip.js +38 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/form.d.ts +5 -0
- package/dist/form.js +2 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +16 -0
- package/dist/lib/cn.js +9 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/shell/app-header.d.ts +41 -0
- package/dist/shell/app-header.js +136 -0
- package/dist/shell/app-header.js.map +1 -0
- package/dist/shell/app-launcher.d.ts +33 -0
- package/dist/shell/app-launcher.js +189 -0
- package/dist/shell/app-launcher.js.map +1 -0
- package/dist/shell/app-shell.d.ts +44 -0
- package/dist/shell/app-shell.js +99 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/comitor-lockup.d.ts +33 -0
- package/dist/shell/comitor-lockup.js +112 -0
- package/dist/shell/comitor-lockup.js.map +1 -0
- package/dist/shell/comitor-logo.d.ts +28 -0
- package/dist/shell/comitor-logo.js +92 -0
- package/dist/shell/comitor-logo.js.map +1 -0
- package/dist/shell/index.d.ts +24 -0
- package/dist/shell/index.js +23 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/mobile-menu.d.ts +26 -0
- package/dist/shell/mobile-menu.js +56 -0
- package/dist/shell/mobile-menu.js.map +1 -0
- package/dist/shell/notifications-button.d.ts +22 -0
- package/dist/shell/notifications-button.js +39 -0
- package/dist/shell/notifications-button.js.map +1 -0
- package/dist/shell/shell-context.d.ts +91 -0
- package/dist/shell/shell-context.js +233 -0
- package/dist/shell/shell-context.js.map +1 -0
- package/dist/shell/shell-link.d.ts +19 -0
- package/dist/shell/shell-link.js +18 -0
- package/dist/shell/shell-link.js.map +1 -0
- package/dist/shell/sidebar-group.d.ts +21 -0
- package/dist/shell/sidebar-group.js +43 -0
- package/dist/shell/sidebar-group.js.map +1 -0
- package/dist/shell/sidebar-menu-item.d.ts +27 -0
- package/dist/shell/sidebar-menu-item.js +152 -0
- package/dist/shell/sidebar-menu-item.js.map +1 -0
- package/dist/shell/sidebar.d.ts +26 -0
- package/dist/shell/sidebar.js +92 -0
- package/dist/shell/sidebar.js.map +1 -0
- package/dist/shell/theme-provider.d.ts +18 -0
- package/dist/shell/theme-provider.js +29 -0
- package/dist/shell/theme-provider.js.map +1 -0
- package/dist/shell/theme-toggle.d.ts +28 -0
- package/dist/shell/theme-toggle.js +67 -0
- package/dist/shell/theme-toggle.js.map +1 -0
- package/dist/shell/types.d.ts +168 -0
- package/dist/shell/types.js +38 -0
- package/dist/shell/types.js.map +1 -0
- package/dist/shell/user-menu.d.ts +42 -0
- package/dist/shell/user-menu.js +132 -0
- package/dist/shell/user-menu.js.map +1 -0
- package/dist/shell/utils.d.ts +62 -0
- package/dist/shell/utils.js +73 -0
- package/dist/shell/utils.js.map +1 -0
- package/dist/shell/workspace-switcher.d.ts +24 -0
- package/dist/shell/workspace-switcher.js +219 -0
- package/dist/shell/workspace-switcher.js.map +1 -0
- package/dist/tokens.d.ts +173 -0
- package/dist/tokens.js +161 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +151 -0
- package/styles.css +357 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
|
|
5
|
+
import { LetterAvatar } from "./letter-avatar.js";
|
|
6
|
+
const overflowSizeClasses = {
|
|
7
|
+
xs: "size-6 text-[10px]",
|
|
8
|
+
sm: "size-7 text-xs",
|
|
9
|
+
md: "size-9 text-xs",
|
|
10
|
+
lg: "size-11 text-sm",
|
|
11
|
+
xl: "size-14 text-base"
|
|
12
|
+
};
|
|
13
|
+
function AvatarGroup({
|
|
14
|
+
items,
|
|
15
|
+
max = 4,
|
|
16
|
+
size = "sm",
|
|
17
|
+
onOverflowClick,
|
|
18
|
+
label = "Th\xE0nh vi\xEAn",
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}) {
|
|
22
|
+
const visible = items.slice(0, Math.max(0, max));
|
|
23
|
+
const overflow = items.length - visible.length;
|
|
24
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
|
|
25
|
+
"div",
|
|
26
|
+
{
|
|
27
|
+
"data-slot": "avatar-group",
|
|
28
|
+
role: "group",
|
|
29
|
+
"aria-label": label,
|
|
30
|
+
className: cn("flex items-center -space-x-2", className),
|
|
31
|
+
...props,
|
|
32
|
+
children: [
|
|
33
|
+
visible.map((item) => /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
34
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
35
|
+
"button",
|
|
36
|
+
{
|
|
37
|
+
type: "button",
|
|
38
|
+
className: "rounded-full ring-2 ring-background transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
39
|
+
children: /* @__PURE__ */ jsx(LetterAvatar, { name: item.name, src: item.src, size, title: void 0 })
|
|
40
|
+
}
|
|
41
|
+
) }),
|
|
42
|
+
/* @__PURE__ */ jsxs(TooltipContent, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: item.name }),
|
|
44
|
+
item.caption && /* @__PURE__ */ jsx("span", { className: "block text-xs opacity-80", children: item.caption })
|
|
45
|
+
] })
|
|
46
|
+
] }, item.id)),
|
|
47
|
+
overflow > 0 && (onOverflowClick ? /* @__PURE__ */ jsxs(
|
|
48
|
+
"button",
|
|
49
|
+
{
|
|
50
|
+
type: "button",
|
|
51
|
+
onClick: onOverflowClick,
|
|
52
|
+
className: cn(
|
|
53
|
+
"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background transition-colors hover:bg-accent hover:text-accent-foreground",
|
|
54
|
+
overflowSizeClasses[size]
|
|
55
|
+
),
|
|
56
|
+
children: [
|
|
57
|
+
"+",
|
|
58
|
+
overflow,
|
|
59
|
+
/* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
60
|
+
"Xem t\u1EA5t c\u1EA3 ",
|
|
61
|
+
items.length,
|
|
62
|
+
" th\xE0nh vi\xEAn"
|
|
63
|
+
] })
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
) : /* @__PURE__ */ jsxs(
|
|
67
|
+
"span",
|
|
68
|
+
{
|
|
69
|
+
className: cn(
|
|
70
|
+
"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background",
|
|
71
|
+
overflowSizeClasses[size]
|
|
72
|
+
),
|
|
73
|
+
children: [
|
|
74
|
+
"+",
|
|
75
|
+
overflow
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
))
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
) });
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
AvatarGroup
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=avatar-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định \"Thành viên\") */\n label?: string;\n}\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n label = \"Thành viên\",\n className,\n ...props\n}: AvatarGroupProps) {\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center -space-x-2\", className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n <button\n type=\"button\"\n className=\"rounded-full ring-2 ring-background transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">Xem tất cả {items.length} thành viên</span>\n </button>\n ) : (\n <span\n className={cn(\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AA2EgB,cAGJ,YAHI;AAxEhB,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AAqBpD,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,gCAAgC,SAAS;AAAA,MACtD,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAKrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAU;AAAA,cAEV,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YACxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aAhBY,KAAK,EAiBnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA,cACT;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,gBAAY,MAAM;AAAA,gBAAO;AAAA,iBAAW;AAAA;AAAA;AAAA,QAChE,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { IconComponent } from './empty-state.js';
|
|
3
|
+
|
|
4
|
+
interface ComboboxOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Dòng phụ mờ dưới nhãn (vd email của người dùng, mã của khách hàng) */
|
|
8
|
+
description?: string;
|
|
9
|
+
icon?: IconComponent;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Nhãn nhóm — các mục cùng nhóm được gom lại và có tiêu đề nhóm */
|
|
12
|
+
group?: string;
|
|
13
|
+
/** Từ khoá phụ để tìm ra mục này (vd mã số, tên viết tắt) */
|
|
14
|
+
keywords?: string[];
|
|
15
|
+
}
|
|
16
|
+
interface ComboboxSharedProps {
|
|
17
|
+
options: ComboboxOption[];
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
emptyText?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).
|
|
24
|
+
* Không truyền = lọc tại chỗ, không dấu tiếng Việt.
|
|
25
|
+
*/
|
|
26
|
+
onSearchChange?: (search: string) => void;
|
|
27
|
+
/** Đang tải kết quả (chỉ có nghĩa ở chế độ bất đồng bộ) */
|
|
28
|
+
loading?: boolean;
|
|
29
|
+
/** Cho phép xoá lựa chọn bằng nút X trên trigger */
|
|
30
|
+
clearable?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
/** className của panel popover (vd đặt bề rộng cố định) */
|
|
33
|
+
contentClassName?: string;
|
|
34
|
+
id?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
"aria-label"?: string;
|
|
37
|
+
"aria-labelledby"?: string;
|
|
38
|
+
"aria-describedby"?: string;
|
|
39
|
+
"aria-invalid"?: true;
|
|
40
|
+
"aria-required"?: true;
|
|
41
|
+
}
|
|
42
|
+
interface ComboboxProps extends ComboboxSharedProps {
|
|
43
|
+
/** Giá trị đang chọn; `null`/`undefined` = chưa chọn */
|
|
44
|
+
value?: string | null;
|
|
45
|
+
onValueChange: (value: string | null) => void;
|
|
46
|
+
}
|
|
47
|
+
interface MultiComboboxProps extends ComboboxSharedProps {
|
|
48
|
+
value: string[];
|
|
49
|
+
onValueChange: (value: string[]) => void;
|
|
50
|
+
/** Số nhãn hiện trên trigger trước khi gộp thành "+k" (mặc định 2) */
|
|
51
|
+
maxDisplay?: number;
|
|
52
|
+
/** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */
|
|
53
|
+
maxSelected?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.
|
|
57
|
+
*
|
|
58
|
+
* Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ
|
|
59
|
+
* (app tự gọi API và cập nhật `options`).
|
|
60
|
+
*/
|
|
61
|
+
declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, onSearchChange, loading, clearable, className, contentClassName, id, name, ...aria }: ComboboxProps): React.JSX.Element;
|
|
62
|
+
/**
|
|
63
|
+
* Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.
|
|
64
|
+
*
|
|
65
|
+
* Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành "+k": danh sách 12 người thực hiện
|
|
66
|
+
* mà đổ hết ra trigger thì hàng form vỡ bố cục.
|
|
67
|
+
*/
|
|
68
|
+
declare function MultiCombobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, onSearchChange, loading, clearable, maxDisplay, maxSelected, className, contentClassName, id, name, ...aria }: MultiComboboxProps): React.JSX.Element;
|
|
69
|
+
|
|
70
|
+
export { Combobox, type ComboboxOption, type ComboboxProps, MultiCombobox, type MultiComboboxProps };
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, ChevronsUpDown, Loader2, X } from "lucide-react";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { Badge } from "../ui/badge.js";
|
|
7
|
+
import { Button } from "../ui/button.js";
|
|
8
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command.js";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
10
|
+
import { removeVietnameseTones } from "./text-utils.js";
|
|
11
|
+
function vietnameseFilter(value, search, keywords) {
|
|
12
|
+
if (!search) return 1;
|
|
13
|
+
const haystack = removeVietnameseTones(`${value} ${keywords?.join(" ") ?? ""}`).toLowerCase();
|
|
14
|
+
return haystack.includes(removeVietnameseTones(search).toLowerCase()) ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
function groupOptions(options) {
|
|
17
|
+
const groups = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const option of options) {
|
|
19
|
+
const key = option.group ?? "";
|
|
20
|
+
const bucket = groups.get(key);
|
|
21
|
+
if (bucket) bucket.push(option);
|
|
22
|
+
else groups.set(key, [option]);
|
|
23
|
+
}
|
|
24
|
+
return [...groups.entries()].map(([label, items]) => ({ label: label || void 0, items }));
|
|
25
|
+
}
|
|
26
|
+
function OptionRow({ option, selected }) {
|
|
27
|
+
const Icon = option.icon;
|
|
28
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
|
+
/* @__PURE__ */ jsx(
|
|
30
|
+
"span",
|
|
31
|
+
{
|
|
32
|
+
"aria-hidden": "true",
|
|
33
|
+
className: cn("flex size-4 shrink-0 items-center justify-center", !selected && "invisible"),
|
|
34
|
+
children: /* @__PURE__ */ jsx(Check, { className: "size-4" })
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }),
|
|
38
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
39
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate", children: option.label }),
|
|
40
|
+
option.description && /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: option.description })
|
|
41
|
+
] })
|
|
42
|
+
] });
|
|
43
|
+
}
|
|
44
|
+
function useComboboxList(options, isAsync) {
|
|
45
|
+
const groups = useMemo(() => groupOptions(options), [options]);
|
|
46
|
+
return { groups, shouldFilter: !isAsync };
|
|
47
|
+
}
|
|
48
|
+
function ComboboxPanel({
|
|
49
|
+
options,
|
|
50
|
+
isAsync,
|
|
51
|
+
loading,
|
|
52
|
+
search,
|
|
53
|
+
onSearch,
|
|
54
|
+
searchPlaceholder,
|
|
55
|
+
emptyText,
|
|
56
|
+
renderItem
|
|
57
|
+
}) {
|
|
58
|
+
const { groups, shouldFilter } = useComboboxList(options, isAsync);
|
|
59
|
+
return /* @__PURE__ */ jsxs(Command, { shouldFilter, filter: vietnameseFilter, children: [
|
|
60
|
+
/* @__PURE__ */ jsx(CommandInput, { placeholder: searchPlaceholder, value: search, onValueChange: onSearch }),
|
|
61
|
+
/* @__PURE__ */ jsx(CommandList, { children: loading ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "size-4 animate-spin" }),
|
|
63
|
+
"\u0110ang t\u1EA3i\u2026"
|
|
64
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx(CommandEmpty, { children: emptyText }),
|
|
66
|
+
groups.map((group) => /* @__PURE__ */ jsx(CommandGroup, { heading: group.label, children: group.items.map(renderItem) }, group.label ?? "__default__"))
|
|
67
|
+
] }) })
|
|
68
|
+
] });
|
|
69
|
+
}
|
|
70
|
+
function Combobox({
|
|
71
|
+
options,
|
|
72
|
+
value,
|
|
73
|
+
onValueChange,
|
|
74
|
+
placeholder = "Ch\u1ECDn\u2026",
|
|
75
|
+
searchPlaceholder = "T\xECm ki\u1EBFm\u2026",
|
|
76
|
+
emptyText = "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.",
|
|
77
|
+
disabled = false,
|
|
78
|
+
onSearchChange,
|
|
79
|
+
loading = false,
|
|
80
|
+
clearable = false,
|
|
81
|
+
className,
|
|
82
|
+
contentClassName,
|
|
83
|
+
id,
|
|
84
|
+
name,
|
|
85
|
+
...aria
|
|
86
|
+
}) {
|
|
87
|
+
const [open, setOpen] = useState(false);
|
|
88
|
+
const [search, setSearch] = useState("");
|
|
89
|
+
const isAsync = onSearchChange !== void 0;
|
|
90
|
+
const selected = options.find((option) => option.value === value);
|
|
91
|
+
const handleSearch = (next) => {
|
|
92
|
+
setSearch(next);
|
|
93
|
+
onSearchChange?.(next);
|
|
94
|
+
};
|
|
95
|
+
const showClear = clearable && Boolean(selected) && !disabled;
|
|
96
|
+
return (
|
|
97
|
+
/* Nút xóa là ANH EM của trigger, không nằm trong nó: <button> lồng <button> là HTML không
|
|
98
|
+
hợp lệ, và nếu hạ xuống <span role="button" tabIndex={-1}> thì người dùng bàn phím vĩnh
|
|
99
|
+
viễn không tới được nút xóa. Đặt tuyệt đối chồng lên phần đệm phải của trigger. */
|
|
100
|
+
/* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
|
|
101
|
+
/* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
102
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
103
|
+
Button,
|
|
104
|
+
{
|
|
105
|
+
type: "button",
|
|
106
|
+
variant: "outline",
|
|
107
|
+
role: "combobox",
|
|
108
|
+
"aria-expanded": open,
|
|
109
|
+
id,
|
|
110
|
+
name,
|
|
111
|
+
disabled,
|
|
112
|
+
className: cn(
|
|
113
|
+
"w-full justify-between gap-2 font-normal",
|
|
114
|
+
!selected && "text-muted-foreground",
|
|
115
|
+
showClear && "pr-12",
|
|
116
|
+
className
|
|
117
|
+
),
|
|
118
|
+
...aria,
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: selected ? selected.label : placeholder }),
|
|
121
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { "aria-hidden": "true", className: "size-4 shrink-0 opacity-50" })
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
) }),
|
|
125
|
+
/* @__PURE__ */ jsx(PopoverContent, { align: "start", className: cn("w-(--radix-popover-trigger-width) p-0", contentClassName), children: /* @__PURE__ */ jsx(
|
|
126
|
+
ComboboxPanel,
|
|
127
|
+
{
|
|
128
|
+
options,
|
|
129
|
+
isAsync,
|
|
130
|
+
loading,
|
|
131
|
+
search,
|
|
132
|
+
onSearch: handleSearch,
|
|
133
|
+
searchPlaceholder,
|
|
134
|
+
emptyText,
|
|
135
|
+
renderItem: (option) => /* @__PURE__ */ jsx(
|
|
136
|
+
CommandItem,
|
|
137
|
+
{
|
|
138
|
+
value: option.value,
|
|
139
|
+
keywords: [option.label, option.description ?? "", ...option.keywords ?? []],
|
|
140
|
+
disabled: option.disabled,
|
|
141
|
+
onSelect: () => {
|
|
142
|
+
onValueChange(option.value === value ? null : option.value);
|
|
143
|
+
setOpen(false);
|
|
144
|
+
setSearch("");
|
|
145
|
+
if (isAsync) onSearchChange?.("");
|
|
146
|
+
},
|
|
147
|
+
children: /* @__PURE__ */ jsx(OptionRow, { option, selected: option.value === value })
|
|
148
|
+
},
|
|
149
|
+
option.value
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
) })
|
|
153
|
+
] }),
|
|
154
|
+
showClear && /* @__PURE__ */ jsx(
|
|
155
|
+
"button",
|
|
156
|
+
{
|
|
157
|
+
type: "button",
|
|
158
|
+
"aria-label": "X\xF3a l\u1EF1a ch\u1ECDn",
|
|
159
|
+
onClick: () => onValueChange(null),
|
|
160
|
+
className: "absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground",
|
|
161
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
] })
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function MultiCombobox({
|
|
168
|
+
options,
|
|
169
|
+
value,
|
|
170
|
+
onValueChange,
|
|
171
|
+
placeholder = "Ch\u1ECDn\u2026",
|
|
172
|
+
searchPlaceholder = "T\xECm ki\u1EBFm\u2026",
|
|
173
|
+
emptyText = "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.",
|
|
174
|
+
disabled = false,
|
|
175
|
+
onSearchChange,
|
|
176
|
+
loading = false,
|
|
177
|
+
clearable = true,
|
|
178
|
+
maxDisplay = 2,
|
|
179
|
+
maxSelected,
|
|
180
|
+
className,
|
|
181
|
+
contentClassName,
|
|
182
|
+
id,
|
|
183
|
+
name,
|
|
184
|
+
...aria
|
|
185
|
+
}) {
|
|
186
|
+
const [open, setOpen] = useState(false);
|
|
187
|
+
const [search, setSearch] = useState("");
|
|
188
|
+
const isAsync = onSearchChange !== void 0;
|
|
189
|
+
const selectedOptions = options.filter((option) => value.includes(option.value));
|
|
190
|
+
const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));
|
|
191
|
+
const overflow = selectedOptions.length - shown.length;
|
|
192
|
+
const limitReached = maxSelected !== void 0 && value.length >= maxSelected;
|
|
193
|
+
const toggle = (optionValue) => {
|
|
194
|
+
if (value.includes(optionValue)) {
|
|
195
|
+
onValueChange(value.filter((item) => item !== optionValue));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (limitReached) return;
|
|
199
|
+
onValueChange([...value, optionValue]);
|
|
200
|
+
};
|
|
201
|
+
const handleSearch = (next) => {
|
|
202
|
+
setSearch(next);
|
|
203
|
+
onSearchChange?.(next);
|
|
204
|
+
};
|
|
205
|
+
const showClear = clearable && value.length > 0 && !disabled;
|
|
206
|
+
return (
|
|
207
|
+
// Cùng lý do như Combobox đơn: nút xóa là anh em của trigger để bấm được bằng bàn phím.
|
|
208
|
+
/* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
|
|
209
|
+
/* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
210
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
211
|
+
Button,
|
|
212
|
+
{
|
|
213
|
+
type: "button",
|
|
214
|
+
variant: "outline",
|
|
215
|
+
role: "combobox",
|
|
216
|
+
"aria-expanded": open,
|
|
217
|
+
id,
|
|
218
|
+
name,
|
|
219
|
+
disabled,
|
|
220
|
+
className: cn(
|
|
221
|
+
"h-auto min-h-9 w-full justify-between gap-2 py-1.5 font-normal",
|
|
222
|
+
value.length === 0 && "text-muted-foreground",
|
|
223
|
+
showClear && "pr-12",
|
|
224
|
+
className
|
|
225
|
+
),
|
|
226
|
+
...aria,
|
|
227
|
+
children: [
|
|
228
|
+
/* @__PURE__ */ jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: value.length === 0 ? placeholder : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
229
|
+
shown.map((option) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "max-w-[10rem]", children: /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label }) }, option.value)),
|
|
230
|
+
overflow > 0 && /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
|
|
231
|
+
"+",
|
|
232
|
+
overflow
|
|
233
|
+
] })
|
|
234
|
+
] }) }),
|
|
235
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { "aria-hidden": "true", className: "size-4 shrink-0 opacity-50" })
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
) }),
|
|
239
|
+
/* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: cn("w-(--radix-popover-trigger-width) p-0", contentClassName), children: [
|
|
240
|
+
/* @__PURE__ */ jsx(
|
|
241
|
+
ComboboxPanel,
|
|
242
|
+
{
|
|
243
|
+
options,
|
|
244
|
+
isAsync,
|
|
245
|
+
loading,
|
|
246
|
+
search,
|
|
247
|
+
onSearch: handleSearch,
|
|
248
|
+
searchPlaceholder,
|
|
249
|
+
emptyText,
|
|
250
|
+
renderItem: (option) => {
|
|
251
|
+
const isSelected = value.includes(option.value);
|
|
252
|
+
return /* @__PURE__ */ jsx(
|
|
253
|
+
CommandItem,
|
|
254
|
+
{
|
|
255
|
+
value: option.value,
|
|
256
|
+
keywords: [option.label, option.description ?? "", ...option.keywords ?? []],
|
|
257
|
+
disabled: option.disabled || limitReached && !isSelected,
|
|
258
|
+
onSelect: () => toggle(option.value),
|
|
259
|
+
children: /* @__PURE__ */ jsx(OptionRow, { option, selected: isSelected })
|
|
260
|
+
},
|
|
261
|
+
option.value
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
),
|
|
266
|
+
maxSelected !== void 0 && /* @__PURE__ */ jsxs("p", { className: "border-t border-border px-3 py-2 text-xs text-muted-foreground", children: [
|
|
267
|
+
"\u0110\xE3 ch\u1ECDn ",
|
|
268
|
+
value.length,
|
|
269
|
+
"/",
|
|
270
|
+
maxSelected
|
|
271
|
+
] })
|
|
272
|
+
] })
|
|
273
|
+
] }),
|
|
274
|
+
showClear && /* @__PURE__ */ jsx(
|
|
275
|
+
"button",
|
|
276
|
+
{
|
|
277
|
+
type: "button",
|
|
278
|
+
"aria-label": "X\xF3a t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
|
|
279
|
+
onClick: () => onValueChange([]),
|
|
280
|
+
className: "absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground",
|
|
281
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
] })
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
Combobox,
|
|
289
|
+
MultiCombobox
|
|
290
|
+
};
|
|
291
|
+
//# sourceMappingURL=combobox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/combobox.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, ChevronsUpDown, Loader2, X } from \"lucide-react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Dòng phụ mờ dưới nhãn (vd email của người dùng, mã của khách hàng) */\n description?: string;\n icon?: IconComponent;\n disabled?: boolean;\n /** Nhãn nhóm — các mục cùng nhóm được gom lại và có tiêu đề nhóm */\n group?: string;\n /** Từ khoá phụ để tìm ra mục này (vd mã số, tên viết tắt) */\n keywords?: string[];\n}\n\ninterface ComboboxSharedProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n disabled?: boolean;\n /**\n * Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).\n * Không truyền = lọc tại chỗ, không dấu tiếng Việt.\n */\n onSearchChange?: (search: string) => void;\n /** Đang tải kết quả (chỉ có nghĩa ở chế độ bất đồng bộ) */\n loading?: boolean;\n /** Cho phép xoá lựa chọn bằng nút X trên trigger */\n clearable?: boolean;\n className?: string;\n /** className của panel popover (vd đặt bề rộng cố định) */\n contentClassName?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\nexport interface ComboboxProps extends ComboboxSharedProps {\n /** Giá trị đang chọn; `null`/`undefined` = chưa chọn */\n value?: string | null;\n onValueChange: (value: string | null) => void;\n}\n\nexport interface MultiComboboxProps extends ComboboxSharedProps {\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Số nhãn hiện trên trigger trước khi gộp thành \"+k\" (mặc định 2) */\n maxDisplay?: number;\n /** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */\n maxSelected?: number;\n}\n\n/**\n * Lọc không dấu cho cmdk: so khớp cả value, label lẫn keywords.\n *\n * cmdk mặc định chấm điểm theo thuật toán riêng có phân biệt dấu — người Việt gõ \"nguyen\"\n * sẽ không ra \"Nguyễn\". Trả 1/0 là đủ vì thứ tự hiển thị đã theo thứ tự app truyền vào.\n */\nfunction vietnameseFilter(value: string, search: string, keywords?: string[]): number {\n if (!search) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(search).toLowerCase()) ? 1 : 0;\n}\n\n/** Gom mục theo `group`, giữ nguyên thứ tự xuất hiện đầu tiên của mỗi nhóm. */\nfunction groupOptions(options: ComboboxOption[]): Array<{ label: string | undefined; items: ComboboxOption[] }> {\n const groups = new Map<string, ComboboxOption[]>();\n for (const option of options) {\n const key = option.group ?? \"\";\n const bucket = groups.get(key);\n if (bucket) bucket.push(option);\n else groups.set(key, [option]);\n }\n return [...groups.entries()].map(([label, items]) => ({ label: label || undefined, items }));\n}\n\nfunction OptionRow({ option, selected }: { option: ComboboxOption; selected: boolean }) {\n const Icon = option.icon;\n return (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\"flex size-4 shrink-0 items-center justify-center\", !selected && \"invisible\")}\n >\n <Check className=\"size-4\" />\n </span>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{option.label}</span>\n {option.description && (\n <span className=\"block truncate text-xs text-muted-foreground\">{option.description}</span>\n )}\n </span>\n </>\n );\n}\n\nfunction useComboboxList(options: ComboboxOption[], isAsync: boolean) {\n const groups = useMemo(() => groupOptions(options), [options]);\n return { groups, shouldFilter: !isAsync };\n}\n\nfunction ComboboxPanel({\n options,\n isAsync,\n loading,\n search,\n onSearch,\n searchPlaceholder,\n emptyText,\n renderItem\n}: {\n options: ComboboxOption[];\n isAsync: boolean;\n loading: boolean;\n search: string;\n onSearch: (value: string) => void;\n searchPlaceholder: string;\n emptyText: string;\n renderItem: (option: ComboboxOption) => ReactNode;\n}) {\n const { groups, shouldFilter } = useComboboxList(options, isAsync);\n\n return (\n <Command shouldFilter={shouldFilter} filter={vietnameseFilter}>\n <CommandInput placeholder={searchPlaceholder} value={search} onValueChange={onSearch} />\n <CommandList>\n {loading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground\">\n <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n Đang tải…\n </div>\n ) : (\n <>\n <CommandEmpty>{emptyText}</CommandEmpty>\n {groups.map((group) => (\n <CommandGroup key={group.label ?? \"__default__\"} heading={group.label}>\n {group.items.map(renderItem)}\n </CommandGroup>\n ))}\n </>\n )}\n </CommandList>\n </Command>\n );\n}\n\n/**\n * Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.\n *\n * Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ\n * (app tự gọi API và cập nhật `options`).\n */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const selected = options.find((option) => option.value === value);\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && Boolean(selected) && !disabled;\n\n return (\n /* Nút xóa là ANH EM của trigger, không nằm trong nó: <button> lồng <button> là HTML không\n hợp lệ, và nếu hạ xuống <span role=\"button\" tabIndex={-1}> thì người dùng bàn phím vĩnh\n viễn không tới được nút xóa. Đặt tuyệt đối chồng lên phần đệm phải của trigger. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"w-full justify-between gap-2 font-normal\",\n !selected && \"text-muted-foreground\",\n showClear && \"pr-12\",\n className\n )}\n {...aria}\n >\n <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled}\n onSelect={() => {\n onValueChange(option.value === value ? null : option.value);\n setOpen(false);\n setSearch(\"\");\n if (isAsync) onSearchChange?.(\"\");\n }}\n >\n <OptionRow option={option} selected={option.value === value} />\n </CommandItem>\n )}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa lựa chọn\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n\n/**\n * Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.\n *\n * Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành \"+k\": danh sách 12 người thực hiện\n * mà đổ hết ra trigger thì hàng form vỡ bố cục.\n */\nexport function MultiCombobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n\n const selectedOptions = options.filter((option) => value.includes(option.value));\n const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));\n const overflow = selectedOptions.length - shown.length;\n const limitReached = maxSelected !== undefined && value.length >= maxSelected;\n\n const toggle = (optionValue: string) => {\n if (value.includes(optionValue)) {\n onValueChange(value.filter((item) => item !== optionValue));\n return;\n }\n if (limitReached) return;\n onValueChange([...value, optionValue]);\n };\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && value.length > 0 && !disabled;\n\n return (\n // Cùng lý do như Combobox đơn: nút xóa là anh em của trigger để bấm được bằng bàn phím.\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"h-auto min-h-9 w-full justify-between gap-2 py-1.5 font-normal\",\n value.length === 0 && \"text-muted-foreground\",\n showClear && \"pr-12\",\n className\n )}\n {...aria}\n >\n <span className=\"flex min-w-0 flex-wrap items-center gap-1\">\n {value.length === 0 ? (\n placeholder\n ) : (\n <>\n {shown.map((option) => (\n <Badge key={option.value} variant=\"secondary\" className=\"max-w-[10rem]\">\n <span className=\"truncate\">{option.label}</span>\n </Badge>\n ))}\n {overflow > 0 && <Badge variant=\"outline\">+{overflow}</Badge>}\n </>\n )}\n </span>\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => {\n const isSelected = value.includes(option.value);\n return (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled || (limitReached && !isSelected)}\n onSelect={() => toggle(option.value)}\n >\n <OptionRow option={option} selected={isSelected} />\n </CommandItem>\n );\n }}\n />\n {maxSelected !== undefined && (\n <p className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n Đã chọn {value.length}/{maxSelected}\n </p>\n )}\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa tất cả lựa chọn\"\n onClick={() => onValueChange([])}\n className=\"absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { IconComponent } from './empty-state.js';
|
|
4
|
+
|
|
5
|
+
interface CommandPaletteItem {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Dòng phụ mờ (đường dẫn, mô tả ngắn, tên workspace) */
|
|
9
|
+
description?: string;
|
|
10
|
+
icon?: IconComponent;
|
|
11
|
+
/** Gợi ý phím tắt hiện bên phải, vd "mod+shift+n" */
|
|
12
|
+
shortcut?: string;
|
|
13
|
+
/** Từ khoá phụ để tìm ra mục (tên tiếng Anh, viết tắt, mã) */
|
|
14
|
+
keywords?: string[];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
onSelect: () => void;
|
|
17
|
+
}
|
|
18
|
+
interface CommandPaletteGroup {
|
|
19
|
+
id: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
items: CommandPaletteItem[];
|
|
22
|
+
}
|
|
23
|
+
interface CommandPaletteProps {
|
|
24
|
+
open: boolean;
|
|
25
|
+
onOpenChange: (open: boolean) => void;
|
|
26
|
+
groups: CommandPaletteGroup[];
|
|
27
|
+
/** Nhóm "Gần đây" luôn nằm trên cùng khi chưa gõ gì */
|
|
28
|
+
recent?: CommandPaletteItem[];
|
|
29
|
+
recentLabel?: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
emptyText?: string;
|
|
32
|
+
/** Có mặt = app tự tìm (gọi API) → tắt lọc nội bộ của cmdk */
|
|
33
|
+
onSearchChange?: (search: string) => void;
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
/** Nội dung chân bảng (gợi ý phím, trạng thái) */
|
|
36
|
+
footer?: ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Tự gắn phím tắt Cmd/Ctrl+K để mở (mặc định true).
|
|
39
|
+
*
|
|
40
|
+
* Shell của Comitor cũng gắn phím tắt này; khi dùng trong shell hãy đặt `false` để tránh
|
|
41
|
+
* hai listener cùng lật một state (mở rồi đóng ngay trong một lần bấm).
|
|
42
|
+
*/
|
|
43
|
+
registerShortcut?: boolean;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bảng lệnh (Cmd/Ctrl+K) — điều hướng nhanh và chạy lệnh, kiểu Linear/Raycast.
|
|
48
|
+
*
|
|
49
|
+
* Đặt ở tầng composite (không phải trong shell) vì app không dùng `AppShell` — trang cài đặt
|
|
50
|
+
* độc lập, công cụ nội bộ — vẫn cần bảng lệnh. Shell chỉ giữ state mở/đóng và truyền xuống.
|
|
51
|
+
*
|
|
52
|
+
* Tìm kiếm mặc định KHÔNG DẤU tiếng Việt: gõ "tao viec" ra "Tạo việc mới".
|
|
53
|
+
*/
|
|
54
|
+
declare function CommandPalette({ open, onOpenChange, groups, recent, recentLabel, placeholder, emptyText, onSearchChange, loading, footer, registerShortcut, className }: CommandPaletteProps): React.JSX.Element;
|
|
55
|
+
|
|
56
|
+
export { CommandPalette, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteProps };
|