@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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/get-change-value/index.ts"],"names":[],"mappings":"AAAA,KAAK,KAAK,GAAG;IACT,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,GAAG,EACH,GAAG,EACH,IAAI,EACJ,SAAS,EACZ,EAAE,KAAK,GAAG,MAAM,CAOhB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function getChangeValue({ value, min, max, step, precision }) {
|
|
2
|
+
const scaled = min + value * (max - min);
|
|
3
|
+
const stepped = Math.round(scaled / step) * step;
|
|
4
|
+
if (precision !== undefined)
|
|
5
|
+
return Number(stepped.toFixed(precision));
|
|
6
|
+
return Math.min(Math.max(stepped, min), max);
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface StorageOptions<T> {
|
|
2
|
+
/** Default value if key doesn't exist */
|
|
3
|
+
defaultValue?: T;
|
|
4
|
+
/** Time-to-live in milliseconds */
|
|
5
|
+
ttl?: number;
|
|
6
|
+
/** Custom serializer function */
|
|
7
|
+
serializer?: (value: T) => string;
|
|
8
|
+
/** Custom deserializer function */
|
|
9
|
+
deserializer?: (value: string) => T;
|
|
10
|
+
}
|
|
11
|
+
export interface StorageItem<T> {
|
|
12
|
+
value: T;
|
|
13
|
+
expiry?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface StorageEventPayload<T> {
|
|
16
|
+
key: string;
|
|
17
|
+
oldValue: T | null;
|
|
18
|
+
newValue: T | null;
|
|
19
|
+
}
|
|
20
|
+
type StorageListener<T> = (payload: StorageEventPayload<T>) => void;
|
|
21
|
+
export declare function createStorage<T>(key: string, options?: StorageOptions<T>): {
|
|
22
|
+
get: () => T | null;
|
|
23
|
+
set: (value: T) => void;
|
|
24
|
+
remove: () => void;
|
|
25
|
+
exists: () => boolean;
|
|
26
|
+
subscribe: (listener: StorageListener<T>) => (() => void);
|
|
27
|
+
update: (updater: (current: T | null) => T) => void;
|
|
28
|
+
key: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const storage: {
|
|
31
|
+
get<T>(key: string, defaultValue?: T): T | null;
|
|
32
|
+
set<T>(key: string, value: T, ttl?: number): void;
|
|
33
|
+
remove(key: string): void;
|
|
34
|
+
clear(): void;
|
|
35
|
+
keys(): string[];
|
|
36
|
+
size(): number;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/storage/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc,CAAC,CAAC;IAC7B,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,CAAC;IAEjB,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,iCAAiC;IACjC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAElC,mCAAmC;IACnC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC;CACtB;AAED,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAIpE,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAc,CAAC,CAAC,CAAM;eAYzD,CAAC,GAAG,IAAI;iBAoBJ,CAAC,KAAG,IAAI;kBAqBT,IAAI;kBAWJ,OAAO;0BAKG,eAAe,CAAC,CAAC,CAAC,KAAG,CAAC,MAAM,IAAI,CAAC;sBA+BrC,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,KAAG,IAAI;;EAc3D;AAED,eAAO,MAAM,OAAO;QACZ,CAAC,OAAO,MAAM,iBAAiB,CAAC,GAAG,CAAC,GAAG,IAAI;QAuB3C,CAAC,OAAO,MAAM,SAAS,CAAC,QAAQ,MAAM,GAAG,IAAI;gBAcrC,MAAM,GAAG,IAAI;aAKhB,IAAI;YAKL,MAAM,EAAE;YAKR,MAAM;CAIjB,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const isBrowser = typeof window !== "undefined";
|
|
2
|
+
export function createStorage(key, options = {}) {
|
|
3
|
+
const { defaultValue, ttl, serializer = JSON.stringify, deserializer = JSON.parse } = options;
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const getFullKey = () => key;
|
|
6
|
+
const get = () => {
|
|
7
|
+
if (!isBrowser)
|
|
8
|
+
return defaultValue ?? null;
|
|
9
|
+
try {
|
|
10
|
+
const raw = localStorage.getItem(getFullKey());
|
|
11
|
+
if (raw === null)
|
|
12
|
+
return defaultValue ?? null;
|
|
13
|
+
const item = deserializer(raw);
|
|
14
|
+
if (item.expiry && Date.now() > item.expiry) {
|
|
15
|
+
remove();
|
|
16
|
+
return defaultValue ?? null;
|
|
17
|
+
}
|
|
18
|
+
return item.value;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return defaultValue ?? null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const set = (value) => {
|
|
25
|
+
if (!isBrowser)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const oldValue = get();
|
|
29
|
+
const item = {
|
|
30
|
+
value,
|
|
31
|
+
expiry: ttl ? Date.now() + ttl : undefined
|
|
32
|
+
};
|
|
33
|
+
localStorage.setItem(getFullKey(), serializer(item));
|
|
34
|
+
listeners.forEach((listener) => listener({ key: getFullKey(), oldValue, newValue: value }));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.warn(`Failed to set localStorage key "${key}":`, error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const remove = () => {
|
|
41
|
+
if (!isBrowser)
|
|
42
|
+
return;
|
|
43
|
+
const oldValue = get();
|
|
44
|
+
localStorage.removeItem(getFullKey());
|
|
45
|
+
listeners.forEach((listener) => listener({ key: getFullKey(), oldValue, newValue: null }));
|
|
46
|
+
};
|
|
47
|
+
const exists = () => {
|
|
48
|
+
if (!isBrowser)
|
|
49
|
+
return false;
|
|
50
|
+
return localStorage.getItem(getFullKey()) !== null;
|
|
51
|
+
};
|
|
52
|
+
const subscribe = (listener) => {
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
const handleStorageEvent = (event) => {
|
|
55
|
+
if (event.key === getFullKey()) {
|
|
56
|
+
try {
|
|
57
|
+
const oldValue = event.oldValue
|
|
58
|
+
? deserializer(event.oldValue).value
|
|
59
|
+
: null;
|
|
60
|
+
const newValue = event.newValue
|
|
61
|
+
? deserializer(event.newValue).value
|
|
62
|
+
: null;
|
|
63
|
+
listener({ key: getFullKey(), oldValue, newValue });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore parse errors
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
if (isBrowser) {
|
|
71
|
+
window.addEventListener("storage", handleStorageEvent);
|
|
72
|
+
}
|
|
73
|
+
return () => {
|
|
74
|
+
listeners.delete(listener);
|
|
75
|
+
if (isBrowser) {
|
|
76
|
+
window.removeEventListener("storage", handleStorageEvent);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const update = (updater) => {
|
|
81
|
+
const current = get();
|
|
82
|
+
set(updater(current));
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
get,
|
|
86
|
+
set,
|
|
87
|
+
remove,
|
|
88
|
+
exists,
|
|
89
|
+
subscribe,
|
|
90
|
+
update,
|
|
91
|
+
key: getFullKey()
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export const storage = {
|
|
95
|
+
get(key, defaultValue) {
|
|
96
|
+
if (!isBrowser)
|
|
97
|
+
return defaultValue ?? null;
|
|
98
|
+
try {
|
|
99
|
+
const raw = localStorage.getItem(key);
|
|
100
|
+
if (raw === null)
|
|
101
|
+
return defaultValue ?? null;
|
|
102
|
+
const parsed = JSON.parse(raw);
|
|
103
|
+
if (parsed && typeof parsed === "object" && "value" in parsed) {
|
|
104
|
+
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
105
|
+
localStorage.removeItem(key);
|
|
106
|
+
return defaultValue ?? null;
|
|
107
|
+
}
|
|
108
|
+
return parsed.value;
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return defaultValue ?? null;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
set(key, value, ttl) {
|
|
117
|
+
if (!isBrowser)
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const item = {
|
|
121
|
+
value,
|
|
122
|
+
expiry: ttl ? Date.now() + ttl : undefined
|
|
123
|
+
};
|
|
124
|
+
localStorage.setItem(key, JSON.stringify(item));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.warn(`Failed to set localStorage key "${key}":`, error);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
remove(key) {
|
|
131
|
+
if (!isBrowser)
|
|
132
|
+
return;
|
|
133
|
+
localStorage.removeItem(key);
|
|
134
|
+
},
|
|
135
|
+
clear() {
|
|
136
|
+
if (!isBrowser)
|
|
137
|
+
return;
|
|
138
|
+
localStorage.clear();
|
|
139
|
+
},
|
|
140
|
+
keys() {
|
|
141
|
+
if (!isBrowser)
|
|
142
|
+
return [];
|
|
143
|
+
return Object.keys(localStorage);
|
|
144
|
+
},
|
|
145
|
+
size() {
|
|
146
|
+
if (!isBrowser)
|
|
147
|
+
return 0;
|
|
148
|
+
return localStorage.length;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/utils/storage/storage.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.test.d.ts","sourceRoot":"","sources":["../../../src/utils/storage/storage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createStorage, createStorageAdapter, storage } from "./index";
|
|
3
|
+
const originalWindow = globalThis.window;
|
|
4
|
+
const createMockStorage = () => {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
return {
|
|
7
|
+
get length() {
|
|
8
|
+
return map.size;
|
|
9
|
+
},
|
|
10
|
+
clear() {
|
|
11
|
+
map.clear();
|
|
12
|
+
},
|
|
13
|
+
getItem(key) {
|
|
14
|
+
return map.has(key) ? map.get(key) : null;
|
|
15
|
+
},
|
|
16
|
+
key(index) {
|
|
17
|
+
return Array.from(map.keys())[index] ?? null;
|
|
18
|
+
},
|
|
19
|
+
removeItem(key) {
|
|
20
|
+
map.delete(key);
|
|
21
|
+
},
|
|
22
|
+
setItem(key, value) {
|
|
23
|
+
map.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const installMockWindow = () => {
|
|
28
|
+
const localStorage = createMockStorage();
|
|
29
|
+
const sessionStorage = createMockStorage();
|
|
30
|
+
const listeners = new Set();
|
|
31
|
+
const windowMock = {
|
|
32
|
+
localStorage,
|
|
33
|
+
sessionStorage,
|
|
34
|
+
addEventListener(type, listener) {
|
|
35
|
+
if (type === "storage" && typeof listener === "function") {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
removeEventListener(type, listener) {
|
|
40
|
+
if (type === "storage" && typeof listener === "function") {
|
|
41
|
+
listeners.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
dispatchStorageEvent(event) {
|
|
45
|
+
listeners.forEach((listener) => listener(event));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(globalThis, "window", {
|
|
49
|
+
value: windowMock,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true
|
|
52
|
+
});
|
|
53
|
+
return windowMock;
|
|
54
|
+
};
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.useRealTimers();
|
|
57
|
+
if (originalWindow === undefined) {
|
|
58
|
+
delete globalThis.window;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
Object.defineProperty(globalThis, "window", {
|
|
62
|
+
value: originalWindow,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
describe("createStorageAdapter", () => {
|
|
69
|
+
it("creates in-memory adapters that can store values", () => {
|
|
70
|
+
const adapter = createStorageAdapter("memory");
|
|
71
|
+
adapter.setItem("theme", "dark");
|
|
72
|
+
expect(adapter.getItem("theme")).toBe("dark");
|
|
73
|
+
expect(adapter.size()).toBe(1);
|
|
74
|
+
expect(adapter.keys()).toEqual(["theme"]);
|
|
75
|
+
});
|
|
76
|
+
it("creates independent memory adapters", () => {
|
|
77
|
+
const first = createStorageAdapter("memory");
|
|
78
|
+
const second = createStorageAdapter("memory");
|
|
79
|
+
first.setItem("theme", "dark");
|
|
80
|
+
expect(first.getItem("theme")).toBe("dark");
|
|
81
|
+
expect(second.getItem("theme")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe("createStorage", () => {
|
|
85
|
+
it("supports session adapter selection", () => {
|
|
86
|
+
const windowMock = installMockWindow();
|
|
87
|
+
const sessionStore = createStorage("theme", {
|
|
88
|
+
adapter: "session"
|
|
89
|
+
});
|
|
90
|
+
sessionStore.set("dark");
|
|
91
|
+
expect(windowMock.sessionStorage.getItem("theme")).not.toBeNull();
|
|
92
|
+
expect(windowMock.localStorage.getItem("theme")).toBeNull();
|
|
93
|
+
expect(sessionStore.get()).toBe("dark");
|
|
94
|
+
});
|
|
95
|
+
it("supports memory adapter without browser APIs", () => {
|
|
96
|
+
delete globalThis.window;
|
|
97
|
+
const memoryStore = createStorage("count", {
|
|
98
|
+
adapter: "memory",
|
|
99
|
+
defaultValue: 0
|
|
100
|
+
});
|
|
101
|
+
memoryStore.set(2);
|
|
102
|
+
memoryStore.update((value) => (value ?? 0) + 1);
|
|
103
|
+
expect(memoryStore.get()).toBe(3);
|
|
104
|
+
});
|
|
105
|
+
it("expires values based on TTL", () => {
|
|
106
|
+
vi.useFakeTimers();
|
|
107
|
+
const memoryStore = createStorage("token", {
|
|
108
|
+
adapter: "memory",
|
|
109
|
+
ttl: 1000
|
|
110
|
+
});
|
|
111
|
+
memoryStore.set("abc");
|
|
112
|
+
vi.advanceTimersByTime(1001);
|
|
113
|
+
expect(memoryStore.get()).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
it("subscribes to local writes and cross-context storage events", () => {
|
|
116
|
+
const windowMock = installMockWindow();
|
|
117
|
+
const localStore = createStorage("theme", { adapter: "local" });
|
|
118
|
+
const listener = vi.fn();
|
|
119
|
+
const unsubscribe = localStore.subscribe(listener);
|
|
120
|
+
localStore.set("dark");
|
|
121
|
+
windowMock.dispatchStorageEvent({
|
|
122
|
+
key: "theme",
|
|
123
|
+
oldValue: JSON.stringify({ value: "dark" }),
|
|
124
|
+
newValue: JSON.stringify({ value: "light" }),
|
|
125
|
+
storageArea: windowMock.localStorage
|
|
126
|
+
});
|
|
127
|
+
expect(listener).toHaveBeenNthCalledWith(1, {
|
|
128
|
+
key: "theme",
|
|
129
|
+
oldValue: null,
|
|
130
|
+
newValue: "dark"
|
|
131
|
+
});
|
|
132
|
+
expect(listener).toHaveBeenNthCalledWith(2, {
|
|
133
|
+
key: "theme",
|
|
134
|
+
oldValue: "dark",
|
|
135
|
+
newValue: "light"
|
|
136
|
+
});
|
|
137
|
+
unsubscribe();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe("storage", () => {
|
|
141
|
+
it("keeps local storage as default and exposes session/memory clients", () => {
|
|
142
|
+
const windowMock = installMockWindow();
|
|
143
|
+
storage.set("theme", "dark");
|
|
144
|
+
storage.session.set("theme", "light");
|
|
145
|
+
storage.memory.set("theme", "system");
|
|
146
|
+
expect(storage.get("theme")).toBe("dark");
|
|
147
|
+
expect(storage.local.get("theme")).toBe("dark");
|
|
148
|
+
expect(storage.session.get("theme")).toBe("light");
|
|
149
|
+
expect(storage.memory.get("theme")).toBe("system");
|
|
150
|
+
expect(windowMock.localStorage.getItem("theme")).not.toBeNull();
|
|
151
|
+
expect(windowMock.sessionStorage.getItem("theme")).not.toBeNull();
|
|
152
|
+
});
|
|
153
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@refraktor/utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "./build/index.js",
|
|
9
|
+
"module": "./build/index.js",
|
|
10
|
+
"types": "./build/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./build/index.d.ts",
|
|
14
|
+
"import": "./build/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"clean": "rm -rf build"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^19.2.7",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { useId } from "./use-id";
|
|
2
|
+
export { useDisclosure } from "./use-disclosure";
|
|
3
|
+
export { useClickOutside } from "./use-click-outside";
|
|
4
|
+
export { useResizeObserver } from "./use-resize-observer";
|
|
5
|
+
export { useMergedRefs } from "./use-merged-refs";
|
|
6
|
+
export { useUncontrolled } from "./use-uncontrolled";
|
|
7
|
+
export { useDebouncedCallback } from "./use-debounced-callback";
|
|
8
|
+
export { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export type UseClickOutsideEvent = keyof DocumentEventMap;
|
|
4
|
+
|
|
5
|
+
type UseClickOutsideNode = HTMLElement | null;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EVENTS: UseClickOutsideEvent[] = ["mousedown", "touchstart"];
|
|
8
|
+
const DEFAULT_NODES: UseClickOutsideNode[] = [];
|
|
9
|
+
|
|
10
|
+
export function useClickOutside<T extends HTMLElement = HTMLElement>(
|
|
11
|
+
handler: (event: Event) => void,
|
|
12
|
+
events: UseClickOutsideEvent[] = DEFAULT_EVENTS,
|
|
13
|
+
nodes: UseClickOutsideNode[] = DEFAULT_NODES
|
|
14
|
+
) {
|
|
15
|
+
const ref = useRef<T>(null);
|
|
16
|
+
const handlerRef = useRef(handler);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
handlerRef.current = handler;
|
|
20
|
+
}, [handler]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const listener = (event: Event) => {
|
|
24
|
+
const target = event.target;
|
|
25
|
+
|
|
26
|
+
if (!(target instanceof Node)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const element = ref.current;
|
|
31
|
+
|
|
32
|
+
if (!element) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const path =
|
|
37
|
+
typeof event.composedPath === "function"
|
|
38
|
+
? event.composedPath()
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
const isInsideElement = path
|
|
42
|
+
? path.includes(element)
|
|
43
|
+
: element.contains(target);
|
|
44
|
+
|
|
45
|
+
if (isInsideElement) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isInsideAdditionalNode = nodes.some((node) => {
|
|
50
|
+
if (!node) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return path ? path.includes(node) : node.contains(target);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (isInsideAdditionalNode) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
handlerRef.current(event);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const eventName of events) {
|
|
65
|
+
document.addEventListener(eventName, listener);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
for (const eventName of events) {
|
|
70
|
+
document.removeEventListener(eventName, listener);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}, [events, nodes]);
|
|
74
|
+
|
|
75
|
+
return ref;
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseDebouncedCallbackOptions {
|
|
4
|
+
leading?: boolean;
|
|
5
|
+
trailing?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type UseDebouncedCallbackReturn<
|
|
9
|
+
TArgs extends unknown[],
|
|
10
|
+
TResult
|
|
11
|
+
> = ((...args: TArgs) => void) & {
|
|
12
|
+
cancel: () => void;
|
|
13
|
+
flush: () => TResult | undefined;
|
|
14
|
+
isPending: () => boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function useDebouncedCallback<TArgs extends unknown[], TResult>(
|
|
18
|
+
callback: (...args: TArgs) => TResult,
|
|
19
|
+
delay: number,
|
|
20
|
+
options: UseDebouncedCallbackOptions = {}
|
|
21
|
+
): UseDebouncedCallbackReturn<TArgs, TResult> {
|
|
22
|
+
const { leading = false, trailing = true } = options;
|
|
23
|
+
const wait = Math.max(0, delay);
|
|
24
|
+
|
|
25
|
+
const callbackRef = useRef(callback);
|
|
26
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
27
|
+
const lastArgsRef = useRef<TArgs | null>(null);
|
|
28
|
+
const shouldCallTrailingRef = useRef(false);
|
|
29
|
+
const resultRef = useRef<TResult | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
callbackRef.current = callback;
|
|
33
|
+
}, [callback]);
|
|
34
|
+
|
|
35
|
+
const invoke = useCallback((args: TArgs) => {
|
|
36
|
+
const result = callbackRef.current(...args);
|
|
37
|
+
resultRef.current = result;
|
|
38
|
+
return result;
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const clearTimer = useCallback(() => {
|
|
42
|
+
if (timeoutRef.current !== null) {
|
|
43
|
+
clearTimeout(timeoutRef.current);
|
|
44
|
+
timeoutRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const cancel = useCallback(() => {
|
|
49
|
+
clearTimer();
|
|
50
|
+
shouldCallTrailingRef.current = false;
|
|
51
|
+
lastArgsRef.current = null;
|
|
52
|
+
}, [clearTimer]);
|
|
53
|
+
|
|
54
|
+
const flush = useCallback(() => {
|
|
55
|
+
if (timeoutRef.current === null) {
|
|
56
|
+
return resultRef.current;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clearTimer();
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
trailing &&
|
|
63
|
+
shouldCallTrailingRef.current &&
|
|
64
|
+
lastArgsRef.current !== null
|
|
65
|
+
) {
|
|
66
|
+
const next = invoke(lastArgsRef.current);
|
|
67
|
+
shouldCallTrailingRef.current = false;
|
|
68
|
+
lastArgsRef.current = null;
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
shouldCallTrailingRef.current = false;
|
|
73
|
+
lastArgsRef.current = null;
|
|
74
|
+
return resultRef.current;
|
|
75
|
+
}, [clearTimer, invoke, trailing]);
|
|
76
|
+
|
|
77
|
+
const isPending = useCallback(() => timeoutRef.current !== null, []);
|
|
78
|
+
|
|
79
|
+
const debounced = useMemo(() => {
|
|
80
|
+
const fn = (...args: TArgs) => {
|
|
81
|
+
if (!leading && !trailing) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hasTimer = timeoutRef.current !== null;
|
|
86
|
+
|
|
87
|
+
if (!hasTimer) {
|
|
88
|
+
if (leading) {
|
|
89
|
+
invoke(args);
|
|
90
|
+
shouldCallTrailingRef.current = false;
|
|
91
|
+
lastArgsRef.current = null;
|
|
92
|
+
} else {
|
|
93
|
+
shouldCallTrailingRef.current = true;
|
|
94
|
+
lastArgsRef.current = args;
|
|
95
|
+
}
|
|
96
|
+
} else if (trailing) {
|
|
97
|
+
shouldCallTrailingRef.current = true;
|
|
98
|
+
lastArgsRef.current = args;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clearTimer();
|
|
102
|
+
timeoutRef.current = setTimeout(() => {
|
|
103
|
+
timeoutRef.current = null;
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
trailing &&
|
|
107
|
+
shouldCallTrailingRef.current &&
|
|
108
|
+
lastArgsRef.current !== null
|
|
109
|
+
) {
|
|
110
|
+
invoke(lastArgsRef.current);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
shouldCallTrailingRef.current = false;
|
|
114
|
+
lastArgsRef.current = null;
|
|
115
|
+
}, wait);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return Object.assign(fn, {
|
|
119
|
+
cancel,
|
|
120
|
+
flush,
|
|
121
|
+
isPending
|
|
122
|
+
});
|
|
123
|
+
}, [cancel, clearTimer, flush, invoke, isPending, leading, trailing, wait]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => cancel, [cancel]);
|
|
126
|
+
|
|
127
|
+
return debounced;
|
|
128
|
+
}
|