@refraktor/utils 0.0.4 → 0.0.5
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/build/hooks/index.d.ts +1 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +1 -0
- package/build/hooks/use-keybind/index.d.ts +12 -0
- package/build/hooks/use-keybind/index.d.ts.map +1 -0
- package/build/hooks/use-keybind/index.js +126 -0
- package/build/utils/keybind/index.d.ts +31 -0
- package/build/utils/keybind/index.d.ts.map +1 -0
- package/build/utils/keybind/index.js +192 -0
- package/package.json +23 -2
- package/.turbo/turbo-build.log +0 -1
- package/refraktor-utils-0.0.1-alpha.0.tgz +0 -0
- package/src/hooks/index.ts +0 -8
- package/src/hooks/use-click-outside/index.ts +0 -76
- package/src/hooks/use-debounced-callback/index.ts +0 -128
- package/src/hooks/use-disclosure/index.ts +0 -67
- package/src/hooks/use-id/index.ts +0 -6
- package/src/hooks/use-isomorphic-layout-effect/index.ts +0 -4
- package/src/hooks/use-merged-refs/index.ts +0 -45
- package/src/hooks/use-resize-observer/index.ts +0 -105
- package/src/hooks/use-uncontrolled/index.ts +0 -36
- package/src/index.ts +0 -2
- package/src/utils/auto-contrast/index.ts +0 -73
- package/src/utils/clamp/index.ts +0 -13
- package/src/utils/context/index.ts +0 -2
- package/src/utils/context/optional-context.tsx +0 -21
- package/src/utils/context/safe-context.tsx +0 -25
- package/src/utils/get-change-value/index.ts +0 -22
- package/src/utils/index.ts +0 -5
- package/src/utils/storage/index.ts +0 -203
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
package/build/hooks/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export { useMergedRefs } from "./use-merged-refs";
|
|
|
6
6
|
export { useUncontrolled } from "./use-uncontrolled";
|
|
7
7
|
export { useDebouncedCallback } from "./use-debounced-callback";
|
|
8
8
|
export { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";
|
|
9
|
+
export { useKeybind } from "./use-keybind";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
package/build/hooks/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { useMergedRefs } from "./use-merged-refs";
|
|
|
6
6
|
export { useUncontrolled } from "./use-uncontrolled";
|
|
7
7
|
export { useDebouncedCallback } from "./use-debounced-callback";
|
|
8
8
|
export { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";
|
|
9
|
+
export { useKeybind } from "./use-keybind";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type UseKeybindEvent = "keydown" | "keyup";
|
|
2
|
+
export interface UseKeybindOptions {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
event?: UseKeybindEvent;
|
|
5
|
+
target?: Window | Document | HTMLElement | null;
|
|
6
|
+
preventDefault?: boolean;
|
|
7
|
+
stopPropagation?: boolean;
|
|
8
|
+
ignoreInputFields?: boolean;
|
|
9
|
+
allowRepeat?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function useKeybind(combo: string, handler: (event: KeyboardEvent) => void, options?: UseKeybindOptions): void;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-keybind/index.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,IAAI,CAAC;IAChD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAqGD,wBAAgB,UAAU,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EACvC,OAAO,GAAE,iBAAsB,GAChC,IAAI,CAkEN"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
const MODIFIER_ALIASES = {
|
|
3
|
+
ctrl: "ctrl",
|
|
4
|
+
control: "ctrl",
|
|
5
|
+
shift: "shift",
|
|
6
|
+
alt: "alt",
|
|
7
|
+
option: "alt",
|
|
8
|
+
meta: "meta",
|
|
9
|
+
cmd: "meta",
|
|
10
|
+
command: "meta",
|
|
11
|
+
super: "meta",
|
|
12
|
+
win: "meta"
|
|
13
|
+
};
|
|
14
|
+
const KEY_ALIASES = {
|
|
15
|
+
esc: "escape",
|
|
16
|
+
return: "enter",
|
|
17
|
+
del: "delete",
|
|
18
|
+
plus: "+",
|
|
19
|
+
space: " ",
|
|
20
|
+
spacebar: " "
|
|
21
|
+
};
|
|
22
|
+
function normalizeToken(token) {
|
|
23
|
+
return token.trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function normalizeKey(key) {
|
|
26
|
+
const normalized = key.trim().toLowerCase();
|
|
27
|
+
return KEY_ALIASES[normalized] ?? normalized;
|
|
28
|
+
}
|
|
29
|
+
function parseCombo(combo) {
|
|
30
|
+
const tokens = combo
|
|
31
|
+
.split("+")
|
|
32
|
+
.map((token) => normalizeToken(token))
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
if (tokens.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const modifiers = new Set();
|
|
38
|
+
let key = null;
|
|
39
|
+
for (const token of tokens) {
|
|
40
|
+
const modifier = MODIFIER_ALIASES[token];
|
|
41
|
+
if (modifier) {
|
|
42
|
+
modifiers.add(modifier);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (key !== null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
key = normalizeKey(token);
|
|
49
|
+
}
|
|
50
|
+
return { modifiers, key };
|
|
51
|
+
}
|
|
52
|
+
function isTypingTarget(target) {
|
|
53
|
+
if (!(target instanceof HTMLElement)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (target.isContentEditable) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const tagName = target.tagName;
|
|
60
|
+
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
61
|
+
}
|
|
62
|
+
function matchesCombo(event, parsed) {
|
|
63
|
+
if (event.ctrlKey !== parsed.modifiers.has("ctrl")) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (event.shiftKey !== parsed.modifiers.has("shift")) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (event.altKey !== parsed.modifiers.has("alt")) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (event.metaKey !== parsed.modifiers.has("meta")) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (parsed.key === null) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return normalizeKey(event.key) === parsed.key;
|
|
79
|
+
}
|
|
80
|
+
export function useKeybind(combo, handler, options = {}) {
|
|
81
|
+
const { enabled = true, event: eventName = "keydown", target = typeof window !== "undefined" ? window : null, preventDefault = false, stopPropagation = false, ignoreInputFields = true, allowRepeat = false } = options;
|
|
82
|
+
const parsed = useMemo(() => parseCombo(combo), [combo]);
|
|
83
|
+
const handlerRef = useRef(handler);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
handlerRef.current = handler;
|
|
86
|
+
}, [handler]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!enabled || !target || parsed === null) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const listener = (keyboardEvent) => {
|
|
92
|
+
if (!(keyboardEvent instanceof KeyboardEvent)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!allowRepeat && keyboardEvent.repeat) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (ignoreInputFields && isTypingTarget(keyboardEvent.target)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!matchesCombo(keyboardEvent, parsed)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (preventDefault) {
|
|
105
|
+
keyboardEvent.preventDefault();
|
|
106
|
+
}
|
|
107
|
+
if (stopPropagation) {
|
|
108
|
+
keyboardEvent.stopPropagation();
|
|
109
|
+
}
|
|
110
|
+
handlerRef.current(keyboardEvent);
|
|
111
|
+
};
|
|
112
|
+
target.addEventListener(eventName, listener);
|
|
113
|
+
return () => {
|
|
114
|
+
target.removeEventListener(eventName, listener);
|
|
115
|
+
};
|
|
116
|
+
}, [
|
|
117
|
+
allowRepeat,
|
|
118
|
+
enabled,
|
|
119
|
+
eventName,
|
|
120
|
+
ignoreInputFields,
|
|
121
|
+
parsed,
|
|
122
|
+
preventDefault,
|
|
123
|
+
stopPropagation,
|
|
124
|
+
target
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface KeyboardLikeEvent {
|
|
2
|
+
key: string;
|
|
3
|
+
altKey: boolean;
|
|
4
|
+
ctrlKey: boolean;
|
|
5
|
+
metaKey: boolean;
|
|
6
|
+
shiftKey: boolean;
|
|
7
|
+
repeat: boolean;
|
|
8
|
+
defaultPrevented: boolean;
|
|
9
|
+
preventDefault: () => void;
|
|
10
|
+
stopPropagation: () => void;
|
|
11
|
+
}
|
|
12
|
+
export type KeybindCombo = string;
|
|
13
|
+
export interface KeybindMatchOptions {
|
|
14
|
+
exactModifiers?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface KeybindBinding<E extends KeyboardLikeEvent = KeyboardLikeEvent> {
|
|
17
|
+
combo: KeybindCombo | KeybindCombo[];
|
|
18
|
+
handler: (event: E) => void;
|
|
19
|
+
preventDefault?: boolean;
|
|
20
|
+
stopPropagation?: boolean;
|
|
21
|
+
allowRepeat?: boolean;
|
|
22
|
+
when?: boolean | ((event: E) => boolean);
|
|
23
|
+
exactModifiers?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface CreateKeybindHandlerOptions {
|
|
26
|
+
skipIfDefaultPrevented?: boolean;
|
|
27
|
+
stopAtFirstMatch?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function matchesKeybind(event: KeyboardLikeEvent, combo: KeybindCombo, options?: KeybindMatchOptions): boolean;
|
|
30
|
+
export declare function createKeybindHandler<E extends KeyboardLikeEvent = KeyboardLikeEvent>(bindings: KeybindBinding<E>[], options?: CreateKeybindHandlerOptions): (event: E) => void;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/keybind/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,WAAW,mBAAmB;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,iBAAiB,GAAG,iBAAiB;IAC3E,KAAK,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IACrC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC;IACzC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IACxC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC9B;AA8LD,wBAAgB,cAAc,CAC1B,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,mBAAwB,GAClC,OAAO,CAQT;AAED,wBAAgB,oBAAoB,CAChC,CAAC,SAAS,iBAAiB,GAAG,iBAAiB,EAE/C,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,EAC7B,OAAO,GAAE,2BAAgC,GAC1C,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CA4EpB"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const KEY_ALIASES = {
|
|
2
|
+
esc: "Escape",
|
|
3
|
+
escape: "Escape",
|
|
4
|
+
enter: "Enter",
|
|
5
|
+
return: "Enter",
|
|
6
|
+
space: " ",
|
|
7
|
+
spacebar: " ",
|
|
8
|
+
left: "ArrowLeft",
|
|
9
|
+
right: "ArrowRight",
|
|
10
|
+
up: "ArrowUp",
|
|
11
|
+
down: "ArrowDown",
|
|
12
|
+
del: "Delete",
|
|
13
|
+
plus: "+"
|
|
14
|
+
};
|
|
15
|
+
const keybindCache = new Map();
|
|
16
|
+
const isApplePlatform = () => {
|
|
17
|
+
if (typeof navigator === "undefined") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const platform = navigator.platform || navigator.userAgent;
|
|
21
|
+
return /Mac|iPod|iPhone|iPad/i.test(platform);
|
|
22
|
+
};
|
|
23
|
+
const normalizeKey = (value) => {
|
|
24
|
+
const key = value.length === 1 ? value : value.trim();
|
|
25
|
+
if (key.length === 0) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
const aliased = KEY_ALIASES[key.toLowerCase()];
|
|
29
|
+
if (aliased) {
|
|
30
|
+
return aliased;
|
|
31
|
+
}
|
|
32
|
+
if (key.length === 1) {
|
|
33
|
+
return key.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
return key;
|
|
36
|
+
};
|
|
37
|
+
const parseKeybind = (combo) => {
|
|
38
|
+
const cached = keybindCache.get(combo);
|
|
39
|
+
if (cached !== undefined) {
|
|
40
|
+
return cached;
|
|
41
|
+
}
|
|
42
|
+
const tokens = combo
|
|
43
|
+
.split("+")
|
|
44
|
+
.map((token) => token.trim())
|
|
45
|
+
.filter((token) => token.length > 0);
|
|
46
|
+
if (tokens.length === 0) {
|
|
47
|
+
keybindCache.set(combo, null);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const parsed = {
|
|
51
|
+
key: "",
|
|
52
|
+
alt: false,
|
|
53
|
+
ctrl: false,
|
|
54
|
+
meta: false,
|
|
55
|
+
shift: false,
|
|
56
|
+
mod: false
|
|
57
|
+
};
|
|
58
|
+
for (const token of tokens) {
|
|
59
|
+
const normalizedToken = token.toLowerCase();
|
|
60
|
+
if (normalizedToken === "alt" || normalizedToken === "option") {
|
|
61
|
+
parsed.alt = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (normalizedToken === "ctrl" ||
|
|
65
|
+
normalizedToken === "control" ||
|
|
66
|
+
normalizedToken === "ctl") {
|
|
67
|
+
parsed.ctrl = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (normalizedToken === "meta" ||
|
|
71
|
+
normalizedToken === "cmd" ||
|
|
72
|
+
normalizedToken === "command") {
|
|
73
|
+
parsed.meta = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (normalizedToken === "shift") {
|
|
77
|
+
parsed.shift = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (normalizedToken === "mod") {
|
|
81
|
+
parsed.mod = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (parsed.key.length > 0) {
|
|
85
|
+
keybindCache.set(combo, null);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
parsed.key = normalizeKey(token);
|
|
89
|
+
}
|
|
90
|
+
if (parsed.key.length === 0) {
|
|
91
|
+
keybindCache.set(combo, null);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
keybindCache.set(combo, parsed);
|
|
95
|
+
return parsed;
|
|
96
|
+
};
|
|
97
|
+
const matchesParsedKeybind = (event, parsed, options = {}) => {
|
|
98
|
+
const eventKey = normalizeKey(event.key);
|
|
99
|
+
if (eventKey !== parsed.key) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const applePlatform = isApplePlatform();
|
|
103
|
+
const requiresCtrl = parsed.ctrl || (parsed.mod && !applePlatform);
|
|
104
|
+
const requiresMeta = parsed.meta || (parsed.mod && applePlatform);
|
|
105
|
+
if (parsed.alt && !event.altKey) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (parsed.shift && !event.shiftKey) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (requiresCtrl && !event.ctrlKey) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (requiresMeta && !event.metaKey) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (!options.exactModifiers) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (!parsed.alt && event.altKey) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (!parsed.shift && event.shiftKey) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (!requiresCtrl && event.ctrlKey) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (!requiresMeta && event.metaKey) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
134
|
+
export function matchesKeybind(event, combo, options = {}) {
|
|
135
|
+
const parsed = parseKeybind(combo);
|
|
136
|
+
if (!parsed) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return matchesParsedKeybind(event, parsed, options);
|
|
140
|
+
}
|
|
141
|
+
export function createKeybindHandler(bindings, options = {}) {
|
|
142
|
+
const { skipIfDefaultPrevented = true, stopAtFirstMatch = true } = options;
|
|
143
|
+
const preparedBindings = bindings
|
|
144
|
+
.map((binding) => {
|
|
145
|
+
const combos = Array.isArray(binding.combo)
|
|
146
|
+
? binding.combo
|
|
147
|
+
: [binding.combo];
|
|
148
|
+
const parsedCombos = combos
|
|
149
|
+
.map(parseKeybind)
|
|
150
|
+
.filter((combo) => combo !== null);
|
|
151
|
+
if (parsedCombos.length === 0) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...binding,
|
|
156
|
+
parsedCombos
|
|
157
|
+
};
|
|
158
|
+
})
|
|
159
|
+
.filter((binding) => binding !== null);
|
|
160
|
+
return (event) => {
|
|
161
|
+
if (skipIfDefaultPrevented && event.defaultPrevented) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const binding of preparedBindings) {
|
|
165
|
+
if (binding.allowRepeat === false && event.repeat) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const shouldHandle = typeof binding.when === "function"
|
|
169
|
+
? binding.when(event)
|
|
170
|
+
: binding.when;
|
|
171
|
+
if (shouldHandle === false) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const isMatch = binding.parsedCombos.some((combo) => matchesParsedKeybind(event, combo, {
|
|
175
|
+
exactModifiers: binding.exactModifiers
|
|
176
|
+
}));
|
|
177
|
+
if (!isMatch) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (binding.preventDefault) {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
}
|
|
183
|
+
if (binding.stopPropagation) {
|
|
184
|
+
event.stopPropagation();
|
|
185
|
+
}
|
|
186
|
+
binding.handler(event);
|
|
187
|
+
if (stopAtFirstMatch) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refraktor/utils",
|
|
3
|
-
"
|
|
3
|
+
"description": "Shared React hooks and utilities for Refraktor",
|
|
4
|
+
"version": "0.0.5",
|
|
4
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/refraktorui/refraktor.git",
|
|
9
|
+
"directory": "packages/utils"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/refraktorui/refraktor/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react",
|
|
16
|
+
"hooks",
|
|
17
|
+
"utilities",
|
|
18
|
+
"utils"
|
|
19
|
+
],
|
|
5
20
|
"publishConfig": {
|
|
6
21
|
"access": "public"
|
|
7
22
|
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"files": [
|
|
25
|
+
"build",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
8
29
|
"main": "./build/index.js",
|
|
9
30
|
"module": "./build/index.js",
|
|
10
31
|
"types": "./build/index.d.ts",
|
|
@@ -23,7 +44,7 @@
|
|
|
23
44
|
"react": ">=18.0.0"
|
|
24
45
|
},
|
|
25
46
|
"devDependencies": {
|
|
26
|
-
"@types/react": "^19.2.
|
|
47
|
+
"@types/react": "^19.2.14",
|
|
27
48
|
"typescript": "^5.9.3"
|
|
28
49
|
}
|
|
29
50
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
$ tsc
|
|
Binary file
|
package/src/hooks/index.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
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";
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/clamp/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|