@comitor/ui 3.3.0 → 3.3.2

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.
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useRef, useState } from "react";
3
+ import { useState } from "react";
4
4
  import { useComitorMessages } from "../../i18n/locale-provider.js";
5
5
  import { cn } from "../../lib/cn.js";
6
6
  import {
@@ -38,7 +38,6 @@ function ConfirmDialog({
38
38
  const pendingLabel = pendingLabelProp ?? t.pendingLabel;
39
39
  const [internalOpen, setInternalOpen] = useState(false);
40
40
  const [pending, setPending] = useState(false);
41
- const returnFocusRef = useRef(null);
42
41
  const isOpen = open ?? internalOpen;
43
42
  const setOpen = (next) => {
44
43
  if (pending && !next) return;
@@ -60,61 +59,41 @@ function ConfirmDialog({
60
59
  };
61
60
  return /* @__PURE__ */ jsxs(AlertDialog, { open: isOpen, onOpenChange: setOpen, children: [
62
61
  trigger && /* @__PURE__ */ jsx(AlertDialogTrigger, { asChild: true, children: trigger }),
63
- /* @__PURE__ */ jsxs(
64
- AlertDialogContent,
65
- {
66
- className,
67
- onOpenAutoFocus: () => {
68
- const active = document.activeElement;
69
- returnFocusRef.current = active instanceof HTMLElement && active !== document.body ? active : null;
70
- },
71
- onCloseAutoFocus: (event) => {
72
- const target = returnFocusRef.current;
73
- returnFocusRef.current = null;
74
- if (trigger) return;
75
- const active = document.activeElement;
76
- if (active && active !== document.body) return;
77
- if (!target?.isConnected) return;
78
- event.preventDefault();
79
- target.focus({ preventScroll: true });
80
- },
81
- children: [
82
- /* @__PURE__ */ jsx(AlertDialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
83
- Icon && /* @__PURE__ */ jsx(
84
- "span",
85
- {
86
- "aria-hidden": "true",
87
- className: cn(
88
- "flex size-9 shrink-0 items-center justify-center rounded-md",
89
- variant === "destructive" ? "bg-destructive/15 text-destructive-ink" : "bg-muted text-muted-foreground"
90
- ),
91
- children: /* @__PURE__ */ jsx(Icon, { className: "size-5" })
92
- }
62
+ /* @__PURE__ */ jsxs(AlertDialogContent, { className, children: [
63
+ /* @__PURE__ */ jsx(AlertDialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
64
+ Icon && /* @__PURE__ */ jsx(
65
+ "span",
66
+ {
67
+ "aria-hidden": "true",
68
+ className: cn(
69
+ "flex size-9 shrink-0 items-center justify-center rounded-md",
70
+ variant === "destructive" ? "bg-destructive/15 text-destructive-ink" : "bg-muted text-muted-foreground"
93
71
  ),
94
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1.5", children: [
95
- /* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
96
- description && /* @__PURE__ */ jsx(AlertDialogDescription, { children: description })
97
- ] })
98
- ] }) }),
99
- children,
100
- /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
101
- /* @__PURE__ */ jsx(AlertDialogCancel, { disabled: pending, children: cancelLabel }),
102
- /* @__PURE__ */ jsx(
103
- AlertDialogAction,
104
- {
105
- disabled: pending || confirmDisabled,
106
- onClick: (event) => {
107
- event.preventDefault();
108
- void handleConfirm();
109
- },
110
- className: cn(variant === "destructive" && buttonVariants({ variant: "destructive" })),
111
- children: pending ? pendingLabel : confirmLabel
112
- }
113
- )
114
- ] })
115
- ]
116
- }
117
- )
72
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-5" })
73
+ }
74
+ ),
75
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1.5", children: [
76
+ /* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
77
+ description && /* @__PURE__ */ jsx(AlertDialogDescription, { children: description })
78
+ ] })
79
+ ] }) }),
80
+ children,
81
+ /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
82
+ /* @__PURE__ */ jsx(AlertDialogCancel, { disabled: pending, children: cancelLabel }),
83
+ /* @__PURE__ */ jsx(
84
+ AlertDialogAction,
85
+ {
86
+ disabled: pending || confirmDisabled,
87
+ onClick: (event) => {
88
+ event.preventDefault();
89
+ void handleConfirm();
90
+ },
91
+ className: cn(variant === "destructive" && buttonVariants({ variant: "destructive" })),
92
+ children: pending ? pendingLabel : confirmLabel
93
+ }
94
+ )
95
+ ] })
96
+ ] })
118
97
  ] });
119
98
  }
120
99
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/confirm-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useRef, useState } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger\n} from \"../ui/alert-dialog.js\";\nimport { buttonVariants } from \"../ui/button.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport interface ConfirmDialogProps {\n /** Controlled — bỏ trống thì dialog tự quản trạng thái mở (dùng kèm `trigger`) */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Phần tử mở dialog (đặt trong `AlertDialogTrigger asChild`) */\n trigger?: ReactNode;\n\n title: ReactNode;\n description?: ReactNode;\n /** Nội dung phụ (danh sách bản ghi sẽ bị ảnh hưởng, ô nhập lý do…) */\n children?: ReactNode;\n icon?: IconComponent;\n\n /*\n * Ba nhãn nút dưới đây: không truyền thì lấy namespace `confirmDialog` của\n * `ComitorLocaleProvider` (mặc định tiếng Anh). Truyền prop là đè riêng chỗ này.\n */\n confirmLabel?: string;\n cancelLabel?: string;\n /**\n * Nhãn nút xác nhận trong lúc `onConfirm` bất đồng bộ đang chạy (khoá `pendingLabel`).\n *\n * Phải đè được chứ không được chôn cứng: tài liệu khuyến khích `onConfirm` trả về Promise, nên\n * đây KHÔNG phải nhánh hiếm — app đặt `confirmLabel=\"Xoá dự án\"` mà quên chuỗi này sẽ thấy nút\n * nhảy sang câu chung chung của bảng đúng vào giây người dùng đang nhìn nó.\n */\n pendingLabel?: string;\n /** \"destructive\" tô nút xác nhận màu đỏ — dùng cho xoá/thu hồi */\n variant?: \"default\" | \"destructive\";\n /**\n * Hành động khi xác nhận. Trả về Promise thì dialog giữ nguyên và bật trạng thái đang chạy\n * cho tới khi Promise xong — đóng ngay rồi mới gọi API là cách chắc chắn nhất để người dùng\n * bấm xoá hai lần.\n */\n onConfirm: () => void | Promise<void>;\n /** Khoá nút xác nhận (vd chưa gõ đúng tên bản ghi cần xoá) */\n confirmDisabled?: boolean;\n /** Nhận lỗi từ `onConfirm` để app hiện toast; không truyền thì lỗi được ghi ra console */\n onError?: (error: unknown) => void;\n className?: string;\n}\n\n/**\n * Hộp thoại xác nhận cho hành động không hoàn tác được (xoá việc, gỡ thành viên, huỷ gói).\n *\n * Bọc `AlertDialog` để mọi app có cùng một cách hỏi lại; nút xác nhận tự vào trạng thái đang\n * chạy (chữ theo `pendingLabel`) khi `onConfirm` bất đồng bộ, và lỗi ném ra sẽ giữ dialog mở để\n * người dùng thử lại.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n trigger,\n title,\n description,\n children,\n icon: Icon,\n confirmLabel: confirmLabelProp,\n cancelLabel: cancelLabelProp,\n pendingLabel: pendingLabelProp,\n variant = \"default\",\n onConfirm,\n confirmDisabled = false,\n onError,\n className\n}: ConfirmDialogProps) {\n const t = useComitorMessages(\"confirmDialog\");\n const confirmLabel = confirmLabelProp ?? t.confirmLabel;\n const cancelLabel = cancelLabelProp ?? t.cancelLabel;\n const pendingLabel = pendingLabelProp ?? t.pendingLabel;\n const [internalOpen, setInternalOpen] = useState(false);\n const [pending, setPending] = useState(false);\n /*\n * ⚠ Trả focus khi đóng (3.1.0). Radix chỉ trả focus về `AlertDialogTrigger` — thiếu trigger thì\n * `onCloseAutoFocus` của nó chặn mặc định rồi focus vào `null`. Mà dialog mở bằng `open` từ một\n * `ActionBar`, một menu hay phím tắt lưới là cách ConfirmDialog được dùng nhiều nhất: đóng xong\n * (Huỷ, Escape, xác nhận) là focus rơi xuống `<body>` (WCAG 2.4.3), dù nút đã mở nó vẫn còn đó.\n * Đo trên comitor-ds: Enter trên \"Refund\" của thanh thao tác, Escape → `document.activeElement`\n * là `<body>`. Nay ghi lại phần tử giữ focus ngay trước khi dialog lấy focus và trả về đó — chỉ\n * khi focus thật sự đã mất: app tự đưa focus đi thì app thắng; phần tử kia đã bị gỡ (dòng vừa\n * xoá, thanh đã đóng theo lựa chọn) thì gói không đoán. Có `trigger` thì để Radix lo như cũ.\n */\n const returnFocusRef = useRef<HTMLElement | null>(null);\n\n const isOpen = open ?? internalOpen;\n const setOpen = (next: boolean) => {\n if (pending && !next) return; // đang chạy thì không cho đóng ngang\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const handleConfirm = async () => {\n try {\n setPending(true);\n await onConfirm();\n setPending(false);\n if (open === undefined) setInternalOpen(false);\n onOpenChange?.(false);\n } catch (error) {\n // Giữ dialog mở để người dùng thấy toast lỗi của app và thử lại.\n // KHÔNG ném tiếp: lời gọi là `void handleConfirm()` trong handler nên ném ra chỉ tạo\n // unhandled rejection mà app không bắt được — lỗi đi ra ngoài qua `onError`.\n setPending(false);\n if (onError) onError(error);\n else console.error(\"[ConfirmDialog] onConfirm thất bại:\", error);\n }\n };\n\n return (\n <AlertDialog open={isOpen} onOpenChange={setOpen}>\n {trigger && <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>}\n <AlertDialogContent\n className={className}\n onOpenAutoFocus={() => {\n // Chạy TRƯỚC khi Radix đưa focus vào dialog: phần tử đang focus vẫn là nơi người dùng đứng.\n const active = document.activeElement;\n returnFocusRef.current = active instanceof HTMLElement && active !== document.body ? active : null;\n }}\n onCloseAutoFocus={(event) => {\n const target = returnFocusRef.current;\n returnFocusRef.current = null;\n if (trigger) return;\n const active = document.activeElement;\n if (active && active !== document.body) return;\n if (!target?.isConnected) return;\n event.preventDefault();\n target.focus({ preventScroll: true });\n }}\n >\n <AlertDialogHeader>\n <div className=\"flex items-start gap-3\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n // Cùng khuôn với `statCardIconVariants`: nền tint `--x` + icon bậc `--x-ink`.\n // `text-destructive` trên `bg-destructive/15` chỉ 4,06:1 (light) / 3,14:1 (dark) —\n // qua ngưỡng 3:1 của icon nhưng hết biên an toàn. Ink: 5,76:1 / 5,71:1 ✓.\n className={cn(\n \"flex size-9 shrink-0 items-center justify-center rounded-md\",\n variant === \"destructive\"\n ? \"bg-destructive/15 text-destructive-ink\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <Icon className=\"size-5\" />\n </span>\n )}\n <div className=\"min-w-0 space-y-1.5\">\n <AlertDialogTitle>{title}</AlertDialogTitle>\n {description && <AlertDialogDescription>{description}</AlertDialogDescription>}\n </div>\n </div>\n </AlertDialogHeader>\n\n {children}\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={pending}>{cancelLabel}</AlertDialogCancel>\n <AlertDialogAction\n disabled={pending || confirmDisabled}\n onClick={(event) => {\n // Chặn hành vi tự đóng của Radix: dialog chỉ đóng sau khi onConfirm chạy xong.\n event.preventDefault();\n void handleConfirm();\n }}\n className={cn(variant === \"destructive\" && buttonVariants({ variant: \"destructive\" }))}\n >\n {pending ? pendingLabel : confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/confirm-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useState } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger\n} from \"../ui/alert-dialog.js\";\nimport { buttonVariants } from \"../ui/button.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport interface ConfirmDialogProps {\n /** Controlled — bỏ trống thì dialog tự quản trạng thái mở (dùng kèm `trigger`) */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Phần tử mở dialog (đặt trong `AlertDialogTrigger asChild`) */\n trigger?: ReactNode;\n\n title: ReactNode;\n description?: ReactNode;\n /** Nội dung phụ (danh sách bản ghi sẽ bị ảnh hưởng, ô nhập lý do…) */\n children?: ReactNode;\n icon?: IconComponent;\n\n /*\n * Ba nhãn nút dưới đây: không truyền thì lấy namespace `confirmDialog` của\n * `ComitorLocaleProvider` (mặc định tiếng Anh). Truyền prop là đè riêng chỗ này.\n */\n confirmLabel?: string;\n cancelLabel?: string;\n /**\n * Nhãn nút xác nhận trong lúc `onConfirm` bất đồng bộ đang chạy (khoá `pendingLabel`).\n *\n * Phải đè được chứ không được chôn cứng: tài liệu khuyến khích `onConfirm` trả về Promise, nên\n * đây KHÔNG phải nhánh hiếm — app đặt `confirmLabel=\"Xoá dự án\"` mà quên chuỗi này sẽ thấy nút\n * nhảy sang câu chung chung của bảng đúng vào giây người dùng đang nhìn nó.\n */\n pendingLabel?: string;\n /** \"destructive\" tô nút xác nhận màu đỏ — dùng cho xoá/thu hồi */\n variant?: \"default\" | \"destructive\";\n /**\n * Hành động khi xác nhận. Trả về Promise thì dialog giữ nguyên và bật trạng thái đang chạy\n * cho tới khi Promise xong — đóng ngay rồi mới gọi API là cách chắc chắn nhất để người dùng\n * bấm xoá hai lần.\n */\n onConfirm: () => void | Promise<void>;\n /** Khoá nút xác nhận (vd chưa gõ đúng tên bản ghi cần xoá) */\n confirmDisabled?: boolean;\n /** Nhận lỗi từ `onConfirm` để app hiện toast; không truyền thì lỗi được ghi ra console */\n onError?: (error: unknown) => void;\n className?: string;\n}\n\n/**\n * Hộp thoại xác nhận cho hành động không hoàn tác được (xoá việc, gỡ thành viên, huỷ gói).\n *\n * Bọc `AlertDialog` để mọi app có cùng một cách hỏi lại; nút xác nhận tự vào trạng thái đang\n * chạy (chữ theo `pendingLabel`) khi `onConfirm` bất đồng bộ, và lỗi ném ra sẽ giữ dialog mở để\n * người dùng thử lại.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n trigger,\n title,\n description,\n children,\n icon: Icon,\n confirmLabel: confirmLabelProp,\n cancelLabel: cancelLabelProp,\n pendingLabel: pendingLabelProp,\n variant = \"default\",\n onConfirm,\n confirmDisabled = false,\n onError,\n className\n}: ConfirmDialogProps) {\n const t = useComitorMessages(\"confirmDialog\");\n const confirmLabel = confirmLabelProp ?? t.confirmLabel;\n const cancelLabel = cancelLabelProp ?? t.cancelLabel;\n const pendingLabel = pendingLabelProp ?? t.pendingLabel;\n const [internalOpen, setInternalOpen] = useState(false);\n const [pending, setPending] = useState(false);\n /*\n * ⚠ Trả focus khi đóng mà không có `trigger` — mở bằng `open` từ `ActionBar`, menu, phím tắt lưới,\n * tức cách ConfirmDialog được dùng nhiều nhất (đo trên comitor-ds, 3.1.0: Enter trên \"Refund\" của\n * thanh thao tác, Escape → `<body>`). Từ 3.3.1 việc này nằm ở `AlertDialogContent` (primitive),\n * không còn một bản riêng ở đây: bản riêng của 3.1.0 ghi nút mở ở `onOpenAutoFocus` nên hụt hai ca\n * — mở từ mục MENU (ghi phải mục menu, thứ bị gỡ cùng menu) và `children` có ô `autoFocus` (Radix\n * không gọi `onOpenAutoFocus`). Có `trigger` thì Radix lo như cũ.\n */\n const isOpen = open ?? internalOpen;\n const setOpen = (next: boolean) => {\n if (pending && !next) return; // đang chạy thì không cho đóng ngang\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const handleConfirm = async () => {\n try {\n setPending(true);\n await onConfirm();\n setPending(false);\n if (open === undefined) setInternalOpen(false);\n onOpenChange?.(false);\n } catch (error) {\n // Giữ dialog mở để người dùng thấy toast lỗi của app và thử lại.\n // KHÔNG ném tiếp: lời gọi là `void handleConfirm()` trong handler nên ném ra chỉ tạo\n // unhandled rejection mà app không bắt được — lỗi đi ra ngoài qua `onError`.\n setPending(false);\n if (onError) onError(error);\n else console.error(\"[ConfirmDialog] onConfirm thất bại:\", error);\n }\n };\n\n return (\n <AlertDialog open={isOpen} onOpenChange={setOpen}>\n {trigger && <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>}\n <AlertDialogContent className={className}>\n <AlertDialogHeader>\n <div className=\"flex items-start gap-3\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n // Cùng khuôn với `statCardIconVariants`: nền tint `--x` + icon bậc `--x-ink`.\n // `text-destructive` trên `bg-destructive/15` chỉ 4,06:1 (light) / 3,14:1 (dark) —\n // qua ngưỡng 3:1 của icon nhưng hết biên an toàn. Ink: 5,76:1 / 5,71:1 ✓.\n className={cn(\n \"flex size-9 shrink-0 items-center justify-center rounded-md\",\n variant === \"destructive\"\n ? \"bg-destructive/15 text-destructive-ink\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <Icon className=\"size-5\" />\n </span>\n )}\n <div className=\"min-w-0 space-y-1.5\">\n <AlertDialogTitle>{title}</AlertDialogTitle>\n {description && <AlertDialogDescription>{description}</AlertDialogDescription>}\n </div>\n </div>\n </AlertDialogHeader>\n\n {children}\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={pending}>{cancelLabel}</AlertDialogCancel>\n <AlertDialogAction\n disabled={pending || confirmDisabled}\n onClick={(event) => {\n // Chặn hành vi tự đóng của Radix: dialog chỉ đóng sau khi onConfirm chạy xong.\n event.preventDefault();\n void handleConfirm();\n }}\n className={cn(variant === \"destructive\" && buttonVariants({ variant: \"destructive\" }))}\n >\n {pending ? pendingLabel : confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":";AA6HkB,cAoBN,YApBM;AA3HlB,SAAyB,gBAAgB;AACzC,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAoDxB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA,EACV;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,IAAI,mBAAmB,eAAe;AAC5C,QAAM,eAAe,oBAAoB,EAAE;AAC3C,QAAM,cAAc,mBAAmB,EAAE;AACzC,QAAM,eAAe,oBAAoB,EAAE;AAC3C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAS5C,QAAM,SAAS,QAAQ;AACvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,WAAW,CAAC,KAAM;AACtB,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,gBAAgB,YAAY;AAChC,QAAI;AACF,iBAAW,IAAI;AACf,YAAM,UAAU;AAChB,iBAAW,KAAK;AAChB,UAAI,SAAS,OAAW,iBAAgB,KAAK;AAC7C,qBAAe,KAAK;AAAA,IACtB,SAAS,OAAO;AAId,iBAAW,KAAK;AAChB,UAAI,QAAS,SAAQ,KAAK;AAAA,UACrB,SAAQ,MAAM,iDAAuC,KAAK;AAAA,IACjE;AAAA,EACF;AAEA,SACE,qBAAC,eAAY,MAAM,QAAQ,cAAc,SACtC;AAAA,eAAW,oBAAC,sBAAmB,SAAO,MAAE,mBAAQ;AAAA,IACjD,qBAAC,sBAAmB,WAClB;AAAA,0BAAC,qBACC,+BAAC,SAAI,WAAU,0BACZ;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YAIZ,WAAW;AAAA,cACT;AAAA,cACA,YAAY,gBACR,2CACA;AAAA,YACN;AAAA,YAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,QAC3B;AAAA,QAEF,qBAAC,SAAI,WAAU,uBACb;AAAA,8BAAC,oBAAkB,iBAAM;AAAA,UACxB,eAAe,oBAAC,0BAAwB,uBAAY;AAAA,WACvD;AAAA,SACF,GACF;AAAA,MAEC;AAAA,MAED,qBAAC,qBACC;AAAA,4BAAC,qBAAkB,UAAU,SAAU,uBAAY;AAAA,QACnD;AAAA,UAAC;AAAA;AAAA,YACC,UAAU,WAAW;AAAA,YACrB,SAAS,CAAC,UAAU;AAElB,oBAAM,eAAe;AACrB,mBAAK,cAAc;AAAA,YACrB;AAAA,YACA,WAAW,GAAG,YAAY,iBAAiB,eAAe,EAAE,SAAS,cAAc,CAAC,CAAC;AAAA,YAEpF,oBAAU,eAAe;AAAA;AAAA,QAC5B;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
@@ -17,7 +17,7 @@ declare function AlertDialogPortal({ ...props }: AlertDialogPortalProps): React.
17
17
  type AlertDialogOverlayProps = React.ComponentProps<typeof AlertDialogPrimitive.Overlay>;
18
18
  declare function AlertDialogOverlay({ className, ...props }: AlertDialogOverlayProps): React.JSX.Element;
19
19
  type AlertDialogContentProps = React.ComponentProps<typeof AlertDialogPrimitive.Content>;
20
- declare function AlertDialogContent({ className, ...props }: AlertDialogContentProps): React.JSX.Element;
20
+ declare function AlertDialogContent({ className, children, onOpenAutoFocus: onOpenAutoFocusProp, onCloseAutoFocus: onCloseAutoFocusProp, ...props }: AlertDialogContentProps): React.JSX.Element;
21
21
  type AlertDialogHeaderProps = React.ComponentProps<"div">;
22
22
  declare function AlertDialogHeader({ className, ...props }: AlertDialogHeaderProps): React.JSX.Element;
23
23
  type AlertDialogFooterProps = React.ComponentProps<"div">;
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
4
4
  import { cn } from "../../lib/cn.js";
5
+ import { useModalReturnFocus } from "../../lib/use-modal-return-focus.js";
5
6
  import { buttonVariants } from "./button.js";
6
7
  function AlertDialog({ ...props }) {
7
8
  return /* @__PURE__ */ jsx(AlertDialogPrimitive.Root, { "data-slot": "alert-dialog", ...props });
@@ -25,10 +26,17 @@ function AlertDialogOverlay({ className, ...props }) {
25
26
  }
26
27
  );
27
28
  }
28
- function AlertDialogContent({ className, ...props }) {
29
+ function AlertDialogContent({
30
+ className,
31
+ children,
32
+ onOpenAutoFocus: onOpenAutoFocusProp,
33
+ onCloseAutoFocus: onCloseAutoFocusProp,
34
+ ...props
35
+ }) {
36
+ const returnFocus = useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp);
29
37
  return /* @__PURE__ */ jsxs(AlertDialogPortal, { children: [
30
38
  /* @__PURE__ */ jsx(AlertDialogOverlay, {}),
31
- /* @__PURE__ */ jsx(
39
+ /* @__PURE__ */ jsxs(
32
40
  AlertDialogPrimitive.Content,
33
41
  {
34
42
  "data-slot": "alert-dialog-content",
@@ -36,7 +44,13 @@ function AlertDialogContent({ className, ...props }) {
36
44
  "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 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border border-border p-6 shadow-overlay duration-200 sm:max-w-lg",
37
45
  className
38
46
  ),
39
- ...props
47
+ ...props,
48
+ onOpenAutoFocus: returnFocus.onOpenAutoFocus,
49
+ onCloseAutoFocus: returnFocus.onCloseAutoFocus,
50
+ children: [
51
+ props.forceMount ? null : returnFocus.recorder,
52
+ children
53
+ ]
40
54
  }
41
55
  )
42
56
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/alert-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { buttonVariants } from \"./button.js\";\n\n/**\n * AlertDialog — hộp thoại xác nhận (Radix AlertDialog): chặn thao tác, buộc chọn\n * Hủy/Đồng ý, không đóng được bằng click ra ngoài.\n *\n * Nút Action/Cancel mượn `buttonVariants` của Button để đồng bộ thị giác.\n * Composite `ConfirmDialog` (Tier 2) sẽ bọc component này.\n */\n\nexport type AlertDialogProps = React.ComponentProps<typeof AlertDialogPrimitive.Root>;\n\nfunction AlertDialog({ ...props }: AlertDialogProps) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nexport type AlertDialogTriggerProps = React.ComponentProps<typeof AlertDialogPrimitive.Trigger>;\n\nfunction AlertDialogTrigger({ ...props }: AlertDialogTriggerProps) {\n return <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />;\n}\n\nexport type AlertDialogPortalProps = React.ComponentProps<typeof AlertDialogPrimitive.Portal>;\n\nfunction AlertDialogPortal({ ...props }: AlertDialogPortalProps) {\n return <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />;\n}\n\nexport type AlertDialogOverlayProps = React.ComponentProps<typeof AlertDialogPrimitive.Overlay>;\n\nfunction AlertDialogOverlay({ className, ...props }: AlertDialogOverlayProps) {\n return (\n <AlertDialogPrimitive.Overlay\n data-slot=\"alert-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\nexport type AlertDialogContentProps = React.ComponentProps<typeof AlertDialogPrimitive.Content>;\n\nfunction AlertDialogContent({ className, ...props }: AlertDialogContentProps) {\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n data-slot=\"alert-dialog-content\"\n className={cn(\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 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border border-border p-6 shadow-overlay duration-200 sm:max-w-lg\",\n className\n )}\n {...props}\n />\n </AlertDialogPortal>\n );\n}\n\nexport type AlertDialogHeaderProps = React.ComponentProps<\"div\">;\n\nfunction AlertDialogHeader({ className, ...props }: AlertDialogHeaderProps) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nexport type AlertDialogFooterProps = React.ComponentProps<\"div\">;\n\nfunction AlertDialogFooter({ className, ...props }: AlertDialogFooterProps) {\n return (\n <div\n data-slot=\"alert-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 AlertDialogTitleProps = React.ComponentProps<typeof AlertDialogPrimitive.Title>;\n\nfunction AlertDialogTitle({ className, ...props }: AlertDialogTitleProps) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\"text-lg font-semibold text-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport type AlertDialogDescriptionProps = React.ComponentProps<typeof AlertDialogPrimitive.Description>;\n\nfunction AlertDialogDescription({ className, ...props }: AlertDialogDescriptionProps) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport type AlertDialogActionProps = React.ComponentProps<typeof AlertDialogPrimitive.Action>;\n\nfunction AlertDialogAction({ className, ...props }: AlertDialogActionProps) {\n return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;\n}\n\nexport type AlertDialogCancelProps = React.ComponentProps<typeof AlertDialogPrimitive.Cancel>;\n\nfunction AlertDialogCancel({ className, ...props }: AlertDialogCancelProps) {\n return <AlertDialogPrimitive.Cancel className={cn(buttonVariants({ variant: \"outline\" }), className)} {...props} />;\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger\n};\n"],"mappings":";AAkBS,cAkCL,YAlCK;AAhBT,YAAY,0BAA0B;AAEtC,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAY/B,SAAS,YAAY,EAAE,GAAG,MAAM,GAAqB;AACnD,SAAO,oBAAC,qBAAqB,MAArB,EAA0B,aAAU,gBAAgB,GAAG,OAAO;AACxE;AAIA,SAAS,mBAAmB,EAAE,GAAG,MAAM,GAA4B;AACjE,SAAO,oBAAC,qBAAqB,SAArB,EAA6B,aAAU,wBAAwB,GAAG,OAAO;AACnF;AAIA,SAAS,kBAAkB,EAAE,GAAG,MAAM,GAA2B;AAC/D,SAAO,oBAAC,qBAAqB,QAArB,EAA4B,aAAU,uBAAuB,GAAG,OAAO;AACjF;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE,qBAAC,qBACC;AAAA,wBAAC,sBAAmB;AAAA,IACpB;AAAA,MAAC,qBAAqB;AAAA,MAArB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,gDAAgD,SAAS;AAAA,MACtE,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,0DAA0D,SAAS;AAAA,MAChF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yCAAyC,SAAS;AAAA,MAC/D,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,uBAAuB,EAAE,WAAW,GAAG,MAAM,GAAgC;AACpF,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SAAO,oBAAC,qBAAqB,QAArB,EAA4B,WAAW,GAAG,eAAe,GAAG,SAAS,GAAI,GAAG,OAAO;AAC7F;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SAAO,oBAAC,qBAAqB,QAArB,EAA4B,WAAW,GAAG,eAAe,EAAE,SAAS,UAAU,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACnH;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/alert-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useModalReturnFocus } from \"../../lib/use-modal-return-focus.js\";\nimport { buttonVariants } from \"./button.js\";\n\n/**\n * AlertDialog — hộp thoại xác nhận (Radix AlertDialog): chặn thao tác, buộc chọn\n * Hủy/Đồng ý, không đóng được bằng click ra ngoài.\n *\n * Nút Action/Cancel mượn `buttonVariants` của Button để đồng bộ thị giác.\n * Composite `ConfirmDialog` (Tier 2) sẽ bọc component này.\n */\n\nexport type AlertDialogProps = React.ComponentProps<typeof AlertDialogPrimitive.Root>;\n\nfunction AlertDialog({ ...props }: AlertDialogProps) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nexport type AlertDialogTriggerProps = React.ComponentProps<typeof AlertDialogPrimitive.Trigger>;\n\nfunction AlertDialogTrigger({ ...props }: AlertDialogTriggerProps) {\n return <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />;\n}\n\nexport type AlertDialogPortalProps = React.ComponentProps<typeof AlertDialogPrimitive.Portal>;\n\nfunction AlertDialogPortal({ ...props }: AlertDialogPortalProps) {\n return <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />;\n}\n\nexport type AlertDialogOverlayProps = React.ComponentProps<typeof AlertDialogPrimitive.Overlay>;\n\nfunction AlertDialogOverlay({ className, ...props }: AlertDialogOverlayProps) {\n return (\n <AlertDialogPrimitive.Overlay\n data-slot=\"alert-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\nexport type AlertDialogContentProps = React.ComponentProps<typeof AlertDialogPrimitive.Content>;\n\nfunction AlertDialogContent({\n className,\n children,\n onOpenAutoFocus: onOpenAutoFocusProp,\n onCloseAutoFocus: onCloseAutoFocusProp,\n ...props\n}: AlertDialogContentProps) {\n /*\n * Trả focus khi đóng mà không có `AlertDialogTrigger` (3.3.1) — cùng lỗi, cùng khuôn với\n * `DialogContent` (xem `lib/use-modal-return-focus.ts`). Trước bản này chỉ `ConfirmDialog` tự vá\n * (3.1.0), và bản vá đó ghi nút mở ở `onOpenAutoFocus` nên hụt hai ca: mở từ mục MENU (ghi phải mục\n * menu — đã bị gỡ) và con có `autoFocus` (Radix không gọi `onOpenAutoFocus`). Nay nằm ở primitive,\n * và `ConfirmDialog` thừa hưởng. Radix vẫn tự đưa focus vào nút Huỷ SAU handler của gói.\n */\n const returnFocus = useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp);\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n data-slot=\"alert-dialog-content\"\n className={cn(\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 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border border-border p-6 shadow-overlay duration-200 sm:max-w-lg\",\n className\n )}\n {...props}\n onOpenAutoFocus={returnFocus.onOpenAutoFocus}\n onCloseAutoFocus={returnFocus.onCloseAutoFocus}\n >\n {props.forceMount ? null : returnFocus.recorder}\n {children}\n </AlertDialogPrimitive.Content>\n </AlertDialogPortal>\n );\n}\n\nexport type AlertDialogHeaderProps = React.ComponentProps<\"div\">;\n\nfunction AlertDialogHeader({ className, ...props }: AlertDialogHeaderProps) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nexport type AlertDialogFooterProps = React.ComponentProps<\"div\">;\n\nfunction AlertDialogFooter({ className, ...props }: AlertDialogFooterProps) {\n return (\n <div\n data-slot=\"alert-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 AlertDialogTitleProps = React.ComponentProps<typeof AlertDialogPrimitive.Title>;\n\nfunction AlertDialogTitle({ className, ...props }: AlertDialogTitleProps) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\"text-lg font-semibold text-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport type AlertDialogDescriptionProps = React.ComponentProps<typeof AlertDialogPrimitive.Description>;\n\nfunction AlertDialogDescription({ className, ...props }: AlertDialogDescriptionProps) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport type AlertDialogActionProps = React.ComponentProps<typeof AlertDialogPrimitive.Action>;\n\nfunction AlertDialogAction({ className, ...props }: AlertDialogActionProps) {\n return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;\n}\n\nexport type AlertDialogCancelProps = React.ComponentProps<typeof AlertDialogPrimitive.Cancel>;\n\nfunction AlertDialogCancel({ className, ...props }: AlertDialogCancelProps) {\n return <AlertDialogPrimitive.Cancel className={cn(buttonVariants({ variant: \"outline\" }), className)} {...props} />;\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger\n};\n"],"mappings":";AAmBS,cAkDH,YAlDG;AAjBT,YAAY,0BAA0B;AAEtC,SAAS,UAAU;AACnB,SAAS,2BAA2B;AACpC,SAAS,sBAAsB;AAY/B,SAAS,YAAY,EAAE,GAAG,MAAM,GAAqB;AACnD,SAAO,oBAAC,qBAAqB,MAArB,EAA0B,aAAU,gBAAgB,GAAG,OAAO;AACxE;AAIA,SAAS,mBAAmB,EAAE,GAAG,MAAM,GAA4B;AACjE,SAAO,oBAAC,qBAAqB,SAArB,EAA6B,aAAU,wBAAwB,GAAG,OAAO;AACnF;AAIA,SAAS,kBAAkB,EAAE,GAAG,MAAM,GAA2B;AAC/D,SAAO,oBAAC,qBAAqB,QAArB,EAA4B,aAAU,uBAAuB,GAAG,OAAO;AACjF;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,GAAG;AACL,GAA4B;AAQ1B,QAAM,cAAc,oBAAoB,qBAAqB,oBAAoB;AACjF,SACE,qBAAC,qBACC;AAAA,wBAAC,sBAAmB;AAAA,IACpB;AAAA,MAAC,qBAAqB;AAAA,MAArB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QACJ,iBAAiB,YAAY;AAAA,QAC7B,kBAAkB,YAAY;AAAA,QAE7B;AAAA,gBAAM,aAAa,OAAO,YAAY;AAAA,UACtC;AAAA;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,gDAAgD,SAAS;AAAA,MACtE,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,0DAA0D,SAAS;AAAA,MAChF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yCAAyC,SAAS;AAAA,MAC/D,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,uBAAuB,EAAE,WAAW,GAAG,MAAM,GAAgC;AACpF,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SAAO,oBAAC,qBAAqB,QAArB,EAA4B,WAAW,GAAG,eAAe,GAAG,SAAS,GAAI,GAAG,OAAO;AAC7F;AAIA,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA2B;AAC1E,SAAO,oBAAC,qBAAqB,QAArB,EAA4B,WAAW,GAAG,eAAe,EAAE,SAAS,UAAU,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACnH;","names":[]}
@@ -80,6 +80,7 @@ function DialogContent({
80
80
  onOpenAutoFocus: returnFocus.onOpenAutoFocus,
81
81
  onCloseAutoFocus: returnFocus.onCloseAutoFocus,
82
82
  children: [
83
+ props.forceMount ? null : returnFocus.recorder,
83
84
  children,
84
85
  showCloseButton && /* @__PURE__ */ jsxs(
85
86
  DialogPrimitive.Close,
@@ -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\";\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":[]}
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`. `returnFocus.recorder` (3.3.1) phải\n * là con ĐẦU TIÊN của nội dung: nó ghi nút mở trước khi một ô `autoFocus` kịp lấy focus.\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 {/* `forceMount` mount MỘT lần chứ không mount lại khi mở — ghi lúc đó là ghi sai lúc. */}\n {props.forceMount ? null : returnFocus.recorder}\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":[]}
@@ -65,6 +65,7 @@ function SheetContent({
65
65
  onOpenAutoFocus: returnFocus.onOpenAutoFocus,
66
66
  onCloseAutoFocus: returnFocus.onCloseAutoFocus,
67
67
  children: [
68
+ props.forceMount ? null : returnFocus.recorder,
68
69
  children,
69
70
  /* @__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: [
70
71
  /* @__PURE__ */ jsx(XIcon, { className: "size-4" }),
@@ -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\";\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":[]}
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 // kể cả `recorder` (3.3.1) đứng làm con đầu tiên của nội dung.\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 {props.forceMount ? null : returnFocus.recorder}\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,cAuFD,YAvFC;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;AAGvC,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,gBAAM,aAAa,OAAO,YAAY;AAAA,UACtC;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":[]}
@@ -34,8 +34,8 @@ function DataGridRowHeightMenu({
34
34
  {
35
35
  "aria-label": msg.rowHeight.label,
36
36
  size: "sm",
37
- className: cn("w-auto gap-2 font-normal", triggerClassName),
38
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: msg.rowHeight.label, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
37
+ className: cn("w-auto gap-1.5 text-xs font-normal", triggerClassName),
38
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder: msg.rowHeight.label, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
39
39
  /* @__PURE__ */ jsx(selectedRowHeight.icon, { "aria-hidden": "true", className: "size-3.5 text-muted-foreground" }),
40
40
  selectedRowHeight.label
41
41
  ] }) })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-grid/data-grid-row-height-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { AlignVerticalSpaceAroundIcon, ChevronsDownUpIcon, EqualIcon, MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from \"../components/ui/select.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useResolvedDataGridMessages } from \"./data-grid-locale.js\";\nimport type { DataGridLocale, DataGridMessages } from \"./messages.js\";\n\n/*\n * Bốn mức chiều cao. PHẢI dựng theo bảng chuỗi chứ không đóng băng thành hằng ở đầu module —\n * nhãn (\"Short\" / \"Thấp\") đổi theo ngôn ngữ, còn `value` và icon thì không.\n */\nfunction buildRowHeights(msg: DataGridMessages) {\n // Tuple (không phải mảng) để `rowHeights[0]` có kiểu chắc chắn — bốn mức là cố định.\n return [\n { label: msg.rowHeight.short, value: \"short\" as const, icon: MinusIcon },\n { label: msg.rowHeight.medium, value: \"medium\" as const, icon: EqualIcon },\n { label: msg.rowHeight.tall, value: \"tall\" as const, icon: AlignVerticalSpaceAroundIcon },\n { label: msg.rowHeight.extraTall, value: \"extra-tall\" as const, icon: ChevronsDownUpIcon }\n ] as const;\n}\n\ninterface DataGridRowHeightMenuProps<TData> extends React.ComponentProps<typeof SelectContent> {\n table: Table<TData>;\n disabled?: boolean;\n /** className cho nút mở menu (mặc định cao 8 như các nút khác trên thanh công cụ) */\n triggerClassName?: string;\n /**\n * Ngôn ngữ của menu: `\"en\"` · `\"vi\"`, hoặc cả một bảng chuỗi tự soạn.\n *\n * ⚠ Thanh công cụ nằm NGOÀI `<DataGrid>`, nên `locale` truyền cho lưới không với tới đây. Bỏ\n * trống thì menu theo `DataGridLocaleProvider`, rồi ngôn ngữ của `ComitorLocaleProvider`, rồi\n * `\"en\"` — app đã đặt `ComitorLocaleProvider` ở root thì không cần làm gì. Prop thắng provider.\n */\n locale?: DataGridLocale | DataGridMessages;\n}\n\n/** Chọn chiều cao dòng của lưới (thấp/vừa/cao/rất cao) — đặt trên thanh công cụ. */\nexport function DataGridRowHeightMenu<TData>({\n table,\n disabled,\n triggerClassName,\n locale,\n ...props\n}: DataGridRowHeightMenuProps<TData>) {\n const msg = useResolvedDataGridMessages(locale);\n const rowHeights = React.useMemo(() => buildRowHeights(msg), [msg]);\n const rowHeight = table.options.meta?.rowHeight;\n const onRowHeightChange = table.options.meta?.onRowHeightChange;\n\n const selectedRowHeight = React.useMemo(\n () => rowHeights.find((option) => option.value === rowHeight) ?? rowHeights[0],\n [rowHeight, rowHeights]\n );\n\n return (\n <Select value={rowHeight} onValueChange={onRowHeightChange} disabled={disabled}>\n <SelectTrigger\n aria-label={msg.rowHeight.label}\n size=\"sm\"\n className={cn(\"w-auto gap-2 font-normal\", triggerClassName)}\n >\n <SelectValue placeholder={msg.rowHeight.label}>\n <span className=\"flex items-center gap-2\">\n <selectedRowHeight.icon aria-hidden=\"true\" className=\"size-3.5 text-muted-foreground\" />\n {selectedRowHeight.label}\n </span>\n </SelectValue>\n </SelectTrigger>\n <SelectContent {...props}>\n <SelectGroup>\n {rowHeights.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n <span className=\"flex items-center gap-2\">\n <option.icon aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n {option.label}\n </span>\n </SelectItem>\n ))}\n </SelectGroup>\n </SelectContent>\n </Select>\n );\n}\n"],"mappings":";AAiEU,SACE,KADF;AA9DV,SAAS,8BAA8B,oBAAoB,WAAW,iBAAiB;AACvF,YAAY,WAAW;AACvB,SAAS,QAAQ,eAAe,aAAa,YAAY,eAAe,mBAAmB;AAC3F,SAAS,UAAU;AACnB,SAAS,mCAAmC;AAO5C,SAAS,gBAAgB,KAAuB;AAE9C,SAAO;AAAA,IACL,EAAE,OAAO,IAAI,UAAU,OAAO,OAAO,SAAkB,MAAM,UAAU;AAAA,IACvE,EAAE,OAAO,IAAI,UAAU,QAAQ,OAAO,UAAmB,MAAM,UAAU;AAAA,IACzE,EAAE,OAAO,IAAI,UAAU,MAAM,OAAO,QAAiB,MAAM,6BAA6B;AAAA,IACxF,EAAE,OAAO,IAAI,UAAU,WAAW,OAAO,cAAuB,MAAM,mBAAmB;AAAA,EAC3F;AACF;AAkBO,SAAS,sBAA6B;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsC;AACpC,QAAM,MAAM,4BAA4B,MAAM;AAC9C,QAAM,aAAa,MAAM,QAAQ,MAAM,gBAAgB,GAAG,GAAG,CAAC,GAAG,CAAC;AAClE,QAAM,YAAY,MAAM,QAAQ,MAAM;AACtC,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAE9C,QAAM,oBAAoB,MAAM;AAAA,IAC9B,MAAM,WAAW,KAAK,CAAC,WAAW,OAAO,UAAU,SAAS,KAAK,WAAW,CAAC;AAAA,IAC7E,CAAC,WAAW,UAAU;AAAA,EACxB;AAEA,SACE,qBAAC,UAAO,OAAO,WAAW,eAAe,mBAAmB,UAC1D;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY,IAAI,UAAU;AAAA,QAC1B,MAAK;AAAA,QACL,WAAW,GAAG,4BAA4B,gBAAgB;AAAA,QAE1D,8BAAC,eAAY,aAAa,IAAI,UAAU,OACtC,+BAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,kBAAkB,MAAlB,EAAuB,eAAY,QAAO,WAAU,kCAAiC;AAAA,UACrF,kBAAkB;AAAA,WACrB,GACF;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,iBAAe,GAAG,OACjB,8BAAC,eACE,qBAAW,IAAI,CAAC,WACf,oBAAC,cAA8B,OAAO,OAAO,OAC3C,+BAAC,UAAK,WAAU,2BACd;AAAA,0BAAC,OAAO,MAAP,EAAY,eAAY,QAAO,WAAU,gCAA+B;AAAA,MACxE,OAAO;AAAA,OACV,KAJe,OAAO,KAKxB,CACD,GACH,GACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/data-grid/data-grid-row-height-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { AlignVerticalSpaceAroundIcon, ChevronsDownUpIcon, EqualIcon, MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from \"../components/ui/select.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useResolvedDataGridMessages } from \"./data-grid-locale.js\";\nimport type { DataGridLocale, DataGridMessages } from \"./messages.js\";\n\n/*\n * Bốn mức chiều cao. PHẢI dựng theo bảng chuỗi chứ không đóng băng thành hằng ở đầu module —\n * nhãn (\"Short\" / \"Thấp\") đổi theo ngôn ngữ, còn `value` và icon thì không.\n */\nfunction buildRowHeights(msg: DataGridMessages) {\n // Tuple (không phải mảng) để `rowHeights[0]` có kiểu chắc chắn — bốn mức là cố định.\n return [\n { label: msg.rowHeight.short, value: \"short\" as const, icon: MinusIcon },\n { label: msg.rowHeight.medium, value: \"medium\" as const, icon: EqualIcon },\n { label: msg.rowHeight.tall, value: \"tall\" as const, icon: AlignVerticalSpaceAroundIcon },\n { label: msg.rowHeight.extraTall, value: \"extra-tall\" as const, icon: ChevronsDownUpIcon }\n ] as const;\n}\n\ninterface DataGridRowHeightMenuProps<TData> extends React.ComponentProps<typeof SelectContent> {\n table: Table<TData>;\n disabled?: boolean;\n /** className cho nút mở menu (mặc định cao 8 như các nút khác trên thanh công cụ) */\n triggerClassName?: string;\n /**\n * Ngôn ngữ của menu: `\"en\"` · `\"vi\"`, hoặc cả một bảng chuỗi tự soạn.\n *\n * ⚠ Thanh công cụ nằm NGOÀI `<DataGrid>`, nên `locale` truyền cho lưới không với tới đây. Bỏ\n * trống thì menu theo `DataGridLocaleProvider`, rồi ngôn ngữ của `ComitorLocaleProvider`, rồi\n * `\"en\"` — app đã đặt `ComitorLocaleProvider` ở root thì không cần làm gì. Prop thắng provider.\n */\n locale?: DataGridLocale | DataGridMessages;\n}\n\n/** Chọn chiều cao dòng của lưới (thấp/vừa/cao/rất cao) — đặt trên thanh công cụ. */\nexport function DataGridRowHeightMenu<TData>({\n table,\n disabled,\n triggerClassName,\n locale,\n ...props\n}: DataGridRowHeightMenuProps<TData>) {\n const msg = useResolvedDataGridMessages(locale);\n const rowHeights = React.useMemo(() => buildRowHeights(msg), [msg]);\n const rowHeight = table.options.meta?.rowHeight;\n const onRowHeightChange = table.options.meta?.onRowHeightChange;\n\n const selectedRowHeight = React.useMemo(\n () => rowHeights.find((option) => option.value === rowHeight) ?? rowHeights[0],\n [rowHeight, rowHeights]\n );\n\n return (\n <Select value={rowHeight} onValueChange={onRowHeightChange} disabled={disabled}>\n {/*\n * ⚠ Khác bản gốc (3.3.2): nút mở mang `text-xs` + `gap-1.5`, đúng bậc chữ của các nút khác\n * trên thanh công cụ (`Button size=\"sm\"` = `h-8 px-3 text-xs gap-1.5`). Bậc `sm` của\n * `SelectTrigger` chỉ hạ CHIỀU CAO (`data-[size=sm]:h-8`), chữ giữ `text-sm` — nên trước bản\n * này nút này là chữ 14px (dòng 20px, icon–chữ 8px) đứng giữa một hàng 12px (dòng 16px,\n * icon–chữ 6px). Đổi ở ĐÂY chứ không đổi bậc `sm` của `SelectTrigger`: đó là giao diện của\n * mọi `Select size=\"sm\"` trong app, và trong gói thì đây là chỗ DUY NHẤT dùng bậc đó.\n */}\n <SelectTrigger\n aria-label={msg.rowHeight.label}\n size=\"sm\"\n className={cn(\"w-auto gap-1.5 text-xs font-normal\", triggerClassName)}\n >\n <SelectValue placeholder={msg.rowHeight.label}>\n <span className=\"flex items-center gap-1.5\">\n <selectedRowHeight.icon aria-hidden=\"true\" className=\"size-3.5 text-muted-foreground\" />\n {selectedRowHeight.label}\n </span>\n </SelectValue>\n </SelectTrigger>\n <SelectContent {...props}>\n <SelectGroup>\n {rowHeights.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n <span className=\"flex items-center gap-2\">\n <option.icon aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n {option.label}\n </span>\n </SelectItem>\n ))}\n </SelectGroup>\n </SelectContent>\n </Select>\n );\n}\n"],"mappings":";AAyEU,SACE,KADF;AAtEV,SAAS,8BAA8B,oBAAoB,WAAW,iBAAiB;AACvF,YAAY,WAAW;AACvB,SAAS,QAAQ,eAAe,aAAa,YAAY,eAAe,mBAAmB;AAC3F,SAAS,UAAU;AACnB,SAAS,mCAAmC;AAO5C,SAAS,gBAAgB,KAAuB;AAE9C,SAAO;AAAA,IACL,EAAE,OAAO,IAAI,UAAU,OAAO,OAAO,SAAkB,MAAM,UAAU;AAAA,IACvE,EAAE,OAAO,IAAI,UAAU,QAAQ,OAAO,UAAmB,MAAM,UAAU;AAAA,IACzE,EAAE,OAAO,IAAI,UAAU,MAAM,OAAO,QAAiB,MAAM,6BAA6B;AAAA,IACxF,EAAE,OAAO,IAAI,UAAU,WAAW,OAAO,cAAuB,MAAM,mBAAmB;AAAA,EAC3F;AACF;AAkBO,SAAS,sBAA6B;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsC;AACpC,QAAM,MAAM,4BAA4B,MAAM;AAC9C,QAAM,aAAa,MAAM,QAAQ,MAAM,gBAAgB,GAAG,GAAG,CAAC,GAAG,CAAC;AAClE,QAAM,YAAY,MAAM,QAAQ,MAAM;AACtC,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAE9C,QAAM,oBAAoB,MAAM;AAAA,IAC9B,MAAM,WAAW,KAAK,CAAC,WAAW,OAAO,UAAU,SAAS,KAAK,WAAW,CAAC;AAAA,IAC7E,CAAC,WAAW,UAAU;AAAA,EACxB;AAEA,SACE,qBAAC,UAAO,OAAO,WAAW,eAAe,mBAAmB,UAS1D;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY,IAAI,UAAU;AAAA,QAC1B,MAAK;AAAA,QACL,WAAW,GAAG,sCAAsC,gBAAgB;AAAA,QAEpE,8BAAC,eAAY,aAAa,IAAI,UAAU,OACtC,+BAAC,UAAK,WAAU,6BACd;AAAA,8BAAC,kBAAkB,MAAlB,EAAuB,eAAY,QAAO,WAAU,kCAAiC;AAAA,UACrF,kBAAkB;AAAA,WACrB,GACF;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,iBAAe,GAAG,OACjB,8BAAC,eACE,qBAAW,IAAI,CAAC,WACf,oBAAC,cAA8B,OAAO,OAAO,OAC3C,+BAAC,UAAK,WAAU,2BACd;AAAA,0BAAC,OAAO,MAAP,EAAY,eAAY,QAAO,WAAU,gCAA+B;AAAA,MACxE,OAAO;AAAA,OACV,KAJe,OAAO,KAKxB,CACD,GACH,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -92,7 +92,6 @@ function getColumnPinningStyle(params) {
92
92
  boxShadow: withBorder ? isLastLeftPinnedColumn ? isRtl ? "4px 0 4px -4px var(--border) inset" : "-4px 0 4px -4px var(--border) inset" : isFirstRightPinnedColumn ? isRtl ? "-4px 0 4px -4px var(--border) inset" : "4px 0 4px -4px var(--border) inset" : void 0 : void 0,
93
93
  left: isRtl ? rightPosition : leftPosition,
94
94
  right: isRtl ? leftPosition : rightPosition,
95
- opacity: isPinned ? 0.97 : 1,
96
95
  position: isPinned ? "sticky" : "relative",
97
96
  background: "var(--card)",
98
97
  width: column.getSize(),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-grid/utils.ts"],"sourcesContent":["import type { Column, Table } from \"@tanstack/react-table\";\nimport { type Locale as DateFnsLocale, format, isValid, parse } from \"date-fns\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n WrapTextIcon\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { type DataGridMessages, getDataGridMessages } from \"./messages.js\";\nimport type { CellOpts, CellPosition, Direction, FileCellData, RowHeightValue } from \"./types.js\";\n\nexport function flexRender<TProps extends object>(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return !!item && typeof item === \"object\" && \"id\" in item && \"name\" in item && \"size\" in item && \"type\" in item;\n}\n\nexport function matchSelectOption(value: string, options: { value: string; label: string }[]): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase()\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required<CellPosition> {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record<RowHeightValue, number> = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record<RowHeightValue, number> = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility<TData>(params: {\n column: Column<TData>;\n nextColumn?: Column<TData>;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn = isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned = nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder = !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder\n };\n}\n\nexport function getColumnPinningStyle<TData>(params: {\n column: Column<TData>;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition = isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition = isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n /*\n * ⚠ Token của gói này là MÀU ĐẦY ĐỦ (`--card: #ffffff`), KHÔNG phải bộ 3 kênh HSL — nên viết\n * `var(--card)` trần, đừng bọc `hsl()`. Bản gốc port từ một hệ token kiểu `\"150 20% 98%\"` nên\n * ở đó `hsl(var(--card))` mới đúng; bê nguyên sang đây thì `hsl(#ffffff)` là giá trị KHÔNG\n * HỢP LỆ, và hệ quả là cột ghim trong suốt — nội dung cuộn phía dưới lộ xuyên qua. Không có\n * lỗi nào được ném ra: CSS bỏ qua khai báo hỏng rồi rơi về `background: transparent`.\n *\n * Nền lấy theo `--card` vì hộp lưới dùng `bg-card` như mọi bảng khác của gói.\n */\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
1
+ {"version":3,"sources":["../../src/data-grid/utils.ts"],"sourcesContent":["import type { Column, Table } from \"@tanstack/react-table\";\nimport { type Locale as DateFnsLocale, format, isValid, parse } from \"date-fns\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n WrapTextIcon\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { type DataGridMessages, getDataGridMessages } from \"./messages.js\";\nimport type { CellOpts, CellPosition, Direction, FileCellData, RowHeightValue } from \"./types.js\";\n\nexport function flexRender<TProps extends object>(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return !!item && typeof item === \"object\" && \"id\" in item && \"name\" in item && \"size\" in item && \"type\" in item;\n}\n\nexport function matchSelectOption(value: string, options: { value: string; label: string }[]): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase()\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required<CellPosition> {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record<RowHeightValue, number> = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record<RowHeightValue, number> = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility<TData>(params: {\n column: Column<TData>;\n nextColumn?: Column<TData>;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn = isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned = nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder = !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder\n };\n}\n\nexport function getColumnPinningStyle<TData>(params: {\n column: Column<TData>;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition = isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition = isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n /*\n * ⚠ Token của gói này là MÀU ĐẦY ĐỦ (`--card: #ffffff`), KHÔNG phải bộ 3 kênh HSL — nên viết\n * `var(--card)` trần, đừng bọc `hsl()`. Bản gốc port từ một hệ token kiểu `\"150 20% 98%\"` nên\n * ở đó `hsl(var(--card))` mới đúng; bê nguyên sang đây thì `hsl(#ffffff)` là giá trị KHÔNG\n * HỢP LỆ, và hệ quả là cột ghim trong suốt — nội dung cuộn phía dưới lộ xuyên qua. Không có\n * lỗi nào được ném ra: CSS bỏ qua khai báo hỏng rồi rơi về `background: transparent`.\n *\n * Nền lấy theo `--card` vì hộp lưới dùng `bg-card` như mọi bảng khác của gói.\n *\n * ⚠ Khác bản gốc (3.3.2): KHÔNG có `opacity`. Bản gốc đặt `opacity: 0.97` cho ô ghim — nền đã\n * đục nhưng cả Ô mờ 3%, nên khi cuộn ngang, chữ của cột đang trôi phía dưới lộ thành vệt mờ sau\n * cột ghim. Cùng một kết cục với lỗi `hsl()` ngay trên (cột ghim để lộ nội dung cuộn qua), chỉ\n * nhạt hơn — và cũng không cổng nào thấy, vì `opacity` nằm ở `style` inline. `test:grid-render`\n * nay canh cả chuyện này. Ranh giới vùng ghim ⇄ vùng cuộn đã có viền cuối cột lo.\n */\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
@@ -1,14 +1,14 @@
1
+ import * as React from 'react';
2
+
1
3
  /**
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
+ * TRẢ FOCUS khi một lớp modal của Radix (`Dialog`, `Sheet`, `AlertDialog`) đóng mà nó được mở KHÔNG
5
+ * qua Trigger — nội bộ, không nằm trong barrel nào.
4
6
  *
5
- * Radix chỉ trả focus về `DialogTrigger`: handler `onCloseAutoFocus` của chính nó chặn mặc định của
7
+ * Radix chỉ trả focus về Trigger: handler `onCloseAutoFocus` của chính nó chặn mặc định của
6
8
  * `FocusScope` rồi gọi `triggerRef.current?.focus()` — thiếu Trigger là focus vào `null`. Mà mở bằng
7
9
  * `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
10
  * 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.
11
+ * tiếp chạy lại từ đầu trang (WCAG 2.4.3).
12
12
  *
13
13
  * Luật — ai thắng khi nhiều bên cùng muốn đưa focus đi:
14
14
  * 1. App: handler `onCloseAutoFocus` của app chạy TRƯỚC; app gọi `preventDefault()` là app nhận việc
@@ -17,10 +17,23 @@
17
17
  * của gói đợi qua một microtask để Radix đi trước.
18
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
19
  * mở. Phần tử đó đã bị gỡ khỏi DOM (dòng vừa xoá) thì gói không đoán.
20
+ *
21
+ * Ghi phần tử mở ở HAI chỗ (3.3.1):
22
+ * · `recorder` — phần tử trả về, component gọi đặt nó làm con ĐẦU TIÊN của `…Primitive.Content`.
23
+ * Ghi sớm nhất, trước cả `autoFocus` của con — lý do ở `ReturnFocusRecorder`. Chỉ ghi nếu chu kỳ
24
+ * mở này chưa ghi gì: StrictMode được phép chạy lại effect của cùng một lần mount, và lần chạy
25
+ * lại ấy thấy focus đã ở ô con.
26
+ * · `onOpenAutoFocus` — Radix chỉ gọi khi focus còn NGOÀI lớp, nên giá trị lúc đó luôn đúng và luôn
27
+ * ĐÈ lên. Nó là đường duy nhất cho nội dung `forceMount` (mount một lần, không mount lại khi mở —
28
+ * nơi gọi không đặt `recorder` cho ca đó).
29
+ * Chu kỳ khép lại ở `onCloseAutoFocus`.
20
30
  */
21
31
  declare function useModalReturnFocus<E extends Event>(onOpenAutoFocusProp: ((event: E) => void) | undefined, onCloseAutoFocusProp: ((event: E) => void) | undefined): {
22
32
  onOpenAutoFocus: (event: E) => void;
23
33
  onCloseAutoFocus: (event: E) => void;
34
+ recorder: React.FunctionComponentElement<{
35
+ record: () => void;
36
+ }>;
24
37
  };
25
38
 
26
39
  export { useModalReturnFocus };
@@ -15,10 +15,23 @@ function returnTargetFrom(active) {
15
15
  }
16
16
  return current instanceof HTMLElement ? current : null;
17
17
  }
18
+ function ReturnFocusRecorder({ record }) {
19
+ React.useInsertionEffect(() => {
20
+ record();
21
+ }, [record]);
22
+ return null;
23
+ }
18
24
  function useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp) {
19
25
  const returnFocusRef = React.useRef(null);
26
+ const recordedRef = React.useRef(false);
27
+ const recordOnMount = React.useCallback(() => {
28
+ if (recordedRef.current) return;
29
+ recordedRef.current = true;
30
+ returnFocusRef.current = returnTargetFrom(document.activeElement);
31
+ }, []);
20
32
  const onOpenAutoFocus = React.useCallback(
21
33
  (event) => {
34
+ recordedRef.current = true;
22
35
  returnFocusRef.current = returnTargetFrom(document.activeElement);
23
36
  onOpenAutoFocusProp?.(event);
24
37
  },
@@ -29,6 +42,7 @@ function useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp) {
29
42
  onCloseAutoFocusProp?.(event);
30
43
  const target = returnFocusRef.current;
31
44
  returnFocusRef.current = null;
45
+ recordedRef.current = false;
32
46
  if (event.defaultPrevented) return;
33
47
  queueMicrotask(() => {
34
48
  const ownerDocument = target?.ownerDocument ?? document;
@@ -40,7 +54,11 @@ function useModalReturnFocus(onOpenAutoFocusProp, onCloseAutoFocusProp) {
40
54
  },
41
55
  [onCloseAutoFocusProp]
42
56
  );
43
- return { onOpenAutoFocus, onCloseAutoFocus };
57
+ const recorder = React.useMemo(
58
+ () => React.createElement(ReturnFocusRecorder, { record: recordOnMount }),
59
+ [recordOnMount]
60
+ );
61
+ return { onOpenAutoFocus, onCloseAutoFocus, recorder };
44
62
  }
45
63
  export {
46
64
  useModalReturnFocus
@@ -1 +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":[]}
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 * Ghi phần tử mở NGAY LÚC nội dung lớp được gắn vào cây — trước mọi `autoFocus` của con.\n *\n * ── Vì sao không đủ nếu chỉ ghi ở `onOpenAutoFocus` (cách của 3.3.0) ─────────────────────\n * Ô con có `autoFocus` được React focus trong `commitMount` — pha LAYOUT của chính lần commit gắn\n * nội dung — TRƯỚC effect của `FocusScope`. Tới lượt `FocusScope`, focus đã nằm trong lớp, và Radix\n * BỎ QUA hẳn sự kiện auto-focus lúc mount (`container.contains(previouslyFocusedElement)`) — tức\n * `onOpenAutoFocus` không bao giờ được gọi, gói không biết ai đã mở lớp, và đóng xong focus rơi\n * xuống `<body>`. Đo được ở comitor-account bằng log `focus()`: focusin nút mở → `INPUT.focus()` từ\n * `commitMount` → lúc đóng không có lời gọi `focus()` nào. Chín hộp thoại của app đó dính.\n *\n * `useInsertionEffect` chạy ở pha MUTATION, và React chạy insertion effect của CẢ cây trước khi pha\n * layout bắt đầu — nên nó đứng trước `autoFocus` của bất kỳ con nào, bất kể con nằm ở đâu trong cây.\n * Không cần ref nào (ref chưa gắn ở pha này), chỉ đọc `document.activeElement`.\n *\n * Component này phải đứng TRONG nội dung (`…Primitive.Content`), không đứng ở vỏ `DialogContent`:\n * vỏ mount một lần khi app render `<Dialog>` đang ĐÓNG, còn nội dung mount lại ở MỖI lần mở.\n */\nfunction ReturnFocusRecorder({ record }: { record: () => void }) {\n React.useInsertionEffect(() => {\n record();\n }, [record]);\n return null;\n}\n\n/**\n * TRẢ FOCUS khi một lớp modal của Radix (`Dialog`, `Sheet`, `AlertDialog`) đóng mà nó được mở KHÔNG\n * qua Trigger — nội bộ, không nằm trong barrel nào.\n *\n * Radix chỉ trả focus về Trigger: 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).\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 *\n * Ghi phần tử mở ở HAI chỗ (3.3.1):\n * · `recorder` — phần tử trả về, component gọi đặt nó làm con ĐẦU TIÊN của `…Primitive.Content`.\n * Ghi sớm nhất, trước cả `autoFocus` của con — lý do ở `ReturnFocusRecorder`. Chỉ ghi nếu chu kỳ\n * mở này chưa ghi gì: StrictMode được phép chạy lại effect của cùng một lần mount, và lần chạy\n * lại ấy thấy focus đã ở ô con.\n * · `onOpenAutoFocus` — Radix chỉ gọi khi focus còn NGOÀI lớp, nên giá trị lúc đó luôn đúng và luôn\n * ĐÈ lên. Nó là đường duy nhất cho nội dung `forceMount` (mount một lần, không mount lại khi mở —\n * nơi gọi không đặt `recorder` cho ca đó).\n * Chu kỳ khép lại ở `onCloseAutoFocus`.\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 const recordedRef = React.useRef(false);\n\n const recordOnMount = React.useCallback(() => {\n if (recordedRef.current) return;\n recordedRef.current = true;\n returnFocusRef.current = returnTargetFrom(document.activeElement);\n }, []);\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 recordedRef.current = true;\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 recordedRef.current = false;\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 const recorder = React.useMemo(\n () => React.createElement(ReturnFocusRecorder, { record: recordOnMount }),\n [recordOnMount]\n );\n\n return { onOpenAutoFocus, onCloseAutoFocus, recorder };\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;AAoBA,SAAS,oBAAoB,EAAE,OAAO,GAA2B;AAC/D,QAAM,mBAAmB,MAAM;AAC7B,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACT;AA8BO,SAAS,oBACd,qBACA,sBACA;AACA,QAAM,iBAAiB,MAAM,OAA2B,IAAI;AAC5D,QAAM,cAAc,MAAM,OAAO,KAAK;AAEtC,QAAM,gBAAgB,MAAM,YAAY,MAAM;AAC5C,QAAI,YAAY,QAAS;AACzB,gBAAY,UAAU;AACtB,mBAAe,UAAU,iBAAiB,SAAS,aAAa;AAAA,EAClE,GAAG,CAAC,CAAC;AAEL,QAAM,kBAAkB,MAAM;AAAA,IAC5B,CAAC,UAAa;AAEZ,kBAAY,UAAU;AACtB,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,kBAAY,UAAU;AACtB,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,QAAM,WAAW,MAAM;AAAA,IACrB,MAAM,MAAM,cAAc,qBAAqB,EAAE,QAAQ,cAAc,CAAC;AAAA,IACxE,CAAC,aAAa;AAAA,EAChB;AAEA,SAAO,EAAE,iBAAiB,kBAAkB,SAAS;AACvD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
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",