@refraktor/utils 0.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/.turbo/turbo-build.log +1 -0
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/build/hooks/index.d.ts +9 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +8 -0
- package/build/hooks/use-click-outside/index.d.ts +5 -0
- package/build/hooks/use-click-outside/index.d.ts.map +1 -0
- package/build/hooks/use-click-outside/index.js +50 -0
- package/build/hooks/use-debounced-callback/index.d.ts +11 -0
- package/build/hooks/use-debounced-callback/index.d.ts.map +1 -0
- package/build/hooks/use-debounced-callback/index.js +88 -0
- package/build/hooks/use-disclosure/index.d.ts +12 -0
- package/build/hooks/use-disclosure/index.d.ts.map +1 -0
- package/build/hooks/use-disclosure/index.js +39 -0
- package/build/hooks/use-id/index.d.ts +2 -0
- package/build/hooks/use-id/index.d.ts.map +1 -0
- package/build/hooks/use-id/index.js +5 -0
- package/build/hooks/use-isomorphic-layout-effect/index.d.ts +3 -0
- package/build/hooks/use-isomorphic-layout-effect/index.d.ts.map +1 -0
- package/build/hooks/use-isomorphic-layout-effect/index.js +2 -0
- package/build/hooks/use-merged-refs/index.d.ts +6 -0
- package/build/hooks/use-merged-refs/index.d.ts.map +1 -0
- package/build/hooks/use-merged-refs/index.js +38 -0
- package/build/hooks/use-resize-observer/index.d.ts +15 -0
- package/build/hooks/use-resize-observer/index.d.ts.map +1 -0
- package/build/hooks/use-resize-observer/index.js +65 -0
- package/build/hooks/use-uncontrolled/index.d.ts +13 -0
- package/build/hooks/use-uncontrolled/index.d.ts.map +1 -0
- package/build/hooks/use-uncontrolled/index.js +12 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2 -0
- package/build/utils/auto-contrast/index.d.ts +2 -0
- package/build/utils/auto-contrast/index.d.ts.map +1 -0
- package/build/utils/auto-contrast/index.js +50 -0
- package/build/utils/clamp/index.d.ts +2 -0
- package/build/utils/clamp/index.d.ts.map +1 -0
- package/build/utils/clamp/index.js +9 -0
- package/build/utils/context/index.d.ts +3 -0
- package/build/utils/context/index.d.ts.map +1 -0
- package/build/utils/context/index.js +2 -0
- package/build/utils/context/optional-context.d.ts +5 -0
- package/build/utils/context/optional-context.d.ts.map +1 -0
- package/build/utils/context/optional-context.js +10 -0
- package/build/utils/context/safe-context.d.ts +5 -0
- package/build/utils/context/safe-context.d.ts.map +1 -0
- package/build/utils/context/safe-context.js +14 -0
- package/build/utils/get-change-value/index.d.ts +10 -0
- package/build/utils/get-change-value/index.d.ts.map +1 -0
- package/build/utils/get-change-value/index.js +7 -0
- package/build/utils/index.d.ts +6 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +5 -0
- package/build/utils/storage/index.d.ts +39 -0
- package/build/utils/storage/index.d.ts.map +1 -0
- package/build/utils/storage/index.js +150 -0
- package/build/utils/storage/storage.d.ts +1 -0
- package/build/utils/storage/storage.d.ts.map +1 -0
- package/build/utils/storage/storage.js +1 -0
- package/build/utils/storage/storage.test.d.ts +2 -0
- package/build/utils/storage/storage.test.d.ts.map +1 -0
- package/build/utils/storage/storage.test.js +153 -0
- package/package.json +29 -0
- package/refraktor-utils-0.0.1-alpha.0.tgz +0 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-click-outside/index.ts +76 -0
- package/src/hooks/use-debounced-callback/index.ts +128 -0
- package/src/hooks/use-disclosure/index.ts +67 -0
- package/src/hooks/use-id/index.ts +6 -0
- package/src/hooks/use-isomorphic-layout-effect/index.ts +4 -0
- package/src/hooks/use-merged-refs/index.ts +45 -0
- package/src/hooks/use-resize-observer/index.ts +105 -0
- package/src/hooks/use-uncontrolled/index.ts +36 -0
- package/src/index.ts +2 -0
- package/src/utils/auto-contrast/index.ts +73 -0
- package/src/utils/clamp/index.ts +13 -0
- package/src/utils/context/index.ts +2 -0
- package/src/utils/context/optional-context.tsx +21 -0
- package/src/utils/context/safe-context.tsx +25 -0
- package/src/utils/get-change-value/index.ts +22 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/storage/index.ts +203 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseDisclosureCallbacks {
|
|
4
|
+
onOpen?: () => void;
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UseDisclosureHandlers {
|
|
9
|
+
open: () => void;
|
|
10
|
+
close: () => void;
|
|
11
|
+
toggle: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type UseDisclosureReturn = [boolean, UseDisclosureHandlers];
|
|
15
|
+
|
|
16
|
+
export function useDisclosure(
|
|
17
|
+
initialState = false,
|
|
18
|
+
callbacks?: UseDisclosureCallbacks
|
|
19
|
+
): UseDisclosureReturn {
|
|
20
|
+
const [opened, setOpened] = useState(initialState);
|
|
21
|
+
const { onOpen, onClose } = callbacks ?? {};
|
|
22
|
+
|
|
23
|
+
const open = useCallback(() => {
|
|
24
|
+
setOpened((isOpened) => {
|
|
25
|
+
if (!isOpened) {
|
|
26
|
+
onOpen?.();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
}, [onOpen]);
|
|
32
|
+
|
|
33
|
+
const close = useCallback(() => {
|
|
34
|
+
setOpened((isOpened) => {
|
|
35
|
+
if (isOpened) {
|
|
36
|
+
onClose?.();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
});
|
|
41
|
+
}, [onClose]);
|
|
42
|
+
|
|
43
|
+
const toggle = useCallback(() => {
|
|
44
|
+
setOpened((isOpened) => {
|
|
45
|
+
const next = !isOpened;
|
|
46
|
+
|
|
47
|
+
if (next) {
|
|
48
|
+
onOpen?.();
|
|
49
|
+
} else {
|
|
50
|
+
onClose?.();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return next;
|
|
54
|
+
});
|
|
55
|
+
}, [onOpen, onClose]);
|
|
56
|
+
|
|
57
|
+
const handlers = useMemo(
|
|
58
|
+
() => ({
|
|
59
|
+
open,
|
|
60
|
+
close,
|
|
61
|
+
toggle
|
|
62
|
+
}),
|
|
63
|
+
[open, close, toggle]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return [opened, handlers];
|
|
67
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Ref, useCallback, RefCallback } from "react";
|
|
2
|
+
|
|
3
|
+
type AnyRef<T> = Ref<T> | undefined;
|
|
4
|
+
type CleanupFunction<T> = ReturnType<RefCallback<T>>;
|
|
5
|
+
|
|
6
|
+
function applyRef<T>(ref: AnyRef<T>, value: T | null): CleanupFunction<T> {
|
|
7
|
+
if (!ref) return;
|
|
8
|
+
|
|
9
|
+
if (typeof ref === "function") {
|
|
10
|
+
return ref(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof ref === "object" && "current" in ref) {
|
|
14
|
+
ref.current = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mergeRefs<T>(...refs: AnyRef<T>[]): RefCallback<T> {
|
|
19
|
+
const cleanups = new Map<AnyRef<T>, Exclude<CleanupFunction<T>, void>>();
|
|
20
|
+
|
|
21
|
+
return (node: T | null) => {
|
|
22
|
+
for (const ref of refs) {
|
|
23
|
+
const cleanup = applyRef(ref, node);
|
|
24
|
+
if (cleanup) cleanups.set(ref, cleanup);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (cleanups.size > 0) {
|
|
28
|
+
return () => {
|
|
29
|
+
for (const ref of refs) {
|
|
30
|
+
const fn = cleanups.get(ref);
|
|
31
|
+
if (typeof fn === "function") {
|
|
32
|
+
fn();
|
|
33
|
+
} else {
|
|
34
|
+
applyRef(ref, null);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
cleanups.clear();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useMergedRefs<T>(...refs: AnyRef<T>[]) {
|
|
44
|
+
return useCallback(mergeRefs(...refs), refs);
|
|
45
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseResizeObserverSize {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UseResizeObserverOptions {
|
|
9
|
+
initialSize?: UseResizeObserverSize;
|
|
10
|
+
box?: ResizeObserverBoxOptions;
|
|
11
|
+
onResize?: (entry: ResizeObserverEntry) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseResizeObserverReturn<T extends HTMLElement = HTMLElement>
|
|
15
|
+
extends UseResizeObserverSize {
|
|
16
|
+
ref: (node: T | null) => void;
|
|
17
|
+
entry: ResizeObserverEntry | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResizeObserverState extends UseResizeObserverSize {
|
|
21
|
+
entry: ResizeObserverEntry | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SIZE: UseResizeObserverSize = {
|
|
25
|
+
width: 0,
|
|
26
|
+
height: 0
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
|
|
30
|
+
options: UseResizeObserverOptions = {}
|
|
31
|
+
): UseResizeObserverReturn<T> {
|
|
32
|
+
const { initialSize = DEFAULT_SIZE, box, onResize } = options;
|
|
33
|
+
const [node, setNode] = useState<T | null>(null);
|
|
34
|
+
const [state, setState] = useState<ResizeObserverState>(() => ({
|
|
35
|
+
width: initialSize.width,
|
|
36
|
+
height: initialSize.height,
|
|
37
|
+
entry: null
|
|
38
|
+
}));
|
|
39
|
+
const onResizeRef = useRef(onResize);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
onResizeRef.current = onResize;
|
|
43
|
+
}, [onResize]);
|
|
44
|
+
|
|
45
|
+
const ref = useCallback((nextNode: T | null) => {
|
|
46
|
+
setNode(nextNode);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!node) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
typeof window === "undefined" ||
|
|
56
|
+
typeof ResizeObserver === "undefined"
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nextWidth = entry.contentRect.width;
|
|
67
|
+
const nextHeight = entry.contentRect.height;
|
|
68
|
+
|
|
69
|
+
setState((current) => {
|
|
70
|
+
if (
|
|
71
|
+
current.width === nextWidth &&
|
|
72
|
+
current.height === nextHeight &&
|
|
73
|
+
current.entry !== null
|
|
74
|
+
) {
|
|
75
|
+
return current;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
width: nextWidth,
|
|
80
|
+
height: nextHeight,
|
|
81
|
+
entry
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
onResizeRef.current?.(entry);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
observer.observe(node, box ? { box } : undefined);
|
|
90
|
+
} catch {
|
|
91
|
+
observer.observe(node);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
observer.disconnect();
|
|
96
|
+
};
|
|
97
|
+
}, [box, node]);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
ref,
|
|
101
|
+
width: state.width,
|
|
102
|
+
height: state.height,
|
|
103
|
+
entry: state.entry
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type UseUncontrolledProps<T> = {
|
|
4
|
+
value?: T;
|
|
5
|
+
defaultValue?: T;
|
|
6
|
+
finalValue?: T;
|
|
7
|
+
onChange?: (value: T, ...payload: any[]) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type UseUncontrolledReturn<T> = [
|
|
11
|
+
T,
|
|
12
|
+
(value: T, ...payload: any[]) => void,
|
|
13
|
+
boolean
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function useUncontrolled<T>({
|
|
17
|
+
value,
|
|
18
|
+
defaultValue,
|
|
19
|
+
finalValue,
|
|
20
|
+
onChange = () => {}
|
|
21
|
+
}: UseUncontrolledProps<T>): UseUncontrolledReturn<T> {
|
|
22
|
+
const [state, setState] = useState(
|
|
23
|
+
defaultValue !== undefined ? defaultValue : finalValue
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const handleChange = (val: T, ...payload: any[]) => {
|
|
27
|
+
setState(val);
|
|
28
|
+
onChange?.(val, ...payload);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
return [value as T, onChange, true];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [state as T, handleChange, false];
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
type RGB = {
|
|
2
|
+
r: number;
|
|
3
|
+
g: number;
|
|
4
|
+
b: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function parseHexColor(color: string): RGB | null {
|
|
8
|
+
const value = color.trim().replace(/^#/, "");
|
|
9
|
+
|
|
10
|
+
if (![3, 4, 6, 8].includes(value.length)) return null;
|
|
11
|
+
if (!/^[0-9a-fA-F]+$/.test(value)) return null;
|
|
12
|
+
|
|
13
|
+
const normalized =
|
|
14
|
+
value.length === 3 || value.length === 4
|
|
15
|
+
? value
|
|
16
|
+
.slice(0, 3)
|
|
17
|
+
.split("")
|
|
18
|
+
.map((char) => `${char}${char}`)
|
|
19
|
+
.join("")
|
|
20
|
+
: value.slice(0, 6);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
24
|
+
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
25
|
+
b: Number.parseInt(normalized.slice(4, 6), 16)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toRelativeLuminance(channel: number): number {
|
|
30
|
+
const sRGB = channel / 255;
|
|
31
|
+
|
|
32
|
+
if (sRGB <= 0.03928) return sRGB / 12.92;
|
|
33
|
+
|
|
34
|
+
return ((sRGB + 0.055) / 1.055) ** 2.4;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getLuminance(color: RGB): number {
|
|
38
|
+
return (
|
|
39
|
+
0.2126 * toRelativeLuminance(color.r) +
|
|
40
|
+
0.7152 * toRelativeLuminance(color.g) +
|
|
41
|
+
0.0722 * toRelativeLuminance(color.b)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getContrastRatio(background: RGB, foreground: RGB): number {
|
|
46
|
+
const backgroundLuminance = getLuminance(background);
|
|
47
|
+
const foregroundLuminance = getLuminance(foreground);
|
|
48
|
+
|
|
49
|
+
const lighter = Math.max(backgroundLuminance, foregroundLuminance);
|
|
50
|
+
const darker = Math.min(backgroundLuminance, foregroundLuminance);
|
|
51
|
+
|
|
52
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getAutoContrastTextColor(
|
|
56
|
+
background: string,
|
|
57
|
+
darkText: string,
|
|
58
|
+
lightText: string,
|
|
59
|
+
fallbackColor: string
|
|
60
|
+
): string {
|
|
61
|
+
const backgroundRGB = parseHexColor(background);
|
|
62
|
+
if (!backgroundRGB) return fallbackColor;
|
|
63
|
+
|
|
64
|
+
const darkTextRGB = parseHexColor(darkText);
|
|
65
|
+
const lightTextRGB = parseHexColor(lightText);
|
|
66
|
+
|
|
67
|
+
if (!darkTextRGB || !lightTextRGB) return fallbackColor;
|
|
68
|
+
|
|
69
|
+
return getContrastRatio(backgroundRGB, darkTextRGB) >=
|
|
70
|
+
getContrastRatio(backgroundRGB, lightTextRGB)
|
|
71
|
+
? darkText
|
|
72
|
+
: lightText;
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function clamp(
|
|
2
|
+
value: number,
|
|
3
|
+
min: number | undefined,
|
|
4
|
+
max: number | undefined
|
|
5
|
+
): number {
|
|
6
|
+
if (min === undefined && max === undefined) return value;
|
|
7
|
+
|
|
8
|
+
if (min !== undefined && max === undefined) return Math.max(min, value);
|
|
9
|
+
|
|
10
|
+
if (min === undefined && max !== undefined) return Math.min(max, value);
|
|
11
|
+
|
|
12
|
+
return Math.min(Math.max(value, min!), max!);
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export function createOptionalContext<ContextValue>(
|
|
4
|
+
initialValue: ContextValue | null = null
|
|
5
|
+
) {
|
|
6
|
+
const Context = createContext<ContextValue | null>(initialValue);
|
|
7
|
+
|
|
8
|
+
const useOptionalContext = () => useContext(Context);
|
|
9
|
+
|
|
10
|
+
const Provider = ({
|
|
11
|
+
value,
|
|
12
|
+
children
|
|
13
|
+
}: {
|
|
14
|
+
value: ContextValue;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) => {
|
|
17
|
+
return <Context.Provider value={value}>{children}</Context.Provider>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return [Provider, useOptionalContext] as const;
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export function createSafeContext<ContextValue>(err: string) {
|
|
4
|
+
const Context = createContext<ContextValue | null>(null);
|
|
5
|
+
|
|
6
|
+
const useSafeContext = () => {
|
|
7
|
+
const context = useContext(Context);
|
|
8
|
+
|
|
9
|
+
if (context === null) {
|
|
10
|
+
throw new Error(err);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return context;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Provider = ({
|
|
17
|
+
children,
|
|
18
|
+
value
|
|
19
|
+
}: {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
value: ContextValue;
|
|
22
|
+
}) => <Context.Provider value={value}>{children}</Context.Provider>;
|
|
23
|
+
|
|
24
|
+
return [Provider, useSafeContext] as const;
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
value: number;
|
|
3
|
+
min: number;
|
|
4
|
+
max: number;
|
|
5
|
+
step: number;
|
|
6
|
+
precision?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getChangeValue({
|
|
10
|
+
value,
|
|
11
|
+
min,
|
|
12
|
+
max,
|
|
13
|
+
step,
|
|
14
|
+
precision
|
|
15
|
+
}: Props): number {
|
|
16
|
+
const scaled = min + value * (max - min);
|
|
17
|
+
const stepped = Math.round(scaled / step) * step;
|
|
18
|
+
|
|
19
|
+
if (precision !== undefined) return Number(stepped.toFixed(precision));
|
|
20
|
+
|
|
21
|
+
return Math.min(Math.max(stepped, min), max);
|
|
22
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export interface StorageOptions<T> {
|
|
2
|
+
/** Default value if key doesn't exist */
|
|
3
|
+
defaultValue?: T;
|
|
4
|
+
|
|
5
|
+
/** Time-to-live in milliseconds */
|
|
6
|
+
ttl?: number;
|
|
7
|
+
|
|
8
|
+
/** Custom serializer function */
|
|
9
|
+
serializer?: (value: T) => string;
|
|
10
|
+
|
|
11
|
+
/** Custom deserializer function */
|
|
12
|
+
deserializer?: (value: string) => T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StorageItem<T> {
|
|
16
|
+
value: T;
|
|
17
|
+
expiry?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StorageEventPayload<T> {
|
|
21
|
+
key: string;
|
|
22
|
+
oldValue: T | null;
|
|
23
|
+
newValue: T | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type StorageListener<T> = (payload: StorageEventPayload<T>) => void;
|
|
27
|
+
|
|
28
|
+
const isBrowser = typeof window !== "undefined";
|
|
29
|
+
|
|
30
|
+
export function createStorage<T>(key: string, options: StorageOptions<T> = {}) {
|
|
31
|
+
const {
|
|
32
|
+
defaultValue,
|
|
33
|
+
ttl,
|
|
34
|
+
serializer = JSON.stringify,
|
|
35
|
+
deserializer = JSON.parse
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
const listeners = new Set<StorageListener<T>>();
|
|
39
|
+
|
|
40
|
+
const getFullKey = () => key;
|
|
41
|
+
|
|
42
|
+
const get = (): T | null => {
|
|
43
|
+
if (!isBrowser) return defaultValue ?? null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const raw = localStorage.getItem(getFullKey());
|
|
47
|
+
if (raw === null) return defaultValue ?? null;
|
|
48
|
+
|
|
49
|
+
const item: StorageItem<T> = deserializer(raw);
|
|
50
|
+
|
|
51
|
+
if (item.expiry && Date.now() > item.expiry) {
|
|
52
|
+
remove();
|
|
53
|
+
return defaultValue ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return item.value;
|
|
57
|
+
} catch {
|
|
58
|
+
return defaultValue ?? null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const set = (value: T): void => {
|
|
63
|
+
if (!isBrowser) return;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const oldValue = get();
|
|
67
|
+
|
|
68
|
+
const item: StorageItem<T> = {
|
|
69
|
+
value,
|
|
70
|
+
expiry: ttl ? Date.now() + ttl : undefined
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
localStorage.setItem(getFullKey(), serializer(item));
|
|
74
|
+
|
|
75
|
+
listeners.forEach((listener) =>
|
|
76
|
+
listener({ key: getFullKey(), oldValue, newValue: value })
|
|
77
|
+
);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn(`Failed to set localStorage key "${key}":`, error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const remove = (): void => {
|
|
84
|
+
if (!isBrowser) return;
|
|
85
|
+
|
|
86
|
+
const oldValue = get();
|
|
87
|
+
localStorage.removeItem(getFullKey());
|
|
88
|
+
|
|
89
|
+
listeners.forEach((listener) =>
|
|
90
|
+
listener({ key: getFullKey(), oldValue, newValue: null })
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const exists = (): boolean => {
|
|
95
|
+
if (!isBrowser) return false;
|
|
96
|
+
return localStorage.getItem(getFullKey()) !== null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const subscribe = (listener: StorageListener<T>): (() => void) => {
|
|
100
|
+
listeners.add(listener);
|
|
101
|
+
|
|
102
|
+
const handleStorageEvent = (event: StorageEvent) => {
|
|
103
|
+
if (event.key === getFullKey()) {
|
|
104
|
+
try {
|
|
105
|
+
const oldValue = event.oldValue
|
|
106
|
+
? (deserializer(event.oldValue) as StorageItem<T>).value
|
|
107
|
+
: null;
|
|
108
|
+
const newValue = event.newValue
|
|
109
|
+
? (deserializer(event.newValue) as StorageItem<T>).value
|
|
110
|
+
: null;
|
|
111
|
+
listener({ key: getFullKey(), oldValue, newValue });
|
|
112
|
+
} catch {
|
|
113
|
+
// Ignore parse errors
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (isBrowser) {
|
|
119
|
+
window.addEventListener("storage", handleStorageEvent);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
listeners.delete(listener);
|
|
124
|
+
if (isBrowser) {
|
|
125
|
+
window.removeEventListener("storage", handleStorageEvent);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const update = (updater: (current: T | null) => T): void => {
|
|
131
|
+
const current = get();
|
|
132
|
+
set(updater(current));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
get,
|
|
137
|
+
set,
|
|
138
|
+
remove,
|
|
139
|
+
exists,
|
|
140
|
+
subscribe,
|
|
141
|
+
update,
|
|
142
|
+
key: getFullKey()
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const storage = {
|
|
147
|
+
get<T>(key: string, defaultValue?: T): T | null {
|
|
148
|
+
if (!isBrowser) return defaultValue ?? null;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const raw = localStorage.getItem(key);
|
|
152
|
+
if (raw === null) return defaultValue ?? null;
|
|
153
|
+
|
|
154
|
+
const parsed = JSON.parse(raw);
|
|
155
|
+
|
|
156
|
+
if (parsed && typeof parsed === "object" && "value" in parsed) {
|
|
157
|
+
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
158
|
+
localStorage.removeItem(key);
|
|
159
|
+
return defaultValue ?? null;
|
|
160
|
+
}
|
|
161
|
+
return parsed.value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return parsed;
|
|
165
|
+
} catch {
|
|
166
|
+
return defaultValue ?? null;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
set<T>(key: string, value: T, ttl?: number): void {
|
|
171
|
+
if (!isBrowser) return;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const item: StorageItem<T> = {
|
|
175
|
+
value,
|
|
176
|
+
expiry: ttl ? Date.now() + ttl : undefined
|
|
177
|
+
};
|
|
178
|
+
localStorage.setItem(key, JSON.stringify(item));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn(`Failed to set localStorage key "${key}":`, error);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
remove(key: string): void {
|
|
185
|
+
if (!isBrowser) return;
|
|
186
|
+
localStorage.removeItem(key);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
clear(): void {
|
|
190
|
+
if (!isBrowser) return;
|
|
191
|
+
localStorage.clear();
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
keys(): string[] {
|
|
195
|
+
if (!isBrowser) return [];
|
|
196
|
+
return Object.keys(localStorage);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
size(): number {
|
|
200
|
+
if (!isBrowser) return 0;
|
|
201
|
+
return localStorage.length;
|
|
202
|
+
}
|
|
203
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./build",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"allowImportingTsExtensions": false
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "build"]
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts","./src/hooks/index.ts","./src/hooks/use-click-outside/index.ts","./src/hooks/use-debounced-callback/index.ts","./src/hooks/use-disclosure/index.ts","./src/hooks/use-id/index.ts","./src/hooks/use-isomorphic-layout-effect/index.ts","./src/hooks/use-merged-refs/index.ts","./src/hooks/use-resize-observer/index.ts","./src/hooks/use-uncontrolled/index.ts","./src/utils/index.ts","./src/utils/auto-contrast/index.ts","./src/utils/clamp/index.ts","./src/utils/context/index.ts","./src/utils/context/optional-context.tsx","./src/utils/context/safe-context.tsx","./src/utils/get-change-value/index.ts","./src/utils/storage/index.ts"],"version":"5.9.3"}
|