@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.
- package/CHANGELOG.md +17 -0
- package/README.md +32 -5
- package/cli/registry.json +14 -0
- package/dist/{chunk-L4PDJ6IB.mjs → chunk-44NX3DAZ.mjs} +3 -3
- package/dist/{chunk-L4PDJ6IB.mjs.map → chunk-44NX3DAZ.mjs.map} +1 -1
- package/dist/chunk-FAMHSJTK.js +19 -0
- package/dist/{chunk-AQHY4S33.js.map → chunk-FAMHSJTK.js.map} +1 -1
- package/dist/chunk-I42UYWYA.mjs +128 -0
- package/dist/chunk-I42UYWYA.mjs.map +1 -0
- package/dist/{chunk-5J6QMTES.js → chunk-IKXO5SJ4.js} +21 -5
- package/dist/chunk-IKXO5SJ4.js.map +1 -0
- package/dist/{chunk-OPUO55TO.mjs → chunk-JXSM2EHC.mjs} +3 -3
- package/dist/{chunk-OPUO55TO.mjs.map → chunk-JXSM2EHC.mjs.map} +1 -1
- package/dist/{chunk-LQPKZ5ZD.js → chunk-LS4GY2ZQ.js} +6 -6
- package/dist/{chunk-LQPKZ5ZD.js.map → chunk-LS4GY2ZQ.js.map} +1 -1
- package/dist/{chunk-VIKQGO4W.mjs → chunk-VQQHVKEU.mjs} +21 -5
- package/dist/{chunk-VIKQGO4W.mjs.map → chunk-VQQHVKEU.mjs.map} +1 -1
- package/dist/chunk-ZVRGLG35.js +144 -0
- package/dist/chunk-ZVRGLG35.js.map +1 -0
- package/dist/design-system/combobox.d.ts +124 -0
- package/dist/design-system/combobox.d.ts.map +1 -0
- package/dist/design-system/facade.js +6 -5
- package/dist/design-system/facade.js.map +1 -1
- package/dist/design-system/facade.mjs +5 -4
- package/dist/design-system/facade.mjs.map +1 -1
- package/dist/design-system/index.d.ts +1 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +13 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useCookie/index.d.ts +2 -0
- package/dist/hooks/useCookie/index.d.ts.map +1 -0
- package/dist/hooks/useCookie/useCookie.d.ts +36 -0
- package/dist/hooks/useCookie/useCookie.d.ts.map +1 -0
- package/dist/hooks/useCookie.js +82 -0
- package/dist/hooks/useCookie.js.map +1 -0
- package/dist/hooks/useCookie.mjs +80 -0
- package/dist/hooks/useCookie.mjs.map +1 -0
- package/dist/hooks/useCountdown/index.d.ts +2 -0
- package/dist/hooks/useCountdown/index.d.ts.map +1 -0
- package/dist/hooks/useCountdown/useCountdown.d.ts +40 -0
- package/dist/hooks/useCountdown/useCountdown.d.ts.map +1 -0
- package/dist/hooks/useCountdown.js +60 -0
- package/dist/hooks/useCountdown.js.map +1 -0
- package/dist/hooks/useCountdown.mjs +58 -0
- package/dist/hooks/useCountdown.mjs.map +1 -0
- package/dist/hooks/useEventListener/index.d.ts +2 -0
- package/dist/hooks/useEventListener/index.d.ts.map +1 -0
- package/dist/hooks/useEventListener/useEventListener.d.ts +22 -0
- package/dist/hooks/useEventListener/useEventListener.d.ts.map +1 -0
- package/dist/hooks/useEventListener.js +45 -0
- package/dist/hooks/useEventListener.js.map +1 -0
- package/dist/hooks/useEventListener.mjs +43 -0
- package/dist/hooks/useEventListener.mjs.map +1 -0
- package/dist/hooks/useGeolocation/index.d.ts +2 -0
- package/dist/hooks/useGeolocation/index.d.ts.map +1 -0
- package/dist/hooks/useGeolocation/useGeolocation.d.ts +48 -0
- package/dist/hooks/useGeolocation/useGeolocation.d.ts.map +1 -0
- package/dist/hooks/useGeolocation.js +111 -0
- package/dist/hooks/useGeolocation.js.map +1 -0
- package/dist/hooks/useGeolocation.mjs +109 -0
- package/dist/hooks/useGeolocation.mjs.map +1 -0
- package/dist/hooks/useHotkeys/index.d.ts +2 -0
- package/dist/hooks/useHotkeys/index.d.ts.map +1 -0
- package/dist/hooks/useHotkeys/useHotkeys.d.ts +24 -0
- package/dist/hooks/useHotkeys/useHotkeys.d.ts.map +1 -0
- package/dist/hooks/useHotkeys.js +86 -0
- package/dist/hooks/useHotkeys.js.map +1 -0
- package/dist/hooks/useHotkeys.mjs +84 -0
- package/dist/hooks/useHotkeys.mjs.map +1 -0
- package/dist/hooks/useIdleTimeout/index.d.ts +2 -0
- package/dist/hooks/useIdleTimeout/index.d.ts.map +1 -0
- package/dist/hooks/useIdleTimeout/useIdleTimeout.d.ts +31 -0
- package/dist/hooks/useIdleTimeout/useIdleTimeout.d.ts.map +1 -0
- package/dist/hooks/useIdleTimeout.js +77 -0
- package/dist/hooks/useIdleTimeout.js.map +1 -0
- package/dist/hooks/useIdleTimeout.mjs +75 -0
- package/dist/hooks/useIdleTimeout.mjs.map +1 -0
- package/dist/hooks/useInterval/index.d.ts +2 -0
- package/dist/hooks/useInterval/index.d.ts.map +1 -0
- package/dist/hooks/useInterval/useInterval.d.ts +12 -0
- package/dist/hooks/useInterval/useInterval.d.ts.map +1 -0
- package/dist/hooks/useInterval.js +27 -0
- package/dist/hooks/useInterval.js.map +1 -0
- package/dist/hooks/useInterval.mjs +25 -0
- package/dist/hooks/useInterval.mjs.map +1 -0
- package/dist/hooks/useKeyPress/index.d.ts +2 -0
- package/dist/hooks/useKeyPress/index.d.ts.map +1 -0
- package/dist/hooks/useKeyPress/useKeyPress.d.ts +15 -0
- package/dist/hooks/useKeyPress/useKeyPress.d.ts.map +1 -0
- package/dist/hooks/useKeyPress.js +47 -0
- package/dist/hooks/useKeyPress.js.map +1 -0
- package/dist/hooks/useKeyPress.mjs +45 -0
- package/dist/hooks/useKeyPress.mjs.map +1 -0
- package/dist/hooks/useLongPress/index.d.ts +2 -0
- package/dist/hooks/useLongPress/index.d.ts.map +1 -0
- package/dist/hooks/useLongPress/useLongPress.d.ts +46 -0
- package/dist/hooks/useLongPress/useLongPress.d.ts.map +1 -0
- package/dist/hooks/useLongPress.js +116 -0
- package/dist/hooks/useLongPress.js.map +1 -0
- package/dist/hooks/useLongPress.mjs +114 -0
- package/dist/hooks/useLongPress.mjs.map +1 -0
- package/dist/hooks/usePrevious/index.d.ts +2 -0
- package/dist/hooks/usePrevious/index.d.ts.map +1 -0
- package/dist/hooks/usePrevious/usePrevious.d.ts +13 -0
- package/dist/hooks/usePrevious/usePrevious.d.ts.map +1 -0
- package/dist/hooks/usePrevious.js +17 -0
- package/dist/hooks/usePrevious.js.map +1 -0
- package/dist/hooks/usePrevious.mjs +15 -0
- package/dist/hooks/usePrevious.mjs.map +1 -0
- package/dist/hooks/useScrollPosition/index.d.ts +2 -0
- package/dist/hooks/useScrollPosition/index.d.ts.map +1 -0
- package/dist/hooks/useScrollPosition/useScrollPosition.d.ts +37 -0
- package/dist/hooks/useScrollPosition/useScrollPosition.d.ts.map +1 -0
- package/dist/hooks/useScrollPosition.js +41 -0
- package/dist/hooks/useScrollPosition.js.map +1 -0
- package/dist/hooks/useScrollPosition.mjs +39 -0
- package/dist/hooks/useScrollPosition.mjs.map +1 -0
- package/dist/hooks/useTimeout/index.d.ts +2 -0
- package/dist/hooks/useTimeout/index.d.ts.map +1 -0
- package/dist/hooks/useTimeout/useTimeout.d.ts +19 -0
- package/dist/hooks/useTimeout/useTimeout.d.ts.map +1 -0
- package/dist/hooks/useTimeout.js +38 -0
- package/dist/hooks/useTimeout.js.map +1 -0
- package/dist/hooks/useTimeout.mjs +36 -0
- package/dist/hooks/useTimeout.mjs.map +1 -0
- package/dist/hooks/useVirtualList/index.d.ts +2 -0
- package/dist/hooks/useVirtualList/index.d.ts.map +1 -0
- package/dist/hooks/useVirtualList/useVirtualList.d.ts +47 -0
- package/dist/hooks/useVirtualList/useVirtualList.d.ts.map +1 -0
- package/dist/hooks/useVirtualList.js +87 -0
- package/dist/hooks/useVirtualList.js.map +1 -0
- package/dist/hooks/useVirtualList.mjs +85 -0
- package/dist/hooks/useVirtualList.mjs.map +1 -0
- package/dist/lib/facade.d.ts.map +1 -1
- package/dist/ui/buttons/animated.js +8 -7
- package/dist/ui/buttons/animated.js.map +1 -1
- package/dist/ui/buttons/animated.mjs +6 -5
- package/dist/ui/buttons/animated.mjs.map +1 -1
- package/dist/ui/buttons.js +9 -8
- package/dist/ui/buttons.mjs +7 -6
- package/dist/ui/combobox/combobox-base.d.ts +37 -0
- package/dist/ui/combobox/combobox-base.d.ts.map +1 -0
- package/dist/ui/combobox/combobox.d.ts +6 -0
- package/dist/ui/combobox/combobox.d.ts.map +1 -0
- package/dist/ui/combobox/index.d.ts +4 -0
- package/dist/ui/combobox/index.d.ts.map +1 -0
- package/dist/ui/combobox/types.d.ts +70 -0
- package/dist/ui/combobox/types.d.ts.map +1 -0
- package/dist/ui/combobox/variants.d.ts +17 -0
- package/dist/ui/combobox/variants.d.ts.map +1 -0
- package/dist/ui/combobox.js +510 -0
- package/dist/ui/combobox.js.map +1 -0
- package/dist/ui/combobox.mjs +495 -0
- package/dist/ui/combobox.mjs.map +1 -0
- package/dist/ui/dynamic-stepper.js +18 -17
- package/dist/ui/dynamic-stepper.js.map +1 -1
- package/dist/ui/dynamic-stepper.mjs +7 -6
- package/dist/ui/dynamic-stepper.mjs.map +1 -1
- package/dist/ui/pagination.js +14 -13
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/pagination.mjs +6 -5
- package/dist/ui/pagination.mjs.map +1 -1
- package/package.json +1 -1
- package/src/design-system/combobox.ts +204 -0
- package/src/design-system/index.ts +1 -0
- package/src/hooks/index.ts +50 -0
- package/src/hooks/useCookie/index.ts +5 -0
- package/src/hooks/useCookie/useCookie.test.ts +57 -0
- package/src/hooks/useCookie/useCookie.ts +133 -0
- package/src/hooks/useCountdown/index.ts +5 -0
- package/src/hooks/useCountdown/useCountdown.test.ts +113 -0
- package/src/hooks/useCountdown/useCountdown.ts +106 -0
- package/src/hooks/useEventListener/index.ts +4 -0
- package/src/hooks/useEventListener/useEventListener.test.ts +60 -0
- package/src/hooks/useEventListener/useEventListener.ts +98 -0
- package/src/hooks/useGeolocation/index.ts +6 -0
- package/src/hooks/useGeolocation/useGeolocation.test.ts +108 -0
- package/src/hooks/useGeolocation/useGeolocation.ts +173 -0
- package/src/hooks/useHotkeys/index.ts +5 -0
- package/src/hooks/useHotkeys/useHotkeys.test.ts +82 -0
- package/src/hooks/useHotkeys/useHotkeys.ts +130 -0
- package/src/hooks/useIdleTimeout/index.ts +5 -0
- package/src/hooks/useIdleTimeout/useIdleTimeout.test.ts +97 -0
- package/src/hooks/useIdleTimeout/useIdleTimeout.ts +111 -0
- package/src/hooks/useInterval/index.ts +1 -0
- package/src/hooks/useInterval/useInterval.test.ts +56 -0
- package/src/hooks/useInterval/useInterval.ts +36 -0
- package/src/hooks/useKeyPress/index.ts +1 -0
- package/src/hooks/useKeyPress/useKeyPress.test.ts +67 -0
- package/src/hooks/useKeyPress/useKeyPress.ts +65 -0
- package/src/hooks/useLongPress/index.ts +5 -0
- package/src/hooks/useLongPress/useLongPress.test.ts +180 -0
- package/src/hooks/useLongPress/useLongPress.ts +177 -0
- package/src/hooks/usePrevious/index.ts +1 -0
- package/src/hooks/usePrevious/usePrevious.test.ts +33 -0
- package/src/hooks/usePrevious/usePrevious.ts +24 -0
- package/src/hooks/useScrollPosition/index.ts +5 -0
- package/src/hooks/useScrollPosition/useScrollPosition.test.ts +69 -0
- package/src/hooks/useScrollPosition/useScrollPosition.ts +88 -0
- package/src/hooks/useTimeout/index.ts +1 -0
- package/src/hooks/useTimeout/useTimeout.test.ts +63 -0
- package/src/hooks/useTimeout/useTimeout.ts +58 -0
- package/src/hooks/useVirtualList/index.ts +6 -0
- package/src/hooks/useVirtualList/useVirtualList.test.ts +102 -0
- package/src/hooks/useVirtualList/useVirtualList.ts +144 -0
- package/src/lib/facade.test.ts +7 -7
- package/src/lib/facade.ts +6 -2
- package/src/ui/combobox/combobox-base.tsx +552 -0
- package/src/ui/combobox/combobox.test.tsx +292 -0
- package/src/ui/combobox/combobox.tsx +8 -0
- package/src/ui/combobox/index.ts +33 -0
- package/src/ui/combobox/types.ts +91 -0
- package/src/ui/combobox/variants.ts +58 -0
- package/dist/chunk-5J6QMTES.js.map +0 -1
- 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,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
|
+
}
|
package/src/lib/facade.test.ts
CHANGED
|
@@ -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(
|
|
88
|
-
|
|
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(
|
|
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
|
-
?
|
|
443
|
-
|
|
442
|
+
? names.has(baseName)
|
|
443
|
+
? baseName
|
|
444
|
+
: undefined
|
|
445
|
+
: hasDarkPair
|
|
446
|
+
? darkName
|
|
447
|
+
: undefined,
|
|
444
448
|
} satisfies ZuiVariable;
|
|
445
449
|
});
|
|
446
450
|
}
|