@comitor/ui 3.0.0 → 3.0.1
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/README.md +5 -1
- package/dist/components/composite/toast.d.ts +2 -1
- package/dist/components/composite/toast.js +3 -9
- package/dist/components/composite/toast.js.map +1 -1
- package/dist/components/ui/action-bar.js +45 -1
- package/dist/components/ui/action-bar.js.map +1 -1
- package/dist/components/ui/circular-progress.js +10 -2
- package/dist/components/ui/circular-progress.js.map +1 -1
- package/dist/components/ui/kanban.js +9 -3
- package/dist/components/ui/kanban.js.map +1 -1
- package/dist/components/ui/key-value.js +70 -76
- package/dist/components/ui/key-value.js.map +1 -1
- package/dist/components/ui/mask-input-constants.js +31 -71
- package/dist/components/ui/mask-input-constants.js.map +1 -1
- package/dist/components/ui/mask-input.js +18 -20
- package/dist/components/ui/mask-input.js.map +1 -1
- package/dist/components/ui/phone-input.js +5 -2
- package/dist/components/ui/phone-input.js.map +1 -1
- package/dist/components/ui/rating.js +4 -2
- package/dist/components/ui/rating.js.map +1 -1
- package/dist/components/ui/slider.d.ts +1 -1
- package/dist/components/ui/slider.js +15 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/sonner.js +2 -0
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/toggle-group.js +12 -1
- package/dist/components/ui/toggle-group.js.map +1 -1
- package/dist/components/ui/tour.js +3 -2
- package/dist/components/ui/tour.js.map +1 -1
- package/dist/data-grid/use-data-grid.d.ts +2 -2
- package/dist/i18n/locale-provider.d.ts +18 -0
- package/dist/i18n/locale-provider.js.map +1 -1
- package/dist/i18n/namespaces/rating.js +2 -1
- package/dist/i18n/namespaces/rating.js.map +1 -1
- package/dist/i18n/namespaces/shell.d.ts +4 -0
- package/dist/i18n/namespaces/shell.js +1 -1
- package/dist/i18n/namespaces/shell.js.map +1 -1
- package/dist/i18n/namespaces/toast.d.ts +3 -2
- package/dist/i18n/namespaces/toast.js.map +1 -1
- package/dist/lib/toast-language.d.ts +8 -0
- package/dist/lib/toast-language.js +19 -0
- package/dist/lib/toast-language.js.map +1 -0
- package/dist/shell/display-axis-provider.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,7 +195,11 @@ import { ComitorLocaleProvider } from "@comitor/ui";
|
|
|
195
195
|
còn lại → tiếng Anh) **và** quyết định định dạng: giờ, số, đồng hồ 12/24 giờ đi thẳng vào `Intl`.
|
|
196
196
|
⚠ Ngày định dạng bằng date-fns (`DatePicker`, `RelativeTime`, `Calendar`, `DataGrid`) chỉ tự ánh
|
|
197
197
|
xạ `vi` → `vi`, mọi mã khác → `enUS` (`12/25/2024`); muốn kiểu Anh (`25/12/2024`) ở `en-GB` thì
|
|
198
|
-
truyền thêm `dateFnsLocale={enGB}` cho provider
|
|
198
|
+
truyền thêm `dateFnsLocale={enGB}` cho provider — ⚠ từ một component `"use client"` của app, KHÔNG
|
|
199
|
+
thẳng từ root layout: locale date-fns là object toàn HÀM, Server Component không truyền được sang
|
|
200
|
+
client, và trang lỗi 500 ("Functions cannot be passed directly to Client Components"). Cùng luật
|
|
201
|
+
cho `messages` có khoá là hàm (xem gạch đầu dòng về HÀM bên dưới). Mẫu dựng sẵn:
|
|
202
|
+
https://design-system.comitor.ai/get-started#language
|
|
199
203
|
- Thứ tự ưu tiên cho MỌI chuỗi: prop của chính component (`labels`, `placeholder`, `clearLabel`…)
|
|
200
204
|
→ provider gần nhất → tiếng Anh. Kiểu của `labels` là `Partial<>` nên truyền một khoá không làm mất
|
|
201
205
|
phần còn lại.
|
|
@@ -40,7 +40,8 @@ declare function dismissToast(id?: string | number): void;
|
|
|
40
40
|
/**
|
|
41
41
|
* Toast bám theo một Promise: "đang lưu…" → "đã lưu" / "lưu thất bại". `success` và `error` luôn
|
|
42
42
|
* do app truyền; `loading` bỏ trống thì lấy khoá `toast.loading` theo ngôn ngữ của `<Toaster />`
|
|
43
|
-
* (tức của `ComitorLocaleProvider` bọc nó — xem `
|
|
43
|
+
* (tức của `ComitorLocaleProvider` bọc nó — xem `lib/toast-language.ts`; từ `3.0.1` cả `SonnerToaster`
|
|
44
|
+
* của `/shell` cũng ghi vào đó). Truyền
|
|
44
45
|
* `loading` tường minh vẫn thắng.
|
|
45
46
|
*/
|
|
46
47
|
declare function toastPromise<T>(promise: Promise<T>, messages: {
|
|
@@ -2,15 +2,9 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { AlertTriangle, CheckCircle2, Info, XCircle } from "lucide-react";
|
|
4
4
|
import { Toaster as SonnerToaster, toast as sonnerToast } from "sonner";
|
|
5
|
-
import {
|
|
6
|
-
import { toastEn } from "../../i18n/namespaces/toast.js";
|
|
7
|
-
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
8
|
-
let activeToastMessages = toastEn;
|
|
5
|
+
import { getActiveToastMessages, useToastLanguageBridge } from "../../lib/toast-language.js";
|
|
9
6
|
function Toaster({ theme = "system", position = "bottom-right", ...props }) {
|
|
10
|
-
|
|
11
|
-
useIsomorphicLayoutEffect(() => {
|
|
12
|
-
activeToastMessages = toastMessages;
|
|
13
|
-
}, [toastMessages]);
|
|
7
|
+
useToastLanguageBridge();
|
|
14
8
|
return /* @__PURE__ */ jsx(
|
|
15
9
|
SonnerToaster,
|
|
16
10
|
{
|
|
@@ -67,7 +61,7 @@ function dismissToast(id) {
|
|
|
67
61
|
}
|
|
68
62
|
function toastPromise(promise, messages) {
|
|
69
63
|
return sonnerToast.promise(promise, {
|
|
70
|
-
loading: messages.loading ??
|
|
64
|
+
loading: messages.loading ?? getActiveToastMessages().loading,
|
|
71
65
|
success: messages.success,
|
|
72
66
|
error: messages.error
|
|
73
67
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertTriangle, CheckCircle2, Info, XCircle } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { Toaster as SonnerToaster, toast as sonnerToast } from \"sonner\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertTriangle, CheckCircle2, Info, XCircle } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { Toaster as SonnerToaster, toast as sonnerToast } from \"sonner\";\nimport { getActiveToastMessages, useToastLanguageBridge } from \"../../lib/toast-language.js\";\n\nexport type ToasterProps = ComponentProps<typeof SonnerToaster>;\n\nexport type ToastVariant = \"default\" | \"success\" | \"warning\" | \"destructive\" | \"info\";\n\nexport interface ToastOptions {\n description?: ReactNode;\n variant?: ToastVariant;\n /** ms; mặc định theo sonner (4000). `Number.POSITIVE_INFINITY` = giữ đến khi người dùng đóng. */\n duration?: number;\n /** Nút hành động, vd { label: \"Hoàn tác\", onClick } */\n action?: { label: string; onClick: () => void };\n /** id để cập nhật/đóng đúng toast đó (dùng với luồng \"đang lưu…\" → \"đã lưu\") */\n id?: string | number;\n}\n\n/**\n * Vùng hiển thị toast — đặt MỘT LẦN ở gốc app.\n *\n * `theme` mặc định \"system\" và KHÔNG đọc `next-themes`: entry \".\" của package phải chạy được ở\n * app không có next-themes. App dùng next-themes hãy truyền xuống, nếu không toast sẽ theo\n * `prefers-color-scheme` của hệ điều hành và lệch với theme người dùng tự chọn:\n *\n * const { resolvedTheme } = useTheme();\n * <Toaster theme={resolvedTheme as \"light\" | \"dark\"} />\n */\nexport function Toaster({ theme = \"system\", position = \"bottom-right\", ...props }: ToasterProps) {\n // Ghi ngôn ngữ của provider cho `toastPromise` đọc — xem `lib/toast-language.ts`.\n useToastLanguageBridge();\n\n return (\n <SonnerToaster\n theme={theme}\n position={position}\n className=\"toaster group\"\n // Icon tô bằng bậc `--x-ink`, KHÔNG phải token tô nền `--x`. Icon là thành phần phi văn\n // bản (ngưỡng 3:1) đứng trên nền `--popover`: `text-warning` (= `--yellow` #F2C94C) trên\n // nền trắng chỉ **1,59:1** ✗ — gần như vô hình ở light (dark thì 11,60:1, lại một chỗ mỗi\n // theme hỏng một nửa); `text-success` 3,13:1 và `text-info` 4,14:1 tuy qua ngưỡng nhưng\n // sát mép. Bậc ink (light / dark): success 6,52 / 10,19 · warning 6,51 / 11,60 ·\n // destructive 7,15 / 6,42 · info 6,68 / 9,54 ✓.\n icons={{\n success: <CheckCircle2 className=\"size-4 text-success-ink\" />,\n warning: <AlertTriangle className=\"size-4 text-warning-ink\" />,\n error: <XCircle className=\"size-4 text-destructive-ink\" />,\n info: <Info className=\"size-4 text-info-ink\" />\n }}\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-popover group-[.toaster]:text-popover-foreground group-[.toaster]:border-border group-[.toaster]:rounded-lg group-[.toaster]:shadow-card\",\n title: \"group-[.toast]:text-sm group-[.toast]:font-medium\",\n description: \"group-[.toast]:text-sm group-[.toast]:text-muted-foreground\",\n // Hai nút này là CONTROL đứng trên nền `--popover`, nên ngoài tương phản CHỮ còn phải\n // nhận ra được RANH GIỚI (WCAG 1.4.11, ngưỡng 3:1):\n // actionButton chữ 10,19:1 (L) · 12,67:1 (D) ✓, nhưng ruột `bg-primary` (gold) chỉ\n // lệch nền popover 1,86:1 ở light. Giữ đúng cách variant `default` của\n // `Button` xử lý — viền trong suốt, để `shadow-control` làm tín hiệu\n // độ cao; đổi khác ở đây sẽ lệch với nút của chính gói.\n // cancelButton chữ 4,89:1 (L) · 4,86:1 (D) ✓, nhưng ruột `bg-muted` chỉ lệch nền\n // popover 1,07:1 (L) · 1,10:1 (D) → nút không có biên nào, đọc như chữ\n // trơn. `border-control-edge`: 3,83:1 (L) · 4,64:1 (D) so với popover,\n // 3,58:1 · 4,21:1 so với chính ruột `bg-muted` ✓ (hợp đồng §3).\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground group-[.toast]:shadow-control\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground group-[.toast]:border group-[.toast]:border-control-edge\"\n }\n }}\n {...props}\n />\n );\n}\n\n/**\n * Bắn toast theo biến thể của Comitor.\n *\n * Vì sao bọc `sonner` thay vì để app gọi thẳng: tên biến thể ở đây khớp với ngôn ngữ của design\n * system (`destructive`, không phải `error`), và khi cần đổi thư viện toast thì chỉ sửa một file.\n */\nexport function toast(title: ReactNode, options: ToastOptions = {}): string | number {\n const { variant = \"default\", description, duration, action, id } = options;\n const config = { description, duration, action, id };\n\n switch (variant) {\n case \"success\":\n return sonnerToast.success(title, config);\n case \"warning\":\n return sonnerToast.warning(title, config);\n case \"destructive\":\n return sonnerToast.error(title, config);\n case \"info\":\n return sonnerToast.info(title, config);\n default:\n return sonnerToast(title, config);\n }\n}\n\n/** Đóng một toast theo id, hoặc đóng tất cả khi không truyền id. */\nexport function dismissToast(id?: string | number): void {\n sonnerToast.dismiss(id);\n}\n\n/**\n * Toast bám theo một Promise: \"đang lưu…\" → \"đã lưu\" / \"lưu thất bại\". `success` và `error` luôn\n * do app truyền; `loading` bỏ trống thì lấy khoá `toast.loading` theo ngôn ngữ của `<Toaster />`\n * (tức của `ComitorLocaleProvider` bọc nó — xem `lib/toast-language.ts`; từ `3.0.1` cả `SonnerToaster`\n * của `/shell` cũng ghi vào đó). Truyền\n * `loading` tường minh vẫn thắng.\n */\nexport function toastPromise<T>(\n promise: Promise<T>,\n messages: {\n loading?: ReactNode;\n success: ReactNode | ((data: T) => ReactNode);\n error: ReactNode | ((error: unknown) => ReactNode);\n }\n) {\n return sonnerToast.promise(promise, {\n loading: messages.loading ?? getActiveToastMessages().loading,\n success: messages.success,\n error: messages.error\n });\n}\n"],"mappings":";AAgDiB;AA9CjB,SAAS,eAAe,cAAc,MAAM,eAAe;AAE3D,SAAS,WAAW,eAAe,SAAS,mBAAmB;AAC/D,SAAS,wBAAwB,8BAA8B;AA2BxD,SAAS,QAAQ,EAAE,QAAQ,UAAU,WAAW,gBAAgB,GAAG,MAAM,GAAiB;AAE/F,yBAAuB;AAEvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,WAAU;AAAA,MAOV,OAAO;AAAA,QACL,SAAS,oBAAC,gBAAa,WAAU,2BAA0B;AAAA,QAC3D,SAAS,oBAAC,iBAAc,WAAU,2BAA0B;AAAA,QAC5D,OAAO,oBAAC,WAAQ,WAAU,+BAA8B;AAAA,QACxD,MAAM,oBAAC,QAAK,WAAU,wBAAuB;AAAA,MAC/C;AAAA,MACA,cAAc;AAAA,QACZ,YAAY;AAAA,UACV,OACE;AAAA,UACF,OAAO;AAAA,UACP,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAWb,cACE;AAAA,UACF,cACE;AAAA,QACJ;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAQO,SAAS,MAAM,OAAkB,UAAwB,CAAC,GAAoB;AACnF,QAAM,EAAE,UAAU,WAAW,aAAa,UAAU,QAAQ,GAAG,IAAI;AACnE,QAAM,SAAS,EAAE,aAAa,UAAU,QAAQ,GAAG;AAEnD,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO,YAAY,QAAQ,OAAO,MAAM;AAAA,IAC1C,KAAK;AACH,aAAO,YAAY,QAAQ,OAAO,MAAM;AAAA,IAC1C,KAAK;AACH,aAAO,YAAY,MAAM,OAAO,MAAM;AAAA,IACxC,KAAK;AACH,aAAO,YAAY,KAAK,OAAO,MAAM;AAAA,IACvC;AACE,aAAO,YAAY,OAAO,MAAM;AAAA,EACpC;AACF;AAGO,SAAS,aAAa,IAA4B;AACvD,cAAY,QAAQ,EAAE;AACxB;AASO,SAAS,aACd,SACA,UAKA;AACA,SAAO,YAAY,QAAQ,SAAS;AAAA,IAClC,SAAS,SAAS,WAAW,uBAAuB,EAAE;AAAA,IACtD,SAAS,SAAS;AAAA,IAClB,OAAO,SAAS;AAAA,EAClB,CAAC;AACH;","names":[]}
|
|
@@ -67,6 +67,8 @@ function ActionBar(props) {
|
|
|
67
67
|
orientation = "horizontal",
|
|
68
68
|
loop = true,
|
|
69
69
|
labels: labelsProp,
|
|
70
|
+
onFocus: onFocusProp,
|
|
71
|
+
onBlur: onBlurProp,
|
|
70
72
|
className,
|
|
71
73
|
style,
|
|
72
74
|
ref,
|
|
@@ -80,6 +82,46 @@ function ActionBar(props) {
|
|
|
80
82
|
onEscapeKeyDown,
|
|
81
83
|
onOpenChange
|
|
82
84
|
});
|
|
85
|
+
const returnFocusRef = React.useRef(null);
|
|
86
|
+
const focusInsideRef = React.useRef(false);
|
|
87
|
+
const onFocus = React.useCallback(
|
|
88
|
+
(event) => {
|
|
89
|
+
onFocusProp?.(event);
|
|
90
|
+
const from = event.relatedTarget;
|
|
91
|
+
if (!focusInsideRef.current && from instanceof HTMLElement && !event.currentTarget.contains(from)) {
|
|
92
|
+
returnFocusRef.current = from;
|
|
93
|
+
}
|
|
94
|
+
focusInsideRef.current = true;
|
|
95
|
+
},
|
|
96
|
+
[onFocusProp]
|
|
97
|
+
);
|
|
98
|
+
const onBlur = React.useCallback(
|
|
99
|
+
(event) => {
|
|
100
|
+
onBlurProp?.(event);
|
|
101
|
+
const to = event.relatedTarget;
|
|
102
|
+
if (!(to instanceof Node) || !event.currentTarget.contains(to)) {
|
|
103
|
+
focusInsideRef.current = false;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[onBlurProp]
|
|
107
|
+
);
|
|
108
|
+
useIsomorphicLayoutEffect(() => {
|
|
109
|
+
if (!open) return;
|
|
110
|
+
return () => {
|
|
111
|
+
const returnTarget = returnFocusRef.current;
|
|
112
|
+
const hadFocus = focusInsideRef.current;
|
|
113
|
+
returnFocusRef.current = null;
|
|
114
|
+
focusInsideRef.current = false;
|
|
115
|
+
if (!hadFocus || !returnTarget) return;
|
|
116
|
+
queueMicrotask(() => {
|
|
117
|
+
const ownerDocument = returnTarget.ownerDocument;
|
|
118
|
+
const active = ownerDocument.activeElement;
|
|
119
|
+
if (active && active !== ownerDocument.body && active.isConnected) return;
|
|
120
|
+
if (!returnTarget.isConnected) return;
|
|
121
|
+
returnTarget.focus({ preventScroll: true });
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}, [open]);
|
|
83
125
|
const dir = useDirection(dirProp);
|
|
84
126
|
const labels = useComitorLabels("actionBar", labelsProp);
|
|
85
127
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -154,7 +196,9 @@ function ActionBar(props) {
|
|
|
154
196
|
...align === "start" && { left: `${alignOffset}px` },
|
|
155
197
|
...align === "end" && { right: `${alignOffset}px` },
|
|
156
198
|
...style
|
|
157
|
-
}
|
|
199
|
+
},
|
|
200
|
+
onFocus,
|
|
201
|
+
onBlur
|
|
158
202
|
}
|
|
159
203
|
),
|
|
160
204
|
portalContainer
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/action-bar.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/action-bar) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n *\n * Dùng ở Comitor: thanh THAO TÁC HÀNG LOẠT nổi ở đáy màn hình khi có dòng được chọn trong\n * `DataTable` / `DataGrid` — `open={selectedIds.length > 0}`, và `onOpenChange(false)` xoá vùng chọn.\n */\n\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComitorLabels } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport type { ActionBarLabels } from \"./action-bar-constants.js\";\nimport { Button } from \"./button.js\";\n\nconst ROOT_NAME = \"ActionBar\";\nconst GROUP_NAME = \"ActionBarGroup\";\nconst ITEM_NAME = \"ActionBarItem\";\nconst CLOSE_NAME = \"ActionBarClose\";\nconst SELECTION_NAME = \"ActionBarSelection\";\nconst SEPARATOR_NAME = \"ActionBarSeparator\";\nconst ITEM_SELECT = \"actionbar.itemSelect\";\nconst ENTRY_FOCUS = \"actionbarFocusGroup.onEntryFocus\";\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof ActionBar>;\ntype ItemElement = React.ComponentRef<typeof ActionBarItem>;\ntype CloseElement = React.ComponentRef<typeof ActionBarClose>;\n\nfunction focusFirst(candidates: React.RefObject<HTMLElement | null>[], preventScroll = false) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidateRef of candidates) {\n const candidate = candidateRef.current;\n if (!candidate) continue;\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus({ preventScroll });\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\nfunction wrapArray<T>(array: T[], startIndex: number) {\n return array.map<T>((_, index) => array[(startIndex + index) % array.length] as T);\n}\n\nfunction getDirectionAwareKey(key: string, dir?: Direction) {\n if (dir !== \"rtl\") return key;\n return key === \"ArrowLeft\" ? \"ArrowRight\" : key === \"ArrowRight\" ? \"ArrowLeft\" : key;\n}\n\ninterface ItemData {\n id: string;\n ref: React.RefObject<ItemElement | null>;\n disabled: boolean;\n}\n\ninterface ActionBarContextValue {\n onOpenChange?: (open: boolean) => void;\n dir: Direction;\n orientation: Orientation;\n loop: boolean;\n labels: ActionBarLabels;\n}\n\nconst ActionBarContext = React.createContext<ActionBarContextValue | null>(null);\n\nfunction useActionBarContext(consumerName: string) {\n const context = React.useContext(ActionBarContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface FocusContextValue {\n tabStopId: string | null;\n onItemFocus: (tabStopId: string) => void;\n onItemShiftTab: () => void;\n onFocusableItemAdd: () => void;\n onFocusableItemRemove: () => void;\n onItemRegister: (item: ItemData) => void;\n onItemUnregister: (id: string) => void;\n getItems: () => ItemData[];\n}\n\nconst FocusContext = React.createContext<FocusContextValue | null>(null);\n\nfunction useFocusContext(consumerName: string) {\n const context = React.useContext(FocusContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`FocusProvider\\``);\n }\n return context;\n}\n\ninterface ActionBarProps extends DivProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n onEscapeKeyDown?: (event: KeyboardEvent) => void;\n align?: \"start\" | \"center\" | \"end\";\n alignOffset?: number;\n side?: \"top\" | \"bottom\";\n sideOffset?: number;\n portalContainer?: Element | DocumentFragment | null;\n dir?: Direction;\n orientation?: Orientation;\n loop?: boolean;\n /**\n * Chuỗi hiển thị và chuỗi a11y (tên thanh, dòng \"N selected\", tên nút đóng). Không truyền thì\n * theo namespace `actionBar` của `ComitorLocaleProvider` (mặc định tiếng Anh); truyền một phần\n * là đủ.\n *\n * ⚠ Thêm so với thượng nguồn: bản gốc không có chuỗi nào — thanh `role=\"toolbar\"` không tên và\n * nút đóng chỉ chứa `<X />` là một nút KHÔNG TÊN với trình đọc màn hình.\n */\n labels?: Partial<ActionBarLabels>;\n}\n\nfunction ActionBar(props: ActionBarProps) {\n const {\n open = false,\n onOpenChange,\n onEscapeKeyDown,\n side = \"bottom\",\n alignOffset = 0,\n align = \"center\",\n sideOffset = 16,\n portalContainer: portalContainerProp,\n dir: dirProp,\n orientation = \"horizontal\",\n loop = true,\n labels: labelsProp,\n className,\n style,\n ref,\n asChild,\n ...rootProps\n } = props;\n\n const [mounted, setMounted] = React.useState(false);\n\n const rootRef = React.useRef<RootElement>(null);\n const composedRef = useComposedRefs(ref, rootRef);\n\n const propsRef = useAsRef({\n onEscapeKeyDown,\n onOpenChange\n });\n\n const dir = useDirection(dirProp);\n\n const labels = useComitorLabels(\"actionBar\", labelsProp);\n\n // Chỉ đọc `document.body` SAU khi mount — lượt render phía server không chạm `document`.\n useIsomorphicLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n\n const ownerDocument = rootRef.current?.ownerDocument ?? document;\n\n function onKeyDown(event: KeyboardEvent) {\n /*\n * ⚠ Khác thượng nguồn: bỏ qua phím Escape ĐÃ được xử lý (`defaultPrevented`) hoặc đang gõ dở\n * bằng bộ gõ (`isComposing` — Telex/VNI qua IME hệ điều hành). Bản gốc nghe Escape ở cấp\n * `document`, nên Escape để đóng một Dialog/Popover/DropdownMenu của Radix (các lớp đó\n * `preventDefault()` khi tự đóng) cũng đóng luôn thanh này — tức XOÁ VÙNG CHỌN của người dùng\n * chỉ vì họ đóng một menu.\n */\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n\n propsRef.current.onEscapeKeyDown?.(event);\n if (!event.defaultPrevented) {\n propsRef.current.onOpenChange?.(false);\n }\n }\n\n ownerDocument.addEventListener(\"keydown\", onKeyDown);\n return () => ownerDocument.removeEventListener(\"keydown\", onKeyDown);\n }, [open, propsRef]);\n\n const contextValue = React.useMemo<ActionBarContextValue>(\n () => ({\n onOpenChange,\n dir,\n orientation,\n loop,\n labels\n }),\n [onOpenChange, dir, orientation, loop, labels]\n );\n\n const portalContainer = portalContainerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!portalContainer || !open) return null;\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ActionBarContext.Provider value={contextValue}>\n {ReactDOM.createPortal(\n <RootPrimitive\n role=\"toolbar\"\n aria-label={labels.toolbar}\n aria-orientation={orientation}\n data-slot=\"action-bar\"\n data-side={side}\n data-align={align}\n data-orientation={orientation}\n dir={dir}\n {...rootProps}\n ref={composedRef}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: mặt nổi đi qua bộ token `popover` + `shadow-overlay` như mọi lớp\n * nổi của gói (Popover, DropdownMenu). Bản gốc `bg-card shadow-lg` — `shadow-lg` là\n * bóng mặc định của Tailwind, không có bản dark của gói (xem CLAUDE.md \"Bóng\").\n */\n \"fixed z-50 rounded-lg border bg-popover text-popover-foreground shadow-overlay outline-none\",\n \"animate-in duration-250 fade-in-0 zoom-in-95 [animation-timing-function:cubic-bezier(0.16,1,0.3,1)]\",\n \"data-[side=bottom]:slide-in-from-bottom-4 data-[side=top]:slide-in-from-top-4\",\n \"motion-reduce:animate-none motion-reduce:transition-none\",\n /*\n * ⚠ Khác thượng nguồn: chặn bề rộng theo khung nhìn và cho hàng ngang XUỐNG DÒNG. Bản gốc\n * không có giới hạn nào — đo ở 375px: thanh rộng 413px, bắt đầu ở x = −19px, hai mép bị\n * cắt và \"Đã chọn 3\" vỡ thành 3 dòng. Xuống dòng chứ không cuộn ngang: vùng cuộn sẽ cắt\n * mất vòng focus của các nút ở mép.\n */\n \"max-w-[calc(100vw-1rem)]\",\n orientation === \"horizontal\"\n ? \"flex flex-row flex-wrap items-center gap-2 px-2 py-1.5\"\n : \"flex flex-col items-start gap-2 px-1.5 py-2\",\n className\n )}\n style={{\n [side]: `${sideOffset}px`,\n ...(align === \"center\" && {\n left: \"50%\",\n translate: \"-50% 0\"\n }),\n ...(align === \"start\" && { left: `${alignOffset}px` }),\n ...(align === \"end\" && { right: `${alignOffset}px` }),\n ...style\n }}\n />,\n portalContainer\n )}\n </ActionBarContext.Provider>\n );\n}\n\ninterface ActionBarSelectionProps extends DivProps {\n /**\n * Số mục đang chọn. Có giá trị thì phần tử tự in `labels.selected(count)` (\"3 selected\") TRƯỚC\n * `children` — nên `children` chỉ còn là dấu ngăn + nút đóng.\n *\n * ⚠ Thêm so với thượng nguồn: bản gốc để app tự viết chữ (\"{n} selected\"), tức chuỗi hiển thị\n * không đi qua `labels`. Bỏ qua khi `asChild` (Slot chỉ nhận đúng MỘT phần tử con).\n */\n count?: number;\n}\n\nfunction ActionBarSelection(props: ActionBarSelectionProps) {\n const { className, asChild, count, children, ...selectionProps } = props;\n\n const { labels } = useActionBarContext(SELECTION_NAME);\n\n const SelectionPrimitive = asChild ? Slot : \"div\";\n\n return (\n <SelectionPrimitive\n data-slot=\"action-bar-selection\"\n {...selectionProps}\n // ⚠ Khác thượng nguồn: `whitespace-nowrap shrink-0` — con số đếm không được vỡ dòng khi thanh hẹp.\n className={cn(\n \"flex shrink-0 items-center gap-1 whitespace-nowrap rounded-sm border px-2 py-1 text-sm font-medium tabular-nums\",\n className\n )}\n >\n {asChild ? (\n children\n ) : (\n <>\n {count !== undefined && labels.selected(count)}\n {children}\n </>\n )}\n </SelectionPrimitive>\n );\n}\n\nfunction ActionBarGroup(props: DivProps) {\n const {\n onBlur: onBlurProp,\n onFocus: onFocusProp,\n onMouseDown: onMouseDownProp,\n className,\n asChild,\n ref,\n ...groupProps\n } = props;\n\n const [tabStopId, setTabStopId] = React.useState<string | null>(null);\n const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false);\n const [focusableItemCount, setFocusableItemCount] = React.useState(0);\n\n const groupRef = React.useRef<HTMLDivElement>(null);\n const composedRef = useComposedRefs(ref, groupRef);\n const isClickFocusRef = React.useRef(false);\n const itemsRef = React.useRef<Map<string, ItemData>>(new Map());\n\n const { dir, orientation } = useActionBarContext(GROUP_NAME);\n\n const onItemFocus = React.useCallback((tabStopId: string) => {\n setTabStopId(tabStopId);\n }, []);\n\n const onItemShiftTab = React.useCallback(() => {\n setIsTabbingBackOut(true);\n }, []);\n\n const onFocusableItemAdd = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount + 1);\n }, []);\n\n const onFocusableItemRemove = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount - 1);\n }, []);\n\n const onItemRegister = React.useCallback((item: ItemData) => {\n itemsRef.current.set(item.id, item);\n }, []);\n\n const onItemUnregister = React.useCallback((id: string) => {\n itemsRef.current.delete(id);\n }, []);\n\n const getItems = React.useCallback(() => {\n return Array.from(itemsRef.current.values())\n .filter((item) => item.ref.current)\n .sort((a, b) => {\n const elementA = a.ref.current;\n const elementB = b.ref.current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n return -1;\n }\n if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n return 1;\n }\n return 0;\n });\n }, []);\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent<HTMLDivElement>) => {\n onBlurProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsTabbingBackOut(false);\n },\n [onBlurProp]\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent<HTMLDivElement>) => {\n onFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n const isKeyboardFocus = !isClickFocusRef.current;\n if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) {\n const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);\n event.currentTarget.dispatchEvent(entryFocusEvent);\n\n if (!entryFocusEvent.defaultPrevented) {\n const items = Array.from(itemsRef.current.values()).filter((item) => !item.disabled);\n const currentItem = items.find((item) => item.id === tabStopId);\n\n const candidateItems = [currentItem, ...items].filter(Boolean) as ItemData[];\n const candidateRefs = candidateItems.map((item) => item.ref);\n focusFirst(candidateRefs, false);\n }\n }\n isClickFocusRef.current = false;\n },\n [onFocusProp, isTabbingBackOut, tabStopId]\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n onMouseDownProp?.(event);\n if (event.defaultPrevented) return;\n\n isClickFocusRef.current = true;\n },\n [onMouseDownProp]\n );\n\n const focusContextValue = React.useMemo<FocusContextValue>(\n () => ({\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems\n }),\n [\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems\n ]\n );\n\n const GroupPrimitive = asChild ? Slot : \"div\";\n\n return (\n <FocusContext.Provider value={focusContextValue}>\n <GroupPrimitive\n role=\"group\"\n data-slot=\"action-bar-group\"\n data-orientation={orientation}\n dir={dir}\n tabIndex={isTabbingBackOut || focusableItemCount === 0 ? -1 : 0}\n {...groupProps}\n ref={composedRef}\n className={cn(\n \"flex gap-2 outline-none\",\n orientation === \"horizontal\" ? \"items-center\" : \"w-full flex-col items-start\",\n className\n )}\n onBlur={onBlur}\n onFocus={onFocus}\n onMouseDown={onMouseDown}\n />\n </FocusContext.Provider>\n );\n}\n\ninterface ActionBarItemProps extends Omit<React.ComponentProps<typeof Button>, \"onSelect\"> {\n onSelect?: (event: Event) => void;\n}\n\nfunction ActionBarItem(props: ActionBarItemProps) {\n const {\n onSelect,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n onMouseDown: onMouseDownProp,\n className,\n disabled,\n ref,\n ...itemProps\n } = props;\n\n const itemRef = React.useRef<ItemElement>(null);\n const composedRef = useComposedRefs(ref, itemRef);\n const isMouseClickRef = React.useRef(false);\n\n const { onOpenChange, dir, orientation, loop } = useActionBarContext(ITEM_NAME);\n const focusContext = useFocusContext(ITEM_NAME);\n\n const itemId = React.useId();\n const isTabStop = focusContext.tabStopId === itemId;\n\n useIsomorphicLayoutEffect(() => {\n focusContext.onItemRegister({\n id: itemId,\n ref: itemRef,\n disabled: !!disabled\n });\n\n if (!disabled) {\n focusContext.onFocusableItemAdd();\n }\n\n return () => {\n focusContext.onItemUnregister(itemId);\n if (!disabled) {\n focusContext.onFocusableItemRemove();\n }\n };\n }, [focusContext, itemId, disabled]);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<ItemElement>) => {\n onClickProp?.(event);\n if (event.defaultPrevented) return;\n\n const item = itemRef.current;\n if (!item) return;\n\n const itemSelectEvent = new CustomEvent(ITEM_SELECT, {\n bubbles: true,\n cancelable: true\n });\n\n item.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), {\n once: true\n });\n\n item.dispatchEvent(itemSelectEvent);\n\n if (!itemSelectEvent.defaultPrevented) {\n onOpenChange?.(false);\n }\n },\n [onClickProp, onOpenChange, onSelect]\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent<ItemElement>) => {\n onFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n focusContext.onItemFocus(itemId);\n isMouseClickRef.current = false;\n },\n [onFocusProp, focusContext, itemId]\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<ItemElement>) => {\n onKeyDownProp?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"Tab\" && event.shiftKey) {\n focusContext.onItemShiftTab();\n return;\n }\n\n if (event.target !== event.currentTarget) return;\n\n const key = getDirectionAwareKey(event.key, dir);\n let focusIntent: \"first\" | \"last\" | \"prev\" | \"next\" | undefined;\n\n if (orientation === \"horizontal\") {\n if (key === \"ArrowLeft\") focusIntent = \"prev\";\n else if (key === \"ArrowRight\") focusIntent = \"next\";\n else if (key === \"Home\") focusIntent = \"first\";\n else if (key === \"End\") focusIntent = \"last\";\n } else {\n if (key === \"ArrowUp\") focusIntent = \"prev\";\n else if (key === \"ArrowDown\") focusIntent = \"next\";\n else if (key === \"Home\") focusIntent = \"first\";\n else if (key === \"End\") focusIntent = \"last\";\n }\n\n if (focusIntent !== undefined) {\n if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;\n event.preventDefault();\n\n const items = focusContext.getItems().filter((item) => !item.disabled);\n let candidateRefs = items.map((item) => item.ref);\n\n if (focusIntent === \"last\") {\n candidateRefs.reverse();\n } else if (focusIntent === \"prev\" || focusIntent === \"next\") {\n if (focusIntent === \"prev\") candidateRefs.reverse();\n const currentIndex = candidateRefs.findIndex((ref) => ref.current === event.currentTarget);\n candidateRefs = loop ? wrapArray(candidateRefs, currentIndex + 1) : candidateRefs.slice(currentIndex + 1);\n }\n\n queueMicrotask(() => focusFirst(candidateRefs));\n }\n },\n [onKeyDownProp, focusContext, dir, orientation, loop]\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent<ItemElement>) => {\n onMouseDownProp?.(event);\n if (event.defaultPrevented) return;\n\n isMouseClickRef.current = true;\n\n if (disabled) {\n event.preventDefault();\n } else {\n focusContext.onItemFocus(itemId);\n }\n },\n [onMouseDownProp, focusContext, itemId, disabled]\n );\n\n return (\n <Button\n type=\"button\"\n data-slot=\"action-bar-item\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={disabled}\n tabIndex={isTabStop ? 0 : -1}\n {...itemProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `Button` của gói KHÔNG tự đặt cỡ `svg` con (khác shadcn gốc), nên\n * `<Copy />` trần trong mục ra 24px giữa một nút cao 32px. Đặt cỡ ở đây, đúng như\n * `DropdownMenuItem`/`CommandItem` của gói — không thêm luật đó vào `Button` (CHANGELOG 2.0.0).\n */\n \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n orientation === \"vertical\" && \"w-full\",\n className\n )}\n ref={composedRef}\n onClick={onClick}\n onFocus={onFocus}\n onKeyDown={onKeyDown}\n onMouseDown={onMouseDown}\n />\n );\n}\n\ninterface ActionBarCloseProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\n/**\n * Nút đóng thanh (thường là \"bỏ chọn\"). Không truyền `children` thì vẽ icon X.\n *\n * ⚠ Khác thượng nguồn: có tên cho trình đọc màn hình — `aria-label` mặc định `labels.close`\n * (\"Clear selection\"). Bản gốc chỉ render `children`, và khuôn dùng của chính thượng nguồn là\n * `<ActionBarClose><X /></ActionBarClose>`: một nút KHÔNG TÊN. Nếu `children` là CHỮ nhìn thấy\n * được thì truyền `aria-label` trùng với chữ đó (WCAG 2.5.3), hoặc `aria-label={undefined}`.\n */\nfunction ActionBarClose(props: ActionBarCloseProps) {\n const { asChild, className, onClick, children, ...closeProps } = props;\n\n const { onOpenChange, labels } = useActionBarContext(CLOSE_NAME);\n\n const onCloseClick = React.useCallback(\n (event: React.MouseEvent<CloseElement>) => {\n onClick?.(event);\n if (event.defaultPrevented) return;\n\n onOpenChange?.(false);\n },\n [onOpenChange, onClick]\n );\n\n const ClosePrimitive = asChild ? Slot : \"button\";\n\n return (\n <ClosePrimitive\n type=\"button\"\n aria-label={labels.close}\n data-slot=\"action-bar-close\"\n {...closeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: cùng khuôn với nút đóng của `Dialog` — `text-muted-foreground` →\n * `hover:text-foreground`, vòng focus `ring-ring/50` có `ring-offset`. Bản gốc\n * `opacity-70 hover:opacity-100` và `focus-visible:border-ring` (nút không có viền nên\n * dòng đó không vẽ gì).\n * ⚠ Và vùng bấm: icon 14px trần là 14×14px, dưới mức 24×24 của WCAG 2.5.8. `p-[5px]` nâng\n * vùng bấm lên 24px, `-m-[5px]` trả lại đúng chừng đó để chip chứa nó không phình ra.\n */\n \"-m-[5px] rounded-xs p-[5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5\",\n className\n )}\n onClick={onCloseClick}\n >\n {children ?? (asChild ? null : <X aria-hidden=\"true\" />)}\n </ClosePrimitive>\n );\n}\n\ninterface ActionBarSeparatorProps extends DivProps {\n orientation?: Orientation;\n}\n\nfunction ActionBarSeparator(props: ActionBarSeparatorProps) {\n const { orientation: orientationProp, asChild, className, ...separatorProps } = props;\n\n const context = useActionBarContext(SEPARATOR_NAME);\n const orientation = orientationProp ?? context.orientation;\n\n const SeparatorPrimitive = asChild ? Slot : \"div\";\n\n return (\n <SeparatorPrimitive\n /*\n * ⚠ Khác thượng nguồn: bỏ thuộc tính role \"separator\" và `aria-orientation` — bản gốc đặt\n * chúng CÙNG `aria-hidden=\"true\"`, tức phần tử đã ra khỏi cây a11y và hai thuộc tính kia\n * không tới được trình đọc màn hình. Để lại thì cổng tương phản đọc thuộc tính role như dấu\n * hiệu CONTROL (thanh chia `Resizable`) và bắt vạch trang trí 1px này chịu 3:1. Hướng vẫn\n * có ở `data-orientation`.\n */\n aria-hidden=\"true\"\n data-orientation={orientation}\n data-slot=\"action-bar-separator\"\n {...separatorProps}\n className={cn(\n \"bg-border in-data-[slot=action-bar-selection]:ml-0.5 in-data-[slot=action-bar-selection]:h-4 in-data-[slot=action-bar-selection]:w-px\",\n orientation === \"horizontal\" ? \"h-6 w-px\" : \"h-px w-full\",\n className\n )}\n />\n );\n}\n\nexport {\n ActionBar,\n ActionBarClose,\n type ActionBarCloseProps,\n ActionBarGroup,\n ActionBarItem,\n type ActionBarItemProps,\n type ActionBarProps,\n ActionBarSelection,\n type ActionBarSelectionProps,\n ActionBarSeparator,\n type ActionBarSeparatorProps\n};\n"],"mappings":"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","names":["tabStopId","event","ref"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/action-bar.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/action-bar) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n *\n * Dùng ở Comitor: thanh THAO TÁC HÀNG LOẠT nổi ở đáy màn hình khi có dòng được chọn trong\n * `DataTable` / `DataGrid` — `open={selectedIds.length > 0}`, và `onOpenChange(false)` xoá vùng chọn.\n */\n\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComitorLabels } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport type { ActionBarLabels } from \"./action-bar-constants.js\";\nimport { Button } from \"./button.js\";\n\nconst ROOT_NAME = \"ActionBar\";\nconst GROUP_NAME = \"ActionBarGroup\";\nconst ITEM_NAME = \"ActionBarItem\";\nconst CLOSE_NAME = \"ActionBarClose\";\nconst SELECTION_NAME = \"ActionBarSelection\";\nconst SEPARATOR_NAME = \"ActionBarSeparator\";\nconst ITEM_SELECT = \"actionbar.itemSelect\";\nconst ENTRY_FOCUS = \"actionbarFocusGroup.onEntryFocus\";\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof ActionBar>;\ntype ItemElement = React.ComponentRef<typeof ActionBarItem>;\ntype CloseElement = React.ComponentRef<typeof ActionBarClose>;\n\nfunction focusFirst(candidates: React.RefObject<HTMLElement | null>[], preventScroll = false) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidateRef of candidates) {\n const candidate = candidateRef.current;\n if (!candidate) continue;\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus({ preventScroll });\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\nfunction wrapArray<T>(array: T[], startIndex: number) {\n return array.map<T>((_, index) => array[(startIndex + index) % array.length] as T);\n}\n\nfunction getDirectionAwareKey(key: string, dir?: Direction) {\n if (dir !== \"rtl\") return key;\n return key === \"ArrowLeft\" ? \"ArrowRight\" : key === \"ArrowRight\" ? \"ArrowLeft\" : key;\n}\n\ninterface ItemData {\n id: string;\n ref: React.RefObject<ItemElement | null>;\n disabled: boolean;\n}\n\ninterface ActionBarContextValue {\n onOpenChange?: (open: boolean) => void;\n dir: Direction;\n orientation: Orientation;\n loop: boolean;\n labels: ActionBarLabels;\n}\n\nconst ActionBarContext = React.createContext<ActionBarContextValue | null>(null);\n\nfunction useActionBarContext(consumerName: string) {\n const context = React.useContext(ActionBarContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface FocusContextValue {\n tabStopId: string | null;\n onItemFocus: (tabStopId: string) => void;\n onItemShiftTab: () => void;\n onFocusableItemAdd: () => void;\n onFocusableItemRemove: () => void;\n onItemRegister: (item: ItemData) => void;\n onItemUnregister: (id: string) => void;\n getItems: () => ItemData[];\n}\n\nconst FocusContext = React.createContext<FocusContextValue | null>(null);\n\nfunction useFocusContext(consumerName: string) {\n const context = React.useContext(FocusContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`FocusProvider\\``);\n }\n return context;\n}\n\ninterface ActionBarProps extends DivProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n onEscapeKeyDown?: (event: KeyboardEvent) => void;\n align?: \"start\" | \"center\" | \"end\";\n alignOffset?: number;\n side?: \"top\" | \"bottom\";\n sideOffset?: number;\n portalContainer?: Element | DocumentFragment | null;\n dir?: Direction;\n orientation?: Orientation;\n loop?: boolean;\n /**\n * Chuỗi hiển thị và chuỗi a11y (tên thanh, dòng \"N selected\", tên nút đóng). Không truyền thì\n * theo namespace `actionBar` của `ComitorLocaleProvider` (mặc định tiếng Anh); truyền một phần\n * là đủ.\n *\n * ⚠ Thêm so với thượng nguồn: bản gốc không có chuỗi nào — thanh `role=\"toolbar\"` không tên và\n * nút đóng chỉ chứa `<X />` là một nút KHÔNG TÊN với trình đọc màn hình.\n */\n labels?: Partial<ActionBarLabels>;\n}\n\nfunction ActionBar(props: ActionBarProps) {\n const {\n open = false,\n onOpenChange,\n onEscapeKeyDown,\n side = \"bottom\",\n alignOffset = 0,\n align = \"center\",\n sideOffset = 16,\n portalContainer: portalContainerProp,\n dir: dirProp,\n orientation = \"horizontal\",\n loop = true,\n labels: labelsProp,\n onFocus: onFocusProp,\n onBlur: onBlurProp,\n className,\n style,\n ref,\n asChild,\n ...rootProps\n } = props;\n\n const [mounted, setMounted] = React.useState(false);\n\n const rootRef = React.useRef<RootElement>(null);\n const composedRef = useComposedRefs(ref, rootRef);\n\n const propsRef = useAsRef({\n onEscapeKeyDown,\n onOpenChange\n });\n\n /*\n * ⚠ Khác thượng nguồn (3.0.1): TRẢ FOCUS khi thanh đóng. Đóng là cả cây bị gỡ khỏi DOM (`return null`),\n * và `ActionBarItem` mặc định gọi `onOpenChange(false)` sau khi chọn — nên người dùng bàn phím vừa\n * bấm Enter trên \"Xoá\" (hay nút đóng, hay Escape) thấy focus rơi xuống `<body>`: Tab kế tiếp chạy\n * lại từ đầu trang (WCAG 2.4.3). Bản gốc để rơi như vậy.\n * Ghi phần tử giữ focus NGAY TRƯỚC khi focus vào thanh, rồi trả về đó nếu lúc đóng focus đang ở\n * trong thanh. App tự đưa focus đi nơi khác (trong handler hay trong effect) thì app thắng.\n */\n const returnFocusRef = React.useRef<HTMLElement | null>(null);\n const focusInsideRef = React.useRef(false);\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent<RootElement>) => {\n onFocusProp?.(event);\n\n /*\n * Chỉ ghi khi focus ĐI VÀO từ ngoài. Menu mở từ một nút trong thanh là con React nhưng nằm ở\n * portal khác: focus từ menu quay về nút có `relatedTarget` ngoài thanh (theo DOM), và không\n * được ghi đè phần tử thật sự đứng trước thanh.\n */\n const from = event.relatedTarget;\n if (!focusInsideRef.current && from instanceof HTMLElement && !event.currentTarget.contains(from)) {\n returnFocusRef.current = from;\n }\n focusInsideRef.current = true;\n },\n [onFocusProp]\n );\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent<RootElement>) => {\n onBlurProp?.(event);\n\n const to = event.relatedTarget;\n if (!(to instanceof Node) || !event.currentTarget.contains(to)) {\n focusInsideRef.current = false;\n }\n },\n [onBlurProp]\n );\n\n useIsomorphicLayoutEffect(() => {\n if (!open) return;\n\n return () => {\n const returnTarget = returnFocusRef.current;\n const hadFocus = focusInsideRef.current;\n returnFocusRef.current = null;\n focusInsideRef.current = false;\n if (!hadFocus || !returnTarget) return;\n\n /*\n * Cleanup này chạy GIỮA lượt commit, lúc React tạm tắt hệ sự kiện: focus ngay tại đây thì\n * `onFocus` của phần tử nhận focus không chạy (roving focus, ô lưới đang chọn… lệch trạng\n * thái). Đợi commit xong — effect của app cũng đã chạy — và chỉ trả khi focus vẫn đang rơi.\n */\n queueMicrotask(() => {\n const ownerDocument = returnTarget.ownerDocument;\n const active = ownerDocument.activeElement;\n if (active && active !== ownerDocument.body && active.isConnected) return;\n if (!returnTarget.isConnected) return;\n returnTarget.focus({ preventScroll: true });\n });\n };\n }, [open]);\n\n const dir = useDirection(dirProp);\n\n const labels = useComitorLabels(\"actionBar\", labelsProp);\n\n // Chỉ đọc `document.body` SAU khi mount — lượt render phía server không chạm `document`.\n useIsomorphicLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n React.useEffect(() => {\n if (!open) return;\n\n const ownerDocument = rootRef.current?.ownerDocument ?? document;\n\n function onKeyDown(event: KeyboardEvent) {\n /*\n * ⚠ Khác thượng nguồn: bỏ qua phím Escape ĐÃ được xử lý (`defaultPrevented`) hoặc đang gõ dở\n * bằng bộ gõ (`isComposing` — Telex/VNI qua IME hệ điều hành). Bản gốc nghe Escape ở cấp\n * `document`, nên Escape để đóng một Dialog/Popover/DropdownMenu của Radix (các lớp đó\n * `preventDefault()` khi tự đóng) cũng đóng luôn thanh này — tức XOÁ VÙNG CHỌN của người dùng\n * chỉ vì họ đóng một menu.\n */\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n\n propsRef.current.onEscapeKeyDown?.(event);\n if (!event.defaultPrevented) {\n propsRef.current.onOpenChange?.(false);\n }\n }\n\n ownerDocument.addEventListener(\"keydown\", onKeyDown);\n return () => ownerDocument.removeEventListener(\"keydown\", onKeyDown);\n }, [open, propsRef]);\n\n const contextValue = React.useMemo<ActionBarContextValue>(\n () => ({\n onOpenChange,\n dir,\n orientation,\n loop,\n labels\n }),\n [onOpenChange, dir, orientation, loop, labels]\n );\n\n const portalContainer = portalContainerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!portalContainer || !open) return null;\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ActionBarContext.Provider value={contextValue}>\n {ReactDOM.createPortal(\n <RootPrimitive\n role=\"toolbar\"\n aria-label={labels.toolbar}\n aria-orientation={orientation}\n data-slot=\"action-bar\"\n data-side={side}\n data-align={align}\n data-orientation={orientation}\n dir={dir}\n {...rootProps}\n ref={composedRef}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: mặt nổi đi qua bộ token `popover` + `shadow-overlay` như mọi lớp\n * nổi của gói (Popover, DropdownMenu). Bản gốc `bg-card shadow-lg` — `shadow-lg` là\n * bóng mặc định của Tailwind, không có bản dark của gói (xem CLAUDE.md \"Bóng\").\n */\n \"fixed z-50 rounded-lg border bg-popover text-popover-foreground shadow-overlay outline-none\",\n \"animate-in duration-250 fade-in-0 zoom-in-95 [animation-timing-function:cubic-bezier(0.16,1,0.3,1)]\",\n \"data-[side=bottom]:slide-in-from-bottom-4 data-[side=top]:slide-in-from-top-4\",\n \"motion-reduce:animate-none motion-reduce:transition-none\",\n /*\n * ⚠ Khác thượng nguồn: chặn bề rộng theo khung nhìn và cho hàng ngang XUỐNG DÒNG. Bản gốc\n * không có giới hạn nào — đo ở 375px: thanh rộng 413px, bắt đầu ở x = −19px, hai mép bị\n * cắt và \"Đã chọn 3\" vỡ thành 3 dòng. Xuống dòng chứ không cuộn ngang: vùng cuộn sẽ cắt\n * mất vòng focus của các nút ở mép.\n */\n \"max-w-[calc(100vw-1rem)]\",\n orientation === \"horizontal\"\n ? \"flex flex-row flex-wrap items-center gap-2 px-2 py-1.5\"\n : \"flex flex-col items-start gap-2 px-1.5 py-2\",\n className\n )}\n style={{\n [side]: `${sideOffset}px`,\n ...(align === \"center\" && {\n left: \"50%\",\n translate: \"-50% 0\"\n }),\n ...(align === \"start\" && { left: `${alignOffset}px` }),\n ...(align === \"end\" && { right: `${alignOffset}px` }),\n ...style\n }}\n onFocus={onFocus}\n onBlur={onBlur}\n />,\n portalContainer\n )}\n </ActionBarContext.Provider>\n );\n}\n\ninterface ActionBarSelectionProps extends DivProps {\n /**\n * Số mục đang chọn. Có giá trị thì phần tử tự in `labels.selected(count)` (\"3 selected\") TRƯỚC\n * `children` — nên `children` chỉ còn là dấu ngăn + nút đóng.\n *\n * ⚠ Thêm so với thượng nguồn: bản gốc để app tự viết chữ (\"{n} selected\"), tức chuỗi hiển thị\n * không đi qua `labels`. Bỏ qua khi `asChild` (Slot chỉ nhận đúng MỘT phần tử con).\n */\n count?: number;\n}\n\nfunction ActionBarSelection(props: ActionBarSelectionProps) {\n const { className, asChild, count, children, ...selectionProps } = props;\n\n const { labels } = useActionBarContext(SELECTION_NAME);\n\n const SelectionPrimitive = asChild ? Slot : \"div\";\n\n return (\n <SelectionPrimitive\n data-slot=\"action-bar-selection\"\n {...selectionProps}\n // ⚠ Khác thượng nguồn: `whitespace-nowrap shrink-0` — con số đếm không được vỡ dòng khi thanh hẹp.\n className={cn(\n \"flex shrink-0 items-center gap-1 whitespace-nowrap rounded-sm border px-2 py-1 text-sm font-medium tabular-nums\",\n className\n )}\n >\n {asChild ? (\n children\n ) : (\n <>\n {count !== undefined && labels.selected(count)}\n {children}\n </>\n )}\n </SelectionPrimitive>\n );\n}\n\nfunction ActionBarGroup(props: DivProps) {\n const {\n onBlur: onBlurProp,\n onFocus: onFocusProp,\n onMouseDown: onMouseDownProp,\n className,\n asChild,\n ref,\n ...groupProps\n } = props;\n\n const [tabStopId, setTabStopId] = React.useState<string | null>(null);\n const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false);\n const [focusableItemCount, setFocusableItemCount] = React.useState(0);\n\n const groupRef = React.useRef<HTMLDivElement>(null);\n const composedRef = useComposedRefs(ref, groupRef);\n const isClickFocusRef = React.useRef(false);\n const itemsRef = React.useRef<Map<string, ItemData>>(new Map());\n\n const { dir, orientation } = useActionBarContext(GROUP_NAME);\n\n const onItemFocus = React.useCallback((tabStopId: string) => {\n setTabStopId(tabStopId);\n }, []);\n\n const onItemShiftTab = React.useCallback(() => {\n setIsTabbingBackOut(true);\n }, []);\n\n const onFocusableItemAdd = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount + 1);\n }, []);\n\n const onFocusableItemRemove = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount - 1);\n }, []);\n\n const onItemRegister = React.useCallback((item: ItemData) => {\n itemsRef.current.set(item.id, item);\n }, []);\n\n const onItemUnregister = React.useCallback((id: string) => {\n itemsRef.current.delete(id);\n }, []);\n\n const getItems = React.useCallback(() => {\n return Array.from(itemsRef.current.values())\n .filter((item) => item.ref.current)\n .sort((a, b) => {\n const elementA = a.ref.current;\n const elementB = b.ref.current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n return -1;\n }\n if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n return 1;\n }\n return 0;\n });\n }, []);\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent<HTMLDivElement>) => {\n onBlurProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsTabbingBackOut(false);\n },\n [onBlurProp]\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent<HTMLDivElement>) => {\n onFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n const isKeyboardFocus = !isClickFocusRef.current;\n if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) {\n const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);\n event.currentTarget.dispatchEvent(entryFocusEvent);\n\n if (!entryFocusEvent.defaultPrevented) {\n const items = Array.from(itemsRef.current.values()).filter((item) => !item.disabled);\n const currentItem = items.find((item) => item.id === tabStopId);\n\n const candidateItems = [currentItem, ...items].filter(Boolean) as ItemData[];\n const candidateRefs = candidateItems.map((item) => item.ref);\n focusFirst(candidateRefs, false);\n }\n }\n isClickFocusRef.current = false;\n },\n [onFocusProp, isTabbingBackOut, tabStopId]\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n onMouseDownProp?.(event);\n if (event.defaultPrevented) return;\n\n isClickFocusRef.current = true;\n },\n [onMouseDownProp]\n );\n\n const focusContextValue = React.useMemo<FocusContextValue>(\n () => ({\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems\n }),\n [\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems\n ]\n );\n\n const GroupPrimitive = asChild ? Slot : \"div\";\n\n return (\n <FocusContext.Provider value={focusContextValue}>\n <GroupPrimitive\n role=\"group\"\n data-slot=\"action-bar-group\"\n data-orientation={orientation}\n dir={dir}\n tabIndex={isTabbingBackOut || focusableItemCount === 0 ? -1 : 0}\n {...groupProps}\n ref={composedRef}\n className={cn(\n \"flex gap-2 outline-none\",\n orientation === \"horizontal\" ? \"items-center\" : \"w-full flex-col items-start\",\n className\n )}\n onBlur={onBlur}\n onFocus={onFocus}\n onMouseDown={onMouseDown}\n />\n </FocusContext.Provider>\n );\n}\n\ninterface ActionBarItemProps extends Omit<React.ComponentProps<typeof Button>, \"onSelect\"> {\n onSelect?: (event: Event) => void;\n}\n\nfunction ActionBarItem(props: ActionBarItemProps) {\n const {\n onSelect,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n onMouseDown: onMouseDownProp,\n className,\n disabled,\n ref,\n ...itemProps\n } = props;\n\n const itemRef = React.useRef<ItemElement>(null);\n const composedRef = useComposedRefs(ref, itemRef);\n const isMouseClickRef = React.useRef(false);\n\n const { onOpenChange, dir, orientation, loop } = useActionBarContext(ITEM_NAME);\n const focusContext = useFocusContext(ITEM_NAME);\n\n const itemId = React.useId();\n const isTabStop = focusContext.tabStopId === itemId;\n\n useIsomorphicLayoutEffect(() => {\n focusContext.onItemRegister({\n id: itemId,\n ref: itemRef,\n disabled: !!disabled\n });\n\n if (!disabled) {\n focusContext.onFocusableItemAdd();\n }\n\n return () => {\n focusContext.onItemUnregister(itemId);\n if (!disabled) {\n focusContext.onFocusableItemRemove();\n }\n };\n }, [focusContext, itemId, disabled]);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<ItemElement>) => {\n onClickProp?.(event);\n if (event.defaultPrevented) return;\n\n const item = itemRef.current;\n if (!item) return;\n\n const itemSelectEvent = new CustomEvent(ITEM_SELECT, {\n bubbles: true,\n cancelable: true\n });\n\n item.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), {\n once: true\n });\n\n item.dispatchEvent(itemSelectEvent);\n\n if (!itemSelectEvent.defaultPrevented) {\n onOpenChange?.(false);\n }\n },\n [onClickProp, onOpenChange, onSelect]\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent<ItemElement>) => {\n onFocusProp?.(event);\n if (event.defaultPrevented) return;\n\n focusContext.onItemFocus(itemId);\n isMouseClickRef.current = false;\n },\n [onFocusProp, focusContext, itemId]\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<ItemElement>) => {\n onKeyDownProp?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"Tab\" && event.shiftKey) {\n focusContext.onItemShiftTab();\n return;\n }\n\n if (event.target !== event.currentTarget) return;\n\n const key = getDirectionAwareKey(event.key, dir);\n let focusIntent: \"first\" | \"last\" | \"prev\" | \"next\" | undefined;\n\n if (orientation === \"horizontal\") {\n if (key === \"ArrowLeft\") focusIntent = \"prev\";\n else if (key === \"ArrowRight\") focusIntent = \"next\";\n else if (key === \"Home\") focusIntent = \"first\";\n else if (key === \"End\") focusIntent = \"last\";\n } else {\n if (key === \"ArrowUp\") focusIntent = \"prev\";\n else if (key === \"ArrowDown\") focusIntent = \"next\";\n else if (key === \"Home\") focusIntent = \"first\";\n else if (key === \"End\") focusIntent = \"last\";\n }\n\n if (focusIntent !== undefined) {\n if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;\n event.preventDefault();\n\n const items = focusContext.getItems().filter((item) => !item.disabled);\n let candidateRefs = items.map((item) => item.ref);\n\n if (focusIntent === \"last\") {\n candidateRefs.reverse();\n } else if (focusIntent === \"prev\" || focusIntent === \"next\") {\n if (focusIntent === \"prev\") candidateRefs.reverse();\n const currentIndex = candidateRefs.findIndex((ref) => ref.current === event.currentTarget);\n candidateRefs = loop ? wrapArray(candidateRefs, currentIndex + 1) : candidateRefs.slice(currentIndex + 1);\n }\n\n queueMicrotask(() => focusFirst(candidateRefs));\n }\n },\n [onKeyDownProp, focusContext, dir, orientation, loop]\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent<ItemElement>) => {\n onMouseDownProp?.(event);\n if (event.defaultPrevented) return;\n\n isMouseClickRef.current = true;\n\n if (disabled) {\n event.preventDefault();\n } else {\n focusContext.onItemFocus(itemId);\n }\n },\n [onMouseDownProp, focusContext, itemId, disabled]\n );\n\n return (\n <Button\n type=\"button\"\n data-slot=\"action-bar-item\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={disabled}\n tabIndex={isTabStop ? 0 : -1}\n {...itemProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `Button` của gói KHÔNG tự đặt cỡ `svg` con (khác shadcn gốc), nên\n * `<Copy />` trần trong mục ra 24px giữa một nút cao 32px. Đặt cỡ ở đây, đúng như\n * `DropdownMenuItem`/`CommandItem` của gói — không thêm luật đó vào `Button` (CHANGELOG 2.0.0).\n */\n \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n orientation === \"vertical\" && \"w-full\",\n className\n )}\n ref={composedRef}\n onClick={onClick}\n onFocus={onFocus}\n onKeyDown={onKeyDown}\n onMouseDown={onMouseDown}\n />\n );\n}\n\ninterface ActionBarCloseProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\n/**\n * Nút đóng thanh (thường là \"bỏ chọn\"). Không truyền `children` thì vẽ icon X.\n *\n * ⚠ Khác thượng nguồn: có tên cho trình đọc màn hình — `aria-label` mặc định `labels.close`\n * (\"Clear selection\"). Bản gốc chỉ render `children`, và khuôn dùng của chính thượng nguồn là\n * `<ActionBarClose><X /></ActionBarClose>`: một nút KHÔNG TÊN. Nếu `children` là CHỮ nhìn thấy\n * được thì truyền `aria-label` trùng với chữ đó (WCAG 2.5.3), hoặc `aria-label={undefined}`.\n */\nfunction ActionBarClose(props: ActionBarCloseProps) {\n const { asChild, className, onClick, children, ...closeProps } = props;\n\n const { onOpenChange, labels } = useActionBarContext(CLOSE_NAME);\n\n const onCloseClick = React.useCallback(\n (event: React.MouseEvent<CloseElement>) => {\n onClick?.(event);\n if (event.defaultPrevented) return;\n\n onOpenChange?.(false);\n },\n [onOpenChange, onClick]\n );\n\n const ClosePrimitive = asChild ? Slot : \"button\";\n\n return (\n <ClosePrimitive\n type=\"button\"\n aria-label={labels.close}\n data-slot=\"action-bar-close\"\n {...closeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: cùng khuôn với nút đóng của `Dialog` — `text-muted-foreground` →\n * `hover:text-foreground`, vòng focus `ring-ring/50` có `ring-offset`. Bản gốc\n * `opacity-70 hover:opacity-100` và `focus-visible:border-ring` (nút không có viền nên\n * dòng đó không vẽ gì).\n * ⚠ Và vùng bấm: icon 14px trần là 14×14px, dưới mức 24×24 của WCAG 2.5.8. `p-[5px]` nâng\n * vùng bấm lên 24px, `-m-[5px]` trả lại đúng chừng đó để chip chứa nó không phình ra.\n */\n \"-m-[5px] rounded-xs p-[5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5\",\n className\n )}\n onClick={onCloseClick}\n >\n {children ?? (asChild ? null : <X aria-hidden=\"true\" />)}\n </ClosePrimitive>\n );\n}\n\ninterface ActionBarSeparatorProps extends DivProps {\n orientation?: Orientation;\n}\n\nfunction ActionBarSeparator(props: ActionBarSeparatorProps) {\n const { orientation: orientationProp, asChild, className, ...separatorProps } = props;\n\n const context = useActionBarContext(SEPARATOR_NAME);\n const orientation = orientationProp ?? context.orientation;\n\n const SeparatorPrimitive = asChild ? Slot : \"div\";\n\n return (\n <SeparatorPrimitive\n /*\n * ⚠ Khác thượng nguồn: bỏ thuộc tính role \"separator\" và `aria-orientation` — bản gốc đặt\n * chúng CÙNG `aria-hidden=\"true\"`, tức phần tử đã ra khỏi cây a11y và hai thuộc tính kia\n * không tới được trình đọc màn hình. Để lại thì cổng tương phản đọc thuộc tính role như dấu\n * hiệu CONTROL (thanh chia `Resizable`) và bắt vạch trang trí 1px này chịu 3:1. Hướng vẫn\n * có ở `data-orientation`.\n */\n aria-hidden=\"true\"\n data-orientation={orientation}\n data-slot=\"action-bar-separator\"\n {...separatorProps}\n className={cn(\n \"bg-border in-data-[slot=action-bar-selection]:ml-0.5 in-data-[slot=action-bar-selection]:h-4 in-data-[slot=action-bar-selection]:w-px\",\n orientation === \"horizontal\" ? \"h-6 w-px\" : \"h-px w-full\",\n className\n )}\n />\n );\n}\n\nexport {\n ActionBar,\n ActionBarClose,\n type ActionBarCloseProps,\n ActionBarGroup,\n ActionBarItem,\n type ActionBarItemProps,\n type ActionBarProps,\n ActionBarSelection,\n type ActionBarSelectionProps,\n ActionBarSeparator,\n type ActionBarSeparatorProps\n};\n"],"mappings":"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","names":["tabStopId","event","ref"]}
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
6
7
|
const CIRCULAR_PROGRESS_NAME = "CircularProgress";
|
|
7
8
|
const INDICATOR_NAME = "CircularProgressIndicator";
|
|
8
9
|
const TRACK_NAME = "CircularProgressTrack";
|
|
@@ -81,6 +82,7 @@ function CircularProgress(props) {
|
|
|
81
82
|
const percentage = getIsValidNumber(value) ? max === min ? 1 : (value - min) / (max - min) : null;
|
|
82
83
|
const labelId = React.useId();
|
|
83
84
|
const valueTextId = React.useId();
|
|
85
|
+
const [hasValueText, setHasValueText] = React.useState(false);
|
|
84
86
|
const contextValue = React.useMemo(
|
|
85
87
|
() => ({
|
|
86
88
|
value,
|
|
@@ -94,7 +96,8 @@ function CircularProgress(props) {
|
|
|
94
96
|
center,
|
|
95
97
|
circumference,
|
|
96
98
|
percentage,
|
|
97
|
-
valueTextId
|
|
99
|
+
valueTextId,
|
|
100
|
+
onValueTextMountChange: setHasValueText
|
|
98
101
|
}),
|
|
99
102
|
[value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]
|
|
100
103
|
);
|
|
@@ -103,7 +106,7 @@ function CircularProgress(props) {
|
|
|
103
106
|
RootPrimitive,
|
|
104
107
|
{
|
|
105
108
|
role: "progressbar",
|
|
106
|
-
"aria-describedby": valueText ? valueTextId : void 0,
|
|
109
|
+
"aria-describedby": valueText && hasValueText ? valueTextId : void 0,
|
|
107
110
|
"aria-labelledby": label ? labelId : void 0,
|
|
108
111
|
"aria-valuemax": max,
|
|
109
112
|
"aria-valuemin": min,
|
|
@@ -233,6 +236,11 @@ function CircularProgressRange(props) {
|
|
|
233
236
|
function CircularProgressValueText(props) {
|
|
234
237
|
const { asChild, className, children, ...valueTextProps } = props;
|
|
235
238
|
const context = useCircularProgressContext(VALUE_TEXT_NAME);
|
|
239
|
+
const { onValueTextMountChange } = context;
|
|
240
|
+
useIsomorphicLayoutEffect(() => {
|
|
241
|
+
onValueTextMountChange(true);
|
|
242
|
+
return () => onValueTextMountChange(false);
|
|
243
|
+
}, [onValueTextMountChange]);
|
|
236
244
|
const ValueTextPrimitive = asChild ? Slot : "span";
|
|
237
245
|
return /* @__PURE__ */ jsx(
|
|
238
246
|
ValueTextPrimitive,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/circular-progress.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/circular-progress) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\nconst CIRCULAR_PROGRESS_NAME = \"CircularProgress\";\nconst INDICATOR_NAME = \"CircularProgressIndicator\";\nconst TRACK_NAME = \"CircularProgressTrack\";\nconst RANGE_NAME = \"CircularProgressRange\";\nconst VALUE_TEXT_NAME = \"CircularProgressValueText\";\n\nconst DEFAULT_MAX = 100;\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc đọc thẳng `process.env.NODE_ENV` ngay trong lúc render. Gói ra ESM\n * từng file, không bundle; ở môi trường không có bundler thay chuỗi đó (ESM thuần trên trình\n * duyệt), `process` không tồn tại → ReferenceError lúc render. Bọc `typeof` để chỗ đó chỉ mất cảnh\n * báo dev chứ không gãy; bundler vẫn thay `process.env.NODE_ENV` và loại nhánh dev như cũ.\n */\nconst IS_DEV = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\";\n\ntype ProgressState = \"indeterminate\" | \"complete\" | \"loading\";\n\nfunction getProgressState(value: number | undefined | null, maxValue: number): ProgressState {\n return value == null ? \"indeterminate\" : value === maxValue ? \"complete\" : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(value: unknown, min: number, max: number): value is number {\n return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n return `${Math.round(percentage)}%`;\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`value\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. \\`value\\` phải là số trong khoảng \\`min\\`…\\`max\\` (tính cả hai đầu), hoặc \\`null\\`/\\`undefined\\` cho tiến trình không xác định. Giá trị sẽ bị kẹp về khoảng hợp lệ.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`max\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. Chỉ nhận số lớn hơn 0 — dùng mặc định ${DEFAULT_MAX}.`;\n}\n\ninterface CircularProgressContextValue {\n value: number | null;\n valueText: string | undefined;\n max: number;\n min: number;\n state: ProgressState;\n radius: number;\n thickness: number;\n size: number;\n center: number;\n circumference: number;\n percentage: number | null;\n valueTextId?: string;\n}\n\nconst CircularProgressContext = React.createContext<CircularProgressContextValue | null>(null);\n\nfunction useCircularProgressContext(consumerName: string) {\n const context = React.useContext(CircularProgressContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${CIRCULAR_PROGRESS_NAME}\\``);\n }\n return context;\n}\n\ninterface CircularProgressProps extends React.ComponentProps<\"div\"> {\n /** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */\n value?: number | null | undefined;\n /**\n * Chuỗi trình đọc màn hình đọc thay cho con số (`aria-valuetext`) và chữ giữa vòng — mặc định\n * phần trăm làm tròn, vd `\"45%\"`.\n */\n getValueText?(value: number, min: number, max: number): string;\n min?: number;\n max?: number;\n /** Đường kính, px. */\n size?: number;\n /** Độ dày nét, px. */\n thickness?: number;\n /**\n * Nhãn HIỆN của vòng, đồng thời là tên của `role=\"progressbar\"`. Không đặt nhãn hiện thì PHẢI\n * truyền `aria-label` — progressbar không tên là lỗi a11y (ARIA 1.2), gói không tự đặt tên\n * chung chung thay app (giống `Progress`).\n */\n label?: string;\n asChild?: boolean;\n}\n\n/**\n * CircularProgress — tiến trình dạng VÒNG, cặp với `Progress` (dạng thanh) của gói.\n * Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),\n * hoặc dùng sẵn `CircularProgressCombined`.\n *\n * Phần đã chạy là `text-primary-ink`, cùng bậc với thanh `Progress`; rãnh xám trung tính. Đổi tông\n * thì truyền bậc INK cho `CircularProgressRange` (vd `className=\"text-success-ink\"`), không phải\n * token tô nền `text-success` — rãnh không cần đổi theo (số đo ở `CircularProgressTrack`).\n *\n * ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong\n * styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).\n */\nfunction CircularProgress(props: CircularProgressProps) {\n const {\n value: valueProp = null,\n getValueText = getDefaultValueText,\n min: minProp = 0,\n max: maxProp,\n size = 48,\n thickness = 4,\n label,\n asChild,\n className,\n children,\n ...rootProps\n } = props;\n\n if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n if (IS_DEV) {\n console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n const min = getIsValidNumber(minProp) ? minProp : 0;\n const max = rawMax <= min ? min + 1 : rawMax;\n\n if (IS_DEV && thickness >= size) {\n console.warn(\n `CircularProgress: \\`thickness\\` (${thickness}) phải nhỏ hơn \\`size\\` (${size}) thì vòng mới vẽ đúng.`\n );\n }\n\n if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n if (IS_DEV) {\n console.error(getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const value = getIsValidValueNumber(valueProp, min, max)\n ? valueProp\n : getIsValidNumber(valueProp) && valueProp > max\n ? max\n : getIsValidNumber(valueProp) && valueProp < min\n ? min\n : null;\n\n const valueText = getIsValidNumber(value) ? getValueText(value, min, max) : undefined;\n const state = getProgressState(value, max);\n const radius = Math.max(0, (size - thickness) / 2);\n const center = size / 2;\n const circumference = 2 * Math.PI * radius;\n\n const percentage = getIsValidNumber(value) ? (max === min ? 1 : (value - min) / (max - min)) : null;\n\n const labelId = React.useId();\n const valueTextId = React.useId();\n\n const contextValue = React.useMemo<CircularProgressContextValue>(\n () => ({\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n valueTextId\n }),\n [value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <CircularProgressContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"progressbar\"\n aria-describedby={valueText ? valueTextId : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-valuemax={max}\n aria-valuemin={min}\n aria-valuenow={getIsValidNumber(value) ? value : undefined}\n aria-valuetext={valueText}\n data-state={state}\n data-value={value ?? undefined}\n data-max={max}\n data-min={min}\n data-percentage={percentage}\n data-slot=\"circular-progress\"\n {...rootProps}\n className={cn(\"relative inline-flex w-fit items-center justify-center\", label && \"flex-col gap-1.5\", className)}\n >\n {/*\n * ⚠ Khác thượng nguồn: bản gốc đặt nhãn làm anh em của vòng NGAY TRONG gốc, trong khi\n * `CircularProgressValueText` là `absolute inset-0` phủ CẢ gốc — có nhãn là chữ \"65%\" đè\n * lên chữ nhãn. Khi có `label`: bọc phần con trong một khung đúng cỡ vòng (`relative`, nên\n * chữ giá trị căn giữa VÒNG), nhãn xuống dưới vòng. Không có `label` thì DOM y như bản gốc.\n */}\n {label ? (\n <div data-slot=\"circular-progress-ring\" className=\"relative inline-flex items-center justify-center\">\n {children}\n </div>\n ) : (\n children\n )}\n {label && (\n <div id={labelId} data-slot=\"circular-progress-label\" className=\"text-sm\">\n {label}\n </div>\n )}\n </RootPrimitive>\n </CircularProgressContext.Provider>\n );\n}\n\nfunction CircularProgressIndicator(props: React.ComponentProps<\"svg\">) {\n const { className, ...indicatorProps } = props;\n\n const context = useCircularProgressContext(INDICATOR_NAME);\n\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n viewBox={`0 0 ${context.size} ${context.size}`}\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-percentage={context.percentage}\n data-slot=\"circular-progress-indicator\"\n width={context.size}\n height={context.size}\n {...indicatorProps}\n className={cn(\"-rotate-90 transform\", className)}\n />\n );\n}\n\nCircularProgressIndicator.displayName = INDICATOR_NAME;\n\nfunction CircularProgressTrack(props: React.ComponentProps<\"circle\">) {\n const { className, ...trackProps } = props;\n\n const context = useCircularProgressContext(TRACK_NAME);\n\n return (\n <circle\n data-state={context.state}\n data-slot=\"circular-progress-track\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n {...trackProps}\n className={cn(\n /*\n * GIỮ NGUYÊN thượng nguồn — rãnh xám trung tính `text-muted-foreground/20`, KHÔNG phải tint\n * `/20` của bậc ink như rãnh `Progress`. Đã đo cả hai; thứ mang THÔNG TIN ở đây là ranh\n * giới cung-đã-chạy ⇄ rãnh (chỗ cung dừng lại chính là giá trị), và rãnh trung tính giữ\n * ranh giới đó TỐT HƠN (bảng tương phản cao, cung `text-primary-ink`):\n *\n * rãnh trung tính tint `primary-ink/20`\n * cung ⇄ rãnh trên --background : 5,03 (L) · 9,65 (D) 4,85 (L) · 7,97 (D)\n * tệ nhất, rãnh trên --muted : 4,71 (L) · 8,00 (D) 4,54 (L) · 6,52 (D)\n * (viền trang trí --border : 2,62 (L) ✗ — loại)\n *\n * Và nó không phụ thuộc tông: app đổi cung sang `text-success-ink`/`text-destructive-ink`\n * thì rãnh vẫn đúng (5,03 · 5,51:1 trên nền trắng) mà không phải đổi theo.\n *\n * Rãnh ⇄ nền trang chỉ ~1,3:1 — CỐ Ý, như rãnh `Progress`: không có cặp màu nào vừa ≥3:1\n * với nhau vừa ≥3:1 với nền trắng (chứng minh ở chú thích `Slider`). Khác `Progress` ở chỗ\n * vòng KHÔNG cần viền khoanh mốc 100%: hình tròn tự khép và cung luôn bắt đầu ở 12 giờ, nên\n * TỈ LỆ đọc được từ góc của cung mà không cần thấy rãnh. Một nét tròn liền, không nét nào\n * cắt nhau, nên màu có alpha không để lộ vệt nối (xem \"Cạm bẫy\" về icon trong CLAUDE.md).\n */\n \"text-muted-foreground/20\",\n className\n )}\n />\n );\n}\n\nfunction CircularProgressRange(props: React.ComponentProps<\"circle\">) {\n const { className, ...rangeProps } = props;\n\n const context = useCircularProgressContext(RANGE_NAME);\n\n const strokeDasharray = context.circumference;\n const strokeDashoffset =\n context.state === \"indeterminate\"\n ? context.circumference * 0.75\n : context.percentage !== null\n ? context.circumference - context.percentage * context.circumference\n : context.circumference;\n\n return (\n <circle\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-slot=\"circular-progress-range\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n strokeDasharray={strokeDasharray}\n strokeDashoffset={strokeDashoffset}\n vectorEffect=\"non-scaling-stroke\"\n {...rangeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN, gold #E8B824 chỉ 1,86:1 trên nền trắng)\n * → `text-primary-ink`, đúng bậc của thanh `Progress`: 6,51:1 (light) · 12,67:1 (dark)\n * trên --background, tệ nhất 6,09:1 · 10,80:1 trên --muted (bảng tương phản cao). Cung là\n * thông tin thị giác (WCAG 1.4.11, 3:1).\n * Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.\n */\n \"origin-center text-primary-ink transition-all duration-300 ease-in-out\",\n context.state === \"indeterminate\" &&\n \"motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none\",\n className\n )}\n />\n );\n}\n\ninterface CircularProgressValueTextProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction CircularProgressValueText(props: CircularProgressValueTextProps) {\n const { asChild, className, children, ...valueTextProps } = props;\n\n const context = useCircularProgressContext(VALUE_TEXT_NAME);\n\n const ValueTextPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ValueTextPrimitive\n id={context.valueTextId}\n data-state={context.state}\n data-slot=\"circular-progress-value-text\"\n {...valueTextProps}\n className={cn(\"absolute inset-0 flex items-center justify-center text-sm font-medium\", className)}\n >\n {children ?? context.valueText}\n </ValueTextPrimitive>\n );\n}\n\nfunction CircularProgressCombined(props: CircularProgressProps) {\n return (\n <CircularProgress {...props}>\n <CircularProgressIndicator>\n <CircularProgressTrack />\n <CircularProgressRange />\n </CircularProgressIndicator>\n <CircularProgressValueText />\n </CircularProgress>\n );\n}\n\nexport {\n CircularProgress,\n CircularProgressCombined,\n CircularProgressIndicator,\n type CircularProgressProps,\n CircularProgressRange,\n CircularProgressTrack,\n CircularProgressValueText,\n type CircularProgressValueTextProps\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/circular-progress.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/circular-progress) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\n\nconst CIRCULAR_PROGRESS_NAME = \"CircularProgress\";\nconst INDICATOR_NAME = \"CircularProgressIndicator\";\nconst TRACK_NAME = \"CircularProgressTrack\";\nconst RANGE_NAME = \"CircularProgressRange\";\nconst VALUE_TEXT_NAME = \"CircularProgressValueText\";\n\nconst DEFAULT_MAX = 100;\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc đọc thẳng `process.env.NODE_ENV` ngay trong lúc render. Gói ra ESM\n * từng file, không bundle; ở môi trường không có bundler thay chuỗi đó (ESM thuần trên trình\n * duyệt), `process` không tồn tại → ReferenceError lúc render. Bọc `typeof` để chỗ đó chỉ mất cảnh\n * báo dev chứ không gãy; bundler vẫn thay `process.env.NODE_ENV` và loại nhánh dev như cũ.\n */\nconst IS_DEV = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\";\n\ntype ProgressState = \"indeterminate\" | \"complete\" | \"loading\";\n\nfunction getProgressState(value: number | undefined | null, maxValue: number): ProgressState {\n return value == null ? \"indeterminate\" : value === maxValue ? \"complete\" : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(value: unknown, min: number, max: number): value is number {\n return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n return `${Math.round(percentage)}%`;\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`value\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. \\`value\\` phải là số trong khoảng \\`min\\`…\\`max\\` (tính cả hai đầu), hoặc \\`null\\`/\\`undefined\\` cho tiến trình không xác định. Giá trị sẽ bị kẹp về khoảng hợp lệ.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`max\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. Chỉ nhận số lớn hơn 0 — dùng mặc định ${DEFAULT_MAX}.`;\n}\n\ninterface CircularProgressContextValue {\n value: number | null;\n valueText: string | undefined;\n max: number;\n min: number;\n state: ProgressState;\n radius: number;\n thickness: number;\n size: number;\n center: number;\n circumference: number;\n percentage: number | null;\n valueTextId?: string;\n /** `CircularProgressValueText` báo nó đang có mặt — xem chú thích ở `aria-describedby`. */\n onValueTextMountChange: (mounted: boolean) => void;\n}\n\nconst CircularProgressContext = React.createContext<CircularProgressContextValue | null>(null);\n\nfunction useCircularProgressContext(consumerName: string) {\n const context = React.useContext(CircularProgressContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${CIRCULAR_PROGRESS_NAME}\\``);\n }\n return context;\n}\n\ninterface CircularProgressProps extends React.ComponentProps<\"div\"> {\n /** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */\n value?: number | null | undefined;\n /**\n * Chuỗi trình đọc màn hình đọc thay cho con số (`aria-valuetext`) và chữ giữa vòng — mặc định\n * phần trăm làm tròn, vd `\"45%\"`.\n */\n getValueText?(value: number, min: number, max: number): string;\n min?: number;\n max?: number;\n /** Đường kính, px. */\n size?: number;\n /** Độ dày nét, px. */\n thickness?: number;\n /**\n * Nhãn HIỆN của vòng, đồng thời là tên của `role=\"progressbar\"`. Không đặt nhãn hiện thì PHẢI\n * truyền `aria-label` — progressbar không tên là lỗi a11y (ARIA 1.2), gói không tự đặt tên\n * chung chung thay app (giống `Progress`).\n */\n label?: string;\n asChild?: boolean;\n}\n\n/**\n * CircularProgress — tiến trình dạng VÒNG, cặp với `Progress` (dạng thanh) của gói.\n * Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),\n * hoặc dùng sẵn `CircularProgressCombined`.\n *\n * Phần đã chạy là `text-primary-ink`, cùng bậc với thanh `Progress`; rãnh xám trung tính. Đổi tông\n * thì truyền bậc INK cho `CircularProgressRange` (vd `className=\"text-success-ink\"`), không phải\n * token tô nền `text-success` — rãnh không cần đổi theo (số đo ở `CircularProgressTrack`).\n *\n * ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong\n * styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).\n */\nfunction CircularProgress(props: CircularProgressProps) {\n const {\n value: valueProp = null,\n getValueText = getDefaultValueText,\n min: minProp = 0,\n max: maxProp,\n size = 48,\n thickness = 4,\n label,\n asChild,\n className,\n children,\n ...rootProps\n } = props;\n\n if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n if (IS_DEV) {\n console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n const min = getIsValidNumber(minProp) ? minProp : 0;\n const max = rawMax <= min ? min + 1 : rawMax;\n\n if (IS_DEV && thickness >= size) {\n console.warn(\n `CircularProgress: \\`thickness\\` (${thickness}) phải nhỏ hơn \\`size\\` (${size}) thì vòng mới vẽ đúng.`\n );\n }\n\n if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n if (IS_DEV) {\n console.error(getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const value = getIsValidValueNumber(valueProp, min, max)\n ? valueProp\n : getIsValidNumber(valueProp) && valueProp > max\n ? max\n : getIsValidNumber(valueProp) && valueProp < min\n ? min\n : null;\n\n const valueText = getIsValidNumber(value) ? getValueText(value, min, max) : undefined;\n const state = getProgressState(value, max);\n const radius = Math.max(0, (size - thickness) / 2);\n const center = size / 2;\n const circumference = 2 * Math.PI * radius;\n\n const percentage = getIsValidNumber(value) ? (max === min ? 1 : (value - min) / (max - min)) : null;\n\n const labelId = React.useId();\n const valueTextId = React.useId();\n const [hasValueText, setHasValueText] = React.useState(false);\n\n const contextValue = React.useMemo<CircularProgressContextValue>(\n () => ({\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n valueTextId,\n onValueTextMountChange: setHasValueText\n }),\n [value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <CircularProgressContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"progressbar\"\n /*\n * ⚠ Khác thượng nguồn (3.0.1): chỉ trỏ tới phần chữ giá trị khi `CircularProgressValueText`\n * THẬT SỰ có trên trang. Bản gốc luôn đặt `aria-describedby` khi có giá trị — vòng không vẽ\n * chữ (chỉ vòng, hay nhãn ở chỗ khác) thì thuộc tính trỏ vào một id không phần tử nào mang\n * (axe: \"aria-describedby references an element that does not exist\"). Giá trị vẫn đi qua\n * `aria-valuetext` như cũ.\n */\n aria-describedby={valueText && hasValueText ? valueTextId : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-valuemax={max}\n aria-valuemin={min}\n aria-valuenow={getIsValidNumber(value) ? value : undefined}\n aria-valuetext={valueText}\n data-state={state}\n data-value={value ?? undefined}\n data-max={max}\n data-min={min}\n data-percentage={percentage}\n data-slot=\"circular-progress\"\n {...rootProps}\n className={cn(\"relative inline-flex w-fit items-center justify-center\", label && \"flex-col gap-1.5\", className)}\n >\n {/*\n * ⚠ Khác thượng nguồn: bản gốc đặt nhãn làm anh em của vòng NGAY TRONG gốc, trong khi\n * `CircularProgressValueText` là `absolute inset-0` phủ CẢ gốc — có nhãn là chữ \"65%\" đè\n * lên chữ nhãn. Khi có `label`: bọc phần con trong một khung đúng cỡ vòng (`relative`, nên\n * chữ giá trị căn giữa VÒNG), nhãn xuống dưới vòng. Không có `label` thì DOM y như bản gốc.\n */}\n {label ? (\n <div data-slot=\"circular-progress-ring\" className=\"relative inline-flex items-center justify-center\">\n {children}\n </div>\n ) : (\n children\n )}\n {label && (\n <div id={labelId} data-slot=\"circular-progress-label\" className=\"text-sm\">\n {label}\n </div>\n )}\n </RootPrimitive>\n </CircularProgressContext.Provider>\n );\n}\n\nfunction CircularProgressIndicator(props: React.ComponentProps<\"svg\">) {\n const { className, ...indicatorProps } = props;\n\n const context = useCircularProgressContext(INDICATOR_NAME);\n\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n viewBox={`0 0 ${context.size} ${context.size}`}\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-percentage={context.percentage}\n data-slot=\"circular-progress-indicator\"\n width={context.size}\n height={context.size}\n {...indicatorProps}\n className={cn(\"-rotate-90 transform\", className)}\n />\n );\n}\n\nCircularProgressIndicator.displayName = INDICATOR_NAME;\n\nfunction CircularProgressTrack(props: React.ComponentProps<\"circle\">) {\n const { className, ...trackProps } = props;\n\n const context = useCircularProgressContext(TRACK_NAME);\n\n return (\n <circle\n data-state={context.state}\n data-slot=\"circular-progress-track\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n {...trackProps}\n className={cn(\n /*\n * GIỮ NGUYÊN thượng nguồn — rãnh xám trung tính `text-muted-foreground/20`, KHÔNG phải tint\n * `/20` của bậc ink như rãnh `Progress`. Đã đo cả hai; thứ mang THÔNG TIN ở đây là ranh\n * giới cung-đã-chạy ⇄ rãnh (chỗ cung dừng lại chính là giá trị), và rãnh trung tính giữ\n * ranh giới đó TỐT HƠN (bảng tương phản cao, cung `text-primary-ink`):\n *\n * rãnh trung tính tint `primary-ink/20`\n * cung ⇄ rãnh trên --background : 5,03 (L) · 9,65 (D) 4,85 (L) · 7,97 (D)\n * tệ nhất, rãnh trên --muted : 4,71 (L) · 8,00 (D) 4,54 (L) · 6,52 (D)\n * (viền trang trí --border : 2,62 (L) ✗ — loại)\n *\n * Và nó không phụ thuộc tông: app đổi cung sang `text-success-ink`/`text-destructive-ink`\n * thì rãnh vẫn đúng (5,03 · 5,51:1 trên nền trắng) mà không phải đổi theo.\n *\n * Rãnh ⇄ nền trang chỉ ~1,3:1 — CỐ Ý, như rãnh `Progress`: không có cặp màu nào vừa ≥3:1\n * với nhau vừa ≥3:1 với nền trắng (chứng minh ở chú thích `Slider`). Khác `Progress` ở chỗ\n * vòng KHÔNG cần viền khoanh mốc 100%: hình tròn tự khép và cung luôn bắt đầu ở 12 giờ, nên\n * TỈ LỆ đọc được từ góc của cung mà không cần thấy rãnh. Một nét tròn liền, không nét nào\n * cắt nhau, nên màu có alpha không để lộ vệt nối (xem \"Cạm bẫy\" về icon trong CLAUDE.md).\n */\n \"text-muted-foreground/20\",\n className\n )}\n />\n );\n}\n\nfunction CircularProgressRange(props: React.ComponentProps<\"circle\">) {\n const { className, ...rangeProps } = props;\n\n const context = useCircularProgressContext(RANGE_NAME);\n\n const strokeDasharray = context.circumference;\n const strokeDashoffset =\n context.state === \"indeterminate\"\n ? context.circumference * 0.75\n : context.percentage !== null\n ? context.circumference - context.percentage * context.circumference\n : context.circumference;\n\n return (\n <circle\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-slot=\"circular-progress-range\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n strokeDasharray={strokeDasharray}\n strokeDashoffset={strokeDashoffset}\n vectorEffect=\"non-scaling-stroke\"\n {...rangeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN, gold #E8B824 chỉ 1,86:1 trên nền trắng)\n * → `text-primary-ink`, đúng bậc của thanh `Progress`: 6,51:1 (light) · 12,67:1 (dark)\n * trên --background, tệ nhất 6,09:1 · 10,80:1 trên --muted (bảng tương phản cao). Cung là\n * thông tin thị giác (WCAG 1.4.11, 3:1).\n * Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.\n */\n \"origin-center text-primary-ink transition-all duration-300 ease-in-out\",\n context.state === \"indeterminate\" &&\n \"motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none\",\n className\n )}\n />\n );\n}\n\ninterface CircularProgressValueTextProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction CircularProgressValueText(props: CircularProgressValueTextProps) {\n const { asChild, className, children, ...valueTextProps } = props;\n\n const context = useCircularProgressContext(VALUE_TEXT_NAME);\n const { onValueTextMountChange } = context;\n\n useIsomorphicLayoutEffect(() => {\n onValueTextMountChange(true);\n return () => onValueTextMountChange(false);\n }, [onValueTextMountChange]);\n\n const ValueTextPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ValueTextPrimitive\n id={context.valueTextId}\n data-state={context.state}\n data-slot=\"circular-progress-value-text\"\n {...valueTextProps}\n className={cn(\"absolute inset-0 flex items-center justify-center text-sm font-medium\", className)}\n >\n {children ?? context.valueText}\n </ValueTextPrimitive>\n );\n}\n\nfunction CircularProgressCombined(props: CircularProgressProps) {\n return (\n <CircularProgress {...props}>\n <CircularProgressIndicator>\n <CircularProgressTrack />\n <CircularProgressRange />\n </CircularProgressIndicator>\n <CircularProgressValueText />\n </CircularProgress>\n );\n}\n\nexport {\n CircularProgress,\n CircularProgressCombined,\n CircularProgressIndicator,\n type CircularProgressProps,\n CircularProgressRange,\n CircularProgressTrack,\n CircularProgressValueText,\n type CircularProgressValueTextProps\n};\n"],"mappings":";AA2MM,SA+BI,KA/BJ;AApMN,SAAS,YAAY;AACrB,YAAY,WAAW;AACvB,SAAS,UAAU;AACnB,SAAS,iCAAiC;AAE1C,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAExB,MAAM,cAAc;AAQpB,MAAM,SAAS,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;AAI1E,SAAS,iBAAiB,OAAkC,UAAiC;AAC3F,SAAO,SAAS,OAAO,kBAAkB,UAAU,WAAW,aAAa;AAC7E;AAEA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AAC3D;AAEA,SAAS,oBAAoB,KAA6B;AACxD,SAAO,iBAAiB,GAAG,KAAK,MAAM;AACxC;AAEA,SAAS,sBAAsB,OAAgB,KAAa,KAA8B;AACxF,SAAO,iBAAiB,KAAK,KAAK,SAAS,OAAO,SAAS;AAC7D;AAEA,SAAS,oBAAoB,OAAe,KAAa,KAAqB;AAC5E,QAAM,aAAa,QAAQ,MAAM,OAAQ,QAAQ,QAAQ,MAAM,OAAQ;AACvE,SAAO,GAAG,KAAK,MAAM,UAAU,CAAC;AAClC;AAEA,SAAS,qBAAqB,WAAmB,eAA+B;AAE9E,SAAO,sBAAsB,SAAS,wBAAmB,aAAa;AACxE;AAEA,SAAS,mBAAmB,WAAmB,eAA+B;AAE5E,SAAO,oBAAoB,SAAS,wBAAmB,aAAa,uHAA0D,WAAW;AAC3I;AAmBA,MAAM,0BAA0B,MAAM,cAAmD,IAAI;AAE7F,SAAS,2BAA2B,cAAsB;AACxD,QAAM,UAAU,MAAM,WAAW,uBAAuB;AACxD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,KAAK,YAAY,wCAA2B,sBAAsB,IAAI;AAAA,EACxF;AACA,SAAO;AACT;AAqCA,SAAS,iBAAiB,OAA8B;AACtD,QAAM;AAAA,IACJ,OAAO,YAAY;AAAA,IACnB,eAAe;AAAA,IACf,KAAK,UAAU;AAAA,IACf,KAAK;AAAA,IACL,OAAO;AAAA,IACP,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,OAAK,WAAW,YAAY,MAAM,CAAC,oBAAoB,OAAO,GAAG;AAC/D,QAAI,QAAQ;AACV,cAAQ,MAAM,mBAAmB,GAAG,OAAO,IAAI,sBAAsB,CAAC;AAAA,IACxE;AAAA,EACF;AAEA,QAAM,SAAS,oBAAoB,OAAO,IAAI,UAAU;AACxD,QAAM,MAAM,iBAAiB,OAAO,IAAI,UAAU;AAClD,QAAM,MAAM,UAAU,MAAM,MAAM,IAAI;AAEtC,MAAI,UAAU,aAAa,MAAM;AAC/B,YAAQ;AAAA,MACN,oCAAoC,SAAS,2CAA4B,IAAI;AAAA,IAC/E;AAAA,EACF;AAEA,MAAI,cAAc,QAAQ,CAAC,sBAAsB,WAAW,KAAK,GAAG,GAAG;AACrE,QAAI,QAAQ;AACV,cAAQ,MAAM,qBAAqB,GAAG,SAAS,IAAI,sBAAsB,CAAC;AAAA,IAC5E;AAAA,EACF;AAEA,QAAM,QAAQ,sBAAsB,WAAW,KAAK,GAAG,IACnD,YACA,iBAAiB,SAAS,KAAK,YAAY,MACzC,MACA,iBAAiB,SAAS,KAAK,YAAY,MACzC,MACA;AAER,QAAM,YAAY,iBAAiB,KAAK,IAAI,aAAa,OAAO,KAAK,GAAG,IAAI;AAC5E,QAAM,QAAQ,iBAAiB,OAAO,GAAG;AACzC,QAAM,SAAS,KAAK,IAAI,IAAI,OAAO,aAAa,CAAC;AACjD,QAAM,SAAS,OAAO;AACtB,QAAM,gBAAgB,IAAI,KAAK,KAAK;AAEpC,QAAM,aAAa,iBAAiB,KAAK,IAAK,QAAQ,MAAM,KAAK,QAAQ,QAAQ,MAAM,OAAQ;AAE/F,QAAM,UAAU,MAAM,MAAM;AAC5B,QAAM,cAAc,MAAM,MAAM;AAChC,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,KAAK;AAE5D,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,wBAAwB;AAAA,IAC1B;AAAA,IACA,CAAC,OAAO,WAAW,KAAK,KAAK,OAAO,QAAQ,WAAW,MAAM,QAAQ,eAAe,YAAY,WAAW;AAAA,EAC7G;AAEA,QAAM,gBAAgB,UAAU,OAAO;AAEvC,SACE,oBAAC,wBAAwB,UAAxB,EAAiC,OAAO,cACvC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MAQL,oBAAkB,aAAa,eAAe,cAAc;AAAA,MAC5D,mBAAiB,QAAQ,UAAU;AAAA,MACnC,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,iBAAe,iBAAiB,KAAK,IAAI,QAAQ;AAAA,MACjD,kBAAgB;AAAA,MAChB,cAAY;AAAA,MACZ,cAAY,SAAS;AAAA,MACrB,YAAU;AAAA,MACV,YAAU;AAAA,MACV,mBAAiB;AAAA,MACjB,aAAU;AAAA,MACT,GAAG;AAAA,MACJ,WAAW,GAAG,0DAA0D,SAAS,oBAAoB,SAAS;AAAA,MAQ7G;AAAA,gBACC,oBAAC,SAAI,aAAU,0BAAyB,WAAU,oDAC/C,UACH,IAEA;AAAA,QAED,SACC,oBAAC,SAAI,IAAI,SAAS,aAAU,2BAA0B,WAAU,WAC7D,iBACH;AAAA;AAAA;AAAA,EAEJ,GACF;AAEJ;AAEA,SAAS,0BAA0B,OAAoC;AACrE,QAAM,EAAE,WAAW,GAAG,eAAe,IAAI;AAEzC,QAAM,UAAU,2BAA2B,cAAc;AAEzD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,WAAU;AAAA,MACV,SAAS,OAAO,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAAA,MAC5C,cAAY,QAAQ;AAAA,MACpB,cAAY,QAAQ,SAAS;AAAA,MAC7B,YAAU,QAAQ;AAAA,MAClB,YAAU,QAAQ;AAAA,MAClB,mBAAiB,QAAQ;AAAA,MACzB,aAAU;AAAA,MACV,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MACf,GAAG;AAAA,MACJ,WAAW,GAAG,wBAAwB,SAAS;AAAA;AAAA,EACjD;AAEJ;AAEA,0BAA0B,cAAc;AAExC,SAAS,sBAAsB,OAAuC;AACpE,QAAM,EAAE,WAAW,GAAG,WAAW,IAAI;AAErC,QAAM,UAAU,2BAA2B,UAAU;AAErD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,QAAQ;AAAA,MACpB,aAAU;AAAA,MACV,IAAI,QAAQ;AAAA,MACZ,IAAI,QAAQ;AAAA,MACZ,GAAG,QAAQ;AAAA,MACX,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa,QAAQ;AAAA,MACrB,eAAc;AAAA,MACd,cAAa;AAAA,MACZ,GAAG;AAAA,MACJ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAqBT;AAAA,QACA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,sBAAsB,OAAuC;AACpE,QAAM,EAAE,WAAW,GAAG,WAAW,IAAI;AAErC,QAAM,UAAU,2BAA2B,UAAU;AAErD,QAAM,kBAAkB,QAAQ;AAChC,QAAM,mBACJ,QAAQ,UAAU,kBACd,QAAQ,gBAAgB,OACxB,QAAQ,eAAe,OACrB,QAAQ,gBAAgB,QAAQ,aAAa,QAAQ,gBACrD,QAAQ;AAEhB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,QAAQ;AAAA,MACpB,cAAY,QAAQ,SAAS;AAAA,MAC7B,YAAU,QAAQ;AAAA,MAClB,YAAU,QAAQ;AAAA,MAClB,aAAU;AAAA,MACV,IAAI,QAAQ;AAAA,MACZ,IAAI,QAAQ;AAAA,MACZ,GAAG,QAAQ;AAAA,MACX,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa,QAAQ;AAAA,MACrB,eAAc;AAAA,MACd;AAAA,MACA;AAAA,MACA,cAAa;AAAA,MACZ,GAAG;AAAA,MACJ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQT;AAAA,QACA,QAAQ,UAAU,mBAChB;AAAA,QACF;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAMA,SAAS,0BAA0B,OAAuC;AACxE,QAAM,EAAE,SAAS,WAAW,UAAU,GAAG,eAAe,IAAI;AAE5D,QAAM,UAAU,2BAA2B,eAAe;AAC1D,QAAM,EAAE,uBAAuB,IAAI;AAEnC,4BAA0B,MAAM;AAC9B,2BAAuB,IAAI;AAC3B,WAAO,MAAM,uBAAuB,KAAK;AAAA,EAC3C,GAAG,CAAC,sBAAsB,CAAC;AAE3B,QAAM,qBAAqB,UAAU,OAAO;AAE5C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI,QAAQ;AAAA,MACZ,cAAY,QAAQ;AAAA,MACpB,aAAU;AAAA,MACT,GAAG;AAAA,MACJ,WAAW,GAAG,yEAAyE,SAAS;AAAA,MAE/F,sBAAY,QAAQ;AAAA;AAAA,EACvB;AAEJ;AAEA,SAAS,yBAAyB,OAA8B;AAC9D,SACE,qBAAC,oBAAkB,GAAG,OACpB;AAAA,yBAAC,6BACC;AAAA,0BAAC,yBAAsB;AAAA,MACvB,oBAAC,yBAAsB;AAAA,OACzB;AAAA,IACA,oBAAC,6BAA0B;AAAA,KAC7B;AAEJ;","names":[]}
|
|
@@ -467,7 +467,6 @@ function KanbanBoard(props) {
|
|
|
467
467
|
children: /* @__PURE__ */ jsx(
|
|
468
468
|
BoardPrimitive,
|
|
469
469
|
{
|
|
470
|
-
"aria-orientation": context.orientation,
|
|
471
470
|
"data-orientation": context.orientation,
|
|
472
471
|
"data-slot": "kanban-board",
|
|
473
472
|
...boardProps,
|
|
@@ -611,7 +610,8 @@ function KanbanColumnHandle(props) {
|
|
|
611
610
|
...isDisabled ? {} : columnContext.listeners,
|
|
612
611
|
ref: composedRef,
|
|
613
612
|
className: cn(
|
|
614
|
-
|
|
613
|
+
// `touch-none`: xem `KanbanItemHandle`.
|
|
614
|
+
"touch-none select-none disabled:pointer-events-none disabled:opacity-50",
|
|
615
615
|
context.flatCursor ? "cursor-default" : "cursor-grab data-dragging:cursor-grabbing",
|
|
616
616
|
className
|
|
617
617
|
),
|
|
@@ -732,7 +732,13 @@ function KanbanItemHandle(props) {
|
|
|
732
732
|
...isDisabled ? {} : itemContext.listeners,
|
|
733
733
|
ref: composedRef,
|
|
734
734
|
className: cn(
|
|
735
|
-
|
|
735
|
+
/*
|
|
736
|
+
* ⚠ Khác thượng nguồn (3.0.1): `touch-none` trên tay cầm. `KanbanItem asHandle` (cả thẻ là tay
|
|
737
|
+
* cầm) đã có nó, tay cầm RỜI thì không — trên màn cảm ứng, vuốt bắt đầu từ tay cầm bị trình
|
|
738
|
+
* duyệt nhận làm cuộn trang, cảm biến chạm của dnd-kit mất sự kiện và thẻ không nhấc lên.
|
|
739
|
+
* Trang Patterns của comitor-ds từng phải tự thêm lớp này cho từng tay cầm.
|
|
740
|
+
*/
|
|
741
|
+
"touch-none select-none disabled:pointer-events-none disabled:opacity-50",
|
|
736
742
|
context.flatCursor ? "cursor-default" : "cursor-grab data-dragging:cursor-grabbing",
|
|
737
743
|
className
|
|
738
744
|
),
|