@comitor/ui 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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":[]}
@@ -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.1",
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",