@pyreon/hooks 0.3.0 → 0.11.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/lib/index.d.ts CHANGED
@@ -13,6 +13,33 @@ declare function useBreakpoint(breakpoints?: BreakpointMap): () => string;
13
13
  */
14
14
  declare function useClickOutside(getEl: () => HTMLElement | null, handler: () => void): void;
15
15
  //#endregion
16
+ //#region src/useClipboard.d.ts
17
+ interface UseClipboardResult {
18
+ /** Copy text to clipboard. Returns true on success. */
19
+ copy: (text: string) => Promise<boolean>;
20
+ /** Whether the last copy succeeded (resets after timeout). */
21
+ copied: () => boolean;
22
+ /** The last successfully copied text. */
23
+ text: () => string;
24
+ }
25
+ /**
26
+ * Reactive clipboard access — copy text and track copied state.
27
+ *
28
+ * @param options.timeout - ms before `copied` resets to false (default: 2000)
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * const { copy, copied } = useClipboard()
33
+ *
34
+ * <button onClick={() => copy("hello")}>
35
+ * {() => copied() ? "Copied!" : "Copy"}
36
+ * </button>
37
+ * ```
38
+ */
39
+ declare function useClipboard(options?: {
40
+ timeout?: number;
41
+ }): UseClipboardResult;
42
+ //#endregion
16
43
  //#region src/useColorScheme.d.ts
17
44
  /**
18
45
  * Returns the OS color scheme preference as 'light' or 'dark'.
@@ -57,6 +84,39 @@ declare const useDebouncedCallback: UseDebouncedCallback;
57
84
  */
58
85
  declare function useDebouncedValue<T>(getter: () => T, delayMs: number): () => T;
59
86
  //#endregion
87
+ //#region src/useDialog.d.ts
88
+ interface UseDialogResult {
89
+ /** Whether the dialog is currently open. */
90
+ open: () => boolean;
91
+ /** Open the dialog. */
92
+ show: () => void;
93
+ /** Open as modal (with backdrop, traps focus). */
94
+ showModal: () => void;
95
+ /** Close the dialog. */
96
+ close: () => void;
97
+ /** Toggle open/closed state. */
98
+ toggle: () => void;
99
+ /** Ref callback — pass to `ref` prop on a `<dialog>` element. */
100
+ ref: (el: HTMLDialogElement) => void;
101
+ }
102
+ /**
103
+ * Signal-driven dialog management for the native `<dialog>` element.
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * const dialog = useDialog()
108
+ *
109
+ * <button onClick={dialog.showModal}>Open</button>
110
+ * <dialog ref={dialog.ref}>
111
+ * <p>Modal content</p>
112
+ * <button onClick={dialog.close}>Close</button>
113
+ * </dialog>
114
+ * ```
115
+ */
116
+ declare function useDialog(options?: {
117
+ onClose?: () => void;
118
+ }): UseDialogResult;
119
+ //#endregion
60
120
  //#region src/useElementSize.d.ts
61
121
  interface Size {
62
122
  width: number;
@@ -67,6 +127,25 @@ interface Size {
67
127
  */
68
128
  declare function useElementSize(getEl: () => HTMLElement | null): () => Size;
69
129
  //#endregion
130
+ //#region src/useEventListener.d.ts
131
+ /**
132
+ * Attach an event listener with automatic cleanup on unmount.
133
+ * Works with Window, Document, HTMLElement, or any EventTarget.
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * useEventListener("keydown", (e) => {
138
+ * if (e.key === "Escape") close()
139
+ * })
140
+ *
141
+ * useEventListener("scroll", handleScroll, { passive: true })
142
+ *
143
+ * // On a specific element:
144
+ * useEventListener("click", handler, {}, () => buttonRef.current)
145
+ * ```
146
+ */
147
+ declare function useEventListener<K extends keyof WindowEventMap>(event: K, handler: (e: WindowEventMap[K]) => void, options?: boolean | AddEventListenerOptions, target?: () => EventTarget | null | undefined): void;
148
+ //#endregion
70
149
  //#region src/useFocus.d.ts
71
150
  interface UseFocusResult {
72
151
  focused: () => boolean;
@@ -105,6 +184,50 @@ interface UseHoverResult {
105
184
  */
106
185
  declare function useHover(): UseHoverResult;
107
186
  //#endregion
187
+ //#region src/useInfiniteScroll.d.ts
188
+ interface UseInfiniteScrollOptions {
189
+ /** Distance from bottom (px) to trigger load. Default: 100 */
190
+ threshold?: number;
191
+ /** Whether loading is in progress (prevents duplicate calls). */
192
+ loading?: () => boolean;
193
+ /** Whether there's more data to load. Default: true */
194
+ hasMore?: () => boolean;
195
+ /** Scroll direction. Default: "down" */
196
+ direction?: "up" | "down";
197
+ }
198
+ interface UseInfiniteScrollResult {
199
+ /** Attach to the scroll container element. */
200
+ ref: (el: HTMLElement | null) => void;
201
+ /** Whether the sentinel is currently visible. */
202
+ triggered: () => boolean;
203
+ }
204
+ /**
205
+ * Signal-driven infinite scroll using IntersectionObserver.
206
+ * Calls `onLoadMore` when the user scrolls near the end of the container.
207
+ *
208
+ * @example
209
+ * ```tsx
210
+ * const items = signal<Item[]>([])
211
+ * const loading = signal(false)
212
+ * const hasMore = signal(true)
213
+ *
214
+ * const { ref } = useInfiniteScroll(() => {
215
+ * loading.set(true)
216
+ * const next = await fetchMore()
217
+ * items.update(prev => [...prev, ...next])
218
+ * hasMore.set(next.length > 0)
219
+ * loading.set(false)
220
+ * }, { loading, hasMore })
221
+ *
222
+ * <div ref={ref} style={{ overflowY: "auto", height: "400px" }}>
223
+ * <For each={items()} by={i => i.id}>
224
+ * {item => <div>{item.name}</div>}
225
+ * </For>
226
+ * </div>
227
+ * ```
228
+ */
229
+ declare function useInfiniteScroll(onLoadMore: () => void | Promise<void>, options?: UseInfiniteScrollOptions): UseInfiniteScrollResult;
230
+ //#endregion
108
231
  //#region src/useIntersection.d.ts
109
232
  /**
110
233
  * Observe element intersection reactively.
@@ -178,6 +301,21 @@ type UseMergedRef = <T>(...refs: (Ref<T> | undefined)[]) => (node: T | null) =>
178
301
  */
179
302
  declare const useMergedRef: <T>(...refs: (Ref<T> | undefined)[]) => ((node: T | null) => void);
180
303
  //#endregion
304
+ //#region src/useOnline.d.ts
305
+ /**
306
+ * Reactive online/offline status.
307
+ * Tracks `navigator.onLine` and updates on connectivity changes.
308
+ *
309
+ * @example
310
+ * ```tsx
311
+ * const online = useOnline()
312
+ * <Show when={!online()} fallback={<App />}>
313
+ * <OfflineBanner />
314
+ * </Show>
315
+ * ```
316
+ */
317
+ declare function useOnline(): () => boolean;
318
+ //#endregion
181
319
  //#region src/usePrevious.d.ts
182
320
  /**
183
321
  * Track the previous value of a reactive getter.
@@ -261,6 +399,37 @@ type UseThrottledCallback = <T extends (...args: any[]) => any>(callback: T, del
261
399
  */
262
400
  declare const useThrottledCallback: UseThrottledCallback;
263
401
  //#endregion
402
+ //#region src/useTimeAgo.d.ts
403
+ type TimeUnit = "second" | "minute" | "hour" | "day" | "week" | "month" | "year";
404
+ interface UseTimeAgoOptions {
405
+ /** Custom formatter. Receives the value, unit, and whether it's in the past. */
406
+ formatter?: (value: number, unit: TimeUnit, isPast: boolean) => string;
407
+ /** Update interval override in ms. If not set, adapts based on age. */
408
+ interval?: number;
409
+ }
410
+ /**
411
+ * Reactive relative time that auto-updates.
412
+ * Returns a signal that displays "2 minutes ago", "just now", etc.
413
+ *
414
+ * @param date - Date object, timestamp, or reactive getter
415
+ *
416
+ * @example
417
+ * ```tsx
418
+ * const timeAgo = useTimeAgo(post.createdAt)
419
+ * <span>{timeAgo}</span>
420
+ * // Renders: "5 minutes ago" → "6 minutes ago" (auto-updates)
421
+ *
422
+ * // With reactive date:
423
+ * const timeAgo = useTimeAgo(() => selectedPost().createdAt)
424
+ *
425
+ * // With custom formatter (e.g. for i18n):
426
+ * const timeAgo = useTimeAgo(date, {
427
+ * formatter: (value, unit, isPast) => t('time.' + unit, { count: value })
428
+ * })
429
+ * ```
430
+ */
431
+ declare function useTimeAgo(date: Date | number | (() => Date | number), options?: UseTimeAgoOptions): () => string;
432
+ //#endregion
264
433
  //#region src/useTimeout.d.ts
265
434
  type UseTimeout = (callback: () => void, delay: number | null) => {
266
435
  reset: () => void;
@@ -308,5 +477,5 @@ interface WindowSize {
308
477
  */
309
478
  declare function useWindowResize(throttleMs?: number): () => WindowSize;
310
479
  //#endregion
311
- export { type BreakpointMap, type Size, type UseControllableState, type UseDebouncedCallback, type UseFocusResult, type UseHoverResult, type UseInterval, type UseIsomorphicLayoutEffect, type UseLatest, type UseMergedRef, type UseRootSize, type UseSpacing, type UseThemeValue, type UseThrottledCallback, type UseTimeout, type UseToggleResult, type UseUpdateEffect, type WindowSize, useBreakpoint, useClickOutside, useColorScheme, useControllableState, useDebouncedCallback, useDebouncedValue, useElementSize, useFocus, useFocusTrap, useHover, useIntersection, useInterval, useIsomorphicLayoutEffect, useKeyboard, useLatest, useMediaQuery, useMergedRef, usePrevious, useReducedMotion, useRootSize, useScrollLock, useSpacing, useThemeValue, useThrottledCallback, useTimeout, useToggle, useUpdateEffect, useWindowResize };
480
+ export { type BreakpointMap, type Size, type UseClipboardResult, type UseControllableState, type UseDebouncedCallback, type UseDialogResult, type UseFocusResult, type UseHoverResult, type UseInfiniteScrollOptions, type UseInfiniteScrollResult, type UseInterval, type UseIsomorphicLayoutEffect, type UseLatest, type UseMergedRef, type UseRootSize, type UseSpacing, type UseThemeValue, type UseThrottledCallback, type UseTimeAgoOptions, type UseTimeout, type UseToggleResult, type UseUpdateEffect, type WindowSize, useBreakpoint, useClickOutside, useClipboard, useColorScheme, useControllableState, useDebouncedCallback, useDebouncedValue, useDialog, useElementSize, useEventListener, useFocus, useFocusTrap, useHover, useInfiniteScroll, useIntersection, useInterval, useIsomorphicLayoutEffect, useKeyboard, useLatest, useMediaQuery, useMergedRef, useOnline, usePrevious, useReducedMotion, useRootSize, useScrollLock, useSpacing, useThemeValue, useThrottledCallback, useTimeAgo, useTimeout, useToggle, useUpdateEffect, useWindowResize };
312
481
  //# sourceMappingURL=index2.d.ts.map
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { onMount, onUnmount } from "@pyreon/core";
2
- import { computed, effect, signal, watch } from "@pyreon/reactivity";
2
+ import { computed, effect, onCleanup, signal, watch } from "@pyreon/reactivity";
3
3
  import { useTheme } from "@pyreon/styler";
4
4
  import { get, throttle } from "@pyreon/ui-core";
5
5
 
@@ -64,6 +64,50 @@ function useClickOutside(getEl, handler) {
64
64
  });
65
65
  }
66
66
 
67
+ //#endregion
68
+ //#region src/useClipboard.ts
69
+ /**
70
+ * Reactive clipboard access — copy text and track copied state.
71
+ *
72
+ * @param options.timeout - ms before `copied` resets to false (default: 2000)
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * const { copy, copied } = useClipboard()
77
+ *
78
+ * <button onClick={() => copy("hello")}>
79
+ * {() => copied() ? "Copied!" : "Copy"}
80
+ * </button>
81
+ * ```
82
+ */
83
+ function useClipboard(options) {
84
+ const timeout = options?.timeout ?? 2e3;
85
+ const copied = signal(false);
86
+ const text = signal("");
87
+ let timer;
88
+ const copy = async (value) => {
89
+ try {
90
+ await navigator.clipboard.writeText(value);
91
+ text.set(value);
92
+ copied.set(true);
93
+ if (timer) clearTimeout(timer);
94
+ timer = setTimeout(() => copied.set(false), timeout);
95
+ return true;
96
+ } catch {
97
+ copied.set(false);
98
+ return false;
99
+ }
100
+ };
101
+ onCleanup(() => {
102
+ if (timer) clearTimeout(timer);
103
+ });
104
+ return {
105
+ copy,
106
+ copied,
107
+ text
108
+ };
109
+ }
110
+
67
111
  //#endregion
68
112
  //#region src/useMediaQuery.ts
69
113
  /**
@@ -184,6 +228,64 @@ function useDebouncedValue(getter, delayMs) {
184
228
  return debounced;
185
229
  }
186
230
 
231
+ //#endregion
232
+ //#region src/useDialog.ts
233
+ /**
234
+ * Signal-driven dialog management for the native `<dialog>` element.
235
+ *
236
+ * @example
237
+ * ```tsx
238
+ * const dialog = useDialog()
239
+ *
240
+ * <button onClick={dialog.showModal}>Open</button>
241
+ * <dialog ref={dialog.ref}>
242
+ * <p>Modal content</p>
243
+ * <button onClick={dialog.close}>Close</button>
244
+ * </dialog>
245
+ * ```
246
+ */
247
+ function useDialog(options) {
248
+ const open = signal(false);
249
+ let dialogEl = null;
250
+ let closeHandler = null;
251
+ const show = () => {
252
+ dialogEl?.show();
253
+ open.set(true);
254
+ };
255
+ const showModal = () => {
256
+ dialogEl?.showModal();
257
+ open.set(true);
258
+ };
259
+ const close = () => {
260
+ dialogEl?.close();
261
+ open.set(false);
262
+ };
263
+ const toggle = () => {
264
+ if (open()) close();
265
+ else showModal();
266
+ };
267
+ const ref = (el) => {
268
+ if (dialogEl && closeHandler) dialogEl.removeEventListener("close", closeHandler);
269
+ dialogEl = el;
270
+ closeHandler = () => {
271
+ open.set(false);
272
+ options?.onClose?.();
273
+ };
274
+ el.addEventListener("close", closeHandler);
275
+ };
276
+ onCleanup(() => {
277
+ if (dialogEl && closeHandler) dialogEl.removeEventListener("close", closeHandler);
278
+ });
279
+ return {
280
+ open,
281
+ show,
282
+ showModal,
283
+ close,
284
+ toggle,
285
+ ref
286
+ };
287
+ }
288
+
187
289
  //#endregion
188
290
  //#region src/useElementSize.ts
189
291
  /**
@@ -219,6 +321,31 @@ function useElementSize(getEl) {
219
321
  return size;
220
322
  }
221
323
 
324
+ //#endregion
325
+ //#region src/useEventListener.ts
326
+ /**
327
+ * Attach an event listener with automatic cleanup on unmount.
328
+ * Works with Window, Document, HTMLElement, or any EventTarget.
329
+ *
330
+ * @example
331
+ * ```tsx
332
+ * useEventListener("keydown", (e) => {
333
+ * if (e.key === "Escape") close()
334
+ * })
335
+ *
336
+ * useEventListener("scroll", handleScroll, { passive: true })
337
+ *
338
+ * // On a specific element:
339
+ * useEventListener("click", handler, {}, () => buttonRef.current)
340
+ * ```
341
+ */
342
+ function useEventListener(event, handler, options, target) {
343
+ if (!(typeof window !== "undefined")) return;
344
+ const el = target?.() ?? window;
345
+ el.addEventListener(event, handler, options);
346
+ onCleanup(() => el.removeEventListener(event, handler, options));
347
+ }
348
+
222
349
  //#endregion
223
350
  //#region src/useFocus.ts
224
351
  /**
@@ -288,6 +415,88 @@ function useHover() {
288
415
  };
289
416
  }
290
417
 
418
+ //#endregion
419
+ //#region src/useInfiniteScroll.ts
420
+ /**
421
+ * Signal-driven infinite scroll using IntersectionObserver.
422
+ * Calls `onLoadMore` when the user scrolls near the end of the container.
423
+ *
424
+ * @example
425
+ * ```tsx
426
+ * const items = signal<Item[]>([])
427
+ * const loading = signal(false)
428
+ * const hasMore = signal(true)
429
+ *
430
+ * const { ref } = useInfiniteScroll(() => {
431
+ * loading.set(true)
432
+ * const next = await fetchMore()
433
+ * items.update(prev => [...prev, ...next])
434
+ * hasMore.set(next.length > 0)
435
+ * loading.set(false)
436
+ * }, { loading, hasMore })
437
+ *
438
+ * <div ref={ref} style={{ overflowY: "auto", height: "400px" }}>
439
+ * <For each={items()} by={i => i.id}>
440
+ * {item => <div>{item.name}</div>}
441
+ * </For>
442
+ * </div>
443
+ * ```
444
+ */
445
+ function useInfiniteScroll(onLoadMore, options) {
446
+ const threshold = options?.threshold ?? 100;
447
+ const direction = options?.direction ?? "down";
448
+ const triggered = signal(false);
449
+ let observer = null;
450
+ let sentinel = null;
451
+ let containerEl = null;
452
+ const handleIntersect = (entries) => {
453
+ const entry = entries[0];
454
+ if (!entry) return;
455
+ triggered.set(entry.isIntersecting);
456
+ if (entry.isIntersecting) {
457
+ if (options?.loading?.()) return;
458
+ if (options?.hasMore && !options.hasMore()) return;
459
+ onLoadMore();
460
+ }
461
+ };
462
+ const setup = (el) => {
463
+ cleanup();
464
+ containerEl = el;
465
+ sentinel = document.createElement("div");
466
+ sentinel.style.height = "1px";
467
+ sentinel.style.width = "100%";
468
+ sentinel.style.pointerEvents = "none";
469
+ sentinel.setAttribute("aria-hidden", "true");
470
+ if (direction === "down") el.appendChild(sentinel);
471
+ else el.insertBefore(sentinel, el.firstChild);
472
+ observer = new IntersectionObserver(handleIntersect, {
473
+ root: el,
474
+ rootMargin: direction === "down" ? `0px 0px ${threshold}px 0px` : `${threshold}px 0px 0px 0px`,
475
+ threshold: 0
476
+ });
477
+ observer.observe(sentinel);
478
+ };
479
+ const cleanup = () => {
480
+ if (observer) {
481
+ observer.disconnect();
482
+ observer = null;
483
+ }
484
+ if (sentinel && containerEl) {
485
+ sentinel.remove();
486
+ sentinel = null;
487
+ }
488
+ };
489
+ const ref = (el) => {
490
+ if (el) setup(el);
491
+ else cleanup();
492
+ };
493
+ onCleanup(cleanup);
494
+ return {
495
+ ref,
496
+ triggered
497
+ };
498
+ }
499
+
291
500
  //#endregion
292
501
  //#region src/useIntersection.ts
293
502
  /**
@@ -388,6 +597,36 @@ const useMergedRef = (...refs) => {
388
597
  };
389
598
  };
390
599
 
600
+ //#endregion
601
+ //#region src/useOnline.ts
602
+ /**
603
+ * Reactive online/offline status.
604
+ * Tracks `navigator.onLine` and updates on connectivity changes.
605
+ *
606
+ * @example
607
+ * ```tsx
608
+ * const online = useOnline()
609
+ * <Show when={!online()} fallback={<App />}>
610
+ * <OfflineBanner />
611
+ * </Show>
612
+ * ```
613
+ */
614
+ function useOnline() {
615
+ const isBrowser = typeof window !== "undefined";
616
+ const online = signal(isBrowser ? navigator.onLine : true);
617
+ if (isBrowser) {
618
+ const setOnline = () => online.set(true);
619
+ const setOffline = () => online.set(false);
620
+ window.addEventListener("online", setOnline);
621
+ window.addEventListener("offline", setOffline);
622
+ onCleanup(() => {
623
+ window.removeEventListener("online", setOnline);
624
+ window.removeEventListener("offline", setOffline);
625
+ });
626
+ }
627
+ return online;
628
+ }
629
+
391
630
  //#endregion
392
631
  //#region src/usePrevious.ts
393
632
  /**
@@ -519,6 +758,119 @@ const useThrottledCallback = (callback, delay) => {
519
758
  return throttled;
520
759
  };
521
760
 
761
+ //#endregion
762
+ //#region src/useTimeAgo.ts
763
+ const INTERVALS = [
764
+ {
765
+ unit: "year",
766
+ seconds: 31536e3
767
+ },
768
+ {
769
+ unit: "month",
770
+ seconds: 2592e3
771
+ },
772
+ {
773
+ unit: "week",
774
+ seconds: 604800
775
+ },
776
+ {
777
+ unit: "day",
778
+ seconds: 86400
779
+ },
780
+ {
781
+ unit: "hour",
782
+ seconds: 3600
783
+ },
784
+ {
785
+ unit: "minute",
786
+ seconds: 60
787
+ },
788
+ {
789
+ unit: "second",
790
+ seconds: 1
791
+ }
792
+ ];
793
+ /**
794
+ * Determine how often to update based on the age of the timestamp.
795
+ * Recent times update more frequently.
796
+ */
797
+ function getRefreshInterval(diffSeconds) {
798
+ if (diffSeconds < 60) return 1e3;
799
+ if (diffSeconds < 3600) return 3e4;
800
+ if (diffSeconds < 86400) return 3e5;
801
+ return 36e5;
802
+ }
803
+ /**
804
+ * Default English formatter using Intl.RelativeTimeFormat.
805
+ */
806
+ const defaultFormatter = (() => {
807
+ const rtf = typeof Intl !== "undefined" ? new Intl.RelativeTimeFormat("en", { numeric: "auto" }) : void 0;
808
+ return (value, unit, isPast) => {
809
+ if (rtf) return rtf.format(isPast ? -value : value, unit);
810
+ const label = value === 1 ? unit : `${unit}s`;
811
+ return isPast ? `${value} ${label} ago` : `in ${value} ${label}`;
812
+ };
813
+ })();
814
+ /**
815
+ * Compute the relative time string for a given timestamp.
816
+ */
817
+ function computeTimeAgo(date, formatter) {
818
+ const now = Date.now();
819
+ const target = typeof date === "number" ? date : date.getTime();
820
+ const diff = Math.abs(now - target);
821
+ const diffSeconds = Math.floor(diff / 1e3);
822
+ const isPast = target < now;
823
+ if (diffSeconds < 5) return "just now";
824
+ for (const { unit, seconds } of INTERVALS) {
825
+ const value = Math.floor(diffSeconds / seconds);
826
+ if (value >= 1) return formatter(value, unit, isPast);
827
+ }
828
+ return "just now";
829
+ }
830
+ /**
831
+ * Reactive relative time that auto-updates.
832
+ * Returns a signal that displays "2 minutes ago", "just now", etc.
833
+ *
834
+ * @param date - Date object, timestamp, or reactive getter
835
+ *
836
+ * @example
837
+ * ```tsx
838
+ * const timeAgo = useTimeAgo(post.createdAt)
839
+ * <span>{timeAgo}</span>
840
+ * // Renders: "5 minutes ago" → "6 minutes ago" (auto-updates)
841
+ *
842
+ * // With reactive date:
843
+ * const timeAgo = useTimeAgo(() => selectedPost().createdAt)
844
+ *
845
+ * // With custom formatter (e.g. for i18n):
846
+ * const timeAgo = useTimeAgo(date, {
847
+ * formatter: (value, unit, isPast) => t('time.' + unit, { count: value })
848
+ * })
849
+ * ```
850
+ */
851
+ function useTimeAgo(date, options) {
852
+ const formatter = options?.formatter ?? defaultFormatter;
853
+ const resolveDate = typeof date === "function" ? date : () => date;
854
+ const result = signal(computeTimeAgo(resolveDate(), formatter));
855
+ let timer;
856
+ let disposed = false;
857
+ function tick() {
858
+ if (disposed) return;
859
+ const d = resolveDate();
860
+ result.set(computeTimeAgo(d, formatter));
861
+ const target = typeof d === "number" ? d : d.getTime();
862
+ const diffSeconds = Math.floor(Math.abs(Date.now() - target) / 1e3);
863
+ const interval = options?.interval ?? getRefreshInterval(diffSeconds);
864
+ timer = setTimeout(tick, interval);
865
+ }
866
+ timer = setTimeout(tick, 0);
867
+ onCleanup(() => {
868
+ disposed = true;
869
+ if (timer) clearTimeout(timer);
870
+ });
871
+ return result;
872
+ }
873
+
522
874
  //#endregion
523
875
  //#region src/useTimeout.ts
524
876
  /**
@@ -613,5 +965,5 @@ function useWindowResize(throttleMs = 200) {
613
965
  }
614
966
 
615
967
  //#endregion
616
- export { useBreakpoint, useClickOutside, useColorScheme, useControllableState, useDebouncedCallback, useDebouncedValue, useElementSize, useFocus, useFocusTrap, useHover, useIntersection, useInterval, useIsomorphicLayoutEffect, useKeyboard, useLatest, useMediaQuery, useMergedRef, usePrevious, useReducedMotion, useRootSize, useScrollLock, useSpacing, useThemeValue, useThrottledCallback, useTimeout, useToggle, useUpdateEffect, useWindowResize };
968
+ export { useBreakpoint, useClickOutside, useClipboard, useColorScheme, useControllableState, useDebouncedCallback, useDebouncedValue, useDialog, useElementSize, useEventListener, useFocus, useFocusTrap, useHover, useInfiniteScroll, useIntersection, useInterval, useIsomorphicLayoutEffect, useKeyboard, useLatest, useMediaQuery, useMergedRef, useOnline, usePrevious, useReducedMotion, useRootSize, useScrollLock, useSpacing, useThemeValue, useThrottledCallback, useTimeAgo, useTimeout, useToggle, useUpdateEffect, useWindowResize };
617
969
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@pyreon/hooks",
3
- "version": "0.3.0",
3
+ "version": "0.11.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/pyreon/ui-system",
7
- "directory": "packages/hooks"
7
+ "directory": "packages/fundamentals/hooks"
8
8
  },
9
9
  "description": "Signal-based reactive utilities for Pyreon",
10
10
  "license": "MIT",
11
11
  "type": "module",
12
12
  "sideEffects": false,
13
13
  "exports": {
14
+ "bun": "./src/index.ts",
14
15
  "source": "./src/index.ts",
15
16
  "import": "./lib/index.js",
16
17
  "types": "./lib/index.d.ts"
@@ -25,7 +26,7 @@
25
26
  "LICENSE"
26
27
  ],
27
28
  "engines": {
28
- "node": ">= 18"
29
+ "node": ">= 22"
29
30
  },
30
31
  "publishConfig": {
31
32
  "access": "public"
@@ -41,13 +42,13 @@
41
42
  "typecheck": "tsc --noEmit"
42
43
  },
43
44
  "peerDependencies": {
44
- "@pyreon/core": ">=0.4.0 <1.0.0",
45
- "@pyreon/reactivity": ">=0.4.0 <1.0.0",
46
- "@pyreon/styler": ">=0.3.0",
47
- "@pyreon/ui-core": ">=0.3.0"
45
+ "@pyreon/core": "^0.11.0",
46
+ "@pyreon/reactivity": "^0.11.0",
47
+ "@pyreon/styler": "^0.11.0",
48
+ "@pyreon/ui-core": "^0.11.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@vitus-labs/tools-rolldown": "^1.15.3",
51
- "@pyreon/typescript": "^0.7.4"
52
+ "@pyreon/typescript": "^0.11.0"
52
53
  }
53
54
  }