@comitor/ui 3.0.0 → 3.1.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/README.md +5 -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/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/composite/toast.d.ts +2 -1
- package/dist/components/composite/toast.js +3 -9
- package/dist/components/composite/toast.js.map +1 -1
- package/dist/components/ui/action-bar.js +45 -1
- package/dist/components/ui/action-bar.js.map +1 -1
- package/dist/components/ui/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/circular-progress.js +10 -2
- package/dist/components/ui/circular-progress.js.map +1 -1
- package/dist/components/ui/kanban.js +9 -3
- package/dist/components/ui/kanban.js.map +1 -1
- package/dist/components/ui/key-value.js +70 -76
- package/dist/components/ui/key-value.js.map +1 -1
- package/dist/components/ui/mask-input-constants.d.ts +34 -2
- package/dist/components/ui/mask-input-constants.js +87 -76
- 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 +23 -21
- 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 +125 -35
- package/dist/components/ui/phone-input.js.map +1 -1
- package/dist/components/ui/rating.js +4 -2
- package/dist/components/ui/rating.js.map +1 -1
- package/dist/components/ui/scroll-spy.js +46 -31
- package/dist/components/ui/scroll-spy.js.map +1 -1
- package/dist/components/ui/slider.d.ts +1 -1
- package/dist/components/ui/slider.js +15 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/sonner.js +2 -0
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/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/components/ui/toggle-group.js +12 -1
- package/dist/components/ui/toggle-group.js.map +1 -1
- package/dist/components/ui/tour.js +3 -2
- package/dist/components/ui/tour.js.map +1 -1
- package/dist/data-grid/data-grid-cell-variants.js +4 -4
- 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 +7 -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/locale-provider.d.ts +18 -0
- package/dist/i18n/locale-provider.js.map +1 -1
- package/dist/i18n/namespaces/rating.js +2 -1
- package/dist/i18n/namespaces/rating.js.map +1 -1
- package/dist/i18n/namespaces/search-filter-bar.js +8 -6
- package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
- package/dist/i18n/namespaces/shell.d.ts +4 -0
- package/dist/i18n/namespaces/shell.js +1 -1
- package/dist/i18n/namespaces/shell.js.map +1 -1
- package/dist/i18n/namespaces/table-pagination.js +6 -4
- package/dist/i18n/namespaces/table-pagination.js.map +1 -1
- package/dist/i18n/namespaces/toast.d.ts +3 -2
- package/dist/i18n/namespaces/toast.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/lib/toast-language.d.ts +8 -0
- package/dist/lib/toast-language.js +19 -0
- package/dist/lib/toast-language.js.map +1 -0
- package/dist/shell/app-shell.d.ts +1 -0
- 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/package.json +1 -1
|
@@ -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"]}
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
6
7
|
const CIRCULAR_PROGRESS_NAME = "CircularProgress";
|
|
7
8
|
const INDICATOR_NAME = "CircularProgressIndicator";
|
|
8
9
|
const TRACK_NAME = "CircularProgressTrack";
|
|
@@ -81,6 +82,7 @@ function CircularProgress(props) {
|
|
|
81
82
|
const percentage = getIsValidNumber(value) ? max === min ? 1 : (value - min) / (max - min) : null;
|
|
82
83
|
const labelId = React.useId();
|
|
83
84
|
const valueTextId = React.useId();
|
|
85
|
+
const [hasValueText, setHasValueText] = React.useState(false);
|
|
84
86
|
const contextValue = React.useMemo(
|
|
85
87
|
() => ({
|
|
86
88
|
value,
|
|
@@ -94,7 +96,8 @@ function CircularProgress(props) {
|
|
|
94
96
|
center,
|
|
95
97
|
circumference,
|
|
96
98
|
percentage,
|
|
97
|
-
valueTextId
|
|
99
|
+
valueTextId,
|
|
100
|
+
onValueTextMountChange: setHasValueText
|
|
98
101
|
}),
|
|
99
102
|
[value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]
|
|
100
103
|
);
|
|
@@ -103,7 +106,7 @@ function CircularProgress(props) {
|
|
|
103
106
|
RootPrimitive,
|
|
104
107
|
{
|
|
105
108
|
role: "progressbar",
|
|
106
|
-
"aria-describedby": valueText ? valueTextId : void 0,
|
|
109
|
+
"aria-describedby": valueText && hasValueText ? valueTextId : void 0,
|
|
107
110
|
"aria-labelledby": label ? labelId : void 0,
|
|
108
111
|
"aria-valuemax": max,
|
|
109
112
|
"aria-valuemin": min,
|
|
@@ -233,6 +236,11 @@ function CircularProgressRange(props) {
|
|
|
233
236
|
function CircularProgressValueText(props) {
|
|
234
237
|
const { asChild, className, children, ...valueTextProps } = props;
|
|
235
238
|
const context = useCircularProgressContext(VALUE_TEXT_NAME);
|
|
239
|
+
const { onValueTextMountChange } = context;
|
|
240
|
+
useIsomorphicLayoutEffect(() => {
|
|
241
|
+
onValueTextMountChange(true);
|
|
242
|
+
return () => onValueTextMountChange(false);
|
|
243
|
+
}, [onValueTextMountChange]);
|
|
236
244
|
const ValueTextPrimitive = asChild ? Slot : "span";
|
|
237
245
|
return /* @__PURE__ */ jsx(
|
|
238
246
|
ValueTextPrimitive,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/circular-progress.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/circular-progress) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\nconst CIRCULAR_PROGRESS_NAME = \"CircularProgress\";\nconst INDICATOR_NAME = \"CircularProgressIndicator\";\nconst TRACK_NAME = \"CircularProgressTrack\";\nconst RANGE_NAME = \"CircularProgressRange\";\nconst VALUE_TEXT_NAME = \"CircularProgressValueText\";\n\nconst DEFAULT_MAX = 100;\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc đọc thẳng `process.env.NODE_ENV` ngay trong lúc render. Gói ra ESM\n * từng file, không bundle; ở môi trường không có bundler thay chuỗi đó (ESM thuần trên trình\n * duyệt), `process` không tồn tại → ReferenceError lúc render. Bọc `typeof` để chỗ đó chỉ mất cảnh\n * báo dev chứ không gãy; bundler vẫn thay `process.env.NODE_ENV` và loại nhánh dev như cũ.\n */\nconst IS_DEV = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\";\n\ntype ProgressState = \"indeterminate\" | \"complete\" | \"loading\";\n\nfunction getProgressState(value: number | undefined | null, maxValue: number): ProgressState {\n return value == null ? \"indeterminate\" : value === maxValue ? \"complete\" : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(value: unknown, min: number, max: number): value is number {\n return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n return `${Math.round(percentage)}%`;\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`value\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. \\`value\\` phải là số trong khoảng \\`min\\`…\\`max\\` (tính cả hai đầu), hoặc \\`null\\`/\\`undefined\\` cho tiến trình không xác định. Giá trị sẽ bị kẹp về khoảng hợp lệ.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`max\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. Chỉ nhận số lớn hơn 0 — dùng mặc định ${DEFAULT_MAX}.`;\n}\n\ninterface CircularProgressContextValue {\n value: number | null;\n valueText: string | undefined;\n max: number;\n min: number;\n state: ProgressState;\n radius: number;\n thickness: number;\n size: number;\n center: number;\n circumference: number;\n percentage: number | null;\n valueTextId?: string;\n}\n\nconst CircularProgressContext = React.createContext<CircularProgressContextValue | null>(null);\n\nfunction useCircularProgressContext(consumerName: string) {\n const context = React.useContext(CircularProgressContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${CIRCULAR_PROGRESS_NAME}\\``);\n }\n return context;\n}\n\ninterface CircularProgressProps extends React.ComponentProps<\"div\"> {\n /** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */\n value?: number | null | undefined;\n /**\n * Chuỗi trình đọc màn hình đọc thay cho con số (`aria-valuetext`) và chữ giữa vòng — mặc định\n * phần trăm làm tròn, vd `\"45%\"`.\n */\n getValueText?(value: number, min: number, max: number): string;\n min?: number;\n max?: number;\n /** Đường kính, px. */\n size?: number;\n /** Độ dày nét, px. */\n thickness?: number;\n /**\n * Nhãn HIỆN của vòng, đồng thời là tên của `role=\"progressbar\"`. Không đặt nhãn hiện thì PHẢI\n * truyền `aria-label` — progressbar không tên là lỗi a11y (ARIA 1.2), gói không tự đặt tên\n * chung chung thay app (giống `Progress`).\n */\n label?: string;\n asChild?: boolean;\n}\n\n/**\n * CircularProgress — tiến trình dạng VÒNG, cặp với `Progress` (dạng thanh) của gói.\n * Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),\n * hoặc dùng sẵn `CircularProgressCombined`.\n *\n * Phần đã chạy là `text-primary-ink`, cùng bậc với thanh `Progress`; rãnh xám trung tính. Đổi tông\n * thì truyền bậc INK cho `CircularProgressRange` (vd `className=\"text-success-ink\"`), không phải\n * token tô nền `text-success` — rãnh không cần đổi theo (số đo ở `CircularProgressTrack`).\n *\n * ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong\n * styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).\n */\nfunction CircularProgress(props: CircularProgressProps) {\n const {\n value: valueProp = null,\n getValueText = getDefaultValueText,\n min: minProp = 0,\n max: maxProp,\n size = 48,\n thickness = 4,\n label,\n asChild,\n className,\n children,\n ...rootProps\n } = props;\n\n if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n if (IS_DEV) {\n console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n const min = getIsValidNumber(minProp) ? minProp : 0;\n const max = rawMax <= min ? min + 1 : rawMax;\n\n if (IS_DEV && thickness >= size) {\n console.warn(\n `CircularProgress: \\`thickness\\` (${thickness}) phải nhỏ hơn \\`size\\` (${size}) thì vòng mới vẽ đúng.`\n );\n }\n\n if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n if (IS_DEV) {\n console.error(getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const value = getIsValidValueNumber(valueProp, min, max)\n ? valueProp\n : getIsValidNumber(valueProp) && valueProp > max\n ? max\n : getIsValidNumber(valueProp) && valueProp < min\n ? min\n : null;\n\n const valueText = getIsValidNumber(value) ? getValueText(value, min, max) : undefined;\n const state = getProgressState(value, max);\n const radius = Math.max(0, (size - thickness) / 2);\n const center = size / 2;\n const circumference = 2 * Math.PI * radius;\n\n const percentage = getIsValidNumber(value) ? (max === min ? 1 : (value - min) / (max - min)) : null;\n\n const labelId = React.useId();\n const valueTextId = React.useId();\n\n const contextValue = React.useMemo<CircularProgressContextValue>(\n () => ({\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n valueTextId\n }),\n [value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <CircularProgressContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"progressbar\"\n aria-describedby={valueText ? valueTextId : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-valuemax={max}\n aria-valuemin={min}\n aria-valuenow={getIsValidNumber(value) ? value : undefined}\n aria-valuetext={valueText}\n data-state={state}\n data-value={value ?? undefined}\n data-max={max}\n data-min={min}\n data-percentage={percentage}\n data-slot=\"circular-progress\"\n {...rootProps}\n className={cn(\"relative inline-flex w-fit items-center justify-center\", label && \"flex-col gap-1.5\", className)}\n >\n {/*\n * ⚠ Khác thượng nguồn: bản gốc đặt nhãn làm anh em của vòng NGAY TRONG gốc, trong khi\n * `CircularProgressValueText` là `absolute inset-0` phủ CẢ gốc — có nhãn là chữ \"65%\" đè\n * lên chữ nhãn. Khi có `label`: bọc phần con trong một khung đúng cỡ vòng (`relative`, nên\n * chữ giá trị căn giữa VÒNG), nhãn xuống dưới vòng. Không có `label` thì DOM y như bản gốc.\n */}\n {label ? (\n <div data-slot=\"circular-progress-ring\" className=\"relative inline-flex items-center justify-center\">\n {children}\n </div>\n ) : (\n children\n )}\n {label && (\n <div id={labelId} data-slot=\"circular-progress-label\" className=\"text-sm\">\n {label}\n </div>\n )}\n </RootPrimitive>\n </CircularProgressContext.Provider>\n );\n}\n\nfunction CircularProgressIndicator(props: React.ComponentProps<\"svg\">) {\n const { className, ...indicatorProps } = props;\n\n const context = useCircularProgressContext(INDICATOR_NAME);\n\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n viewBox={`0 0 ${context.size} ${context.size}`}\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-percentage={context.percentage}\n data-slot=\"circular-progress-indicator\"\n width={context.size}\n height={context.size}\n {...indicatorProps}\n className={cn(\"-rotate-90 transform\", className)}\n />\n );\n}\n\nCircularProgressIndicator.displayName = INDICATOR_NAME;\n\nfunction CircularProgressTrack(props: React.ComponentProps<\"circle\">) {\n const { className, ...trackProps } = props;\n\n const context = useCircularProgressContext(TRACK_NAME);\n\n return (\n <circle\n data-state={context.state}\n data-slot=\"circular-progress-track\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n {...trackProps}\n className={cn(\n /*\n * GIỮ NGUYÊN thượng nguồn — rãnh xám trung tính `text-muted-foreground/20`, KHÔNG phải tint\n * `/20` của bậc ink như rãnh `Progress`. Đã đo cả hai; thứ mang THÔNG TIN ở đây là ranh\n * giới cung-đã-chạy ⇄ rãnh (chỗ cung dừng lại chính là giá trị), và rãnh trung tính giữ\n * ranh giới đó TỐT HƠN (bảng tương phản cao, cung `text-primary-ink`):\n *\n * rãnh trung tính tint `primary-ink/20`\n * cung ⇄ rãnh trên --background : 5,03 (L) · 9,65 (D) 4,85 (L) · 7,97 (D)\n * tệ nhất, rãnh trên --muted : 4,71 (L) · 8,00 (D) 4,54 (L) · 6,52 (D)\n * (viền trang trí --border : 2,62 (L) ✗ — loại)\n *\n * Và nó không phụ thuộc tông: app đổi cung sang `text-success-ink`/`text-destructive-ink`\n * thì rãnh vẫn đúng (5,03 · 5,51:1 trên nền trắng) mà không phải đổi theo.\n *\n * Rãnh ⇄ nền trang chỉ ~1,3:1 — CỐ Ý, như rãnh `Progress`: không có cặp màu nào vừa ≥3:1\n * với nhau vừa ≥3:1 với nền trắng (chứng minh ở chú thích `Slider`). Khác `Progress` ở chỗ\n * vòng KHÔNG cần viền khoanh mốc 100%: hình tròn tự khép và cung luôn bắt đầu ở 12 giờ, nên\n * TỈ LỆ đọc được từ góc của cung mà không cần thấy rãnh. Một nét tròn liền, không nét nào\n * cắt nhau, nên màu có alpha không để lộ vệt nối (xem \"Cạm bẫy\" về icon trong CLAUDE.md).\n */\n \"text-muted-foreground/20\",\n className\n )}\n />\n );\n}\n\nfunction CircularProgressRange(props: React.ComponentProps<\"circle\">) {\n const { className, ...rangeProps } = props;\n\n const context = useCircularProgressContext(RANGE_NAME);\n\n const strokeDasharray = context.circumference;\n const strokeDashoffset =\n context.state === \"indeterminate\"\n ? context.circumference * 0.75\n : context.percentage !== null\n ? context.circumference - context.percentage * context.circumference\n : context.circumference;\n\n return (\n <circle\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-slot=\"circular-progress-range\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n strokeDasharray={strokeDasharray}\n strokeDashoffset={strokeDashoffset}\n vectorEffect=\"non-scaling-stroke\"\n {...rangeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN, gold #E8B824 chỉ 1,86:1 trên nền trắng)\n * → `text-primary-ink`, đúng bậc của thanh `Progress`: 6,51:1 (light) · 12,67:1 (dark)\n * trên --background, tệ nhất 6,09:1 · 10,80:1 trên --muted (bảng tương phản cao). Cung là\n * thông tin thị giác (WCAG 1.4.11, 3:1).\n * Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.\n */\n \"origin-center text-primary-ink transition-all duration-300 ease-in-out\",\n context.state === \"indeterminate\" &&\n \"motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none\",\n className\n )}\n />\n );\n}\n\ninterface CircularProgressValueTextProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction CircularProgressValueText(props: CircularProgressValueTextProps) {\n const { asChild, className, children, ...valueTextProps } = props;\n\n const context = useCircularProgressContext(VALUE_TEXT_NAME);\n\n const ValueTextPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ValueTextPrimitive\n id={context.valueTextId}\n data-state={context.state}\n data-slot=\"circular-progress-value-text\"\n {...valueTextProps}\n className={cn(\"absolute inset-0 flex items-center justify-center text-sm font-medium\", className)}\n >\n {children ?? context.valueText}\n </ValueTextPrimitive>\n );\n}\n\nfunction CircularProgressCombined(props: CircularProgressProps) {\n return (\n <CircularProgress {...props}>\n <CircularProgressIndicator>\n <CircularProgressTrack />\n <CircularProgressRange />\n </CircularProgressIndicator>\n <CircularProgressValueText />\n </CircularProgress>\n );\n}\n\nexport {\n CircularProgress,\n CircularProgressCombined,\n CircularProgressIndicator,\n type CircularProgressProps,\n CircularProgressRange,\n CircularProgressTrack,\n CircularProgressValueText,\n type CircularProgressValueTextProps\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/circular-progress.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/circular-progress) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\n\nconst CIRCULAR_PROGRESS_NAME = \"CircularProgress\";\nconst INDICATOR_NAME = \"CircularProgressIndicator\";\nconst TRACK_NAME = \"CircularProgressTrack\";\nconst RANGE_NAME = \"CircularProgressRange\";\nconst VALUE_TEXT_NAME = \"CircularProgressValueText\";\n\nconst DEFAULT_MAX = 100;\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc đọc thẳng `process.env.NODE_ENV` ngay trong lúc render. Gói ra ESM\n * từng file, không bundle; ở môi trường không có bundler thay chuỗi đó (ESM thuần trên trình\n * duyệt), `process` không tồn tại → ReferenceError lúc render. Bọc `typeof` để chỗ đó chỉ mất cảnh\n * báo dev chứ không gãy; bundler vẫn thay `process.env.NODE_ENV` và loại nhánh dev như cũ.\n */\nconst IS_DEV = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\";\n\ntype ProgressState = \"indeterminate\" | \"complete\" | \"loading\";\n\nfunction getProgressState(value: number | undefined | null, maxValue: number): ProgressState {\n return value == null ? \"indeterminate\" : value === maxValue ? \"complete\" : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(value: unknown, min: number, max: number): value is number {\n return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n return `${Math.round(percentage)}%`;\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`value\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. \\`value\\` phải là số trong khoảng \\`min\\`…\\`max\\` (tính cả hai đầu), hoặc \\`null\\`/\\`undefined\\` cho tiến trình không xác định. Giá trị sẽ bị kẹp về khoảng hợp lệ.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`max\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. Chỉ nhận số lớn hơn 0 — dùng mặc định ${DEFAULT_MAX}.`;\n}\n\ninterface CircularProgressContextValue {\n value: number | null;\n valueText: string | undefined;\n max: number;\n min: number;\n state: ProgressState;\n radius: number;\n thickness: number;\n size: number;\n center: number;\n circumference: number;\n percentage: number | null;\n valueTextId?: string;\n /** `CircularProgressValueText` báo nó đang có mặt — xem chú thích ở `aria-describedby`. */\n onValueTextMountChange: (mounted: boolean) => void;\n}\n\nconst CircularProgressContext = React.createContext<CircularProgressContextValue | null>(null);\n\nfunction useCircularProgressContext(consumerName: string) {\n const context = React.useContext(CircularProgressContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${CIRCULAR_PROGRESS_NAME}\\``);\n }\n return context;\n}\n\ninterface CircularProgressProps extends React.ComponentProps<\"div\"> {\n /** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */\n value?: number | null | undefined;\n /**\n * Chuỗi trình đọc màn hình đọc thay cho con số (`aria-valuetext`) và chữ giữa vòng — mặc định\n * phần trăm làm tròn, vd `\"45%\"`.\n */\n getValueText?(value: number, min: number, max: number): string;\n min?: number;\n max?: number;\n /** Đường kính, px. */\n size?: number;\n /** Độ dày nét, px. */\n thickness?: number;\n /**\n * Nhãn HIỆN của vòng, đồng thời là tên của `role=\"progressbar\"`. Không đặt nhãn hiện thì PHẢI\n * truyền `aria-label` — progressbar không tên là lỗi a11y (ARIA 1.2), gói không tự đặt tên\n * chung chung thay app (giống `Progress`).\n */\n label?: string;\n asChild?: boolean;\n}\n\n/**\n * CircularProgress — tiến trình dạng VÒNG, cặp với `Progress` (dạng thanh) của gói.\n * Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),\n * hoặc dùng sẵn `CircularProgressCombined`.\n *\n * Phần đã chạy là `text-primary-ink`, cùng bậc với thanh `Progress`; rãnh xám trung tính. Đổi tông\n * thì truyền bậc INK cho `CircularProgressRange` (vd `className=\"text-success-ink\"`), không phải\n * token tô nền `text-success` — rãnh không cần đổi theo (số đo ở `CircularProgressTrack`).\n *\n * ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong\n * styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).\n */\nfunction CircularProgress(props: CircularProgressProps) {\n const {\n value: valueProp = null,\n getValueText = getDefaultValueText,\n min: minProp = 0,\n max: maxProp,\n size = 48,\n thickness = 4,\n label,\n asChild,\n className,\n children,\n ...rootProps\n } = props;\n\n if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n if (IS_DEV) {\n console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n const min = getIsValidNumber(minProp) ? minProp : 0;\n const max = rawMax <= min ? min + 1 : rawMax;\n\n if (IS_DEV && thickness >= size) {\n console.warn(\n `CircularProgress: \\`thickness\\` (${thickness}) phải nhỏ hơn \\`size\\` (${size}) thì vòng mới vẽ đúng.`\n );\n }\n\n if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n if (IS_DEV) {\n console.error(getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const value = getIsValidValueNumber(valueProp, min, max)\n ? valueProp\n : getIsValidNumber(valueProp) && valueProp > max\n ? max\n : getIsValidNumber(valueProp) && valueProp < min\n ? min\n : null;\n\n const valueText = getIsValidNumber(value) ? getValueText(value, min, max) : undefined;\n const state = getProgressState(value, max);\n const radius = Math.max(0, (size - thickness) / 2);\n const center = size / 2;\n const circumference = 2 * Math.PI * radius;\n\n const percentage = getIsValidNumber(value) ? (max === min ? 1 : (value - min) / (max - min)) : null;\n\n const labelId = React.useId();\n const valueTextId = React.useId();\n const [hasValueText, setHasValueText] = React.useState(false);\n\n const contextValue = React.useMemo<CircularProgressContextValue>(\n () => ({\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n valueTextId,\n onValueTextMountChange: setHasValueText\n }),\n [value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <CircularProgressContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"progressbar\"\n /*\n * ⚠ Khác thượng nguồn (3.0.1): chỉ trỏ tới phần chữ giá trị khi `CircularProgressValueText`\n * THẬT SỰ có trên trang. Bản gốc luôn đặt `aria-describedby` khi có giá trị — vòng không vẽ\n * chữ (chỉ vòng, hay nhãn ở chỗ khác) thì thuộc tính trỏ vào một id không phần tử nào mang\n * (axe: \"aria-describedby references an element that does not exist\"). Giá trị vẫn đi qua\n * `aria-valuetext` như cũ.\n */\n aria-describedby={valueText && hasValueText ? valueTextId : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-valuemax={max}\n aria-valuemin={min}\n aria-valuenow={getIsValidNumber(value) ? value : undefined}\n aria-valuetext={valueText}\n data-state={state}\n data-value={value ?? undefined}\n data-max={max}\n data-min={min}\n data-percentage={percentage}\n data-slot=\"circular-progress\"\n {...rootProps}\n className={cn(\"relative inline-flex w-fit items-center justify-center\", label && \"flex-col gap-1.5\", className)}\n >\n {/*\n * ⚠ Khác thượng nguồn: bản gốc đặt nhãn làm anh em của vòng NGAY TRONG gốc, trong khi\n * `CircularProgressValueText` là `absolute inset-0` phủ CẢ gốc — có nhãn là chữ \"65%\" đè\n * lên chữ nhãn. Khi có `label`: bọc phần con trong một khung đúng cỡ vòng (`relative`, nên\n * chữ giá trị căn giữa VÒNG), nhãn xuống dưới vòng. Không có `label` thì DOM y như bản gốc.\n */}\n {label ? (\n <div data-slot=\"circular-progress-ring\" className=\"relative inline-flex items-center justify-center\">\n {children}\n </div>\n ) : (\n children\n )}\n {label && (\n <div id={labelId} data-slot=\"circular-progress-label\" className=\"text-sm\">\n {label}\n </div>\n )}\n </RootPrimitive>\n </CircularProgressContext.Provider>\n );\n}\n\nfunction CircularProgressIndicator(props: React.ComponentProps<\"svg\">) {\n const { className, ...indicatorProps } = props;\n\n const context = useCircularProgressContext(INDICATOR_NAME);\n\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n viewBox={`0 0 ${context.size} ${context.size}`}\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-percentage={context.percentage}\n data-slot=\"circular-progress-indicator\"\n width={context.size}\n height={context.size}\n {...indicatorProps}\n className={cn(\"-rotate-90 transform\", className)}\n />\n );\n}\n\nCircularProgressIndicator.displayName = INDICATOR_NAME;\n\nfunction CircularProgressTrack(props: React.ComponentProps<\"circle\">) {\n const { className, ...trackProps } = props;\n\n const context = useCircularProgressContext(TRACK_NAME);\n\n return (\n <circle\n data-state={context.state}\n data-slot=\"circular-progress-track\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n {...trackProps}\n className={cn(\n /*\n * GIỮ NGUYÊN thượng nguồn — rãnh xám trung tính `text-muted-foreground/20`, KHÔNG phải tint\n * `/20` của bậc ink như rãnh `Progress`. Đã đo cả hai; thứ mang THÔNG TIN ở đây là ranh\n * giới cung-đã-chạy ⇄ rãnh (chỗ cung dừng lại chính là giá trị), và rãnh trung tính giữ\n * ranh giới đó TỐT HƠN (bảng tương phản cao, cung `text-primary-ink`):\n *\n * rãnh trung tính tint `primary-ink/20`\n * cung ⇄ rãnh trên --background : 5,03 (L) · 9,65 (D) 4,85 (L) · 7,97 (D)\n * tệ nhất, rãnh trên --muted : 4,71 (L) · 8,00 (D) 4,54 (L) · 6,52 (D)\n * (viền trang trí --border : 2,62 (L) ✗ — loại)\n *\n * Và nó không phụ thuộc tông: app đổi cung sang `text-success-ink`/`text-destructive-ink`\n * thì rãnh vẫn đúng (5,03 · 5,51:1 trên nền trắng) mà không phải đổi theo.\n *\n * Rãnh ⇄ nền trang chỉ ~1,3:1 — CỐ Ý, như rãnh `Progress`: không có cặp màu nào vừa ≥3:1\n * với nhau vừa ≥3:1 với nền trắng (chứng minh ở chú thích `Slider`). Khác `Progress` ở chỗ\n * vòng KHÔNG cần viền khoanh mốc 100%: hình tròn tự khép và cung luôn bắt đầu ở 12 giờ, nên\n * TỈ LỆ đọc được từ góc của cung mà không cần thấy rãnh. Một nét tròn liền, không nét nào\n * cắt nhau, nên màu có alpha không để lộ vệt nối (xem \"Cạm bẫy\" về icon trong CLAUDE.md).\n */\n \"text-muted-foreground/20\",\n className\n )}\n />\n );\n}\n\nfunction CircularProgressRange(props: React.ComponentProps<\"circle\">) {\n const { className, ...rangeProps } = props;\n\n const context = useCircularProgressContext(RANGE_NAME);\n\n const strokeDasharray = context.circumference;\n const strokeDashoffset =\n context.state === \"indeterminate\"\n ? context.circumference * 0.75\n : context.percentage !== null\n ? context.circumference - context.percentage * context.circumference\n : context.circumference;\n\n return (\n <circle\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-slot=\"circular-progress-range\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n strokeDasharray={strokeDasharray}\n strokeDashoffset={strokeDashoffset}\n vectorEffect=\"non-scaling-stroke\"\n {...rangeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN, gold #E8B824 chỉ 1,86:1 trên nền trắng)\n * → `text-primary-ink`, đúng bậc của thanh `Progress`: 6,51:1 (light) · 12,67:1 (dark)\n * trên --background, tệ nhất 6,09:1 · 10,80:1 trên --muted (bảng tương phản cao). Cung là\n * thông tin thị giác (WCAG 1.4.11, 3:1).\n * Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.\n */\n \"origin-center text-primary-ink transition-all duration-300 ease-in-out\",\n context.state === \"indeterminate\" &&\n \"motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none\",\n className\n )}\n />\n );\n}\n\ninterface CircularProgressValueTextProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction CircularProgressValueText(props: CircularProgressValueTextProps) {\n const { asChild, className, children, ...valueTextProps } = props;\n\n const context = useCircularProgressContext(VALUE_TEXT_NAME);\n const { onValueTextMountChange } = context;\n\n useIsomorphicLayoutEffect(() => {\n onValueTextMountChange(true);\n return () => onValueTextMountChange(false);\n }, [onValueTextMountChange]);\n\n const ValueTextPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ValueTextPrimitive\n id={context.valueTextId}\n data-state={context.state}\n data-slot=\"circular-progress-value-text\"\n {...valueTextProps}\n className={cn(\"absolute inset-0 flex items-center justify-center text-sm font-medium\", className)}\n >\n {children ?? context.valueText}\n </ValueTextPrimitive>\n );\n}\n\nfunction CircularProgressCombined(props: CircularProgressProps) {\n return (\n <CircularProgress {...props}>\n <CircularProgressIndicator>\n <CircularProgressTrack />\n <CircularProgressRange />\n </CircularProgressIndicator>\n <CircularProgressValueText />\n </CircularProgress>\n );\n}\n\nexport {\n CircularProgress,\n CircularProgressCombined,\n CircularProgressIndicator,\n type CircularProgressProps,\n CircularProgressRange,\n CircularProgressTrack,\n CircularProgressValueText,\n type CircularProgressValueTextProps\n};\n"],"mappings":"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","names":[]}
|
|
@@ -467,7 +467,6 @@ function KanbanBoard(props) {
|
|
|
467
467
|
children: /* @__PURE__ */ jsx(
|
|
468
468
|
BoardPrimitive,
|
|
469
469
|
{
|
|
470
|
-
"aria-orientation": context.orientation,
|
|
471
470
|
"data-orientation": context.orientation,
|
|
472
471
|
"data-slot": "kanban-board",
|
|
473
472
|
...boardProps,
|
|
@@ -611,7 +610,8 @@ function KanbanColumnHandle(props) {
|
|
|
611
610
|
...isDisabled ? {} : columnContext.listeners,
|
|
612
611
|
ref: composedRef,
|
|
613
612
|
className: cn(
|
|
614
|
-
|
|
613
|
+
// `touch-none`: xem `KanbanItemHandle`.
|
|
614
|
+
"touch-none select-none disabled:pointer-events-none disabled:opacity-50",
|
|
615
615
|
context.flatCursor ? "cursor-default" : "cursor-grab data-dragging:cursor-grabbing",
|
|
616
616
|
className
|
|
617
617
|
),
|
|
@@ -732,7 +732,13 @@ function KanbanItemHandle(props) {
|
|
|
732
732
|
...isDisabled ? {} : itemContext.listeners,
|
|
733
733
|
ref: composedRef,
|
|
734
734
|
className: cn(
|
|
735
|
-
|
|
735
|
+
/*
|
|
736
|
+
* ⚠ Khác thượng nguồn (3.0.1): `touch-none` trên tay cầm. `KanbanItem asHandle` (cả thẻ là tay
|
|
737
|
+
* cầm) đã có nó, tay cầm RỜI thì không — trên màn cảm ứng, vuốt bắt đầu từ tay cầm bị trình
|
|
738
|
+
* duyệt nhận làm cuộn trang, cảm biến chạm của dnd-kit mất sự kiện và thẻ không nhấc lên.
|
|
739
|
+
* Trang Patterns của comitor-ds từng phải tự thêm lớp này cho từng tay cầm.
|
|
740
|
+
*/
|
|
741
|
+
"touch-none select-none disabled:pointer-events-none disabled:opacity-50",
|
|
736
742
|
context.flatCursor ? "cursor-default" : "cursor-grab data-dragging:cursor-grabbing",
|
|
737
743
|
className
|
|
738
744
|
),
|