@zentauri-ui/zentauri-components 1.9.2 → 2.0.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 (215) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +32 -5
  3. package/cli/registry.json +14 -0
  4. package/dist/{chunk-L4PDJ6IB.mjs → chunk-44NX3DAZ.mjs} +3 -3
  5. package/dist/{chunk-L4PDJ6IB.mjs.map → chunk-44NX3DAZ.mjs.map} +1 -1
  6. package/dist/chunk-FAMHSJTK.js +19 -0
  7. package/dist/{chunk-AQHY4S33.js.map → chunk-FAMHSJTK.js.map} +1 -1
  8. package/dist/chunk-I42UYWYA.mjs +128 -0
  9. package/dist/chunk-I42UYWYA.mjs.map +1 -0
  10. package/dist/{chunk-5J6QMTES.js → chunk-IKXO5SJ4.js} +21 -5
  11. package/dist/chunk-IKXO5SJ4.js.map +1 -0
  12. package/dist/{chunk-OPUO55TO.mjs → chunk-JXSM2EHC.mjs} +3 -3
  13. package/dist/{chunk-OPUO55TO.mjs.map → chunk-JXSM2EHC.mjs.map} +1 -1
  14. package/dist/{chunk-LQPKZ5ZD.js → chunk-LS4GY2ZQ.js} +6 -6
  15. package/dist/{chunk-LQPKZ5ZD.js.map → chunk-LS4GY2ZQ.js.map} +1 -1
  16. package/dist/{chunk-VIKQGO4W.mjs → chunk-VQQHVKEU.mjs} +21 -5
  17. package/dist/{chunk-VIKQGO4W.mjs.map → chunk-VQQHVKEU.mjs.map} +1 -1
  18. package/dist/chunk-ZVRGLG35.js +144 -0
  19. package/dist/chunk-ZVRGLG35.js.map +1 -0
  20. package/dist/design-system/combobox.d.ts +124 -0
  21. package/dist/design-system/combobox.d.ts.map +1 -0
  22. package/dist/design-system/facade.js +6 -5
  23. package/dist/design-system/facade.js.map +1 -1
  24. package/dist/design-system/facade.mjs +5 -4
  25. package/dist/design-system/facade.mjs.map +1 -1
  26. package/dist/design-system/index.d.ts +1 -0
  27. package/dist/design-system/index.d.ts.map +1 -1
  28. package/dist/hooks/index.d.ts +13 -0
  29. package/dist/hooks/index.d.ts.map +1 -1
  30. package/dist/hooks/useCookie/index.d.ts +2 -0
  31. package/dist/hooks/useCookie/index.d.ts.map +1 -0
  32. package/dist/hooks/useCookie/useCookie.d.ts +36 -0
  33. package/dist/hooks/useCookie/useCookie.d.ts.map +1 -0
  34. package/dist/hooks/useCookie.js +82 -0
  35. package/dist/hooks/useCookie.js.map +1 -0
  36. package/dist/hooks/useCookie.mjs +80 -0
  37. package/dist/hooks/useCookie.mjs.map +1 -0
  38. package/dist/hooks/useCountdown/index.d.ts +2 -0
  39. package/dist/hooks/useCountdown/index.d.ts.map +1 -0
  40. package/dist/hooks/useCountdown/useCountdown.d.ts +40 -0
  41. package/dist/hooks/useCountdown/useCountdown.d.ts.map +1 -0
  42. package/dist/hooks/useCountdown.js +60 -0
  43. package/dist/hooks/useCountdown.js.map +1 -0
  44. package/dist/hooks/useCountdown.mjs +58 -0
  45. package/dist/hooks/useCountdown.mjs.map +1 -0
  46. package/dist/hooks/useEventListener/index.d.ts +2 -0
  47. package/dist/hooks/useEventListener/index.d.ts.map +1 -0
  48. package/dist/hooks/useEventListener/useEventListener.d.ts +22 -0
  49. package/dist/hooks/useEventListener/useEventListener.d.ts.map +1 -0
  50. package/dist/hooks/useEventListener.js +45 -0
  51. package/dist/hooks/useEventListener.js.map +1 -0
  52. package/dist/hooks/useEventListener.mjs +43 -0
  53. package/dist/hooks/useEventListener.mjs.map +1 -0
  54. package/dist/hooks/useGeolocation/index.d.ts +2 -0
  55. package/dist/hooks/useGeolocation/index.d.ts.map +1 -0
  56. package/dist/hooks/useGeolocation/useGeolocation.d.ts +48 -0
  57. package/dist/hooks/useGeolocation/useGeolocation.d.ts.map +1 -0
  58. package/dist/hooks/useGeolocation.js +111 -0
  59. package/dist/hooks/useGeolocation.js.map +1 -0
  60. package/dist/hooks/useGeolocation.mjs +109 -0
  61. package/dist/hooks/useGeolocation.mjs.map +1 -0
  62. package/dist/hooks/useHotkeys/index.d.ts +2 -0
  63. package/dist/hooks/useHotkeys/index.d.ts.map +1 -0
  64. package/dist/hooks/useHotkeys/useHotkeys.d.ts +24 -0
  65. package/dist/hooks/useHotkeys/useHotkeys.d.ts.map +1 -0
  66. package/dist/hooks/useHotkeys.js +86 -0
  67. package/dist/hooks/useHotkeys.js.map +1 -0
  68. package/dist/hooks/useHotkeys.mjs +84 -0
  69. package/dist/hooks/useHotkeys.mjs.map +1 -0
  70. package/dist/hooks/useIdleTimeout/index.d.ts +2 -0
  71. package/dist/hooks/useIdleTimeout/index.d.ts.map +1 -0
  72. package/dist/hooks/useIdleTimeout/useIdleTimeout.d.ts +31 -0
  73. package/dist/hooks/useIdleTimeout/useIdleTimeout.d.ts.map +1 -0
  74. package/dist/hooks/useIdleTimeout.js +77 -0
  75. package/dist/hooks/useIdleTimeout.js.map +1 -0
  76. package/dist/hooks/useIdleTimeout.mjs +75 -0
  77. package/dist/hooks/useIdleTimeout.mjs.map +1 -0
  78. package/dist/hooks/useInterval/index.d.ts +2 -0
  79. package/dist/hooks/useInterval/index.d.ts.map +1 -0
  80. package/dist/hooks/useInterval/useInterval.d.ts +12 -0
  81. package/dist/hooks/useInterval/useInterval.d.ts.map +1 -0
  82. package/dist/hooks/useInterval.js +27 -0
  83. package/dist/hooks/useInterval.js.map +1 -0
  84. package/dist/hooks/useInterval.mjs +25 -0
  85. package/dist/hooks/useInterval.mjs.map +1 -0
  86. package/dist/hooks/useKeyPress/index.d.ts +2 -0
  87. package/dist/hooks/useKeyPress/index.d.ts.map +1 -0
  88. package/dist/hooks/useKeyPress/useKeyPress.d.ts +15 -0
  89. package/dist/hooks/useKeyPress/useKeyPress.d.ts.map +1 -0
  90. package/dist/hooks/useKeyPress.js +47 -0
  91. package/dist/hooks/useKeyPress.js.map +1 -0
  92. package/dist/hooks/useKeyPress.mjs +45 -0
  93. package/dist/hooks/useKeyPress.mjs.map +1 -0
  94. package/dist/hooks/useLongPress/index.d.ts +2 -0
  95. package/dist/hooks/useLongPress/index.d.ts.map +1 -0
  96. package/dist/hooks/useLongPress/useLongPress.d.ts +46 -0
  97. package/dist/hooks/useLongPress/useLongPress.d.ts.map +1 -0
  98. package/dist/hooks/useLongPress.js +116 -0
  99. package/dist/hooks/useLongPress.js.map +1 -0
  100. package/dist/hooks/useLongPress.mjs +114 -0
  101. package/dist/hooks/useLongPress.mjs.map +1 -0
  102. package/dist/hooks/usePrevious/index.d.ts +2 -0
  103. package/dist/hooks/usePrevious/index.d.ts.map +1 -0
  104. package/dist/hooks/usePrevious/usePrevious.d.ts +13 -0
  105. package/dist/hooks/usePrevious/usePrevious.d.ts.map +1 -0
  106. package/dist/hooks/usePrevious.js +17 -0
  107. package/dist/hooks/usePrevious.js.map +1 -0
  108. package/dist/hooks/usePrevious.mjs +15 -0
  109. package/dist/hooks/usePrevious.mjs.map +1 -0
  110. package/dist/hooks/useScrollPosition/index.d.ts +2 -0
  111. package/dist/hooks/useScrollPosition/index.d.ts.map +1 -0
  112. package/dist/hooks/useScrollPosition/useScrollPosition.d.ts +37 -0
  113. package/dist/hooks/useScrollPosition/useScrollPosition.d.ts.map +1 -0
  114. package/dist/hooks/useScrollPosition.js +41 -0
  115. package/dist/hooks/useScrollPosition.js.map +1 -0
  116. package/dist/hooks/useScrollPosition.mjs +39 -0
  117. package/dist/hooks/useScrollPosition.mjs.map +1 -0
  118. package/dist/hooks/useTimeout/index.d.ts +2 -0
  119. package/dist/hooks/useTimeout/index.d.ts.map +1 -0
  120. package/dist/hooks/useTimeout/useTimeout.d.ts +19 -0
  121. package/dist/hooks/useTimeout/useTimeout.d.ts.map +1 -0
  122. package/dist/hooks/useTimeout.js +38 -0
  123. package/dist/hooks/useTimeout.js.map +1 -0
  124. package/dist/hooks/useTimeout.mjs +36 -0
  125. package/dist/hooks/useTimeout.mjs.map +1 -0
  126. package/dist/hooks/useVirtualList/index.d.ts +2 -0
  127. package/dist/hooks/useVirtualList/index.d.ts.map +1 -0
  128. package/dist/hooks/useVirtualList/useVirtualList.d.ts +47 -0
  129. package/dist/hooks/useVirtualList/useVirtualList.d.ts.map +1 -0
  130. package/dist/hooks/useVirtualList.js +87 -0
  131. package/dist/hooks/useVirtualList.js.map +1 -0
  132. package/dist/hooks/useVirtualList.mjs +85 -0
  133. package/dist/hooks/useVirtualList.mjs.map +1 -0
  134. package/dist/lib/facade.d.ts.map +1 -1
  135. package/dist/ui/buttons/animated.js +8 -7
  136. package/dist/ui/buttons/animated.js.map +1 -1
  137. package/dist/ui/buttons/animated.mjs +6 -5
  138. package/dist/ui/buttons/animated.mjs.map +1 -1
  139. package/dist/ui/buttons.js +9 -8
  140. package/dist/ui/buttons.mjs +7 -6
  141. package/dist/ui/combobox/combobox-base.d.ts +37 -0
  142. package/dist/ui/combobox/combobox-base.d.ts.map +1 -0
  143. package/dist/ui/combobox/combobox.d.ts +6 -0
  144. package/dist/ui/combobox/combobox.d.ts.map +1 -0
  145. package/dist/ui/combobox/index.d.ts +4 -0
  146. package/dist/ui/combobox/index.d.ts.map +1 -0
  147. package/dist/ui/combobox/types.d.ts +70 -0
  148. package/dist/ui/combobox/types.d.ts.map +1 -0
  149. package/dist/ui/combobox/variants.d.ts +17 -0
  150. package/dist/ui/combobox/variants.d.ts.map +1 -0
  151. package/dist/ui/combobox.js +510 -0
  152. package/dist/ui/combobox.js.map +1 -0
  153. package/dist/ui/combobox.mjs +495 -0
  154. package/dist/ui/combobox.mjs.map +1 -0
  155. package/dist/ui/dynamic-stepper.js +18 -17
  156. package/dist/ui/dynamic-stepper.js.map +1 -1
  157. package/dist/ui/dynamic-stepper.mjs +7 -6
  158. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  159. package/dist/ui/pagination.js +14 -13
  160. package/dist/ui/pagination.js.map +1 -1
  161. package/dist/ui/pagination.mjs +6 -5
  162. package/dist/ui/pagination.mjs.map +1 -1
  163. package/package.json +1 -1
  164. package/src/design-system/combobox.ts +204 -0
  165. package/src/design-system/index.ts +1 -0
  166. package/src/hooks/index.ts +50 -0
  167. package/src/hooks/useCookie/index.ts +5 -0
  168. package/src/hooks/useCookie/useCookie.test.ts +57 -0
  169. package/src/hooks/useCookie/useCookie.ts +133 -0
  170. package/src/hooks/useCountdown/index.ts +5 -0
  171. package/src/hooks/useCountdown/useCountdown.test.ts +113 -0
  172. package/src/hooks/useCountdown/useCountdown.ts +106 -0
  173. package/src/hooks/useEventListener/index.ts +4 -0
  174. package/src/hooks/useEventListener/useEventListener.test.ts +60 -0
  175. package/src/hooks/useEventListener/useEventListener.ts +98 -0
  176. package/src/hooks/useGeolocation/index.ts +6 -0
  177. package/src/hooks/useGeolocation/useGeolocation.test.ts +108 -0
  178. package/src/hooks/useGeolocation/useGeolocation.ts +173 -0
  179. package/src/hooks/useHotkeys/index.ts +5 -0
  180. package/src/hooks/useHotkeys/useHotkeys.test.ts +82 -0
  181. package/src/hooks/useHotkeys/useHotkeys.ts +130 -0
  182. package/src/hooks/useIdleTimeout/index.ts +5 -0
  183. package/src/hooks/useIdleTimeout/useIdleTimeout.test.ts +97 -0
  184. package/src/hooks/useIdleTimeout/useIdleTimeout.ts +111 -0
  185. package/src/hooks/useInterval/index.ts +1 -0
  186. package/src/hooks/useInterval/useInterval.test.ts +56 -0
  187. package/src/hooks/useInterval/useInterval.ts +36 -0
  188. package/src/hooks/useKeyPress/index.ts +1 -0
  189. package/src/hooks/useKeyPress/useKeyPress.test.ts +67 -0
  190. package/src/hooks/useKeyPress/useKeyPress.ts +65 -0
  191. package/src/hooks/useLongPress/index.ts +5 -0
  192. package/src/hooks/useLongPress/useLongPress.test.ts +180 -0
  193. package/src/hooks/useLongPress/useLongPress.ts +177 -0
  194. package/src/hooks/usePrevious/index.ts +1 -0
  195. package/src/hooks/usePrevious/usePrevious.test.ts +33 -0
  196. package/src/hooks/usePrevious/usePrevious.ts +24 -0
  197. package/src/hooks/useScrollPosition/index.ts +5 -0
  198. package/src/hooks/useScrollPosition/useScrollPosition.test.ts +69 -0
  199. package/src/hooks/useScrollPosition/useScrollPosition.ts +88 -0
  200. package/src/hooks/useTimeout/index.ts +1 -0
  201. package/src/hooks/useTimeout/useTimeout.test.ts +63 -0
  202. package/src/hooks/useTimeout/useTimeout.ts +58 -0
  203. package/src/hooks/useVirtualList/index.ts +6 -0
  204. package/src/hooks/useVirtualList/useVirtualList.test.ts +102 -0
  205. package/src/hooks/useVirtualList/useVirtualList.ts +144 -0
  206. package/src/lib/facade.test.ts +7 -7
  207. package/src/lib/facade.ts +6 -2
  208. package/src/ui/combobox/combobox-base.tsx +552 -0
  209. package/src/ui/combobox/combobox.test.tsx +292 -0
  210. package/src/ui/combobox/combobox.tsx +8 -0
  211. package/src/ui/combobox/index.ts +33 -0
  212. package/src/ui/combobox/types.ts +91 -0
  213. package/src/ui/combobox/variants.ts +58 -0
  214. package/dist/chunk-5J6QMTES.js.map +0 -1
  215. package/dist/chunk-AQHY4S33.js +0 -19
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+
5
+ export type UseTimeoutResult = {
6
+ /** Cancel the pending timeout (no-op if already fired or cleared). */
7
+ clear: () => void;
8
+ /** Cancel any pending timeout and schedule a fresh one with the current delay. */
9
+ reset: () => void;
10
+ };
11
+
12
+ /**
13
+ * Declarative `setTimeout`: runs `callback` once after `delayMs` milliseconds with automatic cleanup.
14
+ *
15
+ * - The latest callback is kept in a ref, so a new inline function each render does not reschedule.
16
+ * - Pass `null` as the delay to cancel scheduling entirely.
17
+ * - Changing `delayMs` cancels the pending timeout and schedules a new one.
18
+ *
19
+ * @param callback - Function invoked when the timeout fires.
20
+ * @param delayMs - Delay in milliseconds, or `null` to disable.
21
+ * @returns `{ clear, reset }` to cancel or restart the timeout imperatively.
22
+ */
23
+ export function useTimeout(
24
+ callback: () => void,
25
+ delayMs: number | null,
26
+ ): UseTimeoutResult {
27
+ const callbackRef = useRef(callback);
28
+ const timeoutRef = useRef<number | undefined>(undefined);
29
+
30
+ useEffect(() => {
31
+ callbackRef.current = callback;
32
+ }, [callback]);
33
+
34
+ const clear = useCallback(() => {
35
+ if (timeoutRef.current !== undefined) {
36
+ window.clearTimeout(timeoutRef.current);
37
+ timeoutRef.current = undefined;
38
+ }
39
+ }, []);
40
+
41
+ const reset = useCallback(() => {
42
+ clear();
43
+ if (delayMs == null) {
44
+ return;
45
+ }
46
+ timeoutRef.current = window.setTimeout(() => {
47
+ timeoutRef.current = undefined;
48
+ callbackRef.current();
49
+ }, delayMs);
50
+ }, [clear, delayMs]);
51
+
52
+ useEffect(() => {
53
+ reset();
54
+ return clear;
55
+ }, [clear, reset]);
56
+
57
+ return { clear, reset };
58
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ useVirtualList,
3
+ type UseVirtualListParams,
4
+ type UseVirtualListResult,
5
+ type VirtualItem,
6
+ } from "./useVirtualList";
@@ -0,0 +1,102 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { useVirtualList } from "./useVirtualList";
5
+
6
+ function createContainer(clientHeight: number): HTMLDivElement {
7
+ const element = document.createElement("div");
8
+ Object.defineProperty(element, "clientHeight", {
9
+ configurable: true,
10
+ value: clientHeight,
11
+ });
12
+ return element;
13
+ }
14
+
15
+ describe("useVirtualList", () => {
16
+ it("should expose the full content height", () => {
17
+ const { result } = renderHook(() =>
18
+ useVirtualList({ itemCount: 1000, itemHeight: 40 }),
19
+ );
20
+ expect(result.current.totalHeight).toBe(40000);
21
+ });
22
+
23
+ it("should render only the visible window plus overscan", () => {
24
+ const container = createContainer(200);
25
+ const { result } = renderHook(() =>
26
+ useVirtualList({ itemCount: 1000, itemHeight: 40, overscan: 2 }),
27
+ );
28
+ act(() => {
29
+ result.current.setContainerRef(container);
30
+ });
31
+ // viewport 200px / 40px rows = 5 visible + 2 overscan below (none above at top)
32
+ expect(result.current.startIndex).toBe(0);
33
+ expect(result.current.endIndex).toBe(6);
34
+ expect(result.current.virtualItems).toHaveLength(7);
35
+ expect(result.current.virtualItems[0]).toEqual({
36
+ index: 0,
37
+ start: 0,
38
+ size: 40,
39
+ });
40
+ });
41
+
42
+ it("should shift the window on scroll", () => {
43
+ const container = createContainer(200);
44
+ const { result } = renderHook(() =>
45
+ useVirtualList({ itemCount: 1000, itemHeight: 40, overscan: 2 }),
46
+ );
47
+ act(() => {
48
+ result.current.setContainerRef(container);
49
+ });
50
+ act(() => {
51
+ container.scrollTop = 400;
52
+ container.dispatchEvent(new Event("scroll"));
53
+ });
54
+ // rows 10..14 visible, ±2 overscan
55
+ expect(result.current.startIndex).toBe(8);
56
+ expect(result.current.endIndex).toBe(16);
57
+ expect(result.current.virtualItems[0]?.start).toBe(8 * 40);
58
+ });
59
+
60
+ it("should clamp the window at the end of the list", () => {
61
+ const container = createContainer(200);
62
+ const { result } = renderHook(() =>
63
+ useVirtualList({ itemCount: 12, itemHeight: 40, overscan: 3 }),
64
+ );
65
+ act(() => {
66
+ result.current.setContainerRef(container);
67
+ });
68
+ act(() => {
69
+ container.scrollTop = 280;
70
+ container.dispatchEvent(new Event("scroll"));
71
+ });
72
+ expect(result.current.endIndex).toBe(11);
73
+ });
74
+
75
+ it("should handle an empty list", () => {
76
+ const { result } = renderHook(() =>
77
+ useVirtualList({ itemCount: 0, itemHeight: 40 }),
78
+ );
79
+ expect(result.current.virtualItems).toHaveLength(0);
80
+ expect(result.current.totalHeight).toBe(0);
81
+ expect(result.current.endIndex).toBe(-1);
82
+ });
83
+
84
+ it("should scroll to an index, clamped to bounds", () => {
85
+ const container = createContainer(200);
86
+ const { result } = renderHook(() =>
87
+ useVirtualList({ itemCount: 100, itemHeight: 40, overscan: 0 }),
88
+ );
89
+ act(() => {
90
+ result.current.setContainerRef(container);
91
+ });
92
+ act(() => {
93
+ result.current.scrollToIndex(50);
94
+ });
95
+ expect(container.scrollTop).toBe(2000);
96
+ expect(result.current.startIndex).toBe(50);
97
+ act(() => {
98
+ result.current.scrollToIndex(500);
99
+ });
100
+ expect(container.scrollTop).toBe(99 * 40);
101
+ });
102
+ });
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import type { RefCallback } from "react";
4
+ import { useCallback, useEffect, useMemo, useState } from "react";
5
+
6
+ export type VirtualItem = {
7
+ /** Index into your data array. */
8
+ index: number;
9
+ /** Offset from the top of the scroll content in pixels (use for `translateY` / `top`). */
10
+ start: number;
11
+ /** Row height in pixels. */
12
+ size: number;
13
+ };
14
+
15
+ export type UseVirtualListParams = {
16
+ /** Total number of rows in the list. */
17
+ itemCount: number;
18
+ /** Fixed height of every row in pixels (must be > 0). */
19
+ itemHeight: number;
20
+ /** Extra rows rendered above and below the viewport (default `3`). */
21
+ overscan?: number;
22
+ };
23
+
24
+ export type UseVirtualListResult = {
25
+ /** Callback ref for the scrollable container (needs `overflow-y: auto` and a bounded height). */
26
+ setContainerRef: RefCallback<HTMLElement>;
27
+ /** The rows to render, each with its absolute `start` offset. */
28
+ virtualItems: VirtualItem[];
29
+ /** Height of the full list content; set it on an inner spacer element. */
30
+ totalHeight: number;
31
+ /** First rendered index (after overscan). */
32
+ startIndex: number;
33
+ /** Last rendered index (after overscan), `-1` when empty. */
34
+ endIndex: number;
35
+ /** Scroll the container so the given row is at the top. */
36
+ scrollToIndex: (index: number) => void;
37
+ };
38
+
39
+ /**
40
+ * Headless fixed-height list virtualization: renders only the rows visible in the
41
+ * scroll container (plus `overscan`), so lists of tens of thousands of rows stay cheap.
42
+ *
43
+ * Markup recipe: outer container gets `setContainerRef` + `overflow-y: auto` and a height;
44
+ * inside it, one relative spacer div with `height: totalHeight`; each virtual item is
45
+ * absolutely positioned at `translateY(item.start)` with `height: item.size`.
46
+ *
47
+ * Viewport size tracks `ResizeObserver` (when available) and scroll position tracks a
48
+ * passive `scroll` listener. For variable-height rows, reach for a dedicated virtualizer.
49
+ *
50
+ * @param params - {@link UseVirtualListParams}
51
+ * @returns {@link UseVirtualListResult}
52
+ */
53
+ export function useVirtualList({
54
+ itemCount,
55
+ itemHeight,
56
+ overscan = 3,
57
+ }: UseVirtualListParams): UseVirtualListResult {
58
+ const [container, setContainer] = useState<HTMLElement | null>(null);
59
+ const [scrollTop, setScrollTop] = useState(0);
60
+ const [viewportHeight, setViewportHeight] = useState(0);
61
+
62
+ const setContainerRef = useCallback((node: HTMLElement | null) => {
63
+ setContainer(node);
64
+ }, []);
65
+
66
+ useEffect(() => {
67
+ if (container == null) {
68
+ return;
69
+ }
70
+ const onScroll = () => {
71
+ setScrollTop(container.scrollTop);
72
+ };
73
+ const measure = () => {
74
+ setViewportHeight(container.clientHeight);
75
+ };
76
+ measure();
77
+ onScroll();
78
+ container.addEventListener("scroll", onScroll, { passive: true });
79
+ let observer: ResizeObserver | undefined;
80
+ if (typeof ResizeObserver !== "undefined") {
81
+ observer = new ResizeObserver(measure);
82
+ observer.observe(container);
83
+ }
84
+ return () => {
85
+ container.removeEventListener("scroll", onScroll);
86
+ observer?.disconnect();
87
+ };
88
+ }, [container]);
89
+
90
+ const safeItemCount = Math.max(0, Math.floor(itemCount));
91
+ const safeItemHeight = Math.max(1, itemHeight);
92
+ const safeOverscan = Math.max(0, Math.floor(overscan));
93
+
94
+ const totalHeight = safeItemCount * safeItemHeight;
95
+ const startIndex =
96
+ safeItemCount === 0
97
+ ? 0
98
+ : Math.max(
99
+ 0,
100
+ Math.floor(scrollTop / safeItemHeight) - safeOverscan,
101
+ );
102
+ const endIndex =
103
+ safeItemCount === 0
104
+ ? -1
105
+ : Math.min(
106
+ safeItemCount - 1,
107
+ Math.ceil((scrollTop + viewportHeight) / safeItemHeight) -
108
+ 1 +
109
+ safeOverscan,
110
+ );
111
+
112
+ const virtualItems = useMemo(() => {
113
+ const items: VirtualItem[] = [];
114
+ for (let index = startIndex; index <= endIndex; index += 1) {
115
+ items.push({
116
+ index,
117
+ start: index * safeItemHeight,
118
+ size: safeItemHeight,
119
+ });
120
+ }
121
+ return items;
122
+ }, [endIndex, safeItemHeight, startIndex]);
123
+
124
+ const scrollToIndex = useCallback(
125
+ (index: number) => {
126
+ if (container == null || safeItemCount === 0) {
127
+ return;
128
+ }
129
+ const clamped = Math.min(Math.max(index, 0), safeItemCount - 1);
130
+ container.scrollTop = clamped * safeItemHeight;
131
+ setScrollTop(container.scrollTop);
132
+ },
133
+ [container, safeItemCount, safeItemHeight],
134
+ );
135
+
136
+ return {
137
+ setContainerRef,
138
+ virtualItems,
139
+ totalHeight,
140
+ startIndex,
141
+ endIndex,
142
+ scrollToIndex,
143
+ };
144
+ }
@@ -84,9 +84,9 @@ describe("DesignSystem facade", () => {
84
84
  const searchVars = search?.variables() ?? [];
85
85
  expect(searchVars.length).toBeGreaterThan(0);
86
86
  // SearchBar has no tokens of its own — every variable is an Input variable.
87
- expect(searchVars.every((token) => token.name.startsWith("--zui-input"))).toBe(
88
- true,
89
- );
87
+ expect(
88
+ searchVars.every((token) => token.name.startsWith("--zui-input")),
89
+ ).toBe(true);
90
90
  expect(searchVars.length).toBe(input?.variables().length);
91
91
  });
92
92
 
@@ -100,7 +100,9 @@ describe("DesignSystem facade", () => {
100
100
  const contextMenuNames = (contextMenu?.variables() ?? []).map(
101
101
  (token) => token.name,
102
102
  );
103
- const dropdownNames = (dropdown?.variables() ?? []).map((token) => token.name);
103
+ const dropdownNames = (dropdown?.variables() ?? []).map(
104
+ (token) => token.name,
105
+ );
104
106
 
105
107
  expect(contextMenuNames.length).toBeGreaterThan(0);
106
108
  expect(
@@ -153,9 +155,7 @@ describe("DesignSystem facade", () => {
153
155
  it("leaves dark-only variables unpaired", () => {
154
156
  // A `-dark` reference with no light base must stay unpaired rather than
155
157
  // pointing at a variable that was never parsed.
156
- const darkOnly = DesignSystem.parse(
157
- "bg-[var(--zui-probe-only-dark,#000)]",
158
- );
158
+ const darkOnly = DesignSystem.parse("bg-[var(--zui-probe-only-dark,#000)]");
159
159
  expect(darkOnly).toHaveLength(1);
160
160
  expect(darkOnly[0]?.theme).toBe("dark");
161
161
  expect(darkOnly[0]?.pairName).toBeUndefined();
package/src/lib/facade.ts CHANGED
@@ -439,8 +439,12 @@ function classifyVariables(raw: RawVariable[]): ZuiVariable[] {
439
439
  fallback,
440
440
  theme: isDark ? "dark" : hasDarkPair ? "light" : "shared",
441
441
  pairName: isDark
442
- ? (names.has(baseName) ? baseName : undefined)
443
- : (hasDarkPair ? darkName : undefined),
442
+ ? names.has(baseName)
443
+ ? baseName
444
+ : undefined
445
+ : hasDarkPair
446
+ ? darkName
447
+ : undefined,
444
448
  } satisfies ZuiVariable;
445
449
  });
446
450
  }