@nikala-ui/hooks 0.8.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.
@@ -0,0 +1,89 @@
1
+ import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export interface CreateResizeObserverOptions extends ResizeObserverOptions {
4
+ /** Whether the observer is active. Defaults to true. */
5
+ enabled?: boolean | Accessor<boolean>;
6
+ }
7
+
8
+ /**
9
+ * SolidJS reactive primitive for observing element size changes via ResizeObserver.
10
+ *
11
+ * @param target Target element or accessor returning HTML element.
12
+ * @param callback Observer callback invoked on element resize events.
13
+ * @param options ResizeObserver options (box, enabled).
14
+ */
15
+ export function createResizeObserver(
16
+ target: HTMLElement | Accessor<HTMLElement | undefined>,
17
+ callback: ResizeObserverCallback,
18
+ options: CreateResizeObserverOptions = {}
19
+ ): void {
20
+ const getTarget = (): HTMLElement | undefined => {
21
+ if (typeof target === "function") {
22
+ return (target as Accessor<HTMLElement | undefined>)();
23
+ }
24
+ return target;
25
+ };
26
+
27
+ const isEnabled = (): boolean => {
28
+ if (typeof options.enabled === "function") {
29
+ return options.enabled();
30
+ }
31
+ return options.enabled ?? true;
32
+ };
33
+
34
+ createEffect(() => {
35
+ if (typeof window === "undefined" || !window.ResizeObserver) {
36
+ return;
37
+ }
38
+
39
+ if (!isEnabled()) return;
40
+
41
+ const el = getTarget();
42
+ if (!el) return;
43
+
44
+ const observer = new ResizeObserver(callback);
45
+ observer.observe(el, { box: options.box });
46
+
47
+ onCleanup(() => {
48
+ observer.disconnect();
49
+ });
50
+ });
51
+ }
52
+
53
+ export interface CreateElementSizeReturn {
54
+ /** Accessor for element width in pixels */
55
+ width: Accessor<number>;
56
+ /** Accessor for element height in pixels */
57
+ height: Accessor<number>;
58
+ }
59
+
60
+ /**
61
+ * SolidJS reactive primitive returning width and height accessors for a target HTML element.
62
+ *
63
+ * @param target Target element or accessor returning HTML element.
64
+ * @param options ResizeObserver options.
65
+ */
66
+ export function createElementSize(
67
+ target: HTMLElement | Accessor<HTMLElement | undefined>,
68
+ options: CreateResizeObserverOptions = {}
69
+ ): CreateElementSizeReturn {
70
+ const [width, setWidth] = createSignal(0);
71
+ const [height, setHeight] = createSignal(0);
72
+
73
+ createResizeObserver(
74
+ target,
75
+ (entries) => {
76
+ const entry = entries[0];
77
+ if (entry) {
78
+ setWidth(entry.contentRect.width);
79
+ setHeight(entry.contentRect.height);
80
+ }
81
+ },
82
+ options
83
+ );
84
+
85
+ return {
86
+ width,
87
+ height,
88
+ };
89
+ }
@@ -0,0 +1,129 @@
1
+ import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export type ScrollDirection = "up" | "down" | "left" | "right" | "none";
4
+
5
+ export interface CreateScrollPositionOptions {
6
+ /** Target element to observe scroll events on. Defaults to window. */
7
+ target?: HTMLElement | Window | Accessor<HTMLElement | Window | undefined>;
8
+ }
9
+
10
+ export interface CreateScrollPositionReturn {
11
+ /** Accessor for horizontal scroll position (scrollLeft / pageXOffset) */
12
+ x: Accessor<number>;
13
+ /** Accessor for vertical scroll position (scrollTop / pageYOffset) */
14
+ y: Accessor<number>;
15
+ /** Accessor indicating if element is actively scrolling */
16
+ isScrolling: Accessor<boolean>;
17
+ /** Accessor for current scroll direction ('up', 'down', 'left', 'right', 'none') */
18
+ direction: Accessor<ScrollDirection>;
19
+ /** Accessor indicating if scroll is at top (y <= 0) */
20
+ isAtTop: Accessor<boolean>;
21
+ /** Accessor indicating if scroll is at bottom of container */
22
+ isAtBottom: Accessor<boolean>;
23
+ /** Function to programmatically scroll target element */
24
+ scrollTo: (options: ScrollToOptions) => void;
25
+ }
26
+
27
+ /**
28
+ * SolidJS reactive primitive for tracking target element or window scroll position and scroll metrics.
29
+ *
30
+ * @param options Configuration options including target element.
31
+ */
32
+ export function createScrollPosition(
33
+ options: CreateScrollPositionOptions = {}
34
+ ): CreateScrollPositionReturn {
35
+ const [x, setX] = createSignal(0);
36
+ const [y, setY] = createSignal(0);
37
+ const [isScrolling, setIsScrolling] = createSignal(false);
38
+ const [direction, setDirection] = createSignal<ScrollDirection>("none");
39
+ const [isAtTop, setIsAtTop] = createSignal(true);
40
+ const [isAtBottom, setIsAtBottom] = createSignal(false);
41
+
42
+ let scrollTimeout: ReturnType<typeof setTimeout> | undefined;
43
+ let lastX = 0;
44
+ let lastY = 0;
45
+
46
+ const getTarget = (): HTMLElement | Window | undefined => {
47
+ if (typeof window === "undefined") return undefined;
48
+ if (!options.target) return window;
49
+ if (typeof options.target === "function") {
50
+ return (options.target as Accessor<HTMLElement | Window | undefined>)();
51
+ }
52
+ return options.target;
53
+ };
54
+
55
+ const updateScroll = () => {
56
+ const target = getTarget();
57
+ if (!target) return;
58
+
59
+ let currentX = 0;
60
+ let currentY = 0;
61
+ let maxScrollY = 0;
62
+
63
+ if (target === window) {
64
+ currentX = window.scrollX || window.pageXOffset;
65
+ currentY = window.scrollY || window.pageYOffset;
66
+ maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
67
+ } else {
68
+ const el = target as HTMLElement;
69
+ currentX = el.scrollLeft;
70
+ currentY = el.scrollTop;
71
+ maxScrollY = el.scrollHeight - el.clientHeight;
72
+ }
73
+
74
+ // Determine direction
75
+ const deltaX = currentX - lastX;
76
+ const deltaY = currentY - lastY;
77
+
78
+ if (Math.abs(deltaY) > Math.abs(deltaX)) {
79
+ if (deltaY > 0) setDirection("down");
80
+ else if (deltaY < 0) setDirection("up");
81
+ } else if (Math.abs(deltaX) > 0) {
82
+ if (deltaX > 0) setDirection("right");
83
+ else if (deltaX < 0) setDirection("left");
84
+ }
85
+
86
+ lastX = currentX;
87
+ lastY = currentY;
88
+
89
+ setX(currentX);
90
+ setY(currentY);
91
+ setIsAtTop(currentY <= 0);
92
+ setIsAtBottom(maxScrollY > 0 && currentY >= maxScrollY - 1);
93
+ setIsScrolling(true);
94
+
95
+ if (scrollTimeout) clearTimeout(scrollTimeout);
96
+ scrollTimeout = setTimeout(() => {
97
+ setIsScrolling(false);
98
+ }, 150);
99
+ };
100
+
101
+ createEffect(() => {
102
+ const target = getTarget();
103
+ if (!target) return;
104
+
105
+ updateScroll();
106
+
107
+ target.addEventListener("scroll", updateScroll, { passive: true });
108
+ onCleanup(() => {
109
+ target.removeEventListener("scroll", updateScroll);
110
+ if (scrollTimeout) clearTimeout(scrollTimeout);
111
+ });
112
+ });
113
+
114
+ const scrollTo = (scrollOptions: ScrollToOptions) => {
115
+ const target = getTarget();
116
+ if (!target) return;
117
+ target.scrollTo(scrollOptions);
118
+ };
119
+
120
+ return {
121
+ x,
122
+ y,
123
+ isScrolling,
124
+ direction,
125
+ isAtTop,
126
+ isAtBottom,
127
+ scrollTo,
128
+ };
129
+ }
@@ -0,0 +1,106 @@
1
+ import { createSignal, onMount, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export type StorageType = "local" | "session";
4
+
5
+ /**
6
+ * Helper to safely read item from Web Storage.
7
+ */
8
+ function readStorage<T>(key: string, storage: Storage | undefined, fallback: T): T {
9
+ if (!storage) return fallback;
10
+ try {
11
+ const item = storage.getItem(key);
12
+ return item !== null ? JSON.parse(item) : fallback;
13
+ } catch (error) {
14
+ console.warn(`[nikala-ui/hooks] Error reading storage key "${key}":`, error);
15
+ return fallback;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * SolidJS reactive primitive for Web Storage (localStorage / sessionStorage) synchronization.
21
+ *
22
+ * @param key Storage key name.
23
+ * @param initialValue Default initial value if key doesn't exist.
24
+ * @param type Storage type: "local" (default) or "session".
25
+ */
26
+ export function createStorage<T>(
27
+ key: string,
28
+ initialValue: T | Accessor<T>,
29
+ type: StorageType = "local"
30
+ ): [value: Accessor<T>, setValue: (val: T | ((prev: T) => T)) => void, remove: () => void] {
31
+ const getStorage = (): Storage | undefined => {
32
+ if (typeof window === "undefined") return undefined;
33
+ return type === "local" ? window.localStorage : window.sessionStorage;
34
+ };
35
+
36
+ const getFallback = (): T => (typeof initialValue === "function" ? (initialValue as Accessor<T>)() : initialValue);
37
+
38
+ const [value, setInternalValue] = createSignal<T>(getFallback());
39
+
40
+ const setValue = (val: T | ((prev: T) => T)) => {
41
+ const storage = getStorage();
42
+ const currentValue = value();
43
+ const nextValue = typeof val === "function" ? (val as (prev: T) => T)(currentValue) : val;
44
+
45
+ setInternalValue(() => nextValue);
46
+
47
+ if (storage) {
48
+ try {
49
+ storage.setItem(key, JSON.stringify(nextValue));
50
+ } catch (error) {
51
+ console.warn(`[nikala-ui/hooks] Error setting storage key "${key}":`, error);
52
+ }
53
+ }
54
+ };
55
+
56
+ const remove = () => {
57
+ const storage = getStorage();
58
+ setInternalValue(() => getFallback());
59
+ if (storage) {
60
+ storage.removeItem(key);
61
+ }
62
+ };
63
+
64
+ onMount(() => {
65
+ if (typeof window === "undefined") return;
66
+
67
+ const storage = getStorage();
68
+ if (storage) {
69
+ const stored = readStorage<T>(key, storage, getFallback());
70
+ setInternalValue(() => stored);
71
+ }
72
+
73
+ const handleStorageChange = (event: StorageEvent) => {
74
+ if (event.key === key) {
75
+ setInternalValue(() => (event.newValue !== null ? JSON.parse(event.newValue) : getFallback()));
76
+ }
77
+ };
78
+
79
+ window.addEventListener("storage", handleStorageChange);
80
+ onCleanup(() => {
81
+ window.removeEventListener("storage", handleStorageChange);
82
+ });
83
+ });
84
+
85
+ return [value, setValue, remove];
86
+ }
87
+
88
+ /**
89
+ * SolidJS reactive primitive for localStorage synchronization across components and browser tabs.
90
+ */
91
+ export function createLocalStorage<T>(
92
+ key: string,
93
+ initialValue: T | Accessor<T>
94
+ ): [value: Accessor<T>, setValue: (val: T | ((prev: T) => T)) => void, remove: () => void] {
95
+ return createStorage<T>(key, initialValue, "local");
96
+ }
97
+
98
+ /**
99
+ * SolidJS reactive primitive for sessionStorage synchronization.
100
+ */
101
+ export function createSessionStorage<T>(
102
+ key: string,
103
+ initialValue: T | Accessor<T>
104
+ ): [value: Accessor<T>, setValue: (val: T | ((prev: T) => T)) => void, remove: () => void] {
105
+ return createStorage<T>(key, initialValue, "session");
106
+ }
@@ -0,0 +1,150 @@
1
+ import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export interface CreateTimerOptions {
4
+ /** Whether the timer starts running automatically on mount. Defaults to false. */
5
+ autostart?: boolean;
6
+ }
7
+
8
+ export interface CreateTimerReturn {
9
+ /** Accessor indicating if timer is actively running */
10
+ isRunning: Accessor<boolean>;
11
+ /** Function to start or resume the timer */
12
+ start: () => void;
13
+ /** Function to pause/stop the timer */
14
+ stop: () => void;
15
+ /** Function to reset and restart the timer */
16
+ reset: () => void;
17
+ /** Function to toggle running state */
18
+ toggle: () => void;
19
+ }
20
+
21
+ /**
22
+ * SolidJS reactive primitive for recurring interval timers.
23
+ *
24
+ * @param intervalMs Interval duration in milliseconds.
25
+ * @param callback Function to execute on each interval tick.
26
+ * @param options Configuration options including autostart.
27
+ */
28
+ export function createTimer(
29
+ intervalMs: number | Accessor<number>,
30
+ callback: () => void,
31
+ options: CreateTimerOptions = {}
32
+ ): CreateTimerReturn {
33
+ const [isRunning, setIsRunning] = createSignal<boolean>(options.autostart ?? false);
34
+ let timerId: ReturnType<typeof setInterval> | undefined;
35
+
36
+ const getInterval = () => (typeof intervalMs === "function" ? intervalMs() : intervalMs);
37
+
38
+ const stop = () => {
39
+ if (timerId) {
40
+ clearInterval(timerId);
41
+ timerId = undefined;
42
+ }
43
+ setIsRunning(false);
44
+ };
45
+
46
+ const start = () => {
47
+ stop();
48
+ const delay = getInterval();
49
+ if (delay <= 0) return;
50
+
51
+ setIsRunning(true);
52
+ timerId = setInterval(() => {
53
+ callback();
54
+ }, delay);
55
+ };
56
+
57
+ const reset = () => {
58
+ start();
59
+ };
60
+
61
+ const toggle = () => {
62
+ if (isRunning()) {
63
+ stop();
64
+ } else {
65
+ start();
66
+ }
67
+ };
68
+
69
+ createEffect(() => {
70
+ if (isRunning()) {
71
+ start();
72
+ }
73
+ });
74
+
75
+ onCleanup(() => {
76
+ stop();
77
+ });
78
+
79
+ return {
80
+ isRunning,
81
+ start,
82
+ stop,
83
+ reset,
84
+ toggle,
85
+ };
86
+ }
87
+
88
+ export interface CreateCountdownOptions extends CreateTimerOptions {
89
+ /** Callback fired when countdown reaches zero */
90
+ onComplete?: () => void;
91
+ }
92
+
93
+ export interface CreateCountdownReturn extends CreateTimerReturn {
94
+ /** Accessor for remaining seconds */
95
+ remainingSeconds: Accessor<number>;
96
+ /** Accessor for formatted time string (MM:SS) */
97
+ formatted: Accessor<string>;
98
+ }
99
+
100
+ /**
101
+ * SolidJS reactive primitive for countdown timers.
102
+ *
103
+ * @param durationSeconds Total countdown duration in seconds.
104
+ * @param options Configuration options including autostart and onComplete callback.
105
+ */
106
+ export function createCountdown(
107
+ durationSeconds: number | Accessor<number>,
108
+ options: CreateCountdownOptions = {}
109
+ ): CreateCountdownReturn {
110
+ const getInitialDuration = () =>
111
+ typeof durationSeconds === "function" ? durationSeconds() : durationSeconds;
112
+
113
+ const [remaining, setRemaining] = createSignal<number>(getInitialDuration());
114
+
115
+ const timer = createTimer(
116
+ 1000,
117
+ () => {
118
+ setRemaining((prev) => {
119
+ if (prev <= 1) {
120
+ timer.stop();
121
+ options.onComplete?.();
122
+ return 0;
123
+ }
124
+ return prev - 1;
125
+ });
126
+ },
127
+ { autostart: options.autostart }
128
+ );
129
+
130
+ const reset = () => {
131
+ setRemaining(getInitialDuration());
132
+ timer.start();
133
+ };
134
+
135
+ const formatted = (): string => {
136
+ const total = remaining();
137
+ const minutes = Math.floor(total / 60);
138
+ const seconds = total % 60;
139
+ const mm = String(minutes).padStart(2, "0");
140
+ const ss = String(seconds).padStart(2, "0");
141
+ return `${mm}:${ss}`;
142
+ };
143
+
144
+ return {
145
+ ...timer,
146
+ remainingSeconds: remaining,
147
+ formatted,
148
+ reset,
149
+ };
150
+ }
@@ -0,0 +1,41 @@
1
+ import { createSignal, onMount, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export interface CreateWindowSizeReturn {
4
+ /** Accessor for current window inner width in pixels */
5
+ width: Accessor<number>;
6
+ /** Accessor for current window inner height in pixels */
7
+ height: Accessor<number>;
8
+ }
9
+
10
+ /**
11
+ * SolidJS reactive primitive for tracking window viewport dimensions (width and height).
12
+ */
13
+ export function createWindowSize(): CreateWindowSizeReturn {
14
+ const [width, setWidth] = createSignal<number>(
15
+ typeof window !== "undefined" ? window.innerWidth : 0
16
+ );
17
+ const [height, setHeight] = createSignal<number>(
18
+ typeof window !== "undefined" ? window.innerHeight : 0
19
+ );
20
+
21
+ onMount(() => {
22
+ if (typeof window === "undefined") return;
23
+
24
+ const handleResize = () => {
25
+ setWidth(window.innerWidth);
26
+ setHeight(window.innerHeight);
27
+ };
28
+
29
+ handleResize();
30
+
31
+ window.addEventListener("resize", handleResize);
32
+ onCleanup(() => {
33
+ window.removeEventListener("resize", handleResize);
34
+ });
35
+ });
36
+
37
+ return {
38
+ width,
39
+ height,
40
+ };
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export * from "./create-controllable-signal";
2
+ export * from "./create-click-outside";
3
+ export * from "./create-clipboard";
4
+ export * from "./create-keybindings";
5
+ export * from "./create-lock-scroll";
6
+ export * from "./create-disclosure";
7
+ export * from "./create-media-query";
8
+ export * from "./create-debounce";
9
+ export * from "./create-intersection-observer";
10
+ export * from "./create-timer";
11
+ export * from "./create-resize-observer";
12
+ export * from "./create-window-size";
13
+ export * from "./create-scroll-position";
14
+ export * from "./create-focus-trap";
15
+ export * from "./create-mouse-position";
16
+ export * from "./create-long-press";
17
+ export * from "./create-hover";
18
+ export * from "./create-storage";
19
+ export * from "./create-previous";
20
+ export * from "./create-network-status";
21
+ export * from "./create-color-mode";
22
+ export * from "./create-form";
23
+ export * from "./create-input-mask";
24
+ export * from "./create-idle";
25
+ export * from "./create-active-element";