@comitor/ui 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/composite/avatar-group.d.ts +10 -1
- package/dist/components/composite/avatar-group.js +37 -14
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/confirm-dialog.js +56 -35
- package/dist/components/composite/confirm-dialog.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +3 -0
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +41 -1
- package/dist/components/composite/status-config.js +6 -0
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/composite/status-pill.d.ts +4 -1
- package/dist/components/composite/status-pill.js +1 -1
- package/dist/components/composite/status-pill.js.map +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +3 -1
- package/dist/components/composite/table-pagination-constants.js.map +1 -1
- package/dist/components/composite/table-pagination.js +10 -2
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/composite/text-utils.d.ts +4 -23
- package/dist/components/composite/text-utils.js +1 -11
- package/dist/components/composite/text-utils.js.map +1 -1
- package/dist/components/ui/banner.d.ts +8 -0
- package/dist/components/ui/banner.js +149 -35
- package/dist/components/ui/banner.js.map +1 -1
- package/dist/components/ui/mask-input-constants.d.ts +34 -2
- package/dist/components/ui/mask-input-constants.js +56 -5
- package/dist/components/ui/mask-input-constants.js.map +1 -1
- package/dist/components/ui/mask-input.d.ts +6 -5
- package/dist/components/ui/mask-input.js +5 -1
- package/dist/components/ui/mask-input.js.map +1 -1
- package/dist/components/ui/mention.d.ts +14 -2
- package/dist/components/ui/mention.js +33 -6
- package/dist/components/ui/mention.js.map +1 -1
- package/dist/components/ui/phone-input.d.ts +18 -8
- package/dist/components/ui/phone-input.js +120 -33
- package/dist/components/ui/phone-input.js.map +1 -1
- package/dist/components/ui/scroll-spy.js +46 -31
- package/dist/components/ui/scroll-spy.js.map +1 -1
- package/dist/components/ui/stepper.js +26 -3
- package/dist/components/ui/stepper.js.map +1 -1
- package/dist/components/ui/time-picker.d.ts +24 -4
- package/dist/components/ui/time-picker.js +425 -162
- package/dist/components/ui/time-picker.js.map +1 -1
- package/dist/components/ui/timeline.js +45 -9
- package/dist/components/ui/timeline.js.map +1 -1
- package/dist/data-grid/data-grid-cell-variants.js +27 -9
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.d.ts +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js +6 -0
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
- package/dist/data-grid/data-grid-row.js +1 -1
- package/dist/data-grid/data-grid-row.js.map +1 -1
- package/dist/data-grid/data-grid-select-column.js +6 -2
- package/dist/data-grid/data-grid-select-column.js.map +1 -1
- package/dist/data-grid/data-grid-skeleton.d.ts +5 -0
- package/dist/data-grid/data-grid-skeleton.js +1 -4
- package/dist/data-grid/data-grid-skeleton.js.map +1 -1
- package/dist/data-grid/data-grid.d.ts +1 -1
- package/dist/data-grid/data-grid.js +3 -2
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/filters.js +3 -2
- package/dist/data-grid/filters.js.map +1 -1
- package/dist/data-grid/types.d.ts +23 -0
- package/dist/data-grid/use-data-grid.d.ts +1 -0
- package/dist/data-grid/use-data-grid.js +70 -24
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/data-grid/utils.d.ts +7 -0
- package/dist/data-grid/utils.js +3 -1
- package/dist/data-grid/utils.js.map +1 -1
- package/dist/i18n/namespaces/search-filter-bar.js +8 -6
- package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
- package/dist/i18n/namespaces/table-pagination.js +6 -4
- package/dist/i18n/namespaces/table-pagination.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/lib/collect-item-elements.d.ts +47 -0
- package/dist/lib/collect-item-elements.js +22 -0
- package/dist/lib/collect-item-elements.js.map +1 -0
- package/dist/lib/text-search.d.ts +32 -0
- package/dist/lib/text-search.js +17 -0
- package/dist/lib/text-search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +1 -0
- package/dist/shell/display-axis-provider.d.ts +1 -1
- package/dist/shell/index.d.ts +2 -1
- package/dist/shell/shell-context.d.ts +1 -0
- package/dist/shell/utils.d.ts +2 -1
- package/dist/tokens.d.ts +43 -11
- package/dist/tokens.js +36 -9
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
- package/styles.css +73 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/banner.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/banner) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n *\n * Dùng ở Comitor: thông báo TOÀN WORKSPACE ở đỉnh khung app (sắp hết hạn dùng thử, bảo trì, lỗi\n * thanh toán) — đóng được, khác `Alert` là khối tĩnh nằm trong nội dung trang.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\nimport { Button } from \"./button.js\";\n\nconst BANNER_ANIMATION_DURATION = 400;\nconst DEFAULT_BANNER_PRIORITY = 0;\nconst DEFAULT_BANNER_DISMISSIBLE = true;\n\ntype BannerVariant = \"default\" | \"info\" | \"success\" | \"warning\" | \"destructive\";\ntype BannerSide = \"top\" | \"bottom\";\ntype BannerStrategy = \"fixed\" | \"static\" | \"sticky\" | \"absolute\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype CloseElement = React.ComponentRef<typeof BannerClose>;\n\ninterface BannerRenderProps {\n id: string;\n variant?: BannerVariant;\n dismissible: boolean;\n onClose: () => void;\n onRemove: () => void;\n}\n\n// Thượng nguồn đặt tên kiểu này là `BannerContent` — trùng tên component `BannerContent`. Đổi tên\n// để `export { BannerContent }` không mang theo một KIỂU cùng tên mà nghĩa khác hẳn.\ntype BannerContentValue = React.ReactNode | ((props: BannerRenderProps) => React.ReactNode);\n\ninterface BannerData {\n id: string;\n content: BannerContentValue;\n variant?: BannerVariant;\n priority?: number;\n duration?: number;\n dismissible?: boolean;\n onDismiss?: () => void;\n}\n\ninterface StoreState {\n banners: BannerData[];\n removing: Set<string>;\n heights: Map<string, number>;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n notify: () => void;\n onBannerAdd: (banner: Omit<BannerData, \"id\">) => string;\n onBannerRemove: (id: string) => void;\n onBannersClear: () => void;\n onRemovingChange: (id: string, value: boolean) => void;\n onHeightChange: (id: string, height: number) => void;\n onHeightRemove: (id: string) => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`Banners\\``);\n }\n return context;\n}\n\nfunction useStore<T>(store: Store, selector: (state: StoreState) => T): T {\n return React.useSyncExternalStore(\n store.subscribe,\n () => selector(store.getState()),\n () => selector(store.getState())\n );\n}\n\ninterface BannerContextValue {\n id?: string;\n variant?: BannerVariant | null;\n dismissible?: boolean;\n onClose?: () => void;\n}\n\nconst BannerContext = React.createContext<BannerContextValue | null>(null);\n\nfunction useBannerContext(consumerName: string) {\n const context = React.useContext(BannerContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`Banner\\``);\n }\n return context;\n}\n\nfunction useBanner() {\n const { id, variant, dismissible, onClose } = useBannerContext(\"useBanner\");\n const storeContext = React.useContext(StoreContext);\n\n return React.useMemo(() => {\n const onRemove = id && storeContext ? () => storeContext.onBannerRemove(id) : undefined;\n\n return {\n id,\n variant,\n dismissible,\n onClose,\n onRemove\n };\n }, [id, variant, dismissible, onClose, storeContext]);\n}\n\ninterface BannersProps {\n children?: React.ReactNode;\n maxVisible?: number;\n side?: BannerSide;\n strategy?: BannerStrategy;\n container?: Element | DocumentFragment | null;\n}\n\nfunction Banners(props: BannersProps) {\n const { children, maxVisible = 1, side = \"top\", strategy = \"fixed\", container: containerProp } = props;\n\n const instanceId = React.useId();\n const stateRef = useLazyRef<StoreState>(() => ({\n banners: [],\n removing: new Set(),\n heights: new Map()\n }));\n const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n const timeoutsRef = useLazyRef<Map<string, ReturnType<typeof setTimeout>>>(() => new Map());\n const idCounterRef = React.useRef(0);\n\n const store: Store = React.useMemo(\n () => ({\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n notify: () => {\n for (const listener of listenersRef.current) {\n listener();\n }\n },\n onBannerAdd: (banner) => {\n /*\n * ⚠ Khác thượng nguồn: bản gốc `crypto.randomUUID()` — hàm này CHỈ có trong secure\n * context (https, localhost). Mở app qua http bằng IP mạng nội bộ (máy dev, môi trường\n * nội bộ) thì nó là `undefined` và mọi lần thêm banner đều throw. Id chỉ cần duy nhất\n * trong một `Banners` → bộ đếm, tiền tố `useId()` cho chắc khi có nhiều `Banners`.\n */\n idCounterRef.current += 1;\n const id = `${instanceId}-${idCounterRef.current}`;\n const newBanner: BannerData = { ...banner, id };\n const priority = banner.priority ?? DEFAULT_BANNER_PRIORITY;\n\n const banners = [...stateRef.current.banners];\n const insertIndex = banners.findIndex((b) => (b.priority ?? DEFAULT_BANNER_PRIORITY) < priority);\n\n if (insertIndex === -1) {\n banners.push(newBanner);\n } else {\n banners.splice(insertIndex, 0, newBanner);\n }\n\n stateRef.current.banners = banners;\n store.notify();\n\n if (banner.duration && banner.duration > 0) {\n const timeoutId = setTimeout(() => {\n store.onRemovingChange(id, true);\n timeoutsRef.current.delete(id);\n }, banner.duration);\n timeoutsRef.current.set(id, timeoutId);\n }\n\n return id;\n },\n onBannerRemove: (id) => {\n const banner = stateRef.current.banners.find((b) => b.id === id);\n if (!banner) return;\n\n const timeoutId = timeoutsRef.current.get(id);\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutsRef.current.delete(id);\n }\n\n const newRemoving = new Set(stateRef.current.removing);\n newRemoving.delete(id);\n stateRef.current.removing = newRemoving;\n\n banner.onDismiss?.();\n stateRef.current.banners = stateRef.current.banners.filter((b) => b.id !== id);\n store.notify();\n },\n onBannersClear: () => {\n for (const timeoutId of timeoutsRef.current.values()) {\n clearTimeout(timeoutId);\n }\n timeoutsRef.current.clear();\n stateRef.current.removing = new Set();\n stateRef.current.heights = new Map();\n stateRef.current.banners = [];\n store.notify();\n },\n onRemovingChange: (id, value) => {\n const newSet = new Set(stateRef.current.removing);\n if (value) {\n newSet.add(id);\n } else {\n newSet.delete(id);\n }\n stateRef.current.removing = newSet;\n store.notify();\n },\n onHeightChange: (id, height) => {\n if (stateRef.current.heights.get(id) === height) return;\n const newHeights = new Map(stateRef.current.heights);\n newHeights.set(id, height);\n stateRef.current.heights = newHeights;\n store.notify();\n },\n onHeightRemove: (id) => {\n if (!stateRef.current.heights.has(id)) return;\n const newHeights = new Map(stateRef.current.heights);\n newHeights.delete(id);\n stateRef.current.heights = newHeights;\n store.notify();\n }\n }),\n [instanceId, stateRef, listenersRef, timeoutsRef]\n );\n\n const banners = useStore(store, (state) => state.banners);\n const heights = useStore(store, (state) => state.heights);\n const visibleBanners = banners.slice(0, maxVisible);\n\n /*\n * ⚠ Khác thượng nguồn: bản gốc đọc `globalThis.document?.body` NGAY TRONG render. Ở đây chỉ đọc\n * sau khi mount (cùng khuôn với `ActionBar`) — lượt render phía server và lượt hydrate không\n * chạm `document`.\n */\n const [mounted, setMounted] = React.useState(false);\n useIsomorphicLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n const withPortal = strategy === \"fixed\" || strategy === \"absolute\";\n const container = withPortal ? (containerProp ?? (mounted ? globalThis.document?.body : null) ?? null) : null;\n\n const totalHeight = React.useMemo(() => {\n let total = 0;\n for (const banner of visibleBanners) {\n total += heights.get(banner.id) ?? 0;\n }\n return total;\n }, [visibleBanners, heights]);\n\n const bannerContainer = visibleBanners.length > 0 && (\n <div\n data-slot=\"banner-container\"\n data-side={side}\n data-strategy={strategy}\n className={cn(\n \"pointer-events-none right-0 left-0 isolate z-50\",\n /*\n * ⚠ Khác thượng nguồn: bản gốc ghi `transition` thẳng vào `style`, nên lớp\n * `motion-reduce:transition-none` của từng banner KHÔNG BAO GIỜ thắng được (style nội tuyến\n * luôn thắng lớp) — người bật \"giảm chuyển động\" vẫn thấy banner trượt. Nay thời lượng đi\n * qua `--banner-duration`, bằng 0 khi `prefers-reduced-motion`; banner con thừa kế biến này.\n */\n \"motion-reduce:[--banner-duration:0ms]\",\n strategy === \"fixed\" && \"fixed\",\n strategy === \"static\" && \"relative\",\n strategy === \"sticky\" && \"sticky\",\n strategy === \"absolute\" && \"absolute\",\n side === \"top\" ? \"top-0\" : \"bottom-0\"\n )}\n style={{\n height: totalHeight > 0 ? totalHeight : \"auto\",\n transition: `height var(--banner-duration, ${BANNER_ANIMATION_DURATION}ms) cubic-bezier(0.32, 0.72, 0, 1)`\n }}\n >\n {visibleBanners.map((banner, index) => (\n <BannerImpl key={banner.id} banner={banner} side={side} index={index} />\n ))}\n </div>\n );\n\n return (\n <StoreContext.Provider value={store}>\n {strategy === \"static\" || strategy === \"sticky\" ? (\n <>\n {side === \"top\" && bannerContainer}\n {children}\n {side === \"bottom\" && bannerContainer}\n </>\n ) : (\n <>\n {children}\n {container && bannerContainer && ReactDOM.createPortal(bannerContainer, container)}\n </>\n )}\n </StoreContext.Provider>\n );\n}\n\nfunction useBanners() {\n const store = useStoreContext(\"useBanners\");\n const banners = useStore(store, (state) => state.banners);\n\n return React.useMemo(\n () => ({\n onBannerAdd: store.onBannerAdd,\n onBannerRemove: store.onBannerRemove,\n onBannersClear: store.onBannersClear,\n banners\n }),\n [store, banners]\n );\n}\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc tô bằng bảng màu mặc định của Tailwind (`bg-blue-50\n * text-blue-900`, `bg-red-50`… kèm `dark:`) — đi vòng qua hợp đồng token nên không đổi theo bảng\n * tương phản cao. Ở đây dùng ĐÚNG bộ lớp của `Alert` (alert.tsx) để Banner và Alert cùng tông\n * trông như một họ: nền `bg-card`, chữ + icon ở bậc `-ink` của tone. `Alert` chưa có `info` /\n * `success`; hai tông đó theo cùng khuôn với `warning` / `destructive` của nó.\n *\n * Nền ĐẶC `bg-card` chứ không tint `bg-<tone>/15`: ở chế độ `fixed`/`absolute`, banner nằm ĐÈ lên\n * nội dung trang — một nền trong suốt một phần sẽ cho chữ trang lộ xuyên qua chữ banner.\n *\n * Mô tả (`BannerDescription`) giữ `opacity-90` của bản gốc — trên nền đặc nó tương đương đúng\n * `text-<tone>-ink/90` của `AlertDescription`. Cổng không đo được `opacity`, nên số đo tay (trên\n * `--card`, tiêu đề / mô tả 90%):\n *\n * bảng tương phản cao light dark\n * info 6,68 / 5,31 9,54 / 7,92 ✓\n * success 6,52 / 5,20 10,19 / 8,44 ✓\n * warning 6,51 / 5,17 11,60 / 9,56 ✓\n * destructive 7,15 / 5,77 6,42 / 5,41 ✓\n * bảng mặc định (nợ token đã chấp nhận, cùng mức với `Alert`):\n * destructive 4,38 / 3,81 (light) · 4,06 / 3,50 (dark) · info dark 4,28 / 3,71\n */\nconst bannerVariants = cva(\n \"pointer-events-auto relative flex w-full items-center gap-3 border-b px-4 py-3 text-sm motion-reduce:transition-none\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n info: \"bg-card text-info-ink\",\n success: \"bg-card text-success-ink\",\n warning: \"bg-card text-warning-ink\",\n destructive: \"bg-card text-destructive-ink\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n);\n\ninterface BannerImplProps {\n banner: BannerData;\n side: BannerSide;\n index: number;\n}\n\nfunction BannerImpl(props: BannerImplProps) {\n const { banner, side, index } = props;\n\n const store = useStoreContext(\"BannerImpl\");\n const removing = useStore(store, (state) => state.removing.has(banner.id));\n const banners = useStore(store, (state) => state.banners);\n const heights = useStore(store, (state) => state.heights);\n\n const [mounted, setMounted] = React.useState(false);\n const bannerRef = React.useRef<HTMLDivElement>(null);\n const offsetBeforeRemoveRef = React.useRef(0);\n\n const offset = React.useMemo(() => {\n let total = 0;\n for (const b of banners) {\n if (b.id === banner.id) break;\n total += heights.get(b.id) ?? 0;\n }\n return total;\n }, [banners, heights, banner.id]);\n\n if (!removing) {\n offsetBeforeRemoveRef.current = offset;\n }\n\n React.useEffect(() => {\n const frame = requestAnimationFrame(() => setMounted(true));\n return () => cancelAnimationFrame(frame);\n }, []);\n\n useIsomorphicLayoutEffect(() => {\n if (!bannerRef.current || removing) return;\n const height = bannerRef.current.getBoundingClientRect().height;\n store.onHeightChange(banner.id, height);\n }, [store, banner.id, removing]);\n\n React.useEffect(() => {\n if (!removing) return;\n store.onHeightRemove(banner.id);\n const timeoutId = setTimeout(() => store.onBannerRemove(banner.id), BANNER_ANIMATION_DURATION);\n return () => clearTimeout(timeoutId);\n }, [removing, store, banner.id]);\n\n const onClose = React.useCallback(() => store.onRemovingChange(banner.id, true), [store, banner.id]);\n\n const onRemove = React.useCallback(() => store.onBannerRemove(banner.id), [store, banner.id]);\n\n const dismissible = banner.dismissible ?? DEFAULT_BANNER_DISMISSIBLE;\n\n const contextValue = React.useMemo<BannerContextValue>(\n () => ({ id: banner.id, variant: banner.variant, dismissible, onClose }),\n [banner.id, banner.variant, dismissible, onClose]\n );\n\n const renderProps = React.useMemo<BannerRenderProps>(\n () => ({\n id: banner.id,\n variant: banner.variant,\n dismissible,\n onClose,\n onRemove\n }),\n [banner.id, banner.variant, dismissible, onClose, onRemove]\n );\n\n const currentOffset = removing ? offsetBeforeRemoveRef.current : offset;\n const isTop = side === \"top\";\n\n function getTransform() {\n if (!mounted) return isTop ? \"translateY(-100%)\" : \"translateY(100%)\";\n if (removing) {\n return isTop ? `translateY(calc(${currentOffset}px - 100%))` : `translateY(calc(-${currentOffset}px + 100%))`;\n }\n return isTop ? `translateY(${currentOffset}px)` : `translateY(-${currentOffset}px)`;\n }\n\n const opacityDuration = removing ? BANNER_ANIMATION_DURATION / 2 : BANNER_ANIMATION_DURATION;\n\n return (\n <BannerContext.Provider value={contextValue}>\n <div\n role=\"status\"\n aria-live=\"polite\"\n data-slot=\"queued-banner\"\n data-state={removing ? \"closed\" : \"open\"}\n data-mounted={mounted}\n data-removed={removing}\n data-side={side}\n data-front={index === 0}\n data-index={index}\n ref={bannerRef}\n className={bannerVariants({ variant: banner.variant })}\n style={{\n position: \"absolute\",\n [isTop ? \"top\" : \"bottom\"]: 0,\n left: 0,\n right: 0,\n zIndex: removing ? 0 : 50 - index,\n transform: getTransform(),\n opacity: mounted && !removing ? 1 : 0,\n // Thời lượng qua `--banner-duration` (0 khi giảm chuyển động) — xem chú thích ở `Banners`.\n transition: `transform var(--banner-duration, ${BANNER_ANIMATION_DURATION}ms) cubic-bezier(0.32, 0.72, 0, 1), opacity var(--banner-duration, ${opacityDuration}ms) ease`\n }}\n >\n {typeof banner.content === \"function\" ? banner.content(renderProps) : banner.content}\n </div>\n </BannerContext.Provider>\n );\n}\n\ninterface BannerProps extends DivProps, VariantProps<typeof bannerVariants> {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n onDismiss?: () => void;\n priority?: number;\n duration?: number;\n dismissible?: boolean;\n}\n\nfunction Banner(props: BannerProps) {\n const {\n className,\n variant = \"default\",\n open: openProp,\n defaultOpen = true,\n onOpenChange,\n onDismiss,\n priority,\n duration,\n dismissible = DEFAULT_BANNER_DISMISSIBLE,\n children,\n asChild,\n ...rootProps\n } = props;\n\n const store = React.useContext(StoreContext);\n\n const isInsideStore = store !== null;\n const isControlled = openProp !== undefined;\n\n const openRef = useLazyRef(() => openProp ?? defaultOpen);\n const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n const bannerIdRef = React.useRef<string | null>(null);\n const onDismissRef = useAsRef(onDismiss);\n const onOpenChangeRef = useAsRef(onOpenChange);\n\n if (isControlled) {\n openRef.current = openProp;\n }\n\n const subscribe = React.useCallback(\n (cb: () => void) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n [listenersRef]\n );\n\n const getSnapshot = React.useCallback(() => openRef.current, [openRef]);\n\n const open = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n React.useEffect(() => {\n if (!isInsideStore || !store || !open) return;\n\n /*\n * ⚠ Khác thượng nguồn — hai lỗi của `<Banner>` đặt trong `<Banners>`:\n *\n * 1. Hàm dọn của effect này gỡ banner bằng `onBannerRemove`, mà hàm đó GỌI `onDismiss`. Bản\n * gốc vì thế báo \"người dùng đã đóng\" (`onDismiss` + `onOpenChange(false)`) mỗi khi effect\n * chạy lại — tức MỖI LẦN cha re-render, vì `children` là JSX mới — và cả khi unmount (đổi\n * route). Banner tự đóng, và app lưu \"đã tắt thông báo\" cho một lần người dùng chưa hề bấm.\n * Cờ `isCleaningUp` chặn đúng đường đó; đóng thật (nút X, hết `duration`) vẫn báo như cũ.\n * 2. Ở chế độ không kiểm soát, đóng qua hàng đợi không hạ `open` của chính `Banner`, nên lần\n * re-render kế tiếp thêm LẠI banner vừa đóng. Nay hạ `open` như `onClose` bên dưới.\n */\n let isCleaningUp = false;\n\n const id = store.onBannerAdd({\n content: children,\n variant: variant ?? undefined,\n priority,\n dismissible,\n duration,\n onDismiss: () => {\n if (isCleaningUp) return;\n if (!isControlled) {\n openRef.current = false;\n for (const listener of listenersRef.current) {\n listener();\n }\n }\n onDismissRef.current?.();\n onOpenChangeRef.current?.(false);\n }\n });\n bannerIdRef.current = id;\n\n return () => {\n isCleaningUp = true;\n if (bannerIdRef.current) {\n store.onBannerRemove(bannerIdRef.current);\n bannerIdRef.current = null;\n }\n };\n }, [\n isInsideStore,\n store,\n open,\n isControlled,\n children,\n variant,\n priority,\n dismissible,\n duration,\n openRef,\n listenersRef,\n onDismissRef,\n onOpenChangeRef\n ]);\n\n const onClose = React.useCallback(() => {\n if (!isControlled) {\n openRef.current = false;\n for (const listener of listenersRef.current) {\n listener();\n }\n }\n onOpenChangeRef.current?.(false);\n }, [isControlled, openRef, listenersRef, onOpenChangeRef]);\n\n const contextValue = React.useMemo<BannerContextValue>(\n () => ({\n variant,\n dismissible,\n onClose\n }),\n [variant, dismissible, onClose]\n );\n\n if (!open || isInsideStore) return null;\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <BannerContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"status\"\n aria-live=\"polite\"\n data-slot=\"banner\"\n data-state=\"open\"\n className={cn(bannerVariants({ variant, className }))}\n {...rootProps}\n >\n {children}\n </RootPrimitive>\n </BannerContext.Provider>\n );\n}\n\nfunction BannerIcon(props: DivProps) {\n const { className, asChild, ...iconProps } = props;\n\n const IconPrimitive = asChild ? Slot : \"div\";\n\n return (\n <IconPrimitive\n data-slot=\"banner-icon\"\n className={cn(\"flex shrink-0 items-center [&>svg]:size-4\", className)}\n {...iconProps}\n />\n );\n}\n\nfunction BannerContent(props: DivProps) {\n const { className, asChild, ...contentProps } = props;\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ContentPrimitive\n data-slot=\"banner-content\"\n className={cn(\"flex min-w-0 flex-1 flex-col gap-1\", className)}\n {...contentProps}\n />\n );\n}\n\nfunction BannerTitle(props: React.ComponentProps<\"div\">) {\n const { className, ...titleProps } = props;\n\n return <div data-slot=\"banner-title\" className={cn(\"text-sm leading-none font-medium\", className)} {...titleProps} />;\n}\n\nfunction BannerDescription(props: React.ComponentProps<\"div\">) {\n const { className, ...descriptionProps } = props;\n\n return <div data-slot=\"banner-description\" className={cn(\"text-xs opacity-90\", className)} {...descriptionProps} />;\n}\n\nfunction BannerActions(props: DivProps) {\n const { className, asChild, ...actionsProps } = props;\n\n const ActionsPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ActionsPrimitive\n data-slot=\"banner-actions\"\n className={cn(\"flex items-center gap-2\", className)}\n {...actionsProps}\n />\n );\n}\n\ntype BannerCloseProps = React.ComponentProps<typeof Button>;\n\n/**\n * Nút đóng banner. Không truyền `children` thì vẽ icon X.\n *\n * ⚠ Khác thượng nguồn: có tên cho trình đọc màn hình — `aria-label` mặc định lấy khoá\n * `closeLabel` của namespace `banner` (`ComitorLocaleProvider`), ghi đè bằng chính prop\n * `aria-label`. Bản gốc là `Button` chỉ chứa icon X, tức một nút KHÔNG TÊN. Nếu `children` là\n * CHỮ nhìn thấy được thì truyền `aria-label` trùng với chữ đó (WCAG 2.5.3), hoặc\n * `aria-label={undefined}`.\n */\nfunction BannerClose(props: BannerCloseProps) {\n const { onClick: onClickProp, disabled, children, ...closeProps } = props;\n\n const { dismissible = DEFAULT_BANNER_DISMISSIBLE, onClose } = useBannerContext(\"BannerClose\");\n const t = useComitorMessages(\"banner\");\n\n const isDisabled = disabled ?? !dismissible;\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<CloseElement>) => {\n onClickProp?.(event);\n if (event.defaultPrevented || isDisabled) return;\n onClose?.();\n },\n [onClickProp, isDisabled, onClose]\n );\n\n return (\n <Button\n aria-label={t.closeLabel}\n data-slot=\"banner-close\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={onClick}\n disabled={isDisabled}\n {...closeProps}\n >\n {children ?? <X aria-hidden=\"true\" className=\"size-3.5\" />}\n </Button>\n );\n}\n\nexport {\n Banner,\n BannerActions,\n BannerClose,\n type BannerCloseProps,\n BannerContent,\n type BannerData,\n BannerDescription,\n BannerIcon,\n type BannerProps,\n type BannerRenderProps,\n Banners,\n type BannersProps,\n BannerTitle,\n type BannerVariant,\n useBanner,\n useBanners\n};\n"],"mappings":"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","names":["banners"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/banner.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/banner) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n *\n * Dùng ở Comitor: thông báo TOÀN WORKSPACE ở đỉnh khung app (sắp hết hạn dùng thử, bảo trì, lỗi\n * thanh toán) — đóng được, khác `Alert` là khối tĩnh nằm trong nội dung trang.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\nimport { Button } from \"./button.js\";\n\nconst BANNER_ANIMATION_DURATION = 400;\nconst DEFAULT_BANNER_PRIORITY = 0;\nconst DEFAULT_BANNER_DISMISSIBLE = true;\n\ntype BannerVariant = \"default\" | \"info\" | \"success\" | \"warning\" | \"destructive\";\ntype BannerSide = \"top\" | \"bottom\";\ntype BannerStrategy = \"fixed\" | \"static\" | \"sticky\" | \"absolute\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype CloseElement = React.ComponentRef<typeof BannerClose>;\n\ninterface BannerRenderProps {\n id: string;\n variant?: BannerVariant;\n dismissible: boolean;\n onClose: () => void;\n onRemove: () => void;\n}\n\n// Thượng nguồn đặt tên kiểu này là `BannerContent` — trùng tên component `BannerContent`. Đổi tên\n// để `export { BannerContent }` không mang theo một KIỂU cùng tên mà nghĩa khác hẳn.\ntype BannerContentValue = React.ReactNode | ((props: BannerRenderProps) => React.ReactNode);\n\ninterface BannerData {\n id: string;\n content: BannerContentValue;\n variant?: BannerVariant;\n priority?: number;\n duration?: number;\n dismissible?: boolean;\n onDismiss?: () => void;\n}\n\n// Phần của một banner ĐANG trong hàng đợi mà `onBannerUpdate` đổi được tại chỗ. Nội dung không nằm\n// ở đây — nó đi đường riêng, xem `ChildrenSource`.\ntype BannerUpdate = Pick<BannerData, \"variant\" | \"priority\" | \"duration\" | \"dismissible\">;\n\ninterface StoreState {\n banners: BannerData[];\n removing: Set<string>;\n heights: Map<string, number>;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n notify: () => void;\n onBannerAdd: (banner: Omit<BannerData, \"id\">) => string;\n // Nội bộ — KHÔNG xuất qua `useBanners()`. Chỉ `Banner` khai báo dùng (xem chú thích trong thân hàm).\n onBannerUpdate: (id: string, patch: BannerUpdate) => void;\n onBannerRemove: (id: string) => void;\n onBannersClear: () => void;\n onRemovingChange: (id: string, value: boolean) => void;\n onHeightChange: (id: string, height: number) => void;\n onHeightRemove: (id: string) => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`Banners\\``);\n }\n return context;\n}\n\nfunction useStore<T>(store: Store, selector: (state: StoreState) => T): T {\n return React.useSyncExternalStore(\n store.subscribe,\n () => selector(store.getState()),\n () => selector(store.getState())\n );\n}\n\ninterface BannerContextValue {\n id?: string;\n variant?: BannerVariant | null;\n dismissible?: boolean;\n onClose?: () => void;\n}\n\nconst BannerContext = React.createContext<BannerContextValue | null>(null);\n\nfunction useBannerContext(consumerName: string) {\n const context = React.useContext(BannerContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`Banner\\``);\n }\n return context;\n}\n\nfunction useBanner() {\n const { id, variant, dismissible, onClose } = useBannerContext(\"useBanner\");\n const storeContext = React.useContext(StoreContext);\n\n return React.useMemo(() => {\n const onRemove = id && storeContext ? () => storeContext.onBannerRemove(id) : undefined;\n\n return {\n id,\n variant,\n dismissible,\n onClose,\n onRemove\n };\n }, [id, variant, dismissible, onClose, storeContext]);\n}\n\n/**\n * Chèn `banner` vào `banners` (sửa tại chỗ) theo `priority`: cao đứng trước, cùng mức thì xếp CUỐI\n * nhóm. Một luật cho cả thêm mới (`onBannerAdd`) lẫn đổi `priority` tại chỗ (`onBannerUpdate`) —\n * thượng nguồn viết thẳng trong `onBannerAdd`; tách ra để hai đường không lệch nhau.\n */\nfunction insertByPriority(banners: BannerData[], banner: BannerData) {\n const priority = banner.priority ?? DEFAULT_BANNER_PRIORITY;\n const insertIndex = banners.findIndex((b) => (b.priority ?? DEFAULT_BANNER_PRIORITY) < priority);\n\n if (insertIndex === -1) {\n banners.push(banner);\n } else {\n banners.splice(insertIndex, 0, banner);\n }\n}\n\n/**\n * Hẹn giờ tự đóng theo `duration`, huỷ hẹn cũ của cùng `id` trước; `duration` không dương thì chỉ\n * huỷ. Thượng nguồn viết thẳng trong `onBannerAdd`; tách ra vì `onBannerUpdate` cũng hẹn lại.\n */\nfunction scheduleAutoClose(\n timeouts: Map<string, ReturnType<typeof setTimeout>>,\n id: string,\n duration: number | undefined,\n onElapsed: () => void\n) {\n const pendingTimeoutId = timeouts.get(id);\n if (pendingTimeoutId !== undefined) {\n clearTimeout(pendingTimeoutId);\n timeouts.delete(id);\n }\n\n if (!duration || duration <= 0) return;\n\n const timeoutId = setTimeout(() => {\n onElapsed();\n timeouts.delete(id);\n }, duration);\n timeouts.set(id, timeoutId);\n}\n\ninterface BannersProps {\n children?: React.ReactNode;\n maxVisible?: number;\n side?: BannerSide;\n strategy?: BannerStrategy;\n container?: Element | DocumentFragment | null;\n}\n\nfunction Banners(props: BannersProps) {\n const { children, maxVisible = 1, side = \"top\", strategy = \"fixed\", container: containerProp } = props;\n\n const instanceId = React.useId();\n const stateRef = useLazyRef<StoreState>(() => ({\n banners: [],\n removing: new Set(),\n heights: new Map()\n }));\n const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n const timeoutsRef = useLazyRef<Map<string, ReturnType<typeof setTimeout>>>(() => new Map());\n const idCounterRef = React.useRef(0);\n\n const store: Store = React.useMemo(\n () => ({\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n notify: () => {\n for (const listener of listenersRef.current) {\n listener();\n }\n },\n onBannerAdd: (banner) => {\n /*\n * ⚠ Khác thượng nguồn: bản gốc `crypto.randomUUID()` — hàm này CHỈ có trong secure\n * context (https, localhost). Mở app qua http bằng IP mạng nội bộ (máy dev, môi trường\n * nội bộ) thì nó là `undefined` và mọi lần thêm banner đều throw. Id chỉ cần duy nhất\n * trong một `Banners` → bộ đếm, tiền tố `useId()` cho chắc khi có nhiều `Banners`.\n */\n idCounterRef.current += 1;\n const id = `${instanceId}-${idCounterRef.current}`;\n const newBanner: BannerData = { ...banner, id };\n\n const banners = [...stateRef.current.banners];\n insertByPriority(banners, newBanner);\n\n stateRef.current.banners = banners;\n store.notify();\n\n scheduleAutoClose(timeoutsRef.current, id, banner.duration, () => store.onRemovingChange(id, true));\n\n return id;\n },\n onBannerUpdate: (id, patch) => {\n /*\n * ⚠ Khác thượng nguồn (3.1.0): thượng nguồn không có đường ĐỔI một banner đang trong hàng\n * đợi — `Banner` khai báo chỉ biết gỡ ra rồi thêm lại (xem chú thích trong `Banner`). Ở đây\n * đổi TẠI CHỖ: giữ id, giữ vị trí, không gọi `onDismiss`, không phát lại hiệu ứng vào.\n *\n * - `priority` đổi → dời tới đúng chỗ theo luật của `onBannerAdd` (cuối nhóm của mức mới),\n * như thể vừa được thêm với mức đó.\n * - `duration` đổi GIÁ TRỊ → huỷ hẹn cũ và đếm LẠI trọn `duration` mới tính từ lúc đổi (không\n * trừ phần đã trôi — đổi từ \"không tự đóng\" sang 5 giây mà trừ thời gian đã hiện thì banner\n * sẽ biến mất ngay); `0` / `undefined` → thôi tự đóng. Render lại với CÙNG giá trị thì đồng\n * hồ đang chạy giữ nguyên. Banner đang chạy hiệu ứng ra thì không hẹn lại.\n * - Không có gì đổi thật thì không `notify()` — `Banner` gọi hàm này sau mỗi lần các prop ấy\n * đổi, và `useBanners()` của app không nên render lại vì một lần gọi không đổi gì.\n */\n const banners = stateRef.current.banners;\n const index = banners.findIndex((b) => b.id === id);\n const current = banners[index];\n if (!current) return;\n\n const keys = Object.keys(patch) as (keyof BannerUpdate)[];\n if (keys.every((key) => Object.is(current[key], patch[key]))) return;\n\n const next: BannerData = { ...current, ...patch };\n const nextBanners = [...banners];\n if (Object.is(next.priority ?? DEFAULT_BANNER_PRIORITY, current.priority ?? DEFAULT_BANNER_PRIORITY)) {\n nextBanners[index] = next;\n } else {\n nextBanners.splice(index, 1);\n insertByPriority(nextBanners, next);\n }\n stateRef.current.banners = nextBanners;\n\n if (!Object.is(next.duration, current.duration) && !stateRef.current.removing.has(id)) {\n scheduleAutoClose(timeoutsRef.current, id, next.duration, () => store.onRemovingChange(id, true));\n }\n\n store.notify();\n },\n onBannerRemove: (id) => {\n const banner = stateRef.current.banners.find((b) => b.id === id);\n if (!banner) return;\n\n const timeoutId = timeoutsRef.current.get(id);\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutsRef.current.delete(id);\n }\n\n const newRemoving = new Set(stateRef.current.removing);\n newRemoving.delete(id);\n stateRef.current.removing = newRemoving;\n\n banner.onDismiss?.();\n stateRef.current.banners = stateRef.current.banners.filter((b) => b.id !== id);\n store.notify();\n },\n onBannersClear: () => {\n for (const timeoutId of timeoutsRef.current.values()) {\n clearTimeout(timeoutId);\n }\n timeoutsRef.current.clear();\n stateRef.current.removing = new Set();\n stateRef.current.heights = new Map();\n stateRef.current.banners = [];\n store.notify();\n },\n onRemovingChange: (id, value) => {\n const newSet = new Set(stateRef.current.removing);\n if (value) {\n newSet.add(id);\n } else {\n newSet.delete(id);\n }\n stateRef.current.removing = newSet;\n store.notify();\n },\n onHeightChange: (id, height) => {\n if (stateRef.current.heights.get(id) === height) return;\n const newHeights = new Map(stateRef.current.heights);\n newHeights.set(id, height);\n stateRef.current.heights = newHeights;\n store.notify();\n },\n onHeightRemove: (id) => {\n if (!stateRef.current.heights.has(id)) return;\n const newHeights = new Map(stateRef.current.heights);\n newHeights.delete(id);\n stateRef.current.heights = newHeights;\n store.notify();\n }\n }),\n [instanceId, stateRef, listenersRef, timeoutsRef]\n );\n\n const banners = useStore(store, (state) => state.banners);\n const heights = useStore(store, (state) => state.heights);\n const visibleBanners = banners.slice(0, maxVisible);\n\n /*\n * ⚠ Khác thượng nguồn: bản gốc đọc `globalThis.document?.body` NGAY TRONG render. Ở đây chỉ đọc\n * sau khi mount (cùng khuôn với `ActionBar`) — lượt render phía server và lượt hydrate không\n * chạm `document`.\n */\n const [mounted, setMounted] = React.useState(false);\n useIsomorphicLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n const withPortal = strategy === \"fixed\" || strategy === \"absolute\";\n const container = withPortal ? (containerProp ?? (mounted ? globalThis.document?.body : null) ?? null) : null;\n\n const totalHeight = React.useMemo(() => {\n let total = 0;\n for (const banner of visibleBanners) {\n total += heights.get(banner.id) ?? 0;\n }\n return total;\n }, [visibleBanners, heights]);\n\n const bannerContainer = visibleBanners.length > 0 && (\n <div\n data-slot=\"banner-container\"\n data-side={side}\n data-strategy={strategy}\n className={cn(\n \"pointer-events-none right-0 left-0 isolate z-50\",\n /*\n * ⚠ Khác thượng nguồn: bản gốc ghi `transition` thẳng vào `style`, nên lớp\n * `motion-reduce:transition-none` của từng banner KHÔNG BAO GIỜ thắng được (style nội tuyến\n * luôn thắng lớp) — người bật \"giảm chuyển động\" vẫn thấy banner trượt. Nay thời lượng đi\n * qua `--banner-duration`, bằng 0 khi `prefers-reduced-motion`; banner con thừa kế biến này.\n */\n \"motion-reduce:[--banner-duration:0ms]\",\n strategy === \"fixed\" && \"fixed\",\n strategy === \"static\" && \"relative\",\n strategy === \"sticky\" && \"sticky\",\n strategy === \"absolute\" && \"absolute\",\n side === \"top\" ? \"top-0\" : \"bottom-0\"\n )}\n style={{\n height: totalHeight > 0 ? totalHeight : \"auto\",\n transition: `height var(--banner-duration, ${BANNER_ANIMATION_DURATION}ms) cubic-bezier(0.32, 0.72, 0, 1)`\n }}\n >\n {visibleBanners.map((banner, index) => (\n <BannerImpl key={banner.id} banner={banner} side={side} index={index} />\n ))}\n </div>\n );\n\n return (\n <StoreContext.Provider value={store}>\n {strategy === \"static\" || strategy === \"sticky\" ? (\n <>\n {side === \"top\" && bannerContainer}\n {children}\n {side === \"bottom\" && bannerContainer}\n </>\n ) : (\n <>\n {children}\n {container && bannerContainer && ReactDOM.createPortal(bannerContainer, container)}\n </>\n )}\n </StoreContext.Provider>\n );\n}\n\nfunction useBanners() {\n const store = useStoreContext(\"useBanners\");\n const banners = useStore(store, (state) => state.banners);\n\n return React.useMemo(\n () => ({\n onBannerAdd: store.onBannerAdd,\n onBannerRemove: store.onBannerRemove,\n onBannersClear: store.onBannersClear,\n banners\n }),\n [store, banners]\n );\n}\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc tô bằng bảng màu mặc định của Tailwind (`bg-blue-50\n * text-blue-900`, `bg-red-50`… kèm `dark:`) — đi vòng qua hợp đồng token nên không đổi theo bảng\n * tương phản cao. Ở đây dùng ĐÚNG bộ lớp của `Alert` (alert.tsx) để Banner và Alert cùng tông\n * trông như một họ: nền `bg-card`, chữ + icon ở bậc `-ink` của tone. `Alert` chưa có `info` /\n * `success`; hai tông đó theo cùng khuôn với `warning` / `destructive` của nó.\n *\n * Nền ĐẶC `bg-card` chứ không tint `bg-<tone>/15`: ở chế độ `fixed`/`absolute`, banner nằm ĐÈ lên\n * nội dung trang — một nền trong suốt một phần sẽ cho chữ trang lộ xuyên qua chữ banner.\n *\n * Mô tả (`BannerDescription`) giữ `opacity-90` của bản gốc — trên nền đặc nó tương đương đúng\n * `text-<tone>-ink/90` của `AlertDescription`. Cổng không đo được `opacity`, nên số đo tay (trên\n * `--card`, tiêu đề / mô tả 90%):\n *\n * bảng tương phản cao light dark\n * info 6,68 / 5,31 9,54 / 7,92 ✓\n * success 6,52 / 5,20 10,19 / 8,44 ✓\n * warning 6,51 / 5,17 11,60 / 9,56 ✓\n * destructive 7,15 / 5,77 6,42 / 5,41 ✓\n * bảng mặc định (nợ token đã chấp nhận, cùng mức với `Alert`):\n * destructive 4,38 / 3,81 (light) · 4,06 / 3,50 (dark) · info dark 4,28 / 3,71\n */\nconst bannerVariants = cva(\n \"pointer-events-auto relative flex w-full items-center gap-3 border-b px-4 py-3 text-sm motion-reduce:transition-none\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n info: \"bg-card text-info-ink\",\n success: \"bg-card text-success-ink\",\n warning: \"bg-card text-warning-ink\",\n destructive: \"bg-card text-destructive-ink\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n);\n\n/*\n * ⚠ Khác thượng nguồn (3.1.0): TRẢ FOCUS khi banner rời DOM — cùng khuôn, cùng lý do với\n * `ActionBar` (3.0.1). Banner đóng là cây của nó bị gỡ: `Banner` đứng riêng gỡ ngay, banner trong\n * hàng đợi gỡ sau hiệu ứng ra (`BANNER_ANIMATION_DURATION`). Người dùng bàn phím vừa bấm Enter/Space\n * trên `BannerClose` vì thế thấy focus rơi xuống `<body>`, và Tab kế tiếp chạy lại từ đầu trang\n * (WCAG 2.4.3). Bản gốc để rơi như vậy.\n *\n * Ghi phần tử giữ focus NGAY TRƯỚC khi focus vào banner (`relatedTarget` lúc focus đi vào từ ngoài),\n * rồi trả về đó nếu lúc banner rời DOM focus còn ở trong nó. Trong hàng đợi, focus nằm trong\n * `BannerImpl` suốt hiệu ứng ra — nên hook gắn ở `BannerImpl` và trả lúc NÓ unmount, không phải lúc\n * bấm đóng. Cùng đường ấy phủ luôn mọi cách rời DOM khác khi focus đang ở trong: hết `duration`,\n * `open={false}`, `onBannerRemove`, `onBannersClear`, bị banner ưu tiên cao hơn đẩy ra ngoài\n * `maxVisible`, `Banner` khai báo unmount (đổi route).\n *\n * App tự đưa focus đi thì app thắng: trong `onOpenChange` / `onDismiss` (chạy khi banner còn trong\n * DOM — `onBlur` thấy focus ra ngoài nên không còn gì để trả), trong effect, hay sau đó. Không có\n * phần tử để trả (focus vào banner không từ phần tử nào, hoặc phần tử ấy đã bị gỡ) thì gói KHÔNG\n * đoán: app đưa focus đi trong `onOpenChange` (banner thêm bằng `onBannerAdd`: trong `onDismiss`).\n */\nfunction useFocusReturn<T extends HTMLElement>(\n active: boolean,\n onFocusProp?: React.FocusEventHandler<T>,\n onBlurProp?: React.FocusEventHandler<T>\n) {\n const returnFocusRef = React.useRef<HTMLElement | null>(null);\n const focusInsideRef = React.useRef(false);\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent<T>) => {\n onFocusProp?.(event);\n\n /*\n * Chỉ ghi khi focus ĐI VÀO từ ngoài. Menu mở từ một nút trong banner là con React nhưng nằm ở\n * portal khác: focus từ menu quay về nút có `relatedTarget` ngoài banner (theo DOM), và không\n * được ghi đè phần tử thật sự đứng trước banner.\n */\n const from = event.relatedTarget;\n if (!focusInsideRef.current && from instanceof HTMLElement && !event.currentTarget.contains(from)) {\n returnFocusRef.current = from;\n }\n focusInsideRef.current = true;\n },\n [onFocusProp]\n );\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent<T>) => {\n onBlurProp?.(event);\n\n const to = event.relatedTarget;\n if (!(to instanceof Node) || !event.currentTarget.contains(to)) {\n focusInsideRef.current = false;\n }\n },\n [onBlurProp]\n );\n\n useIsomorphicLayoutEffect(() => {\n if (!active) return;\n\n return () => {\n const returnTarget = returnFocusRef.current;\n const hadFocus = focusInsideRef.current;\n returnFocusRef.current = null;\n focusInsideRef.current = false;\n if (!hadFocus || !returnTarget) return;\n\n /*\n * Cleanup này chạy GIỮA lượt commit, lúc React tạm tắt hệ sự kiện (nên `blur` của phần tử bị\n * gỡ không tới được `onBlur` ở trên — cờ `focusInsideRef` còn đúng), và focus ngay tại đây thì\n * `onFocus` của phần tử nhận focus không chạy. Đợi commit xong rồi mới trả, và chỉ khi focus\n * vẫn đang rơi. Effect của app nằm trong CHÍNH lượt commit này khi nó đồng bộ (bấm chuột, gõ\n * phím) thì chạy ngay cuối commit, TRƯỚC microtask này, và thắng luôn; effect ở một lượt commit\n * SAU (vd state app đổi trong `onOpenChange` của banner trong hàng đợi — lời gọi ấy tới từ hẹn\n * giờ của hiệu ứng ra, không từ sự kiện) thì chạy sau và đè lên. Đằng nào app cũng thắng.\n */\n queueMicrotask(() => {\n const ownerDocument = returnTarget.ownerDocument;\n const activeElement = ownerDocument.activeElement;\n if (activeElement && activeElement !== ownerDocument.body && activeElement.isConnected) return;\n if (!returnTarget.isConnected) return;\n returnTarget.focus({ preventScroll: true });\n });\n };\n }, [active]);\n\n return { onFocus, onBlur };\n}\n\ninterface BannerImplProps {\n banner: BannerData;\n side: BannerSide;\n index: number;\n}\n\nfunction BannerImpl(props: BannerImplProps) {\n const { banner, side, index } = props;\n\n const store = useStoreContext(\"BannerImpl\");\n const removing = useStore(store, (state) => state.removing.has(banner.id));\n const banners = useStore(store, (state) => state.banners);\n const heights = useStore(store, (state) => state.heights);\n\n const [mounted, setMounted] = React.useState(false);\n const bannerRef = React.useRef<HTMLDivElement>(null);\n const offsetBeforeRemoveRef = React.useRef(0);\n\n const offset = React.useMemo(() => {\n let total = 0;\n for (const b of banners) {\n if (b.id === banner.id) break;\n total += heights.get(b.id) ?? 0;\n }\n return total;\n }, [banners, heights, banner.id]);\n\n if (!removing) {\n offsetBeforeRemoveRef.current = offset;\n }\n\n React.useEffect(() => {\n const frame = requestAnimationFrame(() => setMounted(true));\n return () => cancelAnimationFrame(frame);\n }, []);\n\n useIsomorphicLayoutEffect(() => {\n const node = bannerRef.current;\n if (!node || removing) return;\n store.onHeightChange(banner.id, node.getBoundingClientRect().height);\n\n /*\n * ⚠ Khác thượng nguồn (3.1.0): đo LẠI mỗi khi cỡ banner đổi. Bản gốc đo một lần lúc mount — đủ\n * cho nó vì `Banner` khai báo gỡ-rồi-thêm (tức mount lại, đo lại) ở mỗi lần render. Nay nội dung\n * đổi TẠI CHỖ (xem `Banner`), nên chữ dài ra một dòng mà không đo lại thì `Banners` giữ chiều cao\n * cũ: banner kế tiếp đè lên nó, và ở `strategy=\"static\"` khung đẩy nội dung trang sai một đoạn.\n * Kèm theo: khung nhìn hẹp lại làm chữ xuống dòng cũng được đo (bản gốc thì không). Đang chạy\n * hiệu ứng ra thì thôi theo dõi — chiều cao của nó đã được gỡ khỏi tổng (`onHeightRemove`).\n */\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n store.onHeightChange(banner.id, node.getBoundingClientRect().height);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, [store, banner.id, removing]);\n\n React.useEffect(() => {\n if (!removing) return;\n store.onHeightRemove(banner.id);\n const timeoutId = setTimeout(() => store.onBannerRemove(banner.id), BANNER_ANIMATION_DURATION);\n return () => clearTimeout(timeoutId);\n }, [removing, store, banner.id]);\n\n const onClose = React.useCallback(() => store.onRemovingChange(banner.id, true), [store, banner.id]);\n\n const onRemove = React.useCallback(() => store.onBannerRemove(banner.id), [store, banner.id]);\n\n // Trả focus lúc banner rời DOM — tức lúc `BannerImpl` unmount, SAU hiệu ứng ra. Xem `useFocusReturn`.\n const { onFocus, onBlur } = useFocusReturn<HTMLDivElement>(true);\n\n const dismissible = banner.dismissible ?? DEFAULT_BANNER_DISMISSIBLE;\n\n const contextValue = React.useMemo<BannerContextValue>(\n () => ({ id: banner.id, variant: banner.variant, dismissible, onClose }),\n [banner.id, banner.variant, dismissible, onClose]\n );\n\n const renderProps = React.useMemo<BannerRenderProps>(\n () => ({\n id: banner.id,\n variant: banner.variant,\n dismissible,\n onClose,\n onRemove\n }),\n [banner.id, banner.variant, dismissible, onClose, onRemove]\n );\n\n const currentOffset = removing ? offsetBeforeRemoveRef.current : offset;\n const isTop = side === \"top\";\n\n function getTransform() {\n if (!mounted) return isTop ? \"translateY(-100%)\" : \"translateY(100%)\";\n if (removing) {\n return isTop ? `translateY(calc(${currentOffset}px - 100%))` : `translateY(calc(-${currentOffset}px + 100%))`;\n }\n return isTop ? `translateY(${currentOffset}px)` : `translateY(-${currentOffset}px)`;\n }\n\n const opacityDuration = removing ? BANNER_ANIMATION_DURATION / 2 : BANNER_ANIMATION_DURATION;\n\n return (\n <BannerContext.Provider value={contextValue}>\n <div\n role=\"status\"\n aria-live=\"polite\"\n data-slot=\"queued-banner\"\n data-state={removing ? \"closed\" : \"open\"}\n data-mounted={mounted}\n data-removed={removing}\n data-side={side}\n data-front={index === 0}\n data-index={index}\n ref={bannerRef}\n className={bannerVariants({ variant: banner.variant })}\n style={{\n position: \"absolute\",\n [isTop ? \"top\" : \"bottom\"]: 0,\n left: 0,\n right: 0,\n zIndex: removing ? 0 : 50 - index,\n transform: getTransform(),\n opacity: mounted && !removing ? 1 : 0,\n // Thời lượng qua `--banner-duration` (0 khi giảm chuyển động) — xem chú thích ở `Banners`.\n transition: `transform var(--banner-duration, ${BANNER_ANIMATION_DURATION}ms) cubic-bezier(0.32, 0.72, 0, 1), opacity var(--banner-duration, ${opacityDuration}ms) ease`\n }}\n onFocus={onFocus}\n onBlur={onBlur}\n >\n {typeof banner.content === \"function\" ? banner.content(renderProps) : banner.content}\n </div>\n </BannerContext.Provider>\n );\n}\n\n/*\n * Nguồn `children` MỚI NHẤT của một `Banner` khai báo, cho banner của nó trong hàng đợi đọc.\n *\n * ⚠ Khác thượng nguồn (3.1.0): nội dung KHÔNG đi qua store của `Banners`. `children` là JSX mới ở mỗi\n * lần cha render, nên đẩy nó vào store (`onBannerUpdate`) là `notify()` ở MỖI lần render — mọi\n * `useBanners()` render lại theo. Một component vừa đọc `useBanners()` vừa render `<Banner>` thì thành\n * vòng lặp vô tận: store đổi → nó render → `children` mới → store đổi… Thay vào đó, mục trong hàng đợi\n * giữ MỘT phần tử cố định (`DeclaredBannerChildren`) tự đăng ký vào nguồn này, và chỉ đúng phần nội\n * dung ấy render lại khi `children` đổi. `banners[i].content` của một banner khai báo vì thế là phần tử\n * đó — render ở đâu cũng ra nội dung mới nhất.\n */\ninterface ChildrenSource {\n get: () => React.ReactNode;\n set: (children: React.ReactNode) => void;\n subscribe: (listener: () => void) => () => void;\n}\n\nfunction createChildrenSource(initialChildren: React.ReactNode): ChildrenSource {\n let children = initialChildren;\n const listeners = new Set<() => void>();\n\n return {\n get: () => children,\n set: (nextChildren) => {\n if (Object.is(children, nextChildren)) return;\n children = nextChildren;\n for (const listener of listeners) {\n listener();\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n }\n };\n}\n\nfunction DeclaredBannerChildren(props: { source: ChildrenSource }) {\n const { source } = props;\n return React.useSyncExternalStore(source.subscribe, source.get, source.get);\n}\n\ninterface BannerProps extends DivProps, VariantProps<typeof bannerVariants> {\n open?: boolean;\n defaultOpen?: boolean;\n /**\n * Báo `false` khi banner đóng THẬT: `BannerClose`; trong `Banners` thêm hết `duration` hay hàng đợi\n * gỡ nó. Không báo khi render lại, khi unmount, hay khi chính app đặt `open={false}`.\n *\n * Focus: nếu lúc banner rời DOM focus còn ở trong nó, gói trả focus về phần tử giữ focus ngay trước\n * khi vào banner. Muốn đưa focus nơi khác (vd tiêu đề trang) thì làm ngay ở đây — app luôn thắng.\n * Không có phần tử ấy (hoặc nó đã bị gỡ) thì gói không đoán, focus rơi về `<body>`.\n */\n onOpenChange?: (open: boolean) => void;\n onDismiss?: () => void;\n priority?: number;\n duration?: number;\n dismissible?: boolean;\n}\n\nfunction Banner(props: BannerProps) {\n const {\n className,\n variant = \"default\",\n open: openProp,\n defaultOpen = true,\n onOpenChange,\n onDismiss,\n priority,\n duration,\n dismissible = DEFAULT_BANNER_DISMISSIBLE,\n children,\n asChild,\n onFocus: onFocusProp,\n onBlur: onBlurProp,\n ...rootProps\n } = props;\n\n const store = React.useContext(StoreContext);\n\n const isInsideStore = store !== null;\n const isControlled = openProp !== undefined;\n\n const openRef = useLazyRef(() => openProp ?? defaultOpen);\n const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n const bannerIdRef = React.useRef<string | null>(null);\n const onDismissRef = useAsRef(onDismiss);\n const onOpenChangeRef = useAsRef(onOpenChange);\n const isControlledRef = useAsRef(isControlled);\n const queuedDataRef = useAsRef<BannerUpdate>({ variant: variant ?? undefined, priority, dismissible, duration });\n const childrenSourceRef = useLazyRef(() => createChildrenSource(children));\n\n if (isControlled) {\n openRef.current = openProp;\n }\n\n const subscribe = React.useCallback(\n (cb: () => void) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n [listenersRef]\n );\n\n const getSnapshot = React.useCallback(() => openRef.current, [openRef]);\n\n const open = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n // Nội dung mới nhất cho banner trong hàng đợi — xem `ChildrenSource`. Chạy sau MỖI commit: `children`\n // đổi thì chỉ phần nội dung trong hàng đợi render lại, trước lượt sơn kế tiếp.\n useIsomorphicLayoutEffect(() => {\n childrenSourceRef.current.set(children);\n });\n\n // Đổi TẠI CHỖ mục đang trong hàng đợi khi các prop dữ liệu đổi — xem `onBannerUpdate`. Chưa đăng ký\n // (lượt mount, hay đang đóng) thì thôi: effect đăng ký bên dưới đọc giá trị mới nhất qua ref.\n useIsomorphicLayoutEffect(() => {\n const id = bannerIdRef.current;\n if (!store || id === null) return;\n store.onBannerUpdate(id, { variant: variant ?? undefined, priority, dismissible, duration });\n }, [store, variant, priority, dismissible, duration]);\n\n React.useEffect(() => {\n if (!store || !open) return;\n\n /*\n * ⚠ Khác thượng nguồn — ba lỗi của `<Banner>` đặt trong `<Banners>`:\n *\n * 1. Hàm dọn của effect này gỡ banner bằng `onBannerRemove`, mà hàm đó GỌI `onDismiss`. Bản\n * gốc vì thế báo \"người dùng đã đóng\" (`onDismiss` + `onOpenChange(false)`) mỗi khi effect\n * chạy lại — ở bản gốc là MỖI LẦN cha re-render (xem 3) — và cả khi unmount (đổi route) hay\n * `open={false}`. Banner tự đóng, và app lưu \"đã tắt thông báo\" cho một lần người dùng chưa\n * hề bấm. Cờ `isCleaningUp` chặn đúng đường đó; đóng thật (nút X, hết `duration`) vẫn báo\n * như cũ, đúng một lần.\n * 2. Ở chế độ không kiểm soát, đóng qua hàng đợi không hạ `open` của chính `Banner`, nên lần\n * re-render kế tiếp thêm LẠI banner vừa đóng. Nay hạ `open` như `onClose` bên dưới.\n * 3. (3.1.0) Deps của bản gốc có `children` (và variant/priority/dismissible/duration):\n * `children` là JSX mới ở mỗi lần cha render, nên MỖI lần render banner rời hàng đợi rồi vào\n * lại với id mới — phát lại hiệu ứng vào, xuống CUỐI nhóm ưu tiên, `duration` đếm lại từ\n * đầu, state bên trong nội dung mất, focus bên trong rơi. Nay đăng ký MỘT lần cho mỗi chu kỳ\n * mở (deps chỉ còn `store`, `open` và các ref ổn định), đọc dữ liệu mới nhất qua ref; thay\n * đổi sau đó đi qua hai effect bên trên. Đóng rồi mở lại (`open` false → true) là một chu kỳ\n * mới: thêm lại, id mới.\n * Hệ quả: mục đã rời hàng đợi mà `open` vẫn `true` — app kiểm soát `open` nhưng bỏ qua\n * `onOpenChange(false)`, hay sau `onBannersClear()` — ở ngoài cho tới khi `open` qua `false`\n * rồi `true`. Bản cũ đưa nó về ở lần render kế tiếp, tức vào một lúc không ai định trước.\n */\n let isCleaningUp = false;\n\n const id = store.onBannerAdd({\n ...queuedDataRef.current,\n content: <DeclaredBannerChildren source={childrenSourceRef.current} />,\n onDismiss: () => {\n if (isCleaningUp) return;\n if (!isControlledRef.current) {\n openRef.current = false;\n for (const listener of listenersRef.current) {\n listener();\n }\n }\n onDismissRef.current?.();\n onOpenChangeRef.current?.(false);\n }\n });\n bannerIdRef.current = id;\n\n return () => {\n isCleaningUp = true;\n bannerIdRef.current = null;\n store.onBannerRemove(id);\n };\n }, [\n store,\n open,\n queuedDataRef,\n childrenSourceRef,\n isControlledRef,\n openRef,\n listenersRef,\n onDismissRef,\n onOpenChangeRef\n ]);\n\n const onClose = React.useCallback(() => {\n if (!isControlled) {\n openRef.current = false;\n for (const listener of listenersRef.current) {\n listener();\n }\n }\n onOpenChangeRef.current?.(false);\n }, [isControlled, openRef, listenersRef, onOpenChangeRef]);\n\n // Chỉ `Banner` đứng riêng tự vẽ phần tử gốc; trong `Banners` thì `BannerImpl` lo — xem `useFocusReturn`.\n const { onFocus, onBlur } = useFocusReturn(open && !isInsideStore, onFocusProp, onBlurProp);\n\n const contextValue = React.useMemo<BannerContextValue>(\n () => ({\n variant,\n dismissible,\n onClose\n }),\n [variant, dismissible, onClose]\n );\n\n if (!open || isInsideStore) return null;\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <BannerContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"status\"\n aria-live=\"polite\"\n data-slot=\"banner\"\n data-state=\"open\"\n className={cn(bannerVariants({ variant, className }))}\n {...rootProps}\n onFocus={onFocus}\n onBlur={onBlur}\n >\n {children}\n </RootPrimitive>\n </BannerContext.Provider>\n );\n}\n\nfunction BannerIcon(props: DivProps) {\n const { className, asChild, ...iconProps } = props;\n\n const IconPrimitive = asChild ? Slot : \"div\";\n\n return (\n <IconPrimitive\n data-slot=\"banner-icon\"\n className={cn(\"flex shrink-0 items-center [&>svg]:size-4\", className)}\n {...iconProps}\n />\n );\n}\n\nfunction BannerContent(props: DivProps) {\n const { className, asChild, ...contentProps } = props;\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ContentPrimitive\n data-slot=\"banner-content\"\n className={cn(\"flex min-w-0 flex-1 flex-col gap-1\", className)}\n {...contentProps}\n />\n );\n}\n\nfunction BannerTitle(props: React.ComponentProps<\"div\">) {\n const { className, ...titleProps } = props;\n\n return <div data-slot=\"banner-title\" className={cn(\"text-sm leading-none font-medium\", className)} {...titleProps} />;\n}\n\nfunction BannerDescription(props: React.ComponentProps<\"div\">) {\n const { className, ...descriptionProps } = props;\n\n return <div data-slot=\"banner-description\" className={cn(\"text-xs opacity-90\", className)} {...descriptionProps} />;\n}\n\nfunction BannerActions(props: DivProps) {\n const { className, asChild, ...actionsProps } = props;\n\n const ActionsPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ActionsPrimitive\n data-slot=\"banner-actions\"\n className={cn(\"flex items-center gap-2\", className)}\n {...actionsProps}\n />\n );\n}\n\ntype BannerCloseProps = React.ComponentProps<typeof Button>;\n\n/**\n * Nút đóng banner. Không truyền `children` thì vẽ icon X.\n *\n * ⚠ Khác thượng nguồn: có tên cho trình đọc màn hình — `aria-label` mặc định lấy khoá\n * `closeLabel` của namespace `banner` (`ComitorLocaleProvider`), ghi đè bằng chính prop\n * `aria-label`. Bản gốc là `Button` chỉ chứa icon X, tức một nút KHÔNG TÊN. Nếu `children` là\n * CHỮ nhìn thấy được thì truyền `aria-label` trùng với chữ đó (WCAG 2.5.3), hoặc\n * `aria-label={undefined}`.\n */\nfunction BannerClose(props: BannerCloseProps) {\n const { onClick: onClickProp, disabled, children, ...closeProps } = props;\n\n const { dismissible = DEFAULT_BANNER_DISMISSIBLE, onClose } = useBannerContext(\"BannerClose\");\n const t = useComitorMessages(\"banner\");\n\n const isDisabled = disabled ?? !dismissible;\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<CloseElement>) => {\n onClickProp?.(event);\n if (event.defaultPrevented || isDisabled) return;\n onClose?.();\n },\n [onClickProp, isDisabled, onClose]\n );\n\n return (\n <Button\n aria-label={t.closeLabel}\n data-slot=\"banner-close\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={onClick}\n disabled={isDisabled}\n {...closeProps}\n >\n {children ?? <X aria-hidden=\"true\" className=\"size-3.5\" />}\n </Button>\n );\n}\n\nexport {\n Banner,\n BannerActions,\n BannerClose,\n type BannerCloseProps,\n BannerContent,\n type BannerData,\n BannerDescription,\n BannerIcon,\n type BannerProps,\n type BannerRenderProps,\n Banners,\n type BannersProps,\n BannerTitle,\n type BannerVariant,\n useBanner,\n useBanners\n};\n"],"mappings":"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","names":["banners"]}
|
|
@@ -10,6 +10,9 @@ declare const REGEX_CACHE: {
|
|
|
10
10
|
readonly hashPattern: RegExp;
|
|
11
11
|
readonly currencyAtEnd: RegExp;
|
|
12
12
|
readonly percentageChars: RegExp;
|
|
13
|
+
readonly percentageCommaChars: RegExp;
|
|
14
|
+
readonly decimalMark: RegExp;
|
|
15
|
+
readonly decimalMarks: RegExp;
|
|
13
16
|
readonly phone: RegExp;
|
|
14
17
|
readonly ssn: RegExp;
|
|
15
18
|
readonly zipCode: RegExp;
|
|
@@ -61,7 +64,21 @@ declare function applyCurrencyMask(opts: {
|
|
|
61
64
|
currency?: string;
|
|
62
65
|
locale?: string;
|
|
63
66
|
}): string;
|
|
64
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Giá trị thô dạng chuẩn (`"12.5"`) → chuỗi hiển thị (`"12.5%"`).
|
|
69
|
+
*
|
|
70
|
+
* ⚠ Khác thượng nguồn (3.1.0): nhận thêm `locale` (tuỳ chọn) và vẽ dấu thập phân của nó — `vi` →
|
|
71
|
+
* `12,5%`. Không truyền thì `.` như thượng nguồn. Chỉ đổi DẤU: không nhóm nghìn, và `%` luôn dính
|
|
72
|
+
* ngay sau số (`Intl` của de-DE viết `12,5 %`) — để con trỏ và nút xoá chỉ phải biết một dáng chuỗi.
|
|
73
|
+
*/
|
|
74
|
+
declare function applyPercentageMask(value: string, opts?: {
|
|
75
|
+
locale?: string;
|
|
76
|
+
}): string;
|
|
77
|
+
/** Ô phần trăm đang đọc và vẽ dấu `,` — `mask-input.tsx` dùng để chọn cách đặt con trỏ. */
|
|
78
|
+
declare function isDecimalCommaPercentage(opts: {
|
|
79
|
+
mask: MaskPatternKey | MaskPattern | undefined;
|
|
80
|
+
locale?: string;
|
|
81
|
+
}): boolean;
|
|
65
82
|
declare function getUnmaskedValue(opts: {
|
|
66
83
|
value: string;
|
|
67
84
|
currency?: string;
|
|
@@ -86,6 +103,21 @@ declare function getCurrencyCaretPosition(opts: {
|
|
|
86
103
|
oldValue?: string;
|
|
87
104
|
previousUnmasked?: string;
|
|
88
105
|
}): number;
|
|
106
|
+
/**
|
|
107
|
+
* Con trỏ sau khi ô phần trăm ở chế độ dấu phẩy (`isDecimalCommaPercentage`) vẽ lại chuỗi.
|
|
108
|
+
*
|
|
109
|
+
* ⚠ Khác thượng nguồn (3.1.0) — hàm MỚI. `getCurrencyCaretPosition` chỉ đếm CHỮ SỐ trước con trỏ,
|
|
110
|
+
* mà ở đây mặt nạ còn đổi chính dấu thập phân (phím `.` của bàn phím số hiện thành `,`): đếm chữ số
|
|
111
|
+
* thì con trỏ rơi về TRƯỚC dấu vừa gõ, và chữ số kế tiếp chen vào phần nguyên — `12` `.` `5` ra
|
|
112
|
+
* `125,%`. Đây đúng là cái bẫy làm cách chữa tạm "`transform` đổi `,` thành `.`" không dùng được.
|
|
113
|
+
* Ở đây đếm cả dấu thập phân ĐƯỢC GIỮ (dấu đầu tiên — luật của `transform`), nên con trỏ đứng sau nó.
|
|
114
|
+
* Chế độ dấu chấm (en…) vẫn đi `getCurrencyCaretPosition` như cũ.
|
|
115
|
+
*/
|
|
116
|
+
declare function getPercentageCaretPosition(opts: {
|
|
117
|
+
newValue: string;
|
|
118
|
+
oldValue: string;
|
|
119
|
+
oldCursorPosition: number;
|
|
120
|
+
}): number;
|
|
89
121
|
declare function getPatternCaretPosition(opts: {
|
|
90
122
|
newValue: string;
|
|
91
123
|
maskPattern: MaskPattern;
|
|
@@ -95,4 +127,4 @@ declare function getPatternCaretPosition(opts: {
|
|
|
95
127
|
previousUnmasked?: string;
|
|
96
128
|
}): number;
|
|
97
129
|
|
|
98
|
-
export { CURRENCY_PERCENTAGE_SYMBOLS, DEFAULT_CURRENCY, MASK_PATTERNS, type MaskPattern, type MaskPatternKey, NUMERIC_MASK_PATTERNS, REGEX_CACHE, type TransformOptions, applyCurrencyMask, applyMask, applyPercentageMask, fromUnmaskedIndex, getCurrencyCaretPosition, getPatternCaretPosition, getUnmaskedValue, isCurrencyAtEnd, isCurrencyMask, toUnmaskedIndex };
|
|
130
|
+
export { CURRENCY_PERCENTAGE_SYMBOLS, DEFAULT_CURRENCY, MASK_PATTERNS, type MaskPattern, type MaskPatternKey, NUMERIC_MASK_PATTERNS, REGEX_CACHE, type TransformOptions, applyCurrencyMask, applyMask, applyPercentageMask, fromUnmaskedIndex, getCurrencyCaretPosition, getPatternCaretPosition, getPercentageCaretPosition, getUnmaskedValue, isCurrencyAtEnd, isCurrencyMask, isDecimalCommaPercentage, toUnmaskedIndex };
|
|
@@ -8,6 +8,7 @@ const formattersCache = /* @__PURE__ */ new Map();
|
|
|
8
8
|
const currencyAtEndCache = /* @__PURE__ */ new Map();
|
|
9
9
|
const currencySymbolsCache = /* @__PURE__ */ new Map();
|
|
10
10
|
const fractionDigitsCache = /* @__PURE__ */ new Map();
|
|
11
|
+
const decimalSeparatorCache = /* @__PURE__ */ new Map();
|
|
11
12
|
const daysInMonthCache = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
12
13
|
const REGEX_CACHE = {
|
|
13
14
|
digitsOnly: /^\d+$/,
|
|
@@ -18,6 +19,9 @@ const REGEX_CACHE = {
|
|
|
18
19
|
hashPattern: /#/g,
|
|
19
20
|
currencyAtEnd: /\d\s*[^\d\s]+$/,
|
|
20
21
|
percentageChars: /[^\d.]/g,
|
|
22
|
+
percentageCommaChars: /[^\d.,]/g,
|
|
23
|
+
decimalMark: /[.,]/,
|
|
24
|
+
decimalMarks: /[.,]/g,
|
|
21
25
|
phone: /^\d{10}$/,
|
|
22
26
|
ssn: /^\d{9}$/,
|
|
23
27
|
zipCode: /^\d{5}$/,
|
|
@@ -132,6 +136,18 @@ function isCanonicalAmount(value, fractionDigits) {
|
|
|
132
136
|
const fractionPart = dot === -1 ? "" : value.slice(dot + 1);
|
|
133
137
|
return /^\d*$/.test(integerPart) && /^\d*$/.test(fractionPart) && fractionPart.length <= fractionDigits;
|
|
134
138
|
}
|
|
139
|
+
function usesDecimalComma(locale) {
|
|
140
|
+
let decimal = decimalSeparatorCache.get(locale);
|
|
141
|
+
if (decimal === void 0) {
|
|
142
|
+
decimal = ".";
|
|
143
|
+
try {
|
|
144
|
+
decimal = new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value ?? ".";
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
decimalSeparatorCache.set(locale, decimal);
|
|
148
|
+
}
|
|
149
|
+
return decimal === ",";
|
|
150
|
+
}
|
|
135
151
|
function isCurrencyAtEnd(opts) {
|
|
136
152
|
const { locale, currency } = opts;
|
|
137
153
|
const key = `${locale}|${currency}`;
|
|
@@ -294,11 +310,23 @@ const MASK_PATTERNS = {
|
|
|
294
310
|
}
|
|
295
311
|
},
|
|
296
312
|
/**
|
|
297
|
-
* Phần trăm
|
|
313
|
+
* Phần trăm `##.##%`, tối đa 2 chữ số lẻ. Giá trị thô (vế thứ hai của `onValueChange`, và thứ
|
|
314
|
+
* `validate` nhận) LUÔN dùng `.`: `"12.5"`.
|
|
315
|
+
*
|
|
316
|
+
* Dấu thập phân theo `locale` (`3.1.0`): ở locale dùng `,` (vi, de, fr…) ô đọc CẢ `,` lẫn `.` là
|
|
317
|
+
* dấu thập phân — `.` vì bàn phím số của điện thoại vẽ dấu theo ngôn ngữ MÁY, không theo app — và
|
|
318
|
+
* hiện `12,5%`. Locale dùng `.` (en…): như thượng nguồn, `,` bị bỏ qua (`12,5` → `125%`).
|
|
298
319
|
*/
|
|
299
320
|
percentage: {
|
|
300
321
|
pattern: "##.##%",
|
|
301
|
-
transform: (value) => {
|
|
322
|
+
transform: (value, { locale = DEFAULT_LOCALE } = {}) => {
|
|
323
|
+
if (usesDecimalComma(locale)) {
|
|
324
|
+
const cleaned2 = value.replace(REGEX_CACHE.percentageCommaChars, "");
|
|
325
|
+
const markIndex = cleaned2.search(REGEX_CACHE.decimalMark);
|
|
326
|
+
if (markIndex === -1) return cleaned2;
|
|
327
|
+
const fractionPart = cleaned2.slice(markIndex + 1).replace(REGEX_CACHE.decimalMarks, "").slice(0, 2);
|
|
328
|
+
return `${cleaned2.slice(0, markIndex)}.${fractionPart}`;
|
|
329
|
+
}
|
|
302
330
|
const cleaned = value.replace(REGEX_CACHE.percentageChars, "");
|
|
303
331
|
const parts = cleaned.split(".");
|
|
304
332
|
if (parts.length > 2) {
|
|
@@ -375,7 +403,7 @@ function applyMask(opts) {
|
|
|
375
403
|
});
|
|
376
404
|
}
|
|
377
405
|
if (pattern.includes("%")) {
|
|
378
|
-
return applyPercentageMask(cleanValue);
|
|
406
|
+
return applyPercentageMask(cleanValue, { locale: isLocalizedPercentage(mask) ? locale : void 0 });
|
|
379
407
|
}
|
|
380
408
|
if (mask === "ipv4") {
|
|
381
409
|
return cleanValue;
|
|
@@ -442,15 +470,22 @@ function applyCurrencyMask(opts) {
|
|
|
442
470
|
return result;
|
|
443
471
|
}
|
|
444
472
|
}
|
|
445
|
-
function applyPercentageMask(value) {
|
|
473
|
+
function applyPercentageMask(value, opts = {}) {
|
|
446
474
|
if (!value) return "";
|
|
475
|
+
const decimalSeparator = usesDecimalComma(opts.locale ?? DEFAULT_LOCALE) ? "," : ".";
|
|
447
476
|
const parts = value.split(".");
|
|
448
477
|
let result = parts[0] ?? "0";
|
|
449
478
|
if (value.includes(".")) {
|
|
450
|
-
result +=
|
|
479
|
+
result += `${decimalSeparator}${(parts[1] ?? "").substring(0, 2)}`;
|
|
451
480
|
}
|
|
452
481
|
return `${result}%`;
|
|
453
482
|
}
|
|
483
|
+
function isLocalizedPercentage(mask) {
|
|
484
|
+
return mask === "percentage" || typeof mask === "object" && mask.transform === MASK_PATTERNS.percentage.transform;
|
|
485
|
+
}
|
|
486
|
+
function isDecimalCommaPercentage(opts) {
|
|
487
|
+
return isLocalizedPercentage(opts.mask) && usesDecimalComma(opts.locale ?? DEFAULT_LOCALE);
|
|
488
|
+
}
|
|
454
489
|
function getUnmaskedValue(opts) {
|
|
455
490
|
const { value, transform, currency, locale } = opts;
|
|
456
491
|
return transform ? transform(value, { currency, locale }) : value.replace(REGEX_CACHE.nonDigits, "");
|
|
@@ -507,6 +542,20 @@ function getCurrencyCaretPosition(opts) {
|
|
|
507
542
|
}
|
|
508
543
|
return newValue.length;
|
|
509
544
|
}
|
|
545
|
+
function getPercentageCaretPosition(opts) {
|
|
546
|
+
const { newValue, oldValue, oldCursorPosition } = opts;
|
|
547
|
+
const markIndex = oldValue.search(REGEX_CACHE.decimalMark);
|
|
548
|
+
const target = oldValue.slice(0, oldCursorPosition).replace(REGEX_CACHE.nonDigits, "").length + (markIndex !== -1 && markIndex < oldCursorPosition ? 1 : 0);
|
|
549
|
+
if (target === 0) return 0;
|
|
550
|
+
let seen = 0;
|
|
551
|
+
for (let i = 0; i < newValue.length; i++) {
|
|
552
|
+
if (/[\d.,]/.test(newValue.charAt(i))) {
|
|
553
|
+
seen++;
|
|
554
|
+
if (seen === target) return i + 1;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return newValue.length;
|
|
558
|
+
}
|
|
510
559
|
function getPatternCaretPosition(opts) {
|
|
511
560
|
const { newValue, maskPattern, currentUnmasked, oldCursorPosition, oldValue, previousUnmasked } = opts;
|
|
512
561
|
let position = 0;
|
|
@@ -552,9 +601,11 @@ export {
|
|
|
552
601
|
fromUnmaskedIndex,
|
|
553
602
|
getCurrencyCaretPosition,
|
|
554
603
|
getPatternCaretPosition,
|
|
604
|
+
getPercentageCaretPosition,
|
|
555
605
|
getUnmaskedValue,
|
|
556
606
|
isCurrencyAtEnd,
|
|
557
607
|
isCurrencyMask,
|
|
608
|
+
isDecimalCommaPercentage,
|
|
558
609
|
toUnmaskedIndex
|
|
559
610
|
};
|
|
560
611
|
//# sourceMappingURL=mask-input-constants.js.map
|