@jbpark/use-hooks 2.11.0 → 3.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 (59) hide show
  1. package/dist/hooks/index.d.mts +12 -1
  2. package/dist/hooks/index.mjs +12 -1
  3. package/dist/hooks/use-click-outside.d.mts +9 -2
  4. package/dist/hooks/use-click-outside.mjs +27 -11
  5. package/dist/hooks/use-click-outside.mjs.map +1 -1
  6. package/dist/hooks/use-debounced-value.d.mts +5 -0
  7. package/dist/hooks/use-debounced-value.mjs +20 -0
  8. package/dist/hooks/use-debounced-value.mjs.map +1 -0
  9. package/dist/hooks/use-event-listener.d.mts +21 -0
  10. package/dist/hooks/use-event-listener.mjs +37 -0
  11. package/dist/hooks/use-event-listener.mjs.map +1 -0
  12. package/dist/hooks/use-file-drop.d.mts +14 -0
  13. package/dist/hooks/use-file-drop.mjs +85 -0
  14. package/dist/hooks/use-file-drop.mjs.map +1 -0
  15. package/dist/hooks/use-image.d.mts +2 -1
  16. package/dist/hooks/use-image.mjs +4 -3
  17. package/dist/hooks/use-image.mjs.map +1 -1
  18. package/dist/hooks/use-intersection-observer.d.mts +8 -1
  19. package/dist/hooks/use-intersection-observer.mjs +37 -5
  20. package/dist/hooks/use-intersection-observer.mjs.map +1 -1
  21. package/dist/hooks/use-interval.d.mts +5 -0
  22. package/dist/hooks/use-interval.mjs +20 -0
  23. package/dist/hooks/use-interval.mjs.map +1 -0
  24. package/dist/hooks/use-key-press.d.mts +14 -0
  25. package/dist/hooks/use-key-press.mjs +63 -0
  26. package/dist/hooks/use-key-press.mjs.map +1 -0
  27. package/dist/hooks/use-merged-ref.d.mts +8 -0
  28. package/dist/hooks/use-merged-ref.mjs +25 -0
  29. package/dist/hooks/use-merged-ref.mjs.map +1 -0
  30. package/dist/hooks/use-mutation-observer.d.mts +11 -0
  31. package/dist/hooks/use-mutation-observer.mjs +33 -0
  32. package/dist/hooks/use-mutation-observer.mjs.map +1 -0
  33. package/dist/hooks/use-previous.d.mts +5 -0
  34. package/dist/hooks/use-previous.mjs +14 -0
  35. package/dist/hooks/use-previous.mjs.map +1 -0
  36. package/dist/hooks/use-resize-observer.d.mts +12 -0
  37. package/dist/hooks/use-resize-observer.mjs +29 -0
  38. package/dist/hooks/use-resize-observer.mjs.map +1 -0
  39. package/dist/hooks/use-scroll-to-elements.d.mts +7 -5
  40. package/dist/hooks/use-scroll-to-elements.mjs +52 -27
  41. package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
  42. package/dist/hooks/use-throttle.d.mts +5 -1
  43. package/dist/hooks/use-throttle.mjs +6 -32
  44. package/dist/hooks/use-throttle.mjs.map +1 -1
  45. package/dist/hooks/use-throttled-callback.d.mts +12 -0
  46. package/dist/hooks/use-throttled-callback.mjs +54 -0
  47. package/dist/hooks/use-throttled-callback.mjs.map +1 -0
  48. package/dist/hooks/use-timeout.d.mts +8 -0
  49. package/dist/hooks/use-timeout.mjs +38 -0
  50. package/dist/hooks/use-timeout.mjs.map +1 -0
  51. package/dist/hooks/use-toggle.d.mts +5 -0
  52. package/dist/hooks/use-toggle.mjs +17 -0
  53. package/dist/hooks/use-toggle.mjs.map +1 -0
  54. package/dist/index.d.mts +13 -2
  55. package/dist/index.mjs +13 -2
  56. package/package.json +1 -1
  57. package/dist/hooks/use-timeline.d.mts +0 -50
  58. package/dist/hooks/use-timeline.mjs +0 -211
  59. package/dist/hooks/use-timeline.mjs.map +0 -1
@@ -1,19 +1,30 @@
1
1
  import { useDebounce } from "./use-debounce.mjs";
2
+ import { useDebouncedValue } from "./use-debounced-value.mjs";
2
3
  import { useBodyScrollLock } from "./use-body-scroll-lock.mjs";
3
4
  import { useClickOutside } from "./use-click-outside.mjs";
4
5
  import { useControllableState } from "./use-controllable-state.mjs";
5
6
  import { useElementPosition } from "./use-element-position.mjs";
6
7
  import { useElementScroll } from "./use-element-scroll.mjs";
8
+ import { useEventListener } from "./use-event-listener.mjs";
9
+ import { useFileDrop } from "./use-file-drop.mjs";
7
10
  import { useFileToDataUrl } from "./use-file-to-data-url.mjs";
8
11
  import { useHistoryState } from "./use-history-state.mjs";
9
12
  import { useIntersectionObserver } from "./use-intersection-observer.mjs";
13
+ import { useInterval } from "./use-interval.mjs";
10
14
  import { useResponsiveSize } from "./use-responsive-size.mjs";
11
15
  import { useImage } from "./use-image.mjs";
16
+ import { useKeyPress } from "./use-key-press.mjs";
12
17
  import { useLocalStorage } from "./use-local-storage.mjs";
18
+ import { useMergedRef } from "./use-merged-ref.mjs";
13
19
  import { useMultiSelect } from "./use-multi-select.mjs";
20
+ import { useMutationObserver } from "./use-mutation-observer.mjs";
21
+ import { usePrevious } from "./use-previous.mjs";
14
22
  import { useRecursiveTimeout } from "./use-recursive-timeout.mjs";
23
+ import { useResizeObserver } from "./use-resize-observer.mjs";
15
24
  import { useScrollToElements } from "./use-scroll-to-elements.mjs";
16
25
  import { useThrottle } from "./use-throttle.mjs";
17
- import { useTimeline } from "./use-timeline.mjs";
26
+ import { useThrottledCallback } from "./use-throttled-callback.mjs";
27
+ import { useTimeout } from "./use-timeout.mjs";
28
+ import { useToggle } from "./use-toggle.mjs";
18
29
  import { useWindowScroll } from "./use-window-scroll.mjs";
19
30
  import { useViewport } from "./use-viewport.mjs";
@@ -1,20 +1,31 @@
1
1
  import useDebounce from "./use-debounce.mjs";
2
+ import useDebouncedValue from "./use-debounced-value.mjs";
2
3
  import useBodyScrollLock from "./use-body-scroll-lock.mjs";
3
4
  import useClickOutside from "./use-click-outside.mjs";
4
5
  import useControllableState from "./use-controllable-state.mjs";
5
6
  import useElementPosition from "./use-element-position.mjs";
6
7
  import useElementScroll from "./use-element-scroll.mjs";
8
+ import useEventListener from "./use-event-listener.mjs";
9
+ import useFileDrop from "./use-file-drop.mjs";
7
10
  import useFileToDataUrl from "./use-file-to-data-url.mjs";
8
11
  import useHistoryState from "./use-history-state.mjs";
9
12
  import useIntersectionObserver from "./use-intersection-observer.mjs";
13
+ import useInterval from "./use-interval.mjs";
10
14
  import useResponsiveSize from "./use-responsive-size.mjs";
11
15
  import useImage from "./use-image.mjs";
16
+ import useKeyPress from "./use-key-press.mjs";
12
17
  import useLocalStorage from "./use-local-storage.mjs";
18
+ import useMergedRef from "./use-merged-ref.mjs";
13
19
  import useMultiSelect from "./use-multi-select.mjs";
20
+ import useMutationObserver from "./use-mutation-observer.mjs";
21
+ import usePrevious from "./use-previous.mjs";
14
22
  import useRecursiveTimeout from "./use-recursive-timeout.mjs";
23
+ import useResizeObserver from "./use-resize-observer.mjs";
15
24
  import useScrollToElements from "./use-scroll-to-elements.mjs";
25
+ import useThrottledCallback from "./use-throttled-callback.mjs";
16
26
  import useThrottle from "./use-throttle.mjs";
17
- import useTimeline from "./use-timeline.mjs";
27
+ import useTimeout from "./use-timeout.mjs";
28
+ import useToggle from "./use-toggle.mjs";
18
29
  import useWindowScroll from "./use-window-scroll.mjs";
19
30
  import useViewport from "./use-viewport.mjs";
20
31
 
@@ -1,7 +1,14 @@
1
- import * as react from "react";
1
+ import { RefObject } from "react";
2
2
 
3
3
  //#region src/hooks/use-click-outside.d.ts
4
- declare const useClickOutside: <T extends HTMLElement = HTMLElement>(handler: (event: MouseEvent | TouchEvent) => void, enabled?: boolean) => react.RefObject<T | null>;
4
+ type ClickOutsideRef = RefObject<HTMLElement | null> | null | undefined;
5
+ type ClickOutsideEvent = PointerEvent | MouseEvent | TouchEvent | KeyboardEvent;
6
+ interface Options {
7
+ enabled?: boolean;
8
+ events?: ('pointerdown' | 'mousedown' | 'touchstart')[];
9
+ escape?: boolean;
10
+ }
11
+ declare const useClickOutside: (refs: ClickOutsideRef | ClickOutsideRef[], handler: (event: ClickOutsideEvent) => void, options?: Options) => void;
5
12
  //#endregion
6
13
  export { useClickOutside };
7
14
  //# sourceMappingURL=use-click-outside.d.mts.map
@@ -1,27 +1,43 @@
1
1
  import { useEffect, useRef } from "react";
2
2
 
3
3
  //#region src/hooks/use-click-outside.ts
4
- const useClickOutside = (handler, enabled = true) => {
5
- const ref = useRef(null);
4
+ const useClickOutside = (refs, handler, options = {}) => {
5
+ const { enabled = true, events = ["pointerdown"], escape = true } = options;
6
6
  const handlerRef = useRef(handler);
7
7
  useEffect(() => {
8
8
  handlerRef.current = handler;
9
9
  });
10
+ const refList = Array.isArray(refs) ? refs : [refs];
11
+ const refListRef = useRef(refList);
12
+ useEffect(() => {
13
+ refListRef.current = refList;
14
+ });
10
15
  useEffect(() => {
11
16
  if (!enabled || typeof document === "undefined") return;
12
- const listener = (event) => {
13
- const el = ref.current;
14
- if (!el || el.contains(event.target)) return;
17
+ const isInside = (target) => refListRef.current.some((ref) => ref?.current?.contains(target));
18
+ const onPointerEvent = (event) => {
19
+ const target = event.target;
20
+ if (!target || isInside(target)) return;
15
21
  handlerRef.current(event);
16
22
  };
17
- document.addEventListener("mousedown", listener);
18
- document.addEventListener("touchstart", listener);
23
+ const onKeyDown = (event) => {
24
+ if (event.key === "Escape") handlerRef.current(event);
25
+ };
26
+ events.forEach((eventName) => {
27
+ document.addEventListener(eventName, onPointerEvent);
28
+ });
29
+ if (escape) document.addEventListener("keydown", onKeyDown);
19
30
  return () => {
20
- document.removeEventListener("mousedown", listener);
21
- document.removeEventListener("touchstart", listener);
31
+ events.forEach((eventName) => {
32
+ document.removeEventListener(eventName, onPointerEvent);
33
+ });
34
+ if (escape) document.removeEventListener("keydown", onKeyDown);
22
35
  };
23
- }, [enabled]);
24
- return ref;
36
+ }, [
37
+ enabled,
38
+ escape,
39
+ events.join(",")
40
+ ]);
25
41
  };
26
42
 
27
43
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-click-outside.mjs","names":[],"sources":["../../src/hooks/use-click-outside.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\nconst useClickOutside = <T extends HTMLElement = HTMLElement>(\n handler: (event: MouseEvent | TouchEvent) => void,\n enabled = true,\n) => {\n const ref = useRef<T>(null);\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') {\n return;\n }\n\n const listener = (event: MouseEvent | TouchEvent) => {\n const el = ref.current;\n if (!el || el.contains(event.target as Node)) {\n return;\n }\n handlerRef.current(event);\n };\n\n document.addEventListener('mousedown', listener);\n document.addEventListener('touchstart', listener);\n\n return () => {\n document.removeEventListener('mousedown', listener);\n document.removeEventListener('touchstart', listener);\n };\n }, [enabled]);\n\n return ref;\n};\n\nexport default useClickOutside;\n"],"mappings":";;;AAEA,MAAM,mBACJ,SACA,UAAU,SACP;CACH,MAAM,MAAM,OAAU,KAAK;CAC3B,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;AAEF,iBAAgB;AACd,MAAI,CAAC,WAAW,OAAO,aAAa,YAClC;EAGF,MAAM,YAAY,UAAmC;GACnD,MAAM,KAAK,IAAI;AACf,OAAI,CAAC,MAAM,GAAG,SAAS,MAAM,OAAe,CAC1C;AAEF,cAAW,QAAQ,MAAM;;AAG3B,WAAS,iBAAiB,aAAa,SAAS;AAChD,WAAS,iBAAiB,cAAc,SAAS;AAEjD,eAAa;AACX,YAAS,oBAAoB,aAAa,SAAS;AACnD,YAAS,oBAAoB,cAAc,SAAS;;IAErD,CAAC,QAAQ,CAAC;AAEb,QAAO"}
1
+ {"version":3,"file":"use-click-outside.mjs","names":[],"sources":["../../src/hooks/use-click-outside.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react';\n\ntype ClickOutsideRef = RefObject<HTMLElement | null> | null | undefined;\n\ntype ClickOutsideEvent = PointerEvent | MouseEvent | TouchEvent | KeyboardEvent;\n\ninterface Options {\n enabled?: boolean;\n events?: ('pointerdown' | 'mousedown' | 'touchstart')[];\n escape?: boolean;\n}\n\n// Takes the \"inside\" ref(s) as an argument instead of creating and\n// returning one — lets a caller pass multiple exclusion targets (e.g. a\n// dropdown's trigger button *and* its portaled panel, which live in\n// different DOM subtrees) instead of only ever watching one element.\nconst useClickOutside = (\n refs: ClickOutsideRef | ClickOutsideRef[],\n handler: (event: ClickOutsideEvent) => void,\n options: Options = {},\n) => {\n const { enabled = true, events = ['pointerdown'], escape = true } = options;\n\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n const refList = Array.isArray(refs) ? refs : [refs];\n // `refs` is almost always an inline array literal at the call site, so\n // depending on it directly would tear down/rebuild the listeners on\n // every render — read the latest value from a ref instead.\n const refListRef = useRef(refList);\n\n useEffect(() => {\n refListRef.current = refList;\n });\n\n const eventsKey = events.join(',');\n\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') {\n return;\n }\n\n const isInside = (target: Node) =>\n refListRef.current.some(ref => ref?.current?.contains(target));\n\n const onPointerEvent = (event: Event) => {\n const target = event.target as Node | null;\n\n if (!target || isInside(target)) {\n return;\n }\n\n handlerRef.current(event as ClickOutsideEvent);\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n handlerRef.current(event);\n }\n };\n\n events.forEach(eventName => {\n document.addEventListener(eventName, onPointerEvent);\n });\n\n if (escape) {\n document.addEventListener('keydown', onKeyDown);\n }\n\n return () => {\n events.forEach(eventName => {\n document.removeEventListener(eventName, onPointerEvent);\n });\n\n if (escape) {\n document.removeEventListener('keydown', onKeyDown);\n }\n };\n // `events` is covered by `eventsKey` below instead of the array\n // itself, which (like `refs`) is usually a fresh literal every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, escape, eventsKey]);\n};\n\nexport default useClickOutside;\n"],"mappings":";;;AAgBA,MAAM,mBACJ,MACA,SACA,UAAmB,EAAE,KAClB;CACH,MAAM,EAAE,UAAU,MAAM,SAAS,CAAC,cAAc,EAAE,SAAS,SAAS;CAEpE,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;CAEF,MAAM,UAAU,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,KAAK;CAInD,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;AAIF,iBAAgB;AACd,MAAI,CAAC,WAAW,OAAO,aAAa,YAClC;EAGF,MAAM,YAAY,WAChB,WAAW,QAAQ,MAAK,QAAO,KAAK,SAAS,SAAS,OAAO,CAAC;EAEhE,MAAM,kBAAkB,UAAiB;GACvC,MAAM,SAAS,MAAM;AAErB,OAAI,CAAC,UAAU,SAAS,OAAO,CAC7B;AAGF,cAAW,QAAQ,MAA2B;;EAGhD,MAAM,aAAa,UAAyB;AAC1C,OAAI,MAAM,QAAQ,SAChB,YAAW,QAAQ,MAAM;;AAI7B,SAAO,SAAQ,cAAa;AAC1B,YAAS,iBAAiB,WAAW,eAAe;IACpD;AAEF,MAAI,OACF,UAAS,iBAAiB,WAAW,UAAU;AAGjD,eAAa;AACX,UAAO,SAAQ,cAAa;AAC1B,aAAS,oBAAoB,WAAW,eAAe;KACvD;AAEF,OAAI,OACF,UAAS,oBAAoB,WAAW,UAAU;;IAMrD;EAAC;EAAS;EA9CK,OAAO,KAAK,IAAI;EA8CH,CAAC"}
@@ -0,0 +1,5 @@
1
+ //#region src/hooks/use-debounced-value.d.ts
2
+ declare const useDebouncedValue: <T>(value: T, delay?: number) => T;
3
+ //#endregion
4
+ export { useDebouncedValue };
5
+ //# sourceMappingURL=use-debounced-value.d.mts.map
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ //#region src/hooks/use-debounced-value.ts
4
+ const useDebouncedValue = (value, delay = 100) => {
5
+ const [debouncedValue, setDebouncedValue] = useState(value);
6
+ const timeoutRef = useRef(null);
7
+ useEffect(() => {
8
+ timeoutRef.current = setTimeout(() => {
9
+ setDebouncedValue(value);
10
+ }, delay);
11
+ return () => {
12
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
13
+ };
14
+ }, [value, delay]);
15
+ return debouncedValue;
16
+ };
17
+
18
+ //#endregion
19
+ export { useDebouncedValue as default };
20
+ //# sourceMappingURL=use-debounced-value.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-debounced-value.mjs","names":[],"sources":["../../src/hooks/use-debounced-value.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\n// The value-shaped counterpart to useDebounce (which is callback-shaped),\n// symmetric with useThrottle's `(value, delay) => value` signature. The\n// common \"just give me the debounced value\" case otherwise needs two\n// pieces of state and an effect built on useDebounce by hand.\nconst useDebouncedValue = <T>(value: T, delay = 100): T => {\n const [debouncedValue, setDebouncedValue] = useState(value);\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n timeoutRef.current = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, [value, delay]);\n\n return debouncedValue;\n};\n\nexport default useDebouncedValue;\n"],"mappings":";;;AAMA,MAAM,qBAAwB,OAAU,QAAQ,QAAW;CACzD,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,MAAM;CAC3D,MAAM,aAAa,OAA6C,KAAK;AAErE,iBAAgB;AACd,aAAW,UAAU,iBAAiB;AACpC,qBAAkB,MAAM;KACvB,MAAM;AAET,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,CAAC,OAAO,MAAM,CAAC;AAElB,QAAO"}
@@ -0,0 +1,21 @@
1
+ import { RefObject } from "react";
2
+
3
+ //#region src/hooks/use-event-listener.d.ts
4
+ interface Options {
5
+ capture?: boolean;
6
+ passive?: boolean;
7
+ once?: boolean;
8
+ enabled?: boolean;
9
+ }
10
+ declare function useEventListener<K extends keyof WindowEventMap>(type: K, handler: (event: WindowEventMap[K]) => void, options?: Options & {
11
+ target?: Window | null;
12
+ }): void;
13
+ declare function useEventListener<K extends keyof DocumentEventMap>(type: K, handler: (event: DocumentEventMap[K]) => void, options: Options & {
14
+ target: Document | RefObject<Document | null>;
15
+ }): void;
16
+ declare function useEventListener<T extends HTMLElement, K extends keyof HTMLElementEventMap>(type: K, handler: (event: HTMLElementEventMap[K]) => void, options: Options & {
17
+ target: RefObject<T | null>;
18
+ }): void;
19
+ //#endregion
20
+ export { useEventListener };
21
+ //# sourceMappingURL=use-event-listener.d.mts.map
@@ -0,0 +1,37 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ //#region src/hooks/use-event-listener.ts
4
+ function useEventListener(type, handler, options = {}) {
5
+ const { target, capture, passive, once, enabled = true } = options;
6
+ const handlerRef = useRef(handler);
7
+ useEffect(() => {
8
+ handlerRef.current = handler;
9
+ });
10
+ useEffect(() => {
11
+ if (!enabled) return;
12
+ const resolvedTarget = target && "current" in target ? target.current : target ?? (typeof window === "undefined" ? null : window);
13
+ if (!resolvedTarget) return;
14
+ const listener = (event) => {
15
+ handlerRef.current(event);
16
+ };
17
+ resolvedTarget.addEventListener(type, listener, {
18
+ capture,
19
+ passive,
20
+ once
21
+ });
22
+ return () => {
23
+ resolvedTarget.removeEventListener(type, listener, { capture });
24
+ };
25
+ }, [
26
+ type,
27
+ target,
28
+ capture,
29
+ passive,
30
+ once,
31
+ enabled
32
+ ]);
33
+ }
34
+
35
+ //#endregion
36
+ export { useEventListener as default };
37
+ //# sourceMappingURL=use-event-listener.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-event-listener.mjs","names":[],"sources":["../../src/hooks/use-event-listener.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react';\n\ninterface Options {\n capture?: boolean;\n passive?: boolean;\n once?: boolean;\n enabled?: boolean;\n}\n\ntype EventListenerTarget = EventTarget | RefObject<EventTarget | null> | null;\n\nfunction useEventListener<K extends keyof WindowEventMap>(\n type: K,\n handler: (event: WindowEventMap[K]) => void,\n options?: Options & { target?: Window | null },\n): void;\nfunction useEventListener<K extends keyof DocumentEventMap>(\n type: K,\n handler: (event: DocumentEventMap[K]) => void,\n options: Options & { target: Document | RefObject<Document | null> },\n): void;\nfunction useEventListener<\n T extends HTMLElement,\n K extends keyof HTMLElementEventMap,\n>(\n type: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n options: Options & { target: RefObject<T | null> },\n): void;\n\n// Resolves `target` (default `window`, or a raw EventTarget, or a\n// RefObject pointing at one) and (de)registers `type` on it — the pair of\n// addEventListener/removeEventListener calls duplicated across ui-kit\n// (marquee item) and live-editor (error handlers). `handler` is read\n// through a ref so a fresh function every render doesn't tear down and\n// re-add the listener.\nfunction useEventListener(\n type: string,\n handler: (event: Event) => void,\n options: Options & { target?: EventListenerTarget } = {},\n) {\n const { target, capture, passive, once, enabled = true } = options;\n\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n const resolvedTarget =\n target && 'current' in target\n ? target.current\n : (target ?? (typeof window === 'undefined' ? null : window));\n\n if (!resolvedTarget) {\n return;\n }\n\n const listener = (event: Event) => {\n handlerRef.current(event);\n };\n\n resolvedTarget.addEventListener(type, listener, { capture, passive, once });\n\n return () => {\n resolvedTarget.removeEventListener(type, listener, { capture });\n };\n }, [type, target, capture, passive, once, enabled]);\n}\n\nexport default useEventListener;\n"],"mappings":";;;AAoCA,SAAS,iBACP,MACA,SACA,UAAsD,EAAE,EACxD;CACA,MAAM,EAAE,QAAQ,SAAS,SAAS,MAAM,UAAU,SAAS;CAE3D,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;AAEF,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,MAAM,iBACJ,UAAU,aAAa,SACnB,OAAO,UACN,WAAW,OAAO,WAAW,cAAc,OAAO;AAEzD,MAAI,CAAC,eACH;EAGF,MAAM,YAAY,UAAiB;AACjC,cAAW,QAAQ,MAAM;;AAG3B,iBAAe,iBAAiB,MAAM,UAAU;GAAE;GAAS;GAAS;GAAM,CAAC;AAE3E,eAAa;AACX,kBAAe,oBAAoB,MAAM,UAAU,EAAE,SAAS,CAAC;;IAEhE;EAAC;EAAM;EAAQ;EAAS;EAAS;EAAM;EAAQ,CAAC"}
@@ -0,0 +1,14 @@
1
+ //#region src/hooks/use-file-drop.d.ts
2
+ interface Options {
3
+ onDrop?: (files: File[]) => void;
4
+ accept?: string;
5
+ multiple?: boolean;
6
+ disabled?: boolean;
7
+ }
8
+ declare const useFileDrop: <T extends HTMLElement = HTMLElement>(options?: Options) => {
9
+ dropRef: (node: T | null) => void;
10
+ isDragging: boolean;
11
+ };
12
+ //#endregion
13
+ export { useFileDrop };
14
+ //# sourceMappingURL=use-file-drop.d.mts.map
@@ -0,0 +1,85 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ //#region src/hooks/use-file-drop.ts
4
+ const matchesAccept = (file, accept) => {
5
+ if (!accept) return true;
6
+ const patterns = accept.split(",").map((pattern) => pattern.trim()).filter(Boolean);
7
+ if (patterns.length === 0) return true;
8
+ return patterns.some((pattern) => {
9
+ if (pattern.startsWith(".")) return file.name.toLowerCase().endsWith(pattern.toLowerCase());
10
+ if (pattern.endsWith("/*")) return file.type.startsWith(pattern.slice(0, -1));
11
+ return file.type === pattern;
12
+ });
13
+ };
14
+ const useFileDrop = (options = {}) => {
15
+ const { onDrop, accept, multiple = true, disabled = false } = options;
16
+ const [isDragging, setIsDragging] = useState(false);
17
+ const dragCounterRef = useRef(0);
18
+ const onDropRef = useRef(onDrop);
19
+ const acceptRef = useRef(accept);
20
+ const multipleRef = useRef(multiple);
21
+ const disabledRef = useRef(disabled);
22
+ useEffect(() => {
23
+ onDropRef.current = onDrop;
24
+ acceptRef.current = accept;
25
+ multipleRef.current = multiple;
26
+ disabledRef.current = disabled;
27
+ });
28
+ useEffect(() => {
29
+ if (disabled) {
30
+ dragCounterRef.current = 0;
31
+ setIsDragging(false);
32
+ }
33
+ }, [disabled]);
34
+ const cleanupRef = useRef(null);
35
+ return {
36
+ dropRef: useCallback((node) => {
37
+ cleanupRef.current?.();
38
+ cleanupRef.current = null;
39
+ if (!node) return;
40
+ const onDragEnter = (event) => {
41
+ if (disabledRef.current) return;
42
+ event.preventDefault();
43
+ dragCounterRef.current += 1;
44
+ if (dragCounterRef.current === 1) setIsDragging(true);
45
+ };
46
+ const onDragLeave = (event) => {
47
+ if (disabledRef.current) return;
48
+ event.preventDefault();
49
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
50
+ if (dragCounterRef.current === 0) setIsDragging(false);
51
+ };
52
+ const onDragOver = (event) => {
53
+ if (disabledRef.current) return;
54
+ event.preventDefault();
55
+ };
56
+ const onDrop = (event) => {
57
+ if (disabledRef.current) return;
58
+ event.preventDefault();
59
+ dragCounterRef.current = 0;
60
+ setIsDragging(false);
61
+ const fileList = event.dataTransfer?.files;
62
+ if (!fileList || fileList.length === 0) return;
63
+ let files = Array.from(fileList).filter((file) => matchesAccept(file, acceptRef.current));
64
+ if (!multipleRef.current) files = files.slice(0, 1);
65
+ if (files.length > 0) onDropRef.current?.(files);
66
+ };
67
+ node.addEventListener("dragenter", onDragEnter);
68
+ node.addEventListener("dragleave", onDragLeave);
69
+ node.addEventListener("dragover", onDragOver);
70
+ node.addEventListener("drop", onDrop);
71
+ cleanupRef.current = () => {
72
+ node.removeEventListener("dragenter", onDragEnter);
73
+ node.removeEventListener("dragleave", onDragLeave);
74
+ node.removeEventListener("dragover", onDragOver);
75
+ node.removeEventListener("drop", onDrop);
76
+ dragCounterRef.current = 0;
77
+ };
78
+ }, []),
79
+ isDragging
80
+ };
81
+ };
82
+
83
+ //#endregion
84
+ export { useFileDrop as default };
85
+ //# sourceMappingURL=use-file-drop.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-file-drop.mjs","names":[],"sources":["../../src/hooks/use-file-drop.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface Options {\n onDrop?: (files: File[]) => void;\n // Comma-separated list, same format as the native <input accept>\n // attribute: extensions ('.png'), MIME types ('image/png'), or\n // wildcard subtypes ('image/*'). Unset accepts everything.\n accept?: string;\n multiple?: boolean;\n disabled?: boolean;\n}\n\nconst matchesAccept = (file: File, accept?: string): boolean => {\n if (!accept) {\n return true;\n }\n\n const patterns = accept\n .split(',')\n .map(pattern => pattern.trim())\n .filter(Boolean);\n\n if (patterns.length === 0) {\n return true;\n }\n\n return patterns.some(pattern => {\n if (pattern.startsWith('.')) {\n return file.name.toLowerCase().endsWith(pattern.toLowerCase());\n }\n\n if (pattern.endsWith('/*')) {\n return file.type.startsWith(pattern.slice(0, -1));\n }\n\n return file.type === pattern;\n });\n};\n\n// Pairs with useFileToDataUrl to cover a drag-and-drop upload area\n// end to end: this gets from \"user dropped something\" to a filtered\n// File[], useFileToDataUrl takes it from there to a data URL.\nconst useFileDrop = <T extends HTMLElement = HTMLElement>(\n options: Options = {},\n) => {\n const { onDrop, accept, multiple = true, disabled = false } = options;\n\n const [isDragging, setIsDragging] = useState(false);\n // `dragleave` also fires when the pointer moves onto a child element\n // (which re-fires `dragenter` on the way back out), not just when it\n // truly leaves the drop target — an enter/leave counter (rather than a\n // plain boolean) is what avoids `isDragging` flickering off and back on\n // as the pointer crosses child element boundaries.\n const dragCounterRef = useRef(0);\n\n const onDropRef = useRef(onDrop);\n const acceptRef = useRef(accept);\n const multipleRef = useRef(multiple);\n const disabledRef = useRef(disabled);\n\n useEffect(() => {\n onDropRef.current = onDrop;\n acceptRef.current = accept;\n multipleRef.current = multiple;\n disabledRef.current = disabled;\n });\n\n useEffect(() => {\n if (disabled) {\n dragCounterRef.current = 0;\n setIsDragging(false);\n }\n }, [disabled]);\n\n const cleanupRef = useRef<(() => void) | null>(null);\n\n const dropRef = useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n\n if (!node) {\n return;\n }\n\n const onDragEnter = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n dragCounterRef.current += 1;\n if (dragCounterRef.current === 1) {\n setIsDragging(true);\n }\n };\n\n const onDragLeave = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);\n if (dragCounterRef.current === 0) {\n setIsDragging(false);\n }\n };\n\n // `preventDefault` here is what tells the browser this is a valid\n // drop target at all — without it, `drop` never fires.\n const onDragOver = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n };\n\n const onDrop = (event: DragEvent) => {\n if (disabledRef.current) {\n return;\n }\n event.preventDefault();\n dragCounterRef.current = 0;\n setIsDragging(false);\n\n const fileList = event.dataTransfer?.files;\n\n if (!fileList || fileList.length === 0) {\n return;\n }\n\n let files = Array.from(fileList).filter(file =>\n matchesAccept(file, acceptRef.current),\n );\n\n if (!multipleRef.current) {\n files = files.slice(0, 1);\n }\n\n if (files.length > 0) {\n onDropRef.current?.(files);\n }\n };\n\n node.addEventListener('dragenter', onDragEnter);\n node.addEventListener('dragleave', onDragLeave);\n node.addEventListener('dragover', onDragOver);\n node.addEventListener('drop', onDrop);\n\n cleanupRef.current = () => {\n node.removeEventListener('dragenter', onDragEnter);\n node.removeEventListener('dragleave', onDragLeave);\n node.removeEventListener('dragover', onDragOver);\n node.removeEventListener('drop', onDrop);\n dragCounterRef.current = 0;\n };\n }, []);\n\n return { dropRef, isDragging };\n};\n\nexport default useFileDrop;\n"],"mappings":";;;AAYA,MAAM,iBAAiB,MAAY,WAA6B;AAC9D,KAAI,CAAC,OACH,QAAO;CAGT,MAAM,WAAW,OACd,MAAM,IAAI,CACV,KAAI,YAAW,QAAQ,MAAM,CAAC,CAC9B,OAAO,QAAQ;AAElB,KAAI,SAAS,WAAW,EACtB,QAAO;AAGT,QAAO,SAAS,MAAK,YAAW;AAC9B,MAAI,QAAQ,WAAW,IAAI,CACzB,QAAO,KAAK,KAAK,aAAa,CAAC,SAAS,QAAQ,aAAa,CAAC;AAGhE,MAAI,QAAQ,SAAS,KAAK,CACxB,QAAO,KAAK,KAAK,WAAW,QAAQ,MAAM,GAAG,GAAG,CAAC;AAGnD,SAAO,KAAK,SAAS;GACrB;;AAMJ,MAAM,eACJ,UAAmB,EAAE,KAClB;CACH,MAAM,EAAE,QAAQ,QAAQ,WAAW,MAAM,WAAW,UAAU;CAE9D,MAAM,CAAC,YAAY,iBAAiB,SAAS,MAAM;CAMnD,MAAM,iBAAiB,OAAO,EAAE;CAEhC,MAAM,YAAY,OAAO,OAAO;CAChC,MAAM,YAAY,OAAO,OAAO;CAChC,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,cAAc,OAAO,SAAS;AAEpC,iBAAgB;AACd,YAAU,UAAU;AACpB,YAAU,UAAU;AACpB,cAAY,UAAU;AACtB,cAAY,UAAU;GACtB;AAEF,iBAAgB;AACd,MAAI,UAAU;AACZ,kBAAe,UAAU;AACzB,iBAAc,MAAM;;IAErB,CAAC,SAAS,CAAC;CAEd,MAAM,aAAa,OAA4B,KAAK;AAkFpD,QAAO;EAAE,SAhFO,aAAa,SAAmB;AAC9C,cAAW,WAAW;AACtB,cAAW,UAAU;AAErB,OAAI,CAAC,KACH;GAGF,MAAM,eAAe,UAAqB;AACxC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;AACtB,mBAAe,WAAW;AAC1B,QAAI,eAAe,YAAY,EAC7B,eAAc,KAAK;;GAIvB,MAAM,eAAe,UAAqB;AACxC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;AACtB,mBAAe,UAAU,KAAK,IAAI,GAAG,eAAe,UAAU,EAAE;AAChE,QAAI,eAAe,YAAY,EAC7B,eAAc,MAAM;;GAMxB,MAAM,cAAc,UAAqB;AACvC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;;GAGxB,MAAM,UAAU,UAAqB;AACnC,QAAI,YAAY,QACd;AAEF,UAAM,gBAAgB;AACtB,mBAAe,UAAU;AACzB,kBAAc,MAAM;IAEpB,MAAM,WAAW,MAAM,cAAc;AAErC,QAAI,CAAC,YAAY,SAAS,WAAW,EACnC;IAGF,IAAI,QAAQ,MAAM,KAAK,SAAS,CAAC,QAAO,SACtC,cAAc,MAAM,UAAU,QAAQ,CACvC;AAED,QAAI,CAAC,YAAY,QACf,SAAQ,MAAM,MAAM,GAAG,EAAE;AAG3B,QAAI,MAAM,SAAS,EACjB,WAAU,UAAU,MAAM;;AAI9B,QAAK,iBAAiB,aAAa,YAAY;AAC/C,QAAK,iBAAiB,aAAa,YAAY;AAC/C,QAAK,iBAAiB,YAAY,WAAW;AAC7C,QAAK,iBAAiB,QAAQ,OAAO;AAErC,cAAW,gBAAgB;AACzB,SAAK,oBAAoB,aAAa,YAAY;AAClD,SAAK,oBAAoB,aAAa,YAAY;AAClD,SAAK,oBAAoB,YAAY,WAAW;AAChD,SAAK,oBAAoB,QAAQ,OAAO;AACxC,mBAAe,UAAU;;KAE1B,EAAE,CAAC;EAEY;EAAY"}
@@ -5,9 +5,10 @@ interface Options {
5
5
  }
6
6
  declare const useImage: (src: string, options?: Options) => {
7
7
  loading: boolean;
8
- error: string | Event | null;
8
+ error: Error | null;
9
9
  loaded: boolean;
10
10
  retry: () => void;
11
+ attemptCount: number;
11
12
  };
12
13
  //#endregion
13
14
  export { useImage };
@@ -26,11 +26,11 @@ const useImage = (src, options = {}) => {
26
26
  setLoaded(true);
27
27
  setError(null);
28
28
  };
29
- img.onerror = (err) => {
29
+ img.onerror = (event) => {
30
30
  if (cancelled) return;
31
31
  setLoading(false);
32
32
  setLoaded(false);
33
- setError(err);
33
+ setError(new Error(`Failed to load image: ${src}`, { cause: event }));
34
34
  if (attemptCount < retryCount) retryTimeoutId = setTimeout(() => {
35
35
  if (!cancelled) setAttemptCount((prev) => prev + 1);
36
36
  }, retryDelay);
@@ -55,7 +55,8 @@ const useImage = (src, options = {}) => {
55
55
  retry: useCallback(() => {
56
56
  setAttemptCount(0);
57
57
  setReloadToken((token) => token + 1);
58
- }, [])
58
+ }, []),
59
+ attemptCount
59
60
  };
60
61
  };
61
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"use-image.mjs","names":[],"sources":["../../src/hooks/use-image.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface Options {\n retryCount?: number;\n retryDelay?: number;\n}\n\nconst useImage = (src: string, options: Options = {}) => {\n const { retryCount = 0, retryDelay = 1000 } = options;\n\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | Event | null>(null);\n const [loaded, setLoaded] = useState(false);\n const [attemptCount, setAttemptCount] = useState(0);\n const [reloadToken, setReloadToken] = useState(0);\n\n useEffect(() => {\n if (!src) {\n setLoading(false);\n setLoaded(false);\n return;\n }\n\n let cancelled = false;\n let retryTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n setLoading(true);\n setError(null);\n\n const img = new Image();\n img.src = src;\n\n img.onload = () => {\n if (cancelled) {\n return;\n }\n setLoading(false);\n setLoaded(true);\n setError(null);\n };\n\n img.onerror = err => {\n if (cancelled) {\n return;\n }\n setLoading(false);\n setLoaded(false);\n setError(err);\n\n if (attemptCount < retryCount) {\n retryTimeoutId = setTimeout(() => {\n if (!cancelled) {\n setAttemptCount(prev => prev + 1);\n }\n }, retryDelay);\n }\n };\n\n return () => {\n cancelled = true;\n img.onload = null;\n img.onerror = null;\n if (retryTimeoutId) {\n clearTimeout(retryTimeoutId);\n }\n };\n }, [src, attemptCount, retryCount, retryDelay, reloadToken]);\n\n const retry = useCallback(() => {\n setAttemptCount(0);\n setReloadToken(token => token + 1);\n }, []);\n\n return {\n loading,\n error,\n loaded,\n retry,\n };\n};\n\nexport default useImage;\n"],"mappings":";;;AAOA,MAAM,YAAY,KAAa,UAAmB,EAAE,KAAK;CACvD,MAAM,EAAE,aAAa,GAAG,aAAa,QAAS;CAE9C,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,CAAC,OAAO,YAAY,SAAgC,KAAK;CAC/D,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,EAAE;AAEjD,iBAAgB;AACd,MAAI,CAAC,KAAK;AACR,cAAW,MAAM;AACjB,aAAU,MAAM;AAChB;;EAGF,IAAI,YAAY;EAChB,IAAI;AAEJ,aAAW,KAAK;AAChB,WAAS,KAAK;EAEd,MAAM,MAAM,IAAI,OAAO;AACvB,MAAI,MAAM;AAEV,MAAI,eAAe;AACjB,OAAI,UACF;AAEF,cAAW,MAAM;AACjB,aAAU,KAAK;AACf,YAAS,KAAK;;AAGhB,MAAI,WAAU,QAAO;AACnB,OAAI,UACF;AAEF,cAAW,MAAM;AACjB,aAAU,MAAM;AAChB,YAAS,IAAI;AAEb,OAAI,eAAe,WACjB,kBAAiB,iBAAiB;AAChC,QAAI,CAAC,UACH,kBAAgB,SAAQ,OAAO,EAAE;MAElC,WAAW;;AAIlB,eAAa;AACX,eAAY;AACZ,OAAI,SAAS;AACb,OAAI,UAAU;AACd,OAAI,eACF,cAAa,eAAe;;IAG/B;EAAC;EAAK;EAAc;EAAY;EAAY;EAAY,CAAC;AAO5D,QAAO;EACL;EACA;EACA;EACA,OATY,kBAAkB;AAC9B,mBAAgB,EAAE;AAClB,mBAAe,UAAS,QAAQ,EAAE;KACjC,EAAE,CAAC;EAOL"}
1
+ {"version":3,"file":"use-image.mjs","names":[],"sources":["../../src/hooks/use-image.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface Options {\n retryCount?: number;\n retryDelay?: number;\n}\n\nconst useImage = (src: string, options: Options = {}) => {\n const { retryCount = 0, retryDelay = 1000 } = options;\n\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const [loaded, setLoaded] = useState(false);\n const [attemptCount, setAttemptCount] = useState(0);\n const [reloadToken, setReloadToken] = useState(0);\n\n useEffect(() => {\n if (!src) {\n setLoading(false);\n setLoaded(false);\n return;\n }\n\n let cancelled = false;\n let retryTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n setLoading(true);\n setError(null);\n\n const img = new Image();\n img.src = src;\n\n img.onload = () => {\n if (cancelled) {\n return;\n }\n setLoading(false);\n setLoaded(true);\n setError(null);\n };\n\n img.onerror = event => {\n if (cancelled) {\n return;\n }\n setLoading(false);\n setLoaded(false);\n // `img.onerror`'s handler type is `OnErrorEventHandler`, so `event`\n // is typed `Event | string` even though the browser only ever\n // passes an `Event` here — that `Event` carries no useful failure\n // reason itself, so it's kept as `cause` rather than surfaced\n // directly as `error`.\n setError(new Error(`Failed to load image: ${src}`, { cause: event }));\n\n if (attemptCount < retryCount) {\n retryTimeoutId = setTimeout(() => {\n if (!cancelled) {\n setAttemptCount(prev => prev + 1);\n }\n }, retryDelay);\n }\n };\n\n return () => {\n cancelled = true;\n img.onload = null;\n img.onerror = null;\n if (retryTimeoutId) {\n clearTimeout(retryTimeoutId);\n }\n };\n }, [src, attemptCount, retryCount, retryDelay, reloadToken]);\n\n const retry = useCallback(() => {\n setAttemptCount(0);\n setReloadToken(token => token + 1);\n }, []);\n\n return {\n loading,\n error,\n loaded,\n retry,\n attemptCount,\n };\n};\n\nexport default useImage;\n"],"mappings":";;;AAOA,MAAM,YAAY,KAAa,UAAmB,EAAE,KAAK;CACvD,MAAM,EAAE,aAAa,GAAG,aAAa,QAAS;CAE9C,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CACtD,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,EAAE;AAEjD,iBAAgB;AACd,MAAI,CAAC,KAAK;AACR,cAAW,MAAM;AACjB,aAAU,MAAM;AAChB;;EAGF,IAAI,YAAY;EAChB,IAAI;AAEJ,aAAW,KAAK;AAChB,WAAS,KAAK;EAEd,MAAM,MAAM,IAAI,OAAO;AACvB,MAAI,MAAM;AAEV,MAAI,eAAe;AACjB,OAAI,UACF;AAEF,cAAW,MAAM;AACjB,aAAU,KAAK;AACf,YAAS,KAAK;;AAGhB,MAAI,WAAU,UAAS;AACrB,OAAI,UACF;AAEF,cAAW,MAAM;AACjB,aAAU,MAAM;AAMhB,YAAS,IAAI,MAAM,yBAAyB,OAAO,EAAE,OAAO,OAAO,CAAC,CAAC;AAErE,OAAI,eAAe,WACjB,kBAAiB,iBAAiB;AAChC,QAAI,CAAC,UACH,kBAAgB,SAAQ,OAAO,EAAE;MAElC,WAAW;;AAIlB,eAAa;AACX,eAAY;AACZ,OAAI,SAAS;AACb,OAAI,UAAU;AACd,OAAI,eACF,cAAa,eAAe;;IAG/B;EAAC;EAAK;EAAc;EAAY;EAAY;EAAY,CAAC;AAO5D,QAAO;EACL;EACA;EACA;EACA,OATY,kBAAkB;AAC9B,mBAAgB,EAAE;AAClB,mBAAe,UAAS,QAAQ,EAAE;KACjC,EAAE,CAAC;EAOJ;EACD"}
@@ -1,5 +1,12 @@
1
1
  //#region src/hooks/use-intersection-observer.d.ts
2
- declare const useIntersectionObserver: <T extends Element = Element>(options?: IntersectionObserverInit) => [(node: T | null) => void, IntersectionObserverEntry | null];
2
+ interface Options extends IntersectionObserverInit {
3
+ freezeOnceVisible?: boolean;
4
+ }
5
+ interface Result {
6
+ entry: IntersectionObserverEntry | null;
7
+ isIntersecting: boolean;
8
+ }
9
+ declare const useIntersectionObserver: <T extends Element = Element>(options?: Options) => [(node: T | null) => void, Result];
3
10
  //#endregion
4
11
  export { useIntersectionObserver };
5
12
  //# sourceMappingURL=use-intersection-observer.d.mts.map
@@ -1,21 +1,53 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
3
  //#region src/hooks/use-intersection-observer.ts
4
4
  const useIntersectionObserver = (options) => {
5
+ const { freezeOnceVisible = false, ...observerInit } = options ?? {};
5
6
  const [entry, setEntry] = useState(null);
6
7
  const observerRef = useRef(null);
7
- return [useCallback((node) => {
8
+ const nodeRef = useRef(null);
9
+ const frozenRef = useRef(false);
10
+ const observerInitKey = JSON.stringify(observerInit);
11
+ const observerInitRef = useRef(observerInit);
12
+ const freezeRef = useRef(freezeOnceVisible);
13
+ useEffect(() => {
14
+ observerInitRef.current = observerInit;
15
+ freezeRef.current = freezeOnceVisible;
16
+ });
17
+ const connect = useCallback(() => {
8
18
  if (observerRef.current) {
9
19
  observerRef.current.disconnect();
10
20
  observerRef.current = null;
11
21
  }
12
- if (!node || typeof IntersectionObserver === "undefined") return;
22
+ const node = nodeRef.current;
23
+ if (!node || typeof IntersectionObserver === "undefined" || frozenRef.current) return;
13
24
  const observer = new IntersectionObserver(([observedEntry]) => {
25
+ if (!observedEntry) return;
14
26
  setEntry(observedEntry);
15
- }, options);
27
+ if (observedEntry.isIntersecting && freezeRef.current) {
28
+ frozenRef.current = true;
29
+ observer.disconnect();
30
+ }
31
+ }, observerInitRef.current);
16
32
  observer.observe(node);
17
33
  observerRef.current = observer;
18
- }, []), entry];
34
+ }, []);
35
+ const ref = useCallback((node) => {
36
+ nodeRef.current = node;
37
+ connect();
38
+ }, []);
39
+ useEffect(() => {
40
+ connect();
41
+ }, [observerInitKey]);
42
+ useEffect(() => {
43
+ return () => {
44
+ observerRef.current?.disconnect();
45
+ };
46
+ }, []);
47
+ return [ref, {
48
+ entry,
49
+ isIntersecting: entry?.isIntersecting ?? false
50
+ }];
19
51
  };
20
52
 
21
53
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-intersection-observer.mjs","names":[],"sources":["../../src/hooks/use-intersection-observer.ts"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nconst useIntersectionObserver = <T extends Element = Element>(\n options?: IntersectionObserverInit,\n): [(node: T | null) => void, IntersectionObserverEntry | null] => {\n const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);\n const observerRef = useRef<IntersectionObserver | null>(null);\n\n // `options` is typically re-created as a fresh object on every render by\n // callers (e.g. spreading caller-provided overrides into defaults), so it\n // intentionally isn't a dependency here — that would tear down and\n // recreate the observer on every render. The ref callback only re-runs\n // when the observed node itself changes, capturing whatever `options`\n // was current at that time (matching the common useIntersectionObserver\n // convention, e.g. @uidotdev/usehooks).\n const ref = useCallback((node: T | null) => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n if (!node || typeof IntersectionObserver === 'undefined') {\n return;\n }\n\n const observer = new IntersectionObserver(([observedEntry]) => {\n setEntry(observedEntry);\n }, options);\n\n observer.observe(node);\n observerRef.current = observer;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [ref, entry];\n};\n\nexport default useIntersectionObserver;\n"],"mappings":";;;AAEA,MAAM,2BACJ,YACiE;CACjE,MAAM,CAAC,OAAO,YAAY,SAA2C,KAAK;CAC1E,MAAM,cAAc,OAAoC,KAAK;AA4B7D,QAAO,CAnBK,aAAa,SAAmB;AAC1C,MAAI,YAAY,SAAS;AACvB,eAAY,QAAQ,YAAY;AAChC,eAAY,UAAU;;AAGxB,MAAI,CAAC,QAAQ,OAAO,yBAAyB,YAC3C;EAGF,MAAM,WAAW,IAAI,sBAAsB,CAAC,mBAAmB;AAC7D,YAAS,cAAc;KACtB,QAAQ;AAEX,WAAS,QAAQ,KAAK;AACtB,cAAY,UAAU;IAErB,EAAE,CAAC,EAEO,MAAM"}
1
+ {"version":3,"file":"use-intersection-observer.mjs","names":[],"sources":["../../src/hooks/use-intersection-observer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface Options extends IntersectionObserverInit {\n // Disconnects for good the first time the target becomes intersecting\n // — the common \"seen once, that's enough\" case (lazy loading, entrance\n // animations, infinite-scroll triggers) that otherwise needs the\n // consumer to track their own \"already fired\" flag.\n freezeOnceVisible?: boolean;\n}\n\ninterface Result {\n entry: IntersectionObserverEntry | null;\n isIntersecting: boolean;\n}\n\nconst useIntersectionObserver = <T extends Element = Element>(\n options?: Options,\n): [(node: T | null) => void, Result] => {\n const { freezeOnceVisible = false, ...observerInit } = options ?? {};\n\n const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);\n const observerRef = useRef<IntersectionObserver | null>(null);\n const nodeRef = useRef<T | null>(null);\n const frozenRef = useRef(false);\n\n // `options` is typically a fresh object literal at the call site —\n // serialize the IntersectionObserverInit part into a stable key so\n // reconnecting only happens when it actually changes, not on every\n // render (the previous version didn't reconnect on options changes at\n // all, so threshold/rootMargin could never be updated at runtime).\n const observerInitKey = JSON.stringify(observerInit);\n const observerInitRef = useRef(observerInit);\n const freezeRef = useRef(freezeOnceVisible);\n\n useEffect(() => {\n observerInitRef.current = observerInit;\n freezeRef.current = freezeOnceVisible;\n });\n\n const connect = useCallback(() => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n const node = nodeRef.current;\n\n if (\n !node ||\n typeof IntersectionObserver === 'undefined' ||\n frozenRef.current\n ) {\n return;\n }\n\n const observer = new IntersectionObserver(([observedEntry]) => {\n if (!observedEntry) {\n return;\n }\n\n setEntry(observedEntry);\n\n if (observedEntry.isIntersecting && freezeRef.current) {\n frozenRef.current = true;\n observer.disconnect();\n }\n }, observerInitRef.current);\n\n observer.observe(node);\n observerRef.current = observer;\n }, []);\n\n const ref = useCallback((node: T | null) => {\n nodeRef.current = node;\n connect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n connect();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [observerInitKey]);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n };\n }, []);\n\n return [ref, { entry, isIntersecting: entry?.isIntersecting ?? false }];\n};\n\nexport default useIntersectionObserver;\n"],"mappings":";;;AAeA,MAAM,2BACJ,YACuC;CACvC,MAAM,EAAE,oBAAoB,OAAO,GAAG,iBAAiB,WAAW,EAAE;CAEpE,MAAM,CAAC,OAAO,YAAY,SAA2C,KAAK;CAC1E,MAAM,cAAc,OAAoC,KAAK;CAC7D,MAAM,UAAU,OAAiB,KAAK;CACtC,MAAM,YAAY,OAAO,MAAM;CAO/B,MAAM,kBAAkB,KAAK,UAAU,aAAa;CACpD,MAAM,kBAAkB,OAAO,aAAa;CAC5C,MAAM,YAAY,OAAO,kBAAkB;AAE3C,iBAAgB;AACd,kBAAgB,UAAU;AAC1B,YAAU,UAAU;GACpB;CAEF,MAAM,UAAU,kBAAkB;AAChC,MAAI,YAAY,SAAS;AACvB,eAAY,QAAQ,YAAY;AAChC,eAAY,UAAU;;EAGxB,MAAM,OAAO,QAAQ;AAErB,MACE,CAAC,QACD,OAAO,yBAAyB,eAChC,UAAU,QAEV;EAGF,MAAM,WAAW,IAAI,sBAAsB,CAAC,mBAAmB;AAC7D,OAAI,CAAC,cACH;AAGF,YAAS,cAAc;AAEvB,OAAI,cAAc,kBAAkB,UAAU,SAAS;AACrD,cAAU,UAAU;AACpB,aAAS,YAAY;;KAEtB,gBAAgB,QAAQ;AAE3B,WAAS,QAAQ,KAAK;AACtB,cAAY,UAAU;IACrB,EAAE,CAAC;CAEN,MAAM,MAAM,aAAa,SAAmB;AAC1C,UAAQ,UAAU;AAClB,WAAS;IAER,EAAE,CAAC;AAEN,iBAAgB;AACd,WAAS;IAER,CAAC,gBAAgB,CAAC;AAErB,iBAAgB;AACd,eAAa;AACX,eAAY,SAAS,YAAY;;IAElC,EAAE,CAAC;AAEN,QAAO,CAAC,KAAK;EAAE;EAAO,gBAAgB,OAAO,kBAAkB;EAAO,CAAC"}
@@ -0,0 +1,5 @@
1
+ //#region src/hooks/use-interval.d.ts
2
+ declare const useInterval: (callback: () => void, delay: number | null) => void;
3
+ //#endregion
4
+ export { useInterval };
5
+ //# sourceMappingURL=use-interval.d.mts.map
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ //#region src/hooks/use-interval.ts
4
+ const useInterval = (callback, delay) => {
5
+ const callbackRef = useRef(callback);
6
+ useEffect(() => {
7
+ callbackRef.current = callback;
8
+ });
9
+ useEffect(() => {
10
+ if (delay === null) return;
11
+ const id = setInterval(() => {
12
+ callbackRef.current();
13
+ }, delay);
14
+ return () => clearInterval(id);
15
+ }, [delay]);
16
+ };
17
+
18
+ //#endregion
19
+ export { useInterval as default };
20
+ //# sourceMappingURL=use-interval.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-interval.mjs","names":[],"sources":["../../src/hooks/use-interval.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\n// Dan Abramov's useInterval pattern: the callback lives in a ref so a\n// fresh function every render doesn't reset the interval — only `delay`\n// actually changing does that. `delay === null` pauses it (same rule as\n// useTimeout/useRecursiveTimeout); toggling back to a number resumes on\n// a fresh interval rather than trying to pick up mid-tick.\nconst useInterval = (callback: () => void, delay: number | null) => {\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => {\n if (delay === null) {\n return;\n }\n\n const id = setInterval(() => {\n callbackRef.current();\n }, delay);\n\n return () => clearInterval(id);\n }, [delay]);\n};\n\nexport default useInterval;\n"],"mappings":";;;AAOA,MAAM,eAAe,UAAsB,UAAyB;CAClE,MAAM,cAAc,OAAO,SAAS;AAEpC,iBAAgB;AACd,cAAY,UAAU;GACtB;AAEF,iBAAgB;AACd,MAAI,UAAU,KACZ;EAGF,MAAM,KAAK,kBAAkB;AAC3B,eAAY,SAAS;KACpB,MAAM;AAET,eAAa,cAAc,GAAG;IAC7B,CAAC,MAAM,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { RefObject } from "react";
2
+
3
+ //#region src/hooks/use-key-press.d.ts
4
+ type KeyPressTarget = RefObject<HTMLElement | null> | Window | Document | HTMLElement | null | undefined;
5
+ interface Options {
6
+ target?: KeyPressTarget;
7
+ enabled?: boolean;
8
+ preventDefault?: boolean;
9
+ ignore?: string;
10
+ }
11
+ declare const useKeyPress: (combo: string | string[], handler: (event: KeyboardEvent) => void, options?: Options) => void;
12
+ //#endregion
13
+ export { useKeyPress };
14
+ //# sourceMappingURL=use-key-press.d.mts.map