@jbpark/use-hooks 4.0.0 → 4.0.1
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/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 -2
- 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 +1 -2
- 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 +26 -26
|
@@ -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
|
|
@@ -1 +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":"
|
|
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,SAAS;AAKjD,MAAM,cAAsC;CAC1C,OAAO;CACP,UAAU;CACV,KAAK;AACP;AAKA,MAAM,cAAc,UAA+B;CACjD,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,MAAM,YAAY,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC;CAK1E,MAAM,SAAS,MAAM,MAAM,SAAS,MAAM;CAC1C,MAAM,aAAa,OAAO,KAAK;CAC/B,IAAI,OACF,eAAe,MAAM,WAAW,KAAK,SAAS,WAAA,CAC9C,YAAY;CAId,IAAI,QAAQ,MAAM,MAAM,SAAS,GAC/B,MAAM;CAGR,MAAM,YAAY,QAAQ;CAE1B,IAAI,OAAO,UAAU,SAAS,MAAM;CACpC,IAAI,OAAO,UAAU,SAAS,MAAM,KAAK,UAAU,SAAS,KAAK;CACjE,MAAM,QAAQ,UAAU,SAAS,OAAO;CACxC,MAAM,MAAM,UAAU,SAAS,KAAK,KAAK,UAAU,SAAS,QAAQ;CAEpE,IAAI,UAAU,SAAS,KAAK,GAAG;EAC7B,IAAI,MAAM,GACR,OAAO;OAEP,OAAO;CAEX;CAEA,OAAO;EAAE;EAAK;EAAM;EAAM;EAAO;CAAI;AACvC;AAMA,MAAM,gBAAgB,OAAsB,UAC1C,MAAM,IAAI,YAAY,MAAM,MAAM,OAClC,MAAM,YAAY,MAAM,QACxB,MAAM,YAAY,MAAM,QACxB,MAAM,aAAa,MAAM,SACzB,MAAM,WAAW,MAAM;AAEzB,MAAM,iBAAiB,WAA+C;CACpE,IAAI,CAAC,QACH,OAAO,OAAO,WAAW,cAAc,OAAO;CAEhD,OAAO,aAAa,SAAS,OAAO,UAAU;AAChD;AAEA,MAAM,eACJ,OACA,SACA,UAAmB,CAAC,MACjB;CACH,MAAM,EAAE,QAAQ,UAAU,MAAM,iBAAiB,OAAO,WAAW;CAEnE,MAAM,aAAa,OAAO,OAAO;CAEjC,gBAAgB;EACd,WAAW,UAAU;CACvB,CAAC;CAED,MAAM,SAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;CACpD,MAAM,eAAe,OAAO,IAAI,UAAU;CAM1C,MAAM,WAAW,OAAO,KAAK,GAAG;CAEhC,gBAAgB;EACd,IAAI,CAAC,SACH;EAGF,IAAI,YAAY;EAChB,IAAI;EACJ,IAAI,iBAAqC;EAEzC,MAAM,aAAa,UAAiB;GAClC,MAAM,gBAAgB;GACtB,MAAM,cAAc,cAAc;GAElC,IAAI,UAAU,aAAa,QAAQ,MAAM,GACvC;GAGF,IAAI,CAAC,aAAa,MAAK,WAAU,aAAa,eAAe,MAAM,CAAC,GAClE;GAGF,IAAI,gBACF,cAAc,eAAe;GAG/B,WAAW,QAAQ,aAAa;EAClC;EAEA,MAAM,eAAe;GACnB,IAAI,WACF;GAOF,IAAI,UAAU,aAAa,UAAU,CAAC,OAAO,SAAS;IACpD,QAAQ,sBAAsB,MAAM;IACpC;GACF;GAEA,iBAAiB,cAAc,MAAM;GAErC,IAAI,CAAC,gBACH;GAGF,eAAe,iBAAiB,WAAW,SAAS;EACtD;EAEA,OAAO;EAEP,aAAa;GACX,YAAY;GACZ,IAAI,UAAU,KAAA,GACZ,qBAAqB,KAAK;GAE5B,gBAAgB,oBAAoB,WAAW,SAAS;EAC1D;CAEF,GAAG;EAAC;EAAS;EAAU;EAAQ;EAAgB;CAAM,CAAC;AACxD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/hooks/use-local-storage.d.ts
|
|
2
2
|
declare const useLocalStorage: <T>(key: string, initialValue: T) => readonly [T, (value: T | ((val: T) => T)) => void];
|
|
3
3
|
//#endregion
|
|
4
|
-
export { useLocalStorage };
|
|
4
|
+
export { useLocalStorage as default };
|
|
5
5
|
//# sourceMappingURL=use-local-storage.d.mts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-local-storage.ts
|
|
4
3
|
const cache = /* @__PURE__ */ new Map();
|
|
5
4
|
const listeners = /* @__PURE__ */ new Map();
|
|
@@ -63,7 +62,10 @@ const useLocalStorage = (key, initialValue) => {
|
|
|
63
62
|
console.error(`Error seeding localStorage key "${key}":`, e);
|
|
64
63
|
}
|
|
65
64
|
}, [key]);
|
|
66
|
-
|
|
65
|
+
const subscribeForKey = useCallback((callback) => subscribe(key, callback), [key]);
|
|
66
|
+
const getSnapshotForKey = useCallback(() => getSnapshot(key, initialValueRef.current), [key]);
|
|
67
|
+
const getServerSnapshot = useCallback(() => initialValueRef.current, []);
|
|
68
|
+
return [useSyncExternalStore(subscribeForKey, getSnapshotForKey, getServerSnapshot), useCallback((value) => {
|
|
67
69
|
try {
|
|
68
70
|
const prev = getSnapshot(key, initialValueRef.current);
|
|
69
71
|
const valueToStore = value instanceof Function ? value(prev) : value;
|
|
@@ -79,7 +81,7 @@ const useLocalStorage = (key, initialValue) => {
|
|
|
79
81
|
}
|
|
80
82
|
}, [key])];
|
|
81
83
|
};
|
|
82
|
-
|
|
83
84
|
//#endregion
|
|
84
85
|
export { useLocalStorage as default };
|
|
86
|
+
|
|
85
87
|
//# sourceMappingURL=use-local-storage.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-local-storage.mjs","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';\n\n// Module-level so every `useLocalStorage(key)` instance for the same key —\n// even across unrelated components in the same tab — shares one cache and\n// subscriber list. That's what makes same-tab sync possible: the native\n// `storage` event only fires in *other* tabs, so same-tab updates have to\n// be broadcast by us via `notify()`.\ninterface CacheEntry<T> {\n raw: string | null;\n value: T;\n}\n\nconst cache = new Map<string, CacheEntry<unknown>>();\nconst listeners = new Map<string, Set<() => void>>();\n\nfunction getListeners(key: string) {\n let set = listeners.get(key);\n if (!set) {\n set = new Set();\n listeners.set(key, set);\n }\n return set;\n}\n\nfunction notify(key: string) {\n getListeners(key).forEach(callback => callback());\n}\n\n// Returns the same cached value reference as long as the raw localStorage\n// string hasn't changed, so `useSyncExternalStore` doesn't see a \"new\"\n// snapshot (and re-render) on every call — only when the value actually\n// changed.\nfunction getSnapshot<T>(key: string, initialValue: T): T {\n const raw = typeof window === 'undefined' ? null : localStorage.getItem(key);\n const cached = cache.get(key) as CacheEntry<T> | undefined;\n\n if (cached && cached.raw === raw) {\n return cached.value;\n }\n\n let value: T;\n try {\n value = raw === null ? initialValue : (JSON.parse(raw) as T);\n } catch {\n value = initialValue;\n }\n\n cache.set(key, { raw, value });\n return value;\n}\n\nfunction subscribe(key: string, callback: () => void) {\n const set = getListeners(key);\n set.add(callback);\n\n const onStorage = (event: StorageEvent) => {\n if (event.key === key) {\n callback();\n }\n };\n\n if (typeof window !== 'undefined') {\n window.addEventListener('storage', onStorage);\n }\n\n return () => {\n set.delete(callback);\n if (typeof window !== 'undefined') {\n window.removeEventListener('storage', onStorage);\n }\n };\n}\n\nconst useLocalStorage = <T>(key: string, initialValue: T) => {\n const initialValueRef = useRef(initialValue);\n\n useEffect(() => {\n initialValueRef.current = initialValue;\n });\n\n // Seed the key with the initial value if it isn't set yet — done in an\n // effect (not inside the state updater below) so it's a one-shot side\n // effect rather than something React could invoke twice under\n // StrictMode/concurrent rendering.\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n try {\n if (localStorage.getItem(key) === null) {\n const raw = JSON.stringify(initialValueRef.current);\n localStorage.setItem(key, raw);\n cache.set(key, { raw, value: initialValueRef.current });\n notify(key);\n }\n } catch (e) {\n console.error(`Error seeding localStorage key \"${key}\":`, e);\n }\n }, [key]);\n\n const subscribeForKey = useCallback(\n (callback: () => void) => subscribe(key, callback),\n [key],\n );\n\n const getSnapshotForKey = useCallback(\n () => getSnapshot(key, initialValueRef.current),\n [key],\n );\n\n const getServerSnapshot = useCallback(() => initialValueRef.current, []);\n\n const storedValue = useSyncExternalStore(\n subscribeForKey,\n getSnapshotForKey,\n getServerSnapshot,\n );\n\n const setValue = useCallback(\n (value: T | ((val: T) => T)) => {\n try {\n const prev = getSnapshot(key, initialValueRef.current);\n const valueToStore = value instanceof Function ? value(prev) : value;\n const raw = JSON.stringify(valueToStore);\n\n localStorage.setItem(key, raw);\n cache.set(key, { raw, value: valueToStore });\n notify(key);\n } catch (e) {\n console.error(`Error setting localStorage key \"${key}\":`, e);\n }\n },\n [key],\n );\n\n return [storedValue, setValue] as const;\n};\n\nexport default useLocalStorage;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-local-storage.mjs","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';\n\n// Module-level so every `useLocalStorage(key)` instance for the same key —\n// even across unrelated components in the same tab — shares one cache and\n// subscriber list. That's what makes same-tab sync possible: the native\n// `storage` event only fires in *other* tabs, so same-tab updates have to\n// be broadcast by us via `notify()`.\ninterface CacheEntry<T> {\n raw: string | null;\n value: T;\n}\n\nconst cache = new Map<string, CacheEntry<unknown>>();\nconst listeners = new Map<string, Set<() => void>>();\n\nfunction getListeners(key: string) {\n let set = listeners.get(key);\n if (!set) {\n set = new Set();\n listeners.set(key, set);\n }\n return set;\n}\n\nfunction notify(key: string) {\n getListeners(key).forEach(callback => callback());\n}\n\n// Returns the same cached value reference as long as the raw localStorage\n// string hasn't changed, so `useSyncExternalStore` doesn't see a \"new\"\n// snapshot (and re-render) on every call — only when the value actually\n// changed.\nfunction getSnapshot<T>(key: string, initialValue: T): T {\n const raw = typeof window === 'undefined' ? null : localStorage.getItem(key);\n const cached = cache.get(key) as CacheEntry<T> | undefined;\n\n if (cached && cached.raw === raw) {\n return cached.value;\n }\n\n let value: T;\n try {\n value = raw === null ? initialValue : (JSON.parse(raw) as T);\n } catch {\n value = initialValue;\n }\n\n cache.set(key, { raw, value });\n return value;\n}\n\nfunction subscribe(key: string, callback: () => void) {\n const set = getListeners(key);\n set.add(callback);\n\n const onStorage = (event: StorageEvent) => {\n if (event.key === key) {\n callback();\n }\n };\n\n if (typeof window !== 'undefined') {\n window.addEventListener('storage', onStorage);\n }\n\n return () => {\n set.delete(callback);\n if (typeof window !== 'undefined') {\n window.removeEventListener('storage', onStorage);\n }\n };\n}\n\nconst useLocalStorage = <T>(key: string, initialValue: T) => {\n const initialValueRef = useRef(initialValue);\n\n useEffect(() => {\n initialValueRef.current = initialValue;\n });\n\n // Seed the key with the initial value if it isn't set yet — done in an\n // effect (not inside the state updater below) so it's a one-shot side\n // effect rather than something React could invoke twice under\n // StrictMode/concurrent rendering.\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n try {\n if (localStorage.getItem(key) === null) {\n const raw = JSON.stringify(initialValueRef.current);\n localStorage.setItem(key, raw);\n cache.set(key, { raw, value: initialValueRef.current });\n notify(key);\n }\n } catch (e) {\n console.error(`Error seeding localStorage key \"${key}\":`, e);\n }\n }, [key]);\n\n const subscribeForKey = useCallback(\n (callback: () => void) => subscribe(key, callback),\n [key],\n );\n\n const getSnapshotForKey = useCallback(\n () => getSnapshot(key, initialValueRef.current),\n [key],\n );\n\n const getServerSnapshot = useCallback(() => initialValueRef.current, []);\n\n const storedValue = useSyncExternalStore(\n subscribeForKey,\n getSnapshotForKey,\n getServerSnapshot,\n );\n\n const setValue = useCallback(\n (value: T | ((val: T) => T)) => {\n try {\n const prev = getSnapshot(key, initialValueRef.current);\n const valueToStore = value instanceof Function ? value(prev) : value;\n const raw = JSON.stringify(valueToStore);\n\n localStorage.setItem(key, raw);\n cache.set(key, { raw, value: valueToStore });\n notify(key);\n } catch (e) {\n console.error(`Error setting localStorage key \"${key}\":`, e);\n }\n },\n [key],\n );\n\n return [storedValue, setValue] as const;\n};\n\nexport default useLocalStorage;\n"],"mappings":";;AAYA,MAAM,wBAAQ,IAAI,IAAiC;AACnD,MAAM,4BAAY,IAAI,IAA6B;AAEnD,SAAS,aAAa,KAAa;CACjC,IAAI,MAAM,UAAU,IAAI,GAAG;CAC3B,IAAI,CAAC,KAAK;EACR,sBAAM,IAAI,IAAI;EACd,UAAU,IAAI,KAAK,GAAG;CACxB;CACA,OAAO;AACT;AAEA,SAAS,OAAO,KAAa;CAC3B,aAAa,GAAG,CAAC,CAAC,SAAQ,aAAY,SAAS,CAAC;AAClD;AAMA,SAAS,YAAe,KAAa,cAAoB;CACvD,MAAM,MAAM,OAAO,WAAW,cAAc,OAAO,aAAa,QAAQ,GAAG;CAC3E,MAAM,SAAS,MAAM,IAAI,GAAG;CAE5B,IAAI,UAAU,OAAO,QAAQ,KAC3B,OAAO,OAAO;CAGhB,IAAI;CACJ,IAAI;EACF,QAAQ,QAAQ,OAAO,eAAgB,KAAK,MAAM,GAAG;CACvD,QAAQ;EACN,QAAQ;CACV;CAEA,MAAM,IAAI,KAAK;EAAE;EAAK;CAAM,CAAC;CAC7B,OAAO;AACT;AAEA,SAAS,UAAU,KAAa,UAAsB;CACpD,MAAM,MAAM,aAAa,GAAG;CAC5B,IAAI,IAAI,QAAQ;CAEhB,MAAM,aAAa,UAAwB;EACzC,IAAI,MAAM,QAAQ,KAChB,SAAS;CAEb;CAEA,IAAI,OAAO,WAAW,aACpB,OAAO,iBAAiB,WAAW,SAAS;CAG9C,aAAa;EACX,IAAI,OAAO,QAAQ;EACnB,IAAI,OAAO,WAAW,aACpB,OAAO,oBAAoB,WAAW,SAAS;CAEnD;AACF;AAEA,MAAM,mBAAsB,KAAa,iBAAoB;CAC3D,MAAM,kBAAkB,OAAO,YAAY;CAE3C,gBAAgB;EACd,gBAAgB,UAAU;CAC5B,CAAC;CAMD,gBAAgB;EACd,IAAI,OAAO,WAAW,aACpB;EAGF,IAAI;GACF,IAAI,aAAa,QAAQ,GAAG,MAAM,MAAM;IACtC,MAAM,MAAM,KAAK,UAAU,gBAAgB,OAAO;IAClD,aAAa,QAAQ,KAAK,GAAG;IAC7B,MAAM,IAAI,KAAK;KAAE;KAAK,OAAO,gBAAgB;IAAQ,CAAC;IACtD,OAAO,GAAG;GACZ;EACF,SAAS,GAAG;GACV,QAAQ,MAAM,mCAAmC,IAAI,KAAK,CAAC;EAC7D;CACF,GAAG,CAAC,GAAG,CAAC;CAER,MAAM,kBAAkB,aACrB,aAAyB,UAAU,KAAK,QAAQ,GACjD,CAAC,GAAG,CACN;CAEA,MAAM,oBAAoB,kBAClB,YAAY,KAAK,gBAAgB,OAAO,GAC9C,CAAC,GAAG,CACN;CAEA,MAAM,oBAAoB,kBAAkB,gBAAgB,SAAS,CAAC,CAAC;CAyBvE,OAAO,CAvBa,qBAClB,iBACA,mBACA,iBAoBgB,GAjBD,aACd,UAA+B;EAC9B,IAAI;GACF,MAAM,OAAO,YAAY,KAAK,gBAAgB,OAAO;GACrD,MAAM,eAAe,iBAAiB,WAAW,MAAM,IAAI,IAAI;GAC/D,MAAM,MAAM,KAAK,UAAU,YAAY;GAEvC,aAAa,QAAQ,KAAK,GAAG;GAC7B,MAAM,IAAI,KAAK;IAAE;IAAK,OAAO;GAAa,CAAC;GAC3C,OAAO,GAAG;EACZ,SAAS,GAAG;GACV,QAAQ,MAAM,mCAAmC,IAAI,KAAK,CAAC;EAC7D;CACF,GACA,CAAC,GAAG,CAGsB,CAAC;AAC/B"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { RefObject } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-merged-ref.d.ts
|
|
4
3
|
type MergeableRef<T> = ((node: T | null) => void | (() => void)) | RefObject<T | null> | null | undefined;
|
|
5
4
|
declare const useMergedRef: <T>(...refs: MergeableRef<T>[]) => (node: T | null) => () => void;
|
|
6
5
|
//#endregion
|
|
7
|
-
export { useMergedRef };
|
|
6
|
+
export { useMergedRef as default };
|
|
8
7
|
//# sourceMappingURL=use-merged-ref.d.mts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/hooks/use-merged-ref.ts
|
|
4
3
|
const useMergedRef = (...refs) => {
|
|
5
4
|
return useCallback((node) => {
|
|
@@ -25,7 +24,7 @@ const useMergedRef = (...refs) => {
|
|
|
25
24
|
};
|
|
26
25
|
}, refs);
|
|
27
26
|
};
|
|
28
|
-
|
|
29
27
|
//#endregion
|
|
30
28
|
export { useMergedRef as default };
|
|
29
|
+
|
|
31
30
|
//# sourceMappingURL=use-merged-ref.mjs.map
|
|
@@ -1 +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":"
|
|
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;CACtD,OAAO,aAAa,SAAmB;EAGrC,MAAM,WAAW,KAAK,KAAI,QAAO;GAC/B,IAAI,CAAC,KACH;GAGF,IAAI,OAAO,QAAQ,YAAY;IAC7B,MAAM,UAAU,IAAI,IAAI;IACxB,OAAO,OAAO,YAAY,aAAa,UAAU,KAAA;GACnD;GAEA,IAAI,UAAU;EAEhB,CAAC;EAOD,aAAa;GACX,KAAK,SAAS,KAAK,MAAM;IACvB,IAAI,CAAC,KACH;IAGF,IAAI,OAAO,QAAQ,YAAY;KAC7B,MAAM,UAAU,SAAS;KACzB,IAAI,SACF,QAAQ;UAER,IAAI,IAAI;KAEV;IACF;IAEA,IAAI,UAAU;GAChB,CAAC;EACH;CAKF,GAAG,IAAI;AACT"}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
2
|
//#region src/hooks/use-multi-select.ts
|
|
4
3
|
const useMultiSelect = (count) => {
|
|
5
4
|
const [rawSelected, setRawSelected] = useState(/* @__PURE__ */ new Set());
|
|
6
5
|
const [anchor, setAnchor] = useState(null);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}, [rawSelected, count]);
|
|
11
|
-
useEffect(() => {
|
|
6
|
+
const [prevCount, setPrevCount] = useState(count);
|
|
7
|
+
if (count !== prevCount) {
|
|
8
|
+
setPrevCount(count);
|
|
12
9
|
setRawSelected((prev) => {
|
|
13
10
|
const next = new Set([...prev].filter((index) => index < count));
|
|
14
11
|
return next.size === prev.size ? prev : next;
|
|
15
12
|
});
|
|
16
13
|
setAnchor((prev) => prev !== null && prev >= count ? null : prev);
|
|
17
|
-
}
|
|
14
|
+
}
|
|
15
|
+
const selected = useMemo(() => {
|
|
16
|
+
const next = new Set([...rawSelected].filter((index) => index < count));
|
|
17
|
+
return next.size === rawSelected.size ? rawSelected : next;
|
|
18
|
+
}, [rawSelected, count]);
|
|
18
19
|
const toggle = useCallback((index, shiftKey = false) => {
|
|
19
20
|
if (index < 0 || index >= count) return;
|
|
20
21
|
setRawSelected((prev) => {
|
|
@@ -46,7 +47,7 @@ const useMultiSelect = (count) => {
|
|
|
46
47
|
replace
|
|
47
48
|
};
|
|
48
49
|
};
|
|
49
|
-
|
|
50
50
|
//#endregion
|
|
51
51
|
export { useMultiSelect as default };
|
|
52
|
+
|
|
52
53
|
//# sourceMappingURL=use-multi-select.mjs.map
|