@comitor/ui 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,7 @@ interface ToastOptions {
27
27
  * const { resolvedTheme } = useTheme();
28
28
  * <Toaster theme={resolvedTheme as "light" | "dark"} />
29
29
  */
30
- declare function Toaster({ theme, position, ...props }: ToasterProps): React.JSX.Element;
30
+ declare function Toaster({ theme, position, toastOptions, ...props }: ToasterProps): React.JSX.Element;
31
31
  /**
32
32
  * Bắn toast theo biến thể của Comitor.
33
33
  *
@@ -2,15 +2,18 @@
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 { useComitorMessages } from "../../i18n/locale-provider.js";
5
6
  import { getActiveToastMessages, useToastLanguageBridge } from "../../lib/toast-language.js";
6
- function Toaster({ theme = "system", position = "bottom-right", ...props }) {
7
+ function Toaster({ theme = "system", position = "bottom-right", toastOptions, ...props }) {
7
8
  useToastLanguageBridge();
9
+ const t = useComitorMessages("toast");
8
10
  return /* @__PURE__ */ jsx(
9
11
  SonnerToaster,
10
12
  {
11
13
  theme,
12
14
  position,
13
15
  className: "toaster group",
16
+ containerAriaLabel: t.containerAriaLabel,
14
17
  icons: {
15
18
  success: /* @__PURE__ */ jsx(CheckCircle2, { className: "size-4 text-success-ink" }),
16
19
  warning: /* @__PURE__ */ jsx(AlertTriangle, { className: "size-4 text-warning-ink" }),
@@ -18,28 +21,32 @@ function Toaster({ theme = "system", position = "bottom-right", ...props }) {
18
21
  info: /* @__PURE__ */ jsx(Info, { className: "size-4 text-info-ink" })
19
22
  },
20
23
  toastOptions: {
21
- classNames: {
22
- toast: "group toast group-[.toaster]:bg-popover group-[.toaster]:text-popover-foreground group-[.toaster]:border-border group-[.toaster]:rounded-lg group-[.toaster]:shadow-card",
23
- title: "group-[.toast]:text-sm group-[.toast]:font-medium",
24
- description: "group-[.toast]:text-sm group-[.toast]:text-muted-foreground",
25
- // 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
26
- // nhận ra được RANH GIỚI (WCAG 1.4.11, ngưỡng 3:1):
27
- // actionButton chữ 10,19:1 (L) · 12,67:1 (D) ✓, nhưng ruột `bg-primary` (gold) chỉ
28
- // lệch nền popover 1,86:1 ở light. Giữ đúng cách variant `default` của
29
- // `Button` xử lý — viền trong suốt, để `shadow-control` làm tín hiệu
30
- // độ cao; đổi khác ở đây sẽ lệch với nút của chính gói.
31
- // cancelButton chữ 4,89:1 (L) · 4,86:1 (D) ✓, nhưng ruột `bg-muted` chỉ lệch nền
32
- // popover 1,07:1 (L) · 1,10:1 (D) → nút không có biên nào, đọc như chữ
33
- // trơn. `border-control-edge`: 3,83:1 (L) · 4,64:1 (D) so với popover,
34
- // 3,58:1 · 4,21:1 so với chính ruột `bg-muted` ✓ (hợp đồng §3).
35
- actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground group-[.toast]:shadow-control",
36
- cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground group-[.toast]:border group-[.toast]:border-control-edge"
37
- }
24
+ closeButtonAriaLabel: t.closeButtonAriaLabel,
25
+ ...toastOptions ?? COMITOR_TOAST_OPTIONS
38
26
  },
39
27
  ...props
40
28
  }
41
29
  );
42
30
  }
31
+ const COMITOR_TOAST_OPTIONS = {
32
+ classNames: {
33
+ toast: "group toast group-[.toaster]:bg-popover group-[.toaster]:text-popover-foreground group-[.toaster]:border-border group-[.toaster]:rounded-lg group-[.toaster]:shadow-card",
34
+ title: "group-[.toast]:text-sm group-[.toast]:font-medium",
35
+ description: "group-[.toast]:text-sm group-[.toast]:text-muted-foreground",
36
+ // 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
37
+ // nhận ra được RANH GIỚI (WCAG 1.4.11, ngưỡng 3:1):
38
+ // actionButton chữ 10,19:1 (L) · 12,67:1 (D) ✓, nhưng ruột `bg-primary` (gold) chỉ
39
+ // lệch nền popover 1,86:1 ở light. Giữ đúng cách variant `default` của
40
+ // `Button` xử lý — viền trong suốt, để `shadow-control` làm tín hiệu
41
+ // độ cao; đổi khác ở đây sẽ lệch với nút của chính gói.
42
+ // cancelButton chữ 4,89:1 (L) · 4,86:1 (D) ✓, nhưng ruột `bg-muted` chỉ lệch nền
43
+ // popover 1,07:1 (L) · 1,10:1 (D) → nút không có biên nào, đọc như chữ
44
+ // trơn. `border-control-edge`: 3,83:1 (L) · 4,64:1 (D) so với popover,
45
+ // 3,58:1 · 4,21:1 so với chính ruột `bg-muted` ✓ (hợp đồng §3).
46
+ actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground group-[.toast]:shadow-control",
47
+ cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground group-[.toast]:border group-[.toast]:border-control-edge"
48
+ }
49
+ };
43
50
  function toast(title, options = {}) {
44
51
  const { variant = "default", description, duration, action, id } = options;
45
52
  const config = { description, duration, action, id };
@@ -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 { 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":[]}
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 { useComitorMessages } from \"../../i18n/locale-provider.js\";\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\", toastOptions, ...props }: ToasterProps) {\n // Ghi ngôn ngữ của provider cho `toastPromise` đọc — xem `lib/toast-language.ts`.\n useToastLanguageBridge();\n const t = useComitorMessages(\"toast\");\n\n return (\n <SonnerToaster\n theme={theme}\n position={position}\n className=\"toaster group\"\n // Tên vùng toast theo provider — mặc định của sonner là \"Notifications\" CỨNG (xem\n // `namespaces/toast.ts`). Đứng TRƯỚC `{...props}` nên prop của app vẫn thắng.\n containerAriaLabel={t.containerAriaLabel}\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 /*\n * ⚠ `toastOptions` của app THAY cả khối (kể cả `classNames` bên dưới) — hành vi có từ trước,\n * giữ nguyên. Riêng `closeButtonAriaLabel` thì luôn có mặc định theo provider, trừ khi app tự\n * khai: thiếu nó thì nút × (khi bật `closeButton`) đọc \"Close toast\" giữa giao diện tiếng Việt.\n */\n toastOptions={{\n closeButtonAriaLabel: t.closeButtonAriaLabel,\n ...(toastOptions ?? COMITOR_TOAST_OPTIONS)\n }}\n {...props}\n />\n );\n}\n\n/**\n * Lớp của từng toast — khối `toastOptions` mặc định của `Toaster`. Hằng module, không dựng lại mỗi\n * lượt render.\n */\nconst COMITOR_TOAST_OPTIONS: NonNullable<ToasterProps[\"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: \"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\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":";AAqDiB;AAnDjB,SAAS,eAAe,cAAc,MAAM,eAAe;AAE3D,SAAS,WAAW,eAAe,SAAS,mBAAmB;AAC/D,SAAS,0BAA0B;AACnC,SAAS,wBAAwB,8BAA8B;AA2BxD,SAAS,QAAQ,EAAE,QAAQ,UAAU,WAAW,gBAAgB,cAAc,GAAG,MAAM,GAAiB;AAE7G,yBAAuB;AACvB,QAAM,IAAI,mBAAmB,OAAO;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,WAAU;AAAA,MAGV,oBAAoB,EAAE;AAAA,MAOtB,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,MAMA,cAAc;AAAA,QACZ,sBAAsB,EAAE;AAAA,QACxB,GAAI,gBAAgB;AAAA,MACtB;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAMA,MAAM,wBAAmE;AAAA,EACvE,YAAY;AAAA,IACV,OACE;AAAA,IACF,OAAO;AAAA,IACP,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWb,cAAc;AAAA,IACd,cACE;AAAA,EACJ;AACF;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":[]}
@@ -18,6 +18,26 @@ interface ActionBarProps extends DivProps {
18
18
  side?: "top" | "bottom";
19
19
  sideOffset?: number;
20
20
  portalContainer?: Element | DocumentFragment | null;
21
+ /**
22
+ * Căn NGANG theo đâu — `align`/`alignOffset` đo từ hộp nào. Chiều dọc (`side`/`sideOffset`) luôn
23
+ * theo khung nhìn: thanh vẫn nổi ở đáy (hay đỉnh) màn hình.
24
+ *
25
+ * - `"viewport"` (mặc định): khung nhìn — đúng như trước `3.3.0`.
26
+ * - `"container"`: hộp của `portalContainer` — giữa (hay mép) phần tử đó, và rộng tối đa bằng nó.
27
+ * Dành cho thanh phục vụ MỘT vùng (bảng, lưới) trong khung app có thanh bên: giữa khung nhìn lệch
28
+ * khỏi giữa vùng đúng nửa bề rộng thanh bên (120px với thanh bên 240px), và thanh trông như trôi
29
+ * lạc sang một phía. Đặt `portalContainer` là một khối CÙNG BỀ RỘNG với vùng — một `<div>` rỗng
30
+ * ngay sau bảng là đủ, và đó cũng là chỗ đúng cho thứ tự Tab.
31
+ *
32
+ * ⚠ Thêm so với thượng nguồn. Hai giới hạn, cả hai có lý do:
33
+ * - Đo bằng toạ độ KHUNG NHÌN, đo lại khi phần tử hoặc cửa sổ ĐỔI CỠ (thu/mở thanh bên, xoay máy).
34
+ * Phần tử DỜI chỗ mà không đổi cỡ thì không đo lại — `ResizeObserver` không báo chuyện đó.
35
+ * - Tổ tiên nào của `portalContainer` tạo khối chứa cho `position: fixed` (`transform`, `filter`,
36
+ * `contain: layout|paint`) thì toạ độ khung nhìn lệch đúng bằng vị trí khối đó. Ở đó dùng
37
+ * `"viewport"`: `left: 50%` khi ấy đã tự tính theo chính khối chứa.
38
+ * Không có `portalContainer` (thanh nằm ở `<body>`) thì hai chế độ như nhau.
39
+ */
40
+ anchor?: "viewport" | "container";
21
41
  dir?: Direction;
22
42
  orientation?: Orientation;
23
43
  loop?: boolean;
@@ -53,6 +53,17 @@ function useFocusContext(consumerName) {
53
53
  }
54
54
  return context;
55
55
  }
56
+ function horizontalPosition(align, alignOffset, box) {
57
+ if (!box) {
58
+ if (align === "center") return { left: "50%", translate: "-50% 0" };
59
+ if (align === "start") return { left: `${alignOffset}px` };
60
+ return { right: `${alignOffset}px` };
61
+ }
62
+ const maxWidth = `min(100vw - 1rem, ${box.width}px)`;
63
+ if (align === "center") return { left: `${box.left + box.width / 2}px`, translate: "-50% 0", maxWidth };
64
+ if (align === "start") return { left: `${box.left + alignOffset}px`, maxWidth };
65
+ return { right: `${box.viewportWidth - (box.left + box.width) + alignOffset}px`, maxWidth };
66
+ }
56
67
  function ActionBar(props) {
57
68
  const {
58
69
  open = false,
@@ -63,6 +74,7 @@ function ActionBar(props) {
63
74
  align = "center",
64
75
  sideOffset = 16,
65
76
  portalContainer: portalContainerProp,
77
+ anchor = "viewport",
66
78
  dir: dirProp,
67
79
  orientation = "horizontal",
68
80
  loop = true,
@@ -127,6 +139,28 @@ function ActionBar(props) {
127
139
  useIsomorphicLayoutEffect(() => {
128
140
  setMounted(true);
129
141
  }, []);
142
+ const anchorElement = anchor === "container" && portalContainerProp && "getBoundingClientRect" in portalContainerProp ? portalContainerProp : null;
143
+ const [anchorBox, setAnchorBox] = React.useState(null);
144
+ useIsomorphicLayoutEffect(() => {
145
+ if (!open || !anchorElement) return;
146
+ const view = anchorElement.ownerDocument.defaultView;
147
+ function measure() {
148
+ if (!anchorElement) return;
149
+ const rect = anchorElement.getBoundingClientRect();
150
+ const viewportWidth = anchorElement.ownerDocument.documentElement.clientWidth;
151
+ setAnchorBox(
152
+ (previous) => previous && previous.left === rect.left && previous.width === rect.width && previous.viewportWidth === viewportWidth ? previous : { left: rect.left, width: rect.width, viewportWidth }
153
+ );
154
+ }
155
+ measure();
156
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
157
+ observer?.observe(anchorElement);
158
+ view?.addEventListener("resize", measure);
159
+ return () => {
160
+ observer?.disconnect();
161
+ view?.removeEventListener("resize", measure);
162
+ };
163
+ }, [open, anchorElement]);
130
164
  React.useEffect(() => {
131
165
  if (!open) return;
132
166
  const ownerDocument = rootRef.current?.ownerDocument ?? document;
@@ -175,6 +209,16 @@ function ActionBar(props) {
175
209
  */
176
210
  "fixed z-50 rounded-lg border bg-popover text-popover-foreground shadow-overlay outline-none",
177
211
  "animate-in duration-250 fade-in-0 zoom-in-95 [animation-timing-function:cubic-bezier(0.16,1,0.3,1)]",
212
+ /*
213
+ * ⚠ `transition-none` (3.3.0) — thanh không có transition nào CỐ Ý: hiệu ứng xuất hiện là
214
+ * keyframe (`animate-in`). Nhưng `duration-250` ở dòng trên, đặt cho keyframe đó, trong
215
+ * Tailwind v4 còn ghi luôn `transition-duration: 250ms`; mà `transition-property` mặc định
216
+ * của CSS là `all` — nên MỌI thay đổi thuộc tính của thanh trượt 250ms. Đo được với
217
+ * `anchor="container"`: lượt đo vị trí (trước lượt sơn) đổi `left` từ giữa khung nhìn sang
218
+ * giữa vùng, và thanh TRƯỢT NGANG 120px ở mỗi lần mở. Theo vùng khi thanh bên thu/mở thì
219
+ * vẫn mượt: `ResizeObserver` báo từng khung hình của chính hiệu ứng thu/mở đó.
220
+ */
221
+ "transition-none",
178
222
  "data-[side=bottom]:slide-in-from-bottom-4 data-[side=top]:slide-in-from-top-4",
179
223
  "motion-reduce:animate-none motion-reduce:transition-none",
180
224
  /*
@@ -182,19 +226,20 @@ function ActionBar(props) {
182
226
  * không có giới hạn nào — đo ở 375px: thanh rộng 413px, bắt đầu ở x = −19px, hai mép bị
183
227
  * 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
184
228
  * mất vòng focus của các nút ở mép.
229
+ *
230
+ * ⚠ Và `w-max` (3.3.0) — thiếu nó thì trần kia thành vô nghĩa. Phần tử `fixed` rộng "co
231
+ * theo nội dung" chỉ được dùng phần khung chứa nằm SAU mốc `left`; với `left: 50%` của
232
+ * `align="center"` là đúng NỬA khung nhìn. Đo trên app thật: khung 1129px, thanh rộng
233
+ * đúng 564,5px và gãy "3 selected ×" xuống hàng thứ hai dù nội dung chỉ cần 621px. `w-max`
234
+ * cho thanh rộng bằng nội dung, `max-w-*` vẫn chặn trần, nên chỉ màn hẹp mới xuống dòng.
185
235
  */
186
- "max-w-[calc(100vw-1rem)]",
236
+ "w-max max-w-[calc(100vw-1rem)]",
187
237
  orientation === "horizontal" ? "flex flex-row flex-wrap items-center gap-2 px-2 py-1.5" : "flex flex-col items-start gap-2 px-1.5 py-2",
188
238
  className
189
239
  ),
190
240
  style: {
191
241
  [side]: `${sideOffset}px`,
192
- ...align === "center" && {
193
- left: "50%",
194
- translate: "-50% 0"
195
- },
196
- ...align === "start" && { left: `${alignOffset}px` },
197
- ...align === "end" && { right: `${alignOffset}px` },
242
+ ...horizontalPosition(align, alignOffset, anchorElement ? anchorBox : null),
198
243
  ...style
199
244
  },
200
245
  onFocus,
@@ -346,7 +391,13 @@ function ActionBarGroup(props) {
346
391
  ref: composedRef,
347
392
  className: cn(
348
393
  "flex gap-2 outline-none",
349
- orientation === "horizontal" ? "items-center" : "w-full flex-col items-start",
394
+ /*
395
+ * ⚠ `flex-wrap` (3.3.0): thanh xuống dòng ở màn hẹp (2.1.0) nhưng NHÓM thì không — nó là một
396
+ * mục flex duy nhất của thanh, nên bốn nút nằm cứng trên một hàng. Đo ở 375px: nhóm 387px
397
+ * trong thanh 359px, nút cuối thò ra tới x = 404, ngoài màn hình. Xuống dòng chứ không cuộn
398
+ * ngang — cùng lý do với thanh: vùng cuộn cắt mất vòng focus của nút ở mép.
399
+ */
400
+ orientation === "horizontal" ? "flex-wrap items-center" : "w-full flex-col items-start",
350
401
  className
351
402
  ),
352
403
  onBlur,
@@ -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 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"]}
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 /**\n * Căn NGANG theo đâu — `align`/`alignOffset` đo từ hộp nào. Chiều dọc (`side`/`sideOffset`) luôn\n * theo khung nhìn: thanh vẫn nổi ở đáy (hay đỉnh) màn hình.\n *\n * - `\"viewport\"` (mặc định): khung nhìn — đúng như trước `3.3.0`.\n * - `\"container\"`: hộp của `portalContainer` — giữa (hay mép) phần tử đó, và rộng tối đa bằng nó.\n * Dành cho thanh phục vụ MỘT vùng (bảng, lưới) trong khung app có thanh bên: giữa khung nhìn lệch\n * khỏi giữa vùng đúng nửa bề rộng thanh bên (120px với thanh bên 240px), và thanh trông như trôi\n * lạc sang một phía. Đặt `portalContainer` là một khối CÙNG BỀ RỘNG với vùng — một `<div>` rỗng\n * ngay sau bảng là đủ, và đó cũng là chỗ đúng cho thứ tự Tab.\n *\n * ⚠ Thêm so với thượng nguồn. Hai giới hạn, cả hai có lý do:\n * - Đo bằng toạ độ KHUNG NHÌN, đo lại khi phần tử hoặc cửa sổ ĐỔI CỠ (thu/mở thanh bên, xoay máy).\n * Phần tử DỜI chỗ mà không đổi cỡ thì không đo lại — `ResizeObserver` không báo chuyện đó.\n * - Tổ tiên nào của `portalContainer` tạo khối chứa cho `position: fixed` (`transform`, `filter`,\n * `contain: layout|paint`) thì toạ độ khung nhìn lệch đúng bằng vị trí khối đó. Ở đó dùng\n * `\"viewport\"`: `left: 50%` khi ấy đã tự tính theo chính khối chứa.\n * Không có `portalContainer` (thanh nằm ở `<body>`) thì hai chế độ như nhau.\n */\n anchor?: \"viewport\" | \"container\";\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\n/** Hộp của `portalContainer` theo toạ độ khung nhìn — cho `anchor=\"container\"`. */\ninterface AnchorBox {\n left: number;\n width: number;\n /** `documentElement.clientWidth` lúc đo — để tính `right` cho `align=\"end\"`. */\n viewportWidth: number;\n}\n\n/**\n * Vị trí NGANG của thanh. `box` null = theo khung nhìn (hành vi trước `3.3.0`, và cả lượt render\n * đầu của `anchor=\"container\"` khi chưa đo — lượt đó không bao giờ được sơn, xem layout effect).\n *\n * Có `box` thì trần bề rộng thành `min(khung nhìn − 1rem, bề rộng vùng)`: tâm vùng thường lệch khỏi\n * tâm khung nhìn, và một thanh rộng hơn vùng sẽ thò qua mép màn hình ở phía gần hơn. Chạm trần thì\n * thanh xuống dòng trong đúng bề rộng của vùng.\n */\nfunction horizontalPosition(\n align: \"start\" | \"center\" | \"end\",\n alignOffset: number,\n box: AnchorBox | null\n): React.CSSProperties {\n if (!box) {\n if (align === \"center\") return { left: \"50%\", translate: \"-50% 0\" };\n if (align === \"start\") return { left: `${alignOffset}px` };\n return { right: `${alignOffset}px` };\n }\n\n const maxWidth = `min(100vw - 1rem, ${box.width}px)`;\n if (align === \"center\") return { left: `${box.left + box.width / 2}px`, translate: \"-50% 0\", maxWidth };\n if (align === \"start\") return { left: `${box.left + alignOffset}px`, maxWidth };\n return { right: `${box.viewportWidth - (box.left + box.width) + alignOffset}px`, maxWidth };\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 anchor = \"viewport\",\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 /*\n * `anchor=\"container\"`: hộp của `portalContainer` theo toạ độ khung nhìn. Đo trong LAYOUT effect —\n * chạy sau commit nhưng TRƯỚC lượt sơn — nên thanh không bao giờ hiện một khung hình ở giữa khung\n * nhìn rồi mới nhảy sang giữa vùng. Không đặt lại về `null` khi đóng: lần mở sau đo lại trước lượt\n * sơn, và giữ giá trị cũ thì bớt được một lượt render.\n */\n /*\n * ⚠ Phép kiểm `in`, KHÔNG `instanceof Element`: thân component này chạy cả ở lượt render SERVER\n * (SSR của component client), nơi `Element` không tồn tại — `instanceof` ở đó ném ReferenceError\n * và làm đổ cả trang. `in` còn đúng với phần tử của một iframe (khác realm, `instanceof` trả sai).\n * `DocumentFragment` không có `getBoundingClientRect`, nên nó rơi về `null` = theo khung nhìn.\n */\n const anchorElement =\n anchor === \"container\" && portalContainerProp && \"getBoundingClientRect\" in portalContainerProp\n ? portalContainerProp\n : null;\n const [anchorBox, setAnchorBox] = React.useState<AnchorBox | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (!open || !anchorElement) return;\n\n const view = anchorElement.ownerDocument.defaultView;\n function measure() {\n if (!anchorElement) return;\n const rect = anchorElement.getBoundingClientRect();\n // `clientWidth` chứ không `innerWidth`: khung chứa của `fixed` KHÔNG gồm thanh cuộn dọc, nên\n // `right` đo từ mép trong của nó.\n const viewportWidth = anchorElement.ownerDocument.documentElement.clientWidth;\n setAnchorBox((previous) =>\n previous &&\n previous.left === rect.left &&\n previous.width === rect.width &&\n previous.viewportWidth === viewportWidth\n ? previous\n : { left: rect.left, width: rect.width, viewportWidth }\n );\n }\n\n measure();\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(measure);\n observer?.observe(anchorElement);\n view?.addEventListener(\"resize\", measure);\n return () => {\n observer?.disconnect();\n view?.removeEventListener(\"resize\", measure);\n };\n }, [open, anchorElement]);\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 /*\n * ⚠ `transition-none` (3.3.0) — thanh không có transition nào CỐ Ý: hiệu ứng xuất hiện là\n * keyframe (`animate-in`). Nhưng `duration-250` ở dòng trên, đặt cho keyframe đó, trong\n * Tailwind v4 còn ghi luôn `transition-duration: 250ms`; mà `transition-property` mặc định\n * của CSS là `all` — nên MỌI thay đổi thuộc tính của thanh trượt 250ms. Đo được với\n * `anchor=\"container\"`: lượt đo vị trí (trước lượt sơn) đổi `left` từ giữa khung nhìn sang\n * giữa vùng, và thanh TRƯỢT NGANG 120px ở mỗi lần mở. Theo vùng khi thanh bên thu/mở thì\n * vẫn mượt: `ResizeObserver` báo từng khung hình của chính hiệu ứng thu/mở đó.\n */\n \"transition-none\",\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 * ⚠ Và `w-max` (3.3.0) — thiếu nó thì trần kia thành vô nghĩa. Phần tử `fixed` rộng \"co\n * theo nội dung\" chỉ được dùng phần khung chứa nằm SAU mốc `left`; với `left: 50%` của\n * `align=\"center\"` là đúng NỬA khung nhìn. Đo trên app thật: khung 1129px, thanh rộng\n * đúng 564,5px và gãy \"3 selected ×\" xuống hàng thứ hai dù nội dung chỉ cần 621px. `w-max`\n * cho thanh rộng bằng nội dung, `max-w-*` vẫn chặn trần, nên chỉ màn hẹp mới xuống dòng.\n */\n \"w-max 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 ...horizontalPosition(align, alignOffset, anchorElement ? anchorBox : null),\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 /*\n * ⚠ `flex-wrap` (3.3.0): thanh xuống dòng ở màn hẹp (2.1.0) nhưng NHÓM thì không — nó là một\n * mục flex duy nhất của thanh, nên bốn nút nằm cứng trên một hàng. Đo ở 375px: nhóm 387px\n * trong thanh 359px, nút cuối thò ra tới x = 404, ngoài màn hình. Xuống dòng chứ không cuộn\n * ngang — cùng lý do với thanh: vùng cuộn cắt mất vòng focus của nút ở mép.\n */\n orientation === \"horizontal\" ? \"flex-wrap 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":";AAmYQ,SA8FA,UA9FA,KA8FA,YA9FA;AAzXR,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AACrB,SAAS,SAAS;AAClB,YAAY,WAAW;AACvB,YAAY,cAAc;AAC1B,SAAS,wBAAwB;AACjC,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,iCAAiC;AAE1C,SAAS,cAAc;AAEvB,MAAM,YAAY;AAClB,MAAM,aAAa;AACnB,MAAM,YAAY;AAClB,MAAM,aAAa;AACnB,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,cAAc;AACpB,MAAM,cAAc;AACpB,MAAM,gBAAgB,EAAE,SAAS,OAAO,YAAY,KAAK;AAazD,SAAS,WAAW,YAAmD,gBAAgB,OAAO;AAC5F,QAAM,6BAA6B,SAAS;AAC5C,aAAW,gBAAgB,YAAY;AACrC,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAChB,QAAI,cAAc,2BAA4B;AAC9C,cAAU,MAAM,EAAE,cAAc,CAAC;AACjC,QAAI,SAAS,kBAAkB,2BAA4B;AAAA,EAC7D;AACF;AAEA,SAAS,UAAa,OAAY,YAAoB;AACpD,SAAO,MAAM,IAAO,CAAC,GAAG,UAAU,OAAO,aAAa,SAAS,MAAM,MAAM,CAAM;AACnF;AAEA,SAAS,qBAAqB,KAAa,KAAiB;AAC1D,MAAI,QAAQ,MAAO,QAAO;AAC1B,SAAO,QAAQ,cAAc,eAAe,QAAQ,eAAe,cAAc;AACnF;AAgBA,MAAM,mBAAmB,MAAM,cAA4C,IAAI;AAE/E,SAAS,oBAAoB,cAAsB;AACjD,QAAM,UAAU,MAAM,WAAW,gBAAgB;AACjD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,KAAK,YAAY,wCAA2B,SAAS,IAAI;AAAA,EAC3E;AACA,SAAO;AACT;AAaA,MAAM,eAAe,MAAM,cAAwC,IAAI;AAEvE,SAAS,gBAAgB,cAAsB;AAC7C,QAAM,UAAU,MAAM,WAAW,YAAY;AAC7C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,KAAK,YAAY,sDAAyC;AAAA,EAC5E;AACA,SAAO;AACT;AA6DA,SAAS,mBACP,OACA,aACA,KACqB;AACrB,MAAI,CAAC,KAAK;AACR,QAAI,UAAU,SAAU,QAAO,EAAE,MAAM,OAAO,WAAW,SAAS;AAClE,QAAI,UAAU,QAAS,QAAO,EAAE,MAAM,GAAG,WAAW,KAAK;AACzD,WAAO,EAAE,OAAO,GAAG,WAAW,KAAK;AAAA,EACrC;AAEA,QAAM,WAAW,qBAAqB,IAAI,KAAK;AAC/C,MAAI,UAAU,SAAU,QAAO,EAAE,MAAM,GAAG,IAAI,OAAO,IAAI,QAAQ,CAAC,MAAM,WAAW,UAAU,SAAS;AACtG,MAAI,UAAU,QAAS,QAAO,EAAE,MAAM,GAAG,IAAI,OAAO,WAAW,MAAM,SAAS;AAC9E,SAAO,EAAE,OAAO,GAAG,IAAI,iBAAiB,IAAI,OAAO,IAAI,SAAS,WAAW,MAAM,SAAS;AAC5F;AAEA,SAAS,UAAU,OAAuB;AACxC,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,SAAS;AAAA,IACT,KAAK;AAAA,IACL,cAAc;AAAA,IACd,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAElD,QAAM,UAAU,MAAM,OAAoB,IAAI;AAC9C,QAAM,cAAc,gBAAgB,KAAK,OAAO;AAEhD,QAAM,WAAW,SAAS;AAAA,IACxB;AAAA,IACA;AAAA,EACF,CAAC;AAUD,QAAM,iBAAiB,MAAM,OAA2B,IAAI;AAC5D,QAAM,iBAAiB,MAAM,OAAO,KAAK;AAEzC,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,UAAyC;AACxC,oBAAc,KAAK;AAOnB,YAAM,OAAO,MAAM;AACnB,UAAI,CAAC,eAAe,WAAW,gBAAgB,eAAe,CAAC,MAAM,cAAc,SAAS,IAAI,GAAG;AACjG,uBAAe,UAAU;AAAA,MAC3B;AACA,qBAAe,UAAU;AAAA,IAC3B;AAAA,IACA,CAAC,WAAW;AAAA,EACd;AAEA,QAAM,SAAS,MAAM;AAAA,IACnB,CAAC,UAAyC;AACxC,mBAAa,KAAK;AAElB,YAAM,KAAK,MAAM;AACjB,UAAI,EAAE,cAAc,SAAS,CAAC,MAAM,cAAc,SAAS,EAAE,GAAG;AAC9D,uBAAe,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,4BAA0B,MAAM;AAC9B,QAAI,CAAC,KAAM;AAEX,WAAO,MAAM;AACX,YAAM,eAAe,eAAe;AACpC,YAAM,WAAW,eAAe;AAChC,qBAAe,UAAU;AACzB,qBAAe,UAAU;AACzB,UAAI,CAAC,YAAY,CAAC,aAAc;AAOhC,qBAAe,MAAM;AACnB,cAAM,gBAAgB,aAAa;AACnC,cAAM,SAAS,cAAc;AAC7B,YAAI,UAAU,WAAW,cAAc,QAAQ,OAAO,YAAa;AACnE,YAAI,CAAC,aAAa,YAAa;AAC/B,qBAAa,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,MAAM,aAAa,OAAO;AAEhC,QAAM,SAAS,iBAAiB,aAAa,UAAU;AAGvD,4BAA0B,MAAM;AAC9B,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAcL,QAAM,gBACJ,WAAW,eAAe,uBAAuB,2BAA2B,sBACxE,sBACA;AACN,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAA2B,IAAI;AAEvE,4BAA0B,MAAM;AAC9B,QAAI,CAAC,QAAQ,CAAC,cAAe;AAE7B,UAAM,OAAO,cAAc,cAAc;AACzC,aAAS,UAAU;AACjB,UAAI,CAAC,cAAe;AACpB,YAAM,OAAO,cAAc,sBAAsB;AAGjD,YAAM,gBAAgB,cAAc,cAAc,gBAAgB;AAClE;AAAA,QAAa,CAAC,aACZ,YACA,SAAS,SAAS,KAAK,QACvB,SAAS,UAAU,KAAK,SACxB,SAAS,kBAAkB,gBACvB,WACA,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,cAAc;AAAA,MAC1D;AAAA,IACF;AAEA,YAAQ;AACR,UAAM,WAAW,OAAO,mBAAmB,cAAc,OAAO,IAAI,eAAe,OAAO;AAC1F,cAAU,QAAQ,aAAa;AAC/B,UAAM,iBAAiB,UAAU,OAAO;AACxC,WAAO,MAAM;AACX,gBAAU,WAAW;AACrB,YAAM,oBAAoB,UAAU,OAAO;AAAA,IAC7C;AAAA,EACF,GAAG,CAAC,MAAM,aAAa,CAAC;AAExB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AAEX,UAAM,gBAAgB,QAAQ,SAAS,iBAAiB;AAExD,aAAS,UAAU,OAAsB;AAQvC,UAAI,MAAM,QAAQ,YAAY,MAAM,oBAAoB,MAAM,YAAa;AAE3E,eAAS,QAAQ,kBAAkB,KAAK;AACxC,UAAI,CAAC,MAAM,kBAAkB;AAC3B,iBAAS,QAAQ,eAAe,KAAK;AAAA,MACvC;AAAA,IACF;AAEA,kBAAc,iBAAiB,WAAW,SAAS;AACnD,WAAO,MAAM,cAAc,oBAAoB,WAAW,SAAS;AAAA,EACrE,GAAG,CAAC,MAAM,QAAQ,CAAC;AAEnB,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,cAAc,KAAK,aAAa,MAAM,MAAM;AAAA,EAC/C;AAEA,QAAM,kBAAkB,wBAAwB,UAAU,WAAW,UAAU,OAAO;AAEtF,MAAI,CAAC,mBAAmB,CAAC,KAAM,QAAO;AAEtC,QAAM,gBAAgB,UAAU,OAAO;AAEvC,SACE,oBAAC,iBAAiB,UAAjB,EAA0B,OAAO,cAC/B,mBAAS;AAAA,IACR;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAY,OAAO;AAAA,QACnB,oBAAkB;AAAA,QAClB,aAAU;AAAA,QACV,aAAW;AAAA,QACX,cAAY;AAAA,QACZ,oBAAkB;AAAA,QAClB;AAAA,QACC,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMT;AAAA,UACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAUA;AAAA,UACA;AAAA,UACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAaA;AAAA,UACA,gBAAgB,eACZ,2DACA;AAAA,UACJ;AAAA,QACF;AAAA,QACA,OAAO;AAAA,UACL,CAAC,IAAI,GAAG,GAAG,UAAU;AAAA,UACrB,GAAG,mBAAmB,OAAO,aAAa,gBAAgB,YAAY,IAAI;AAAA,UAC1E,GAAG;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACA;AAAA,EACF,GACF;AAEJ;AAaA,SAAS,mBAAmB,OAAgC;AAC1D,QAAM,EAAE,WAAW,SAAS,OAAO,UAAU,GAAG,eAAe,IAAI;AAEnE,QAAM,EAAE,OAAO,IAAI,oBAAoB,cAAc;AAErD,QAAM,qBAAqB,UAAU,OAAO;AAE5C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACT,GAAG;AAAA,MAEJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,oBACC,WAEA,iCACG;AAAA,kBAAU,UAAa,OAAO,SAAS,KAAK;AAAA,QAC5C;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;AAEA,SAAS,eAAe,OAAiB;AACvC,QAAM;AAAA,IACJ,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,KAAK;AACpE,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,MAAM,SAAS,CAAC;AAEpE,QAAM,WAAW,MAAM,OAAuB,IAAI;AAClD,QAAM,cAAc,gBAAgB,KAAK,QAAQ;AACjD,QAAM,kBAAkB,MAAM,OAAO,KAAK;AAC1C,QAAM,WAAW,MAAM,OAA8B,oBAAI,IAAI,CAAC;AAE9D,QAAM,EAAE,KAAK,YAAY,IAAI,oBAAoB,UAAU;AAE3D,QAAM,cAAc,MAAM,YAAY,CAACA,eAAsB;AAC3D,iBAAaA,UAAS;AAAA,EACxB,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,wBAAoB,IAAI;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,MAAM,YAAY,MAAM;AACjD,0BAAsB,CAAC,cAAc,YAAY,CAAC;AAAA,EACpD,GAAG,CAAC,CAAC;AAEL,QAAM,wBAAwB,MAAM,YAAY,MAAM;AACpD,0BAAsB,CAAC,cAAc,YAAY,CAAC;AAAA,EACpD,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM,YAAY,CAAC,SAAmB;AAC3D,aAAS,QAAQ,IAAI,KAAK,IAAI,IAAI;AAAA,EACpC,GAAG,CAAC,CAAC;AAEL,QAAM,mBAAmB,MAAM,YAAY,CAAC,OAAe;AACzD,aAAS,QAAQ,OAAO,EAAE;AAAA,EAC5B,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,MAAM,YAAY,MAAM;AACvC,WAAO,MAAM,KAAK,SAAS,QAAQ,OAAO,CAAC,EACxC,OAAO,CAAC,SAAS,KAAK,IAAI,OAAO,EACjC,KAAK,CAAC,GAAG,MAAM;AACd,YAAM,WAAW,EAAE,IAAI;AACvB,YAAM,WAAW,EAAE,IAAI;AACvB,UAAI,CAAC,YAAY,CAAC,SAAU,QAAO;AACnC,YAAM,WAAW,SAAS,wBAAwB,QAAQ;AAC1D,UAAI,WAAW,KAAK,6BAA6B;AAC/C,eAAO;AAAA,MACT;AACA,UAAI,WAAW,KAAK,6BAA6B;AAC/C,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACL,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,MAAM;AAAA,IACnB,CAAC,UAA4C;AAC3C,mBAAa,KAAK;AAClB,UAAI,MAAM,iBAAkB;AAE5B,0BAAoB,KAAK;AAAA,IAC3B;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,UAA4C;AAC3C,oBAAc,KAAK;AACnB,UAAI,MAAM,iBAAkB;AAE5B,YAAM,kBAAkB,CAAC,gBAAgB;AACzC,UAAI,MAAM,WAAW,MAAM,iBAAiB,mBAAmB,CAAC,kBAAkB;AAChF,cAAM,kBAAkB,IAAI,YAAY,aAAa,aAAa;AAClE,cAAM,cAAc,cAAc,eAAe;AAEjD,YAAI,CAAC,gBAAgB,kBAAkB;AACrC,gBAAM,QAAQ,MAAM,KAAK,SAAS,QAAQ,OAAO,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,KAAK,QAAQ;AACnF,gBAAM,cAAc,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,SAAS;AAE9D,gBAAM,iBAAiB,CAAC,aAAa,GAAG,KAAK,EAAE,OAAO,OAAO;AAC7D,gBAAM,gBAAgB,eAAe,IAAI,CAAC,SAAS,KAAK,GAAG;AAC3D,qBAAW,eAAe,KAAK;AAAA,QACjC;AAAA,MACF;AACA,sBAAgB,UAAU;AAAA,IAC5B;AAAA,IACA,CAAC,aAAa,kBAAkB,SAAS;AAAA,EAC3C;AAEA,QAAM,cAAc,MAAM;AAAA,IACxB,CAAC,UAA4C;AAC3C,wBAAkB,KAAK;AACvB,UAAI,MAAM,iBAAkB;AAE5B,sBAAgB,UAAU;AAAA,IAC5B;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAEA,QAAM,oBAAoB,MAAM;AAAA,IAC9B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iBAAiB,UAAU,OAAO;AAExC,SACE,oBAAC,aAAa,UAAb,EAAsB,OAAO,mBAC5B;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,oBAAkB;AAAA,MAClB;AAAA,MACA,UAAU,oBAAoB,uBAAuB,IAAI,KAAK;AAAA,MAC7D,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOA,gBAAgB,eAAe,2BAA2B;AAAA,QAC1D;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF,GACF;AAEJ;AAMA,SAAS,cAAc,OAA2B;AAChD,QAAM;AAAA,IACJ;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,UAAU,MAAM,OAAoB,IAAI;AAC9C,QAAM,cAAc,gBAAgB,KAAK,OAAO;AAChD,QAAM,kBAAkB,MAAM,OAAO,KAAK;AAE1C,QAAM,EAAE,cAAc,KAAK,aAAa,KAAK,IAAI,oBAAoB,SAAS;AAC9E,QAAM,eAAe,gBAAgB,SAAS;AAE9C,QAAM,SAAS,MAAM,MAAM;AAC3B,QAAM,YAAY,aAAa,cAAc;AAE7C,4BAA0B,MAAM;AAC9B,iBAAa,eAAe;AAAA,MAC1B,IAAI;AAAA,MACJ,KAAK;AAAA,MACL,UAAU,CAAC,CAAC;AAAA,IACd,CAAC;AAED,QAAI,CAAC,UAAU;AACb,mBAAa,mBAAmB;AAAA,IAClC;AAEA,WAAO,MAAM;AACX,mBAAa,iBAAiB,MAAM;AACpC,UAAI,CAAC,UAAU;AACb,qBAAa,sBAAsB;AAAA,MACrC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,cAAc,QAAQ,QAAQ,CAAC;AAEnC,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,UAAyC;AACxC,oBAAc,KAAK;AACnB,UAAI,MAAM,iBAAkB;AAE5B,YAAM,OAAO,QAAQ;AACrB,UAAI,CAAC,KAAM;AAEX,YAAM,kBAAkB,IAAI,YAAY,aAAa;AAAA,QACnD,SAAS;AAAA,QACT,YAAY;AAAA,MACd,CAAC;AAED,WAAK,iBAAiB,aAAa,CAACC,WAAU,WAAWA,MAAK,GAAG;AAAA,QAC/D,MAAM;AAAA,MACR,CAAC;AAED,WAAK,cAAc,eAAe;AAElC,UAAI,CAAC,gBAAgB,kBAAkB;AACrC,uBAAe,KAAK;AAAA,MACtB;AAAA,IACF;AAAA,IACA,CAAC,aAAa,cAAc,QAAQ;AAAA,EACtC;AAEA,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,UAAyC;AACxC,oBAAc,KAAK;AACnB,UAAI,MAAM,iBAAkB;AAE5B,mBAAa,YAAY,MAAM;AAC/B,sBAAgB,UAAU;AAAA,IAC5B;AAAA,IACA,CAAC,aAAa,cAAc,MAAM;AAAA,EACpC;AAEA,QAAM,YAAY,MAAM;AAAA,IACtB,CAAC,UAA4C;AAC3C,sBAAgB,KAAK;AACrB,UAAI,MAAM,iBAAkB;AAE5B,UAAI,MAAM,QAAQ,SAAS,MAAM,UAAU;AACzC,qBAAa,eAAe;AAC5B;AAAA,MACF;AAEA,UAAI,MAAM,WAAW,MAAM,cAAe;AAE1C,YAAM,MAAM,qBAAqB,MAAM,KAAK,GAAG;AAC/C,UAAI;AAEJ,UAAI,gBAAgB,cAAc;AAChC,YAAI,QAAQ,YAAa,eAAc;AAAA,iBAC9B,QAAQ,aAAc,eAAc;AAAA,iBACpC,QAAQ,OAAQ,eAAc;AAAA,iBAC9B,QAAQ,MAAO,eAAc;AAAA,MACxC,OAAO;AACL,YAAI,QAAQ,UAAW,eAAc;AAAA,iBAC5B,QAAQ,YAAa,eAAc;AAAA,iBACnC,QAAQ,OAAQ,eAAc;AAAA,iBAC9B,QAAQ,MAAO,eAAc;AAAA,MACxC;AAEA,UAAI,gBAAgB,QAAW;AAC7B,YAAI,MAAM,WAAW,MAAM,WAAW,MAAM,UAAU,MAAM,SAAU;AACtE,cAAM,eAAe;AAErB,cAAM,QAAQ,aAAa,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,KAAK,QAAQ;AACrE,YAAI,gBAAgB,MAAM,IAAI,CAAC,SAAS,KAAK,GAAG;AAEhD,YAAI,gBAAgB,QAAQ;AAC1B,wBAAc,QAAQ;AAAA,QACxB,WAAW,gBAAgB,UAAU,gBAAgB,QAAQ;AAC3D,cAAI,gBAAgB,OAAQ,eAAc,QAAQ;AAClD,gBAAM,eAAe,cAAc,UAAU,CAACC,SAAQA,KAAI,YAAY,MAAM,aAAa;AACzF,0BAAgB,OAAO,UAAU,eAAe,eAAe,CAAC,IAAI,cAAc,MAAM,eAAe,CAAC;AAAA,QAC1G;AAEA,uBAAe,MAAM,WAAW,aAAa,CAAC;AAAA,MAChD;AAAA,IACF;AAAA,IACA,CAAC,eAAe,cAAc,KAAK,aAAa,IAAI;AAAA,EACtD;AAEA,QAAM,cAAc,MAAM;AAAA,IACxB,CAAC,UAAyC;AACxC,wBAAkB,KAAK;AACvB,UAAI,MAAM,iBAAkB;AAE5B,sBAAgB,UAAU;AAE1B,UAAI,UAAU;AACZ,cAAM,eAAe;AAAA,MACvB,OAAO;AACL,qBAAa,YAAY,MAAM;AAAA,MACjC;AAAA,IACF;AAAA,IACA,CAAC,iBAAiB,cAAc,QAAQ,QAAQ;AAAA,EAClD;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,SAAQ;AAAA,MACR,MAAK;AAAA,MACL;AAAA,MACA,UAAU,YAAY,IAAI;AAAA,MACzB,GAAG;AAAA,MACJ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMT;AAAA,QACA,gBAAgB,cAAc;AAAA,QAC9B;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;AAcA,SAAS,eAAe,OAA4B;AAClD,QAAM,EAAE,SAAS,WAAW,SAAS,UAAU,GAAG,WAAW,IAAI;AAEjE,QAAM,EAAE,cAAc,OAAO,IAAI,oBAAoB,UAAU;AAE/D,QAAM,eAAe,MAAM;AAAA,IACzB,CAAC,UAA0C;AACzC,gBAAU,KAAK;AACf,UAAI,MAAM,iBAAkB;AAE5B,qBAAe,KAAK;AAAA,IACtB;AAAA,IACA,CAAC,cAAc,OAAO;AAAA,EACxB;AAEA,QAAM,iBAAiB,UAAU,OAAO;AAExC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,OAAO;AAAA,MACnB,aAAU;AAAA,MACT,GAAG;AAAA,MACJ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAST;AAAA,QACA;AAAA,MACF;AAAA,MACA,SAAS;AAAA,MAER,uBAAa,UAAU,OAAO,oBAAC,KAAE,eAAY,QAAO;AAAA;AAAA,EACvD;AAEJ;AAMA,SAAS,mBAAmB,OAAgC;AAC1D,QAAM,EAAE,aAAa,iBAAiB,SAAS,WAAW,GAAG,eAAe,IAAI;AAEhF,QAAM,UAAU,oBAAoB,cAAc;AAClD,QAAM,cAAc,mBAAmB,QAAQ;AAE/C,QAAM,qBAAqB,UAAU,OAAO;AAE5C,SACE;AAAA,IAAC;AAAA;AAAA,MAQC,eAAY;AAAA,MACZ,oBAAkB;AAAA,MAClB,aAAU;AAAA,MACT,GAAG;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA,gBAAgB,eAAe,aAAa;AAAA,QAC5C;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":["tabStopId","event","ref"]}
@@ -33,7 +33,7 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> &
33
33
  /** Nhãn cho trình đọc màn hình của nút đóng. Mặc định: khoá `closeLabel` của namespace `dialog`. */
34
34
  closeLabel?: string;
35
35
  };
36
- declare function DialogContent({ className, children, showCloseButton, size, closeLabel: closeLabelProp, ...props }: DialogContentProps): React.JSX.Element;
36
+ declare function DialogContent({ className, children, showCloseButton, size, closeLabel: closeLabelProp, onOpenAutoFocus: onOpenAutoFocusProp, onCloseAutoFocus: onCloseAutoFocusProp, ...props }: DialogContentProps): React.JSX.Element;
37
37
  type DialogBodyProps = React.ComponentProps<"div">;
38
38
  /**
39
39
  * Vùng nội dung CUỘN của hộp thoại, giữa header và footer.
@@ -4,6 +4,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
4
4
  import { XIcon } from "lucide-react";
5
5
  import { useComitorMessages } from "../../i18n/locale-provider.js";
6
6
  import { cn } from "../../lib/cn.js";
7
+ import { useModalReturnFocus } from "../../lib/use-modal-return-focus.js";
7
8
  function Dialog({ ...props }) {
8
9
  return /* @__PURE__ */ jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
9
10
  }
@@ -41,10 +42,13 @@ function DialogContent({
41
42
  showCloseButton = true,
42
43
  size = "md",
43
44
  closeLabel: closeLabelProp,
45
+ onOpenAutoFocus: onOpenAutoFocusProp,
46
+ onCloseAutoFocus: onCloseAutoFocusProp,
44
47
  ...props
45
48
  }) {
46
49
  const t = useComitorMessages("dialog");
47
50
  const closeLabel = closeLabelProp ?? t.closeLabel;
51
+ const returnFocus = useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp);
48
52
  return /* @__PURE__ */ jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
49
53
  /* @__PURE__ */ jsx(DialogOverlay, {}),
50
54
  /* @__PURE__ */ jsxs(
@@ -73,6 +77,8 @@ function DialogContent({
73
77
  className
74
78
  ),
75
79
  ...props,
80
+ onOpenAutoFocus: returnFocus.onOpenAutoFocus,
81
+ onCloseAutoFocus: returnFocus.onCloseAutoFocus,
76
82
  children: [
77
83
  children,
78
84
  showCloseButton && /* @__PURE__ */ jsxs(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Dialog — hộp thoại modal (Radix Dialog), style new-york + token Comitor.\n *\n * Khác bản shadcn gốc: có prop `size` (sm/md/lg/xl) và `DialogBody` để tách vùng\n * nội dung cuộn khỏi header/footer.\n */\n\nexport type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>;\n\nfunction Dialog({ ...props }: DialogProps) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nexport type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Trigger>;\n\nfunction DialogTrigger({ ...props }: DialogTriggerProps) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nexport type DialogPortalProps = React.ComponentProps<typeof DialogPrimitive.Portal>;\n\nfunction DialogPortal({ ...props }: DialogPortalProps) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nexport type DialogCloseProps = React.ComponentProps<typeof DialogPrimitive.Close>;\n\nfunction DialogClose({ ...props }: DialogCloseProps) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nexport type DialogOverlayProps = React.ComponentProps<typeof DialogPrimitive.Overlay>;\n\nfunction DialogOverlay({ className, ...props }: DialogOverlayProps) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-ink/60 dark:bg-ink/80 backdrop-blur-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\n/** Bề ngang tối đa theo cỡ — khai tĩnh để Tailwind quét được (không sinh class động). */\nconst dialogSizeStyles = {\n sm: \"sm:max-w-sm\",\n md: \"sm:max-w-md\",\n lg: \"sm:max-w-lg\",\n xl: \"sm:max-w-xl\"\n} as const;\n\nexport type DialogSize = keyof typeof dialogSizeStyles;\n\nexport type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> & {\n /** Hiện nút đóng ở góc phải trên. Mặc định true. */\n showCloseButton?: boolean;\n /** Cỡ hộp thoại theo breakpoint sm trở lên. Mặc định \"md\". */\n size?: DialogSize;\n /** Nhãn cho trình đọc màn hình của nút đóng. Mặc định: khoá `closeLabel` của namespace `dialog`. */\n closeLabel?: string;\n};\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n size = \"md\",\n closeLabel: closeLabelProp,\n ...props\n}: DialogContentProps) {\n const t = useComitorMessages(\"dialog\");\n const closeLabel = closeLabelProp ?? t.closeLabel;\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n /*\n * ⚠ `max-h` + `overflow-y-auto` là BẮT BUỘC, không phải trang trí.\n *\n * Hộp thoại là `position: fixed` căn giữa bằng `translate-y-[-50%]`. Không có trần\n * chiều cao thì một biểu mẫu dài tràn ra CẢ HAI phía màn hình — và vì nó `fixed` nên\n * trang không cuộn nó được: phần đầu và phần cuối (kể cả nút Gửi) trở nên KHÔNG THỂ\n * chạm tới. Đo được ở hộp \"Mời thành viên\": nội dung 812px trong khung nhìn 613px.\n *\n * ⚠ `flex flex-col` chứ KHÔNG phải `grid`. Với `grid`, mọi hàng là `auto` nên không\n * hàng nào chịu co lại — muốn `DialogBody` cuộn thì phải khai `grid-template-rows`,\n * mà làm vậy là ghim thứ tự con (header phải là con thứ nhất). Flex cho `DialogBody`\n * co lại theo `min-h-0` bất kể nó nằm ở đâu.\n *\n * `overflow-y-auto` ở đây là LƯỚI AN TOÀN cho hộp thoại KHÔNG dùng `DialogBody`:\n * có `DialogBody` thì chính nó cuộn và thanh cuộn ngoài này không bao giờ xuất hiện.\n */\n \"bg-card data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col gap-4 overflow-y-auto rounded-xl border border-border p-6 shadow-overlay duration-200\",\n dialogSizeStyles[size],\n className\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"absolute top-4 right-4 rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground hover:bg-muted 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-4\"\n >\n <XIcon />\n <span className=\"sr-only\">{closeLabel}</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nexport type DialogBodyProps = React.ComponentProps<\"div\">;\n\n/**\n * Vùng nội dung CUỘN của hộp thoại, giữa header và footer.\n *\n * ⚠ Trước 1.3.2 component này render `<div className={cn(className)} />` — tức KHÔNG LÀM GÌ CẢ,\n * trong khi chính tài liệu của nó (và của `Dialog`) nói nó \"tách vùng nội dung cuộn khỏi\n * header/footer\". Một component không làm điều nó tự nhận là loại lỗi tệ nhất trong một gói dùng\n * chung: chỗ gọi đã làm ĐÚNG rồi mà vẫn hỏng, nên người ta đi tìm nguyên nhân ở app.\n *\n * ⚠ `flex-auto` chứ không `flex-1`: `flex-1` là `flex: 1 1 0%`, tức lấy 0 làm cỡ cơ sở. `flex-auto`\n * (`1 1 auto`) lấy cỡ NỘI DUNG làm cơ sở, nên hộp thoại ngắn vẫn ôm sát nội dung thay vì bị kéo\n * giãn. `min-h-0` là mảnh bắt buộc còn lại — thiếu nó, `min-height: auto` mặc định của flex item\n * giữ nguyên chiều cao nội dung và không gì cuộn được.\n */\nfunction DialogBody({ className, ...props }: DialogBodyProps) {\n return (\n <div\n data-slot=\"dialog-body\"\n /*\n * ⚠ `-m-1 p-1` KHÔNG phải để cho thoáng — nó là chỗ cho FOCUS RING thở.\n *\n * CSS không cho một trục `auto` còn trục kia `visible`: đặt `overflow-y: auto` thì\n * `overflow-x` **tự thành `auto`**, và hộp cắt xuất hiện ở CẢ HAI chiều. Con của vùng này\n * là `Input` `w-full`, tức mép nó trùng khít mép hộp cắt (đo được: chỗ trống hai bên đúng\n * 0px) — nên `focus:ring-2` nằm 2px bên ngoài bị cắt sạch. Ô đang focus trông như không\n * focus, và đó là dấu hiệu DUY NHẤT cho người dùng bàn phím.\n *\n * `p-1` cho 4,5px chỗ trống (rem gốc 18px); `-m-1` kéo hộp lại đúng bấy nhiêu nên vị trí\n * NỘI DUNG không đổi một pixel — khoảng cách tới header/footer vẫn là `gap-4`.\n *\n * ⚠ Đừng bê khuôn này sang `SheetContent`/`DrawerContent`: chúng `position: fixed` kèm\n * `inset-x-0`/`right-0`, nên margin ÂM làm chúng rộng ra 9px và tràn khỏi khung nhìn.\n * Ở đó, con phải tự mang padding — `SheetHeader`/`SheetFooter` đã có `p-4`.\n */\n className={cn(\"-m-1 min-h-0 flex-auto overflow-y-auto p-1\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogHeaderProps = React.ComponentProps<\"div\">;\n\nfunction DialogHeader({ className, ...props }: DialogHeaderProps) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogFooterProps = React.ComponentProps<\"div\">;\n\nfunction DialogFooter({ className, ...props }: DialogFooterProps) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogTitleProps = React.ComponentProps<typeof DialogPrimitive.Title>;\n\nfunction DialogTitle({ className, ...props }: DialogTitleProps) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg font-semibold text-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogDescriptionProps = React.ComponentProps<typeof DialogPrimitive.Description>;\n\nfunction DialogDescription({ className, ...props }: DialogDescriptionProps) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useModalReturnFocus } from \"../../lib/use-modal-return-focus.js\";\n\n/**\n * Dialog — hộp thoại modal (Radix Dialog), style new-york + token Comitor.\n *\n * Khác bản shadcn gốc: có prop `size` (sm/md/lg/xl) và `DialogBody` để tách vùng\n * nội dung cuộn khỏi header/footer.\n */\n\nexport type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>;\n\nfunction Dialog({ ...props }: DialogProps) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nexport type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Trigger>;\n\nfunction DialogTrigger({ ...props }: DialogTriggerProps) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nexport type DialogPortalProps = React.ComponentProps<typeof DialogPrimitive.Portal>;\n\nfunction DialogPortal({ ...props }: DialogPortalProps) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nexport type DialogCloseProps = React.ComponentProps<typeof DialogPrimitive.Close>;\n\nfunction DialogClose({ ...props }: DialogCloseProps) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nexport type DialogOverlayProps = React.ComponentProps<typeof DialogPrimitive.Overlay>;\n\nfunction DialogOverlay({ className, ...props }: DialogOverlayProps) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-ink/60 dark:bg-ink/80 backdrop-blur-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\n/** Bề ngang tối đa theo cỡ — khai tĩnh để Tailwind quét được (không sinh class động). */\nconst dialogSizeStyles = {\n sm: \"sm:max-w-sm\",\n md: \"sm:max-w-md\",\n lg: \"sm:max-w-lg\",\n xl: \"sm:max-w-xl\"\n} as const;\n\nexport type DialogSize = keyof typeof dialogSizeStyles;\n\nexport type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> & {\n /** Hiện nút đóng ở góc phải trên. Mặc định true. */\n showCloseButton?: boolean;\n /** Cỡ hộp thoại theo breakpoint sm trở lên. Mặc định \"md\". */\n size?: DialogSize;\n /** Nhãn cho trình đọc màn hình của nút đóng. Mặc định: khoá `closeLabel` của namespace `dialog`. */\n closeLabel?: string;\n};\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n size = \"md\",\n closeLabel: closeLabelProp,\n onOpenAutoFocus: onOpenAutoFocusProp,\n onCloseAutoFocus: onCloseAutoFocusProp,\n ...props\n}: DialogContentProps) {\n const t = useComitorMessages(\"dialog\");\n const closeLabel = closeLabelProp ?? t.closeLabel;\n /*\n * ⚠ Trả focus khi đóng mà KHÔNG có `DialogTrigger` (3.3.0) — mở bằng `open` từ menu, dòng bảng,\n * phím tắt. Radix chỉ trả về Trigger, thiếu nó thì focus rơi xuống `<body>`. Handler của app vẫn\n * chạy, và vẫn thắng — xem `lib/use-modal-return-focus.ts`.\n */\n const returnFocus = useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp);\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n /*\n * ⚠ `max-h` + `overflow-y-auto` là BẮT BUỘC, không phải trang trí.\n *\n * Hộp thoại là `position: fixed` căn giữa bằng `translate-y-[-50%]`. Không có trần\n * chiều cao thì một biểu mẫu dài tràn ra CẢ HAI phía màn hình — và vì nó `fixed` nên\n * trang không cuộn nó được: phần đầu và phần cuối (kể cả nút Gửi) trở nên KHÔNG THỂ\n * chạm tới. Đo được ở hộp \"Mời thành viên\": nội dung 812px trong khung nhìn 613px.\n *\n * ⚠ `flex flex-col` chứ KHÔNG phải `grid`. Với `grid`, mọi hàng là `auto` nên không\n * hàng nào chịu co lại — muốn `DialogBody` cuộn thì phải khai `grid-template-rows`,\n * mà làm vậy là ghim thứ tự con (header phải là con thứ nhất). Flex cho `DialogBody`\n * co lại theo `min-h-0` bất kể nó nằm ở đâu.\n *\n * `overflow-y-auto` ở đây là LƯỚI AN TOÀN cho hộp thoại KHÔNG dùng `DialogBody`:\n * có `DialogBody` thì chính nó cuộn và thanh cuộn ngoài này không bao giờ xuất hiện.\n */\n \"bg-card data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col gap-4 overflow-y-auto rounded-xl border border-border p-6 shadow-overlay duration-200\",\n dialogSizeStyles[size],\n className\n )}\n {...props}\n onOpenAutoFocus={returnFocus.onOpenAutoFocus}\n onCloseAutoFocus={returnFocus.onCloseAutoFocus}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"absolute top-4 right-4 rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground hover:bg-muted 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-4\"\n >\n <XIcon />\n <span className=\"sr-only\">{closeLabel}</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nexport type DialogBodyProps = React.ComponentProps<\"div\">;\n\n/**\n * Vùng nội dung CUỘN của hộp thoại, giữa header và footer.\n *\n * ⚠ Trước 1.3.2 component này render `<div className={cn(className)} />` — tức KHÔNG LÀM GÌ CẢ,\n * trong khi chính tài liệu của nó (và của `Dialog`) nói nó \"tách vùng nội dung cuộn khỏi\n * header/footer\". Một component không làm điều nó tự nhận là loại lỗi tệ nhất trong một gói dùng\n * chung: chỗ gọi đã làm ĐÚNG rồi mà vẫn hỏng, nên người ta đi tìm nguyên nhân ở app.\n *\n * ⚠ `flex-auto` chứ không `flex-1`: `flex-1` là `flex: 1 1 0%`, tức lấy 0 làm cỡ cơ sở. `flex-auto`\n * (`1 1 auto`) lấy cỡ NỘI DUNG làm cơ sở, nên hộp thoại ngắn vẫn ôm sát nội dung thay vì bị kéo\n * giãn. `min-h-0` là mảnh bắt buộc còn lại — thiếu nó, `min-height: auto` mặc định của flex item\n * giữ nguyên chiều cao nội dung và không gì cuộn được.\n */\nfunction DialogBody({ className, ...props }: DialogBodyProps) {\n return (\n <div\n data-slot=\"dialog-body\"\n /*\n * ⚠ `-m-1 p-1` KHÔNG phải để cho thoáng — nó là chỗ cho FOCUS RING thở.\n *\n * CSS không cho một trục `auto` còn trục kia `visible`: đặt `overflow-y: auto` thì\n * `overflow-x` **tự thành `auto`**, và hộp cắt xuất hiện ở CẢ HAI chiều. Con của vùng này\n * là `Input` `w-full`, tức mép nó trùng khít mép hộp cắt (đo được: chỗ trống hai bên đúng\n * 0px) — nên `focus:ring-2` nằm 2px bên ngoài bị cắt sạch. Ô đang focus trông như không\n * focus, và đó là dấu hiệu DUY NHẤT cho người dùng bàn phím.\n *\n * `p-1` cho 4,5px chỗ trống (rem gốc 18px); `-m-1` kéo hộp lại đúng bấy nhiêu nên vị trí\n * NỘI DUNG không đổi một pixel — khoảng cách tới header/footer vẫn là `gap-4`.\n *\n * ⚠ Đừng bê khuôn này sang `SheetContent`/`DrawerContent`: chúng `position: fixed` kèm\n * `inset-x-0`/`right-0`, nên margin ÂM làm chúng rộng ra 9px và tràn khỏi khung nhìn.\n * Ở đó, con phải tự mang padding — `SheetHeader`/`SheetFooter` đã có `p-4`.\n */\n className={cn(\"-m-1 min-h-0 flex-auto overflow-y-auto p-1\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogHeaderProps = React.ComponentProps<\"div\">;\n\nfunction DialogHeader({ className, ...props }: DialogHeaderProps) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogFooterProps = React.ComponentProps<\"div\">;\n\nfunction DialogFooter({ className, ...props }: DialogFooterProps) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogTitleProps = React.ComponentProps<typeof DialogPrimitive.Title>;\n\nfunction DialogTitle({ className, ...props }: DialogTitleProps) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg font-semibold text-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport type DialogDescriptionProps = React.ComponentProps<typeof DialogPrimitive.Description>;\n\nfunction DialogDescription({ className, ...props }: DialogDescriptionProps) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger\n};\n"],"mappings":"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","names":[]}
@@ -25,7 +25,7 @@ type SheetContentProps = React.ComponentProps<typeof DialogPrimitive.Content> &
25
25
  /** Nhãn cho trình đọc màn hình của nút đóng. Mặc định: khoá `closeLabel` của namespace `sheet`. */
26
26
  closeLabel?: string;
27
27
  };
28
- declare function SheetContent({ className, children, side, closeLabel: closeLabelProp, ...props }: SheetContentProps): React.JSX.Element;
28
+ declare function SheetContent({ className, children, side, closeLabel: closeLabelProp, onOpenAutoFocus: onOpenAutoFocusProp, onCloseAutoFocus: onCloseAutoFocusProp, ...props }: SheetContentProps): React.JSX.Element;
29
29
  type SheetHeaderProps = React.ComponentProps<"div">;
30
30
  declare function SheetHeader({ className, ...props }: SheetHeaderProps): React.JSX.Element;
31
31
  type SheetFooterProps = React.ComponentProps<"div">;
@@ -4,6 +4,7 @@ import * as SheetPrimitive from "@radix-ui/react-dialog";
4
4
  import { XIcon } from "lucide-react";
5
5
  import { useComitorMessages } from "../../i18n/locale-provider.js";
6
6
  import { cn } from "../../lib/cn.js";
7
+ import { useModalReturnFocus } from "../../lib/use-modal-return-focus.js";
7
8
  function Sheet({ ...props }) {
8
9
  return /* @__PURE__ */ jsx(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
9
10
  }
@@ -34,10 +35,13 @@ function SheetContent({
34
35
  children,
35
36
  side = "right",
36
37
  closeLabel: closeLabelProp,
38
+ onOpenAutoFocus: onOpenAutoFocusProp,
39
+ onCloseAutoFocus: onCloseAutoFocusProp,
37
40
  ...props
38
41
  }) {
39
42
  const t = useComitorMessages("sheet");
40
43
  const closeLabel = closeLabelProp ?? t.closeLabel;
44
+ const returnFocus = useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp);
41
45
  return /* @__PURE__ */ jsxs(SheetPortal, { children: [
42
46
  /* @__PURE__ */ jsx(SheetOverlay, {}),
43
47
  /* @__PURE__ */ jsxs(
@@ -58,6 +62,8 @@ function SheetContent({
58
62
  className
59
63
  ),
60
64
  ...props,
65
+ onOpenAutoFocus: returnFocus.onOpenAutoFocus,
66
+ onCloseAutoFocus: returnFocus.onCloseAutoFocus,
61
67
  children: [
62
68
  children,
63
69
  /* @__PURE__ */ jsxs(SheetPrimitive.Close, { className: "absolute top-4 right-4 rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground hover:bg-muted 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", children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/sheet.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Sheet — panel trượt từ cạnh màn hình (dựng trên Radix Dialog).\n *\n * Dùng cho menu di động, bộ lọc, panel chi tiết. Shell Tier 3 dùng `side=\"left\"`\n * cho MobileMenu. Với panel kéo-thả kiểu mobile (có tay cầm, kéo để đóng) thì\n * dùng `Drawer` (vaul) thay vì Sheet.\n */\n\nexport type SheetProps = React.ComponentProps<typeof SheetPrimitive.Root>;\n\nfunction Sheet({ ...props }: SheetProps) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nexport type SheetTriggerProps = React.ComponentProps<typeof SheetPrimitive.Trigger>;\n\nfunction SheetTrigger({ ...props }: SheetTriggerProps) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nexport type SheetCloseProps = React.ComponentProps<typeof SheetPrimitive.Close>;\n\nfunction SheetClose({ ...props }: SheetCloseProps) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nexport type SheetPortalProps = React.ComponentProps<typeof SheetPrimitive.Portal>;\n\nfunction SheetPortal({ ...props }: SheetPortalProps) {\n return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nexport type SheetOverlayProps = React.ComponentProps<typeof SheetPrimitive.Overlay>;\n\nfunction SheetOverlay({ className, ...props }: SheetOverlayProps) {\n return (\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-ink/60 dark:bg-ink/80 backdrop-blur-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type SheetSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport type SheetContentProps = React.ComponentProps<typeof SheetPrimitive.Content> & {\n /** Cạnh màn hình mà panel trượt ra. Mặc định \"right\". */\n side?: SheetSide;\n /** Nhãn cho trình đọc màn hình của nút đóng. Mặc định: khoá `closeLabel` của namespace `sheet`. */\n closeLabel?: string;\n};\n\nfunction SheetContent({\n className,\n children,\n side = \"right\",\n closeLabel: closeLabelProp,\n ...props\n}: SheetContentProps) {\n const t = useComitorMessages(\"sheet\");\n const closeLabel = closeLabelProp ?? t.closeLabel;\n return (\n <SheetPortal>\n <SheetOverlay />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n /*\n * `overflow-y-auto`: cùng lỗi với `DialogContent` trước 1.3.2 — `position: fixed`,\n * `h-full`, mà không có `overflow` nào, nên nội dung dài hơn khung nhìn bị CẮT và\n * không cuộn được. `SheetFooter` mang `mt-auto` nên nó vẫn dính đáy khi nội dung ngắn.\n */\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 overflow-y-auto shadow-overlay transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n side === \"right\" &&\n \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n side === \"left\" &&\n \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n side === \"top\" &&\n \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n side === \"bottom\" &&\n \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n className\n )}\n {...props}\n >\n {children}\n <SheetPrimitive.Close className=\"absolute top-4 right-4 rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground hover:bg-muted 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\">\n <XIcon className=\"size-4\" />\n <span className=\"sr-only\">{closeLabel}</span>\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPortal>\n );\n}\n\nexport type SheetHeaderProps = React.ComponentProps<\"div\">;\n\nfunction SheetHeader({ className, ...props }: SheetHeaderProps) {\n return <div data-slot=\"sheet-header\" className={cn(\"flex flex-col gap-1.5 p-4\", className)} {...props} />;\n}\n\nexport type SheetFooterProps = React.ComponentProps<\"div\">;\n\nfunction SheetFooter({ className, ...props }: SheetFooterProps) {\n return <div data-slot=\"sheet-footer\" className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)} {...props} />;\n}\n\nexport type SheetTitleProps = React.ComponentProps<typeof SheetPrimitive.Title>;\n\nfunction SheetTitle({ className, ...props }: SheetTitleProps) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn(\"text-foreground font-semibold\", className)}\n {...props}\n />\n );\n}\n\nexport type SheetDescriptionProps = React.ComponentProps<typeof SheetPrimitive.Description>;\n\nfunction SheetDescription({ className, ...props }: SheetDescriptionProps) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger\n};\n"],"mappings":";AAmBS,cA+ED,YA/EC;AAjBT,YAAY,oBAAoB;AAChC,SAAS,aAAa;AAEtB,SAAS,0BAA0B;AACnC,SAAS,UAAU;AAYnB,SAAS,MAAM,EAAE,GAAG,MAAM,GAAe;AACvC,SAAO,oBAAC,eAAe,MAAf,EAAoB,aAAU,SAAS,GAAG,OAAO;AAC3D;AAIA,SAAS,aAAa,EAAE,GAAG,MAAM,GAAsB;AACrD,SAAO,oBAAC,eAAe,SAAf,EAAuB,aAAU,iBAAiB,GAAG,OAAO;AACtE;AAIA,SAAS,WAAW,EAAE,GAAG,MAAM,GAAoB;AACjD,SAAO,oBAAC,eAAe,OAAf,EAAqB,aAAU,eAAe,GAAG,OAAO;AAClE;AAIA,SAAS,YAAY,EAAE,GAAG,MAAM,GAAqB;AACnD,SAAO,oBAAC,eAAe,QAAf,EAAsB,aAAU,gBAAgB,GAAG,OAAO;AACpE;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,GAAG;AACL,GAAsB;AACpB,QAAM,IAAI,mBAAmB,OAAO;AACpC,QAAM,aAAa,kBAAkB,EAAE;AACvC,SACE,qBAAC,eACC;AAAA,wBAAC,gBAAa;AAAA,IACd;AAAA,MAAC,eAAe;AAAA,MAAf;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMT;AAAA,UACA,SAAS,WACP;AAAA,UACF,SAAS,UACP;AAAA,UACF,SAAS,SACP;AAAA,UACF,SAAS,YACP;AAAA,UACF;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAEH;AAAA;AAAA,UACD,qBAAC,eAAe,OAAf,EAAqB,WAAU,qTAC9B;AAAA,gCAAC,SAAM,WAAU,UAAS;AAAA,YAC1B,oBAAC,UAAK,WAAU,WAAW,sBAAW;AAAA,aACxC;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,SAAI,aAAU,gBAAe,WAAW,GAAG,6BAA6B,SAAS,GAAI,GAAG,OAAO;AACzG;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,SAAI,aAAU,gBAAe,WAAW,GAAG,mCAAmC,SAAS,GAAI,GAAG,OAAO;AAC/G;AAIA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/sheet.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useModalReturnFocus } from \"../../lib/use-modal-return-focus.js\";\n\n/**\n * Sheet — panel trượt từ cạnh màn hình (dựng trên Radix Dialog).\n *\n * Dùng cho menu di động, bộ lọc, panel chi tiết. Shell Tier 3 dùng `side=\"left\"`\n * cho MobileMenu. Với panel kéo-thả kiểu mobile (có tay cầm, kéo để đóng) thì\n * dùng `Drawer` (vaul) thay vì Sheet.\n */\n\nexport type SheetProps = React.ComponentProps<typeof SheetPrimitive.Root>;\n\nfunction Sheet({ ...props }: SheetProps) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nexport type SheetTriggerProps = React.ComponentProps<typeof SheetPrimitive.Trigger>;\n\nfunction SheetTrigger({ ...props }: SheetTriggerProps) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nexport type SheetCloseProps = React.ComponentProps<typeof SheetPrimitive.Close>;\n\nfunction SheetClose({ ...props }: SheetCloseProps) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nexport type SheetPortalProps = React.ComponentProps<typeof SheetPrimitive.Portal>;\n\nfunction SheetPortal({ ...props }: SheetPortalProps) {\n return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nexport type SheetOverlayProps = React.ComponentProps<typeof SheetPrimitive.Overlay>;\n\nfunction SheetOverlay({ className, ...props }: SheetOverlayProps) {\n return (\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-ink/60 dark:bg-ink/80 backdrop-blur-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type SheetSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport type SheetContentProps = React.ComponentProps<typeof SheetPrimitive.Content> & {\n /** Cạnh màn hình mà panel trượt ra. Mặc định \"right\". */\n side?: SheetSide;\n /** Nhãn cho trình đọc màn hình của nút đóng. Mặc định: khoá `closeLabel` của namespace `sheet`. */\n closeLabel?: string;\n};\n\nfunction SheetContent({\n className,\n children,\n side = \"right\",\n closeLabel: closeLabelProp,\n onOpenAutoFocus: onOpenAutoFocusProp,\n onCloseAutoFocus: onCloseAutoFocusProp,\n ...props\n}: SheetContentProps) {\n const t = useComitorMessages(\"sheet\");\n const closeLabel = closeLabelProp ?? t.closeLabel;\n // Trả focus khi đóng mà không có `SheetTrigger` (3.3.0) — cùng lỗi, cùng khuôn với `DialogContent`.\n const returnFocus = useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp);\n return (\n <SheetPortal>\n <SheetOverlay />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n /*\n * `overflow-y-auto`: cùng lỗi với `DialogContent` trước 1.3.2 — `position: fixed`,\n * `h-full`, mà không có `overflow` nào, nên nội dung dài hơn khung nhìn bị CẮT và\n * không cuộn được. `SheetFooter` mang `mt-auto` nên nó vẫn dính đáy khi nội dung ngắn.\n */\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 overflow-y-auto shadow-overlay transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n side === \"right\" &&\n \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n side === \"left\" &&\n \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n side === \"top\" &&\n \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n side === \"bottom\" &&\n \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n className\n )}\n {...props}\n onOpenAutoFocus={returnFocus.onOpenAutoFocus}\n onCloseAutoFocus={returnFocus.onCloseAutoFocus}\n >\n {children}\n <SheetPrimitive.Close className=\"absolute top-4 right-4 rounded-md p-1 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground hover:bg-muted 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\">\n <XIcon className=\"size-4\" />\n <span className=\"sr-only\">{closeLabel}</span>\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPortal>\n );\n}\n\nexport type SheetHeaderProps = React.ComponentProps<\"div\">;\n\nfunction SheetHeader({ className, ...props }: SheetHeaderProps) {\n return <div data-slot=\"sheet-header\" className={cn(\"flex flex-col gap-1.5 p-4\", className)} {...props} />;\n}\n\nexport type SheetFooterProps = React.ComponentProps<\"div\">;\n\nfunction SheetFooter({ className, ...props }: SheetFooterProps) {\n return <div data-slot=\"sheet-footer\" className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)} {...props} />;\n}\n\nexport type SheetTitleProps = React.ComponentProps<typeof SheetPrimitive.Title>;\n\nfunction SheetTitle({ className, ...props }: SheetTitleProps) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn(\"text-foreground font-semibold\", className)}\n {...props}\n />\n );\n}\n\nexport type SheetDescriptionProps = React.ComponentProps<typeof SheetPrimitive.Description>;\n\nfunction SheetDescription({ className, ...props }: SheetDescriptionProps) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger\n};\n"],"mappings":";AAoBS,cAqFD,YArFC;AAlBT,YAAY,oBAAoB;AAChC,SAAS,aAAa;AAEtB,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAAS,2BAA2B;AAYpC,SAAS,MAAM,EAAE,GAAG,MAAM,GAAe;AACvC,SAAO,oBAAC,eAAe,MAAf,EAAoB,aAAU,SAAS,GAAG,OAAO;AAC3D;AAIA,SAAS,aAAa,EAAE,GAAG,MAAM,GAAsB;AACrD,SAAO,oBAAC,eAAe,SAAf,EAAuB,aAAU,iBAAiB,GAAG,OAAO;AACtE;AAIA,SAAS,WAAW,EAAE,GAAG,MAAM,GAAoB;AACjD,SAAO,oBAAC,eAAe,OAAf,EAAqB,aAAU,eAAe,GAAG,OAAO;AAClE;AAIA,SAAS,YAAY,EAAE,GAAG,MAAM,GAAqB;AACnD,SAAO,oBAAC,eAAe,QAAf,EAAsB,aAAU,gBAAgB,GAAG,OAAO;AACpE;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,GAAG;AACL,GAAsB;AACpB,QAAM,IAAI,mBAAmB,OAAO;AACpC,QAAM,aAAa,kBAAkB,EAAE;AAEvC,QAAM,cAAc,oBAAoB,qBAAqB,oBAAoB;AACjF,SACE,qBAAC,eACC;AAAA,wBAAC,gBAAa;AAAA,IACd;AAAA,MAAC,eAAe;AAAA,MAAf;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMT;AAAA,UACA,SAAS,WACP;AAAA,UACF,SAAS,UACP;AAAA,UACF,SAAS,SACP;AAAA,UACF,SAAS,YACP;AAAA,UACF;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QACJ,iBAAiB,YAAY;AAAA,QAC7B,kBAAkB,YAAY;AAAA,QAE7B;AAAA;AAAA,UACD,qBAAC,eAAe,OAAf,EAAqB,WAAU,qTAC9B;AAAA,gCAAC,SAAM,WAAU,UAAS;AAAA,YAC1B,oBAAC,UAAK,WAAU,WAAW,sBAAW;AAAA,aACxC;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,SAAI,aAAU,gBAAe,WAAW,GAAG,6BAA6B,SAAS,GAAI,GAAG,OAAO;AACzG;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,SAAI,aAAU,gBAAe,WAAW,GAAG,mCAAmC,SAAS,GAAI,GAAG,OAAO;AAC/G;AAIA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -22,6 +22,6 @@ import { ToasterProps } from 'sonner';
22
22
  * hai tên" chỉ đúng khi tên trần không còn lối ra.
23
23
  */
24
24
  type SonnerToasterProps = ToasterProps;
25
- declare const Toaster: ({ ...props }: ToasterProps) => React.JSX.Element;
25
+ declare const Toaster: ({ toastOptions, ...props }: ToasterProps) => React.JSX.Element;
26
26
 
27
27
  export { Toaster as SonnerToaster, type SonnerToasterProps };
@@ -2,10 +2,12 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { useTheme } from "next-themes";
4
4
  import { Toaster as Sonner } from "sonner";
5
+ import { useComitorMessages } from "../../i18n/locale-provider.js";
5
6
  import { useToastLanguageBridge } from "../../lib/toast-language.js";
6
- const Toaster = ({ ...props }) => {
7
+ const Toaster = ({ toastOptions, ...props }) => {
7
8
  const { theme = "system" } = useTheme();
8
9
  useToastLanguageBridge();
10
+ const t = useComitorMessages("toast");
9
11
  return /* @__PURE__ */ jsx(
10
12
  Sonner,
11
13
  {
@@ -16,6 +18,8 @@ const Toaster = ({ ...props }) => {
16
18
  "--normal-text": "var(--popover-foreground)",
17
19
  "--normal-border": "var(--border)"
18
20
  },
21
+ containerAriaLabel: t.containerAriaLabel,
22
+ toastOptions: { closeButtonAriaLabel: t.closeButtonAriaLabel, ...toastOptions },
19
23
  ...props
20
24
  }
21
25
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/sonner.tsx"],"sourcesContent":["\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\";\nimport { useToastLanguageBridge } from \"../../lib/toast-language.js\";\n\n/**\n * Toaster — bọc `sonner` và nối màu vào token popover của Comitor.\n *\n * Phụ thuộc `next-themes` (peer OPTIONAL) để đọc theme hiện tại; app nào không\n * dùng next-themes thì đừng render component này — hoặc render `<Sonner>` trực tiếp.\n *\n * Đặt duy nhất MỘT `<Toaster />` ở gốc app; gọi `toast()` của `sonner` ở bất kỳ đâu.\n * Bản có icon theo trạng thái (success/warning/info) là composite `Toaster` ở Tier 2.\n */\n\n/**\n * Cửa DUY NHẤT của file này: `SonnerToaster` + `SonnerToasterProps`.\n *\n * Tên trần `Toaster`/`ToasterProps` cố ý KHÔNG xuất, dù chúng là tên tự nhiên ở đây: composite\n * `Toaster`/`ToasterProps` (`composite/toast.tsx`) đã chiếm đúng hai tên đó ở entry `.`. Nếu để\n * chúng xuất ra thì (a) `shell/index.ts` chỉ lấy bản `Sonner*` nên chúng là export CHẾT, và\n * (b) ai thêm `export * from \"./components/ui/sonner.js\"` vào `src/index.ts` sẽ vỡ build ngay,\n * còn thêm vào `shell/index.ts` thì im lặng dựng thêm một cửa `Toaster` thứ hai.\n * Giữ hai toaster là quyết định có chủ ý (docs/quyet-dinh-tich-hop.md §31) — nhưng \"hai toaster,\n * hai tên\" chỉ đúng khi tên trần không còn lối ra.\n */\nexport type SonnerToasterProps = ToasterProps;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme();\n /*\n * Ghi ngôn ngữ của `ComitorLocaleProvider` cho `toastPromise` — như `Toaster` ở Tier 2. Thiếu dòng\n * này (tới `3.0.0`) thì toast \"đang chạy\" của app dùng shell luôn nói tiếng Anh, dù đã có provider.\n */\n useToastLanguageBridge();\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\"\n } as React.CSSProperties\n }\n {...props}\n />\n );\n};\n\nexport { Toaster as SonnerToaster };\n"],"mappings":";AAuCI;AArCJ,SAAS,gBAAgB;AAEzB,SAAS,WAAW,cAAiC;AACrD,SAAS,8BAA8B;AAyBvC,MAAM,UAAU,CAAC,EAAE,GAAG,MAAM,MAAoB;AAC9C,QAAM,EAAE,QAAQ,SAAS,IAAI,SAAS;AAKtC,yBAAuB;AAEvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,OACE;AAAA,QACE,eAAe;AAAA,QACf,iBAAiB;AAAA,QACjB,mBAAmB;AAAA,MACrB;AAAA,MAED,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/sonner.tsx"],"sourcesContent":["\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { useToastLanguageBridge } from \"../../lib/toast-language.js\";\n\n/**\n * Toaster — bọc `sonner` và nối màu vào token popover của Comitor.\n *\n * Phụ thuộc `next-themes` (peer OPTIONAL) để đọc theme hiện tại; app nào không\n * dùng next-themes thì đừng render component này — hoặc render `<Sonner>` trực tiếp.\n *\n * Đặt duy nhất MỘT `<Toaster />` ở gốc app; gọi `toast()` của `sonner` ở bất kỳ đâu.\n * Bản có icon theo trạng thái (success/warning/info) là composite `Toaster` ở Tier 2.\n */\n\n/**\n * Cửa DUY NHẤT của file này: `SonnerToaster` + `SonnerToasterProps`.\n *\n * Tên trần `Toaster`/`ToasterProps` cố ý KHÔNG xuất, dù chúng là tên tự nhiên ở đây: composite\n * `Toaster`/`ToasterProps` (`composite/toast.tsx`) đã chiếm đúng hai tên đó ở entry `.`. Nếu để\n * chúng xuất ra thì (a) `shell/index.ts` chỉ lấy bản `Sonner*` nên chúng là export CHẾT, và\n * (b) ai thêm `export * from \"./components/ui/sonner.js\"` vào `src/index.ts` sẽ vỡ build ngay,\n * còn thêm vào `shell/index.ts` thì im lặng dựng thêm một cửa `Toaster` thứ hai.\n * Giữ hai toaster là quyết định có chủ ý (docs/quyet-dinh-tich-hop.md §31) — nhưng \"hai toaster,\n * hai tên\" chỉ đúng khi tên trần không còn lối ra.\n */\nexport type SonnerToasterProps = ToasterProps;\n\nconst Toaster = ({ toastOptions, ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme();\n /*\n * Ghi ngôn ngữ của `ComitorLocaleProvider` cho `toastPromise` — như `Toaster` ở Tier 2. Thiếu dòng\n * này (tới `3.0.0`) thì toast \"đang chạy\" của app dùng shell luôn nói tiếng Anh, dù đã có provider.\n */\n useToastLanguageBridge();\n const t = useComitorMessages(\"toast\");\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\"\n } as React.CSSProperties\n }\n /*\n * Hai chuỗi a11y mà sonner viết cứng bằng tiếng Anh — tên vùng toast (\"Notifications\") và\n * tên nút × (\"Close toast\") — nay theo provider (3.3.0). Trước bản này app phải tự truyền,\n * và app nào quên thì trình đọc màn hình nghe tiếng Anh giữa giao diện tiếng Việt.\n * Prop của app vẫn thắng: `containerAriaLabel` qua `{...props}` đứng sau, `toastOptions` của\n * app trải lên trên mặc định.\n */\n containerAriaLabel={t.containerAriaLabel}\n toastOptions={{ closeButtonAriaLabel: t.closeButtonAriaLabel, ...toastOptions }}\n {...props}\n />\n );\n};\n\nexport { Toaster as SonnerToaster };\n"],"mappings":";AAyCI;AAvCJ,SAAS,gBAAgB;AAEzB,SAAS,WAAW,cAAiC;AACrD,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAyBvC,MAAM,UAAU,CAAC,EAAE,cAAc,GAAG,MAAM,MAAoB;AAC5D,QAAM,EAAE,QAAQ,SAAS,IAAI,SAAS;AAKtC,yBAAuB;AACvB,QAAM,IAAI,mBAAmB,OAAO;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,OACE;AAAA,QACE,eAAe;AAAA,QACf,iBAAiB;AAAA,QACjB,mBAAmB;AAAA,MACrB;AAAA,MASF,oBAAoB,EAAE;AAAA,MACtB,cAAc,EAAE,sBAAsB,EAAE,sBAAsB,GAAG,aAAa;AAAA,MAC7E,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -107,7 +107,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
107
107
  pasteDialog: PasteDialogState;
108
108
  onRowAdd: ((event?: React.MouseEvent<HTMLDivElement>) => Promise<void>) | undefined;
109
109
  adjustLayout: boolean;
110
- locale: "en" | "vi" | {
110
+ locale: {
111
111
  grid: {
112
112
  label: string;
113
113
  addRow: string;
@@ -382,7 +382,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
382
382
  shortcuts: string;
383
383
  };
384
384
  };
385
- } | undefined;
385
+ } | "en" | "vi" | undefined;
386
386
  };
387
387
 
388
388
  export { type UseDataGridProps, useDataGrid };
@@ -1,14 +1,23 @@
1
1
  /**
2
- * Chữ của `toastPromise`.
2
+ * Chữ của hai toaster (`Toaster` của entry chính, `SonnerToaster` của `/shell`) và của `toastPromise`.
3
3
  *
4
4
  * `toastPromise` là hàm THƯỜNG gọi từ event handler, ngoài lượt render — nó không tự đọc được
5
- * `ComitorLocaleProvider`. Toaster (luôn nằm trong cây, dưới provider — `Toaster` của entry chính
6
- * hay `SonnerToaster` của `/shell`) ghi bảng này theo ngôn ngữ hiện hành cho nó dùng; xem
7
- * `lib/toast-language.ts`.
5
+ * `ComitorLocaleProvider`. Toaster (luôn nằm trong cây, dưới provider) ghi bảng này theo ngôn ngữ
6
+ * hiện hành cho nó dùng; xem `lib/toast-language.ts`.
7
+ *
8
+ * ⚠ Hai khoá `…AriaLabel` (3.3.0) thay hai chuỗi TIẾNG ANH CỨNG của sonner — thứ trước bản này là
9
+ * chữ tiếng Anh DUY NHẤT còn lại trên một trang tiếng Việt đã đặt provider (đo trên comitor-account):
10
+ * · `containerAriaLabel` — tên vùng toast (`<section aria-label>`), chỗ trình đọc màn hình nhảy tới
11
+ * bằng phím tắt. Sonner TỰ NỐI phím tắt vào sau ("Thông báo alt+T"), nên ở đây chỉ có phần chữ.
12
+ * · `closeButtonAriaLabel` — tên nút × trên từng toast, chỉ hiện khi app bật `closeButton`.
8
13
  */
9
14
  interface ToastMessages {
10
15
  /** Chữ của toast lúc Promise còn chạy — `messages.loading` của `toastPromise` */
11
16
  loading: string;
17
+ /** Tên vùng chứa toast cho trình đọc màn hình — prop `containerAriaLabel` của sonner */
18
+ containerAriaLabel: string;
19
+ /** Tên nút đóng trên mỗi toast — `toastOptions.closeButtonAriaLabel` của sonner */
20
+ closeButtonAriaLabel: string;
12
21
  }
13
22
  declare const toastEn: ToastMessages;
14
23
  declare const toastVi: ToastMessages;
@@ -1,5 +1,13 @@
1
- const toastEn = { loading: "Working\u2026" };
2
- const toastVi = { loading: "\u0110ang x\u1EED l\xFD\u2026" };
1
+ const toastEn = {
2
+ loading: "Working\u2026",
3
+ containerAriaLabel: "Notifications",
4
+ closeButtonAriaLabel: "Close notification"
5
+ };
6
+ const toastVi = {
7
+ loading: "\u0110ang x\u1EED l\xFD\u2026",
8
+ containerAriaLabel: "Th\xF4ng b\xE1o",
9
+ closeButtonAriaLabel: "\u0110\xF3ng th\xF4ng b\xE1o"
10
+ };
3
11
  export {
4
12
  toastEn,
5
13
  toastVi
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/toast.ts"],"sourcesContent":["/**\n * Chữ của `toastPromise`.\n *\n * `toastPromise` là hàm THƯỜNG gọi từ event handler, ngoài lượt render — nó không tự đọc được\n * `ComitorLocaleProvider`. Toaster (luôn nằm trong cây, dưới provider — `Toaster` của entry chính\n * hay `SonnerToaster` của `/shell`) ghi bảng này theo ngôn ngữ hiện hành cho nó dùng; xem\n * `lib/toast-language.ts`.\n */\nexport interface ToastMessages {\n /** Chữ của toast lúc Promise còn chạy — `messages.loading` của `toastPromise` */\n loading: string;\n}\n\nexport const toastEn: ToastMessages = { loading: \"Working…\" };\n\nexport const toastVi: ToastMessages = { loading: \"Đang xử lý…\" };\n"],"mappings":"AAaO,MAAM,UAAyB,EAAE,SAAS,gBAAW;AAErD,MAAM,UAAyB,EAAE,SAAS,gCAAc;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/toast.ts"],"sourcesContent":["/**\n * Chữ của hai toaster (`Toaster` của entry chính, `SonnerToaster` của `/shell`) và của `toastPromise`.\n *\n * `toastPromise` là hàm THƯỜNG gọi từ event handler, ngoài lượt render — nó không tự đọc được\n * `ComitorLocaleProvider`. Toaster (luôn nằm trong cây, dưới provider) ghi bảng này theo ngôn ngữ\n * hiện hành cho nó dùng; xem `lib/toast-language.ts`.\n *\n * ⚠ Hai khoá `…AriaLabel` (3.3.0) thay hai chuỗi TIẾNG ANH CỨNG của sonner — thứ trước bản này là\n * chữ tiếng Anh DUY NHẤT còn lại trên một trang tiếng Việt đã đặt provider (đo trên comitor-account):\n * · `containerAriaLabel` — tên vùng toast (`<section aria-label>`), chỗ trình đọc màn hình nhảy tới\n * bằng phím tắt. Sonner TỰ NỐI phím tắt vào sau (\"Thông báo alt+T\"), nên ở đây chỉ có phần chữ.\n * · `closeButtonAriaLabel` — tên nút × trên từng toast, chỉ hiện khi app bật `closeButton`.\n */\nexport interface ToastMessages {\n /** Chữ của toast lúc Promise còn chạy — `messages.loading` của `toastPromise` */\n loading: string;\n /** Tên vùng chứa toast cho trình đọc màn hình — prop `containerAriaLabel` của sonner */\n containerAriaLabel: string;\n /** Tên nút đóng trên mỗi toast — `toastOptions.closeButtonAriaLabel` của sonner */\n closeButtonAriaLabel: string;\n}\n\nexport const toastEn: ToastMessages = {\n loading: \"Working…\",\n containerAriaLabel: \"Notifications\",\n closeButtonAriaLabel: \"Close notification\"\n};\n\nexport const toastVi: ToastMessages = {\n loading: \"Đang xử lý…\",\n containerAriaLabel: \"Thông báo\",\n closeButtonAriaLabel: \"Đóng thông báo\"\n};\n"],"mappings":"AAsBO,MAAM,UAAyB;AAAA,EACpC,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,sBAAsB;AACxB;AAEO,MAAM,UAAyB;AAAA,EACpC,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,sBAAsB;AACxB;","names":[]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * TRẢ FOCUS khi một lớp modal của Radix (`Dialog`, `Sheet`) đóng mà nó được mở KHÔNG qua Trigger —
3
+ * nội bộ, không nằm trong barrel nào.
4
+ *
5
+ * Radix chỉ trả focus về `DialogTrigger`: handler `onCloseAutoFocus` của chính nó chặn mặc định của
6
+ * `FocusScope` rồi gọi `triggerRef.current?.focus()` — thiếu Trigger là focus vào `null`. Mà mở bằng
7
+ * `open` từ một mục menu, một dòng bảng, một `ActionBar` hay một phím tắt là cách hộp thoại được dùng
8
+ * nhiều nhất trong app thật: đóng xong (Escape, nút ×, nút Huỷ) là focus rơi xuống `<body>`, Tab kế
9
+ * tiếp chạy lại từ đầu trang (WCAG 2.4.3). Đo trên một trang dựng từ `dist` (3.3.0): mở `Dialog` từ
10
+ * một `Button` thường, Escape bằng phím thật → `document.activeElement` là `<body>`.
11
+ * `ConfirmDialog` đã tự vá đúng lỗi này ở `3.1.0`; đây là cùng khuôn, đặt ở tầng primitive.
12
+ *
13
+ * Luật — ai thắng khi nhiều bên cùng muốn đưa focus đi:
14
+ * 1. App: handler `onCloseAutoFocus` của app chạy TRƯỚC; app gọi `preventDefault()` là app nhận việc
15
+ * (Radix cũng bỏ qua Trigger), gói không làm gì thêm.
16
+ * 2. Radix: handler của nó chạy ngay SAU handler này và trả focus về Trigger nếu có — nên phép kiểm
17
+ * của gói đợi qua một microtask để Radix đi trước.
18
+ * 3. Gói: chỉ khi focus THẬT SỰ đã rơi (`<body>`) mới trả về phần tử giữ focus ngay trước khi lớp
19
+ * mở. Phần tử đó đã bị gỡ khỏi DOM (dòng vừa xoá) thì gói không đoán.
20
+ */
21
+ declare function useModalReturnFocus<E extends Event>(onOpenAutoFocusProp: ((event: E) => void) | undefined, onCloseAutoFocusProp: ((event: E) => void) | undefined): {
22
+ onOpenAutoFocus: (event: E) => void;
23
+ onCloseAutoFocus: (event: E) => void;
24
+ };
25
+
26
+ export { useModalReturnFocus };
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ function returnTargetFrom(active) {
3
+ let current = active;
4
+ for (let depth = 0; depth < 5; depth++) {
5
+ if (!(current instanceof HTMLElement) || current === current.ownerDocument.body) return null;
6
+ const menu = current.closest('[role="menu"]');
7
+ if (!menu) return current;
8
+ const ownerDocument = menu.ownerDocument;
9
+ const labelledBy = menu.getAttribute("aria-labelledby");
10
+ const byLabel = labelledBy ? ownerDocument.getElementById(labelledBy) : null;
11
+ const byControls = menu.id ? ownerDocument.querySelector(`[aria-controls="${CSS.escape(menu.id)}"]`) : null;
12
+ const opener = byLabel ?? byControls;
13
+ if (!opener) return current;
14
+ current = opener;
15
+ }
16
+ return current instanceof HTMLElement ? current : null;
17
+ }
18
+ function useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp) {
19
+ const returnFocusRef = React.useRef(null);
20
+ const onOpenAutoFocus = React.useCallback(
21
+ (event) => {
22
+ returnFocusRef.current = returnTargetFrom(document.activeElement);
23
+ onOpenAutoFocusProp?.(event);
24
+ },
25
+ [onOpenAutoFocusProp]
26
+ );
27
+ const onCloseAutoFocus = React.useCallback(
28
+ (event) => {
29
+ onCloseAutoFocusProp?.(event);
30
+ const target = returnFocusRef.current;
31
+ returnFocusRef.current = null;
32
+ if (event.defaultPrevented) return;
33
+ queueMicrotask(() => {
34
+ const ownerDocument = target?.ownerDocument ?? document;
35
+ const active = ownerDocument.activeElement;
36
+ if (active && active !== ownerDocument.body) return;
37
+ if (!target?.isConnected) return;
38
+ target.focus({ preventScroll: true });
39
+ });
40
+ },
41
+ [onCloseAutoFocusProp]
42
+ );
43
+ return { onOpenAutoFocus, onCloseAutoFocus };
44
+ }
45
+ export {
46
+ useModalReturnFocus
47
+ };
48
+ //# sourceMappingURL=use-modal-return-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/use-modal-return-focus.ts"],"sourcesContent":["import * as React from \"react\";\n\n/**\n * Phần tử để trả focus về: phần tử đang giữ focus lúc lớp mở — TRỪ khi nó nằm trong một MENU.\n *\n * Mở hộp thoại từ một mục `DropdownMenu` (\"Đổi vai trò…\" trên dòng bảng) là ca phổ biến nhất, và\n * lúc hộp thoại lấy focus thì phần tử đang giữ focus là chính MỤC MENU — thứ bị gỡ cùng menu ngay\n * sau đó. Ghi nó lại là trả về một phần tử đã chết, tức lại rơi xuống `<body>` (đo được trước khi\n * có nhánh này). Người dùng \"đứng\" ở NÚT MỞ menu: Radix nối nó vào nội dung menu bằng\n * `aria-labelledby` (id của nút), và nút trỏ lại bằng `aria-controls` (id của nội dung) — tra cả hai,\n * vì app đặt `id` riêng cho nút mở thì `aria-labelledby` trỏ vào một id không còn tồn tại.\n * Menu con: lặp lên tới menu gốc (nút mở menu con cũng bị gỡ theo).\n */\nfunction returnTargetFrom(active: Element | null): HTMLElement | null {\n let current = active;\n for (let depth = 0; depth < 5; depth++) {\n if (!(current instanceof HTMLElement) || current === current.ownerDocument.body) return null;\n const menu = current.closest<HTMLElement>('[role=\"menu\"]');\n if (!menu) return current;\n\n const ownerDocument = menu.ownerDocument;\n const labelledBy = menu.getAttribute(\"aria-labelledby\");\n const byLabel = labelledBy ? ownerDocument.getElementById(labelledBy) : null;\n const byControls = menu.id ? ownerDocument.querySelector(`[aria-controls=\"${CSS.escape(menu.id)}\"]`) : null;\n const opener = byLabel ?? byControls;\n // Không tìm ra nút mở (menu không phải của Radix): trả về chính phần tử — lúc đóng, nó đã bị gỡ\n // thì gói không đoán, như mọi ca khác.\n if (!opener) return current;\n current = opener;\n }\n return current instanceof HTMLElement ? current : null;\n}\n\n/**\n * TRẢ FOCUS khi một lớp modal của Radix (`Dialog`, `Sheet`) đóng mà nó được mở KHÔNG qua Trigger —\n * nội bộ, không nằm trong barrel nào.\n *\n * Radix chỉ trả focus về `DialogTrigger`: handler `onCloseAutoFocus` của chính nó chặn mặc định của\n * `FocusScope` rồi gọi `triggerRef.current?.focus()` — thiếu Trigger là focus vào `null`. Mà mở bằng\n * `open` từ một mục menu, một dòng bảng, một `ActionBar` hay một phím tắt là cách hộp thoại được dùng\n * nhiều nhất trong app thật: đóng xong (Escape, nút ×, nút Huỷ) là focus rơi xuống `<body>`, Tab kế\n * tiếp chạy lại từ đầu trang (WCAG 2.4.3). Đo trên một trang dựng từ `dist` (3.3.0): mở `Dialog` từ\n * một `Button` thường, Escape bằng phím thật → `document.activeElement` là `<body>`.\n * `ConfirmDialog` đã tự vá đúng lỗi này ở `3.1.0`; đây là cùng khuôn, đặt ở tầng primitive.\n *\n * Luật — ai thắng khi nhiều bên cùng muốn đưa focus đi:\n * 1. App: handler `onCloseAutoFocus` của app chạy TRƯỚC; app gọi `preventDefault()` là app nhận việc\n * (Radix cũng bỏ qua Trigger), gói không làm gì thêm.\n * 2. Radix: handler của nó chạy ngay SAU handler này và trả focus về Trigger nếu có — nên phép kiểm\n * của gói đợi qua một microtask để Radix đi trước.\n * 3. Gói: chỉ khi focus THẬT SỰ đã rơi (`<body>`) mới trả về phần tử giữ focus ngay trước khi lớp\n * mở. Phần tử đó đã bị gỡ khỏi DOM (dòng vừa xoá) thì gói không đoán.\n */\nexport function useModalReturnFocus<E extends Event>(\n onOpenAutoFocusProp: ((event: E) => void) | undefined,\n onCloseAutoFocusProp: ((event: E) => void) | undefined\n) {\n const returnFocusRef = React.useRef<HTMLElement | null>(null);\n\n const onOpenAutoFocus = React.useCallback(\n (event: E) => {\n // Chạy TRƯỚC khi Radix đưa focus vào lớp: phần tử đang giữ focus là nơi người dùng đứng.\n returnFocusRef.current = returnTargetFrom(document.activeElement);\n onOpenAutoFocusProp?.(event);\n },\n [onOpenAutoFocusProp]\n );\n\n const onCloseAutoFocus = React.useCallback(\n (event: E) => {\n onCloseAutoFocusProp?.(event);\n const target = returnFocusRef.current;\n returnFocusRef.current = null;\n if (event.defaultPrevented) return;\n\n queueMicrotask(() => {\n const ownerDocument = target?.ownerDocument ?? document;\n const active = ownerDocument.activeElement;\n if (active && active !== ownerDocument.body) return;\n if (!target?.isConnected) return;\n target.focus({ preventScroll: true });\n });\n },\n [onCloseAutoFocusProp]\n );\n\n return { onOpenAutoFocus, onCloseAutoFocus };\n}\n"],"mappings":"AAAA,YAAY,WAAW;AAavB,SAAS,iBAAiB,QAA4C;AACpE,MAAI,UAAU;AACd,WAAS,QAAQ,GAAG,QAAQ,GAAG,SAAS;AACtC,QAAI,EAAE,mBAAmB,gBAAgB,YAAY,QAAQ,cAAc,KAAM,QAAO;AACxF,UAAM,OAAO,QAAQ,QAAqB,eAAe;AACzD,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,gBAAgB,KAAK;AAC3B,UAAM,aAAa,KAAK,aAAa,iBAAiB;AACtD,UAAM,UAAU,aAAa,cAAc,eAAe,UAAU,IAAI;AACxE,UAAM,aAAa,KAAK,KAAK,cAAc,cAAc,mBAAmB,IAAI,OAAO,KAAK,EAAE,CAAC,IAAI,IAAI;AACvG,UAAM,SAAS,WAAW;AAG1B,QAAI,CAAC,OAAQ,QAAO;AACpB,cAAU;AAAA,EACZ;AACA,SAAO,mBAAmB,cAAc,UAAU;AACpD;AAsBO,SAAS,oBACd,qBACA,sBACA;AACA,QAAM,iBAAiB,MAAM,OAA2B,IAAI;AAE5D,QAAM,kBAAkB,MAAM;AAAA,IAC5B,CAAC,UAAa;AAEZ,qBAAe,UAAU,iBAAiB,SAAS,aAAa;AAChE,4BAAsB,KAAK;AAAA,IAC7B;AAAA,IACA,CAAC,mBAAmB;AAAA,EACtB;AAEA,QAAM,mBAAmB,MAAM;AAAA,IAC7B,CAAC,UAAa;AACZ,6BAAuB,KAAK;AAC5B,YAAM,SAAS,eAAe;AAC9B,qBAAe,UAAU;AACzB,UAAI,MAAM,iBAAkB;AAE5B,qBAAe,MAAM;AACnB,cAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,cAAM,SAAS,cAAc;AAC7B,YAAI,UAAU,WAAW,cAAc,KAAM;AAC7C,YAAI,CAAC,QAAQ,YAAa;AAC1B,eAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,MACtC,CAAC;AAAA,IACH;AAAA,IACA,CAAC,oBAAoB;AAAA,EACvB;AAEA,SAAO,EAAE,iBAAiB,iBAAiB;AAC7C;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",