@jbpark/use-hooks 2.11.0 → 4.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 (71) hide show
  1. package/README.ko.md +40 -22
  2. package/README.md +40 -22
  3. package/dist/hooks/index.d.mts +14 -3
  4. package/dist/hooks/index.mjs +14 -3
  5. package/dist/hooks/use-click-outside.d.mts +9 -2
  6. package/dist/hooks/use-click-outside.mjs +27 -11
  7. package/dist/hooks/use-click-outside.mjs.map +1 -1
  8. package/dist/hooks/use-debounced-callback.d.mts +14 -0
  9. package/dist/hooks/{use-debounce.mjs → use-debounced-callback.mjs} +4 -4
  10. package/dist/hooks/use-debounced-callback.mjs.map +1 -0
  11. package/dist/hooks/use-debounced-value.d.mts +5 -0
  12. package/dist/hooks/use-debounced-value.mjs +20 -0
  13. package/dist/hooks/use-debounced-value.mjs.map +1 -0
  14. package/dist/hooks/use-event-listener.d.mts +21 -0
  15. package/dist/hooks/use-event-listener.mjs +50 -0
  16. package/dist/hooks/use-event-listener.mjs.map +1 -0
  17. package/dist/hooks/use-file-drop.d.mts +14 -0
  18. package/dist/hooks/use-file-drop.mjs +85 -0
  19. package/dist/hooks/use-file-drop.mjs.map +1 -0
  20. package/dist/hooks/use-image.d.mts +2 -1
  21. package/dist/hooks/use-image.mjs +4 -3
  22. package/dist/hooks/use-image.mjs.map +1 -1
  23. package/dist/hooks/use-intersection-observer.d.mts +8 -1
  24. package/dist/hooks/use-intersection-observer.mjs +37 -5
  25. package/dist/hooks/use-intersection-observer.mjs.map +1 -1
  26. package/dist/hooks/use-interval.d.mts +5 -0
  27. package/dist/hooks/use-interval.mjs +20 -0
  28. package/dist/hooks/use-interval.mjs.map +1 -0
  29. package/dist/hooks/use-key-press.d.mts +14 -0
  30. package/dist/hooks/use-key-press.mjs +85 -0
  31. package/dist/hooks/use-key-press.mjs.map +1 -0
  32. package/dist/hooks/use-merged-ref.d.mts +8 -0
  33. package/dist/hooks/use-merged-ref.mjs +31 -0
  34. package/dist/hooks/use-merged-ref.mjs.map +1 -0
  35. package/dist/hooks/use-mutation-observer.d.mts +11 -0
  36. package/dist/hooks/use-mutation-observer.mjs +47 -0
  37. package/dist/hooks/use-mutation-observer.mjs.map +1 -0
  38. package/dist/hooks/use-previous.d.mts +5 -0
  39. package/dist/hooks/use-previous.mjs +14 -0
  40. package/dist/hooks/use-previous.mjs.map +1 -0
  41. package/dist/hooks/use-resize-observer.d.mts +12 -0
  42. package/dist/hooks/use-resize-observer.mjs +53 -0
  43. package/dist/hooks/use-resize-observer.mjs.map +1 -0
  44. package/dist/hooks/use-responsive-size.mjs +2 -2
  45. package/dist/hooks/use-responsive-size.mjs.map +1 -1
  46. package/dist/hooks/use-scroll-to-elements.d.mts +7 -5
  47. package/dist/hooks/use-scroll-to-elements.mjs +52 -27
  48. package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
  49. package/dist/hooks/use-throttled-callback.d.mts +12 -0
  50. package/dist/hooks/use-throttled-callback.mjs +54 -0
  51. package/dist/hooks/use-throttled-callback.mjs.map +1 -0
  52. package/dist/hooks/use-throttled-value.d.mts +9 -0
  53. package/dist/hooks/use-throttled-value.mjs +16 -0
  54. package/dist/hooks/use-throttled-value.mjs.map +1 -0
  55. package/dist/hooks/use-timeout.d.mts +8 -0
  56. package/dist/hooks/use-timeout.mjs +38 -0
  57. package/dist/hooks/use-timeout.mjs.map +1 -0
  58. package/dist/hooks/use-toggle.d.mts +5 -0
  59. package/dist/hooks/use-toggle.mjs +17 -0
  60. package/dist/hooks/use-toggle.mjs.map +1 -0
  61. package/dist/index.d.mts +15 -4
  62. package/dist/index.mjs +15 -4
  63. package/package.json +1 -1
  64. package/dist/hooks/use-debounce.d.mts +0 -14
  65. package/dist/hooks/use-debounce.mjs.map +0 -1
  66. package/dist/hooks/use-throttle.d.mts +0 -5
  67. package/dist/hooks/use-throttle.mjs +0 -42
  68. package/dist/hooks/use-throttle.mjs.map +0 -1
  69. package/dist/hooks/use-timeline.d.mts +0 -50
  70. package/dist/hooks/use-timeline.mjs +0 -211
  71. package/dist/hooks/use-timeline.mjs.map +0 -1
@@ -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
@@ -0,0 +1,85 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ //#region src/hooks/use-key-press.ts
4
+ const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
5
+ const KEY_ALIASES = {
6
+ space: " ",
7
+ spacebar: " ",
8
+ esc: "escape"
9
+ };
10
+ const parseCombo = (combo) => {
11
+ const parts = combo.split("+");
12
+ const modifiers = parts.slice(0, -1).map((part) => part.trim().toLowerCase());
13
+ const rawKey = parts[parts.length - 1] ?? "";
14
+ const trimmedKey = rawKey.trim();
15
+ let key = (trimmedKey === "" && rawKey !== "" ? rawKey : trimmedKey).toLowerCase();
16
+ if (key === "" && parts.length > 1) key = "+";
17
+ key = KEY_ALIASES[key] ?? key;
18
+ let ctrl = modifiers.includes("ctrl");
19
+ let meta = modifiers.includes("meta") || modifiers.includes("cmd");
20
+ const shift = modifiers.includes("shift");
21
+ const alt = modifiers.includes("alt") || modifiers.includes("option");
22
+ if (modifiers.includes("mod")) if (isMac()) meta = true;
23
+ else ctrl = true;
24
+ return {
25
+ key,
26
+ ctrl,
27
+ meta,
28
+ shift,
29
+ alt
30
+ };
31
+ };
32
+ const matchesCombo = (event, combo) => event.key.toLowerCase() === combo.key && event.ctrlKey === combo.ctrl && event.metaKey === combo.meta && event.shiftKey === combo.shift && event.altKey === combo.alt;
33
+ const resolveTarget = (target) => {
34
+ if (!target) return typeof window === "undefined" ? null : window;
35
+ return "current" in target ? target.current : target;
36
+ };
37
+ const useKeyPress = (combo, handler, options = {}) => {
38
+ const { target, enabled = true, preventDefault = false, ignore } = options;
39
+ const handlerRef = useRef(handler);
40
+ useEffect(() => {
41
+ handlerRef.current = handler;
42
+ });
43
+ const combos = Array.isArray(combo) ? combo : [combo];
44
+ const parsedCombos = combos.map(parseCombo);
45
+ useEffect(() => {
46
+ if (!enabled) return;
47
+ let cancelled = false;
48
+ let rafId;
49
+ let resolvedTarget = null;
50
+ const onKeyDown = (event) => {
51
+ const keyboardEvent = event;
52
+ const eventTarget = keyboardEvent.target;
53
+ if (ignore && eventTarget?.closest(ignore)) return;
54
+ if (!parsedCombos.some((parsed) => matchesCombo(keyboardEvent, parsed))) return;
55
+ if (preventDefault) keyboardEvent.preventDefault();
56
+ handlerRef.current(keyboardEvent);
57
+ };
58
+ const attach = () => {
59
+ if (cancelled) return;
60
+ if (target && "current" in target && !target.current) {
61
+ rafId = requestAnimationFrame(attach);
62
+ return;
63
+ }
64
+ resolvedTarget = resolveTarget(target);
65
+ if (!resolvedTarget) return;
66
+ resolvedTarget.addEventListener("keydown", onKeyDown);
67
+ };
68
+ attach();
69
+ return () => {
70
+ cancelled = true;
71
+ if (rafId !== void 0) cancelAnimationFrame(rafId);
72
+ resolvedTarget?.removeEventListener("keydown", onKeyDown);
73
+ };
74
+ }, [
75
+ enabled,
76
+ combos.join(","),
77
+ target,
78
+ preventDefault,
79
+ ignore
80
+ ]);
81
+ };
82
+
83
+ //#endregion
84
+ export { useKeyPress as default };
85
+ //# sourceMappingURL=use-key-press.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-key-press.mjs","names":[],"sources":["../../src/hooks/use-key-press.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react';\n\ntype KeyPressTarget =\n | RefObject<HTMLElement | null>\n | Window\n | Document\n | HTMLElement\n | null\n | undefined;\n\ninterface Options {\n target?: KeyPressTarget;\n enabled?: boolean;\n preventDefault?: boolean;\n // CSS selector — a keydown whose target is inside a matching element is\n // ignored (e.g. don't hijack Cmd+Z while the user is typing in a code\n // editor that has its own undo).\n ignore?: string;\n}\n\ninterface ParsedCombo {\n key: string;\n ctrl: boolean;\n meta: boolean;\n shift: boolean;\n alt: boolean;\n}\n\nconst isMac = () =>\n typeof navigator !== 'undefined' &&\n /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);\n\n// Human-friendly spellings mapped to the actual `event.key` value, so\n// callers can write `'space'` instead of a literal ' ' (whose trimmed form\n// is empty and easy to lose) or `'esc'` for Escape.\nconst KEY_ALIASES: Record<string, string> = {\n space: ' ',\n spacebar: ' ',\n esc: 'escape',\n};\n\n// `mod` normalizes to the platform's usual \"primary\" modifier (Cmd on\n// macOS, Ctrl elsewhere) so a single combo string covers both without the\n// caller branching on platform themselves.\nconst parseCombo = (combo: string): ParsedCombo => {\n const parts = combo.split('+');\n const modifiers = parts.slice(0, -1).map(part => part.trim().toLowerCase());\n\n // The last segment is the key. Trim it like the modifiers so 'ctrl + z'\n // still resolves to 'z' — but if trimming would wipe a non-empty segment\n // entirely, the key *is* whitespace: the space key, whose event.key is ' '.\n const rawKey = parts[parts.length - 1] ?? '';\n const trimmedKey = rawKey.trim();\n let key = (\n trimmedKey === '' && rawKey !== '' ? rawKey : trimmedKey\n ).toLowerCase();\n\n // A bare '+' or a combo like 'ctrl++' splits to an empty final segment —\n // the '+' separator itself is the intended key.\n if (key === '' && parts.length > 1) {\n key = '+';\n }\n\n key = KEY_ALIASES[key] ?? key;\n\n let ctrl = modifiers.includes('ctrl');\n let meta = modifiers.includes('meta') || modifiers.includes('cmd');\n const shift = modifiers.includes('shift');\n const alt = modifiers.includes('alt') || modifiers.includes('option');\n\n if (modifiers.includes('mod')) {\n if (isMac()) {\n meta = true;\n } else {\n ctrl = true;\n }\n }\n\n return { key, ctrl, meta, shift, alt };\n};\n\n// Modifiers not named in the combo are required to be *absent*, not just\n// ignored — otherwise 'mod+z' and 'mod+shift+z' registered as separate\n// bindings (the exact motivating case for this hook) would both fire on\n// the same Cmd+Shift+Z keypress instead of only the latter.\nconst matchesCombo = (event: KeyboardEvent, combo: ParsedCombo) =>\n event.key.toLowerCase() === combo.key &&\n event.ctrlKey === combo.ctrl &&\n event.metaKey === combo.meta &&\n event.shiftKey === combo.shift &&\n event.altKey === combo.alt;\n\nconst resolveTarget = (target: KeyPressTarget): EventTarget | null => {\n if (!target) {\n return typeof window === 'undefined' ? null : window;\n }\n return 'current' in target ? target.current : target;\n};\n\nconst useKeyPress = (\n combo: string | string[],\n handler: (event: KeyboardEvent) => void,\n options: Options = {},\n) => {\n const { target, enabled = true, preventDefault = false, ignore } = options;\n\n const handlerRef = useRef(handler);\n\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n const combos = Array.isArray(combo) ? combo : [combo];\n const parsedCombos = combos.map(parseCombo);\n // `combos` is typically a fresh array literal at the call site — depend\n // on a stable string proxy instead so this doesn't tear down and\n // re-register the listener on every render. `target` itself (a\n // RefObject, or window/document/an element variable) is normally\n // already stable across renders, so it's used directly below.\n const comboKey = combos.join(',');\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n let cancelled = false;\n let rafId: number | undefined;\n let resolvedTarget: EventTarget | null = null;\n\n const onKeyDown = (event: Event) => {\n const keyboardEvent = event as KeyboardEvent;\n const eventTarget = keyboardEvent.target as HTMLElement | null;\n\n if (ignore && eventTarget?.closest(ignore)) {\n return;\n }\n\n if (!parsedCombos.some(parsed => matchesCombo(keyboardEvent, parsed))) {\n return;\n }\n\n if (preventDefault) {\n keyboardEvent.preventDefault();\n }\n\n handlerRef.current(keyboardEvent);\n };\n\n const attach = () => {\n if (cancelled) {\n return;\n }\n\n // A RefObject target can still be null right after mount (conditional\n // render, portal, lazy mount) — retry every frame until it's\n // populated instead of giving up on the first effect run. The default\n // window and raw element/document targets have nothing to wait for.\n if (target && 'current' in target && !target.current) {\n rafId = requestAnimationFrame(attach);\n return;\n }\n\n resolvedTarget = resolveTarget(target);\n\n if (!resolvedTarget) {\n return;\n }\n\n resolvedTarget.addEventListener('keydown', onKeyDown);\n };\n\n attach();\n\n return () => {\n cancelled = true;\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\n resolvedTarget?.removeEventListener('keydown', onKeyDown);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, comboKey, target, preventDefault, ignore]);\n};\n\nexport default useKeyPress;\n"],"mappings":";;;AA4BA,MAAM,cACJ,OAAO,cAAc,eACrB,uBAAuB,KAAK,UAAU,UAAU;AAKlD,MAAM,cAAsC;CAC1C,OAAO;CACP,UAAU;CACV,KAAK;CACN;AAKD,MAAM,cAAc,UAA+B;CACjD,MAAM,QAAQ,MAAM,MAAM,IAAI;CAC9B,MAAM,YAAY,MAAM,MAAM,GAAG,GAAG,CAAC,KAAI,SAAQ,KAAK,MAAM,CAAC,aAAa,CAAC;CAK3E,MAAM,SAAS,MAAM,MAAM,SAAS,MAAM;CAC1C,MAAM,aAAa,OAAO,MAAM;CAChC,IAAI,OACF,eAAe,MAAM,WAAW,KAAK,SAAS,YAC9C,aAAa;AAIf,KAAI,QAAQ,MAAM,MAAM,SAAS,EAC/B,OAAM;AAGR,OAAM,YAAY,QAAQ;CAE1B,IAAI,OAAO,UAAU,SAAS,OAAO;CACrC,IAAI,OAAO,UAAU,SAAS,OAAO,IAAI,UAAU,SAAS,MAAM;CAClE,MAAM,QAAQ,UAAU,SAAS,QAAQ;CACzC,MAAM,MAAM,UAAU,SAAS,MAAM,IAAI,UAAU,SAAS,SAAS;AAErE,KAAI,UAAU,SAAS,MAAM,CAC3B,KAAI,OAAO,CACT,QAAO;KAEP,QAAO;AAIX,QAAO;EAAE;EAAK;EAAM;EAAM;EAAO;EAAK;;AAOxC,MAAM,gBAAgB,OAAsB,UAC1C,MAAM,IAAI,aAAa,KAAK,MAAM,OAClC,MAAM,YAAY,MAAM,QACxB,MAAM,YAAY,MAAM,QACxB,MAAM,aAAa,MAAM,SACzB,MAAM,WAAW,MAAM;AAEzB,MAAM,iBAAiB,WAA+C;AACpE,KAAI,CAAC,OACH,QAAO,OAAO,WAAW,cAAc,OAAO;AAEhD,QAAO,aAAa,SAAS,OAAO,UAAU;;AAGhD,MAAM,eACJ,OACA,SACA,UAAmB,EAAE,KAClB;CACH,MAAM,EAAE,QAAQ,UAAU,MAAM,iBAAiB,OAAO,WAAW;CAEnE,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;CAEF,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM;CACrD,MAAM,eAAe,OAAO,IAAI,WAAW;AAQ3C,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,IAAI,YAAY;EAChB,IAAI;EACJ,IAAI,iBAAqC;EAEzC,MAAM,aAAa,UAAiB;GAClC,MAAM,gBAAgB;GACtB,MAAM,cAAc,cAAc;AAElC,OAAI,UAAU,aAAa,QAAQ,OAAO,CACxC;AAGF,OAAI,CAAC,aAAa,MAAK,WAAU,aAAa,eAAe,OAAO,CAAC,CACnE;AAGF,OAAI,eACF,eAAc,gBAAgB;AAGhC,cAAW,QAAQ,cAAc;;EAGnC,MAAM,eAAe;AACnB,OAAI,UACF;AAOF,OAAI,UAAU,aAAa,UAAU,CAAC,OAAO,SAAS;AACpD,YAAQ,sBAAsB,OAAO;AACrC;;AAGF,oBAAiB,cAAc,OAAO;AAEtC,OAAI,CAAC,eACH;AAGF,kBAAe,iBAAiB,WAAW,UAAU;;AAGvD,UAAQ;AAER,eAAa;AACX,eAAY;AACZ,OAAI,UAAU,OACZ,sBAAqB,MAAM;AAE7B,mBAAgB,oBAAoB,WAAW,UAAU;;IAG1D;EAAC;EA/Da,OAAO,KAAK,IAAI;EA+DV;EAAQ;EAAgB;EAAO,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { RefObject } from "react";
2
+
3
+ //#region src/hooks/use-merged-ref.d.ts
4
+ type MergeableRef<T> = ((node: T | null) => void | (() => void)) | RefObject<T | null> | null | undefined;
5
+ declare const useMergedRef: <T>(...refs: MergeableRef<T>[]) => (node: T | null) => () => void;
6
+ //#endregion
7
+ export { useMergedRef };
8
+ //# sourceMappingURL=use-merged-ref.d.mts.map
@@ -0,0 +1,31 @@
1
+ import { useCallback } from "react";
2
+
3
+ //#region src/hooks/use-merged-ref.ts
4
+ const useMergedRef = (...refs) => {
5
+ return useCallback((node) => {
6
+ const cleanups = refs.map((ref) => {
7
+ if (!ref) return;
8
+ if (typeof ref === "function") {
9
+ const cleanup = ref(node);
10
+ return typeof cleanup === "function" ? cleanup : void 0;
11
+ }
12
+ ref.current = node;
13
+ });
14
+ return () => {
15
+ refs.forEach((ref, i) => {
16
+ if (!ref) return;
17
+ if (typeof ref === "function") {
18
+ const cleanup = cleanups[i];
19
+ if (cleanup) cleanup();
20
+ else ref(null);
21
+ return;
22
+ }
23
+ ref.current = null;
24
+ });
25
+ };
26
+ }, refs);
27
+ };
28
+
29
+ //#endregion
30
+ export { useMergedRef as default };
31
+ //# sourceMappingURL=use-merged-ref.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-merged-ref.mjs","names":[],"sources":["../../src/hooks/use-merged-ref.ts"],"sourcesContent":["import { type RefObject, useCallback } from 'react';\n\ntype MergeableRef<T> =\n | ((node: T | null) => void | (() => void))\n | RefObject<T | null>\n | null\n | undefined;\n\n// Merges any number of refs (forwarded function refs, RefObjects, or\n// either left null/undefined) into one callback ref that updates all of\n// them. Collects React 19's optional per-ref cleanup return values and\n// runs them together when the node detaches.\nconst useMergedRef = <T>(...refs: MergeableRef<T>[]) => {\n return useCallback((node: T | null) => {\n // Per-ref cleanups, aligned to `refs` by index: a function ref that\n // returns its own cleanup keeps it here, everything else is undefined.\n const cleanups = refs.map(ref => {\n if (!ref) {\n return undefined;\n }\n\n if (typeof ref === 'function') {\n const cleanup = ref(node);\n return typeof cleanup === 'function' ? cleanup : undefined;\n }\n\n ref.current = node;\n return undefined;\n });\n\n // Always return a cleanup and detach *every* ref here. When any ref\n // returns a cleanup, React 19 runs this instead of re-invoking the\n // callback with null on detach — so if we only ran the collected\n // cleanups, the object refs and cleanup-less function refs merged\n // alongside would never be released and would pin a stale node.\n return () => {\n refs.forEach((ref, i) => {\n if (!ref) {\n return;\n }\n\n if (typeof ref === 'function') {\n const cleanup = cleanups[i];\n if (cleanup) {\n cleanup();\n } else {\n ref(null);\n }\n return;\n }\n\n ref.current = null;\n });\n };\n // The number of refs passed at a given call site is stable across\n // renders even though this array literal isn't — same pattern every\n // ref-merging hook of this shape relies on.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, refs);\n};\n\nexport default useMergedRef;\n"],"mappings":";;;AAYA,MAAM,gBAAmB,GAAG,SAA4B;AACtD,QAAO,aAAa,SAAmB;EAGrC,MAAM,WAAW,KAAK,KAAI,QAAO;AAC/B,OAAI,CAAC,IACH;AAGF,OAAI,OAAO,QAAQ,YAAY;IAC7B,MAAM,UAAU,IAAI,KAAK;AACzB,WAAO,OAAO,YAAY,aAAa,UAAU;;AAGnD,OAAI,UAAU;IAEd;AAOF,eAAa;AACX,QAAK,SAAS,KAAK,MAAM;AACvB,QAAI,CAAC,IACH;AAGF,QAAI,OAAO,QAAQ,YAAY;KAC7B,MAAM,UAAU,SAAS;AACzB,SAAI,QACF,UAAS;SAET,KAAI,KAAK;AAEX;;AAGF,QAAI,UAAU;KACd;;IAMH,KAAK"}
@@ -0,0 +1,11 @@
1
+ import { RefObject } from "react";
2
+
3
+ //#region src/hooks/use-mutation-observer.d.ts
4
+ type Target<T extends Node> = RefObject<T | null> | T | null | undefined;
5
+ interface Options extends MutationObserverInit {
6
+ enabled?: boolean;
7
+ }
8
+ declare const useMutationObserver: <T extends Node>(target: Target<T>, callback: MutationCallback, options?: Options) => void;
9
+ //#endregion
10
+ export { useMutationObserver };
11
+ //# sourceMappingURL=use-mutation-observer.d.mts.map
@@ -0,0 +1,47 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ //#region src/hooks/use-mutation-observer.ts
4
+ const resolveTarget = (target) => {
5
+ if (!target) return null;
6
+ return "current" in target ? target.current : target;
7
+ };
8
+ const useMutationObserver = (target, callback, options = {}) => {
9
+ const { enabled = true, ...mutationOptions } = options;
10
+ const callbackRef = useRef(callback);
11
+ useEffect(() => {
12
+ callbackRef.current = callback;
13
+ });
14
+ useEffect(() => {
15
+ if (!enabled || typeof MutationObserver === "undefined") return;
16
+ let cancelled = false;
17
+ let rafId;
18
+ let observer;
19
+ const attach = () => {
20
+ if (cancelled) return;
21
+ if (target && "current" in target && !target.current) {
22
+ rafId = requestAnimationFrame(attach);
23
+ return;
24
+ }
25
+ const node = resolveTarget(target);
26
+ if (!node) return;
27
+ observer = new MutationObserver((mutations, obs) => {
28
+ callbackRef.current(mutations, obs);
29
+ });
30
+ observer.observe(node, mutationOptions);
31
+ };
32
+ attach();
33
+ return () => {
34
+ cancelled = true;
35
+ if (rafId !== void 0) cancelAnimationFrame(rafId);
36
+ observer?.disconnect();
37
+ };
38
+ }, [
39
+ target,
40
+ enabled,
41
+ JSON.stringify(mutationOptions)
42
+ ]);
43
+ };
44
+
45
+ //#endregion
46
+ export { useMutationObserver as default };
47
+ //# sourceMappingURL=use-mutation-observer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-mutation-observer.mjs","names":[],"sources":["../../src/hooks/use-mutation-observer.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react';\n\ntype Target<T extends Node> = RefObject<T | null> | T | null | undefined;\n\ninterface Options extends MutationObserverInit {\n enabled?: boolean;\n}\n\nconst resolveTarget = <T extends Node>(target: Target<T>): T | null => {\n if (!target) {\n return null;\n }\n return 'current' in target ? target.current : target;\n};\n\n// Takes the target directly (a RefObject, or a plain Node like\n// document.head that isn't behind any React ref at all) rather than\n// producing its own — this and useResizeObserver together replace the\n// ResizeObserver+MutationObserver pair live-editor's iframe hand-rolls\n// for auto-sizing its preview content.\nconst useMutationObserver = <T extends Node>(\n target: Target<T>,\n callback: MutationCallback,\n options: Options = {},\n) => {\n const { enabled = true, ...mutationOptions } = options;\n\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n // `mutationOptions` is typically a fresh object literal at the call\n // site — serialize it into a stable key instead of depending on the\n // object itself, which would tear down and recreate the observer on\n // every render.\n const optionsKey = JSON.stringify(mutationOptions);\n\n useEffect(() => {\n if (!enabled || typeof MutationObserver === 'undefined') {\n return;\n }\n\n let cancelled = false;\n let rafId: number | undefined;\n let observer: MutationObserver | undefined;\n\n const attach = () => {\n if (cancelled) {\n return;\n }\n\n // A RefObject target can still be null right after mount (conditional\n // render, portal, lazy mount) — retry every frame until it's\n // populated instead of giving up on the first effect run, since the\n // ref object's identity doesn't change to re-run this effect. A raw\n // node has nothing to wait for.\n if (target && 'current' in target && !target.current) {\n rafId = requestAnimationFrame(attach);\n return;\n }\n\n const node = resolveTarget(target);\n\n if (!node) {\n return;\n }\n\n observer = new MutationObserver((mutations, obs) => {\n callbackRef.current(mutations, obs);\n });\n\n observer.observe(node, mutationOptions);\n };\n\n attach();\n\n return () => {\n cancelled = true;\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\n observer?.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [target, enabled, optionsKey]);\n};\n\nexport default useMutationObserver;\n"],"mappings":";;;AAQA,MAAM,iBAAiC,WAAgC;AACrE,KAAI,CAAC,OACH,QAAO;AAET,QAAO,aAAa,SAAS,OAAO,UAAU;;AAQhD,MAAM,uBACJ,QACA,UACA,UAAmB,EAAE,KAClB;CACH,MAAM,EAAE,UAAU,MAAM,GAAG,oBAAoB;CAE/C,MAAM,cAAc,OAAO,SAAS;AAEpC,iBAAgB;AACd,cAAY,UAAU;GACtB;AAQF,iBAAgB;AACd,MAAI,CAAC,WAAW,OAAO,qBAAqB,YAC1C;EAGF,IAAI,YAAY;EAChB,IAAI;EACJ,IAAI;EAEJ,MAAM,eAAe;AACnB,OAAI,UACF;AAQF,OAAI,UAAU,aAAa,UAAU,CAAC,OAAO,SAAS;AACpD,YAAQ,sBAAsB,OAAO;AACrC;;GAGF,MAAM,OAAO,cAAc,OAAO;AAElC,OAAI,CAAC,KACH;AAGF,cAAW,IAAI,kBAAkB,WAAW,QAAQ;AAClD,gBAAY,QAAQ,WAAW,IAAI;KACnC;AAEF,YAAS,QAAQ,MAAM,gBAAgB;;AAGzC,UAAQ;AAER,eAAa;AACX,eAAY;AACZ,OAAI,UAAU,OACZ,sBAAqB,MAAM;AAE7B,aAAU,YAAY;;IAGvB;EAAC;EAAQ;EAjDO,KAAK,UAAU,gBAAgB;EAiDlB,CAAC"}
@@ -0,0 +1,5 @@
1
+ //#region src/hooks/use-previous.d.ts
2
+ declare const usePrevious: <T>(value: T) => T | undefined;
3
+ //#endregion
4
+ export { usePrevious };
5
+ //# sourceMappingURL=use-previous.d.mts.map
@@ -0,0 +1,14 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ //#region src/hooks/use-previous.ts
4
+ const usePrevious = (value) => {
5
+ const ref = useRef(void 0);
6
+ useEffect(() => {
7
+ ref.current = value;
8
+ });
9
+ return ref.current;
10
+ };
11
+
12
+ //#endregion
13
+ export { usePrevious as default };
14
+ //# sourceMappingURL=use-previous.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-previous.mjs","names":[],"sources":["../../src/hooks/use-previous.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\n// Returns the value from the *previous* render. Updated in an effect\n// (which runs after render/paint) rather than during render, so the\n// current render still reads whatever was current one render ago.\nconst usePrevious = <T>(value: T): T | undefined => {\n const ref = useRef<T | undefined>(undefined);\n\n useEffect(() => {\n ref.current = value;\n });\n\n return ref.current;\n};\n\nexport default usePrevious;\n"],"mappings":";;;AAKA,MAAM,eAAkB,UAA4B;CAClD,MAAM,MAAM,OAAsB,OAAU;AAE5C,iBAAgB;AACd,MAAI,UAAU;GACd;AAEF,QAAO,IAAI"}
@@ -0,0 +1,12 @@
1
+ //#region src/hooks/use-resize-observer.d.ts
2
+ interface Size {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ interface Options {
7
+ box?: ResizeObserverBoxOptions;
8
+ }
9
+ declare const useResizeObserver: <T extends Element = Element>(options?: Options) => [(node: T | null) => void, Size | null];
10
+ //#endregion
11
+ export { useResizeObserver };
12
+ //# sourceMappingURL=use-resize-observer.d.mts.map
@@ -0,0 +1,53 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ //#region src/hooks/use-resize-observer.ts
4
+ const useResizeObserver = (options) => {
5
+ const box = options?.box ?? "content-box";
6
+ const [size, setSize] = useState(null);
7
+ const observerRef = useRef(null);
8
+ const nodeRef = useRef(null);
9
+ const boxRef = useRef(box);
10
+ useEffect(() => {
11
+ boxRef.current = box;
12
+ });
13
+ const connect = useCallback(() => {
14
+ if (observerRef.current) {
15
+ observerRef.current.disconnect();
16
+ observerRef.current = null;
17
+ }
18
+ const node = nodeRef.current;
19
+ if (!node || typeof ResizeObserver === "undefined") return;
20
+ const currentBox = boxRef.current;
21
+ const observer = new ResizeObserver(([entry]) => {
22
+ if (!entry) return;
23
+ const measurement = (currentBox === "border-box" ? entry.borderBoxSize : entry.contentBoxSize)?.[0];
24
+ if (measurement) setSize({
25
+ width: measurement.inlineSize,
26
+ height: measurement.blockSize
27
+ });
28
+ else setSize({
29
+ width: entry.contentRect.width,
30
+ height: entry.contentRect.height
31
+ });
32
+ });
33
+ observer.observe(node, { box: currentBox });
34
+ observerRef.current = observer;
35
+ }, []);
36
+ const ref = useCallback((node) => {
37
+ nodeRef.current = node;
38
+ connect();
39
+ }, []);
40
+ useEffect(() => {
41
+ connect();
42
+ }, [box]);
43
+ useEffect(() => {
44
+ return () => {
45
+ observerRef.current?.disconnect();
46
+ };
47
+ }, []);
48
+ return [ref, size];
49
+ };
50
+
51
+ //#endregion
52
+ export { useResizeObserver as default };
53
+ //# sourceMappingURL=use-resize-observer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-resize-observer.mjs","names":[],"sources":["../../src/hooks/use-resize-observer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface Size {\n width: number;\n height: number;\n}\n\ninterface Options {\n box?: ResizeObserverBoxOptions;\n}\n\n// The unprocessed version of useResponsiveSize/useElementScroll/\n// useElementPosition — those each return values shaped for their own\n// purpose (breakpoints, scroll position, a viewport-relative DOMRect).\n// This one just reports an element's own width/height.\nconst useResizeObserver = <T extends Element = Element>(\n options?: Options,\n): [(node: T | null) => void, Size | null] => {\n const box = options?.box ?? 'content-box';\n\n const [size, setSize] = useState<Size | null>(null);\n const observerRef = useRef<ResizeObserver | null>(null);\n const nodeRef = useRef<T | null>(null);\n\n // `options` is typically a fresh object literal at the call site — track\n // the current box in a ref (read at connect time) and reconnect only when\n // the value actually changes, so `box` can be flipped at runtime without\n // tearing the observer down on every render. The previous version pinned\n // whatever `box` was current when the node first attached and never\n // reacted to changes — the same bug #118 fixed in useIntersectionObserver.\n const boxRef = useRef(box);\n\n useEffect(() => {\n boxRef.current = box;\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 (!node || typeof ResizeObserver === 'undefined') {\n return;\n }\n\n const currentBox = boxRef.current;\n\n const observer = new ResizeObserver(([entry]) => {\n if (!entry) {\n return;\n }\n\n const boxSize =\n currentBox === 'border-box'\n ? entry.borderBoxSize\n : entry.contentBoxSize;\n const measurement = boxSize?.[0];\n\n if (measurement) {\n // inlineSize/blockSize are writing-mode relative; for the default\n // horizontal-tb writing mode they map to width/height as expected.\n setSize({\n width: measurement.inlineSize,\n height: measurement.blockSize,\n });\n } else {\n // contentBoxSize/borderBoxSize come back as empty arrays for a\n // `display: none` target, so destructuring the first element would\n // throw. contentRect still reports (0×0 there) and is already in\n // physical width/height, so it's also unaffected by writing-mode.\n setSize({\n width: entry.contentRect.width,\n height: entry.contentRect.height,\n });\n }\n });\n\n observer.observe(node, { box: currentBox });\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 }, [box]);\n\n useEffect(() => {\n return () => {\n observerRef.current?.disconnect();\n };\n }, []);\n\n return [ref, size];\n};\n\nexport default useResizeObserver;\n"],"mappings":";;;AAeA,MAAM,qBACJ,YAC4C;CAC5C,MAAM,MAAM,SAAS,OAAO;CAE5B,MAAM,CAAC,MAAM,WAAW,SAAsB,KAAK;CACnD,MAAM,cAAc,OAA8B,KAAK;CACvD,MAAM,UAAU,OAAiB,KAAK;CAQtC,MAAM,SAAS,OAAO,IAAI;AAE1B,iBAAgB;AACd,SAAO,UAAU;GACjB;CAEF,MAAM,UAAU,kBAAkB;AAChC,MAAI,YAAY,SAAS;AACvB,eAAY,QAAQ,YAAY;AAChC,eAAY,UAAU;;EAGxB,MAAM,OAAO,QAAQ;AAErB,MAAI,CAAC,QAAQ,OAAO,mBAAmB,YACrC;EAGF,MAAM,aAAa,OAAO;EAE1B,MAAM,WAAW,IAAI,gBAAgB,CAAC,WAAW;AAC/C,OAAI,CAAC,MACH;GAOF,MAAM,eAHJ,eAAe,eACX,MAAM,gBACN,MAAM,kBACkB;AAE9B,OAAI,YAGF,SAAQ;IACN,OAAO,YAAY;IACnB,QAAQ,YAAY;IACrB,CAAC;OAMF,SAAQ;IACN,OAAO,MAAM,YAAY;IACzB,QAAQ,MAAM,YAAY;IAC3B,CAAC;IAEJ;AAEF,WAAS,QAAQ,MAAM,EAAE,KAAK,YAAY,CAAC;AAC3C,cAAY,UAAU;IACrB,EAAE,CAAC;CAEN,MAAM,MAAM,aAAa,SAAmB;AAC1C,UAAQ,UAAU;AAClB,WAAS;IAER,EAAE,CAAC;AAEN,iBAAgB;AACd,WAAS;IAER,CAAC,IAAI,CAAC;AAET,iBAAgB;AACd,eAAa;AACX,eAAY,SAAS,YAAY;;IAElC,EAAE,CAAC;AAEN,QAAO,CAAC,KAAK,KAAK"}
@@ -1,4 +1,4 @@
1
- import useDebounce from "./use-debounce.mjs";
1
+ import useDebouncedCallback from "./use-debounced-callback.mjs";
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
 
4
4
  //#region src/hooks/use-responsive-size.ts
@@ -65,7 +65,7 @@ const useResponsiveSize = (options) => {
65
65
  const nextBreakpoint = getBreakpointInfo(next.width);
66
66
  setBreakpoint((prev) => breakpointEqual(prev, nextBreakpoint) ? prev : nextBreakpoint);
67
67
  }, []);
68
- const debouncedCommit = useDebounce(commit, {
68
+ const debouncedCommit = useDebouncedCallback(commit, {
69
69
  delay,
70
70
  autoInvoke: false
71
71
  });
@@ -1 +1 @@
1
- {"version":3,"file":"use-responsive-size.mjs","names":[],"sources":["../../src/hooks/use-responsive-size.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\n\nimport useDebounce from './use-debounce';\n\ntype Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\ninterface BreakpointInfo {\n current: Breakpoint;\n xs: boolean;\n sm: boolean;\n md: boolean;\n lg: boolean;\n xl: boolean;\n '2xl': boolean;\n}\n\ninterface Options {\n delay?: number;\n container?: HTMLElement | null;\n // Measure the viewport (window.innerWidth/innerHeight) instead of an\n // element/document.body. Useful when no `container`/ref is attached and\n // the breakpoint should reflect the viewport rather than document.body's\n // box, which can diverge from it if body has margin/transform.\n viewport?: boolean;\n}\n\nconst BREAKPOINTS = {\n xs: 0, // < 640px\n sm: 640, // >= 640px\n md: 768, // >= 768px\n lg: 1024, // >= 1024px\n xl: 1280, // >= 1280px\n '2xl': 1536, // >= 1536px\n} as const;\n\nconst getBreakpointInfo = (width: number): BreakpointInfo => {\n let current: Breakpoint = 'xs';\n\n if (width >= BREAKPOINTS['2xl']) {\n current = '2xl';\n } else if (width >= BREAKPOINTS.xl) {\n current = 'xl';\n } else if (width >= BREAKPOINTS.lg) {\n current = 'lg';\n } else if (width >= BREAKPOINTS.md) {\n current = 'md';\n } else if (width >= BREAKPOINTS.sm) {\n current = 'sm';\n } else {\n current = 'xs';\n }\n\n return {\n current,\n xs: width < BREAKPOINTS.sm,\n sm: width >= BREAKPOINTS.sm && width < BREAKPOINTS.md,\n md: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,\n lg: width >= BREAKPOINTS.lg && width < BREAKPOINTS.xl,\n xl: width >= BREAKPOINTS.xl && width < BREAKPOINTS['2xl'],\n '2xl': width >= BREAKPOINTS['2xl'],\n };\n};\n\nconst breakpointEqual = (a: BreakpointInfo, b: BreakpointInfo) =>\n a.current === b.current &&\n a.xs === b.xs &&\n a.sm === b.sm &&\n a.md === b.md &&\n a.lg === b.lg &&\n a.xl === b.xl &&\n a['2xl'] === b['2xl'];\n\nconst measureTarget = (target: HTMLElement) => ({\n width: target.offsetWidth,\n height: target.offsetHeight,\n});\n\nconst measureViewport = () => ({\n width: window.innerWidth,\n height: window.innerHeight,\n});\n\nconst useResponsiveSize = <T extends HTMLElement>(options?: Options) => {\n const { delay = 100, container, viewport = false } = options || {};\n\n const [element, setElement] = useState<T | null>(null);\n const [size, setSize] = useState({ width: 0, height: 0 });\n const [breakpoint, setBreakpoint] = useState<BreakpointInfo>(() =>\n getBreakpointInfo(0),\n );\n\n const observerRef = useRef<ResizeObserver | null>(null);\n const latestRef = useRef({ width: 0, height: 0 });\n const committedRef = useRef({ width: 0, height: 0 });\n\n const ref = useCallback((node: T | null) => {\n setElement(node);\n }, []);\n\n // `size` and `breakpoint` are always committed together here, so\n // consumers never observe one reflecting a newer measurement than the\n // other.\n const commit = useCallback(() => {\n const next = latestRef.current;\n\n if (\n committedRef.current.width === next.width &&\n committedRef.current.height === next.height\n ) {\n return;\n }\n\n committedRef.current = next;\n setSize(next);\n\n const nextBreakpoint = getBreakpointInfo(next.width);\n setBreakpoint(prev =>\n breakpointEqual(prev, nextBreakpoint) ? prev : nextBreakpoint,\n );\n }, []);\n\n // Manual invocation only (`autoInvoke: false`) — reuses useDebounce's\n // debounced-callback machinery instead of duplicating it here.\n const debouncedCommit = useDebounce(commit, { delay, autoInvoke: false });\n\n // Measure synchronously before paint so the very first render reflects\n // the real size/breakpoint instead of the `{0,0}`/`xs` placeholder —\n // only later updates (from the observer/listener below) go through the\n // debounced path.\n useLayoutEffect(() => {\n const measured = viewport\n ? measureViewport()\n : (() => {\n const target = container ?? element ?? document.body;\n return target ? measureTarget(target) : null;\n })();\n\n if (!measured) {\n return;\n }\n\n latestRef.current = measured;\n commit();\n }, [container, element, viewport, commit]);\n\n useEffect(() => {\n if (viewport) {\n const onResize = () => {\n latestRef.current = measureViewport();\n debouncedCommit();\n };\n\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }\n\n const target = container ?? element ?? document.body;\n\n if (!target) {\n return;\n }\n\n if (observerRef.current) {\n observerRef.current.disconnect();\n }\n\n observerRef.current = new ResizeObserver(() => {\n requestAnimationFrame(() => {\n latestRef.current = measureTarget(target);\n debouncedCommit();\n });\n });\n\n observerRef.current.observe(target);\n\n return () => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n };\n }, [container, element, viewport, debouncedCommit]);\n\n return {\n size,\n breakpoint,\n ref,\n };\n};\n\nexport default useResponsiveSize;\n"],"mappings":";;;;AAgCA,MAAM,cAAc;CAClB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACR;AAED,MAAM,qBAAqB,UAAkC;CAC3D,IAAI,UAAsB;AAE1B,KAAI,SAAS,YAAY,OACvB,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;KAEV,WAAU;AAGZ,QAAO;EACL;EACA,IAAI,QAAQ,YAAY;EACxB,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,OAAO,SAAS,YAAY;EAC7B;;AAGH,MAAM,mBAAmB,GAAmB,MAC1C,EAAE,YAAY,EAAE,WAChB,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,WAAW,EAAE;AAEjB,MAAM,iBAAiB,YAAyB;CAC9C,OAAO,OAAO;CACd,QAAQ,OAAO;CAChB;AAED,MAAM,yBAAyB;CAC7B,OAAO,OAAO;CACd,QAAQ,OAAO;CAChB;AAED,MAAM,qBAA4C,YAAsB;CACtE,MAAM,EAAE,QAAQ,KAAK,WAAW,WAAW,UAAU,WAAW,EAAE;CAElE,MAAM,CAAC,SAAS,cAAc,SAAmB,KAAK;CACtD,MAAM,CAAC,MAAM,WAAW,SAAS;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;CACzD,MAAM,CAAC,YAAY,iBAAiB,eAClC,kBAAkB,EAAE,CACrB;CAED,MAAM,cAAc,OAA8B,KAAK;CACvD,MAAM,YAAY,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;CACjD,MAAM,eAAe,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;CAEpD,MAAM,MAAM,aAAa,SAAmB;AAC1C,aAAW,KAAK;IACf,EAAE,CAAC;CAKN,MAAM,SAAS,kBAAkB;EAC/B,MAAM,OAAO,UAAU;AAEvB,MACE,aAAa,QAAQ,UAAU,KAAK,SACpC,aAAa,QAAQ,WAAW,KAAK,OAErC;AAGF,eAAa,UAAU;AACvB,UAAQ,KAAK;EAEb,MAAM,iBAAiB,kBAAkB,KAAK,MAAM;AACpD,iBAAc,SACZ,gBAAgB,MAAM,eAAe,GAAG,OAAO,eAChD;IACA,EAAE,CAAC;CAIN,MAAM,kBAAkB,YAAY,QAAQ;EAAE;EAAO,YAAY;EAAO,CAAC;AAMzE,uBAAsB;EACpB,MAAM,WAAW,WACb,iBAAiB,UACV;GACL,MAAM,SAAS,aAAa,WAAW,SAAS;AAChD,UAAO,SAAS,cAAc,OAAO,GAAG;MACtC;AAER,MAAI,CAAC,SACH;AAGF,YAAU,UAAU;AACpB,UAAQ;IACP;EAAC;EAAW;EAAS;EAAU;EAAO,CAAC;AAE1C,iBAAgB;AACd,MAAI,UAAU;GACZ,MAAM,iBAAiB;AACrB,cAAU,UAAU,iBAAiB;AACrC,qBAAiB;;AAGnB,UAAO,iBAAiB,UAAU,SAAS;AAC3C,gBAAa,OAAO,oBAAoB,UAAU,SAAS;;EAG7D,MAAM,SAAS,aAAa,WAAW,SAAS;AAEhD,MAAI,CAAC,OACH;AAGF,MAAI,YAAY,QACd,aAAY,QAAQ,YAAY;AAGlC,cAAY,UAAU,IAAI,qBAAqB;AAC7C,+BAA4B;AAC1B,cAAU,UAAU,cAAc,OAAO;AACzC,qBAAiB;KACjB;IACF;AAEF,cAAY,QAAQ,QAAQ,OAAO;AAEnC,eAAa;AACX,eAAY,SAAS,YAAY;AACjC,eAAY,UAAU;;IAEvB;EAAC;EAAW;EAAS;EAAU;EAAgB,CAAC;AAEnD,QAAO;EACL;EACA;EACA;EACD"}
1
+ {"version":3,"file":"use-responsive-size.mjs","names":[],"sources":["../../src/hooks/use-responsive-size.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\n\nimport useDebouncedCallback from './use-debounced-callback';\n\ntype Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\ninterface BreakpointInfo {\n current: Breakpoint;\n xs: boolean;\n sm: boolean;\n md: boolean;\n lg: boolean;\n xl: boolean;\n '2xl': boolean;\n}\n\ninterface Options {\n delay?: number;\n container?: HTMLElement | null;\n // Measure the viewport (window.innerWidth/innerHeight) instead of an\n // element/document.body. Useful when no `container`/ref is attached and\n // the breakpoint should reflect the viewport rather than document.body's\n // box, which can diverge from it if body has margin/transform.\n viewport?: boolean;\n}\n\nconst BREAKPOINTS = {\n xs: 0, // < 640px\n sm: 640, // >= 640px\n md: 768, // >= 768px\n lg: 1024, // >= 1024px\n xl: 1280, // >= 1280px\n '2xl': 1536, // >= 1536px\n} as const;\n\nconst getBreakpointInfo = (width: number): BreakpointInfo => {\n let current: Breakpoint = 'xs';\n\n if (width >= BREAKPOINTS['2xl']) {\n current = '2xl';\n } else if (width >= BREAKPOINTS.xl) {\n current = 'xl';\n } else if (width >= BREAKPOINTS.lg) {\n current = 'lg';\n } else if (width >= BREAKPOINTS.md) {\n current = 'md';\n } else if (width >= BREAKPOINTS.sm) {\n current = 'sm';\n } else {\n current = 'xs';\n }\n\n return {\n current,\n xs: width < BREAKPOINTS.sm,\n sm: width >= BREAKPOINTS.sm && width < BREAKPOINTS.md,\n md: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,\n lg: width >= BREAKPOINTS.lg && width < BREAKPOINTS.xl,\n xl: width >= BREAKPOINTS.xl && width < BREAKPOINTS['2xl'],\n '2xl': width >= BREAKPOINTS['2xl'],\n };\n};\n\nconst breakpointEqual = (a: BreakpointInfo, b: BreakpointInfo) =>\n a.current === b.current &&\n a.xs === b.xs &&\n a.sm === b.sm &&\n a.md === b.md &&\n a.lg === b.lg &&\n a.xl === b.xl &&\n a['2xl'] === b['2xl'];\n\nconst measureTarget = (target: HTMLElement) => ({\n width: target.offsetWidth,\n height: target.offsetHeight,\n});\n\nconst measureViewport = () => ({\n width: window.innerWidth,\n height: window.innerHeight,\n});\n\nconst useResponsiveSize = <T extends HTMLElement>(options?: Options) => {\n const { delay = 100, container, viewport = false } = options || {};\n\n const [element, setElement] = useState<T | null>(null);\n const [size, setSize] = useState({ width: 0, height: 0 });\n const [breakpoint, setBreakpoint] = useState<BreakpointInfo>(() =>\n getBreakpointInfo(0),\n );\n\n const observerRef = useRef<ResizeObserver | null>(null);\n const latestRef = useRef({ width: 0, height: 0 });\n const committedRef = useRef({ width: 0, height: 0 });\n\n const ref = useCallback((node: T | null) => {\n setElement(node);\n }, []);\n\n // `size` and `breakpoint` are always committed together here, so\n // consumers never observe one reflecting a newer measurement than the\n // other.\n const commit = useCallback(() => {\n const next = latestRef.current;\n\n if (\n committedRef.current.width === next.width &&\n committedRef.current.height === next.height\n ) {\n return;\n }\n\n committedRef.current = next;\n setSize(next);\n\n const nextBreakpoint = getBreakpointInfo(next.width);\n setBreakpoint(prev =>\n breakpointEqual(prev, nextBreakpoint) ? prev : nextBreakpoint,\n );\n }, []);\n\n // Manual invocation only (`autoInvoke: false`) — reuses\n // useDebouncedCallback's machinery instead of duplicating it here.\n const debouncedCommit = useDebouncedCallback(commit, {\n delay,\n autoInvoke: false,\n });\n\n // Measure synchronously before paint so the very first render reflects\n // the real size/breakpoint instead of the `{0,0}`/`xs` placeholder —\n // only later updates (from the observer/listener below) go through the\n // debounced path.\n useLayoutEffect(() => {\n const measured = viewport\n ? measureViewport()\n : (() => {\n const target = container ?? element ?? document.body;\n return target ? measureTarget(target) : null;\n })();\n\n if (!measured) {\n return;\n }\n\n latestRef.current = measured;\n commit();\n }, [container, element, viewport, commit]);\n\n useEffect(() => {\n if (viewport) {\n const onResize = () => {\n latestRef.current = measureViewport();\n debouncedCommit();\n };\n\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }\n\n const target = container ?? element ?? document.body;\n\n if (!target) {\n return;\n }\n\n if (observerRef.current) {\n observerRef.current.disconnect();\n }\n\n observerRef.current = new ResizeObserver(() => {\n requestAnimationFrame(() => {\n latestRef.current = measureTarget(target);\n debouncedCommit();\n });\n });\n\n observerRef.current.observe(target);\n\n return () => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n };\n }, [container, element, viewport, debouncedCommit]);\n\n return {\n size,\n breakpoint,\n ref,\n };\n};\n\nexport default useResponsiveSize;\n"],"mappings":";;;;AAgCA,MAAM,cAAc;CAClB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACR;AAED,MAAM,qBAAqB,UAAkC;CAC3D,IAAI,UAAsB;AAE1B,KAAI,SAAS,YAAY,OACvB,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;UACD,SAAS,YAAY,GAC9B,WAAU;KAEV,WAAU;AAGZ,QAAO;EACL;EACA,IAAI,QAAQ,YAAY;EACxB,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,IAAI,SAAS,YAAY,MAAM,QAAQ,YAAY;EACnD,OAAO,SAAS,YAAY;EAC7B;;AAGH,MAAM,mBAAmB,GAAmB,MAC1C,EAAE,YAAY,EAAE,WAChB,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,OAAO,EAAE,MACX,EAAE,WAAW,EAAE;AAEjB,MAAM,iBAAiB,YAAyB;CAC9C,OAAO,OAAO;CACd,QAAQ,OAAO;CAChB;AAED,MAAM,yBAAyB;CAC7B,OAAO,OAAO;CACd,QAAQ,OAAO;CAChB;AAED,MAAM,qBAA4C,YAAsB;CACtE,MAAM,EAAE,QAAQ,KAAK,WAAW,WAAW,UAAU,WAAW,EAAE;CAElE,MAAM,CAAC,SAAS,cAAc,SAAmB,KAAK;CACtD,MAAM,CAAC,MAAM,WAAW,SAAS;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;CACzD,MAAM,CAAC,YAAY,iBAAiB,eAClC,kBAAkB,EAAE,CACrB;CAED,MAAM,cAAc,OAA8B,KAAK;CACvD,MAAM,YAAY,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;CACjD,MAAM,eAAe,OAAO;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;CAEpD,MAAM,MAAM,aAAa,SAAmB;AAC1C,aAAW,KAAK;IACf,EAAE,CAAC;CAKN,MAAM,SAAS,kBAAkB;EAC/B,MAAM,OAAO,UAAU;AAEvB,MACE,aAAa,QAAQ,UAAU,KAAK,SACpC,aAAa,QAAQ,WAAW,KAAK,OAErC;AAGF,eAAa,UAAU;AACvB,UAAQ,KAAK;EAEb,MAAM,iBAAiB,kBAAkB,KAAK,MAAM;AACpD,iBAAc,SACZ,gBAAgB,MAAM,eAAe,GAAG,OAAO,eAChD;IACA,EAAE,CAAC;CAIN,MAAM,kBAAkB,qBAAqB,QAAQ;EACnD;EACA,YAAY;EACb,CAAC;AAMF,uBAAsB;EACpB,MAAM,WAAW,WACb,iBAAiB,UACV;GACL,MAAM,SAAS,aAAa,WAAW,SAAS;AAChD,UAAO,SAAS,cAAc,OAAO,GAAG;MACtC;AAER,MAAI,CAAC,SACH;AAGF,YAAU,UAAU;AACpB,UAAQ;IACP;EAAC;EAAW;EAAS;EAAU;EAAO,CAAC;AAE1C,iBAAgB;AACd,MAAI,UAAU;GACZ,MAAM,iBAAiB;AACrB,cAAU,UAAU,iBAAiB;AACrC,qBAAiB;;AAGnB,UAAO,iBAAiB,UAAU,SAAS;AAC3C,gBAAa,OAAO,oBAAoB,UAAU,SAAS;;EAG7D,MAAM,SAAS,aAAa,WAAW,SAAS;AAEhD,MAAI,CAAC,OACH;AAGF,MAAI,YAAY,QACd,aAAY,QAAQ,YAAY;AAGlC,cAAY,UAAU,IAAI,qBAAqB;AAC7C,+BAA4B;AAC1B,cAAU,UAAU,cAAc,OAAO;AACzC,qBAAiB;KACjB;IACF;AAEF,cAAY,QAAQ,QAAQ,OAAO;AAEnC,eAAa;AACX,eAAY,SAAS,YAAY;AACjC,eAAY,UAAU;;IAEvB;EAAC;EAAW;EAAS;EAAU;EAAgB,CAAC;AAEnD,QAAO;EACL;EACA;EACA;EACD"}
@@ -1,13 +1,15 @@
1
- import * as react from "react";
1
+ import { RefObject } from "react";
2
2
 
3
3
  //#region src/hooks/use-scroll-to-elements.d.ts
4
+ type ElementKey = string;
5
+ type ScrollContainer = HTMLElement | RefObject<HTMLElement | null> | null;
4
6
  interface Options extends ScrollIntoViewOptions {
5
7
  offset?: number;
8
+ container?: ScrollContainer;
6
9
  }
7
- declare const useScrollToElements: (options?: Options) => {
8
- elementRefs: react.RefObject<(HTMLElement | null)[]>;
9
- setElementRef: (element: HTMLElement | null, index: number) => void;
10
- scrollToElement: (index: number) => void;
10
+ declare const useScrollToElements: (defaultOptions?: Options) => {
11
+ register: (key: ElementKey) => (node: HTMLElement | null) => void;
12
+ scrollTo: (key: ElementKey, options?: Options) => void;
11
13
  };
12
14
  //#endregion
13
15
  export { useScrollToElements };