@develemit/web-hooks 0.1.0 → 0.3.0
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/CHANGELOG.md +28 -0
- package/README.md +311 -0
- package/dist/auto-grow.cjs +60 -0
- package/dist/auto-grow.d.cts +14 -0
- package/dist/auto-grow.d.ts +14 -0
- package/dist/auto-grow.js +9 -0
- package/dist/chunk-4PVVTQ5V.js +74 -0
- package/dist/chunk-5CA5RIMH.js +34 -0
- package/dist/chunk-5KIH2XLV.js +136 -0
- package/dist/chunk-5XRVPZHD.js +125 -0
- package/dist/chunk-6KCRVSLS.js +80 -0
- package/dist/chunk-DNU57XEJ.js +82 -0
- package/dist/chunk-DQBKU7HI.js +46 -0
- package/dist/chunk-EXEJI4FK.js +43 -0
- package/dist/chunk-FE3BQD2I.js +44 -0
- package/dist/chunk-HTVNN2NM.js +29 -0
- package/dist/chunk-IWQDAJEP.js +69 -0
- package/dist/chunk-JG3GPTMY.js +101 -0
- package/dist/chunk-LZKAGDER.js +22 -0
- package/dist/chunk-NKLEB5TR.js +98 -0
- package/dist/chunk-S4YKMGQE.js +23 -0
- package/dist/chunk-TNEJ34T5.js +86 -0
- package/dist/chunk-UJ366T3Y.js +146 -0
- package/dist/chunk-UPVNSGIK.js +72 -0
- package/dist/chunk-WVM2LTWQ.js +42 -0
- package/dist/chunk-WXL22EWK.js +21 -0
- package/dist/chunk-YFMMSLMB.js +41 -0
- package/dist/chunk-Z3X27PCA.js +108 -0
- package/dist/clipboard.cjs +18 -0
- package/dist/clipboard.d.cts +10 -1
- package/dist/clipboard.d.ts +10 -1
- package/dist/clipboard.js +8 -57
- package/dist/debounce.cjs +96 -0
- package/dist/debounce.d.cts +21 -0
- package/dist/debounce.d.ts +21 -0
- package/dist/debounce.js +11 -0
- package/dist/drawer-swipe.cjs +16 -4
- package/dist/drawer-swipe.d.cts +7 -3
- package/dist/drawer-swipe.d.ts +7 -3
- package/dist/drawer-swipe.js +16 -101
- package/dist/escape.cjs +1 -0
- package/dist/escape.js +3 -17
- package/dist/focus-trap.cjs +3 -1
- package/dist/focus-trap.js +6 -72
- package/dist/hotkey.cjs +127 -0
- package/dist/hotkey.d.cts +31 -0
- package/dist/hotkey.d.ts +31 -0
- package/dist/hotkey.js +15 -0
- package/dist/index.cjs +1207 -7
- package/dist/index.d.cts +20 -60
- package/dist/index.d.ts +20 -60
- package/dist/index.js +167 -247
- package/dist/keyboard-inset.cjs +113 -0
- package/dist/keyboard-inset.d.cts +25 -0
- package/dist/keyboard-inset.d.ts +25 -0
- package/dist/keyboard-inset.js +11 -0
- package/dist/media-query.cjs +90 -0
- package/dist/media-query.d.cts +12 -0
- package/dist/media-query.d.ts +12 -0
- package/dist/media-query.js +17 -0
- package/dist/online-status.cjs +46 -0
- package/dist/online-status.d.cts +3 -0
- package/dist/online-status.d.ts +3 -0
- package/dist/online-status.js +7 -0
- package/dist/outside-press.cjs +73 -0
- package/dist/outside-press.d.cts +17 -0
- package/dist/outside-press.d.ts +17 -0
- package/dist/outside-press.js +11 -0
- package/dist/push.cjs +201 -0
- package/dist/push.d.cts +37 -0
- package/dist/push.d.ts +37 -0
- package/dist/push.js +14 -0
- package/dist/pwa.cjs +198 -0
- package/dist/pwa.d.cts +35 -0
- package/dist/pwa.d.ts +35 -0
- package/dist/pwa.js +22 -0
- package/dist/scroll-lock.cjs +107 -0
- package/dist/scroll-lock.d.cts +8 -0
- package/dist/scroll-lock.d.ts +8 -0
- package/dist/scroll-lock.js +11 -0
- package/dist/service-worker.cjs +128 -0
- package/dist/service-worker.d.cts +23 -0
- package/dist/service-worker.d.ts +23 -0
- package/dist/service-worker.js +11 -0
- package/dist/share.cjs +139 -0
- package/dist/share.d.cts +23 -0
- package/dist/share.d.ts +23 -0
- package/dist/share.js +16 -0
- package/dist/storage-state.cjs +161 -0
- package/dist/storage-state.d.cts +14 -0
- package/dist/storage-state.d.ts +14 -0
- package/dist/storage-state.js +7 -0
- package/dist/unsaved-guard.cjs +48 -0
- package/dist/unsaved-guard.d.cts +5 -0
- package/dist/unsaved-guard.d.ts +5 -0
- package/dist/unsaved-guard.js +9 -0
- package/dist/visibility-polling.cjs +98 -0
- package/dist/visibility-polling.d.cts +14 -0
- package/dist/visibility-polling.d.ts +14 -0
- package/dist/visibility-polling.js +9 -0
- package/package.json +85 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/storage-state/use-storage-state.ts
|
|
4
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
5
|
+
|
|
6
|
+
// src/storage-state/storage-store.ts
|
|
7
|
+
function getStorage(kind) {
|
|
8
|
+
try {
|
|
9
|
+
if (typeof window === "undefined") return null;
|
|
10
|
+
return kind === "session" ? window.sessionStorage : window.localStorage;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function readRaw(storage, key) {
|
|
16
|
+
try {
|
|
17
|
+
return storage ? storage.getItem(key) : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeRaw(storage, key, value) {
|
|
23
|
+
try {
|
|
24
|
+
if (!storage) return false;
|
|
25
|
+
storage.setItem(key, value);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function removeRaw(storage, key) {
|
|
32
|
+
try {
|
|
33
|
+
if (!storage) return false;
|
|
34
|
+
storage.removeItem(key);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function parseOr(raw, parse, fallback) {
|
|
41
|
+
if (raw === null) return fallback;
|
|
42
|
+
try {
|
|
43
|
+
return parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var slot = (kind, key) => `${kind}\0${key}`;
|
|
49
|
+
var memory = /* @__PURE__ */ new Map();
|
|
50
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
51
|
+
function readCurrent(kind, key) {
|
|
52
|
+
const held = memory.get(slot(kind, key));
|
|
53
|
+
if (held !== void 0) return held;
|
|
54
|
+
return readRaw(getStorage(kind), key);
|
|
55
|
+
}
|
|
56
|
+
function notify(id) {
|
|
57
|
+
for (const listener of [...listeners.get(id) ?? []]) listener();
|
|
58
|
+
}
|
|
59
|
+
function writeCurrent(kind, key, raw) {
|
|
60
|
+
const id = slot(kind, key);
|
|
61
|
+
if (writeRaw(getStorage(kind), key, raw)) memory.delete(id);
|
|
62
|
+
else memory.set(id, raw);
|
|
63
|
+
notify(id);
|
|
64
|
+
}
|
|
65
|
+
function removeCurrent(kind, key) {
|
|
66
|
+
const id = slot(kind, key);
|
|
67
|
+
if (removeRaw(getStorage(kind), key)) memory.delete(id);
|
|
68
|
+
else memory.set(id, null);
|
|
69
|
+
notify(id);
|
|
70
|
+
}
|
|
71
|
+
function onStorageEvent(event) {
|
|
72
|
+
const kinds = [];
|
|
73
|
+
if (!event.storageArea || event.storageArea === getStorage("local")) kinds.push("local");
|
|
74
|
+
if (!event.storageArea || event.storageArea === getStorage("session")) kinds.push("session");
|
|
75
|
+
for (const id of [...listeners.keys()]) {
|
|
76
|
+
const [kind, key] = id.split("\0");
|
|
77
|
+
if (!kinds.includes(kind)) continue;
|
|
78
|
+
if (event.key !== null && event.key !== key) continue;
|
|
79
|
+
memory.delete(id);
|
|
80
|
+
notify(id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function subscribeToKey(kind, key, listener) {
|
|
84
|
+
const id = slot(kind, key);
|
|
85
|
+
const wasEmpty = listeners.size === 0;
|
|
86
|
+
let set = listeners.get(id);
|
|
87
|
+
if (!set) listeners.set(id, set = /* @__PURE__ */ new Set());
|
|
88
|
+
set.add(listener);
|
|
89
|
+
if (wasEmpty) window.addEventListener("storage", onStorageEvent);
|
|
90
|
+
return () => {
|
|
91
|
+
const current = listeners.get(id);
|
|
92
|
+
current?.delete(listener);
|
|
93
|
+
if (current?.size === 0) listeners.delete(id);
|
|
94
|
+
if (listeners.size === 0) window.removeEventListener("storage", onStorageEvent);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/storage-state/use-storage-state.ts
|
|
99
|
+
function useStorageState(key, defaultValue, options = {}) {
|
|
100
|
+
const kind = options.storage ?? "local";
|
|
101
|
+
const serialize = options.serialize ?? JSON.stringify;
|
|
102
|
+
const parse = options.parse ?? JSON.parse;
|
|
103
|
+
const latest = useRef({ serialize, parse, defaultValue });
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
latest.current = { serialize, parse, defaultValue };
|
|
106
|
+
});
|
|
107
|
+
const cache = useRef(null);
|
|
108
|
+
const subscribe = useCallback((onChange) => subscribeToKey(kind, key, onChange), [kind, key]);
|
|
109
|
+
const getSnapshot = useCallback(() => {
|
|
110
|
+
const raw = readCurrent(kind, key);
|
|
111
|
+
const hit = cache.current;
|
|
112
|
+
if (hit && hit.kind === kind && hit.key === key && hit.raw === raw && hit.parse === parse) return hit.value;
|
|
113
|
+
const value2 = parseOr(raw, parse, defaultValue);
|
|
114
|
+
cache.current = { kind, key, raw, parse, value: value2 };
|
|
115
|
+
return value2;
|
|
116
|
+
}, [kind, key, parse, defaultValue]);
|
|
117
|
+
const getServerSnapshot = useCallback(() => defaultValue, [defaultValue]);
|
|
118
|
+
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
119
|
+
const set = useCallback(
|
|
120
|
+
(next) => {
|
|
121
|
+
const { serialize: encode, parse: decode, defaultValue: fallback } = latest.current;
|
|
122
|
+
try {
|
|
123
|
+
const resolved = typeof next === "function" ? next(parseOr(readCurrent(kind, key), decode, fallback)) : next;
|
|
124
|
+
writeCurrent(kind, key, encode(resolved));
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
[kind, key]
|
|
129
|
+
);
|
|
130
|
+
const remove = useCallback(() => removeCurrent(kind, key), [kind, key]);
|
|
131
|
+
return [value, set, remove];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
useStorageState
|
|
136
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/drawer-swipe/swipe-intent.ts
|
|
4
|
+
var BACK_ZONE_PX = 60;
|
|
5
|
+
var FORWARD_ZONE_PX = 30;
|
|
6
|
+
var OPEN_DISTANCE_PX = 80;
|
|
7
|
+
var CLOSE_DISTANCE_PX = 60;
|
|
8
|
+
var VERTICAL_CANCEL_PX = 40;
|
|
9
|
+
var MAX_SWIPE_DURATION_MS = 450;
|
|
10
|
+
var HORIZONTAL_DOMINANCE = 1.5;
|
|
11
|
+
function isInOpenZone(x, viewportWidth) {
|
|
12
|
+
return x > BACK_ZONE_PX && x < viewportWidth - FORWARD_ZONE_PX;
|
|
13
|
+
}
|
|
14
|
+
function resolveSwipeIntent(start, end, drawerOpen, viewportWidth, startedInIgnoredRegion = false) {
|
|
15
|
+
if (startedInIgnoredRegion) return null;
|
|
16
|
+
const dx = end.x - start.x;
|
|
17
|
+
const dy = end.y - start.y;
|
|
18
|
+
if (end.time - start.time > MAX_SWIPE_DURATION_MS) return null;
|
|
19
|
+
if (Math.abs(dx) < Math.abs(dy) * HORIZONTAL_DOMINANCE) return null;
|
|
20
|
+
if (drawerOpen) {
|
|
21
|
+
return dx <= -CLOSE_DISTANCE_PX ? "close" : null;
|
|
22
|
+
}
|
|
23
|
+
if (dx < OPEN_DISTANCE_PX) return null;
|
|
24
|
+
return isInOpenZone(start.x, viewportWidth) ? "open" : null;
|
|
25
|
+
}
|
|
26
|
+
function isInsideHorizontalScroller(target) {
|
|
27
|
+
let node = target instanceof Element ? target : null;
|
|
28
|
+
while (node && node !== document.body) {
|
|
29
|
+
const { overflowX } = window.getComputedStyle(node);
|
|
30
|
+
if ((overflowX === "auto" || overflowX === "scroll") && node.scrollWidth > node.clientWidth) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
node = node.parentElement;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
var DEFAULT_IGNORE_SELECTOR = "[data-no-swipe]";
|
|
38
|
+
function isInIgnoredRegion(target, selector) {
|
|
39
|
+
if (!selector || !(target instanceof Element)) return false;
|
|
40
|
+
return target.closest(selector) !== null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/drawer-swipe/use-drawer-swipe.ts
|
|
44
|
+
import { useEffect, useRef } from "react";
|
|
45
|
+
function isWideViewport(maxViewportWidth) {
|
|
46
|
+
return window.matchMedia(`(min-width: ${maxViewportWidth}px)`).matches;
|
|
47
|
+
}
|
|
48
|
+
function useDrawerSwipe({
|
|
49
|
+
drawerOpen,
|
|
50
|
+
onOpen,
|
|
51
|
+
onClose,
|
|
52
|
+
enabled = true,
|
|
53
|
+
maxViewportWidth = 768,
|
|
54
|
+
ignoreSelector = DEFAULT_IGNORE_SELECTOR
|
|
55
|
+
}) {
|
|
56
|
+
const start = useRef(null);
|
|
57
|
+
const handlers = useRef({ drawerOpen, onOpen, onClose, maxViewportWidth, ignoreSelector });
|
|
58
|
+
handlers.current = { drawerOpen, onOpen, onClose, maxViewportWidth, ignoreSelector };
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!enabled) return;
|
|
61
|
+
function handleStart(event) {
|
|
62
|
+
start.current = null;
|
|
63
|
+
if (event.touches.length !== 1) return;
|
|
64
|
+
if (isWideViewport(handlers.current.maxViewportWidth)) return;
|
|
65
|
+
if (isInIgnoredRegion(event.target, handlers.current.ignoreSelector)) return;
|
|
66
|
+
if (!handlers.current.drawerOpen && isInsideHorizontalScroller(event.target)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const touch = event.touches[0];
|
|
70
|
+
if (!touch) return;
|
|
71
|
+
start.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
|
72
|
+
}
|
|
73
|
+
function handleMove(event) {
|
|
74
|
+
const origin = start.current;
|
|
75
|
+
const touch = event.touches[0];
|
|
76
|
+
if (!origin || !touch) return;
|
|
77
|
+
if (Math.abs(touch.clientY - origin.y) > VERTICAL_CANCEL_PX) {
|
|
78
|
+
start.current = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function handleEnd(event) {
|
|
82
|
+
const origin = start.current;
|
|
83
|
+
start.current = null;
|
|
84
|
+
const touch = event.changedTouches[0];
|
|
85
|
+
if (!origin || !touch) return;
|
|
86
|
+
const { drawerOpen: drawerOpen2, onOpen: onOpen2, onClose: onClose2 } = handlers.current;
|
|
87
|
+
const intent = resolveSwipeIntent(
|
|
88
|
+
origin,
|
|
89
|
+
{ x: touch.clientX, y: touch.clientY, time: Date.now() },
|
|
90
|
+
drawerOpen2,
|
|
91
|
+
window.innerWidth
|
|
92
|
+
);
|
|
93
|
+
if (intent === "open") onOpen2();
|
|
94
|
+
if (intent === "close") onClose2();
|
|
95
|
+
}
|
|
96
|
+
function handleCancel() {
|
|
97
|
+
start.current = null;
|
|
98
|
+
}
|
|
99
|
+
document.addEventListener("touchstart", handleStart, { passive: true });
|
|
100
|
+
document.addEventListener("touchmove", handleMove, { passive: true });
|
|
101
|
+
document.addEventListener("touchend", handleEnd, { passive: true });
|
|
102
|
+
document.addEventListener("touchcancel", handleCancel, { passive: true });
|
|
103
|
+
return () => {
|
|
104
|
+
document.removeEventListener("touchstart", handleStart);
|
|
105
|
+
document.removeEventListener("touchmove", handleMove);
|
|
106
|
+
document.removeEventListener("touchend", handleEnd);
|
|
107
|
+
document.removeEventListener("touchcancel", handleCancel);
|
|
108
|
+
};
|
|
109
|
+
}, [enabled]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
BACK_ZONE_PX,
|
|
114
|
+
FORWARD_ZONE_PX,
|
|
115
|
+
OPEN_DISTANCE_PX,
|
|
116
|
+
CLOSE_DISTANCE_PX,
|
|
117
|
+
VERTICAL_CANCEL_PX,
|
|
118
|
+
MAX_SWIPE_DURATION_MS,
|
|
119
|
+
isInOpenZone,
|
|
120
|
+
resolveSwipeIntent,
|
|
121
|
+
isInsideHorizontalScroller,
|
|
122
|
+
DEFAULT_IGNORE_SELECTOR,
|
|
123
|
+
isInIgnoredRegion,
|
|
124
|
+
useDrawerSwipe
|
|
125
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/scroll-lock/use-scroll-lock.ts
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
// src/scroll-lock/lock-styles.ts
|
|
7
|
+
var LOCKED_PROPERTIES = [
|
|
8
|
+
"position",
|
|
9
|
+
"top",
|
|
10
|
+
"left",
|
|
11
|
+
"right",
|
|
12
|
+
"width",
|
|
13
|
+
"overflow",
|
|
14
|
+
"paddingRight"
|
|
15
|
+
];
|
|
16
|
+
function computeLockStyles({ scrollY, scrollbarWidth, existingPaddingRight }) {
|
|
17
|
+
const styles = {
|
|
18
|
+
position: "fixed",
|
|
19
|
+
top: `-${scrollY}px`,
|
|
20
|
+
left: "0",
|
|
21
|
+
right: "0",
|
|
22
|
+
width: "100%",
|
|
23
|
+
overflow: "hidden"
|
|
24
|
+
};
|
|
25
|
+
if (scrollbarWidth > 0) styles.paddingRight = `${existingPaddingRight + scrollbarWidth}px`;
|
|
26
|
+
return styles;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/scroll-lock/scroll-lock.ts
|
|
30
|
+
var count = 0;
|
|
31
|
+
var saved = null;
|
|
32
|
+
function applyFirstLock() {
|
|
33
|
+
const { body, documentElement } = document;
|
|
34
|
+
const inline = {};
|
|
35
|
+
for (const property of LOCKED_PROPERTIES) inline[property] = body.style[property];
|
|
36
|
+
saved = { scrollY: window.scrollY, inline };
|
|
37
|
+
const styles = computeLockStyles({
|
|
38
|
+
scrollY: saved.scrollY,
|
|
39
|
+
scrollbarWidth: Math.max(0, window.innerWidth - documentElement.clientWidth),
|
|
40
|
+
existingPaddingRight: parseFloat(getComputedStyle(body).paddingRight) || 0
|
|
41
|
+
});
|
|
42
|
+
Object.assign(body.style, styles);
|
|
43
|
+
}
|
|
44
|
+
function restoreLastLock() {
|
|
45
|
+
if (!saved) return;
|
|
46
|
+
const { scrollY, inline } = saved;
|
|
47
|
+
saved = null;
|
|
48
|
+
Object.assign(document.body.style, inline);
|
|
49
|
+
window.scrollTo(0, scrollY);
|
|
50
|
+
}
|
|
51
|
+
function lockScroll() {
|
|
52
|
+
if (count === 0) applyFirstLock();
|
|
53
|
+
count += 1;
|
|
54
|
+
let released = false;
|
|
55
|
+
return () => {
|
|
56
|
+
if (released) return;
|
|
57
|
+
released = true;
|
|
58
|
+
count -= 1;
|
|
59
|
+
if (count === 0) restoreLastLock();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function unlockScroll() {
|
|
63
|
+
if (count === 0) return;
|
|
64
|
+
count -= 1;
|
|
65
|
+
if (count === 0) restoreLastLock();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/scroll-lock/use-scroll-lock.ts
|
|
69
|
+
function useScrollLock(active = true) {
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!active) return;
|
|
72
|
+
return lockScroll();
|
|
73
|
+
}, [active]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
lockScroll,
|
|
78
|
+
unlockScroll,
|
|
79
|
+
useScrollLock
|
|
80
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/focus-trap/focusables.ts
|
|
4
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
5
|
+
function focusableWithin(container) {
|
|
6
|
+
return Array.from(
|
|
7
|
+
container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
8
|
+
).filter((el) => !el.closest("[hidden], [inert]"));
|
|
9
|
+
}
|
|
10
|
+
function nextFocusTarget(items, current, shiftKey, container) {
|
|
11
|
+
const first = items[0];
|
|
12
|
+
const last = items[items.length - 1];
|
|
13
|
+
if (!first || !last) return null;
|
|
14
|
+
if (!current || !container.contains(current)) return shiftKey ? last : first;
|
|
15
|
+
if (shiftKey && (current === first || current === container)) return last;
|
|
16
|
+
if (!shiftKey && current === last) return first;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/focus-trap/use-focus-trap.ts
|
|
21
|
+
import { useEffect, useRef } from "react";
|
|
22
|
+
var activeTraps = [];
|
|
23
|
+
function useFocusTrap(containerRef, {
|
|
24
|
+
active,
|
|
25
|
+
initialFocusRef,
|
|
26
|
+
returnFocusRef,
|
|
27
|
+
restoreFocus = true
|
|
28
|
+
}) {
|
|
29
|
+
const openerRef = useRef(null);
|
|
30
|
+
if (active && !openerRef.current && typeof document !== "undefined") {
|
|
31
|
+
openerRef.current = document.activeElement;
|
|
32
|
+
}
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!active) openerRef.current = null;
|
|
35
|
+
}, [active]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const container = containerRef.current;
|
|
38
|
+
if (!active || !container) return;
|
|
39
|
+
const token = {};
|
|
40
|
+
activeTraps.push(token);
|
|
41
|
+
if (!container.contains(document.activeElement)) {
|
|
42
|
+
const target = initialFocusRef?.current ?? focusableWithin(container)[0] ?? container;
|
|
43
|
+
if (target === container && !container.hasAttribute("tabindex")) {
|
|
44
|
+
container.setAttribute("tabindex", "-1");
|
|
45
|
+
}
|
|
46
|
+
target.focus();
|
|
47
|
+
}
|
|
48
|
+
function onKeyDown(event) {
|
|
49
|
+
if (event.key !== "Tab" || !container) return;
|
|
50
|
+
if (activeTraps[activeTraps.length - 1] !== token) return;
|
|
51
|
+
const items = focusableWithin(container);
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const target = nextFocusTarget(
|
|
57
|
+
items,
|
|
58
|
+
document.activeElement,
|
|
59
|
+
event.shiftKey,
|
|
60
|
+
container
|
|
61
|
+
);
|
|
62
|
+
if (target) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
target.focus();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
document.addEventListener("keydown", onKeyDown);
|
|
68
|
+
return () => {
|
|
69
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
70
|
+
activeTraps.splice(activeTraps.indexOf(token), 1);
|
|
71
|
+
const opener = returnFocusRef?.current ?? openerRef.current;
|
|
72
|
+
if (restoreFocus && opener?.isConnected) opener.focus();
|
|
73
|
+
};
|
|
74
|
+
}, [active, containerRef, initialFocusRef, returnFocusRef, restoreFocus]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
FOCUSABLE_SELECTOR,
|
|
79
|
+
focusableWithin,
|
|
80
|
+
nextFocusTarget,
|
|
81
|
+
useFocusTrap
|
|
82
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/outside-press/outside-press.ts
|
|
4
|
+
function isOutside(target, elements, ignoreSelector) {
|
|
5
|
+
if (!target) return false;
|
|
6
|
+
if (!target.isConnected) return false;
|
|
7
|
+
if (ignoreSelector && isInIgnoredRegion(target, ignoreSelector)) return false;
|
|
8
|
+
return !elements.some((el) => el?.contains(target));
|
|
9
|
+
}
|
|
10
|
+
function isPrimaryPress(event) {
|
|
11
|
+
return !(event.pointerType === "mouse" && event.button !== 0);
|
|
12
|
+
}
|
|
13
|
+
function isInIgnoredRegion(target, selector) {
|
|
14
|
+
const element = target instanceof Element ? target : target.parentElement;
|
|
15
|
+
return element?.closest(selector) != null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/outside-press/use-outside-press.ts
|
|
19
|
+
import { useEffect, useRef } from "react";
|
|
20
|
+
function useOutsidePress(refs, onOutsidePress, { enabled = true, ignore } = {}) {
|
|
21
|
+
const refsRef = useRef(refs);
|
|
22
|
+
const handlerRef = useRef(onOutsidePress);
|
|
23
|
+
const ignoreRef = useRef(ignore);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
ignoreRef.current = ignore;
|
|
26
|
+
refsRef.current = refs;
|
|
27
|
+
handlerRef.current = onOutsidePress;
|
|
28
|
+
});
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!enabled) return;
|
|
31
|
+
function onPointerDown(event) {
|
|
32
|
+
if (!isPrimaryPress(event)) return;
|
|
33
|
+
const list = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
34
|
+
const elements = list.map((ref) => ref.current);
|
|
35
|
+
if (isOutside(event.target, elements, ignoreRef.current)) handlerRef.current(event);
|
|
36
|
+
}
|
|
37
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
38
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
39
|
+
}, [enabled]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
isOutside,
|
|
44
|
+
isPrimaryPress,
|
|
45
|
+
useOutsidePress
|
|
46
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
copyText
|
|
4
|
+
} from "./chunk-FE3BQD2I.js";
|
|
5
|
+
|
|
6
|
+
// src/clipboard/use-copy-feedback.ts
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
function useCopyFeedback({ resetMs = 1500 } = {}) {
|
|
9
|
+
const [status, setStatus] = useState("idle");
|
|
10
|
+
const timerRef = useRef(void 0);
|
|
11
|
+
const clearTimer = useCallback(() => {
|
|
12
|
+
if (timerRef.current !== void 0) clearTimeout(timerRef.current);
|
|
13
|
+
timerRef.current = void 0;
|
|
14
|
+
}, []);
|
|
15
|
+
useEffect(() => clearTimer, [clearTimer]);
|
|
16
|
+
const copy = useCallback(
|
|
17
|
+
async (text) => {
|
|
18
|
+
const result = await copyText(text);
|
|
19
|
+
clearTimer();
|
|
20
|
+
setStatus(result.ok ? "copied" : "failed");
|
|
21
|
+
timerRef.current = setTimeout(() => setStatus("idle"), resetMs);
|
|
22
|
+
return result;
|
|
23
|
+
},
|
|
24
|
+
[clearTimer, resetMs]
|
|
25
|
+
);
|
|
26
|
+
return { copy, status };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/clipboard/read-text.ts
|
|
30
|
+
async function readText() {
|
|
31
|
+
const hasClipboardApi = typeof navigator !== "undefined" && !!navigator.clipboard?.readText && typeof window !== "undefined" && window.isSecureContext;
|
|
32
|
+
if (!hasClipboardApi) return { ok: false, reason: "unavailable" };
|
|
33
|
+
try {
|
|
34
|
+
return { ok: true, text: await navigator.clipboard.readText() };
|
|
35
|
+
} catch {
|
|
36
|
+
return { ok: false, reason: "denied" };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
useCopyFeedback,
|
|
42
|
+
readText
|
|
43
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/clipboard/copy-text.ts
|
|
4
|
+
async function copyText(text) {
|
|
5
|
+
const hasClipboardApi = typeof navigator !== "undefined" && !!navigator.clipboard?.writeText && typeof window !== "undefined" && window.isSecureContext;
|
|
6
|
+
if (hasClipboardApi) {
|
|
7
|
+
try {
|
|
8
|
+
await navigator.clipboard.writeText(text);
|
|
9
|
+
return { ok: true };
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (execCommandCopy(text)) return { ok: true };
|
|
14
|
+
return { ok: false, reason: hasClipboardApi ? "denied" : "unavailable" };
|
|
15
|
+
}
|
|
16
|
+
function execCommandCopy(text) {
|
|
17
|
+
if (typeof document === "undefined") return false;
|
|
18
|
+
const textarea = document.createElement("textarea");
|
|
19
|
+
textarea.value = text;
|
|
20
|
+
textarea.readOnly = true;
|
|
21
|
+
textarea.style.position = "fixed";
|
|
22
|
+
textarea.style.top = "-1000px";
|
|
23
|
+
textarea.style.left = "-1000px";
|
|
24
|
+
textarea.style.fontSize = "16px";
|
|
25
|
+
const previouslyFocused = document.activeElement;
|
|
26
|
+
document.body.appendChild(textarea);
|
|
27
|
+
try {
|
|
28
|
+
textarea.focus({ preventScroll: true });
|
|
29
|
+
textarea.select();
|
|
30
|
+
textarea.setSelectionRange(0, text.length);
|
|
31
|
+
return document.execCommand("copy");
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
} finally {
|
|
35
|
+
document.body.removeChild(textarea);
|
|
36
|
+
if (previouslyFocused instanceof HTMLElement && previouslyFocused !== document.body) {
|
|
37
|
+
previouslyFocused.focus({ preventScroll: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
copyText
|
|
44
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
COARSE_POINTER_QUERY,
|
|
4
|
+
REDUCED_MOTION_QUERY,
|
|
5
|
+
getMatches,
|
|
6
|
+
subscribe
|
|
7
|
+
} from "./chunk-WVM2LTWQ.js";
|
|
8
|
+
|
|
9
|
+
// src/media-query/use-media-query.ts
|
|
10
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
11
|
+
function useMediaQuery(query, options = {}) {
|
|
12
|
+
const serverDefault = options.serverDefault ?? false;
|
|
13
|
+
const subscribeToQuery = useCallback((onChange) => subscribe(query, onChange), [query]);
|
|
14
|
+
const getSnapshot = useCallback(() => getMatches(query) ?? serverDefault, [query, serverDefault]);
|
|
15
|
+
const getServerSnapshot = useCallback(() => serverDefault, [serverDefault]);
|
|
16
|
+
return useSyncExternalStore(subscribeToQuery, getSnapshot, getServerSnapshot);
|
|
17
|
+
}
|
|
18
|
+
function useReducedMotion(options) {
|
|
19
|
+
return useMediaQuery(REDUCED_MOTION_QUERY, options);
|
|
20
|
+
}
|
|
21
|
+
function useCoarsePointer(options) {
|
|
22
|
+
return useMediaQuery(COARSE_POINTER_QUERY, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
useMediaQuery,
|
|
27
|
+
useReducedMotion,
|
|
28
|
+
useCoarsePointer
|
|
29
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/debounce/debouncer.ts
|
|
4
|
+
function createDebouncer(getFn, getMs) {
|
|
5
|
+
let timer;
|
|
6
|
+
let pending;
|
|
7
|
+
function clear() {
|
|
8
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9
|
+
timer = void 0;
|
|
10
|
+
pending = void 0;
|
|
11
|
+
}
|
|
12
|
+
function run() {
|
|
13
|
+
const args = pending;
|
|
14
|
+
clear();
|
|
15
|
+
if (args) getFn()(...args);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
call(...args) {
|
|
19
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
20
|
+
pending = args;
|
|
21
|
+
timer = setTimeout(run, getMs());
|
|
22
|
+
},
|
|
23
|
+
cancel: clear,
|
|
24
|
+
flush() {
|
|
25
|
+
if (pending) run();
|
|
26
|
+
},
|
|
27
|
+
isPending: () => pending !== void 0
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/debounce/use-debounced-value.ts
|
|
32
|
+
import { useEffect, useState } from "react";
|
|
33
|
+
function useDebouncedValue(value, ms) {
|
|
34
|
+
const [debounced, setDebounced] = useState(value);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const timer = setTimeout(() => setDebounced(value), ms);
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}, [value, ms]);
|
|
39
|
+
return debounced;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/debounce/use-debounced-callback.ts
|
|
43
|
+
import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
44
|
+
function useDebouncedCallback(fn, ms) {
|
|
45
|
+
const fnRef = useRef(fn);
|
|
46
|
+
const msRef = useRef(ms);
|
|
47
|
+
useEffect2(() => {
|
|
48
|
+
fnRef.current = fn;
|
|
49
|
+
msRef.current = ms;
|
|
50
|
+
});
|
|
51
|
+
const [debounced] = useState2(() => {
|
|
52
|
+
const debouncer = createDebouncer(
|
|
53
|
+
() => fnRef.current,
|
|
54
|
+
() => msRef.current
|
|
55
|
+
);
|
|
56
|
+
return Object.assign((...args) => debouncer.call(...args), {
|
|
57
|
+
cancel: debouncer.cancel,
|
|
58
|
+
flush: debouncer.flush
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
useEffect2(() => () => debounced.cancel(), [debounced]);
|
|
62
|
+
return debounced;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
createDebouncer,
|
|
67
|
+
useDebouncedValue,
|
|
68
|
+
useDebouncedCallback
|
|
69
|
+
};
|