@zentauri-ui/zentauri-components 1.3.1 → 1.4.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 (307) hide show
  1. package/README.md +78 -0
  2. package/cli/cli.integration.test.ts +51 -0
  3. package/cli/index.mjs +664 -0
  4. package/cli/registry.json +36 -0
  5. package/cli/rewrite-imports.mjs +57 -0
  6. package/cli/rewrite-imports.test.ts +71 -0
  7. package/dist/ui/slider/slider.d.ts +18 -0
  8. package/dist/ui/slider/slider.d.ts.map +1 -1
  9. package/dist/ui/slider.js +21 -25
  10. package/dist/ui/slider.js.map +1 -1
  11. package/dist/ui/slider.mjs +21 -25
  12. package/dist/ui/slider.mjs.map +1 -1
  13. package/package.json +8 -2
  14. package/src/hooks/index.ts +48 -0
  15. package/src/hooks/useBodyScrollLock/index.ts +1 -0
  16. package/src/hooks/useBodyScrollLock/useBodyScrollLock.test.ts +51 -0
  17. package/src/hooks/useBodyScrollLock/useBodyScrollLock.ts +48 -0
  18. package/src/hooks/useClickOutside/index.ts +5 -0
  19. package/src/hooks/useClickOutside/useClickOutside.test.tsx +60 -0
  20. package/src/hooks/useClickOutside/useClickOutside.ts +52 -0
  21. package/src/hooks/useClipboard/index.ts +1 -0
  22. package/src/hooks/useClipboard/useClipboard.test.ts +101 -0
  23. package/src/hooks/useClipboard/useClipboard.ts +69 -0
  24. package/src/hooks/useControllableState/index.ts +4 -0
  25. package/src/hooks/useControllableState/useControllableState.test.ts +59 -0
  26. package/src/hooks/useControllableState/useControllableState.ts +49 -0
  27. package/src/hooks/useDebouncedValue/index.ts +1 -0
  28. package/src/hooks/useDebouncedValue/useDebouncedValue.test.ts +74 -0
  29. package/src/hooks/useDebouncedValue/useDebouncedValue.ts +29 -0
  30. package/src/hooks/useDisclosure/index.ts +5 -0
  31. package/src/hooks/useDisclosure/useDisclosure.test.ts +64 -0
  32. package/src/hooks/useDisclosure/useDisclosure.ts +62 -0
  33. package/src/hooks/useDocumentTitle/index.ts +4 -0
  34. package/src/hooks/useDocumentTitle/useDocumentTitle.test.ts +40 -0
  35. package/src/hooks/useDocumentTitle/useDocumentTitle.ts +58 -0
  36. package/src/hooks/useFocusManagement/index.ts +1 -0
  37. package/src/hooks/useFocusManagement/useFocusManagement.test.tsx +45 -0
  38. package/src/hooks/useFocusManagement/useFocusManagement.ts +77 -0
  39. package/src/hooks/useHover/index.ts +1 -0
  40. package/src/hooks/useHover/useHover.test.ts +45 -0
  41. package/src/hooks/useHover/useHover.ts +45 -0
  42. package/src/hooks/useInView/index.ts +1 -0
  43. package/src/hooks/useInView/useInView.test.ts +43 -0
  44. package/src/hooks/useInView/useInView.ts +28 -0
  45. package/src/hooks/useIntersectionObserver/index.ts +4 -0
  46. package/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts +75 -0
  47. package/src/hooks/useIntersectionObserver/useIntersectionObserver.ts +54 -0
  48. package/src/hooks/useIsMounted/index.ts +1 -0
  49. package/src/hooks/useIsMounted/useIsMounted.test.ts +25 -0
  50. package/src/hooks/useIsMounted/useIsMounted.ts +22 -0
  51. package/src/hooks/useIsomorphicLayoutEffect/index.ts +1 -0
  52. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.ts +19 -0
  53. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +12 -0
  54. package/src/hooks/useLocalStorage/index.ts +4 -0
  55. package/src/hooks/useLocalStorage/useLocalStorage.test.ts +99 -0
  56. package/src/hooks/useLocalStorage/useLocalStorage.ts +109 -0
  57. package/src/hooks/useMediaQuery/index.ts +1 -0
  58. package/src/hooks/useMediaQuery/useMediaQuery.test.ts +63 -0
  59. package/src/hooks/useMediaQuery/useMediaQuery.ts +37 -0
  60. package/src/hooks/useNetworkStatus/index.ts +1 -0
  61. package/src/hooks/useNetworkStatus/useNetworkStatus.test.ts +53 -0
  62. package/src/hooks/useNetworkStatus/useNetworkStatus.ts +33 -0
  63. package/src/hooks/usePageVisibility/index.ts +1 -0
  64. package/src/hooks/usePageVisibility/usePageVisibility.test.ts +21 -0
  65. package/src/hooks/usePageVisibility/usePageVisibility.ts +31 -0
  66. package/src/hooks/usePagination/index.ts +6 -0
  67. package/src/hooks/usePagination/usePagination.test.ts +139 -0
  68. package/src/hooks/usePagination/usePagination.ts +153 -0
  69. package/src/hooks/usePrefersColorScheme/index.ts +4 -0
  70. package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.test.ts +53 -0
  71. package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.ts +21 -0
  72. package/src/hooks/usePrefersReducedMotion/index.ts +1 -0
  73. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.test.ts +27 -0
  74. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.ts +14 -0
  75. package/src/hooks/useResizeObserver/index.ts +4 -0
  76. package/src/hooks/useResizeObserver/useResizeObserver.test.ts +68 -0
  77. package/src/hooks/useResizeObserver/useResizeObserver.ts +58 -0
  78. package/src/hooks/useSessionStorage/index.ts +4 -0
  79. package/src/hooks/useSessionStorage/useSessionStorage.test.ts +54 -0
  80. package/src/hooks/useSessionStorage/useSessionStorage.ts +84 -0
  81. package/src/hooks/useThrottledCallback/index.ts +1 -0
  82. package/src/hooks/useThrottledCallback/useThrottledCallback.test.ts +75 -0
  83. package/src/hooks/useThrottledCallback/useThrottledCallback.ts +36 -0
  84. package/src/hooks/useToggle/index.ts +1 -0
  85. package/src/hooks/useToggle/useToggle.test.ts +40 -0
  86. package/src/hooks/useToggle/useToggle.ts +22 -0
  87. package/src/hooks/useWindowSize/index.ts +1 -0
  88. package/src/hooks/useWindowSize/useWindowSize.test.ts +23 -0
  89. package/src/hooks/useWindowSize/useWindowSize.ts +39 -0
  90. package/src/lib/utils.ts +25 -0
  91. package/src/ui/accordion/accordion-base.tsx +223 -0
  92. package/src/ui/accordion/accordion.test.tsx +146 -0
  93. package/src/ui/accordion/accordion.tsx +11 -0
  94. package/src/ui/accordion/animated/accordion-content-animated.tsx +46 -0
  95. package/src/ui/accordion/animated/accordion-root-animated.tsx +10 -0
  96. package/src/ui/accordion/animated/animations.ts +16 -0
  97. package/src/ui/accordion/animated/index.ts +7 -0
  98. package/src/ui/accordion/animated/types.ts +7 -0
  99. package/src/ui/accordion/index.ts +23 -0
  100. package/src/ui/accordion/types.ts +48 -0
  101. package/src/ui/accordion/variants.ts +115 -0
  102. package/src/ui/alert/alert-base.tsx +157 -0
  103. package/src/ui/alert/alert.test.tsx +150 -0
  104. package/src/ui/alert/alert.tsx +9 -0
  105. package/src/ui/alert/animated/alert-animated.tsx +20 -0
  106. package/src/ui/alert/animated/animations.ts +20 -0
  107. package/src/ui/alert/animated/index.ts +3 -0
  108. package/src/ui/alert/animated/types.ts +16 -0
  109. package/src/ui/alert/index.ts +22 -0
  110. package/src/ui/alert/types.ts +28 -0
  111. package/src/ui/alert/variants.ts +74 -0
  112. package/src/ui/avatar/animated/animations.ts +11 -0
  113. package/src/ui/avatar/animated/avatar-animated.tsx +25 -0
  114. package/src/ui/avatar/animated/index.ts +6 -0
  115. package/src/ui/avatar/animated/types.ts +16 -0
  116. package/src/ui/avatar/avatar-base.tsx +184 -0
  117. package/src/ui/avatar/avatar.test.tsx +51 -0
  118. package/src/ui/avatar/avatar.tsx +11 -0
  119. package/src/ui/avatar/index.ts +16 -0
  120. package/src/ui/avatar/types.ts +36 -0
  121. package/src/ui/avatar/variants.ts +52 -0
  122. package/src/ui/badge/animated/animations.ts +20 -0
  123. package/src/ui/badge/animated/badge-animated.tsx +28 -0
  124. package/src/ui/badge/animated/index.ts +5 -0
  125. package/src/ui/badge/animated/types.ts +18 -0
  126. package/src/ui/badge/badge-base.tsx +53 -0
  127. package/src/ui/badge/badge.test.tsx +48 -0
  128. package/src/ui/badge/badge.tsx +9 -0
  129. package/src/ui/badge/index.ts +5 -0
  130. package/src/ui/badge/types.ts +25 -0
  131. package/src/ui/badge/variants.ts +85 -0
  132. package/src/ui/breadcrumb/breadcrumb.test.tsx +62 -0
  133. package/src/ui/breadcrumb/breadcrumb.tsx +135 -0
  134. package/src/ui/breadcrumb/index.ts +28 -0
  135. package/src/ui/breadcrumb/types.ts +29 -0
  136. package/src/ui/breadcrumb/variants.ts +53 -0
  137. package/src/ui/buttons/animated/animations.ts +34 -0
  138. package/src/ui/buttons/animated/button-animated.tsx +70 -0
  139. package/src/ui/buttons/animated/index.ts +5 -0
  140. package/src/ui/buttons/animated/types.ts +29 -0
  141. package/src/ui/buttons/button-base.tsx +59 -0
  142. package/src/ui/buttons/button.test.tsx +480 -0
  143. package/src/ui/buttons/button.tsx +9 -0
  144. package/src/ui/buttons/index.ts +5 -0
  145. package/src/ui/buttons/types.ts +14 -0
  146. package/src/ui/buttons/variants.ts +77 -0
  147. package/src/ui/card/animated/animations.ts +32 -0
  148. package/src/ui/card/animated/card-animated.tsx +28 -0
  149. package/src/ui/card/animated/index.ts +12 -0
  150. package/src/ui/card/animated/types.ts +8 -0
  151. package/src/ui/card/card-base.tsx +146 -0
  152. package/src/ui/card/card.test.tsx +79 -0
  153. package/src/ui/card/card.tsx +11 -0
  154. package/src/ui/card/index.ts +21 -0
  155. package/src/ui/card/types.ts +42 -0
  156. package/src/ui/card/variants.ts +122 -0
  157. package/src/ui/divider/animated/animations.ts +27 -0
  158. package/src/ui/divider/animated/divider-animated.tsx +24 -0
  159. package/src/ui/divider/animated/index.ts +4 -0
  160. package/src/ui/divider/animated/types.ts +18 -0
  161. package/src/ui/divider/divider-base.tsx +80 -0
  162. package/src/ui/divider/divider.tsx +9 -0
  163. package/src/ui/divider/index.ts +14 -0
  164. package/src/ui/divider/types.ts +18 -0
  165. package/src/ui/divider/variants.ts +98 -0
  166. package/src/ui/drawer/animated/animations.ts +39 -0
  167. package/src/ui/drawer/animated/drawer-content-animated.tsx +101 -0
  168. package/src/ui/drawer/animated/index.ts +14 -0
  169. package/src/ui/drawer/animated/types.ts +18 -0
  170. package/src/ui/drawer/drawer-base.tsx +259 -0
  171. package/src/ui/drawer/drawer.test.tsx +132 -0
  172. package/src/ui/drawer/drawer.tsx +11 -0
  173. package/src/ui/drawer/index.ts +21 -0
  174. package/src/ui/drawer/types.ts +39 -0
  175. package/src/ui/drawer/variants.ts +122 -0
  176. package/src/ui/dropdown/dropdown.test.tsx +114 -0
  177. package/src/ui/dropdown/dropdown.tsx +179 -0
  178. package/src/ui/dropdown/index.ts +15 -0
  179. package/src/ui/dropdown/types.ts +68 -0
  180. package/src/ui/dropdown/variants.ts +138 -0
  181. package/src/ui/empty-state/animated/animations.ts +19 -0
  182. package/src/ui/empty-state/animated/empty-state-animated.tsx +23 -0
  183. package/src/ui/empty-state/animated/index.ts +7 -0
  184. package/src/ui/empty-state/animated/types.ts +26 -0
  185. package/src/ui/empty-state/empty-state-base.tsx +114 -0
  186. package/src/ui/empty-state/empty-state.tsx +9 -0
  187. package/src/ui/empty-state/index.ts +10 -0
  188. package/src/ui/empty-state/types.ts +19 -0
  189. package/src/ui/empty-state/variants.ts +51 -0
  190. package/src/ui/file-upload/file-upload.test.tsx +36 -0
  191. package/src/ui/file-upload/file-upload.tsx +119 -0
  192. package/src/ui/file-upload/index.ts +5 -0
  193. package/src/ui/file-upload/types.ts +21 -0
  194. package/src/ui/file-upload/variants.ts +29 -0
  195. package/src/ui/inputs/animated/animations.ts +36 -0
  196. package/src/ui/inputs/animated/index.ts +5 -0
  197. package/src/ui/inputs/animated/input-animated.tsx +124 -0
  198. package/src/ui/inputs/animated/types.ts +40 -0
  199. package/src/ui/inputs/index.ts +5 -0
  200. package/src/ui/inputs/input-base.tsx +114 -0
  201. package/src/ui/inputs/input.test.tsx +414 -0
  202. package/src/ui/inputs/input.tsx +8 -0
  203. package/src/ui/inputs/types.ts +18 -0
  204. package/src/ui/inputs/variants.ts +316 -0
  205. package/src/ui/modal/animated/animations.ts +29 -0
  206. package/src/ui/modal/animated/index.ts +5 -0
  207. package/src/ui/modal/animated/modal-content-animated.tsx +96 -0
  208. package/src/ui/modal/animated/types.ts +23 -0
  209. package/src/ui/modal/index.ts +21 -0
  210. package/src/ui/modal/modal-base.tsx +279 -0
  211. package/src/ui/modal/modal.test.tsx +129 -0
  212. package/src/ui/modal/modal.tsx +8 -0
  213. package/src/ui/modal/types.ts +31 -0
  214. package/src/ui/modal/variants.ts +109 -0
  215. package/src/ui/pagination/index.ts +13 -0
  216. package/src/ui/pagination/pagination.test.tsx +165 -0
  217. package/src/ui/pagination/pagination.tsx +237 -0
  218. package/src/ui/pagination/types.ts +66 -0
  219. package/src/ui/pagination/variants.ts +97 -0
  220. package/src/ui/progress/animated/animations.ts +9 -0
  221. package/src/ui/progress/animated/index.ts +17 -0
  222. package/src/ui/progress/animated/progress-animated.tsx +133 -0
  223. package/src/ui/progress/animated/types.ts +35 -0
  224. package/src/ui/progress/index.ts +10 -0
  225. package/src/ui/progress/progress-base.tsx +151 -0
  226. package/src/ui/progress/progress.test.tsx +84 -0
  227. package/src/ui/progress/progress.tsx +12 -0
  228. package/src/ui/progress/types.ts +33 -0
  229. package/src/ui/progress/variants.ts +105 -0
  230. package/src/ui/select/index.ts +25 -0
  231. package/src/ui/select/select.test.tsx +128 -0
  232. package/src/ui/select/select.tsx +221 -0
  233. package/src/ui/select/types.ts +77 -0
  234. package/src/ui/select/variants.ts +163 -0
  235. package/src/ui/skeleton/animated/animations.ts +15 -0
  236. package/src/ui/skeleton/animated/index.ts +20 -0
  237. package/src/ui/skeleton/animated/skeleton-animated.tsx +119 -0
  238. package/src/ui/skeleton/animated/types.ts +49 -0
  239. package/src/ui/skeleton/index.ts +24 -0
  240. package/src/ui/skeleton/skeleton-base.tsx +288 -0
  241. package/src/ui/skeleton/skeleton.tsx +8 -0
  242. package/src/ui/skeleton/types.ts +31 -0
  243. package/src/ui/skeleton/variants.ts +254 -0
  244. package/src/ui/slider/index.ts +22 -0
  245. package/src/ui/slider/slider.test.tsx +94 -0
  246. package/src/ui/slider/slider.tsx +728 -0
  247. package/src/ui/slider/types.ts +66 -0
  248. package/src/ui/slider/variants.ts +81 -0
  249. package/src/ui/spinner/animated/index.ts +5 -0
  250. package/src/ui/spinner/animated/spinner.test.tsx +41 -0
  251. package/src/ui/spinner/animated/spinner.tsx +143 -0
  252. package/src/ui/spinner/animated/types.ts +11 -0
  253. package/src/ui/spinner/animated/variants.ts +50 -0
  254. package/src/ui/stepper/index.ts +22 -0
  255. package/src/ui/stepper/stepper.test.tsx +183 -0
  256. package/src/ui/stepper/stepper.tsx +172 -0
  257. package/src/ui/stepper/types.ts +32 -0
  258. package/src/ui/stepper/variants.ts +69 -0
  259. package/src/ui/table/animated/animations.ts +9 -0
  260. package/src/ui/table/animated/index.ts +15 -0
  261. package/src/ui/table/animated/table-animated.tsx +15 -0
  262. package/src/ui/table/animated/types.ts +16 -0
  263. package/src/ui/table/index.ts +22 -0
  264. package/src/ui/table/table-base.tsx +197 -0
  265. package/src/ui/table/table.tsx +13 -0
  266. package/src/ui/table/types.ts +47 -0
  267. package/src/ui/table/variants.ts +105 -0
  268. package/src/ui/tabs/animated/animations.ts +48 -0
  269. package/src/ui/tabs/animated/index.ts +8 -0
  270. package/src/ui/tabs/animated/tabs-content-animated.tsx +46 -0
  271. package/src/ui/tabs/animated/types.ts +24 -0
  272. package/src/ui/tabs/index.ts +10 -0
  273. package/src/ui/tabs/tabs-base.tsx +185 -0
  274. package/src/ui/tabs/tabs.test.tsx +53 -0
  275. package/src/ui/tabs/tabs.tsx +2 -0
  276. package/src/ui/tabs/types.ts +88 -0
  277. package/src/ui/tabs/variants.ts +70 -0
  278. package/src/ui/toast/animated/animations.ts +17 -0
  279. package/src/ui/toast/animated/index.ts +9 -0
  280. package/src/ui/toast/animated/toast-animated.tsx +96 -0
  281. package/src/ui/toast/animated/types.ts +13 -0
  282. package/src/ui/toast/index.ts +26 -0
  283. package/src/ui/toast/toast-base.tsx +231 -0
  284. package/src/ui/toast/toast.test.tsx +102 -0
  285. package/src/ui/toast/toast.tsx +13 -0
  286. package/src/ui/toast/types.ts +57 -0
  287. package/src/ui/toast/variants.ts +73 -0
  288. package/src/ui/toggle/animated/animations.ts +9 -0
  289. package/src/ui/toggle/animated/index.ts +7 -0
  290. package/src/ui/toggle/animated/toggle-animated.tsx +76 -0
  291. package/src/ui/toggle/animated/types.ts +13 -0
  292. package/src/ui/toggle/index.ts +5 -0
  293. package/src/ui/toggle/toggle-base.tsx +70 -0
  294. package/src/ui/toggle/toggle.test.tsx +44 -0
  295. package/src/ui/toggle/toggle.tsx +9 -0
  296. package/src/ui/toggle/types.ts +18 -0
  297. package/src/ui/toggle/variants.ts +84 -0
  298. package/src/ui/tooltip/animated/animations.ts +16 -0
  299. package/src/ui/tooltip/animated/index.ts +10 -0
  300. package/src/ui/tooltip/animated/tooltip-content-animated.tsx +47 -0
  301. package/src/ui/tooltip/animated/types.ts +19 -0
  302. package/src/ui/tooltip/index.ts +17 -0
  303. package/src/ui/tooltip/tooltip-base.tsx +152 -0
  304. package/src/ui/tooltip/tooltip.test.tsx +84 -0
  305. package/src/ui/tooltip/tooltip.tsx +8 -0
  306. package/src/ui/tooltip/types.ts +57 -0
  307. package/src/ui/tooltip/variants.ts +61 -0
@@ -0,0 +1,43 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { useInView } from "./useInView";
5
+
6
+ describe("useInView", () => {
7
+ const OriginalIO = globalThis.IntersectionObserver;
8
+ let lastCallback: IntersectionObserverCallback | undefined;
9
+
10
+ afterEach(() => {
11
+ globalThis.IntersectionObserver = OriginalIO;
12
+ lastCallback = undefined;
13
+ });
14
+
15
+ it("should derive inView from isIntersecting", () => {
16
+ class MockIntersectionObserver {
17
+ constructor(cb: IntersectionObserverCallback) {
18
+ lastCallback = cb;
19
+ }
20
+
21
+ observe = vi.fn();
22
+
23
+ disconnect = vi.fn();
24
+ }
25
+
26
+ globalThis.IntersectionObserver =
27
+ MockIntersectionObserver as unknown as typeof IntersectionObserver;
28
+
29
+ const { result } = renderHook(() => useInView());
30
+ const el = document.createElement("div");
31
+ act(() => {
32
+ result.current[0](el);
33
+ });
34
+ expect(result.current[1]).toBe(false);
35
+ act(() => {
36
+ lastCallback?.(
37
+ [{ isIntersecting: true, target: el } as unknown as IntersectionObserverEntry],
38
+ {} as IntersectionObserver,
39
+ );
40
+ });
41
+ expect(result.current[1]).toBe(true);
42
+ });
43
+ });
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import type { RefCallback } from "react";
4
+
5
+ import { useIntersectionObserver } from "../useIntersectionObserver";
6
+
7
+ export type UseInViewParams = IntersectionObserverInit & {
8
+ /** When `false`, no observer is attached (useful to pause work when off-screen lists are virtualized). */
9
+ enabled?: boolean;
10
+ };
11
+
12
+ /**
13
+ * Convenience wrapper around {@link useIntersectionObserver} that exposes only a boolean `inView` flag.
14
+ *
15
+ * `inView` is `true` when the latest `IntersectionObserverEntry.isIntersecting` is truthy; otherwise `false`.
16
+ * Accepts the same `root`, `rootMargin`, `threshold`, and `enabled` options as the underlying observer.
17
+ *
18
+ * @typeParam T - Observed element type.
19
+ * @param params - IntersectionObserver options plus optional `enabled` flag.
20
+ * @returns `[setRef, inView]` callback ref and intersection boolean.
21
+ */
22
+ export function useInView<T extends Element>(
23
+ params: UseInViewParams = {},
24
+ ): [RefCallback<T>, boolean] {
25
+ const [setRef, entry] = useIntersectionObserver<T>(params);
26
+ const inView = Boolean(entry?.isIntersecting);
27
+ return [setRef, inView];
28
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ useIntersectionObserver,
3
+ type UseIntersectionObserverParams,
4
+ } from "./useIntersectionObserver";
@@ -0,0 +1,75 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { useIntersectionObserver } from "./useIntersectionObserver";
5
+
6
+ describe("useIntersectionObserver", () => {
7
+ const OriginalIO = globalThis.IntersectionObserver;
8
+ let lastCallback: IntersectionObserverCallback | undefined;
9
+
10
+ afterEach(() => {
11
+ globalThis.IntersectionObserver = OriginalIO;
12
+ lastCallback = undefined;
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ it("should observe element and expose latest entry from callback", () => {
17
+ const observeSpy = vi.fn();
18
+ class MockIntersectionObserver {
19
+ constructor(cb: IntersectionObserverCallback) {
20
+ lastCallback = cb;
21
+ }
22
+
23
+ observe = observeSpy;
24
+
25
+ disconnect = vi.fn();
26
+ }
27
+
28
+ globalThis.IntersectionObserver =
29
+ MockIntersectionObserver as unknown as typeof IntersectionObserver;
30
+
31
+ const { result } = renderHook(() => useIntersectionObserver());
32
+ const node = document.createElement("div");
33
+ act(() => {
34
+ result.current[0](node);
35
+ });
36
+ expect(observeSpy).toHaveBeenCalledWith(node);
37
+ const rect = node.getBoundingClientRect();
38
+ const fakeEntry: IntersectionObserverEntry = {
39
+ boundingClientRect: rect,
40
+ intersectionRect: rect,
41
+ intersectionRatio: 1,
42
+ isIntersecting: true,
43
+ rootBounds: null,
44
+ target: node,
45
+ time: 0,
46
+ };
47
+ act(() => {
48
+ lastCallback?.([fakeEntry], {} as IntersectionObserver);
49
+ });
50
+ expect(result.current[1]?.isIntersecting).toBe(true);
51
+ });
52
+
53
+ it("should not observe when enabled is false", () => {
54
+ const observeSpy = vi.fn();
55
+ class MockIntersectionObserver {
56
+ constructor(_cb: IntersectionObserverCallback) {}
57
+
58
+ observe = observeSpy;
59
+
60
+ disconnect = vi.fn();
61
+ }
62
+
63
+ globalThis.IntersectionObserver =
64
+ MockIntersectionObserver as unknown as typeof IntersectionObserver;
65
+
66
+ const { result } = renderHook(() =>
67
+ useIntersectionObserver({ enabled: false }),
68
+ );
69
+ const node = document.createElement("div");
70
+ act(() => {
71
+ result.current[0](node);
72
+ });
73
+ expect(observeSpy).not.toHaveBeenCalled();
74
+ });
75
+ });
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import type { RefCallback } from "react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+
6
+ export type UseIntersectionObserverParams = IntersectionObserverInit & {
7
+ /** When `false`, disconnects the observer until re-enabled (saves work for hidden or inactive content). */
8
+ enabled?: boolean;
9
+ };
10
+
11
+ /**
12
+ * Observes a single element with the browser `IntersectionObserver` API and exposes the latest entry.
13
+ *
14
+ * Returns a callback ref: assign it to the element to measure. When `enabled` is false or `IntersectionObserver`
15
+ * is undefined (unsupported environment), the effect is a no-op and `entry` may stay `undefined`.
16
+ *
17
+ * @typeParam T - Observed element type.
18
+ * @param params - Standard `IntersectionObserverInit` fields plus optional `enabled` (default `true`).
19
+ * @returns `[setRef, entry]` where `entry` is the most recent callback record for the observed target.
20
+ */
21
+ export function useIntersectionObserver<T extends Element>(
22
+ params: UseIntersectionObserverParams = {},
23
+ ): [RefCallback<T>, IntersectionObserverEntry | undefined] {
24
+ const { enabled = true, root, rootMargin, threshold } = params;
25
+ const [element, setElement] = useState<T | null>(null);
26
+ const [entry, setEntry] = useState<IntersectionObserverEntry | undefined>(
27
+ undefined,
28
+ );
29
+
30
+ const setRef = useCallback((node: T | null) => {
31
+ setElement(node);
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (!enabled || element == null) {
36
+ return;
37
+ }
38
+ if (typeof IntersectionObserver === "undefined") {
39
+ return;
40
+ }
41
+ const observer = new IntersectionObserver(
42
+ (records) => {
43
+ setEntry(records[0]);
44
+ },
45
+ { root, rootMargin, threshold },
46
+ );
47
+ observer.observe(element);
48
+ return () => {
49
+ observer.disconnect();
50
+ };
51
+ }, [element, enabled, root, rootMargin, threshold]);
52
+
53
+ return [setRef, entry];
54
+ }
@@ -0,0 +1 @@
1
+ export { useIsMounted } from "./useIsMounted";
@@ -0,0 +1,25 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { useIsMounted } from "./useIsMounted";
5
+
6
+ describe("useIsMounted", () => {
7
+ it("should return true after mount", () => {
8
+ const { result } = renderHook(() => useIsMounted());
9
+ expect(result.current()).toBe(true);
10
+ });
11
+
12
+ it("should return false after unmount", () => {
13
+ const { result, unmount } = renderHook(() => useIsMounted());
14
+ expect(result.current()).toBe(true);
15
+ unmount();
16
+ expect(result.current()).toBe(false);
17
+ });
18
+
19
+ it("should keep stable function identity", () => {
20
+ const { result, rerender } = renderHook(() => useIsMounted());
21
+ const first = result.current;
22
+ rerender();
23
+ expect(result.current).toBe(first);
24
+ });
25
+ });
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+
5
+ /**
6
+ * Returns a stable function that reports whether the component is currently mounted.
7
+ *
8
+ * Useful to guard async completions (fetch, timers) so `setState` does not run after unmount.
9
+ * The returned function identity is stable across renders; it reads a ref updated in an effect.
10
+ *
11
+ * @returns `() => boolean` — `true` after mount until unmount cleanup runs.
12
+ */
13
+ export function useIsMounted(): () => boolean {
14
+ const mounted = useRef(false);
15
+ useEffect(() => {
16
+ mounted.current = true;
17
+ return () => {
18
+ mounted.current = false;
19
+ };
20
+ }, []);
21
+ return useCallback(() => mounted.current, []);
22
+ }
@@ -0,0 +1 @@
1
+ export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
@@ -0,0 +1,19 @@
1
+ import { useRef } from "react";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
6
+
7
+ describe("useIsomorphicLayoutEffect", () => {
8
+ it("should run effect in jsdom without throwing", () => {
9
+ const order: string[] = [];
10
+ renderHook(() => {
11
+ const ref = useRef(false);
12
+ useIsomorphicLayoutEffect(() => {
13
+ order.push("effect");
14
+ ref.current = true;
15
+ }, []);
16
+ });
17
+ expect(order).toEqual(["effect"]);
18
+ });
19
+ });
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { useEffect, useLayoutEffect } from "react";
4
+
5
+ /**
6
+ * Runs `useLayoutEffect` in the browser and `useEffect` on the server to avoid SSR warnings.
7
+ *
8
+ * Use for DOM measurements or synchronous paint updates that must run before the browser paints,
9
+ * but only when the code path is safe on the client; on the server the timing matches `useEffect`.
10
+ */
11
+ export const useIsomorphicLayoutEffect =
12
+ typeof window !== "undefined" ? useLayoutEffect : useEffect;
@@ -0,0 +1,4 @@
1
+ export {
2
+ useLocalStorage,
3
+ type UseLocalStorageResult,
4
+ } from "./useLocalStorage";
@@ -0,0 +1,99 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { useLocalStorage } from "./useLocalStorage";
5
+
6
+ const keyPrefix = "zt-test-localstorage";
7
+
8
+ describe("useLocalStorage", () => {
9
+ afterEach(() => {
10
+ localStorage.clear();
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ it("should read existing JSON value on mount", () => {
15
+ const key = `${keyPrefix}-read`;
16
+ localStorage.setItem(key, JSON.stringify({ count: 2 }));
17
+ const { result } = renderHook(() =>
18
+ useLocalStorage(key, { count: 0 }),
19
+ );
20
+ const value = result.current[0];
21
+ expect(value).toEqual({ count: 2 });
22
+ });
23
+
24
+ it("should fall back to initialValue when key missing", () => {
25
+ const key = `${keyPrefix}-missing`;
26
+ const { result } = renderHook(() =>
27
+ useLocalStorage(key, { count: 0 }),
28
+ );
29
+ const value = result.current[0];
30
+ expect(value).toEqual({ count: 0 });
31
+ });
32
+
33
+ it("should persist setValue and support functional updates", () => {
34
+ const key = `${keyPrefix}-set`;
35
+ const { result } = renderHook(() =>
36
+ useLocalStorage(key, { count: 0 }),
37
+ );
38
+ act(() => {
39
+ result.current[1]({ count: 5 });
40
+ });
41
+ expect(result.current[0]).toEqual({ count: 5 });
42
+ expect(localStorage.getItem(key)).toBe(JSON.stringify({ count: 5 }));
43
+ act(() => {
44
+ result.current[1]((previous) => ({ count: previous.count + 1 }));
45
+ });
46
+ expect(result.current[0]).toEqual({ count: 6 });
47
+ });
48
+
49
+ it("should remove key and reset to initialValue", () => {
50
+ const key = `${keyPrefix}-remove`;
51
+ localStorage.setItem(key, JSON.stringify({ ok: true }));
52
+ const { result } = renderHook(() =>
53
+ useLocalStorage(key, { ok: false }),
54
+ );
55
+ const remove = result.current[2];
56
+ act(() => {
57
+ remove();
58
+ });
59
+ expect(localStorage.getItem(key)).toBeNull();
60
+ expect(result.current[0]).toEqual({ ok: false });
61
+ });
62
+
63
+ it("should reconcile when storage event fires for same key", () => {
64
+ const key = `${keyPrefix}-storage`;
65
+ const { result } = renderHook(() =>
66
+ useLocalStorage(key, { v: 0 }),
67
+ );
68
+ act(() => {
69
+ window.dispatchEvent(
70
+ new StorageEvent("storage", {
71
+ key,
72
+ newValue: JSON.stringify({ v: 99 }),
73
+ storageArea: window.localStorage,
74
+ }),
75
+ );
76
+ });
77
+ const value= result.current[0];
78
+ expect(value).toEqual({ v: 99 });
79
+ });
80
+
81
+ it("should reset to initial when storage event clears key", () => {
82
+ const key = `${keyPrefix}-cleared`;
83
+ localStorage.setItem(key, JSON.stringify({ v: 1 }));
84
+ const { result } = renderHook(() =>
85
+ useLocalStorage(key, { v: 0 }),
86
+ );
87
+ act(() => {
88
+ window.dispatchEvent(
89
+ new StorageEvent("storage", {
90
+ key,
91
+ newValue: null,
92
+ storageArea: window.localStorage,
93
+ }),
94
+ );
95
+ });
96
+ const value= result.current[0];
97
+ expect(value).toEqual({ v: 0 });
98
+ });
99
+ });
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+
5
+ function readValue<T>(key: string, fallback: T): T {
6
+ if (typeof window === "undefined") {
7
+ return fallback;
8
+ }
9
+ try {
10
+ const raw = window.localStorage.getItem(key);
11
+ if (raw == null) {
12
+ return fallback;
13
+ }
14
+ return JSON.parse(raw) as T;
15
+ } catch {
16
+ return fallback;
17
+ }
18
+ }
19
+
20
+ export type UseLocalStorageResult<T> = [
21
+ T,
22
+ (value: T | ((previous: T) => T)) => void,
23
+ () => void,
24
+ ];
25
+
26
+ /**
27
+ * Syncs JSON-serialized state with `window.localStorage` under a fixed key.
28
+ *
29
+ * - Initial read runs on the client; SSR uses `initialValue`.
30
+ * - `setValue` persists with `JSON.stringify` and updates React state; functional updates supported.
31
+ * - `remove` clears the key and resets state to `initialValue`.
32
+ * - Subscribes to the `storage` event so updates from other tabs (or same tab via `storage` dispatch quirks) reconcile.
33
+ * - Read/write failures (private mode, quota) fall back silently on read; write errors are swallowed except `remove` logs on failure.
34
+ *
35
+ * @typeParam T - Stored value type; must be compatible with `JSON.stringify` / `parse`.
36
+ * @param key - `localStorage` key.
37
+ * @param initialValue - Fallback when missing, invalid JSON, or during SSR.
38
+ * @returns `[stored, setValue, remove]` tuple mirroring `useState` plus explicit removal.
39
+ */
40
+ export function useLocalStorage<T>(
41
+ key: string,
42
+ initialValue: T,
43
+ ): UseLocalStorageResult<T> {
44
+ const [stored, setStored] = useState<T>(() =>
45
+ readValue(key, initialValue),
46
+ );
47
+
48
+ const setValue = useCallback(
49
+ (value: T | ((previous: T) => T)) => {
50
+ setStored((previous) => {
51
+ const next =
52
+ typeof value === "function" ? (value as (p: T) => T)(previous) : value;
53
+ try {
54
+ if (typeof window !== "undefined") {
55
+ window.localStorage.setItem(key, JSON.stringify(next));
56
+ }
57
+ } catch {
58
+ /* quota or private mode */
59
+ }
60
+ return next;
61
+ });
62
+ },
63
+ [key],
64
+ );
65
+
66
+ const remove = useCallback(() => {
67
+ try {
68
+ if (typeof window !== "undefined") {
69
+ window.localStorage.removeItem(key);
70
+ }
71
+ } catch {
72
+ console.error(`Failed to remove item from localStorage: ${key}`);
73
+ }
74
+ setStored(initialValue);
75
+ }, [initialValue, key]);
76
+
77
+ useEffect(() => {
78
+ setStored((prev) => {
79
+ const next = readValue(key, initialValue);
80
+ return JSON.stringify(prev) === JSON.stringify(next) ? prev : next;
81
+ });
82
+ }, [key]);
83
+
84
+ useEffect(() => {
85
+ if (typeof window === "undefined") {
86
+ return;
87
+ }
88
+ const onStorage = (event: StorageEvent) => {
89
+ if (event.key !== key) {
90
+ return;
91
+ }
92
+ if (event.newValue == null) {
93
+ setStored(initialValue);
94
+ return;
95
+ }
96
+ try {
97
+ setStored(JSON.parse(event.newValue) as T);
98
+ } catch {
99
+ setStored(initialValue);
100
+ }
101
+ };
102
+ window.addEventListener("storage", onStorage);
103
+ return () => {
104
+ window.removeEventListener("storage", onStorage);
105
+ };
106
+ }, [initialValue, key]);
107
+
108
+ return [stored, setValue, remove];
109
+ }
@@ -0,0 +1 @@
1
+ export { useMediaQuery } from "./useMediaQuery";
@@ -0,0 +1,63 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { useMediaQuery } from "./useMediaQuery";
5
+
6
+ function createControllableMatchMedia(initialMatches: boolean) {
7
+ let matches = initialMatches;
8
+ const listeners = new Set<() => void>();
9
+ return {
10
+ setMatches(next: boolean) {
11
+ matches = next;
12
+ listeners.forEach((listener) => listener());
13
+ },
14
+ implementation: (query: string) => {
15
+ const mql = {
16
+ media: query,
17
+ get matches() {
18
+ return matches;
19
+ },
20
+ addEventListener: (_type: string, listener: () => void) => {
21
+ listeners.add(listener);
22
+ },
23
+ removeEventListener: (_type: string, listener: () => void) => {
24
+ listeners.delete(listener);
25
+ },
26
+ addListener: vi.fn(),
27
+ removeListener: vi.fn(),
28
+ dispatchEvent: vi.fn(),
29
+ };
30
+ return mql as unknown as MediaQueryList;
31
+ },
32
+ };
33
+ }
34
+
35
+ describe("useMediaQuery", () => {
36
+ afterEach(() => {
37
+ vi.spyOn(window, "matchMedia").mockRestore();
38
+ });
39
+
40
+ it("should reflect matchMedia matches and subscribe to change", () => {
41
+ const { setMatches, implementation } = createControllableMatchMedia(false);
42
+ const matchMediaSpy = vi
43
+ .spyOn(window, "matchMedia")
44
+ .mockImplementation(implementation);
45
+
46
+ const { result } = renderHook(() => useMediaQuery("(min-width: 1px)"));
47
+ expect(result.current).toBe(false);
48
+ act(() => {
49
+ setMatches(true);
50
+ });
51
+ expect(result.current).toBe(true);
52
+ expect(matchMediaSpy).toHaveBeenCalledWith("(min-width: 1px)");
53
+ });
54
+
55
+ it("should sync to media.matches after mount when defaultValue differs", () => {
56
+ const { implementation } = createControllableMatchMedia(false);
57
+ vi.spyOn(window, "matchMedia").mockImplementation(implementation);
58
+ const { result } = renderHook(() =>
59
+ useMediaQuery("(min-width: 99999px)", true),
60
+ );
61
+ expect(result.current).toBe(false);
62
+ });
63
+ });
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /**
6
+ * Subscribes to `window.matchMedia(query)` and returns whether the query currently matches.
7
+ *
8
+ * On the server or when `matchMedia` is missing, `defaultValue` is used for the initial render and the effect no-ops.
9
+ * The `change` listener keeps `matches` updated when the viewport or user settings change.
10
+ *
11
+ * @param query - A valid media query string, e.g. `"(min-width: 768px)"`.
12
+ * @param defaultValue - Value to use before hydration or when `matchMedia` is unavailable (default `false`).
13
+ * @returns Current `matches` boolean for the query.
14
+ */
15
+ export function useMediaQuery(
16
+ query: string,
17
+ defaultValue = false,
18
+ ): boolean {
19
+ const [matches, setMatches] = useState(defaultValue);
20
+
21
+ useEffect(() => {
22
+ if (typeof window === "undefined" || !window.matchMedia) {
23
+ return;
24
+ }
25
+ const media = window.matchMedia(query);
26
+ const onChange = () => {
27
+ setMatches(media.matches);
28
+ };
29
+ setMatches(media.matches);
30
+ media.addEventListener("change", onChange);
31
+ return () => {
32
+ media.removeEventListener("change", onChange);
33
+ };
34
+ }, [query]);
35
+
36
+ return matches;
37
+ }
@@ -0,0 +1 @@
1
+ export { useNetworkStatus } from "./useNetworkStatus";
@@ -0,0 +1,53 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { useNetworkStatus } from "./useNetworkStatus";
5
+
6
+ describe("useNetworkStatus", () => {
7
+ const originalOnLine = navigator.onLine;
8
+
9
+ afterEach(() => {
10
+ Object.defineProperty(navigator, "onLine", {
11
+ configurable: true,
12
+ writable: true,
13
+ value: originalOnLine,
14
+ });
15
+ });
16
+
17
+ it("should reflect navigator.onLine initially", () => {
18
+ Object.defineProperty(navigator, "onLine", {
19
+ configurable: true,
20
+ writable: true,
21
+ value: true,
22
+ });
23
+ const { result } = renderHook(() => useNetworkStatus());
24
+ expect(result.current).toBe(true);
25
+ });
26
+
27
+ it("should update on offline and online window events", () => {
28
+ Object.defineProperty(navigator, "onLine", {
29
+ configurable: true,
30
+ writable: true,
31
+ value: true,
32
+ });
33
+ const { result } = renderHook(() => useNetworkStatus());
34
+ act(() => {
35
+ Object.defineProperty(navigator, "onLine", {
36
+ configurable: true,
37
+ writable: true,
38
+ value: false,
39
+ });
40
+ window.dispatchEvent(new Event("offline"));
41
+ });
42
+ expect(result.current).toBe(false);
43
+ act(() => {
44
+ Object.defineProperty(navigator, "onLine", {
45
+ configurable: true,
46
+ writable: true,
47
+ value: true,
48
+ });
49
+ window.dispatchEvent(new Event("online"));
50
+ });
51
+ expect(result.current).toBe(true);
52
+ });
53
+ });