@jbpark/use-hooks 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +22 -10
- package/README.md +24 -12
- package/dist/hooks/index.d.mts +31 -30
- package/dist/hooks/index.mjs +1 -2
- package/dist/hooks/use-body-scroll-lock.d.mts +1 -1
- package/dist/hooks/use-body-scroll-lock.mjs +1 -5
- package/dist/hooks/use-body-scroll-lock.mjs.map +1 -1
- package/dist/hooks/use-click-outside.d.mts +1 -2
- package/dist/hooks/use-click-outside.mjs +3 -3
- package/dist/hooks/use-click-outside.mjs.map +1 -1
- package/dist/hooks/use-controllable-state.d.mts +1 -1
- package/dist/hooks/use-controllable-state.mjs +1 -2
- package/dist/hooks/use-controllable-state.mjs.map +1 -1
- package/dist/hooks/use-debounced-callback.d.mts +2 -6
- package/dist/hooks/use-debounced-callback.mjs +9 -9
- package/dist/hooks/use-debounced-callback.mjs.map +1 -1
- package/dist/hooks/use-debounced-value.d.mts +1 -1
- package/dist/hooks/use-debounced-value.mjs +1 -2
- package/dist/hooks/use-debounced-value.mjs.map +1 -1
- package/dist/hooks/use-element-position.d.mts +1 -2
- package/dist/hooks/use-element-position.mjs +1 -2
- package/dist/hooks/use-element-position.mjs.map +1 -1
- package/dist/hooks/use-element-scroll.d.mts +2 -4
- package/dist/hooks/use-element-scroll.mjs +3 -3
- package/dist/hooks/use-element-scroll.mjs.map +1 -1
- package/dist/hooks/use-event-listener.d.mts +1 -2
- package/dist/hooks/use-event-listener.mjs +1 -2
- package/dist/hooks/use-event-listener.mjs.map +1 -1
- package/dist/hooks/use-file-drop.d.mts +1 -1
- package/dist/hooks/use-file-drop.mjs +7 -6
- package/dist/hooks/use-file-drop.mjs.map +1 -1
- package/dist/hooks/use-file-to-data-url.d.mts +1 -1
- package/dist/hooks/use-file-to-data-url.mjs +1 -2
- package/dist/hooks/use-file-to-data-url.mjs.map +1 -1
- package/dist/hooks/use-history-state.d.mts +1 -1
- package/dist/hooks/use-history-state.mjs +3 -3
- package/dist/hooks/use-history-state.mjs.map +1 -1
- package/dist/hooks/use-image.d.mts +1 -1
- package/dist/hooks/use-image.mjs +24 -8
- package/dist/hooks/use-image.mjs.map +1 -1
- package/dist/hooks/use-intersection-observer.d.mts +1 -1
- package/dist/hooks/use-intersection-observer.mjs +1 -2
- package/dist/hooks/use-intersection-observer.mjs.map +1 -1
- package/dist/hooks/use-interval.d.mts +1 -1
- package/dist/hooks/use-interval.mjs +1 -2
- package/dist/hooks/use-interval.mjs.map +1 -1
- package/dist/hooks/use-key-press.d.mts +1 -2
- package/dist/hooks/use-key-press.mjs +7 -5
- package/dist/hooks/use-key-press.mjs.map +1 -1
- package/dist/hooks/use-local-storage.d.mts +1 -1
- package/dist/hooks/use-local-storage.mjs +5 -3
- package/dist/hooks/use-local-storage.mjs.map +1 -1
- package/dist/hooks/use-merged-ref.d.mts +1 -2
- package/dist/hooks/use-merged-ref.mjs +1 -2
- package/dist/hooks/use-merged-ref.mjs.map +1 -1
- package/dist/hooks/use-multi-select.d.mts +1 -1
- package/dist/hooks/use-multi-select.mjs +10 -9
- package/dist/hooks/use-multi-select.mjs.map +1 -1
- package/dist/hooks/use-mutation-observer.d.mts +1 -2
- package/dist/hooks/use-mutation-observer.mjs +3 -3
- package/dist/hooks/use-mutation-observer.mjs.map +1 -1
- package/dist/hooks/use-previous.d.mts +1 -1
- package/dist/hooks/use-previous.mjs +1 -2
- package/dist/hooks/use-previous.mjs.map +1 -1
- package/dist/hooks/use-recursive-timeout.d.mts +1 -1
- package/dist/hooks/use-recursive-timeout.mjs +1 -2
- package/dist/hooks/use-recursive-timeout.mjs.map +1 -1
- package/dist/hooks/use-resize-observer.d.mts +1 -1
- package/dist/hooks/use-resize-observer.mjs +1 -2
- package/dist/hooks/use-resize-observer.mjs.map +1 -1
- package/dist/hooks/use-responsive-size.d.mts +1 -1
- package/dist/hooks/use-responsive-size.mjs +1 -2
- package/dist/hooks/use-responsive-size.mjs.map +1 -1
- package/dist/hooks/use-scroll-to-elements.d.mts +1 -2
- package/dist/hooks/use-scroll-to-elements.mjs +14 -3
- package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
- package/dist/hooks/use-throttled-callback.d.mts +2 -5
- package/dist/hooks/use-throttled-callback.mjs +6 -8
- package/dist/hooks/use-throttled-callback.mjs.map +1 -1
- package/dist/hooks/use-throttled-value.d.mts +1 -1
- package/dist/hooks/use-throttled-value.mjs +1 -2
- package/dist/hooks/use-throttled-value.mjs.map +1 -1
- package/dist/hooks/use-timeout.d.mts +1 -1
- package/dist/hooks/use-timeout.mjs +1 -2
- package/dist/hooks/use-timeout.mjs.map +1 -1
- package/dist/hooks/use-toggle.d.mts +1 -1
- package/dist/hooks/use-toggle.mjs +1 -2
- package/dist/hooks/use-toggle.mjs.map +1 -1
- package/dist/hooks/use-viewport.d.mts +1 -1
- package/dist/hooks/use-viewport.mjs +1 -2
- package/dist/hooks/use-viewport.mjs.map +1 -1
- package/dist/hooks/use-window-scroll.d.mts +1 -2
- package/dist/hooks/use-window-scroll.mjs +1 -2
- package/dist/hooks/use-window-scroll.mjs.map +1 -1
- package/dist/index.d.mts +30 -30
- package/dist/index.mjs +1 -2
- package/package.json +21 -30
|
@@ -1 +1 @@
|
|
|
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":"
|
|
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;CAC3D,IAAI,MAAM,GACR,OAAO;CAET,IAAI,CAAC,KAAK,CAAC,GACT,OAAO;CAET,OACE,EAAE,MAAM,EAAE,KACV,EAAE,MAAM,EAAE,KACV,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,QAAQ,EAAE,OACZ,EAAE,SAAS,EAAE;AAEjB;AAEA,MAAM,sBAAyB,eAAoC;CACjE,MAAM,CAAC,MAAM,WAAW,SAAyB,IAAI;CACrD,MAAM,UAAU,OAAuB,IAAI;CAE3C,gBAAgB;EACd,MAAM,cAAc,QAAuC;GACzD,IAAI,OAAO,QAAQ,UACjB,OAAO,SAAS,cAAc,GAAG;GAEnC,OAAO,IAAI;EACb;EAEA,IAAI;EACJ,IAAI,iBAAqC;EAEzC,MAAM,cAAc,SAAyB;GAC3C,IAAI,WAAW,QAAQ,SAAS,IAAI,GAClC;GAEF,QAAQ,UAAU;GAClB,QAAQ,IAAI;EACd;EAMA,MAAM,iBACJ,OAAO,mBAAmB,cACtB,IAAI,qBAAqB,WAAW,CAAC,IACrC,KAAA;EAEN,SAAS,aAAa;GACpB,MAAM,UAAU,WAAW,UAAU;GAErC,IAAI,YAAY,gBAAgB;IAC9B,IAAI,gBACF,gBAAgB,UAAU,cAAc;IAE1C,IAAI,SACF,gBAAgB,QAAQ,OAAO;IAEjC,iBAAiB;GACnB;GAEA,WAAW,UAAU,QAAQ,sBAAsB,IAAI,IAAI;EAC7D;EAEA,MAAM,iBAAiB;GACrB,QAAQ,sBAAsB,UAAU;EAC1C;EAEA,WAAW;EAKX,OAAO,iBAAiB,UAAU,UAAU;GAC1C,SAAS;GACT,SAAS;EACX,CAAC;EACD,OAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;EAK7D,MAAM,mBACJ,OAAO,eAAe,YAAY,OAAO,qBAAqB,cAC1D,IAAI,iBAAiB,QAAQ,IAC7B,KAAA;EACN,kBAAkB,QAAQ,SAAS,MAAM;GACvC,WAAW;GACX,SAAS;EACX,CAAC;EAED,aAAa;GACX,IAAI,UAAU,KAAA,GACZ,qBAAqB,KAAK;GAE5B,OAAO,oBAAoB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;GAChE,OAAO,oBAAoB,UAAU,QAAQ;GAC7C,gBAAgB,WAAW;GAC3B,kBAAkB,WAAW;EAC/B;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,OAAO;AACT"}
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
interface Options {
|
|
3
3
|
threshold?: number;
|
|
4
4
|
}
|
|
5
|
-
declare const useElementScroll: ({
|
|
6
|
-
threshold
|
|
7
|
-
}?: Options) => {
|
|
5
|
+
declare const useElementScroll: ({ threshold }?: Options) => {
|
|
8
6
|
element: HTMLElement | null;
|
|
9
7
|
setRef: (el: HTMLElement | null) => void;
|
|
10
8
|
scrollY: number;
|
|
@@ -16,5 +14,5 @@ declare const useElementScroll: ({
|
|
|
16
14
|
scrollHeight: number;
|
|
17
15
|
};
|
|
18
16
|
//#endregion
|
|
19
|
-
export { useElementScroll };
|
|
17
|
+
export { useElementScroll as default };
|
|
20
18
|
//# sourceMappingURL=use-element-scroll.d.mts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-element-scroll.ts
|
|
4
3
|
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
4
|
const useElementScroll = ({ threshold = 1 } = {}) => {
|
|
@@ -39,9 +38,10 @@ const useElementScroll = ({ threshold = 1 } = {}) => {
|
|
|
39
38
|
});
|
|
40
39
|
return;
|
|
41
40
|
}
|
|
41
|
+
const percentage = Math.min(100, Math.max(0, scrollTop / scrollableHeight * 100));
|
|
42
42
|
commit({
|
|
43
43
|
scrollY: scrollTop,
|
|
44
|
-
scrollPercentage:
|
|
44
|
+
scrollPercentage: percentage,
|
|
45
45
|
isAtTop: scrollTop <= 0,
|
|
46
46
|
isAtBottom: scrollTop >= scrollableHeight - threshold,
|
|
47
47
|
scrollableHeight,
|
|
@@ -84,7 +84,7 @@ const useElementScroll = ({ threshold = 1 } = {}) => {
|
|
|
84
84
|
setRef
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
|
-
|
|
88
87
|
//#endregion
|
|
89
88
|
export { useElementScroll as default };
|
|
89
|
+
|
|
90
90
|
//# sourceMappingURL=use-element-scroll.mjs.map
|
|
@@ -1 +1 @@
|
|
|
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":"
|
|
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,CAAC,MAAM;CAC5D,MAAM,CAAC,SAAS,cAAc,SAA6B,IAAI;CAC/D,MAAM,CAAC,gBAAgB,qBAAqB,SAAyB;EACnE,SAAS;EACT,kBAAkB;EAClB,SAAS;EACT,YAAY;EACZ,kBAAkB;EAClB,cAAc;EACd,cAAc;CAChB,CAAC;CACD,MAAM,oBAAoB,OAAO,cAAc;CAE/C,MAAM,SAAS,aAAa,OAA2B;EACrD,WAAW,EAAE;CACf,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,CAAC,SACH;EAGF,MAAM,UAAU,SAAyB;GACvC,IAAI,oBAAoB,kBAAkB,SAAS,IAAI,GACrD;GAEF,kBAAkB,UAAU;GAC5B,kBAAkB,IAAI;EACxB;EAEA,MAAM,6BAA6B;GACjC,MAAM,EAAE,WAAW,cAAc,iBAAiB;GAClD,MAAM,mBAAmB,eAAe;GAExC,IAAI,oBAAoB,GAAG;IACzB,OAAO;KACL,SAAS;KACT,kBAAkB;KAClB,SAAS;KACT,YAAY;KACZ,kBAAkB;KAClB;KACA;IACF,CAAC;IACD;GACF;GAEA,MAAM,aAAa,KAAK,IACtB,KACA,KAAK,IAAI,GAAI,YAAY,mBAAoB,GAAG,CAClD;GAEA,OAAO;IACL,SAAS;IACT,kBAAkB;IAClB,SAAS,aAAa;IACtB,YAAY,aAAa,mBAAmB;IAC5C;IACA;IACA;GACF,CAAC;EACH;EAEA,qBAAqB;EAErB,MAAM,iBAAiB;GACrB,qBAAqB;EACvB;EAEA,QAAQ,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;EAE9D,MAAM,iBAAiB,IAAI,qBAAqB;GAC9C,qBAAqB;EACvB,CAAC;EAMD,MAAM,wBAAwB;GAC5B,eAAe,QAAQ,OAAO;GAC9B,MAAM,KAAK,QAAQ,QAAQ,CAAC,CAAC,SAAQ,UAAS;IAC5C,eAAe,QAAQ,KAAK;GAC9B,CAAC;EACH;EAEA,gBAAgB;EAKhB,MAAM,mBAAmB,IAAI,uBAAuB;GAClD,gBAAgB;GAChB,qBAAqB;EACvB,CAAC;EACD,iBAAiB,QAAQ,SAAS;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC;EAEpE,aAAa;GACX,QAAQ,oBAAoB,UAAU,QAAQ;GAC9C,eAAe,WAAW;GAC1B,iBAAiB,WAAW;EAC9B;CACF,GAAG,CAAC,SAAS,SAAS,CAAC;CAEvB,OAAO;EAAE,GAAG;EAAgB;EAAS;CAAO;AAC9C"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { RefObject } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-event-listener.d.ts
|
|
4
3
|
interface Options {
|
|
5
4
|
capture?: boolean;
|
|
@@ -17,5 +16,5 @@ declare function useEventListener<T extends HTMLElement, K extends keyof HTMLEle
|
|
|
17
16
|
target: RefObject<T | null>;
|
|
18
17
|
}): void;
|
|
19
18
|
//#endregion
|
|
20
|
-
export { useEventListener };
|
|
19
|
+
export { useEventListener as default };
|
|
21
20
|
//# sourceMappingURL=use-event-listener.d.mts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-event-listener.ts
|
|
4
3
|
function useEventListener(type, handler, options = {}) {
|
|
5
4
|
const { target, capture, passive, once, enabled = true } = options;
|
|
@@ -44,7 +43,7 @@ function useEventListener(type, handler, options = {}) {
|
|
|
44
43
|
enabled
|
|
45
44
|
]);
|
|
46
45
|
}
|
|
47
|
-
|
|
48
46
|
//#endregion
|
|
49
47
|
export { useEventListener as default };
|
|
48
|
+
|
|
50
49
|
//# sourceMappingURL=use-event-listener.mjs.map
|
|
@@ -1 +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 let cancelled = false;\n let rafId: number | undefined;\n let resolvedTarget: EventTarget | null = null;\n\n const listener = (event: Event) => {\n handlerRef.current(event);\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. A raw\n // target or the default window has nothing to wait for.\n if (target && 'current' in target && !target.current) {\n rafId = requestAnimationFrame(attach);\n return;\n }\n\n 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 resolvedTarget.addEventListener(type, listener, {\n capture,\n passive,\n once,\n });\n };\n\n attach();\n\n return () => {\n cancelled = true;\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\n resolvedTarget?.removeEventListener(type, listener, { capture });\n };\n }, [type, target, capture, passive, once, enabled]);\n}\n\nexport default useEventListener;\n"],"mappings":"
|
|
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 let cancelled = false;\n let rafId: number | undefined;\n let resolvedTarget: EventTarget | null = null;\n\n const listener = (event: Event) => {\n handlerRef.current(event);\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. A raw\n // target or the default window has nothing to wait for.\n if (target && 'current' in target && !target.current) {\n rafId = requestAnimationFrame(attach);\n return;\n }\n\n 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 resolvedTarget.addEventListener(type, listener, {\n capture,\n passive,\n once,\n });\n };\n\n attach();\n\n return () => {\n cancelled = true;\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\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,CAAC,GACvD;CACA,MAAM,EAAE,QAAQ,SAAS,SAAS,MAAM,UAAU,SAAS;CAE3D,MAAM,aAAa,OAAO,OAAO;CAEjC,gBAAgB;EACd,WAAW,UAAU;CACvB,CAAC;CAED,gBAAgB;EACd,IAAI,CAAC,SACH;EAGF,IAAI,YAAY;EAChB,IAAI;EACJ,IAAI,iBAAqC;EAEzC,MAAM,YAAY,UAAiB;GACjC,WAAW,QAAQ,KAAK;EAC1B;EAEA,MAAM,eAAe;GACnB,IAAI,WACF;GAOF,IAAI,UAAU,aAAa,UAAU,CAAC,OAAO,SAAS;IACpD,QAAQ,sBAAsB,MAAM;IACpC;GACF;GAEA,iBACE,UAAU,aAAa,SACnB,OAAO,UACN,WAAW,OAAO,WAAW,cAAc,OAAO;GAEzD,IAAI,CAAC,gBACH;GAGF,eAAe,iBAAiB,MAAM,UAAU;IAC9C;IACA;IACA;GACF,CAAC;EACH;EAEA,OAAO;EAEP,aAAa;GACX,YAAY;GACZ,IAAI,UAAU,KAAA,GACZ,qBAAqB,KAAK;GAE5B,gBAAgB,oBAAoB,MAAM,UAAU,EAAE,QAAQ,CAAC;EACjE;CACF,GAAG;EAAC;EAAM;EAAQ;EAAS;EAAS;EAAM;CAAO,CAAC;AACpD"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-file-drop.ts
|
|
4
3
|
const matchesAccept = (file, accept) => {
|
|
5
4
|
if (!accept) return true;
|
|
@@ -25,11 +24,13 @@ const useFileDrop = (options = {}) => {
|
|
|
25
24
|
multipleRef.current = multiple;
|
|
26
25
|
disabledRef.current = disabled;
|
|
27
26
|
});
|
|
27
|
+
const [prevDisabled, setPrevDisabled] = useState(disabled);
|
|
28
|
+
if (disabled !== prevDisabled) {
|
|
29
|
+
setPrevDisabled(disabled);
|
|
30
|
+
if (disabled) setIsDragging(false);
|
|
31
|
+
}
|
|
28
32
|
useEffect(() => {
|
|
29
|
-
if (disabled)
|
|
30
|
-
dragCounterRef.current = 0;
|
|
31
|
-
setIsDragging(false);
|
|
32
|
-
}
|
|
33
|
+
if (disabled) dragCounterRef.current = 0;
|
|
33
34
|
}, [disabled]);
|
|
34
35
|
const cleanupRef = useRef(null);
|
|
35
36
|
return {
|
|
@@ -79,7 +80,7 @@ const useFileDrop = (options = {}) => {
|
|
|
79
80
|
isDragging
|
|
80
81
|
};
|
|
81
82
|
};
|
|
82
|
-
|
|
83
83
|
//#endregion
|
|
84
84
|
export { useFileDrop as default };
|
|
85
|
+
|
|
85
86
|
//# sourceMappingURL=use-file-drop.mjs.map
|
|
@@ -1 +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
|
|
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 const [prevDisabled, setPrevDisabled] = useState(disabled);\n\n // Adjusted directly in render (React's \"adjust state during render\"\n // pattern) instead of an effect, so `isDragging` clears in the same\n // render `disabled` flips true rather than the render after. The\n // counter itself is a ref, not render output, so it's zeroed separately\n // below — refs may only be written in effects/handlers, never in render.\n if (disabled !== prevDisabled) {\n setPrevDisabled(disabled);\n\n if (disabled) {\n setIsDragging(false);\n }\n }\n\n useEffect(() => {\n if (disabled) {\n dragCounterRef.current = 0;\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;CAC9D,IAAI,CAAC,QACH,OAAO;CAGT,MAAM,WAAW,OACd,MAAM,GAAG,CAAC,CACV,KAAI,YAAW,QAAQ,KAAK,CAAC,CAAC,CAC9B,OAAO,OAAO;CAEjB,IAAI,SAAS,WAAW,GACtB,OAAO;CAGT,OAAO,SAAS,MAAK,YAAW;EAC9B,IAAI,QAAQ,WAAW,GAAG,GACxB,OAAO,KAAK,KAAK,YAAY,CAAC,CAAC,SAAS,QAAQ,YAAY,CAAC;EAG/D,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO,KAAK,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;EAGlD,OAAO,KAAK,SAAS;CACvB,CAAC;AACH;AAKA,MAAM,eACJ,UAAmB,CAAC,MACjB;CACH,MAAM,EAAE,QAAQ,QAAQ,WAAW,MAAM,WAAW,UAAU;CAE9D,MAAM,CAAC,YAAY,iBAAiB,SAAS,KAAK;CAMlD,MAAM,iBAAiB,OAAO,CAAC;CAE/B,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,cAAc,OAAO,QAAQ;CACnC,MAAM,cAAc,OAAO,QAAQ;CAEnC,gBAAgB;EACd,UAAU,UAAU;EACpB,UAAU,UAAU;EACpB,YAAY,UAAU;EACtB,YAAY,UAAU;CACxB,CAAC;CAED,MAAM,CAAC,cAAc,mBAAmB,SAAS,QAAQ;CAOzD,IAAI,aAAa,cAAc;EAC7B,gBAAgB,QAAQ;EAExB,IAAI,UACF,cAAc,KAAK;CAEvB;CAEA,gBAAgB;EACd,IAAI,UACF,eAAe,UAAU;CAE7B,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,aAAa,OAA4B,IAAI;CAkFnD,OAAO;EAAE,SAhFO,aAAa,SAAmB;GAC9C,WAAW,UAAU;GACrB,WAAW,UAAU;GAErB,IAAI,CAAC,MACH;GAGF,MAAM,eAAe,UAAqB;IACxC,IAAI,YAAY,SACd;IAEF,MAAM,eAAe;IACrB,eAAe,WAAW;IAC1B,IAAI,eAAe,YAAY,GAC7B,cAAc,IAAI;GAEtB;GAEA,MAAM,eAAe,UAAqB;IACxC,IAAI,YAAY,SACd;IAEF,MAAM,eAAe;IACrB,eAAe,UAAU,KAAK,IAAI,GAAG,eAAe,UAAU,CAAC;IAC/D,IAAI,eAAe,YAAY,GAC7B,cAAc,KAAK;GAEvB;GAIA,MAAM,cAAc,UAAqB;IACvC,IAAI,YAAY,SACd;IAEF,MAAM,eAAe;GACvB;GAEA,MAAM,UAAU,UAAqB;IACnC,IAAI,YAAY,SACd;IAEF,MAAM,eAAe;IACrB,eAAe,UAAU;IACzB,cAAc,KAAK;IAEnB,MAAM,WAAW,MAAM,cAAc;IAErC,IAAI,CAAC,YAAY,SAAS,WAAW,GACnC;IAGF,IAAI,QAAQ,MAAM,KAAK,QAAQ,CAAC,CAAC,QAAO,SACtC,cAAc,MAAM,UAAU,OAAO,CACvC;IAEA,IAAI,CAAC,YAAY,SACf,QAAQ,MAAM,MAAM,GAAG,CAAC;IAG1B,IAAI,MAAM,SAAS,GACjB,UAAU,UAAU,KAAK;GAE7B;GAEA,KAAK,iBAAiB,aAAa,WAAW;GAC9C,KAAK,iBAAiB,aAAa,WAAW;GAC9C,KAAK,iBAAiB,YAAY,UAAU;GAC5C,KAAK,iBAAiB,QAAQ,MAAM;GAEpC,WAAW,gBAAgB;IACzB,KAAK,oBAAoB,aAAa,WAAW;IACjD,KAAK,oBAAoB,aAAa,WAAW;IACjD,KAAK,oBAAoB,YAAY,UAAU;IAC/C,KAAK,oBAAoB,QAAQ,MAAM;IACvC,eAAe,UAAU;GAC3B;EACF,GAAG,CAAC,CAEW;EAAG;CAAW;AAC/B"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-file-to-data-url.ts
|
|
4
3
|
const useFileToDataUrl = () => {
|
|
5
4
|
return useCallback((file) => {
|
|
@@ -16,7 +15,7 @@ const useFileToDataUrl = () => {
|
|
|
16
15
|
});
|
|
17
16
|
}, []);
|
|
18
17
|
};
|
|
19
|
-
|
|
20
18
|
//#endregion
|
|
21
19
|
export { useFileToDataUrl as default };
|
|
20
|
+
|
|
22
21
|
//# sourceMappingURL=use-file-to-data-url.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-file-to-data-url.mjs","names":[],"sources":["../../src/hooks/use-file-to-data-url.ts"],"sourcesContent":["import { useCallback } from 'react';\n\nconst useFileToDataUrl = () => {\n const readAsDataUrl = useCallback((file: File): Promise<string> => {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n\n reader.onload = () => {\n if (typeof reader.result === 'string') {\n resolve(reader.result);\n } else {\n reject(new Error('Failed to read file as a data URL.'));\n }\n };\n\n reader.onerror = () => {\n reject(reader.error ?? new Error('Failed to read file.'));\n };\n\n reader.readAsDataURL(file);\n });\n }, []);\n\n return readAsDataUrl;\n};\n\nexport default useFileToDataUrl;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-file-to-data-url.mjs","names":[],"sources":["../../src/hooks/use-file-to-data-url.ts"],"sourcesContent":["import { useCallback } from 'react';\n\nconst useFileToDataUrl = () => {\n const readAsDataUrl = useCallback((file: File): Promise<string> => {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n\n reader.onload = () => {\n if (typeof reader.result === 'string') {\n resolve(reader.result);\n } else {\n reject(new Error('Failed to read file as a data URL.'));\n }\n };\n\n reader.onerror = () => {\n reject(reader.error ?? new Error('Failed to read file.'));\n };\n\n reader.readAsDataURL(file);\n });\n }, []);\n\n return readAsDataUrl;\n};\n\nexport default useFileToDataUrl;\n"],"mappings":";;AAEA,MAAM,yBAAyB;CAqB7B,OApBsB,aAAa,SAAgC;EACjE,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,SAAS,IAAI,WAAW;GAE9B,OAAO,eAAe;IACpB,IAAI,OAAO,OAAO,WAAW,UAC3B,QAAQ,OAAO,MAAM;SAErB,uBAAO,IAAI,MAAM,oCAAoC,CAAC;GAE1D;GAEA,OAAO,gBAAgB;IACrB,OAAO,OAAO,yBAAS,IAAI,MAAM,sBAAsB,CAAC;GAC1D;GAEA,OAAO,cAAc,IAAI;EAC3B,CAAC;CACH,GAAG,CAAC,CAEe;AACrB"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useReducer } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-history-state.ts
|
|
4
3
|
const takeLast = (arr, limit) => limit <= 0 ? [] : arr.slice(-limit);
|
|
5
4
|
const takeFirst = (arr, limit) => limit <= 0 ? [] : arr.slice(0, limit);
|
|
@@ -42,7 +41,8 @@ const createReducer = (limit) => (state, action) => {
|
|
|
42
41
|
};
|
|
43
42
|
const DEFAULT_LIMIT = 50;
|
|
44
43
|
const useHistoryState = (initialValue, options) => {
|
|
45
|
-
const
|
|
44
|
+
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
45
|
+
const [state, dispatch] = useReducer(createReducer(limit), {
|
|
46
46
|
past: [],
|
|
47
47
|
present: initialValue,
|
|
48
48
|
future: []
|
|
@@ -75,7 +75,7 @@ const useHistoryState = (initialValue, options) => {
|
|
|
75
75
|
canRedo: state.future.length > 0
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
|
-
|
|
79
78
|
//#endregion
|
|
80
79
|
export { useHistoryState as default };
|
|
80
|
+
|
|
81
81
|
//# sourceMappingURL=use-history-state.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-history-state.mjs","names":[],"sources":["../../src/hooks/use-history-state.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\n// `arr.slice(-limit)` breaks for `limit <= 0`: `slice(-0)` is `slice(0)`,\n// which keeps the whole array instead of truncating to nothing. Treating\n// any non-positive limit as \"keep none\" makes `limit: 0` (and negative\n// limits, previously undefined behavior) actually mean no history.\nconst takeLast = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(-limit);\n\nconst takeFirst = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(0, limit);\n\ntype Action<T> =\n | { type: 'SET'; value: T | ((prev: T) => T) }\n | { type: 'UNDO' }\n | { type: 'REDO' }\n | { type: 'RESET'; value: T };\n\nconst createReducer =\n <T>(limit: number) =>\n (state: HistoryState<T>, action: Action<T>): HistoryState<T> => {\n switch (action.type) {\n case 'SET': {\n const resolved =\n action.value instanceof Function\n ? action.value(state.present)\n : action.value;\n\n if (resolved === state.present) {\n return state;\n }\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: resolved,\n future: [],\n };\n }\n\n case 'UNDO': {\n if (state.past.length === 0) {\n return state;\n }\n\n const previous = state.past[state.past.length - 1] as T;\n\n return {\n past: state.past.slice(0, -1),\n present: previous,\n // Cap `future` at `limit` too — otherwise a long run of undos\n // (more than `limit` of them) grows `future` unbounded, even\n // though `past` is kept within `limit` by REDO/SET.\n future: takeFirst([state.present, ...state.future], limit),\n };\n }\n\n case 'REDO': {\n if (state.future.length === 0) {\n return state;\n }\n\n const [next, ...rest] = state.future;\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: next as T,\n future: rest,\n };\n }\n\n case 'RESET': {\n return { past: [], present: action.value, future: [] };\n }\n\n default: {\n return state;\n }\n }\n };\n\ninterface UseHistoryStateOptions {\n limit?: number;\n}\n\nconst DEFAULT_LIMIT = 50;\n\nconst useHistoryState = <T>(\n initialValue: T,\n options?: UseHistoryStateOptions,\n) => {\n const limit = options?.limit ?? DEFAULT_LIMIT;\n\n const [state, dispatch] = useReducer(createReducer<T>(limit), {\n past: [],\n present: initialValue,\n future: [],\n });\n\n const setValue = useCallback((value: T | ((prev: T) => T)) => {\n dispatch({ type: 'SET', value });\n }, []);\n\n const undo = useCallback(() => {\n dispatch({ type: 'UNDO' });\n }, []);\n\n const redo = useCallback(() => {\n dispatch({ type: 'REDO' });\n }, []);\n\n const reset = useCallback((value: T) => {\n dispatch({ type: 'RESET', value });\n }, []);\n\n return {\n value: state.present,\n setValue,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n } as const;\n};\n\nexport default useHistoryState;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-history-state.mjs","names":[],"sources":["../../src/hooks/use-history-state.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\n// `arr.slice(-limit)` breaks for `limit <= 0`: `slice(-0)` is `slice(0)`,\n// which keeps the whole array instead of truncating to nothing. Treating\n// any non-positive limit as \"keep none\" makes `limit: 0` (and negative\n// limits, previously undefined behavior) actually mean no history.\nconst takeLast = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(-limit);\n\nconst takeFirst = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(0, limit);\n\ntype Action<T> =\n | { type: 'SET'; value: T | ((prev: T) => T) }\n | { type: 'UNDO' }\n | { type: 'REDO' }\n | { type: 'RESET'; value: T };\n\nconst createReducer =\n <T>(limit: number) =>\n (state: HistoryState<T>, action: Action<T>): HistoryState<T> => {\n switch (action.type) {\n case 'SET': {\n const resolved =\n action.value instanceof Function\n ? action.value(state.present)\n : action.value;\n\n if (resolved === state.present) {\n return state;\n }\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: resolved,\n future: [],\n };\n }\n\n case 'UNDO': {\n if (state.past.length === 0) {\n return state;\n }\n\n const previous = state.past[state.past.length - 1] as T;\n\n return {\n past: state.past.slice(0, -1),\n present: previous,\n // Cap `future` at `limit` too — otherwise a long run of undos\n // (more than `limit` of them) grows `future` unbounded, even\n // though `past` is kept within `limit` by REDO/SET.\n future: takeFirst([state.present, ...state.future], limit),\n };\n }\n\n case 'REDO': {\n if (state.future.length === 0) {\n return state;\n }\n\n const [next, ...rest] = state.future;\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: next as T,\n future: rest,\n };\n }\n\n case 'RESET': {\n return { past: [], present: action.value, future: [] };\n }\n\n default: {\n return state;\n }\n }\n };\n\ninterface UseHistoryStateOptions {\n limit?: number;\n}\n\nconst DEFAULT_LIMIT = 50;\n\nconst useHistoryState = <T>(\n initialValue: T,\n options?: UseHistoryStateOptions,\n) => {\n const limit = options?.limit ?? DEFAULT_LIMIT;\n\n const [state, dispatch] = useReducer(createReducer<T>(limit), {\n past: [],\n present: initialValue,\n future: [],\n });\n\n const setValue = useCallback((value: T | ((prev: T) => T)) => {\n dispatch({ type: 'SET', value });\n }, []);\n\n const undo = useCallback(() => {\n dispatch({ type: 'UNDO' });\n }, []);\n\n const redo = useCallback(() => {\n dispatch({ type: 'REDO' });\n }, []);\n\n const reset = useCallback((value: T) => {\n dispatch({ type: 'RESET', value });\n }, []);\n\n return {\n value: state.present,\n setValue,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n } as const;\n};\n\nexport default useHistoryState;\n"],"mappings":";;AAYA,MAAM,YAAe,KAAU,UAC7B,SAAS,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK;AAEpC,MAAM,aAAgB,KAAU,UAC9B,SAAS,IAAI,CAAC,IAAI,IAAI,MAAM,GAAG,KAAK;AAQtC,MAAM,iBACA,WACH,OAAwB,WAAuC;CAC9D,QAAQ,OAAO,MAAf;EACE,KAAK,OAAO;GACV,MAAM,WACJ,OAAO,iBAAiB,WACpB,OAAO,MAAM,MAAM,OAAO,IAC1B,OAAO;GAEb,IAAI,aAAa,MAAM,SACrB,OAAO;GAGT,OAAO;IACL,MAAM,SAAS,CAAC,GAAG,MAAM,MAAM,MAAM,OAAO,GAAG,KAAK;IACpD,SAAS;IACT,QAAQ,CAAC;GACX;EACF;EAEA,KAAK,QAAQ;GACX,IAAI,MAAM,KAAK,WAAW,GACxB,OAAO;GAGT,MAAM,WAAW,MAAM,KAAK,MAAM,KAAK,SAAS;GAEhD,OAAO;IACL,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE;IAC5B,SAAS;IAIT,QAAQ,UAAU,CAAC,MAAM,SAAS,GAAG,MAAM,MAAM,GAAG,KAAK;GAC3D;EACF;EAEA,KAAK,QAAQ;GACX,IAAI,MAAM,OAAO,WAAW,GAC1B,OAAO;GAGT,MAAM,CAAC,MAAM,GAAG,QAAQ,MAAM;GAE9B,OAAO;IACL,MAAM,SAAS,CAAC,GAAG,MAAM,MAAM,MAAM,OAAO,GAAG,KAAK;IACpD,SAAS;IACT,QAAQ;GACV;EACF;EAEA,KAAK,SACH,OAAO;GAAE,MAAM,CAAC;GAAG,SAAS,OAAO;GAAO,QAAQ,CAAC;EAAE;EAGvD,SACE,OAAO;CAEX;AACF;AAMF,MAAM,gBAAgB;AAEtB,MAAM,mBACJ,cACA,YACG;CACH,MAAM,QAAQ,SAAS,SAAS;CAEhC,MAAM,CAAC,OAAO,YAAY,WAAW,cAAiB,KAAK,GAAG;EAC5D,MAAM,CAAC;EACP,SAAS;EACT,QAAQ,CAAC;CACX,CAAC;CAED,MAAM,WAAW,aAAa,UAAgC;EAC5D,SAAS;GAAE,MAAM;GAAO;EAAM,CAAC;CACjC,GAAG,CAAC,CAAC;CAEL,MAAM,OAAO,kBAAkB;EAC7B,SAAS,EAAE,MAAM,OAAO,CAAC;CAC3B,GAAG,CAAC,CAAC;CAEL,MAAM,OAAO,kBAAkB;EAC7B,SAAS,EAAE,MAAM,OAAO,CAAC;CAC3B,GAAG,CAAC,CAAC;CAEL,MAAM,QAAQ,aAAa,UAAa;EACtC,SAAS;GAAE,MAAM;GAAS;EAAM,CAAC;CACnC,GAAG,CAAC,CAAC;CAEL,OAAO;EACL,OAAO,MAAM;EACb;EACA;EACA;EACA;EACA,SAAS,MAAM,KAAK,SAAS;EAC7B,SAAS,MAAM,OAAO,SAAS;CACjC;AACF"}
|
package/dist/hooks/use-image.mjs
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-image.ts
|
|
4
3
|
const useImage = (src, options = {}) => {
|
|
5
4
|
const { retryCount = 0, retryDelay = 1e3 } = options;
|
|
6
|
-
const [loading, setLoading] = useState(
|
|
5
|
+
const [loading, setLoading] = useState(() => Boolean(src));
|
|
7
6
|
const [error, setError] = useState(null);
|
|
8
7
|
const [loaded, setLoaded] = useState(false);
|
|
9
8
|
const [attemptCount, setAttemptCount] = useState(0);
|
|
10
9
|
const [reloadToken, setReloadToken] = useState(0);
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const [prevTrigger, setPrevTrigger] = useState({
|
|
11
|
+
src,
|
|
12
|
+
attemptCount,
|
|
13
|
+
retryCount,
|
|
14
|
+
retryDelay,
|
|
15
|
+
reloadToken
|
|
16
|
+
});
|
|
17
|
+
if (prevTrigger.src !== src || prevTrigger.attemptCount !== attemptCount || prevTrigger.retryCount !== retryCount || prevTrigger.retryDelay !== retryDelay || prevTrigger.reloadToken !== reloadToken) {
|
|
18
|
+
setPrevTrigger({
|
|
19
|
+
src,
|
|
20
|
+
attemptCount,
|
|
21
|
+
retryCount,
|
|
22
|
+
retryDelay,
|
|
23
|
+
reloadToken
|
|
24
|
+
});
|
|
25
|
+
if (src) {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
} else {
|
|
13
29
|
setLoading(false);
|
|
14
30
|
setLoaded(false);
|
|
15
|
-
return;
|
|
16
31
|
}
|
|
32
|
+
}
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!src) return;
|
|
17
35
|
let cancelled = false;
|
|
18
36
|
let retryTimeoutId;
|
|
19
|
-
setLoading(true);
|
|
20
|
-
setError(null);
|
|
21
37
|
const img = new Image();
|
|
22
38
|
img.src = src;
|
|
23
39
|
img.onload = () => {
|
|
@@ -59,7 +75,7 @@ const useImage = (src, options = {}) => {
|
|
|
59
75
|
attemptCount
|
|
60
76
|
};
|
|
61
77
|
};
|
|
62
|
-
|
|
63
78
|
//#endregion
|
|
64
79
|
export { useImage as default };
|
|
80
|
+
|
|
65
81
|
//# sourceMappingURL=use-image.mjs.map
|
|
@@ -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(
|
|
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(() => Boolean(src));\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 const [prevTrigger, setPrevTrigger] = useState({\n src,\n attemptCount,\n retryCount,\n retryDelay,\n reloadToken,\n });\n\n // Adjusted directly in render (React's \"adjust state during render\"\n // pattern) instead of inside the effect below — mirrors that effect's\n // own dependency list, so `loading`/`error`/`loaded` reset in the same\n // render a reload is triggered rather than the render after.\n const triggerChanged =\n prevTrigger.src !== src ||\n prevTrigger.attemptCount !== attemptCount ||\n prevTrigger.retryCount !== retryCount ||\n prevTrigger.retryDelay !== retryDelay ||\n prevTrigger.reloadToken !== reloadToken;\n\n if (triggerChanged) {\n setPrevTrigger({ src, attemptCount, retryCount, retryDelay, reloadToken });\n\n if (src) {\n setLoading(true);\n setError(null);\n } else {\n setLoading(false);\n setLoaded(false);\n }\n }\n\n useEffect(() => {\n if (!src) {\n return;\n }\n\n let cancelled = false;\n let retryTimeoutId: ReturnType<typeof setTimeout> | undefined;\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,CAAC,MAAM;CACvD,MAAM,EAAE,aAAa,GAAG,aAAa,QAAS;CAE9C,MAAM,CAAC,SAAS,cAAc,eAAe,QAAQ,GAAG,CAAC;CACzD,MAAM,CAAC,OAAO,YAAY,SAAuB,IAAI;CACrD,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK;CAC1C,MAAM,CAAC,cAAc,mBAAmB,SAAS,CAAC;CAClD,MAAM,CAAC,aAAa,kBAAkB,SAAS,CAAC;CAChD,MAAM,CAAC,aAAa,kBAAkB,SAAS;EAC7C;EACA;EACA;EACA;EACA;CACF,CAAC;CAaD,IANE,YAAY,QAAQ,OACpB,YAAY,iBAAiB,gBAC7B,YAAY,eAAe,cAC3B,YAAY,eAAe,cAC3B,YAAY,gBAAgB,aAEV;EAClB,eAAe;GAAE;GAAK;GAAc;GAAY;GAAY;EAAY,CAAC;EAEzE,IAAI,KAAK;GACP,WAAW,IAAI;GACf,SAAS,IAAI;EACf,OAAO;GACL,WAAW,KAAK;GAChB,UAAU,KAAK;EACjB;CACF;CAEA,gBAAgB;EACd,IAAI,CAAC,KACH;EAGF,IAAI,YAAY;EAChB,IAAI;EAEJ,MAAM,MAAM,IAAI,MAAM;EACtB,IAAI,MAAM;EAEV,IAAI,eAAe;GACjB,IAAI,WACF;GAEF,WAAW,KAAK;GAChB,UAAU,IAAI;GACd,SAAS,IAAI;EACf;EAEA,IAAI,WAAU,UAAS;GACrB,IAAI,WACF;GAEF,WAAW,KAAK;GAChB,UAAU,KAAK;GAMf,SAAS,IAAI,MAAM,yBAAyB,OAAO,EAAE,OAAO,MAAM,CAAC,CAAC;GAEpE,IAAI,eAAe,YACjB,iBAAiB,iBAAiB;IAChC,IAAI,CAAC,WACH,iBAAgB,SAAQ,OAAO,CAAC;GAEpC,GAAG,UAAU;EAEjB;EAEA,aAAa;GACX,YAAY;GACZ,IAAI,SAAS;GACb,IAAI,UAAU;GACd,IAAI,gBACF,aAAa,cAAc;EAE/B;CACF,GAAG;EAAC;EAAK;EAAc;EAAY;EAAY;CAAW,CAAC;CAO3D,OAAO;EACL;EACA;EACA;EACA,OATY,kBAAkB;GAC9B,gBAAgB,CAAC;GACjB,gBAAe,UAAS,QAAQ,CAAC;EACnC,GAAG,CAAC,CAME;EACJ;CACF;AACF"}
|
|
@@ -8,5 +8,5 @@ interface Result {
|
|
|
8
8
|
}
|
|
9
9
|
declare const useIntersectionObserver: <T extends Element = Element>(options?: Options) => [(node: T | null) => void, Result];
|
|
10
10
|
//#endregion
|
|
11
|
-
export { useIntersectionObserver };
|
|
11
|
+
export { useIntersectionObserver as default };
|
|
12
12
|
//# sourceMappingURL=use-intersection-observer.d.mts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-intersection-observer.ts
|
|
4
3
|
const useIntersectionObserver = (options) => {
|
|
5
4
|
const { freezeOnceVisible = false, ...observerInit } = options ?? {};
|
|
@@ -49,7 +48,7 @@ const useIntersectionObserver = (options) => {
|
|
|
49
48
|
isIntersecting: entry?.isIntersecting ?? false
|
|
50
49
|
}];
|
|
51
50
|
};
|
|
52
|
-
|
|
53
51
|
//#endregion
|
|
54
52
|
export { useIntersectionObserver as default };
|
|
53
|
+
|
|
55
54
|
//# sourceMappingURL=use-intersection-observer.mjs.map
|
|
@@ -1 +1 @@
|
|
|
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":"
|
|
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,CAAC;CAEnE,MAAM,CAAC,OAAO,YAAY,SAA2C,IAAI;CACzE,MAAM,cAAc,OAAoC,IAAI;CAC5D,MAAM,UAAU,OAAiB,IAAI;CACrC,MAAM,YAAY,OAAO,KAAK;CAO9B,MAAM,kBAAkB,KAAK,UAAU,YAAY;CACnD,MAAM,kBAAkB,OAAO,YAAY;CAC3C,MAAM,YAAY,OAAO,iBAAiB;CAE1C,gBAAgB;EACd,gBAAgB,UAAU;EAC1B,UAAU,UAAU;CACtB,CAAC;CAED,MAAM,UAAU,kBAAkB;EAChC,IAAI,YAAY,SAAS;GACvB,YAAY,QAAQ,WAAW;GAC/B,YAAY,UAAU;EACxB;EAEA,MAAM,OAAO,QAAQ;EAErB,IACE,CAAC,QACD,OAAO,yBAAyB,eAChC,UAAU,SAEV;EAGF,MAAM,WAAW,IAAI,sBAAsB,CAAC,mBAAmB;GAC7D,IAAI,CAAC,eACH;GAGF,SAAS,aAAa;GAEtB,IAAI,cAAc,kBAAkB,UAAU,SAAS;IACrD,UAAU,UAAU;IACpB,SAAS,WAAW;GACtB;EACF,GAAG,gBAAgB,OAAO;EAE1B,SAAS,QAAQ,IAAI;EACrB,YAAY,UAAU;CACxB,GAAG,CAAC,CAAC;CAEL,MAAM,MAAM,aAAa,SAAmB;EAC1C,QAAQ,UAAU;EAClB,QAAQ;CAEV,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,QAAQ;CAEV,GAAG,CAAC,eAAe,CAAC;CAEpB,gBAAgB;EACd,aAAa;GACX,YAAY,SAAS,WAAW;EAClC;CACF,GAAG,CAAC,CAAC;CAEL,OAAO,CAAC,KAAK;EAAE;EAAO,gBAAgB,OAAO,kBAAkB;CAAM,CAAC;AACxE"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-interval.ts
|
|
4
3
|
const useInterval = (callback, delay) => {
|
|
5
4
|
const callbackRef = useRef(callback);
|
|
@@ -14,7 +13,7 @@ const useInterval = (callback, delay) => {
|
|
|
14
13
|
return () => clearInterval(id);
|
|
15
14
|
}, [delay]);
|
|
16
15
|
};
|
|
17
|
-
|
|
18
16
|
//#endregion
|
|
19
17
|
export { useInterval as default };
|
|
18
|
+
|
|
20
19
|
//# sourceMappingURL=use-interval.mjs.map
|
|
@@ -1 +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":"
|
|
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,QAAQ;CAEnC,gBAAgB;EACd,YAAY,UAAU;CACxB,CAAC;CAED,gBAAgB;EACd,IAAI,UAAU,MACZ;EAGF,MAAM,KAAK,kBAAkB;GAC3B,YAAY,QAAQ;EACtB,GAAG,KAAK;EAER,aAAa,cAAc,EAAE;CAC/B,GAAG,CAAC,KAAK,CAAC;AACZ"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { RefObject } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-key-press.d.ts
|
|
4
3
|
type KeyPressTarget = RefObject<HTMLElement | null> | Window | Document | HTMLElement | null | undefined;
|
|
5
4
|
interface Options {
|
|
@@ -10,5 +9,5 @@ interface Options {
|
|
|
10
9
|
}
|
|
11
10
|
declare const useKeyPress: (combo: string | string[], handler: (event: KeyboardEvent) => void, options?: Options) => void;
|
|
12
11
|
//#endregion
|
|
13
|
-
export { useKeyPress };
|
|
12
|
+
export { useKeyPress as default };
|
|
14
13
|
//# sourceMappingURL=use-key-press.d.mts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-key-press.ts
|
|
4
3
|
const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
|
5
4
|
const KEY_ALIASES = {
|
|
@@ -19,8 +18,10 @@ const parseCombo = (combo) => {
|
|
|
19
18
|
let meta = modifiers.includes("meta") || modifiers.includes("cmd");
|
|
20
19
|
const shift = modifiers.includes("shift");
|
|
21
20
|
const alt = modifiers.includes("alt") || modifiers.includes("option");
|
|
22
|
-
if (modifiers.includes("mod"))
|
|
23
|
-
|
|
21
|
+
if (modifiers.includes("mod")) {
|
|
22
|
+
if (isMac()) meta = true;
|
|
23
|
+
else ctrl = true;
|
|
24
|
+
}
|
|
24
25
|
return {
|
|
25
26
|
key,
|
|
26
27
|
ctrl,
|
|
@@ -42,6 +43,7 @@ const useKeyPress = (combo, handler, options = {}) => {
|
|
|
42
43
|
});
|
|
43
44
|
const combos = Array.isArray(combo) ? combo : [combo];
|
|
44
45
|
const parsedCombos = combos.map(parseCombo);
|
|
46
|
+
const comboKey = combos.join(",");
|
|
45
47
|
useEffect(() => {
|
|
46
48
|
if (!enabled) return;
|
|
47
49
|
let cancelled = false;
|
|
@@ -73,13 +75,13 @@ const useKeyPress = (combo, handler, options = {}) => {
|
|
|
73
75
|
};
|
|
74
76
|
}, [
|
|
75
77
|
enabled,
|
|
76
|
-
|
|
78
|
+
comboKey,
|
|
77
79
|
target,
|
|
78
80
|
preventDefault,
|
|
79
81
|
ignore
|
|
80
82
|
]);
|
|
81
83
|
};
|
|
82
|
-
|
|
83
84
|
//#endregion
|
|
84
85
|
export { useKeyPress as default };
|
|
86
|
+
|
|
85
87
|
//# sourceMappingURL=use-key-press.mjs.map
|