@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
|
@@ -89,15 +89,19 @@ function ScrollSpy(props) {
|
|
|
89
89
|
};
|
|
90
90
|
}, [listenersRef, stateRef, onValueChangeRef]);
|
|
91
91
|
const sectionMapRef = React.useRef(/* @__PURE__ */ new Map());
|
|
92
|
+
const sectionObserverRef = React.useRef(null);
|
|
92
93
|
const isScrollingRef = React.useRef(false);
|
|
93
94
|
const rafIdRef = React.useRef(null);
|
|
94
95
|
const isMountedRef = React.useRef(false);
|
|
95
96
|
const scrollTimeoutRef = React.useRef(null);
|
|
96
97
|
const onSectionRegister = React.useCallback((id, element) => {
|
|
97
98
|
sectionMapRef.current.set(id, element);
|
|
99
|
+
sectionObserverRef.current?.observe(element);
|
|
98
100
|
}, []);
|
|
99
|
-
const onSectionUnregister = React.useCallback((id) => {
|
|
100
|
-
sectionMapRef.current
|
|
101
|
+
const onSectionUnregister = React.useCallback((id, element) => {
|
|
102
|
+
const sectionMap = sectionMapRef.current;
|
|
103
|
+
if (sectionMap.get(id) === element) sectionMap.delete(id);
|
|
104
|
+
sectionObserverRef.current?.unobserve(element);
|
|
101
105
|
}, []);
|
|
102
106
|
const onScrollToSection = React.useCallback(
|
|
103
107
|
(sectionId) => {
|
|
@@ -151,45 +155,57 @@ function ScrollSpy(props) {
|
|
|
151
155
|
}, [value, store, onScrollToSectionRef]);
|
|
152
156
|
useIsomorphicLayoutEffect(() => {
|
|
153
157
|
const sectionMap = sectionMapRef.current;
|
|
154
|
-
if (sectionMap.size === 0) return;
|
|
155
158
|
const observerRootMargin = rootMargin ?? `${-offset}px 0px -70% 0px`;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
let observer = null;
|
|
160
|
+
const onIntersect = (entries) => {
|
|
161
|
+
if (isScrollingRef.current) return;
|
|
162
|
+
if (rafIdRef.current !== null) {
|
|
163
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
164
|
+
}
|
|
165
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
166
|
+
const intersecting = entries.filter((entry) => entry.isIntersecting);
|
|
167
|
+
if (intersecting.length === 0) return;
|
|
168
|
+
const topmost = intersecting.reduce((prev, curr) => {
|
|
169
|
+
return curr.boundingClientRect.top < prev.boundingClientRect.top ? curr : prev;
|
|
170
|
+
});
|
|
171
|
+
const id = topmost.target.id;
|
|
172
|
+
if (id && sectionMap.has(id)) {
|
|
173
|
+
store.setState("value", id);
|
|
161
174
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
store.setState("value", id);
|
|
171
|
-
}
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const sectionObserver = {
|
|
178
|
+
observe: (element) => {
|
|
179
|
+
observer ??= new IntersectionObserver(onIntersect, {
|
|
180
|
+
root: scrollContainer,
|
|
181
|
+
rootMargin: observerRootMargin,
|
|
182
|
+
threshold
|
|
172
183
|
});
|
|
184
|
+
observer.observe(element);
|
|
173
185
|
},
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
rootMargin: observerRootMargin,
|
|
177
|
-
threshold
|
|
186
|
+
unobserve: (element) => {
|
|
187
|
+
observer?.unobserve(element);
|
|
178
188
|
}
|
|
179
|
-
|
|
189
|
+
};
|
|
190
|
+
sectionObserverRef.current = sectionObserver;
|
|
180
191
|
for (const element of sectionMap.values()) {
|
|
181
|
-
|
|
192
|
+
sectionObserver.observe(element);
|
|
182
193
|
}
|
|
183
194
|
return () => {
|
|
184
|
-
|
|
195
|
+
sectionObserverRef.current = null;
|
|
196
|
+
observer?.disconnect();
|
|
185
197
|
if (rafIdRef.current !== null) {
|
|
186
198
|
cancelAnimationFrame(rafIdRef.current);
|
|
187
199
|
}
|
|
200
|
+
};
|
|
201
|
+
}, [offset, rootMargin, threshold, scrollContainer]);
|
|
202
|
+
React.useEffect(() => {
|
|
203
|
+
return () => {
|
|
188
204
|
if (scrollTimeoutRef.current !== null) {
|
|
189
205
|
clearTimeout(scrollTimeoutRef.current);
|
|
190
206
|
}
|
|
191
207
|
};
|
|
192
|
-
}, [
|
|
208
|
+
}, []);
|
|
193
209
|
const contextValue = React.useMemo(
|
|
194
210
|
() => ({
|
|
195
211
|
dir,
|
|
@@ -304,16 +320,15 @@ function ScrollSpyViewport(props) {
|
|
|
304
320
|
function ScrollSpySection(props) {
|
|
305
321
|
const { asChild, ref, value, ...sectionProps } = props;
|
|
306
322
|
const { orientation, onSectionRegister, onSectionUnregister } = useScrollSpyContext(SECTION_NAME);
|
|
307
|
-
const
|
|
308
|
-
const composedRef = useComposedRefs(ref,
|
|
323
|
+
const [element, setElement] = React.useState(null);
|
|
324
|
+
const composedRef = useComposedRefs(ref, setElement);
|
|
309
325
|
useIsomorphicLayoutEffect(() => {
|
|
310
|
-
const element = sectionRef.current;
|
|
311
326
|
if (!element || !value) return;
|
|
312
327
|
onSectionRegister(value, element);
|
|
313
328
|
return () => {
|
|
314
|
-
onSectionUnregister(value);
|
|
329
|
+
onSectionUnregister(value, element);
|
|
315
330
|
};
|
|
316
|
-
}, [value, onSectionRegister, onSectionUnregister]);
|
|
331
|
+
}, [element, value, onSectionRegister, onSectionUnregister]);
|
|
317
332
|
const SectionPrimitive = asChild ? Slot : "div";
|
|
318
333
|
return /* @__PURE__ */ jsx(
|
|
319
334
|
SectionPrimitive,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/scroll-spy.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/scroll-spy) — 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: trang cài đặt dài và trang chi tiết bản ghi (các mục của một liên hệ CRM) có\n * mục lục điều hướng trong trang.\n *\n * ⚠ `orientation` giữ NGHĨA của thượng nguồn — nó nói về hướng xếp của KHUNG, không phải của danh\n * sách link: `horizontal` (mặc định) = mục lục đứng bên cạnh nội dung, link xếp DỌC; `vertical` =\n * mục lục nằm trên nội dung, link xếp NGANG.\n */\n\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\n\nconst ROOT_NAME = \"ScrollSpy\";\nconst NAV_NAME = \"ScrollSpyNav\";\nconst LINK_NAME = \"ScrollSpyLink\";\nconst VIEWPORT_NAME = \"ScrollSpyViewport\";\nconst SECTION_NAME = \"ScrollSpySection\";\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ntype LinkElement = React.ComponentRef<typeof ScrollSpyLink>;\ntype SectionElement = React.ComponentRef<typeof ScrollSpySection>;\n\nfunction getDefaultScrollBehavior(): ScrollBehavior {\n if (typeof window === \"undefined\") return \"smooth\";\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ? \"auto\" : \"smooth\";\n}\n\ninterface StoreState {\n value: string;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStore<T>(selector: (state: StoreState) => T, ogStore?: Store | null): T {\n const contextStore = React.useContext(StoreContext);\n\n const store = ogStore ?? contextStore;\n\n if (!store) {\n throw new Error(`\\`useStore\\` phải được gọi bên trong \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ScrollSpyContextValue {\n offset: number;\n scrollBehavior: ScrollBehavior | undefined;\n dir: Direction;\n orientation: Orientation;\n scrollContainer: HTMLElement | null;\n isScrollingRef: React.RefObject<boolean>;\n onSectionRegister: (id: string, element: SectionElement) => void;\n onSectionUnregister: (id: string) => void;\n onScrollToSection: (sectionId: string) => void;\n}\n\nconst ScrollSpyContext = React.createContext<ScrollSpyContextValue | null>(null);\n\nfunction useScrollSpyContext(consumerName: string) {\n const context = React.useContext(ScrollSpyContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface ScrollSpyProps extends React.ComponentProps<\"div\"> {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n rootMargin?: string;\n threshold?: number | number[];\n offset?: number;\n /**\n * Kiểu cuộn khi bấm link. Không truyền thì `\"smooth\"`, hoặc `\"auto\"` nếu người dùng bật giảm\n * chuyển động (`prefers-reduced-motion`).\n */\n scrollBehavior?: ScrollBehavior;\n scrollContainer?: HTMLElement | null;\n dir?: Direction;\n orientation?: Orientation;\n asChild?: boolean;\n}\n\nfunction ScrollSpy(props: ScrollSpyProps) {\n const {\n value,\n defaultValue,\n onValueChange,\n rootMargin,\n threshold = 0.1,\n offset = 0,\n /*\n * ⚠ Khác thượng nguồn: bản gốc tính mặc định NGAY TRONG render (`scrollBehavior =\n * getDefaultScrollBehavior()` → `window.matchMedia` mỗi lượt render). Ở đây để `undefined` và\n * chỉ phân giải lúc cuộn thật — không chạm `window` khi render, và đọc đúng thiết lập giảm\n * chuyển động TẠI THỜI ĐIỂM bấm chứ không phải lúc render gần nhất.\n */\n scrollBehavior,\n scrollContainer = null,\n dir: dirProp,\n orientation = \"horizontal\",\n asChild,\n className,\n ...rootProps\n } = props;\n\n const dir = useDirection(dirProp);\n\n const stateRef = useLazyRef<StoreState>(() => ({\n value: value ?? defaultValue ?? \"\"\n }));\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const onValueChangeRef = useAsRef(onValueChange);\n\n const store = React.useMemo<Store>(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => {\n return stateRef.current;\n },\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n\n stateRef.current[key] = value;\n\n if (key === \"value\" && value) {\n onValueChangeRef.current?.(value);\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n }\n };\n }, [listenersRef, stateRef, onValueChangeRef]);\n\n const sectionMapRef = React.useRef(new Map<string, Element>());\n const isScrollingRef = React.useRef(false);\n const rafIdRef = React.useRef<number | null>(null);\n const isMountedRef = React.useRef(false);\n const scrollTimeoutRef = React.useRef<number | null>(null);\n\n const onSectionRegister = React.useCallback((id: string, element: SectionElement) => {\n sectionMapRef.current.set(id, element);\n }, []);\n\n const onSectionUnregister = React.useCallback((id: string) => {\n sectionMapRef.current.delete(id);\n }, []);\n\n const onScrollToSection = React.useCallback(\n (sectionId: string) => {\n /*\n * ⚠ Khác thượng nguồn: `CSS.escape` quanh id. Bản gốc ghép thẳng `#${sectionId}` vào\n * `querySelector`, nên id bắt đầu bằng chữ số (\"2024-hop-dong\") hay chứa ký tự đặc biệt\n * (id của `useId()` như \":r1:\") làm nó THROW `SyntaxError` ngay khi bấm link.\n */\n const section = scrollContainer\n ? scrollContainer.querySelector(`#${CSS.escape(sectionId)}`)\n : document.getElementById(sectionId);\n\n if (!section) {\n store.setState(\"value\", sectionId);\n return;\n }\n\n // Bật cờ để observer không ghi đè giá trị trong lúc đang cuộn theo lệnh.\n isScrollingRef.current = true;\n store.setState(\"value\", sectionId);\n\n const behavior = scrollBehavior ?? getDefaultScrollBehavior();\n\n if (scrollContainer) {\n const containerRect = scrollContainer.getBoundingClientRect();\n const sectionRect = section.getBoundingClientRect();\n const scrollTop = scrollContainer.scrollTop;\n const offsetPosition = sectionRect.top - containerRect.top + scrollTop - offset;\n\n scrollContainer.scrollTo({\n top: offsetPosition,\n behavior\n });\n } else {\n const sectionPosition = section.getBoundingClientRect().top;\n const offsetPosition = sectionPosition + window.scrollY - offset;\n\n window.scrollTo({\n top: offsetPosition,\n behavior\n });\n }\n\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n\n scrollTimeoutRef.current = window.setTimeout(() => {\n isScrollingRef.current = false;\n }, 500);\n },\n [scrollContainer, offset, scrollBehavior, store]\n );\n\n /*\n * ⚠ Khác thượng nguồn: chỉ cuộn theo lệnh khi `value` đổi TỪ BÊN NGOÀI. Bản gốc cuộn mỗi khi\n * effect chạy lại sau lần đầu, với hai hệ quả đã đo trên trình duyệt:\n * · StrictMode chạy effect hai lần lúc mount → lần hai cuộn luôn CẢ TRANG tới mục (scrollY\n * 0 → ~5050) vì `scrollContainer` của app lúc đó còn `null`. Next dev bật StrictMode sẵn.\n * · Chế độ controlled tự kéo giật: observer đặt giá trị → `onValueChange` → app trả về prop\n * `value` → effect cuộn tới ĐẦU mục đó, ngay giữa lúc người dùng đang cuộn.\n * Luật mới: lúc mount không cuộn (đúng ý nhánh mount của bản gốc); về sau, `value` trùng với giá\n * trị store đang giữ nghĩa là nó chỉ là tiếng vọng của chính observer/link → không cuộn.\n * `defaultValue` đã vào store từ lúc khởi tạo (`stateRef`), không cần đồng bộ thêm.\n */\n const prevValuePropRef = React.useRef<string | undefined>(undefined);\n const onScrollToSectionRef = useAsRef(onScrollToSection);\n\n useIsomorphicLayoutEffect(() => {\n const previous = prevValuePropRef.current;\n prevValuePropRef.current = value;\n if (value === undefined || value === previous) return;\n\n if (previous === undefined || !isMountedRef.current) {\n isMountedRef.current = true;\n store.setState(\"value\", value);\n return;\n }\n\n if (value === store.getState().value) return;\n onScrollToSectionRef.current(value);\n }, [value, store, onScrollToSectionRef]);\n\n useIsomorphicLayoutEffect(() => {\n const sectionMap = sectionMapRef.current;\n if (sectionMap.size === 0) return;\n\n const observerRootMargin = rootMargin ?? `${-offset}px 0px -70% 0px`;\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (isScrollingRef.current) return;\n\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n }\n\n rafIdRef.current = requestAnimationFrame(() => {\n const intersecting = entries.filter((entry) => entry.isIntersecting);\n\n if (intersecting.length === 0) return;\n\n const topmost = intersecting.reduce((prev, curr) => {\n return curr.boundingClientRect.top < prev.boundingClientRect.top ? curr : prev;\n });\n\n const id = topmost.target.id;\n if (id && sectionMap.has(id)) {\n store.setState(\"value\", id);\n }\n });\n },\n {\n root: scrollContainer,\n rootMargin: observerRootMargin,\n threshold\n }\n );\n\n for (const element of sectionMap.values()) {\n observer.observe(element);\n }\n\n return () => {\n observer.disconnect();\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n }\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n };\n }, [offset, rootMargin, threshold, scrollContainer]);\n\n const contextValue = React.useMemo<ScrollSpyContextValue>(\n () => ({\n dir,\n orientation,\n offset,\n scrollBehavior,\n scrollContainer,\n isScrollingRef,\n onSectionRegister,\n onSectionUnregister,\n onScrollToSection\n }),\n [\n dir,\n orientation,\n offset,\n scrollBehavior,\n scrollContainer,\n onSectionRegister,\n onSectionUnregister,\n onScrollToSection\n ]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <StoreContext.Provider value={store}>\n <ScrollSpyContext.Provider value={contextValue}>\n <RootPrimitive\n data-orientation={orientation}\n data-slot=\"scroll-spy\"\n dir={dir}\n {...rootProps}\n className={cn(\"flex\", orientation === \"horizontal\" ? \"flex-row\" : \"flex-col\", className)}\n />\n </ScrollSpyContext.Provider>\n </StoreContext.Provider>\n );\n}\n\ninterface ScrollSpyNavProps extends React.ComponentProps<\"nav\"> {\n asChild?: boolean;\n}\n\nfunction ScrollSpyNav(props: ScrollSpyNavProps) {\n const { asChild, className, ...navProps } = props;\n\n const { dir, orientation } = useScrollSpyContext(NAV_NAME);\n const t = useComitorMessages(\"scrollSpy\");\n\n const NavPrimitive = asChild ? Slot : \"nav\";\n\n return (\n <NavPrimitive\n /*\n * ⚠ Khác thượng nguồn: `<nav>` có tên mặc định (khoá `navLabel` của namespace `scrollSpy`,\n * theo `ComitorLocaleProvider`). Trong khung app Comitor đã có ít nhất MỘT landmark\n * `navigation` (sidebar), nên một `<nav>` thứ hai không tên là hai landmark trùng loại không\n * phân biệt được trong danh sách landmark của trình đọc màn hình. Ghi đè bằng chính prop\n * `aria-label` (cùng khuôn `Breadcrumb`/`Pagination` của gói).\n */\n aria-label={t.navLabel}\n data-orientation={orientation}\n data-slot=\"scroll-spy-nav\"\n dir={dir}\n {...navProps}\n className={cn(\"flex gap-2\", orientation === \"horizontal\" ? \"flex-col\" : \"flex-row\", className)}\n />\n );\n}\n\ninterface ScrollSpyLinkProps extends React.ComponentProps<\"a\"> {\n value: string;\n asChild?: boolean;\n}\n\nfunction ScrollSpyLink(props: ScrollSpyLinkProps) {\n const { value: linkValue, asChild, onClick, className, ...linkProps } = props;\n\n const { orientation, onScrollToSection } = useScrollSpyContext(LINK_NAME);\n const value = useStore((state) => state.value);\n const isActive = value === linkValue;\n\n const onLinkClick = React.useCallback(\n (event: React.MouseEvent<LinkElement>) => {\n event.preventDefault();\n onClick?.(event);\n onScrollToSection(linkValue);\n },\n [linkValue, onClick, onScrollToSection]\n );\n\n const LinkPrimitive = asChild ? Slot : \"a\";\n\n return (\n <LinkPrimitive\n /*\n * ⚠ Khác thượng nguồn: bản gốc chỉ đặt `data-state=\"active\"` — thuộc tính mà trình đọc màn\n * hình KHÔNG đọc, nên mục đang xem chỉ được báo bằng hình. `aria-current=\"location\"` là giá\n * trị ARIA dành cho \"vị trí hiện tại\" trong một ngữ cảnh — ở đây là mục đang xem của trang.\n * Không dùng `\"page\"`: link trỏ tới một mục TRONG trang, không phải tới một trang khác.\n */\n aria-current={isActive ? \"location\" : undefined}\n data-orientation={orientation}\n data-slot=\"scroll-spy-link\"\n data-state={isActive ? \"active\" : \"inactive\"}\n {...linkProps}\n href={asChild ? undefined : `#${linkValue}`}\n className={cn(\n \"px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground data-[state=active]:bg-accent data-[state=active]:text-foreground\",\n /*\n * ⚠ Khác thượng nguồn: thêm VẠCH ACTIVE. Bản gốc báo mục đang xem chỉ bằng `bg-accent` —\n * #FBF5DC trên nền trắng là 1,09:1 (dark: 1,17:1), không đủ làm chỉ báo (WCAG 1.4.11 đòi\n * 3:1 cho thành phần phi văn bản mang nghĩa) — cộng đổi màu chữ, mà màu không được là tín\n * hiệu DUY NHẤT (1.4.1). Vạch 2px `border-primary-ink` là đúng khuôn `Tabs` biến thể\n * `default` (tabs-variants.ts): bậc `-ink` vì nó đi lên nền TRANG, và bảng tương phản cao\n * chỉ đổi được nó qua token này. Đặt ở cạnh ĐẦU (dọc) hoặc cạnh DƯỚI (ngang) theo hướng\n * xếp link; `border-s`/`rounded-e` là thuộc tính logic nên đúng cả RTL.\n */\n \"border-transparent data-[state=active]:border-primary-ink\",\n orientation === \"horizontal\" ? \"rounded-e-md border-s-2\" : \"rounded-t-md border-b-2\",\n // Vòng focus cùng khuôn `Tabs` của gói (TABS_TRIGGER_BASE).\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n className\n )}\n onClick={onLinkClick}\n />\n );\n}\n\ninterface ScrollSpyViewportProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\nfunction ScrollSpyViewport(props: ScrollSpyViewportProps) {\n const { asChild, className, ...viewportProps } = props;\n\n const { dir, orientation } = useScrollSpyContext(VIEWPORT_NAME);\n\n const ViewportPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ViewportPrimitive\n data-orientation={orientation}\n data-slot=\"scroll-spy-viewport\"\n dir={dir}\n {...viewportProps}\n className={cn(\"flex flex-1 flex-col gap-8\", className)}\n />\n );\n}\n\ninterface ScrollSpySectionProps extends React.ComponentProps<\"div\"> {\n value: string;\n asChild?: boolean;\n}\n\nfunction ScrollSpySection(props: ScrollSpySectionProps) {\n const { asChild, ref, value, ...sectionProps } = props;\n\n const { orientation, onSectionRegister, onSectionUnregister } = useScrollSpyContext(SECTION_NAME);\n const sectionRef = React.useRef<SectionElement>(null);\n const composedRef = useComposedRefs(ref, sectionRef);\n\n useIsomorphicLayoutEffect(() => {\n const element = sectionRef.current;\n if (!element || !value) return;\n\n onSectionRegister(value, element);\n\n return () => {\n onSectionUnregister(value);\n };\n }, [value, onSectionRegister, onSectionUnregister]);\n\n const SectionPrimitive = asChild ? Slot : \"div\";\n\n return (\n <SectionPrimitive\n data-orientation={orientation}\n data-slot=\"scroll-spy-section\"\n {...sectionProps}\n id={value}\n ref={composedRef}\n />\n );\n}\n\nexport {\n ScrollSpy,\n ScrollSpyLink,\n type ScrollSpyLinkProps,\n ScrollSpyNav,\n type ScrollSpyNavProps,\n type ScrollSpyProps,\n ScrollSpySection,\n type ScrollSpySectionProps,\n ScrollSpyViewport,\n type ScrollSpyViewportProps\n};\n"],"mappings":"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","names":["value"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/scroll-spy.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/scroll-spy) — 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: trang cài đặt dài và trang chi tiết bản ghi (các mục của một liên hệ CRM) có\n * mục lục điều hướng trong trang.\n *\n * ⚠ `orientation` giữ NGHĨA của thượng nguồn — nó nói về hướng xếp của KHUNG, không phải của danh\n * sách link: `horizontal` (mặc định) = mục lục đứng bên cạnh nội dung, link xếp DỌC; `vertical` =\n * mục lục nằm trên nội dung, link xếp NGANG.\n */\n\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\n\nconst ROOT_NAME = \"ScrollSpy\";\nconst NAV_NAME = \"ScrollSpyNav\";\nconst LINK_NAME = \"ScrollSpyLink\";\nconst VIEWPORT_NAME = \"ScrollSpyViewport\";\nconst SECTION_NAME = \"ScrollSpySection\";\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ntype LinkElement = React.ComponentRef<typeof ScrollSpyLink>;\ntype SectionElement = React.ComponentRef<typeof ScrollSpySection>;\n\nfunction getDefaultScrollBehavior(): ScrollBehavior {\n if (typeof window === \"undefined\") return \"smooth\";\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ? \"auto\" : \"smooth\";\n}\n\ninterface StoreState {\n value: string;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStore<T>(selector: (state: StoreState) => T, ogStore?: Store | null): T {\n const contextStore = React.useContext(StoreContext);\n\n const store = ogStore ?? contextStore;\n\n if (!store) {\n throw new Error(`\\`useStore\\` phải được gọi bên trong \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ScrollSpyContextValue {\n offset: number;\n scrollBehavior: ScrollBehavior | undefined;\n dir: Direction;\n orientation: Orientation;\n scrollContainer: HTMLElement | null;\n isScrollingRef: React.RefObject<boolean>;\n onSectionRegister: (id: string, element: SectionElement) => void;\n onSectionUnregister: (id: string, element: SectionElement) => void;\n onScrollToSection: (sectionId: string) => void;\n}\n\n/** Cửa vào IntersectionObserver đang sống của `ScrollSpy` — xem effect dựng observer. */\ninterface SectionObserver {\n observe: (element: Element) => void;\n unobserve: (element: Element) => void;\n}\n\nconst ScrollSpyContext = React.createContext<ScrollSpyContextValue | null>(null);\n\nfunction useScrollSpyContext(consumerName: string) {\n const context = React.useContext(ScrollSpyContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface ScrollSpyProps extends React.ComponentProps<\"div\"> {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n rootMargin?: string;\n threshold?: number | number[];\n offset?: number;\n /**\n * Kiểu cuộn khi bấm link. Không truyền thì `\"smooth\"`, hoặc `\"auto\"` nếu người dùng bật giảm\n * chuyển động (`prefers-reduced-motion`).\n */\n scrollBehavior?: ScrollBehavior;\n scrollContainer?: HTMLElement | null;\n dir?: Direction;\n orientation?: Orientation;\n asChild?: boolean;\n}\n\nfunction ScrollSpy(props: ScrollSpyProps) {\n const {\n value,\n defaultValue,\n onValueChange,\n rootMargin,\n threshold = 0.1,\n offset = 0,\n /*\n * ⚠ Khác thượng nguồn: bản gốc tính mặc định NGAY TRONG render (`scrollBehavior =\n * getDefaultScrollBehavior()` → `window.matchMedia` mỗi lượt render). Ở đây để `undefined` và\n * chỉ phân giải lúc cuộn thật — không chạm `window` khi render, và đọc đúng thiết lập giảm\n * chuyển động TẠI THỜI ĐIỂM bấm chứ không phải lúc render gần nhất.\n */\n scrollBehavior,\n scrollContainer = null,\n dir: dirProp,\n orientation = \"horizontal\",\n asChild,\n className,\n ...rootProps\n } = props;\n\n const dir = useDirection(dirProp);\n\n const stateRef = useLazyRef<StoreState>(() => ({\n value: value ?? defaultValue ?? \"\"\n }));\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const onValueChangeRef = useAsRef(onValueChange);\n\n const store = React.useMemo<Store>(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => {\n return stateRef.current;\n },\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n\n stateRef.current[key] = value;\n\n if (key === \"value\" && value) {\n onValueChangeRef.current?.(value);\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n }\n };\n }, [listenersRef, stateRef, onValueChangeRef]);\n\n const sectionMapRef = React.useRef(new Map<string, Element>());\n const sectionObserverRef = React.useRef<SectionObserver | null>(null);\n const isScrollingRef = React.useRef(false);\n const rafIdRef = React.useRef<number | null>(null);\n const isMountedRef = React.useRef(false);\n const scrollTimeoutRef = React.useRef<number | null>(null);\n\n /*\n * ⚠ Khác thượng nguồn (3.1.0): đăng ký/huỷ một mục là observe/unobserve NGAY trên observer đang\n * sống (nếu đã có) — xem effect dựng observer bên dưới. Bản gốc chỉ ghi vào `sectionMap`.\n */\n const onSectionRegister = React.useCallback((id: string, element: SectionElement) => {\n sectionMapRef.current.set(id, element);\n sectionObserverRef.current?.observe(element);\n }, []);\n\n const onSectionUnregister = React.useCallback((id: string, element: SectionElement) => {\n const sectionMap = sectionMapRef.current;\n // Chỉ gỡ khoá khi nó còn trỏ đúng phần tử này: hai mục trùng `value` (lỗi của app — id trùng\n // trên trang) thì mục rời đi không kéo mục còn lại ra khỏi bảng.\n if (sectionMap.get(id) === element) sectionMap.delete(id);\n sectionObserverRef.current?.unobserve(element);\n }, []);\n\n const onScrollToSection = React.useCallback(\n (sectionId: string) => {\n /*\n * ⚠ Khác thượng nguồn: `CSS.escape` quanh id. Bản gốc ghép thẳng `#${sectionId}` vào\n * `querySelector`, nên id bắt đầu bằng chữ số (\"2024-hop-dong\") hay chứa ký tự đặc biệt\n * (id của `useId()` như \":r1:\") làm nó THROW `SyntaxError` ngay khi bấm link.\n */\n const section = scrollContainer\n ? scrollContainer.querySelector(`#${CSS.escape(sectionId)}`)\n : document.getElementById(sectionId);\n\n if (!section) {\n store.setState(\"value\", sectionId);\n return;\n }\n\n // Bật cờ để observer không ghi đè giá trị trong lúc đang cuộn theo lệnh.\n isScrollingRef.current = true;\n store.setState(\"value\", sectionId);\n\n const behavior = scrollBehavior ?? getDefaultScrollBehavior();\n\n if (scrollContainer) {\n const containerRect = scrollContainer.getBoundingClientRect();\n const sectionRect = section.getBoundingClientRect();\n const scrollTop = scrollContainer.scrollTop;\n const offsetPosition = sectionRect.top - containerRect.top + scrollTop - offset;\n\n scrollContainer.scrollTo({\n top: offsetPosition,\n behavior\n });\n } else {\n const sectionPosition = section.getBoundingClientRect().top;\n const offsetPosition = sectionPosition + window.scrollY - offset;\n\n window.scrollTo({\n top: offsetPosition,\n behavior\n });\n }\n\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n\n scrollTimeoutRef.current = window.setTimeout(() => {\n isScrollingRef.current = false;\n }, 500);\n },\n [scrollContainer, offset, scrollBehavior, store]\n );\n\n /*\n * ⚠ Khác thượng nguồn: chỉ cuộn theo lệnh khi `value` đổi TỪ BÊN NGOÀI. Bản gốc cuộn mỗi khi\n * effect chạy lại sau lần đầu, với hai hệ quả đã đo trên trình duyệt:\n * · StrictMode chạy effect hai lần lúc mount → lần hai cuộn luôn CẢ TRANG tới mục (scrollY\n * 0 → ~5050) vì `scrollContainer` của app lúc đó còn `null`. Next dev bật StrictMode sẵn.\n * · Chế độ controlled tự kéo giật: observer đặt giá trị → `onValueChange` → app trả về prop\n * `value` → effect cuộn tới ĐẦU mục đó, ngay giữa lúc người dùng đang cuộn.\n * Luật mới: lúc mount không cuộn (đúng ý nhánh mount của bản gốc); về sau, `value` trùng với giá\n * trị store đang giữ nghĩa là nó chỉ là tiếng vọng của chính observer/link → không cuộn.\n * `defaultValue` đã vào store từ lúc khởi tạo (`stateRef`), không cần đồng bộ thêm.\n */\n const prevValuePropRef = React.useRef<string | undefined>(undefined);\n const onScrollToSectionRef = useAsRef(onScrollToSection);\n\n useIsomorphicLayoutEffect(() => {\n const previous = prevValuePropRef.current;\n prevValuePropRef.current = value;\n if (value === undefined || value === previous) return;\n\n if (previous === undefined || !isMountedRef.current) {\n isMountedRef.current = true;\n store.setState(\"value\", value);\n return;\n }\n\n if (value === store.getState().value) return;\n onScrollToSectionRef.current(value);\n }, [value, store, onScrollToSectionRef]);\n\n /*\n * ⚠ Khác thượng nguồn (3.1.0): observer theo dõi mục THEO LÚC chúng đăng ký/huỷ. Bản gốc chỉ\n * `observe` những mục có mặt lúc effect này chạy, mà effect chỉ chạy lại khi `offset` /\n * `rootMargin` / `threshold` / `scrollContainer` đổi — nên mục mount SAU (dữ liệu tải chậm: phần\n * Deals của một liên hệ CRM) không bao giờ được theo dõi, và spy mount trước mọi mục thì không\n * dựng observer nào. Nay effect đặt vào `sectionObserverRef` một cửa `observe`/`unobserve`, và\n * `onSectionRegister`/`onSectionUnregister` gọi thẳng cửa đó: mục đến/đi KHÔNG dựng lại observer.\n * Observer vẫn chỉ dựng lại khi bốn tuỳ chọn trên đổi (chúng không đổi được trên một observer đã\n * có), và vẫn chỉ được TẠO khi có mục đầu tiên — spy chưa có mục nào không tạo observer, như bản\n * gốc (môi trường thiếu API này, vd jsdom, không đổi hành vi).\n *\n * Không cần cờ nào để biết \"observer đã có chưa\", vì thứ tự của React lo phần đó: layout effect\n * của CON chạy trước của CHA, và mọi cleanup của một lần commit chạy trước mọi effect. Mục đăng ký\n * trong CÙNG lần commit mà effect này chạy (vd observer dựng lại vì `scrollContainer` vừa có) gặp\n * cửa còn `null`, chỉ vào `sectionMap`, rồi được observe ở vòng lặp dưới; mục đăng ký ở commit sau\n * đi thẳng vào observer đang sống. StrictMode (gỡ cả cây: CHA trước, rồi dựng lại: CON trước) đi\n * đúng hai nhánh đó.\n *\n * Luật chọn mục của bản gốc giữ nguyên: trong MỘT lần báo, mục vừa vào dải theo dõi và nằm cao\n * nhất thành mục đang xem. Một mục mới hiện ra ngay trong dải cũng là \"vừa vào dải\" — nó thành mục\n * đang xem, như khi người dùng cuộn tới nó. Mục hiện ra ngoài dải thì không đổi gì.\n */\n useIsomorphicLayoutEffect(() => {\n const sectionMap = sectionMapRef.current;\n const observerRootMargin = rootMargin ?? `${-offset}px 0px -70% 0px`;\n let observer: IntersectionObserver | null = null;\n\n const onIntersect = (entries: IntersectionObserverEntry[]) => {\n if (isScrollingRef.current) return;\n\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n }\n\n rafIdRef.current = requestAnimationFrame(() => {\n const intersecting = entries.filter((entry) => entry.isIntersecting);\n\n if (intersecting.length === 0) return;\n\n const topmost = intersecting.reduce((prev, curr) => {\n return curr.boundingClientRect.top < prev.boundingClientRect.top ? curr : prev;\n });\n\n const id = topmost.target.id;\n if (id && sectionMap.has(id)) {\n store.setState(\"value\", id);\n }\n });\n };\n\n const sectionObserver: SectionObserver = {\n observe: (element) => {\n observer ??= new IntersectionObserver(onIntersect, {\n root: scrollContainer,\n rootMargin: observerRootMargin,\n threshold\n });\n observer.observe(element);\n },\n unobserve: (element) => {\n observer?.unobserve(element);\n }\n };\n\n sectionObserverRef.current = sectionObserver;\n\n for (const element of sectionMap.values()) {\n sectionObserver.observe(element);\n }\n\n return () => {\n sectionObserverRef.current = null;\n observer?.disconnect();\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n }\n };\n }, [offset, rootMargin, threshold, scrollContainer]);\n\n /*\n * ⚠ Khác thượng nguồn (3.1.0): hẹn giờ tắt cờ \"đang cuộn theo lệnh\" (`onScrollToSection`) chỉ bị\n * huỷ khi spy rời DOM. Bản gốc huỷ nó trong cleanup của observer — tức cả khi observer DỰNG LẠI —\n * mà không tắt cờ: đổi `offset` / `scrollContainer` giữa lúc đang cuộn theo lệnh là cờ kẹt ở\n * `true`, observer bỏ qua mọi lần báo, và spy đứng hình tới lần bấm link kế tiếp (đã đo: bấm link\n * rồi đổi `offset` ngay, cuộn tay sau đó không đổi mục đang xem nữa).\n */\n React.useEffect(() => {\n return () => {\n if (scrollTimeoutRef.current !== null) {\n clearTimeout(scrollTimeoutRef.current);\n }\n };\n }, []);\n\n const contextValue = React.useMemo<ScrollSpyContextValue>(\n () => ({\n dir,\n orientation,\n offset,\n scrollBehavior,\n scrollContainer,\n isScrollingRef,\n onSectionRegister,\n onSectionUnregister,\n onScrollToSection\n }),\n [\n dir,\n orientation,\n offset,\n scrollBehavior,\n scrollContainer,\n onSectionRegister,\n onSectionUnregister,\n onScrollToSection\n ]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <StoreContext.Provider value={store}>\n <ScrollSpyContext.Provider value={contextValue}>\n <RootPrimitive\n data-orientation={orientation}\n data-slot=\"scroll-spy\"\n dir={dir}\n {...rootProps}\n className={cn(\"flex\", orientation === \"horizontal\" ? \"flex-row\" : \"flex-col\", className)}\n />\n </ScrollSpyContext.Provider>\n </StoreContext.Provider>\n );\n}\n\ninterface ScrollSpyNavProps extends React.ComponentProps<\"nav\"> {\n asChild?: boolean;\n}\n\nfunction ScrollSpyNav(props: ScrollSpyNavProps) {\n const { asChild, className, ...navProps } = props;\n\n const { dir, orientation } = useScrollSpyContext(NAV_NAME);\n const t = useComitorMessages(\"scrollSpy\");\n\n const NavPrimitive = asChild ? Slot : \"nav\";\n\n return (\n <NavPrimitive\n /*\n * ⚠ Khác thượng nguồn: `<nav>` có tên mặc định (khoá `navLabel` của namespace `scrollSpy`,\n * theo `ComitorLocaleProvider`). Trong khung app Comitor đã có ít nhất MỘT landmark\n * `navigation` (sidebar), nên một `<nav>` thứ hai không tên là hai landmark trùng loại không\n * phân biệt được trong danh sách landmark của trình đọc màn hình. Ghi đè bằng chính prop\n * `aria-label` (cùng khuôn `Breadcrumb`/`Pagination` của gói).\n */\n aria-label={t.navLabel}\n data-orientation={orientation}\n data-slot=\"scroll-spy-nav\"\n dir={dir}\n {...navProps}\n className={cn(\"flex gap-2\", orientation === \"horizontal\" ? \"flex-col\" : \"flex-row\", className)}\n />\n );\n}\n\ninterface ScrollSpyLinkProps extends React.ComponentProps<\"a\"> {\n value: string;\n asChild?: boolean;\n}\n\nfunction ScrollSpyLink(props: ScrollSpyLinkProps) {\n const { value: linkValue, asChild, onClick, className, ...linkProps } = props;\n\n const { orientation, onScrollToSection } = useScrollSpyContext(LINK_NAME);\n const value = useStore((state) => state.value);\n const isActive = value === linkValue;\n\n const onLinkClick = React.useCallback(\n (event: React.MouseEvent<LinkElement>) => {\n event.preventDefault();\n onClick?.(event);\n onScrollToSection(linkValue);\n },\n [linkValue, onClick, onScrollToSection]\n );\n\n const LinkPrimitive = asChild ? Slot : \"a\";\n\n return (\n <LinkPrimitive\n /*\n * ⚠ Khác thượng nguồn: bản gốc chỉ đặt `data-state=\"active\"` — thuộc tính mà trình đọc màn\n * hình KHÔNG đọc, nên mục đang xem chỉ được báo bằng hình. `aria-current=\"location\"` là giá\n * trị ARIA dành cho \"vị trí hiện tại\" trong một ngữ cảnh — ở đây là mục đang xem của trang.\n * Không dùng `\"page\"`: link trỏ tới một mục TRONG trang, không phải tới một trang khác.\n */\n aria-current={isActive ? \"location\" : undefined}\n data-orientation={orientation}\n data-slot=\"scroll-spy-link\"\n data-state={isActive ? \"active\" : \"inactive\"}\n {...linkProps}\n href={asChild ? undefined : `#${linkValue}`}\n className={cn(\n \"px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground data-[state=active]:bg-accent data-[state=active]:text-foreground\",\n /*\n * ⚠ Khác thượng nguồn: thêm VẠCH ACTIVE. Bản gốc báo mục đang xem chỉ bằng `bg-accent` —\n * #FBF5DC trên nền trắng là 1,09:1 (dark: 1,17:1), không đủ làm chỉ báo (WCAG 1.4.11 đòi\n * 3:1 cho thành phần phi văn bản mang nghĩa) — cộng đổi màu chữ, mà màu không được là tín\n * hiệu DUY NHẤT (1.4.1). Vạch 2px `border-primary-ink` là đúng khuôn `Tabs` biến thể\n * `default` (tabs-variants.ts): bậc `-ink` vì nó đi lên nền TRANG, và bảng tương phản cao\n * chỉ đổi được nó qua token này. Đặt ở cạnh ĐẦU (dọc) hoặc cạnh DƯỚI (ngang) theo hướng\n * xếp link; `border-s`/`rounded-e` là thuộc tính logic nên đúng cả RTL.\n */\n \"border-transparent data-[state=active]:border-primary-ink\",\n orientation === \"horizontal\" ? \"rounded-e-md border-s-2\" : \"rounded-t-md border-b-2\",\n // Vòng focus cùng khuôn `Tabs` của gói (TABS_TRIGGER_BASE).\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n className\n )}\n onClick={onLinkClick}\n />\n );\n}\n\ninterface ScrollSpyViewportProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\nfunction ScrollSpyViewport(props: ScrollSpyViewportProps) {\n const { asChild, className, ...viewportProps } = props;\n\n const { dir, orientation } = useScrollSpyContext(VIEWPORT_NAME);\n\n const ViewportPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ViewportPrimitive\n data-orientation={orientation}\n data-slot=\"scroll-spy-viewport\"\n dir={dir}\n {...viewportProps}\n className={cn(\"flex flex-1 flex-col gap-8\", className)}\n />\n );\n}\n\ninterface ScrollSpySectionProps extends React.ComponentProps<\"div\"> {\n value: string;\n asChild?: boolean;\n}\n\nfunction ScrollSpySection(props: ScrollSpySectionProps) {\n const { asChild, ref, value, ...sectionProps } = props;\n\n const { orientation, onSectionRegister, onSectionUnregister } = useScrollSpyContext(SECTION_NAME);\n /*\n * ⚠ Khác thượng nguồn (3.1.0): phần tử giữ trong STATE (ref gọi setter), không trong ref object, để\n * đăng ký lại khi NÚT DOM đổi mà `value` thì không. Ca thật: `asChild` quanh một nội dung tải chậm\n * (`{loaded ? <section>…</section> : <Skeleton />}`) — đổi loại phần tử là React thay hẳn nút, và\n * bản gốc (effect chỉ theo `value`) cứ quan sát nút skeleton đã rời DOM: mục ấy không bao giờ sáng\n * (đã đo trên trình duyệt). Giá phải trả: mỗi mục render thêm MỘT lượt lúc mount (và mỗi lần nút\n * đổi). Cây con không render lại — `children` vẫn là cùng tham chiếu phần tử nên React bỏ qua; riêng\n * `asChild` thì phần tử con do `Slot` nhân bản render thêm lượt đó cùng mục.\n */\n const [element, setElement] = React.useState<SectionElement | null>(null);\n const composedRef = useComposedRefs(ref, setElement);\n\n useIsomorphicLayoutEffect(() => {\n if (!element || !value) return;\n\n onSectionRegister(value, element);\n\n return () => {\n onSectionUnregister(value, element);\n };\n }, [element, value, onSectionRegister, onSectionUnregister]);\n\n const SectionPrimitive = asChild ? Slot : \"div\";\n\n return (\n <SectionPrimitive\n data-orientation={orientation}\n data-slot=\"scroll-spy-section\"\n {...sectionProps}\n id={value}\n ref={composedRef}\n />\n );\n}\n\nexport {\n ScrollSpy,\n ScrollSpyLink,\n type ScrollSpyLinkProps,\n ScrollSpyNav,\n type ScrollSpyNavProps,\n type ScrollSpyProps,\n ScrollSpySection,\n type ScrollSpySectionProps,\n ScrollSpyViewport,\n type ScrollSpyViewportProps\n};\n"],"mappings":"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","names":["value"]}
|
|
@@ -26,6 +26,6 @@ import * as React from 'react';
|
|
|
26
26
|
* vừa ≥3:1 với nhau vừa ≥3:1 với nền.
|
|
27
27
|
*/
|
|
28
28
|
type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root>;
|
|
29
|
-
declare function Slider({ className, defaultValue, value, min, max, ...props }: SliderProps): React.JSX.Element;
|
|
29
|
+
declare function Slider({ className, defaultValue, value, min, max, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }: SliderProps): React.JSX.Element;
|
|
30
30
|
|
|
31
31
|
export { Slider, type SliderProps };
|
|
@@ -3,11 +3,22 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
-
function Slider({
|
|
6
|
+
function Slider({
|
|
7
|
+
className,
|
|
8
|
+
defaultValue,
|
|
9
|
+
value,
|
|
10
|
+
min = 0,
|
|
11
|
+
max = 100,
|
|
12
|
+
"aria-label": ariaLabel,
|
|
13
|
+
"aria-labelledby": ariaLabelledby,
|
|
14
|
+
...props
|
|
15
|
+
}) {
|
|
7
16
|
const _values = React.useMemo(
|
|
8
17
|
() => Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max],
|
|
9
18
|
[value, defaultValue, min, max]
|
|
10
19
|
);
|
|
20
|
+
const singleThumb = _values.length === 1;
|
|
21
|
+
const thumbName = singleThumb ? { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby } : {};
|
|
11
22
|
return /* @__PURE__ */ jsxs(
|
|
12
23
|
SliderPrimitive.Root,
|
|
13
24
|
{
|
|
@@ -21,6 +32,8 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
21
32
|
className
|
|
22
33
|
),
|
|
23
34
|
...props,
|
|
35
|
+
"aria-label": singleThumb ? void 0 : ariaLabel,
|
|
36
|
+
"aria-labelledby": singleThumb ? void 0 : ariaLabelledby,
|
|
24
37
|
children: [
|
|
25
38
|
/* @__PURE__ */ jsx(
|
|
26
39
|
SliderPrimitive.Track,
|
|
@@ -40,6 +53,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
|
|
|
40
53
|
SliderPrimitive.Thumb,
|
|
41
54
|
{
|
|
42
55
|
"data-slot": "slider-thumb",
|
|
56
|
+
...thumbName,
|
|
43
57
|
className: "border-control-on ring-ring/50 block size-4 shrink-0 rounded-full border bg-control-knob shadow-sm transition-colors duration-150 ease-out hover:ring-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50"
|
|
44
58
|
},
|
|
45
59
|
index
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Slider (Radix) — hỗ trợ 1 hoặc nhiều thumb (range) và cả hướng dọc.\n * Số thumb suy ra từ `value`/`defaultValue`; không truyền gì thì mặc định 2 thumb\n * ứng với [min, max].\n *\n * ─ MÀU: CÙNG BỘ TOKEN VỚI `Switch`/`Checkbox` ─\n *\n * Thanh trượt là thành phần phi văn bản (WCAG 1.4.11, ngưỡng 3:1). Bản trước hỏng cả ba phía\n * ở light: rãnh `bg-muted` chỉ **1,07:1** với nền trắng (không nhìn thấy cái slider), đoạn đã\n * chọn `bg-primary` **1,86:1** với nền và **1,73:1** với rãnh, núm `bg-white` **1,86:1** với\n * đoạn đã chọn — và `bg-white` cứng thì ở dark núm ăn 1,49:1 với đoạn gold sáng.\n *\n * Nay đúng bộ ba của hợp đồng màu (docs/hop-dong-mau.md §3–§4):\n *\n * rãnh `bg-control-edge` : 3,83:1 (light) · 4,94:1 (dark) với nền trang ✓\n * đã chọn `bg-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n * núm `bg-control-knob` : 6,51 / 12,67:1 với đoạn đã chọn · 3,83 / 4,94:1 với rãnh ✓\n * viền núm `border-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n *\n * ⚠ Đoạn ĐÃ CHỌN ⇄ rãnh chỉ 1,70:1 (light) / 2,57:1 (dark) — giống hệt cặp rãnh BẬT ⇄ rãnh TẮT\n * của `Switch`, và chấp nhận vì cùng lý do: VỊ TRÍ NÚM mới là thứ mang giá trị, còn hai màu kia\n * đều tự đạt ≥3:1 với nền trang và với núm. Với nền trang trắng thì không tồn tại cặp màu nào\n * vừa ≥3:1 với nhau vừa ≥3:1 với nền.\n */\n\nexport type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root>;\n\
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Slider (Radix) — hỗ trợ 1 hoặc nhiều thumb (range) và cả hướng dọc.\n * Số thumb suy ra từ `value`/`defaultValue`; không truyền gì thì mặc định 2 thumb\n * ứng với [min, max].\n *\n * ─ MÀU: CÙNG BỘ TOKEN VỚI `Switch`/`Checkbox` ─\n *\n * Thanh trượt là thành phần phi văn bản (WCAG 1.4.11, ngưỡng 3:1). Bản trước hỏng cả ba phía\n * ở light: rãnh `bg-muted` chỉ **1,07:1** với nền trắng (không nhìn thấy cái slider), đoạn đã\n * chọn `bg-primary` **1,86:1** với nền và **1,73:1** với rãnh, núm `bg-white` **1,86:1** với\n * đoạn đã chọn — và `bg-white` cứng thì ở dark núm ăn 1,49:1 với đoạn gold sáng.\n *\n * Nay đúng bộ ba của hợp đồng màu (docs/hop-dong-mau.md §3–§4):\n *\n * rãnh `bg-control-edge` : 3,83:1 (light) · 4,94:1 (dark) với nền trang ✓\n * đã chọn `bg-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n * núm `bg-control-knob` : 6,51 / 12,67:1 với đoạn đã chọn · 3,83 / 4,94:1 với rãnh ✓\n * viền núm `border-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n *\n * ⚠ Đoạn ĐÃ CHỌN ⇄ rãnh chỉ 1,70:1 (light) / 2,57:1 (dark) — giống hệt cặp rãnh BẬT ⇄ rãnh TẮT\n * của `Switch`, và chấp nhận vì cùng lý do: VỊ TRÍ NÚM mới là thứ mang giá trị, còn hai màu kia\n * đều tự đạt ≥3:1 với nền trang và với núm. Với nền trang trắng thì không tồn tại cặp màu nào\n * vừa ≥3:1 với nhau vừa ≥3:1 với nền.\n */\n\nexport type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root>;\n\n/*\n * ─ TÊN CHO TRÌNH ĐỌC MÀN HÌNH ĐI XUỐNG NÚM (3.0.1) ─\n *\n * Radix đặt `role=\"slider\"` trên từng `Thumb`, còn `Root` chỉ là một `<span>` không vai trò. Trước\n * `3.0.1`, `aria-label` / `aria-labelledby` truyền cho `Slider` rơi xuống `Root` — tức lên một phần\n * tử KHÔNG AI focus, và núm thật sự nhận bàn phím thì không có tên (axe: \"ARIA slider elements\n * must have an accessible name\"). Gặp khi dựng thanh zoom cho `Cropper`.\n *\n * Nay với slider MỘT núm, hai thuộc tính đó đi thẳng xuống núm. Slider NHIỀU núm (range) giữ\n * nguyên như cũ: Radix tự đặt tên từng núm (\"Minimum\" / \"Maximum\"), và chép cùng một tên xuống\n * cả hai núm sẽ xoá mất đúng phân biệt đó — range cần tên riêng thì đặt nhãn cho từng núm ở app.\n */\nfunction Slider({\n className,\n defaultValue,\n value,\n min = 0,\n max = 100,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n ...props\n}: SliderProps) {\n const _values = React.useMemo(\n () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),\n [value, defaultValue, min, max]\n );\n const singleThumb = _values.length === 1;\n /*\n * Rải CÓ ĐIỀU KIỆN, đừng truyền `aria-label={undefined}` xuống núm: `Thumb` của Radix rải props\n * của app SAU tên nó tự đặt, nên chỉ một khoá mang giá trị `undefined` cũng đủ xoá \"Minimum\" /\n * \"Maximum\" của slider range.\n */\n const thumbName = singleThumb ? { \"aria-label\": ariaLabel, \"aria-labelledby\": ariaLabelledby } : {};\n\n return (\n <SliderPrimitive.Root\n data-slot=\"slider\"\n defaultValue={defaultValue}\n value={value}\n min={min}\n max={max}\n className={cn(\n \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n className\n )}\n {...props}\n aria-label={singleThumb ? undefined : ariaLabel}\n aria-labelledby={singleThumb ? undefined : ariaLabelledby}\n >\n <SliderPrimitive.Track\n data-slot=\"slider-track\"\n className=\"bg-slider-track relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\"\n >\n <SliderPrimitive.Range\n data-slot=\"slider-range\"\n className=\"bg-control-on absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n />\n </SliderPrimitive.Track>\n {Array.from({ length: _values.length }, (_, index) => (\n <SliderPrimitive.Thumb\n data-slot=\"slider-thumb\"\n // biome-ignore lint/suspicious/noArrayIndexKey: thumb chỉ phân biệt bằng vị trí, không có id\n key={index}\n {...thumbName}\n className=\"border-control-on ring-ring/50 block size-4 shrink-0 rounded-full border bg-control-knob shadow-sm transition-colors duration-150 ease-out hover:ring-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\"\n />\n ))}\n </SliderPrimitive.Root>\n );\n}\n\nexport { Slider };\n"],"mappings":";AAoEI,SAkBI,KAlBJ;AAlEJ,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AAyCnB,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAgB;AACd,QAAM,UAAU,MAAM;AAAA,IACpB,MAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,KAAK,GAAG;AAAA,IAC5F,CAAC,OAAO,cAAc,KAAK,GAAG;AAAA,EAChC;AACA,QAAM,cAAc,QAAQ,WAAW;AAMvC,QAAM,YAAY,cAAc,EAAE,cAAc,WAAW,mBAAmB,eAAe,IAAI,CAAC;AAElG,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MACJ,cAAY,cAAc,SAAY;AAAA,MACtC,mBAAiB,cAAc,SAAY;AAAA,MAE3C;AAAA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YACV,WAAU;AAAA,YAEV;AAAA,cAAC,gBAAgB;AAAA,cAAhB;AAAA,gBACC,aAAU;AAAA,gBACV,WAAU;AAAA;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,QACC,MAAM,KAAK,EAAE,QAAQ,QAAQ,OAAO,GAAG,CAAC,GAAG,UAC1C;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YAGT,GAAG;AAAA,YACJ,WAAU;AAAA;AAAA,UAFL;AAAA,QAGP,CACD;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useTheme } from "next-themes";
|
|
4
4
|
import { Toaster as Sonner } from "sonner";
|
|
5
|
+
import { useToastLanguageBridge } from "../../lib/toast-language.js";
|
|
5
6
|
const Toaster = ({ ...props }) => {
|
|
6
7
|
const { theme = "system" } = useTheme();
|
|
8
|
+
useToastLanguageBridge();
|
|
7
9
|
return /* @__PURE__ */ jsx(
|
|
8
10
|
Sonner,
|
|
9
11
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/sonner.tsx"],"sourcesContent":["\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\";\n\n/**\n * Toaster — bọc `sonner` và nối màu vào token popover của Comitor.\n *\n * Phụ thuộc `next-themes` (peer OPTIONAL) để đọc theme hiện tại; app nào không\n * dùng next-themes thì đừng render component này — hoặc render `<Sonner>` trực tiếp.\n *\n * Đặt duy nhất MỘT `<Toaster />` ở gốc app; gọi `toast()` của `sonner` ở bất kỳ đâu.\n * Bản có icon theo trạng thái (success/warning/info) là composite `Toaster` ở Tier 2.\n */\n\n/**\n * Cửa DUY NHẤT của file này: `SonnerToaster` + `SonnerToasterProps`.\n *\n * Tên trần `Toaster`/`ToasterProps` cố ý KHÔNG xuất, dù chúng là tên tự nhiên ở đây: composite\n * `Toaster`/`ToasterProps` (`composite/toast.tsx`) đã chiếm đúng hai tên đó ở entry `.`. Nếu để\n * chúng xuất ra thì (a) `shell/index.ts` chỉ lấy bản `Sonner*` nên chúng là export CHẾT, và\n * (b) ai thêm `export * from \"./components/ui/sonner.js\"` vào `src/index.ts` sẽ vỡ build ngay,\n * còn thêm vào `shell/index.ts` thì im lặng dựng thêm một cửa `Toaster` thứ hai.\n * Giữ hai toaster là quyết định có chủ ý (docs/quyet-dinh-tich-hop.md §31) — nhưng \"hai toaster,\n * hai tên\" chỉ đúng khi tên trần không còn lối ra.\n */\nexport type SonnerToasterProps = ToasterProps;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme();\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\"\n } as React.CSSProperties\n }\n {...props}\n />\n );\n};\n\nexport { Toaster as SonnerToaster };\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/sonner.tsx"],"sourcesContent":["\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport type * as React from \"react\";\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\";\nimport { useToastLanguageBridge } from \"../../lib/toast-language.js\";\n\n/**\n * Toaster — bọc `sonner` và nối màu vào token popover của Comitor.\n *\n * Phụ thuộc `next-themes` (peer OPTIONAL) để đọc theme hiện tại; app nào không\n * dùng next-themes thì đừng render component này — hoặc render `<Sonner>` trực tiếp.\n *\n * Đặt duy nhất MỘT `<Toaster />` ở gốc app; gọi `toast()` của `sonner` ở bất kỳ đâu.\n * Bản có icon theo trạng thái (success/warning/info) là composite `Toaster` ở Tier 2.\n */\n\n/**\n * Cửa DUY NHẤT của file này: `SonnerToaster` + `SonnerToasterProps`.\n *\n * Tên trần `Toaster`/`ToasterProps` cố ý KHÔNG xuất, dù chúng là tên tự nhiên ở đây: composite\n * `Toaster`/`ToasterProps` (`composite/toast.tsx`) đã chiếm đúng hai tên đó ở entry `.`. Nếu để\n * chúng xuất ra thì (a) `shell/index.ts` chỉ lấy bản `Sonner*` nên chúng là export CHẾT, và\n * (b) ai thêm `export * from \"./components/ui/sonner.js\"` vào `src/index.ts` sẽ vỡ build ngay,\n * còn thêm vào `shell/index.ts` thì im lặng dựng thêm một cửa `Toaster` thứ hai.\n * Giữ hai toaster là quyết định có chủ ý (docs/quyet-dinh-tich-hop.md §31) — nhưng \"hai toaster,\n * hai tên\" chỉ đúng khi tên trần không còn lối ra.\n */\nexport type SonnerToasterProps = ToasterProps;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme();\n /*\n * Ghi ngôn ngữ của `ComitorLocaleProvider` cho `toastPromise` — như `Toaster` ở Tier 2. Thiếu dòng\n * này (tới `3.0.0`) thì toast \"đang chạy\" của app dùng shell luôn nói tiếng Anh, dù đã có provider.\n */\n useToastLanguageBridge();\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\"\n } as React.CSSProperties\n }\n {...props}\n />\n );\n};\n\nexport { Toaster as SonnerToaster };\n"],"mappings":";AAuCI;AArCJ,SAAS,gBAAgB;AAEzB,SAAS,WAAW,cAAiC;AACrD,SAAS,8BAA8B;AAyBvC,MAAM,UAAU,CAAC,EAAE,GAAG,MAAM,MAAoB;AAC9C,QAAM,EAAE,QAAQ,SAAS,IAAI,SAAS;AAKtC,yBAAuB;AAEvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,OACE;AAAA,QACE,eAAe;AAAA,QACf,iBAAiB;AAAA,QACjB,mBAAmB;AAAA,MACrB;AAAA,MAED,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -6,6 +6,7 @@ import { Check } from "lucide-react";
|
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { useComitorLabels } from "../../i18n/locale-provider.js";
|
|
8
8
|
import { cn } from "../../lib/cn.js";
|
|
9
|
+
import { collectItemElements } from "../../lib/collect-item-elements.js";
|
|
9
10
|
import { useComposedRefs } from "../../lib/compose-refs.js";
|
|
10
11
|
import { useAsRef } from "../../lib/use-as-ref.js";
|
|
11
12
|
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
@@ -73,6 +74,15 @@ function getDataState(value, itemValue, stepState, steps, variant = "item") {
|
|
|
73
74
|
}
|
|
74
75
|
return "inactive";
|
|
75
76
|
}
|
|
77
|
+
function getStepsFromChildren(children) {
|
|
78
|
+
const steps = /* @__PURE__ */ new Map();
|
|
79
|
+
const items = collectItemElements(children, { item: StepperItem, list: StepperList });
|
|
80
|
+
for (const { props } of items ?? []) {
|
|
81
|
+
const { value, completed = false, disabled = false } = props;
|
|
82
|
+
steps.set(value, { value, completed, disabled });
|
|
83
|
+
}
|
|
84
|
+
return steps;
|
|
85
|
+
}
|
|
76
86
|
const StoreContext = React.createContext(null);
|
|
77
87
|
function useStoreContext(consumerName) {
|
|
78
88
|
const context = React.useContext(StoreContext);
|
|
@@ -84,7 +94,8 @@ function useStoreContext(consumerName) {
|
|
|
84
94
|
function useStore(selector) {
|
|
85
95
|
const store = useStoreContext("useStepper");
|
|
86
96
|
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
87
|
-
|
|
97
|
+
const getServerSnapshot = React.useCallback(() => selector(store.getInitialState()), [store, selector]);
|
|
98
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
|
|
88
99
|
}
|
|
89
100
|
const StepperContext = React.createContext(null);
|
|
90
101
|
function useStepperContext(consumerName) {
|
|
@@ -120,6 +131,11 @@ function Stepper(props) {
|
|
|
120
131
|
steps: /* @__PURE__ */ new Map(),
|
|
121
132
|
value: value ?? defaultValue ?? ""
|
|
122
133
|
}));
|
|
134
|
+
const initialStateRef = useLazyRef(() => ({
|
|
135
|
+
steps: getStepsFromChildren(props.children),
|
|
136
|
+
value: stateRef.current.value
|
|
137
|
+
}));
|
|
138
|
+
const registeredRef = React.useRef(false);
|
|
123
139
|
const propsRef = useAsRef({
|
|
124
140
|
onValueChange,
|
|
125
141
|
onValueComplete,
|
|
@@ -133,7 +149,10 @@ function Stepper(props) {
|
|
|
133
149
|
listenersRef.current.add(cb);
|
|
134
150
|
return () => listenersRef.current.delete(cb);
|
|
135
151
|
},
|
|
136
|
-
|
|
152
|
+
// ⚠ Khác thượng nguồn (3.1.0): trước khi các bước đăng ký, trạng thái có hiệu lực là trạng thái
|
|
153
|
+
// đọc từ cây — xem `initialStateRef`. Các hàm ghi bên dưới luôn ghi vào `stateRef`.
|
|
154
|
+
getState: () => registeredRef.current ? stateRef.current : initialStateRef.current,
|
|
155
|
+
getInitialState: () => initialStateRef.current,
|
|
137
156
|
setState: (key, value2) => {
|
|
138
157
|
if (Object.is(stateRef.current[key], value2)) return;
|
|
139
158
|
if (key === "value" && typeof value2 === "string") {
|
|
@@ -200,12 +219,16 @@ function Stepper(props) {
|
|
|
200
219
|
}
|
|
201
220
|
}
|
|
202
221
|
};
|
|
203
|
-
}, [listenersRef, stateRef, propsRef]);
|
|
222
|
+
}, [listenersRef, stateRef, initialStateRef, propsRef]);
|
|
204
223
|
useIsomorphicLayoutEffect(() => {
|
|
205
224
|
if (value !== void 0) {
|
|
206
225
|
store.setState("value", value);
|
|
207
226
|
}
|
|
208
227
|
}, [value]);
|
|
228
|
+
useIsomorphicLayoutEffect(() => {
|
|
229
|
+
registeredRef.current = true;
|
|
230
|
+
store.notify();
|
|
231
|
+
}, [store]);
|
|
209
232
|
const dir = useDirection(dirProp);
|
|
210
233
|
const instanceId = React.useId();
|
|
211
234
|
const rootId = id ?? instanceId;
|