@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,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
4
|
+
import { Loader2, Search } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "../ui/command.js";
|
|
8
|
+
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog.js";
|
|
9
|
+
import { KeyboardHint } from "./keyboard-hint.js";
|
|
10
|
+
import { removeVietnameseTones } from "./text-utils.js";
|
|
11
|
+
function PaletteInput({
|
|
12
|
+
value,
|
|
13
|
+
onValueChange,
|
|
14
|
+
placeholder,
|
|
15
|
+
loading
|
|
16
|
+
}) {
|
|
17
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-12 items-center gap-2 border-b border-border px-3", children: [
|
|
18
|
+
loading ? /* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "size-4 shrink-0 animate-spin text-muted-foreground" }) : /* @__PURE__ */ jsx(Search, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }),
|
|
19
|
+
/* @__PURE__ */ jsx(
|
|
20
|
+
CommandPrimitive.Input,
|
|
21
|
+
{
|
|
22
|
+
value,
|
|
23
|
+
onValueChange,
|
|
24
|
+
placeholder,
|
|
25
|
+
"aria-label": placeholder,
|
|
26
|
+
className: "flex h-11 w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground"
|
|
27
|
+
}
|
|
28
|
+
),
|
|
29
|
+
/* @__PURE__ */ jsx(KeyboardHint, { keys: "esc", className: "hidden sm:inline-flex" })
|
|
30
|
+
] });
|
|
31
|
+
}
|
|
32
|
+
function PaletteRow({ item }) {
|
|
33
|
+
const Icon = item.icon;
|
|
34
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
35
|
+
Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }),
|
|
36
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
37
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate", children: item.label }),
|
|
38
|
+
item.description && /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: item.description })
|
|
39
|
+
] }),
|
|
40
|
+
item.shortcut && /* @__PURE__ */ jsx(KeyboardHint, { keys: item.shortcut, className: "ml-auto" })
|
|
41
|
+
] });
|
|
42
|
+
}
|
|
43
|
+
function CommandPalette({
|
|
44
|
+
open,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
groups,
|
|
47
|
+
recent = [],
|
|
48
|
+
recentLabel = "G\u1EA7n \u0111\xE2y",
|
|
49
|
+
placeholder = "T\xECm l\u1EC7nh, trang, ng\u01B0\u1EDDi\u2026",
|
|
50
|
+
emptyText = "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.",
|
|
51
|
+
onSearchChange,
|
|
52
|
+
loading = false,
|
|
53
|
+
footer,
|
|
54
|
+
registerShortcut = true,
|
|
55
|
+
className
|
|
56
|
+
}) {
|
|
57
|
+
const [search, setSearch] = useState("");
|
|
58
|
+
const isAsync = onSearchChange !== void 0;
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) setSearch("");
|
|
61
|
+
}, [open]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!registerShortcut) return;
|
|
64
|
+
const onKeyDown = (event) => {
|
|
65
|
+
if (event.key !== "k" || !(event.metaKey || event.ctrlKey)) return;
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
onOpenChange(!open);
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("keydown", onKeyDown);
|
|
70
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
71
|
+
}, [registerShortcut, open, onOpenChange]);
|
|
72
|
+
const handleSearch = (value) => {
|
|
73
|
+
setSearch(value);
|
|
74
|
+
onSearchChange?.(value);
|
|
75
|
+
};
|
|
76
|
+
const runItem = (item) => {
|
|
77
|
+
if (item.disabled) return;
|
|
78
|
+
onOpenChange(false);
|
|
79
|
+
item.onSelect();
|
|
80
|
+
};
|
|
81
|
+
const showRecent = recent.length > 0 && search.trim() === "";
|
|
82
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { size: "lg", showCloseButton: false, className: cn("overflow-hidden p-0", className), children: [
|
|
83
|
+
/* @__PURE__ */ jsx(DialogTitle, { className: "sr-only", children: "B\u1EA3ng l\u1EC7nh" }),
|
|
84
|
+
/* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: "G\xF5 \u0111\u1EC3 t\xECm l\u1EC7nh, trang ho\u1EB7c ng\u01B0\u1EDDi; d\xF9ng m\u0169i t\xEAn \u0111\u1EC3 ch\u1ECDn v\xE0 Enter \u0111\u1EC3 ch\u1EA1y." }),
|
|
85
|
+
/* @__PURE__ */ jsxs(
|
|
86
|
+
Command,
|
|
87
|
+
{
|
|
88
|
+
shouldFilter: !isAsync,
|
|
89
|
+
filter: (value, query, keywords) => {
|
|
90
|
+
if (!query) return 1;
|
|
91
|
+
const haystack = removeVietnameseTones(`${value} ${keywords?.join(" ") ?? ""}`).toLowerCase();
|
|
92
|
+
return haystack.includes(removeVietnameseTones(query).toLowerCase()) ? 1 : 0;
|
|
93
|
+
},
|
|
94
|
+
className: "bg-transparent",
|
|
95
|
+
children: [
|
|
96
|
+
/* @__PURE__ */ jsx(PaletteInput, { value: search, onValueChange: handleSearch, placeholder, loading }),
|
|
97
|
+
/* @__PURE__ */ jsxs(CommandList, { className: "max-h-[22rem]", children: [
|
|
98
|
+
!loading && /* @__PURE__ */ jsx(CommandEmpty, { children: emptyText }),
|
|
99
|
+
showRecent && /* @__PURE__ */ jsx(CommandGroup, { heading: recentLabel, children: recent.map((item) => /* @__PURE__ */ jsx(
|
|
100
|
+
CommandItem,
|
|
101
|
+
{
|
|
102
|
+
value: `recent-${item.id}`,
|
|
103
|
+
keywords: [item.label, item.description ?? "", ...item.keywords ?? []],
|
|
104
|
+
disabled: item.disabled,
|
|
105
|
+
onSelect: () => runItem(item),
|
|
106
|
+
children: /* @__PURE__ */ jsx(PaletteRow, { item })
|
|
107
|
+
},
|
|
108
|
+
`recent-${item.id}`
|
|
109
|
+
)) }),
|
|
110
|
+
groups.map((group) => /* @__PURE__ */ jsx(CommandGroup, { heading: group.label, children: group.items.map((item) => /* @__PURE__ */ jsx(
|
|
111
|
+
CommandItem,
|
|
112
|
+
{
|
|
113
|
+
value: item.id,
|
|
114
|
+
keywords: [item.label, item.description ?? "", ...item.keywords ?? []],
|
|
115
|
+
disabled: item.disabled,
|
|
116
|
+
onSelect: () => runItem(item),
|
|
117
|
+
children: /* @__PURE__ */ jsx(PaletteRow, { item })
|
|
118
|
+
},
|
|
119
|
+
item.id
|
|
120
|
+
)) }, group.id))
|
|
121
|
+
] }),
|
|
122
|
+
footer && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 border-t border-border px-3 py-2 text-xs text-muted-foreground", children: footer })
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
] }) });
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
CommandPalette
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=command-palette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/command-palette.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Loader2, Search } from \"lucide-react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogTitle } from \"../ui/dialog.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { KeyboardHint } from \"./keyboard-hint.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface CommandPaletteItem {\n id: string;\n label: string;\n /** Dòng phụ mờ (đường dẫn, mô tả ngắn, tên workspace) */\n description?: string;\n icon?: IconComponent;\n /** Gợi ý phím tắt hiện bên phải, vd \"mod+shift+n\" */\n shortcut?: string;\n /** Từ khoá phụ để tìm ra mục (tên tiếng Anh, viết tắt, mã) */\n keywords?: string[];\n disabled?: boolean;\n onSelect: () => void;\n}\n\nexport interface CommandPaletteGroup {\n id: string;\n label?: string;\n items: CommandPaletteItem[];\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n groups: CommandPaletteGroup[];\n /** Nhóm \"Gần đây\" luôn nằm trên cùng khi chưa gõ gì */\n recent?: CommandPaletteItem[];\n recentLabel?: string;\n placeholder?: string;\n emptyText?: string;\n /** Có mặt = app tự tìm (gọi API) → tắt lọc nội bộ của cmdk */\n onSearchChange?: (search: string) => void;\n loading?: boolean;\n /** Nội dung chân bảng (gợi ý phím, trạng thái) */\n footer?: ReactNode;\n /**\n * Tự gắn phím tắt Cmd/Ctrl+K để mở (mặc định true).\n *\n * 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\n * hai listener cùng lật một state (mở rồi đóng ngay trong một lần bấm).\n */\n registerShortcut?: boolean;\n className?: string;\n}\n\n/**\n * Ô nhập của palette — dựng thẳng trên `cmdk` thay vì dùng `CommandInput` của tầng primitive.\n *\n * Lý do: cần thay icon kính lúp bằng vòng xoay khi đang tìm phía server, và cần chỗ đặt gợi ý\n * phím Esc bên phải — `CommandInput` khoá cứng cả hai chi tiết đó.\n */\nfunction PaletteInput({\n value,\n onValueChange,\n placeholder,\n loading\n}: {\n value: string;\n onValueChange: (value: string) => void;\n placeholder: string;\n loading: boolean;\n}) {\n return (\n <div className=\"flex h-12 items-center gap-2 border-b border-border px-3\">\n {loading ? (\n <Loader2 aria-hidden=\"true\" className=\"size-4 shrink-0 animate-spin text-muted-foreground\" />\n ) : (\n <Search aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n )}\n <CommandPrimitive.Input\n value={value}\n onValueChange={onValueChange}\n placeholder={placeholder}\n aria-label={placeholder}\n className=\"flex h-11 w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground\"\n />\n <KeyboardHint keys=\"esc\" className=\"hidden sm:inline-flex\" />\n </div>\n );\n}\n\nfunction PaletteRow({ item }: { item: CommandPaletteItem }) {\n const Icon = item.icon;\n return (\n <>\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\">{item.label}</span>\n {item.description && <span className=\"block truncate text-xs text-muted-foreground\">{item.description}</span>}\n </span>\n {item.shortcut && <KeyboardHint keys={item.shortcut} className=\"ml-auto\" />}\n </>\n );\n}\n\n/**\n * Bảng lệnh (Cmd/Ctrl+K) — điều hướng nhanh và chạy lệnh, kiểu Linear/Raycast.\n *\n * Đặt ở tầng composite (không phải trong shell) vì app không dùng `AppShell` — trang cài đặt\n * độ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.\n *\n * 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\".\n */\nexport function CommandPalette({\n open,\n onOpenChange,\n groups,\n recent = [],\n recentLabel = \"Gần đây\",\n placeholder = \"Tìm lệnh, trang, người…\",\n emptyText = \"Không tìm thấy kết quả.\",\n onSearchChange,\n loading = false,\n footer,\n registerShortcut = true,\n className\n}: CommandPaletteProps) {\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n\n // Reset ô tìm mỗi lần mở: mở lại palette mà còn nguyên từ khoá cũ là hành vi gây bất ngờ.\n useEffect(() => {\n if (!open) setSearch(\"\");\n }, [open]);\n\n useEffect(() => {\n if (!registerShortcut) return;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"k\" || !(event.metaKey || event.ctrlKey)) return;\n event.preventDefault();\n onOpenChange(!open);\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [registerShortcut, open, onOpenChange]);\n\n const handleSearch = (value: string) => {\n setSearch(value);\n onSearchChange?.(value);\n };\n\n const runItem = (item: CommandPaletteItem) => {\n if (item.disabled) return;\n onOpenChange(false);\n item.onSelect();\n };\n\n const showRecent = recent.length > 0 && search.trim() === \"\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent size=\"lg\" showCloseButton={false} className={cn(\"overflow-hidden p-0\", className)}>\n <DialogTitle className=\"sr-only\">Bảng lệnh</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Gõ để tìm lệnh, trang hoặc người; dùng mũi tên để chọn và Enter để chạy.\n </DialogDescription>\n\n <Command\n shouldFilter={!isAsync}\n filter={(value, query, keywords) => {\n if (!query) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(query).toLowerCase()) ? 1 : 0;\n }}\n className=\"bg-transparent\"\n >\n <PaletteInput value={search} onValueChange={handleSearch} placeholder={placeholder} loading={loading} />\n\n <CommandList className=\"max-h-[22rem]\">\n {!loading && <CommandEmpty>{emptyText}</CommandEmpty>}\n\n {showRecent && (\n <CommandGroup heading={recentLabel}>\n {recent.map((item) => (\n <CommandItem\n key={`recent-${item.id}`}\n value={`recent-${item.id}`}\n keywords={[item.label, item.description ?? \"\", ...(item.keywords ?? [])]}\n disabled={item.disabled}\n onSelect={() => runItem(item)}\n >\n <PaletteRow item={item} />\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {groups.map((group) => (\n <CommandGroup key={group.id} heading={group.label}>\n {group.items.map((item) => (\n <CommandItem\n key={item.id}\n value={item.id}\n keywords={[item.label, item.description ?? \"\", ...(item.keywords ?? [])]}\n disabled={item.disabled}\n onSelect={() => runItem(item)}\n >\n <PaletteRow item={item} />\n </CommandItem>\n ))}\n </CommandGroup>\n ))}\n </CommandList>\n\n {footer && (\n <div className=\"flex items-center gap-2 border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n {footer}\n </div>\n )}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { IconComponent } from './empty-state.js';
|
|
4
|
+
|
|
5
|
+
interface ConfirmDialogProps {
|
|
6
|
+
/** Controlled — bỏ trống thì dialog tự quản trạng thái mở (dùng kèm `trigger`) */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
/** Phần tử mở dialog (đặt trong `AlertDialogTrigger asChild`) */
|
|
10
|
+
trigger?: ReactNode;
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
description?: ReactNode;
|
|
13
|
+
/** Nội dung phụ (danh sách bản ghi sẽ bị ảnh hưởng, ô nhập lý do…) */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
icon?: IconComponent;
|
|
16
|
+
confirmLabel?: string;
|
|
17
|
+
cancelLabel?: string;
|
|
18
|
+
/** "destructive" tô nút xác nhận màu đỏ — dùng cho xoá/thu hồi */
|
|
19
|
+
variant?: "default" | "destructive";
|
|
20
|
+
/**
|
|
21
|
+
* Hành động khi xác nhận. Trả về Promise thì dialog giữ nguyên và bật trạng thái đang chạy
|
|
22
|
+
* cho tới khi Promise xong — đóng ngay rồi mới gọi API là cách chắc chắn nhất để người dùng
|
|
23
|
+
* bấm xoá hai lần.
|
|
24
|
+
*/
|
|
25
|
+
onConfirm: () => void | Promise<void>;
|
|
26
|
+
/** Khoá nút xác nhận (vd chưa gõ đúng tên bản ghi cần xoá) */
|
|
27
|
+
confirmDisabled?: boolean;
|
|
28
|
+
/** Nhận lỗi từ `onConfirm` để app hiện toast; không truyền thì lỗi được ghi ra console */
|
|
29
|
+
onError?: (error: unknown) => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Hộp thoại xác nhận cho hành động không hoàn tác được (xoá việc, gỡ thành viên, huỷ gói).
|
|
34
|
+
*
|
|
35
|
+
* Bọc `AlertDialog` để mọi app có cùng một cách hỏi lại; nút xác nhận tự vào trạng thái
|
|
36
|
+
* "Đang xử lý…" khi `onConfirm` bất đồng bộ, và lỗi ném ra sẽ giữ dialog mở để người dùng thử lại.
|
|
37
|
+
*/
|
|
38
|
+
declare function ConfirmDialog({ open, onOpenChange, trigger, title, description, children, icon: Icon, confirmLabel, cancelLabel, variant, onConfirm, confirmDisabled, onError, className }: ConfirmDialogProps): React.JSX.Element;
|
|
39
|
+
|
|
40
|
+
export { ConfirmDialog, type ConfirmDialogProps };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogAction,
|
|
8
|
+
AlertDialogCancel,
|
|
9
|
+
AlertDialogContent,
|
|
10
|
+
AlertDialogDescription,
|
|
11
|
+
AlertDialogFooter,
|
|
12
|
+
AlertDialogHeader,
|
|
13
|
+
AlertDialogTitle,
|
|
14
|
+
AlertDialogTrigger
|
|
15
|
+
} from "../ui/alert-dialog.js";
|
|
16
|
+
import { buttonVariants } from "../ui/button.js";
|
|
17
|
+
function ConfirmDialog({
|
|
18
|
+
open,
|
|
19
|
+
onOpenChange,
|
|
20
|
+
trigger,
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
children,
|
|
24
|
+
icon: Icon,
|
|
25
|
+
confirmLabel = "X\xE1c nh\u1EADn",
|
|
26
|
+
cancelLabel = "H\u1EE7y",
|
|
27
|
+
variant = "default",
|
|
28
|
+
onConfirm,
|
|
29
|
+
confirmDisabled = false,
|
|
30
|
+
onError,
|
|
31
|
+
className
|
|
32
|
+
}) {
|
|
33
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
34
|
+
const [pending, setPending] = useState(false);
|
|
35
|
+
const isOpen = open ?? internalOpen;
|
|
36
|
+
const setOpen = (next) => {
|
|
37
|
+
if (pending && !next) return;
|
|
38
|
+
if (open === void 0) setInternalOpen(next);
|
|
39
|
+
onOpenChange?.(next);
|
|
40
|
+
};
|
|
41
|
+
const handleConfirm = async () => {
|
|
42
|
+
try {
|
|
43
|
+
setPending(true);
|
|
44
|
+
await onConfirm();
|
|
45
|
+
setPending(false);
|
|
46
|
+
if (open === void 0) setInternalOpen(false);
|
|
47
|
+
onOpenChange?.(false);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
setPending(false);
|
|
50
|
+
if (onError) onError(error);
|
|
51
|
+
else console.error("[ConfirmDialog] onConfirm th\u1EA5t b\u1EA1i:", error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return /* @__PURE__ */ jsxs(AlertDialog, { open: isOpen, onOpenChange: setOpen, children: [
|
|
55
|
+
trigger && /* @__PURE__ */ jsx(AlertDialogTrigger, { asChild: true, children: trigger }),
|
|
56
|
+
/* @__PURE__ */ jsxs(AlertDialogContent, { className, children: [
|
|
57
|
+
/* @__PURE__ */ jsx(AlertDialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
58
|
+
Icon && /* @__PURE__ */ jsx(
|
|
59
|
+
"span",
|
|
60
|
+
{
|
|
61
|
+
"aria-hidden": "true",
|
|
62
|
+
className: cn(
|
|
63
|
+
"flex size-9 shrink-0 items-center justify-center rounded-md",
|
|
64
|
+
variant === "destructive" ? "bg-destructive/15 text-destructive" : "bg-muted text-muted-foreground"
|
|
65
|
+
),
|
|
66
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "size-5" })
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1.5", children: [
|
|
70
|
+
/* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
|
|
71
|
+
description && /* @__PURE__ */ jsx(AlertDialogDescription, { children: description })
|
|
72
|
+
] })
|
|
73
|
+
] }) }),
|
|
74
|
+
children,
|
|
75
|
+
/* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
|
|
76
|
+
/* @__PURE__ */ jsx(AlertDialogCancel, { disabled: pending, children: cancelLabel }),
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
AlertDialogAction,
|
|
79
|
+
{
|
|
80
|
+
disabled: pending || confirmDisabled,
|
|
81
|
+
onClick: (event) => {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
void handleConfirm();
|
|
84
|
+
},
|
|
85
|
+
className: cn(variant === "destructive" && buttonVariants({ variant: "destructive" })),
|
|
86
|
+
children: pending ? "\u0110ang x\u1EED l\xFD\u2026" : confirmLabel
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
] })
|
|
90
|
+
] })
|
|
91
|
+
] });
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
ConfirmDialog
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=confirm-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/confirm-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger\n} from \"../ui/alert-dialog.js\";\nimport { buttonVariants } from \"../ui/button.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport interface ConfirmDialogProps {\n /** Controlled — bỏ trống thì dialog tự quản trạng thái mở (dùng kèm `trigger`) */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Phần tử mở dialog (đặt trong `AlertDialogTrigger asChild`) */\n trigger?: ReactNode;\n\n title: ReactNode;\n description?: ReactNode;\n /** Nội dung phụ (danh sách bản ghi sẽ bị ảnh hưởng, ô nhập lý do…) */\n children?: ReactNode;\n icon?: IconComponent;\n\n confirmLabel?: string;\n cancelLabel?: string;\n /** \"destructive\" tô nút xác nhận màu đỏ — dùng cho xoá/thu hồi */\n variant?: \"default\" | \"destructive\";\n /**\n * Hành động khi xác nhận. Trả về Promise thì dialog giữ nguyên và bật trạng thái đang chạy\n * cho tới khi Promise xong — đóng ngay rồi mới gọi API là cách chắc chắn nhất để người dùng\n * bấm xoá hai lần.\n */\n onConfirm: () => void | Promise<void>;\n /** Khoá nút xác nhận (vd chưa gõ đúng tên bản ghi cần xoá) */\n confirmDisabled?: boolean;\n /** Nhận lỗi từ `onConfirm` để app hiện toast; không truyền thì lỗi được ghi ra console */\n onError?: (error: unknown) => void;\n className?: string;\n}\n\n/**\n * Hộp thoại xác nhận cho hành động không hoàn tác được (xoá việc, gỡ thành viên, huỷ gói).\n *\n * Bọc `AlertDialog` để mọi app có cùng một cách hỏi lại; nút xác nhận tự vào trạng thái\n * \"Đang xử lý…\" khi `onConfirm` bất đồng bộ, và lỗi ném ra sẽ giữ dialog mở để người dùng thử lại.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n trigger,\n title,\n description,\n children,\n icon: Icon,\n confirmLabel = \"Xác nhận\",\n cancelLabel = \"Hủy\",\n variant = \"default\",\n onConfirm,\n confirmDisabled = false,\n onError,\n className\n}: ConfirmDialogProps) {\n const [internalOpen, setInternalOpen] = useState(false);\n const [pending, setPending] = useState(false);\n\n const isOpen = open ?? internalOpen;\n const setOpen = (next: boolean) => {\n if (pending && !next) return; // đang chạy thì không cho đóng ngang\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const handleConfirm = async () => {\n try {\n setPending(true);\n await onConfirm();\n setPending(false);\n if (open === undefined) setInternalOpen(false);\n onOpenChange?.(false);\n } catch (error) {\n // Giữ dialog mở để người dùng thấy toast lỗi của app và thử lại.\n // KHÔNG ném tiếp: lời gọi là `void handleConfirm()` trong handler nên ném ra chỉ tạo\n // unhandled rejection mà app không bắt được — lỗi đi ra ngoài qua `onError`.\n setPending(false);\n if (onError) onError(error);\n else console.error(\"[ConfirmDialog] onConfirm thất bại:\", error);\n }\n };\n\n return (\n <AlertDialog open={isOpen} onOpenChange={setOpen}>\n {trigger && <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>}\n <AlertDialogContent className={className}>\n <AlertDialogHeader>\n <div className=\"flex items-start gap-3\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"flex size-9 shrink-0 items-center justify-center rounded-md\",\n variant === \"destructive\" ? \"bg-destructive/15 text-destructive\" : \"bg-muted text-muted-foreground\"\n )}\n >\n <Icon className=\"size-5\" />\n </span>\n )}\n <div className=\"min-w-0 space-y-1.5\">\n <AlertDialogTitle>{title}</AlertDialogTitle>\n {description && <AlertDialogDescription>{description}</AlertDialogDescription>}\n </div>\n </div>\n </AlertDialogHeader>\n\n {children}\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={pending}>{cancelLabel}</AlertDialogCancel>\n <AlertDialogAction\n disabled={pending || confirmDisabled}\n onClick={(event) => {\n // Chặn hành vi tự đóng của Radix: dialog chỉ đóng sau khi onConfirm chạy xong.\n event.preventDefault();\n void handleConfirm();\n }}\n className={cn(variant === \"destructive\" && buttonVariants({ variant: \"destructive\" }))}\n >\n {pending ? \"Đang xử lý…\" : confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":";AAmGkB,cAeN,YAfM;AAjGlB,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAuCxB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,eAAe;AAAA,EACf,cAAc;AAAA,EACd,UAAU;AAAA,EACV;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,QAAM,SAAS,QAAQ;AACvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,WAAW,CAAC,KAAM;AACtB,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,gBAAgB,YAAY;AAChC,QAAI;AACF,iBAAW,IAAI;AACf,YAAM,UAAU;AAChB,iBAAW,KAAK;AAChB,UAAI,SAAS,OAAW,iBAAgB,KAAK;AAC7C,qBAAe,KAAK;AAAA,IACtB,SAAS,OAAO;AAId,iBAAW,KAAK;AAChB,UAAI,QAAS,SAAQ,KAAK;AAAA,UACrB,SAAQ,MAAM,iDAAuC,KAAK;AAAA,IACjE;AAAA,EACF;AAEA,SACE,qBAAC,eAAY,MAAM,QAAQ,cAAc,SACtC;AAAA,eAAW,oBAAC,sBAAmB,SAAO,MAAE,mBAAQ;AAAA,IACjD,qBAAC,sBAAmB,WAClB;AAAA,0BAAC,qBACC,+BAAC,SAAI,WAAU,0BACZ;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,YAAY,gBAAgB,uCAAuC;AAAA,YACrE;AAAA,YAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,QAC3B;AAAA,QAEF,qBAAC,SAAI,WAAU,uBACb;AAAA,8BAAC,oBAAkB,iBAAM;AAAA,UACxB,eAAe,oBAAC,0BAAwB,uBAAY;AAAA,WACvD;AAAA,SACF,GACF;AAAA,MAEC;AAAA,MAED,qBAAC,qBACC;AAAA,4BAAC,qBAAkB,UAAU,SAAU,uBAAY;AAAA,QACnD;AAAA,UAAC;AAAA;AAAA,YACC,UAAU,WAAW;AAAA,YACrB,SAAS,CAAC,UAAU;AAElB,oBAAM,eAAe;AACrB,mBAAK,cAAc;AAAA,YACrB;AAAA,YACA,WAAW,GAAG,YAAY,iBAAiB,eAAe,EAAE,SAAS,cAAc,CAAC,CAAC;AAAA,YAEpF,oBAAU,kCAAgB;AAAA;AAAA,QAC7B;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ButtonProps } from '../ui/button.js';
|
|
3
|
+
import 'class-variance-authority/types';
|
|
4
|
+
import 'class-variance-authority';
|
|
5
|
+
|
|
6
|
+
interface CopyButtonProps extends Omit<ButtonProps, "children" | "onCopy"> {
|
|
7
|
+
/** Nội dung được chép vào clipboard */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Nhãn khi chưa chép (mặc định "Sao chép"). `null` = chỉ icon. */
|
|
10
|
+
label?: string | null;
|
|
11
|
+
/** Nhãn sau khi chép thành công */
|
|
12
|
+
copiedLabel?: string;
|
|
13
|
+
/** Thời gian giữ trạng thái "đã chép" (ms) */
|
|
14
|
+
resetAfterMs?: number;
|
|
15
|
+
onCopied?: (value: string) => void;
|
|
16
|
+
onCopyError?: (error: unknown) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Chép giá trị vào clipboard kèm phản hồi thị giác.
|
|
20
|
+
*
|
|
21
|
+
* Có đường lui khi `navigator.clipboard` không dùng được (trang http không phải localhost,
|
|
22
|
+
* WebView cũ): tạo textarea ẩn rồi `execCommand("copy")`. Thiếu đường lui này thì nút im lặng
|
|
23
|
+
* không làm gì — người dùng tưởng đã chép và dán ra nội dung cũ.
|
|
24
|
+
*/
|
|
25
|
+
declare function CopyButton({ value, label, copiedLabel, resetAfterMs, onCopied, onCopyError, variant, size, className, ...props }: CopyButtonProps): React.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { CopyButton, type CopyButtonProps };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Copy } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { Button } from "../ui/button.js";
|
|
7
|
+
function CopyButton({
|
|
8
|
+
value,
|
|
9
|
+
label = "Sao ch\xE9p",
|
|
10
|
+
copiedLabel = "\u0110\xE3 ch\xE9p",
|
|
11
|
+
resetAfterMs = 2e3,
|
|
12
|
+
onCopied,
|
|
13
|
+
onCopyError,
|
|
14
|
+
variant = "ghost",
|
|
15
|
+
size = "sm",
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}) {
|
|
19
|
+
const [copied, setCopied] = useState(false);
|
|
20
|
+
const timerRef = useRef(null);
|
|
21
|
+
useEffect(
|
|
22
|
+
() => () => {
|
|
23
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
24
|
+
},
|
|
25
|
+
[]
|
|
26
|
+
);
|
|
27
|
+
const writeFallback = (text) => {
|
|
28
|
+
try {
|
|
29
|
+
const textarea = document.createElement("textarea");
|
|
30
|
+
textarea.value = text;
|
|
31
|
+
textarea.setAttribute("readonly", "");
|
|
32
|
+
textarea.style.position = "fixed";
|
|
33
|
+
textarea.style.opacity = "0";
|
|
34
|
+
document.body.appendChild(textarea);
|
|
35
|
+
textarea.select();
|
|
36
|
+
const ok = document.execCommand("copy");
|
|
37
|
+
document.body.removeChild(textarea);
|
|
38
|
+
return ok;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const copy = async () => {
|
|
44
|
+
try {
|
|
45
|
+
if (navigator.clipboard?.writeText) {
|
|
46
|
+
await navigator.clipboard.writeText(value);
|
|
47
|
+
} else if (!writeFallback(value)) {
|
|
48
|
+
throw new Error("Tr\xECnh duy\u1EC7t kh\xF4ng cho ph\xE9p ghi clipboard");
|
|
49
|
+
}
|
|
50
|
+
setCopied(true);
|
|
51
|
+
onCopied?.(value);
|
|
52
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
53
|
+
timerRef.current = setTimeout(() => setCopied(false), resetAfterMs);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
onCopyError?.(error);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const Icon = copied ? Check : Copy;
|
|
59
|
+
return /* @__PURE__ */ jsxs(
|
|
60
|
+
Button,
|
|
61
|
+
{
|
|
62
|
+
type: "button",
|
|
63
|
+
variant,
|
|
64
|
+
size,
|
|
65
|
+
onClick: () => void copy(),
|
|
66
|
+
"aria-label": label ?? (copied ? copiedLabel : "Sao ch\xE9p"),
|
|
67
|
+
className: cn("gap-1.5", copied && "text-success", className),
|
|
68
|
+
...props,
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsx(Icon, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
|
|
71
|
+
label !== null && /* @__PURE__ */ jsx("span", { children: copied ? copiedLabel : label }),
|
|
72
|
+
/* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "sr-only", children: copied ? copiedLabel : "" })
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
CopyButton
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=copy-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/copy-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps } from \"../ui/button.js\";\n\nexport interface CopyButtonProps extends Omit<ButtonProps, \"children\" | \"onCopy\"> {\n /** Nội dung được chép vào clipboard */\n value: string;\n /** Nhãn khi chưa chép (mặc định \"Sao chép\"). `null` = chỉ icon. */\n label?: string | null;\n /** Nhãn sau khi chép thành công */\n copiedLabel?: string;\n /** Thời gian giữ trạng thái \"đã chép\" (ms) */\n resetAfterMs?: number;\n onCopied?: (value: string) => void;\n onCopyError?: (error: unknown) => void;\n}\n\n/**\n * Chép giá trị vào clipboard kèm phản hồi thị giác.\n *\n * Có đường lui khi `navigator.clipboard` không dùng được (trang http không phải localhost,\n * WebView cũ): tạo textarea ẩn rồi `execCommand(\"copy\")`. Thiếu đường lui này thì nút im lặng\n * không làm gì — người dùng tưởng đã chép và dán ra nội dung cũ.\n */\nexport function CopyButton({\n value,\n label = \"Sao chép\",\n copiedLabel = \"Đã chép\",\n resetAfterMs = 2000,\n onCopied,\n onCopyError,\n variant = \"ghost\",\n size = \"sm\",\n className,\n ...props\n}: CopyButtonProps) {\n const [copied, setCopied] = useState(false);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(\n () => () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n },\n []\n );\n\n const writeFallback = (text: string): boolean => {\n try {\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.opacity = \"0\";\n document.body.appendChild(textarea);\n textarea.select();\n const ok = document.execCommand(\"copy\");\n document.body.removeChild(textarea);\n return ok;\n } catch {\n return false;\n }\n };\n\n const copy = async () => {\n try {\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(value);\n } else if (!writeFallback(value)) {\n throw new Error(\"Trình duyệt không cho phép ghi clipboard\");\n }\n\n setCopied(true);\n onCopied?.(value);\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => setCopied(false), resetAfterMs);\n } catch (error) {\n onCopyError?.(error);\n }\n };\n\n const Icon = copied ? Check : Copy;\n\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={size}\n onClick={() => void copy()}\n aria-label={label ?? (copied ? copiedLabel : \"Sao chép\")}\n className={cn(\"gap-1.5\", copied && \"text-success\", className)}\n {...props}\n >\n <Icon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {label !== null && <span>{copied ? copiedLabel : label}</span>}\n <span aria-live=\"polite\" className=\"sr-only\">\n {copied ? copiedLabel : \"\"}\n </span>\n </Button>\n );\n}\n"],"mappings":";AAsFI,SASE,KATF;AApFJ,SAAS,OAAO,YAAY;AAC5B,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,UAAU;AACnB,SAAS,cAAgC;AAsBlC,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,WAAW,OAA6C,IAAI;AAElE;AAAA,IACE,MAAM,MAAM;AACV,UAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AAAA,IACrD;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,gBAAgB,CAAC,SAA0B;AAC/C,QAAI;AACF,YAAM,WAAW,SAAS,cAAc,UAAU;AAClD,eAAS,QAAQ;AACjB,eAAS,aAAa,YAAY,EAAE;AACpC,eAAS,MAAM,WAAW;AAC1B,eAAS,MAAM,UAAU;AACzB,eAAS,KAAK,YAAY,QAAQ;AAClC,eAAS,OAAO;AAChB,YAAM,KAAK,SAAS,YAAY,MAAM;AACtC,eAAS,KAAK,YAAY,QAAQ;AAClC,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,OAAO,YAAY;AACvB,QAAI;AACF,UAAI,UAAU,WAAW,WAAW;AAClC,cAAM,UAAU,UAAU,UAAU,KAAK;AAAA,MAC3C,WAAW,CAAC,cAAc,KAAK,GAAG;AAChC,cAAM,IAAI,MAAM,wDAA0C;AAAA,MAC5D;AAEA,gBAAU,IAAI;AACd,iBAAW,KAAK;AAChB,UAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,eAAS,UAAU,WAAW,MAAM,UAAU,KAAK,GAAG,YAAY;AAAA,IACpE,SAAS,OAAO;AACd,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,QAAM,OAAO,SAAS,QAAQ;AAE9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,SAAS,MAAM,KAAK,KAAK;AAAA,MACzB,cAAY,UAAU,SAAS,cAAc;AAAA,MAC7C,WAAW,GAAG,WAAW,UAAU,gBAAgB,SAAS;AAAA,MAC3D,GAAG;AAAA,MAEJ;AAAA,4BAAC,QAAK,eAAY,QAAO,WAAU,qBAAoB;AAAA,QACtD,UAAU,QAAQ,oBAAC,UAAM,mBAAS,cAAc,OAAM;AAAA,QACvD,oBAAC,UAAK,aAAU,UAAS,WAAU,WAChC,mBAAS,cAAc,IAC1B;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type SortDirection = "asc" | "desc";
|
|
5
|
+
interface DataTableSort {
|
|
6
|
+
columnId: string;
|
|
7
|
+
direction: SortDirection;
|
|
8
|
+
}
|
|
9
|
+
type DataTableAlign = "left" | "center" | "right";
|
|
10
|
+
/**
|
|
11
|
+
* Toàn bộ chuỗi hiển thị (kể cả chuỗi dành riêng cho trình đọc màn hình) của `DataTable` và
|
|
12
|
+
* `DataTableColumnToggle` — app tiếng Anh (hoặc ngôn ngữ khác) override từng khoá qua prop
|
|
13
|
+
* `labels`. Mặc định là tiếng Việt.
|
|
14
|
+
*
|
|
15
|
+
* Cùng khuôn với `ShellLabels` ở tầng shell: gói dùng chung cho MỌI app Comitor nên chuỗi
|
|
16
|
+
* chôn cứng trong component là ngõ cụt cho app đa ngôn ngữ.
|
|
17
|
+
*/
|
|
18
|
+
interface DataTableLabels {
|
|
19
|
+
/** aria-label ô chọn ở đầu bảng khi CHƯA chọn hết */
|
|
20
|
+
selectAllRows: string;
|
|
21
|
+
/** aria-label ô chọn ở đầu bảng khi ĐÃ chọn hết */
|
|
22
|
+
deselectAllRows: string;
|
|
23
|
+
/** aria-label ô chọn của một dòng khi dòng chưa được chọn */
|
|
24
|
+
selectRow: string;
|
|
25
|
+
/** aria-label ô chọn của một dòng khi dòng đã được chọn */
|
|
26
|
+
deselectRow: string;
|
|
27
|
+
/** Tiêu đề trạng thái rỗng mặc định (bỏ qua nếu app truyền `empty`) */
|
|
28
|
+
emptyTitle: string;
|
|
29
|
+
/** Mô tả trạng thái rỗng mặc định */
|
|
30
|
+
emptyDescription: string;
|
|
31
|
+
/** Nhãn nút + tiêu đề menu ẩn/hiện cột */
|
|
32
|
+
columnToggle: string;
|
|
33
|
+
}
|
|
34
|
+
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
35
|
+
declare const DEFAULT_DATA_TABLE_LABELS: DataTableLabels;
|
|
36
|
+
interface DataTableColumn<TRow> {
|
|
37
|
+
id: string;
|
|
38
|
+
/** Nội dung ô tiêu đề */
|
|
39
|
+
header: ReactNode;
|
|
40
|
+
/** Nhãn chữ dùng trong menu ẩn/hiện cột — bắt buộc khi `header` không phải chuỗi */
|
|
41
|
+
label?: string;
|
|
42
|
+
/** Render ô dữ liệu — nhận cả chỉ số dòng để đánh số thứ tự */
|
|
43
|
+
cell: (row: TRow, rowIndex: number) => ReactNode;
|
|
44
|
+
sortable?: boolean;
|
|
45
|
+
align?: DataTableAlign;
|
|
46
|
+
/**
|
|
47
|
+
* Bề rộng cột tính bằng px. BẮT BUỘC với cột ghim (`pinned`) vì offset `left/right` của
|
|
48
|
+
* sticky được tính cộng dồn từ giá trị này — thiếu width thì các cột ghim chồng lên nhau.
|
|
49
|
+
*/
|
|
50
|
+
width?: number;
|
|
51
|
+
minWidth?: number;
|
|
52
|
+
/** Ghim cột khi cuộn ngang (bảng nhiều cột) */
|
|
53
|
+
pinned?: "left" | "right";
|
|
54
|
+
/** Cho phép người dùng ẩn cột này (mặc định true; cột định danh nên đặt false) */
|
|
55
|
+
hideable?: boolean;
|
|
56
|
+
headerClassName?: string;
|
|
57
|
+
cellClassName?: string;
|
|
58
|
+
}
|
|
59
|
+
interface DataTableProps<TRow> {
|
|
60
|
+
columns: DataTableColumn<TRow>[];
|
|
61
|
+
rows: TRow[];
|
|
62
|
+
/** Khoá ổn định của dòng — dùng cho `key` React và cho tập chọn */
|
|
63
|
+
getRowId: (row: TRow, index: number) => string;
|
|
64
|
+
/** Đang tải → hiện `loadingRowCount` dòng khung xương thay vì bảng trống */
|
|
65
|
+
loading?: boolean;
|
|
66
|
+
loadingRowCount?: number;
|
|
67
|
+
/** Nội dung khi không có dòng nào (mặc định EmptyState theo `labels.emptyTitle`) */
|
|
68
|
+
empty?: ReactNode;
|
|
69
|
+
/** Sắp xếp — controlled; `null` = không sắp xếp. App tự sắp/tự gọi API. */
|
|
70
|
+
sort?: DataTableSort | null;
|
|
71
|
+
onSortChange?: (sort: DataTableSort | null) => void;
|
|
72
|
+
/** Chọn dòng bằng checkbox — controlled */
|
|
73
|
+
selectable?: boolean;
|
|
74
|
+
selectedIds?: string[];
|
|
75
|
+
onSelectedIdsChange?: (ids: string[]) => void;
|
|
76
|
+
/** Khoá chọn với một số dòng (vd bản ghi đã khoá) */
|
|
77
|
+
isRowSelectable?: (row: TRow, index: number) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Cột đang hiện — controlled. Đổi bố cục đi qua `DataTableColumnToggle`, app tự lưu theo
|
|
80
|
+
* người dùng (server, localStorage…).
|
|
81
|
+
*
|
|
82
|
+
* Package cố ý KHÔNG tự ghi localStorage: ở SaaS nhiều workspace, bố cục bảng phải theo
|
|
83
|
+
* cặp (người dùng × workspace) và thường cần đồng bộ giữa các máy — chuyện của app.
|
|
84
|
+
*/
|
|
85
|
+
visibleColumns?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* Bấm vào dòng (vd mở chi tiết bản ghi). Có prop này thì dòng vào chuỗi Tab và nhận
|
|
88
|
+
* Enter/Space — mở chi tiết KHÔNG được là thao tác chỉ-chuột.
|
|
89
|
+
*/
|
|
90
|
+
onRowClick?: (row: TRow, index: number) => void;
|
|
91
|
+
rowClassName?: (row: TRow, index: number) => string | undefined;
|
|
92
|
+
/** Dòng đang được làm nổi (vd bản ghi vừa mở ở panel bên) */
|
|
93
|
+
isRowActive?: (row: TRow, index: number) => boolean;
|
|
94
|
+
/** Tiêu đề dính đỉnh vùng cuộn (mặc định true) */
|
|
95
|
+
stickyHeader?: boolean;
|
|
96
|
+
/** Bề rộng tối thiểu của bảng — bật cuộn ngang khi màn hình hẹp */
|
|
97
|
+
minWidth?: number | string;
|
|
98
|
+
density?: "comfortable" | "compact";
|
|
99
|
+
/** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `DataTableLabels` */
|
|
100
|
+
labels?: Partial<DataTableLabels>;
|
|
101
|
+
/** Nhãn của bảng cho trình đọc màn hình (bắt buộc khi trang có nhiều bảng) */
|
|
102
|
+
"aria-label"?: string;
|
|
103
|
+
"aria-labelledby"?: string;
|
|
104
|
+
className?: string;
|
|
105
|
+
/** className của khung cuộn bọc ngoài */
|
|
106
|
+
containerClassName?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Bảng dữ liệu chuẩn của Comitor: tiêu đề dính, sắp xếp, chọn dòng, ghim cột, ẩn/hiện cột,
|
|
110
|
+
* trạng thái tải và trạng thái rỗng.
|
|
111
|
+
*
|
|
112
|
+
* Mọi trạng thái đều **controlled** — package không giữ dữ liệu, không gọi API, không tự phân
|
|
113
|
+
* trang. Nhờ vậy dùng được cả với dữ liệu client (mảng trong bộ nhớ) lẫn server-side
|
|
114
|
+
* pagination mà không phải viết hai component.
|
|
115
|
+
*/
|
|
116
|
+
declare function DataTable<TRow>({ columns, rows, getRowId, loading, loadingRowCount, empty, sort, onSortChange, selectable, selectedIds, onSelectedIdsChange, isRowSelectable, visibleColumns, onRowClick, rowClassName, isRowActive, stickyHeader, minWidth, density, labels: labelsProp, className, containerClassName, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby }: DataTableProps<TRow>): React.JSX.Element;
|
|
117
|
+
interface DataTableColumnToggleProps<TRow> {
|
|
118
|
+
columns: DataTableColumn<TRow>[];
|
|
119
|
+
/** Danh sách cột đang hiện — cùng nguồn với `DataTable.visibleColumns` */
|
|
120
|
+
visibleColumns: string[];
|
|
121
|
+
onVisibleColumnsChange: (visibleColumns: string[]) => void;
|
|
122
|
+
/** Nhãn nút + tiêu đề menu; không truyền thì lấy `labels.columnToggle` */
|
|
123
|
+
label?: string;
|
|
124
|
+
/** Ghi đè chuỗi hiển thị (mặc định tiếng Việt) — xem `DataTableLabels` */
|
|
125
|
+
labels?: Partial<DataTableLabels>;
|
|
126
|
+
className?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Menu ẩn/hiện cột đi kèm `DataTable` (đặt trên thanh công cụ của trang danh sách).
|
|
130
|
+
*
|
|
131
|
+
* Không cho ẩn hết: luôn giữ lại ít nhất một cột, nếu không bảng thành khối trắng vô nghĩa
|
|
132
|
+
* và người dùng không có đường quay lại.
|
|
133
|
+
*/
|
|
134
|
+
declare function DataTableColumnToggle<TRow>({ columns, visibleColumns, onVisibleColumnsChange, label, labels: labelsProp, className }: DataTableColumnToggleProps<TRow>): React.JSX.Element;
|
|
135
|
+
|
|
136
|
+
export { DEFAULT_DATA_TABLE_LABELS, DataTable, type DataTableAlign, type DataTableColumn, DataTableColumnToggle, type DataTableColumnToggleProps, type DataTableLabels, type DataTableProps, type DataTableSort, type SortDirection };
|