@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.
Files changed (110) hide show
  1. package/README.md +5 -1
  2. package/dist/components/composite/confirm-dialog.js +56 -35
  3. package/dist/components/composite/confirm-dialog.js.map +1 -1
  4. package/dist/components/composite/search-filter-bar-constants.d.ts +3 -0
  5. package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
  6. package/dist/components/composite/table-pagination-constants.d.ts +3 -1
  7. package/dist/components/composite/table-pagination-constants.js.map +1 -1
  8. package/dist/components/composite/table-pagination.js +10 -2
  9. package/dist/components/composite/table-pagination.js.map +1 -1
  10. package/dist/components/composite/text-utils.d.ts +4 -23
  11. package/dist/components/composite/text-utils.js +1 -11
  12. package/dist/components/composite/text-utils.js.map +1 -1
  13. package/dist/components/composite/toast.d.ts +2 -1
  14. package/dist/components/composite/toast.js +3 -9
  15. package/dist/components/composite/toast.js.map +1 -1
  16. package/dist/components/ui/action-bar.js +45 -1
  17. package/dist/components/ui/action-bar.js.map +1 -1
  18. package/dist/components/ui/banner.d.ts +8 -0
  19. package/dist/components/ui/banner.js +149 -35
  20. package/dist/components/ui/banner.js.map +1 -1
  21. package/dist/components/ui/circular-progress.js +10 -2
  22. package/dist/components/ui/circular-progress.js.map +1 -1
  23. package/dist/components/ui/kanban.js +9 -3
  24. package/dist/components/ui/kanban.js.map +1 -1
  25. package/dist/components/ui/key-value.js +70 -76
  26. package/dist/components/ui/key-value.js.map +1 -1
  27. package/dist/components/ui/mask-input-constants.d.ts +34 -2
  28. package/dist/components/ui/mask-input-constants.js +87 -76
  29. package/dist/components/ui/mask-input-constants.js.map +1 -1
  30. package/dist/components/ui/mask-input.d.ts +6 -5
  31. package/dist/components/ui/mask-input.js +23 -21
  32. package/dist/components/ui/mask-input.js.map +1 -1
  33. package/dist/components/ui/mention.d.ts +14 -2
  34. package/dist/components/ui/mention.js +33 -6
  35. package/dist/components/ui/mention.js.map +1 -1
  36. package/dist/components/ui/phone-input.d.ts +18 -8
  37. package/dist/components/ui/phone-input.js +125 -35
  38. package/dist/components/ui/phone-input.js.map +1 -1
  39. package/dist/components/ui/rating.js +4 -2
  40. package/dist/components/ui/rating.js.map +1 -1
  41. package/dist/components/ui/scroll-spy.js +46 -31
  42. package/dist/components/ui/scroll-spy.js.map +1 -1
  43. package/dist/components/ui/slider.d.ts +1 -1
  44. package/dist/components/ui/slider.js +15 -1
  45. package/dist/components/ui/slider.js.map +1 -1
  46. package/dist/components/ui/sonner.js +2 -0
  47. package/dist/components/ui/sonner.js.map +1 -1
  48. package/dist/components/ui/stepper.js +26 -3
  49. package/dist/components/ui/stepper.js.map +1 -1
  50. package/dist/components/ui/time-picker.d.ts +24 -4
  51. package/dist/components/ui/time-picker.js +425 -162
  52. package/dist/components/ui/time-picker.js.map +1 -1
  53. package/dist/components/ui/timeline.js +45 -9
  54. package/dist/components/ui/timeline.js.map +1 -1
  55. package/dist/components/ui/toggle-group.js +12 -1
  56. package/dist/components/ui/toggle-group.js.map +1 -1
  57. package/dist/components/ui/tour.js +3 -2
  58. package/dist/components/ui/tour.js.map +1 -1
  59. package/dist/data-grid/data-grid-cell-variants.js +4 -4
  60. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  61. package/dist/data-grid/data-grid-cell-wrapper.d.ts +1 -1
  62. package/dist/data-grid/data-grid-cell-wrapper.js +6 -0
  63. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  64. package/dist/data-grid/data-grid-row.js +1 -1
  65. package/dist/data-grid/data-grid-row.js.map +1 -1
  66. package/dist/data-grid/data-grid-select-column.js +6 -2
  67. package/dist/data-grid/data-grid-select-column.js.map +1 -1
  68. package/dist/data-grid/data-grid-skeleton.d.ts +5 -0
  69. package/dist/data-grid/data-grid-skeleton.js +1 -4
  70. package/dist/data-grid/data-grid-skeleton.js.map +1 -1
  71. package/dist/data-grid/data-grid.d.ts +1 -1
  72. package/dist/data-grid/data-grid.js +3 -2
  73. package/dist/data-grid/data-grid.js.map +1 -1
  74. package/dist/data-grid/filters.js +3 -2
  75. package/dist/data-grid/filters.js.map +1 -1
  76. package/dist/data-grid/types.d.ts +7 -0
  77. package/dist/data-grid/use-data-grid.d.ts +1 -0
  78. package/dist/data-grid/use-data-grid.js +70 -24
  79. package/dist/data-grid/use-data-grid.js.map +1 -1
  80. package/dist/data-grid/utils.d.ts +7 -0
  81. package/dist/data-grid/utils.js +3 -1
  82. package/dist/data-grid/utils.js.map +1 -1
  83. package/dist/i18n/locale-provider.d.ts +18 -0
  84. package/dist/i18n/locale-provider.js.map +1 -1
  85. package/dist/i18n/namespaces/rating.js +2 -1
  86. package/dist/i18n/namespaces/rating.js.map +1 -1
  87. package/dist/i18n/namespaces/search-filter-bar.js +8 -6
  88. package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
  89. package/dist/i18n/namespaces/shell.d.ts +4 -0
  90. package/dist/i18n/namespaces/shell.js +1 -1
  91. package/dist/i18n/namespaces/shell.js.map +1 -1
  92. package/dist/i18n/namespaces/table-pagination.js +6 -4
  93. package/dist/i18n/namespaces/table-pagination.js.map +1 -1
  94. package/dist/i18n/namespaces/toast.d.ts +3 -2
  95. package/dist/i18n/namespaces/toast.js.map +1 -1
  96. package/dist/index.d.ts +2 -1
  97. package/dist/lib/collect-item-elements.d.ts +47 -0
  98. package/dist/lib/collect-item-elements.js +22 -0
  99. package/dist/lib/collect-item-elements.js.map +1 -0
  100. package/dist/lib/text-search.d.ts +32 -0
  101. package/dist/lib/text-search.js +17 -0
  102. package/dist/lib/text-search.js.map +1 -0
  103. package/dist/lib/toast-language.d.ts +8 -0
  104. package/dist/lib/toast-language.js +19 -0
  105. package/dist/lib/toast-language.js.map +1 -0
  106. package/dist/shell/app-shell.d.ts +1 -0
  107. package/dist/shell/index.d.ts +2 -1
  108. package/dist/shell/shell-context.d.ts +1 -0
  109. package/dist/shell/utils.d.ts +2 -1
  110. 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.delete(id);
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
- const observer = new IntersectionObserver(
157
- (entries) => {
158
- if (isScrollingRef.current) return;
159
- if (rafIdRef.current !== null) {
160
- cancelAnimationFrame(rafIdRef.current);
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
- rafIdRef.current = requestAnimationFrame(() => {
163
- const intersecting = entries.filter((entry) => entry.isIntersecting);
164
- if (intersecting.length === 0) return;
165
- const topmost = intersecting.reduce((prev, curr) => {
166
- return curr.boundingClientRect.top < prev.boundingClientRect.top ? curr : prev;
167
- });
168
- const id = topmost.target.id;
169
- if (id && sectionMap.has(id)) {
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
- root: scrollContainer,
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
- observer.observe(element);
192
+ sectionObserver.observe(element);
182
193
  }
183
194
  return () => {
184
- observer.disconnect();
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
- }, [offset, rootMargin, threshold, scrollContainer]);
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 sectionRef = React.useRef(null);
308
- const composedRef = useComposedRefs(ref, sectionRef);
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({ className, defaultValue, value, min = 0, max = 100, ...props }) {
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\nfunction Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: SliderProps) {\n const _values = React.useMemo(\n () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),\n [value, defaultValue, min, max]\n );\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 >\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 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":";AAwCI,SAgBI,KAhBJ;AAtCJ,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AA6BnB,SAAS,OAAO,EAAE,WAAW,cAAc,OAAO,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,GAAgB;AAC7F,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;AAEA,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,MAEJ;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,YAGV,WAAU;AAAA;AAAA,UADL;AAAA,QAEP,CACD;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
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":";AAiCI;AA/BJ,SAAS,gBAAgB;AAEzB,SAAS,WAAW,cAAiC;AAyBrD,MAAM,UAAU,CAAC,EAAE,GAAG,MAAM,MAAoB;AAC9C,QAAM,EAAE,QAAQ,SAAS,IAAI,SAAS;AAEtC,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":[]}
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
- return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
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
- getState: () => stateRef.current,
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;