@jbpark/use-hooks 2.10.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 (74) 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-controllable-state.d.mts +4 -5
  7. package/dist/hooks/use-controllable-state.mjs +10 -4
  8. package/dist/hooks/use-controllable-state.mjs.map +1 -1
  9. package/dist/hooks/use-debounced-value.d.mts +5 -0
  10. package/dist/hooks/use-debounced-value.mjs +20 -0
  11. package/dist/hooks/use-debounced-value.mjs.map +1 -0
  12. package/dist/hooks/use-element-position.mjs +37 -8
  13. package/dist/hooks/use-element-position.mjs.map +1 -1
  14. package/dist/hooks/use-element-scroll.d.mts +6 -1
  15. package/dist/hooks/use-element-scroll.mjs +30 -8
  16. package/dist/hooks/use-element-scroll.mjs.map +1 -1
  17. package/dist/hooks/use-event-listener.d.mts +21 -0
  18. package/dist/hooks/use-event-listener.mjs +37 -0
  19. package/dist/hooks/use-event-listener.mjs.map +1 -0
  20. package/dist/hooks/use-file-drop.d.mts +14 -0
  21. package/dist/hooks/use-file-drop.mjs +85 -0
  22. package/dist/hooks/use-file-drop.mjs.map +1 -0
  23. package/dist/hooks/use-image.d.mts +2 -1
  24. package/dist/hooks/use-image.mjs +4 -3
  25. package/dist/hooks/use-image.mjs.map +1 -1
  26. package/dist/hooks/use-intersection-observer.d.mts +8 -1
  27. package/dist/hooks/use-intersection-observer.mjs +37 -5
  28. package/dist/hooks/use-intersection-observer.mjs.map +1 -1
  29. package/dist/hooks/use-interval.d.mts +5 -0
  30. package/dist/hooks/use-interval.mjs +20 -0
  31. package/dist/hooks/use-interval.mjs.map +1 -0
  32. package/dist/hooks/use-key-press.d.mts +14 -0
  33. package/dist/hooks/use-key-press.mjs +63 -0
  34. package/dist/hooks/use-key-press.mjs.map +1 -0
  35. package/dist/hooks/use-merged-ref.d.mts +8 -0
  36. package/dist/hooks/use-merged-ref.mjs +25 -0
  37. package/dist/hooks/use-merged-ref.mjs.map +1 -0
  38. package/dist/hooks/use-multi-select.mjs +14 -5
  39. package/dist/hooks/use-multi-select.mjs.map +1 -1
  40. package/dist/hooks/use-mutation-observer.d.mts +11 -0
  41. package/dist/hooks/use-mutation-observer.mjs +33 -0
  42. package/dist/hooks/use-mutation-observer.mjs.map +1 -0
  43. package/dist/hooks/use-previous.d.mts +5 -0
  44. package/dist/hooks/use-previous.mjs +14 -0
  45. package/dist/hooks/use-previous.mjs.map +1 -0
  46. package/dist/hooks/use-resize-observer.d.mts +12 -0
  47. package/dist/hooks/use-resize-observer.mjs +29 -0
  48. package/dist/hooks/use-resize-observer.mjs.map +1 -0
  49. package/dist/hooks/use-responsive-size.d.mts +1 -0
  50. package/dist/hooks/use-responsive-size.mjs +70 -54
  51. package/dist/hooks/use-responsive-size.mjs.map +1 -1
  52. package/dist/hooks/use-scroll-to-elements.d.mts +7 -5
  53. package/dist/hooks/use-scroll-to-elements.mjs +52 -27
  54. package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
  55. package/dist/hooks/use-throttle.d.mts +5 -1
  56. package/dist/hooks/use-throttle.mjs +6 -32
  57. package/dist/hooks/use-throttle.mjs.map +1 -1
  58. package/dist/hooks/use-throttled-callback.d.mts +12 -0
  59. package/dist/hooks/use-throttled-callback.mjs +54 -0
  60. package/dist/hooks/use-throttled-callback.mjs.map +1 -0
  61. package/dist/hooks/use-timeout.d.mts +8 -0
  62. package/dist/hooks/use-timeout.mjs +38 -0
  63. package/dist/hooks/use-timeout.mjs.map +1 -0
  64. package/dist/hooks/use-toggle.d.mts +5 -0
  65. package/dist/hooks/use-toggle.mjs +17 -0
  66. package/dist/hooks/use-toggle.mjs.map +1 -0
  67. package/dist/hooks/use-window-scroll.mjs +60 -38
  68. package/dist/hooks/use-window-scroll.mjs.map +1 -1
  69. package/dist/index.d.mts +13 -2
  70. package/dist/index.mjs +13 -2
  71. package/package.json +1 -1
  72. package/dist/hooks/use-timeline.d.mts +0 -50
  73. package/dist/hooks/use-timeline.mjs +0 -176
  74. 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"}
@@ -4,11 +4,10 @@ interface UseControllableStateProps<T> {
4
4
  defaultValue?: T;
5
5
  onChange?: (value: T) => void;
6
6
  }
7
- declare const useControllableState: <T>({
8
- value,
9
- defaultValue,
10
- onChange
11
- }: UseControllableStateProps<T>) => [T, (next: T) => void];
7
+ declare function useControllableState<T>(props: UseControllableStateProps<T> & {
8
+ defaultValue: T;
9
+ }): [T, (next: T) => void];
10
+ declare function useControllableState<T>(props: UseControllableStateProps<T>): [T | undefined, (next: T) => void];
12
11
  //#endregion
13
12
  export { useControllableState };
14
13
  //# sourceMappingURL=use-controllable-state.d.mts.map
@@ -1,14 +1,20 @@
1
- import { useCallback, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
3
  //#region src/hooks/use-controllable-state.ts
4
- const useControllableState = ({ value, defaultValue, onChange }) => {
4
+ function useControllableState({ value, defaultValue, onChange }) {
5
5
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
6
6
  const isControlled = value !== void 0;
7
- return [isControlled ? value : uncontrolledValue, useCallback((next) => {
7
+ const currentValue = isControlled ? value : uncontrolledValue;
8
+ const wasControlledRef = useRef(isControlled);
9
+ useEffect(() => {
10
+ if (!(globalThis.process?.env?.NODE_ENV === "production") && wasControlledRef.current !== isControlled) console.warn(`useControllableState is changing from ${wasControlledRef.current ? "controlled" : "uncontrolled"} to ${isControlled ? "controlled" : "uncontrolled"}. A component should not switch between controlled and uncontrolled — decide on one and stick with it (e.g. by always passing a non-undefined \`value\`, or never passing one).`);
11
+ wasControlledRef.current = isControlled;
12
+ }, [isControlled]);
13
+ return [currentValue, useCallback((next) => {
8
14
  if (!isControlled) setUncontrolledValue(next);
9
15
  onChange?.(next);
10
16
  }, [isControlled, onChange])];
11
- };
17
+ }
12
18
 
13
19
  //#endregion
14
20
  export { useControllableState as default };
@@ -1 +1 @@
1
- {"version":3,"file":"use-controllable-state.mjs","names":[],"sources":["../../src/hooks/use-controllable-state.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\n\ninterface UseControllableStateProps<T> {\n value?: T;\n defaultValue?: T;\n onChange?: (value: T) => void;\n}\n\n// Backs a controlled/uncontrolled prop pair (`value`/`defaultValue`/\n// `onChange`) with one hook: falls back to internal state whenever `value`\n// is `undefined`, and always calls `onChange` regardless of which mode is\n// active, so callers don't have to branch on it themselves.\nconst useControllableState = <T>({\n value,\n defaultValue,\n onChange,\n}: UseControllableStateProps<T>): [T, (next: T) => void] => {\n const [uncontrolledValue, setUncontrolledValue] = useState<T | undefined>(\n defaultValue,\n );\n\n const isControlled = value !== undefined;\n const currentValue = (isControlled ? value : uncontrolledValue) as T;\n\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) {\n setUncontrolledValue(next);\n }\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n return [currentValue, setValue];\n};\n\nexport default useControllableState;\n"],"mappings":";;;AAYA,MAAM,wBAA2B,EAC/B,OACA,cACA,eAC0D;CAC1D,MAAM,CAAC,mBAAmB,wBAAwB,SAChD,aACD;CAED,MAAM,eAAe,UAAU;AAa/B,QAAO,CAZe,eAAe,QAAQ,mBAE5B,aACd,SAAY;AACX,MAAI,CAAC,aACH,sBAAqB,KAAK;AAE5B,aAAW,KAAK;IAElB,CAAC,cAAc,SAAS,CACzB,CAE8B"}
1
+ {"version":3,"file":"use-controllable-state.mjs","names":[],"sources":["../../src/hooks/use-controllable-state.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface UseControllableStateProps<T> {\n value?: T;\n defaultValue?: T;\n onChange?: (value: T) => void;\n}\n\n// Backs a controlled/uncontrolled prop pair (`value`/`defaultValue`/\n// `onChange`) with one hook: falls back to internal state whenever `value`\n// is `undefined`, and always calls `onChange` regardless of which mode is\n// active, so callers don't have to branch on it themselves.\n//\n// `value === undefined` is what marks a render as uncontrolled, so it can't\n// also be used as a real, controlled value of `undefined` — that's an\n// inherent limitation of this \"sentinel\" style of controlled-state\n// detection (shared by most hooks of this shape, including Radix's own),\n// not something fixable without a different API (e.g. an explicit\n// `controlled` boolean prop instead of inferring it from `value`).\nfunction useControllableState<T>(\n props: UseControllableStateProps<T> & { defaultValue: T },\n): [T, (next: T) => void];\nfunction useControllableState<T>(\n props: UseControllableStateProps<T>,\n): [T | undefined, (next: T) => void];\nfunction useControllableState<T>({\n value,\n defaultValue,\n onChange,\n}: UseControllableStateProps<T>) {\n const [uncontrolledValue, setUncontrolledValue] = useState<T | undefined>(\n defaultValue,\n );\n\n const isControlled = value !== undefined;\n const currentValue = isControlled ? value : uncontrolledValue;\n\n const wasControlledRef = useRef(isControlled);\n\n useEffect(() => {\n // Read `process.env.NODE_ENV` through `globalThis` rather than the\n // bare `process` global — this hook ships as plain ESM and can end up\n // somewhere with no bundler define for it (or no `process` at all, and\n // no @types/node in scope to even name it), where referencing it\n // directly would fail instead of just missing a warning.\n const nodeEnv = (\n globalThis as { process?: { env?: { NODE_ENV?: string } } }\n ).process?.env?.NODE_ENV;\n const isProduction = nodeEnv === 'production';\n\n if (!isProduction && wasControlledRef.current !== isControlled) {\n console.warn(\n `useControllableState is changing from ${\n wasControlledRef.current ? 'controlled' : 'uncontrolled'\n } to ${\n isControlled ? 'controlled' : 'uncontrolled'\n }. A component should not switch between controlled and uncontrolled — decide on one and stick with it (e.g. by always passing a non-undefined \\`value\\`, or never passing one).`,\n );\n }\n wasControlledRef.current = isControlled;\n }, [isControlled]);\n\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) {\n setUncontrolledValue(next);\n }\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n return [currentValue, setValue];\n}\n\nexport default useControllableState;\n"],"mappings":";;;AAyBA,SAAS,qBAAwB,EAC/B,OACA,cACA,YAC+B;CAC/B,MAAM,CAAC,mBAAmB,wBAAwB,SAChD,aACD;CAED,MAAM,eAAe,UAAU;CAC/B,MAAM,eAAe,eAAe,QAAQ;CAE5C,MAAM,mBAAmB,OAAO,aAAa;AAE7C,iBAAgB;AAWd,MAAI,EAJF,WACA,SAAS,KAAK,aACiB,iBAEZ,iBAAiB,YAAY,aAChD,SAAQ,KACN,yCACE,iBAAiB,UAAU,eAAe,eAC3C,MACC,eAAe,eAAe,eAC/B,iLACF;AAEH,mBAAiB,UAAU;IAC1B,CAAC,aAAa,CAAC;AAYlB,QAAO,CAAC,cAVS,aACd,SAAY;AACX,MAAI,CAAC,aACH,sBAAqB,KAAK;AAE5B,aAAW,KAAK;IAElB,CAAC,cAAc,SAAS,CACzB,CAE8B"}
@@ -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"}
@@ -1,27 +1,56 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useRef, useState } from "react";
2
2
 
3
3
  //#region src/hooks/use-element-position.ts
4
+ const rectsEqual = (a, b) => {
5
+ if (a === b) return true;
6
+ if (!a || !b) return false;
7
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height && a.top === b.top && a.left === b.left;
8
+ };
4
9
  const useElementPosition = (elementRef) => {
5
10
  const [rect, setRect] = useState(null);
11
+ const rectRef = useRef(null);
6
12
  useEffect(() => {
7
13
  const getElement = (ref) => {
8
14
  if (typeof ref === "string") return document.querySelector(ref);
9
15
  return ref.current;
10
16
  };
11
- const updateRect = () => {
12
- const element = getElement(elementRef);
13
- if (element) setRect(element.getBoundingClientRect());
14
- else setRect(null);
17
+ let rafId;
18
+ let currentElement = null;
19
+ const commitRect = (next) => {
20
+ if (rectsEqual(rectRef.current, next)) return;
21
+ rectRef.current = next;
22
+ setRect(next);
15
23
  };
24
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => updateRect()) : void 0;
25
+ function updateRect() {
26
+ const element = getElement(elementRef);
27
+ if (element !== currentElement) {
28
+ if (currentElement) resizeObserver?.unobserve(currentElement);
29
+ if (element) resizeObserver?.observe(element);
30
+ currentElement = element;
31
+ }
32
+ commitRect(element ? element.getBoundingClientRect() : null);
33
+ }
16
34
  const onUpdate = () => {
17
- requestAnimationFrame(updateRect);
35
+ rafId = requestAnimationFrame(updateRect);
18
36
  };
19
37
  updateRect();
20
- window.addEventListener("scroll", onUpdate, { passive: true });
38
+ window.addEventListener("scroll", onUpdate, {
39
+ passive: true,
40
+ capture: true
41
+ });
21
42
  window.addEventListener("resize", onUpdate, { passive: true });
43
+ const mutationObserver = typeof elementRef === "string" && typeof MutationObserver !== "undefined" ? new MutationObserver(onUpdate) : void 0;
44
+ mutationObserver?.observe(document.body, {
45
+ childList: true,
46
+ subtree: true
47
+ });
22
48
  return () => {
23
- window.removeEventListener("scroll", onUpdate);
49
+ if (rafId !== void 0) cancelAnimationFrame(rafId);
50
+ window.removeEventListener("scroll", onUpdate, { capture: true });
24
51
  window.removeEventListener("resize", onUpdate);
52
+ resizeObserver?.disconnect();
53
+ mutationObserver?.disconnect();
25
54
  };
26
55
  }, [elementRef]);
27
56
  return rect;
@@ -1 +1 @@
1
- {"version":3,"file":"use-element-position.mjs","names":[],"sources":["../../src/hooks/use-element-position.ts"],"sourcesContent":["import { type RefObject, useEffect, useState } from 'react';\n\ntype ElementReference<T> = string | RefObject<T>;\n\nconst useElementPosition = <T>(elementRef: ElementReference<T>) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n const getElement = (ref: ElementReference<T>): T | null => {\n if (typeof ref === 'string') {\n return document.querySelector(ref) as T;\n }\n return ref.current;\n };\n\n const updateRect = () => {\n const element = getElement(elementRef) as HTMLElement | null;\n if (element) {\n setRect(element.getBoundingClientRect());\n } else {\n setRect(null);\n }\n };\n\n const onUpdate = () => {\n requestAnimationFrame(updateRect);\n };\n\n updateRect();\n\n window.addEventListener('scroll', onUpdate, { passive: true });\n window.addEventListener('resize', onUpdate, { passive: true });\n\n return () => {\n window.removeEventListener('scroll', onUpdate);\n window.removeEventListener('resize', onUpdate);\n };\n }, [elementRef]);\n\n return rect;\n};\n\nexport default useElementPosition;\n"],"mappings":";;;AAIA,MAAM,sBAAyB,eAAoC;CACjE,MAAM,CAAC,MAAM,WAAW,SAAyB,KAAK;AAEtD,iBAAgB;EACd,MAAM,cAAc,QAAuC;AACzD,OAAI,OAAO,QAAQ,SACjB,QAAO,SAAS,cAAc,IAAI;AAEpC,UAAO,IAAI;;EAGb,MAAM,mBAAmB;GACvB,MAAM,UAAU,WAAW,WAAW;AACtC,OAAI,QACF,SAAQ,QAAQ,uBAAuB,CAAC;OAExC,SAAQ,KAAK;;EAIjB,MAAM,iBAAiB;AACrB,yBAAsB,WAAW;;AAGnC,cAAY;AAEZ,SAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AAC9D,SAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AAE9D,eAAa;AACX,UAAO,oBAAoB,UAAU,SAAS;AAC9C,UAAO,oBAAoB,UAAU,SAAS;;IAE/C,CAAC,WAAW,CAAC;AAEhB,QAAO"}
1
+ {"version":3,"file":"use-element-position.mjs","names":[],"sources":["../../src/hooks/use-element-position.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef, useState } from 'react';\n\ntype ElementReference<T> = string | RefObject<T>;\n\nconst rectsEqual = (a: DOMRect | null, b: DOMRect | null) => {\n if (a === b) {\n return true;\n }\n if (!a || !b) {\n return false;\n }\n return (\n a.x === b.x &&\n a.y === b.y &&\n a.width === b.width &&\n a.height === b.height &&\n a.top === b.top &&\n a.left === b.left\n );\n};\n\nconst useElementPosition = <T>(elementRef: ElementReference<T>) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n const rectRef = useRef<DOMRect | null>(null);\n\n useEffect(() => {\n const getElement = (ref: ElementReference<T>): T | null => {\n if (typeof ref === 'string') {\n return document.querySelector(ref) as T | null;\n }\n return ref.current;\n };\n\n let rafId: number | undefined;\n let currentElement: HTMLElement | null = null;\n\n const commitRect = (next: DOMRect | null) => {\n if (rectsEqual(rectRef.current, next)) {\n return;\n }\n rectRef.current = next;\n setRect(next);\n };\n\n // ResizeObserver only fires for the observed element's own size/layout\n // changes, so it's kept pointed at whichever element is actually\n // current — which can change under a string selector, or if the DOM\n // node behind a ref gets swapped out.\n const resizeObserver =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => updateRect())\n : undefined;\n\n function updateRect() {\n const element = getElement(elementRef) as HTMLElement | null;\n\n if (element !== currentElement) {\n if (currentElement) {\n resizeObserver?.unobserve(currentElement);\n }\n if (element) {\n resizeObserver?.observe(element);\n }\n currentElement = element;\n }\n\n commitRect(element ? element.getBoundingClientRect() : null);\n }\n\n const onUpdate = () => {\n rafId = requestAnimationFrame(updateRect);\n };\n\n updateRect();\n\n // `capture: true` also catches scroll events from scrollable ancestor\n // containers, which don't bubble and so wouldn't otherwise reach a\n // listener on `window`.\n window.addEventListener('scroll', onUpdate, {\n passive: true,\n capture: true,\n });\n window.addEventListener('resize', onUpdate, { passive: true });\n\n // A string selector's target may not exist yet when this effect first\n // runs — watch the DOM for it instead of leaving `rect` permanently\n // null once it does mount.\n const mutationObserver =\n typeof elementRef === 'string' && typeof MutationObserver !== 'undefined'\n ? new MutationObserver(onUpdate)\n : undefined;\n mutationObserver?.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n return () => {\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\n window.removeEventListener('scroll', onUpdate, { capture: true });\n window.removeEventListener('resize', onUpdate);\n resizeObserver?.disconnect();\n mutationObserver?.disconnect();\n };\n }, [elementRef]);\n\n return rect;\n};\n\nexport default useElementPosition;\n"],"mappings":";;;AAIA,MAAM,cAAc,GAAmB,MAAsB;AAC3D,KAAI,MAAM,EACR,QAAO;AAET,KAAI,CAAC,KAAK,CAAC,EACT,QAAO;AAET,QACE,EAAE,MAAM,EAAE,KACV,EAAE,MAAM,EAAE,KACV,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,QAAQ,EAAE,OACZ,EAAE,SAAS,EAAE;;AAIjB,MAAM,sBAAyB,eAAoC;CACjE,MAAM,CAAC,MAAM,WAAW,SAAyB,KAAK;CACtD,MAAM,UAAU,OAAuB,KAAK;AAE5C,iBAAgB;EACd,MAAM,cAAc,QAAuC;AACzD,OAAI,OAAO,QAAQ,SACjB,QAAO,SAAS,cAAc,IAAI;AAEpC,UAAO,IAAI;;EAGb,IAAI;EACJ,IAAI,iBAAqC;EAEzC,MAAM,cAAc,SAAyB;AAC3C,OAAI,WAAW,QAAQ,SAAS,KAAK,CACnC;AAEF,WAAQ,UAAU;AAClB,WAAQ,KAAK;;EAOf,MAAM,iBACJ,OAAO,mBAAmB,cACtB,IAAI,qBAAqB,YAAY,CAAC,GACtC;EAEN,SAAS,aAAa;GACpB,MAAM,UAAU,WAAW,WAAW;AAEtC,OAAI,YAAY,gBAAgB;AAC9B,QAAI,eACF,iBAAgB,UAAU,eAAe;AAE3C,QAAI,QACF,iBAAgB,QAAQ,QAAQ;AAElC,qBAAiB;;AAGnB,cAAW,UAAU,QAAQ,uBAAuB,GAAG,KAAK;;EAG9D,MAAM,iBAAiB;AACrB,WAAQ,sBAAsB,WAAW;;AAG3C,cAAY;AAKZ,SAAO,iBAAiB,UAAU,UAAU;GAC1C,SAAS;GACT,SAAS;GACV,CAAC;AACF,SAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;EAK9D,MAAM,mBACJ,OAAO,eAAe,YAAY,OAAO,qBAAqB,cAC1D,IAAI,iBAAiB,SAAS,GAC9B;AACN,oBAAkB,QAAQ,SAAS,MAAM;GACvC,WAAW;GACX,SAAS;GACV,CAAC;AAEF,eAAa;AACX,OAAI,UAAU,OACZ,sBAAqB,MAAM;AAE7B,UAAO,oBAAoB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AACjE,UAAO,oBAAoB,UAAU,SAAS;AAC9C,mBAAgB,YAAY;AAC5B,qBAAkB,YAAY;;IAE/B,CAAC,WAAW,CAAC;AAEhB,QAAO"}
@@ -1,5 +1,10 @@
1
1
  //#region src/hooks/use-element-scroll.d.ts
2
- declare const useElementScroll: () => {
2
+ interface Options {
3
+ threshold?: number;
4
+ }
5
+ declare const useElementScroll: ({
6
+ threshold
7
+ }?: Options) => {
3
8
  element: HTMLElement | null;
4
9
  setRef: (el: HTMLElement | null) => void;
5
10
  scrollY: number;
@@ -1,7 +1,8 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
3
  //#region src/hooks/use-element-scroll.ts
4
- const useElementScroll = () => {
4
+ const scrollPositionEqual = (a, b) => a.scrollY === b.scrollY && a.scrollPercentage === b.scrollPercentage && a.isAtTop === b.isAtTop && a.isAtBottom === b.isAtBottom && a.scrollableHeight === b.scrollableHeight && a.clientHeight === b.clientHeight && a.scrollHeight === b.scrollHeight;
5
+ const useElementScroll = ({ threshold = 1 } = {}) => {
5
6
  const [element, setElement] = useState(null);
6
7
  const [scrollPosition, setScrollPosition] = useState({
7
8
  scrollY: 0,
@@ -12,16 +13,22 @@ const useElementScroll = () => {
12
13
  clientHeight: 0,
13
14
  scrollHeight: 0
14
15
  });
16
+ const scrollPositionRef = useRef(scrollPosition);
15
17
  const setRef = useCallback((el) => {
16
18
  setElement(el);
17
19
  }, []);
18
20
  useEffect(() => {
19
21
  if (!element) return;
22
+ const commit = (next) => {
23
+ if (scrollPositionEqual(scrollPositionRef.current, next)) return;
24
+ scrollPositionRef.current = next;
25
+ setScrollPosition(next);
26
+ };
20
27
  const updateScrollPosition = () => {
21
28
  const { scrollTop, scrollHeight, clientHeight } = element;
22
29
  const scrollableHeight = scrollHeight - clientHeight;
23
30
  if (scrollableHeight <= 0) {
24
- setScrollPosition({
31
+ commit({
25
32
  scrollY: 0,
26
33
  scrollPercentage: 0,
27
34
  isAtTop: true,
@@ -32,11 +39,11 @@ const useElementScroll = () => {
32
39
  });
33
40
  return;
34
41
  }
35
- setScrollPosition({
42
+ commit({
36
43
  scrollY: scrollTop,
37
44
  scrollPercentage: Math.min(100, Math.max(0, scrollTop / scrollableHeight * 100)),
38
45
  isAtTop: scrollTop <= 0,
39
- isAtBottom: scrollTop >= scrollableHeight - 1,
46
+ isAtBottom: scrollTop >= scrollableHeight - threshold,
40
47
  scrollableHeight,
41
48
  clientHeight,
42
49
  scrollHeight
@@ -50,12 +57,27 @@ const useElementScroll = () => {
50
57
  const resizeObserver = new ResizeObserver(() => {
51
58
  updateScrollPosition();
52
59
  });
53
- resizeObserver.observe(element);
60
+ const observeChildren = () => {
61
+ resizeObserver.observe(element);
62
+ Array.from(element.children).forEach((child) => {
63
+ resizeObserver.observe(child);
64
+ });
65
+ };
66
+ observeChildren();
67
+ const mutationObserver = new MutationObserver(() => {
68
+ observeChildren();
69
+ updateScrollPosition();
70
+ });
71
+ mutationObserver.observe(element, {
72
+ childList: true,
73
+ subtree: true
74
+ });
54
75
  return () => {
55
76
  element.removeEventListener("scroll", onScroll);
56
- resizeObserver.unobserve(element);
77
+ resizeObserver.disconnect();
78
+ mutationObserver.disconnect();
57
79
  };
58
- }, [element]);
80
+ }, [element, threshold]);
59
81
  return {
60
82
  ...scrollPosition,
61
83
  element,
@@ -1 +1 @@
1
- {"version":3,"file":"use-element-scroll.mjs","names":[],"sources":["../../src/hooks/use-element-scroll.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface ScrollPosition {\n scrollY: number;\n scrollPercentage: number;\n isAtTop: boolean;\n isAtBottom: boolean;\n scrollableHeight: number;\n clientHeight: number;\n scrollHeight: number;\n}\n\nconst useElementScroll = () => {\n const [element, setElement] = useState<HTMLElement | null>(null);\n const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: false,\n scrollableHeight: 0,\n clientHeight: 0,\n scrollHeight: 0,\n });\n\n const setRef = useCallback((el: HTMLElement | null) => {\n setElement(el);\n }, []);\n\n useEffect(() => {\n if (!element) {\n return;\n }\n\n const updateScrollPosition = () => {\n const { scrollTop, scrollHeight, clientHeight } = element;\n const scrollableHeight = scrollHeight - clientHeight;\n\n if (scrollableHeight <= 0) {\n setScrollPosition({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: true,\n scrollableHeight: 0,\n clientHeight,\n scrollHeight,\n });\n return;\n }\n\n const percentage = Math.min(\n 100,\n Math.max(0, (scrollTop / scrollableHeight) * 100),\n );\n\n setScrollPosition({\n scrollY: scrollTop,\n scrollPercentage: percentage,\n isAtTop: scrollTop <= 0,\n isAtBottom: scrollTop >= scrollableHeight - 1,\n scrollableHeight,\n clientHeight,\n scrollHeight,\n });\n };\n\n updateScrollPosition();\n\n const onScroll = () => {\n updateScrollPosition();\n };\n\n element.addEventListener('scroll', onScroll, { passive: true });\n\n const resizeObserver = new ResizeObserver(() => {\n updateScrollPosition();\n });\n\n resizeObserver.observe(element);\n\n return () => {\n element.removeEventListener('scroll', onScroll);\n resizeObserver.unobserve(element);\n };\n }, [element]);\n\n return { ...scrollPosition, element, setRef };\n};\n\nexport default useElementScroll;\n"],"mappings":";;;AAYA,MAAM,yBAAyB;CAC7B,MAAM,CAAC,SAAS,cAAc,SAA6B,KAAK;CAChE,MAAM,CAAC,gBAAgB,qBAAqB,SAAyB;EACnE,SAAS;EACT,kBAAkB;EAClB,SAAS;EACT,YAAY;EACZ,kBAAkB;EAClB,cAAc;EACd,cAAc;EACf,CAAC;CAEF,MAAM,SAAS,aAAa,OAA2B;AACrD,aAAW,GAAG;IACb,EAAE,CAAC;AAEN,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,MAAM,6BAA6B;GACjC,MAAM,EAAE,WAAW,cAAc,iBAAiB;GAClD,MAAM,mBAAmB,eAAe;AAExC,OAAI,oBAAoB,GAAG;AACzB,sBAAkB;KAChB,SAAS;KACT,kBAAkB;KAClB,SAAS;KACT,YAAY;KACZ,kBAAkB;KAClB;KACA;KACD,CAAC;AACF;;AAQF,qBAAkB;IAChB,SAAS;IACT,kBAPiB,KAAK,IACtB,KACA,KAAK,IAAI,GAAI,YAAY,mBAAoB,IAAI,CAClD;IAKC,SAAS,aAAa;IACtB,YAAY,aAAa,mBAAmB;IAC5C;IACA;IACA;IACD,CAAC;;AAGJ,wBAAsB;EAEtB,MAAM,iBAAiB;AACrB,yBAAsB;;AAGxB,UAAQ,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;EAE/D,MAAM,iBAAiB,IAAI,qBAAqB;AAC9C,yBAAsB;IACtB;AAEF,iBAAe,QAAQ,QAAQ;AAE/B,eAAa;AACX,WAAQ,oBAAoB,UAAU,SAAS;AAC/C,kBAAe,UAAU,QAAQ;;IAElC,CAAC,QAAQ,CAAC;AAEb,QAAO;EAAE,GAAG;EAAgB;EAAS;EAAQ"}
1
+ {"version":3,"file":"use-element-scroll.mjs","names":[],"sources":["../../src/hooks/use-element-scroll.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface ScrollPosition {\n scrollY: number;\n scrollPercentage: number;\n isAtTop: boolean;\n isAtBottom: boolean;\n scrollableHeight: number;\n clientHeight: number;\n scrollHeight: number;\n}\n\nconst scrollPositionEqual = (a: ScrollPosition, b: ScrollPosition) =>\n a.scrollY === b.scrollY &&\n a.scrollPercentage === b.scrollPercentage &&\n a.isAtTop === b.isAtTop &&\n a.isAtBottom === b.isAtBottom &&\n a.scrollableHeight === b.scrollableHeight &&\n a.clientHeight === b.clientHeight &&\n a.scrollHeight === b.scrollHeight;\n\ninterface Options {\n // How close to the bottom (in px) still counts as \"at bottom\". Matches\n // the previous hardcoded value by default.\n threshold?: number;\n}\n\nconst useElementScroll = ({ threshold = 1 }: Options = {}) => {\n const [element, setElement] = useState<HTMLElement | null>(null);\n const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: false,\n scrollableHeight: 0,\n clientHeight: 0,\n scrollHeight: 0,\n });\n const scrollPositionRef = useRef(scrollPosition);\n\n const setRef = useCallback((el: HTMLElement | null) => {\n setElement(el);\n }, []);\n\n useEffect(() => {\n if (!element) {\n return;\n }\n\n const commit = (next: ScrollPosition) => {\n if (scrollPositionEqual(scrollPositionRef.current, next)) {\n return;\n }\n scrollPositionRef.current = next;\n setScrollPosition(next);\n };\n\n const updateScrollPosition = () => {\n const { scrollTop, scrollHeight, clientHeight } = element;\n const scrollableHeight = scrollHeight - clientHeight;\n\n if (scrollableHeight <= 0) {\n commit({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: true,\n scrollableHeight: 0,\n clientHeight,\n scrollHeight,\n });\n return;\n }\n\n const percentage = Math.min(\n 100,\n Math.max(0, (scrollTop / scrollableHeight) * 100),\n );\n\n commit({\n scrollY: scrollTop,\n scrollPercentage: percentage,\n isAtTop: scrollTop <= 0,\n isAtBottom: scrollTop >= scrollableHeight - threshold,\n scrollableHeight,\n clientHeight,\n scrollHeight,\n });\n };\n\n updateScrollPosition();\n\n const onScroll = () => {\n updateScrollPosition();\n };\n\n element.addEventListener('scroll', onScroll, { passive: true });\n\n const resizeObserver = new ResizeObserver(() => {\n updateScrollPosition();\n });\n\n // Observing only the container misses content that grows/shrinks\n // `scrollHeight` without resizing the container's own box (a list\n // gaining items, an image finishing its load) — also watch every\n // child so their own size changes are caught.\n const observeChildren = () => {\n resizeObserver.observe(element);\n Array.from(element.children).forEach(child => {\n resizeObserver.observe(child);\n });\n };\n\n observeChildren();\n\n // Catches children being added/removed (re-syncs which elements the\n // ResizeObserver above watches, and recalculates immediately) —\n // ResizeObserver alone doesn't see brand new children until this runs.\n const mutationObserver = new MutationObserver(() => {\n observeChildren();\n updateScrollPosition();\n });\n mutationObserver.observe(element, { childList: true, subtree: true });\n\n return () => {\n element.removeEventListener('scroll', onScroll);\n resizeObserver.disconnect();\n mutationObserver.disconnect();\n };\n }, [element, threshold]);\n\n return { ...scrollPosition, element, setRef };\n};\n\nexport default useElementScroll;\n"],"mappings":";;;AAYA,MAAM,uBAAuB,GAAmB,MAC9C,EAAE,YAAY,EAAE,WAChB,EAAE,qBAAqB,EAAE,oBACzB,EAAE,YAAY,EAAE,WAChB,EAAE,eAAe,EAAE,cACnB,EAAE,qBAAqB,EAAE,oBACzB,EAAE,iBAAiB,EAAE,gBACrB,EAAE,iBAAiB,EAAE;AAQvB,MAAM,oBAAoB,EAAE,YAAY,MAAe,EAAE,KAAK;CAC5D,MAAM,CAAC,SAAS,cAAc,SAA6B,KAAK;CAChE,MAAM,CAAC,gBAAgB,qBAAqB,SAAyB;EACnE,SAAS;EACT,kBAAkB;EAClB,SAAS;EACT,YAAY;EACZ,kBAAkB;EAClB,cAAc;EACd,cAAc;EACf,CAAC;CACF,MAAM,oBAAoB,OAAO,eAAe;CAEhD,MAAM,SAAS,aAAa,OAA2B;AACrD,aAAW,GAAG;IACb,EAAE,CAAC;AAEN,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,MAAM,UAAU,SAAyB;AACvC,OAAI,oBAAoB,kBAAkB,SAAS,KAAK,CACtD;AAEF,qBAAkB,UAAU;AAC5B,qBAAkB,KAAK;;EAGzB,MAAM,6BAA6B;GACjC,MAAM,EAAE,WAAW,cAAc,iBAAiB;GAClD,MAAM,mBAAmB,eAAe;AAExC,OAAI,oBAAoB,GAAG;AACzB,WAAO;KACL,SAAS;KACT,kBAAkB;KAClB,SAAS;KACT,YAAY;KACZ,kBAAkB;KAClB;KACA;KACD,CAAC;AACF;;AAQF,UAAO;IACL,SAAS;IACT,kBAPiB,KAAK,IACtB,KACA,KAAK,IAAI,GAAI,YAAY,mBAAoB,IAAI,CAClD;IAKC,SAAS,aAAa;IACtB,YAAY,aAAa,mBAAmB;IAC5C;IACA;IACA;IACD,CAAC;;AAGJ,wBAAsB;EAEtB,MAAM,iBAAiB;AACrB,yBAAsB;;AAGxB,UAAQ,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;EAE/D,MAAM,iBAAiB,IAAI,qBAAqB;AAC9C,yBAAsB;IACtB;EAMF,MAAM,wBAAwB;AAC5B,kBAAe,QAAQ,QAAQ;AAC/B,SAAM,KAAK,QAAQ,SAAS,CAAC,SAAQ,UAAS;AAC5C,mBAAe,QAAQ,MAAM;KAC7B;;AAGJ,mBAAiB;EAKjB,MAAM,mBAAmB,IAAI,uBAAuB;AAClD,oBAAiB;AACjB,yBAAsB;IACtB;AACF,mBAAiB,QAAQ,SAAS;GAAE,WAAW;GAAM,SAAS;GAAM,CAAC;AAErE,eAAa;AACX,WAAQ,oBAAoB,UAAU,SAAS;AAC/C,kBAAe,YAAY;AAC3B,oBAAiB,YAAY;;IAE9B,CAAC,SAAS,UAAU,CAAC;AAExB,QAAO;EAAE,GAAG;EAAgB;EAAS;EAAQ"}
@@ -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