@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +311 -0
  3. package/dist/auto-grow.cjs +60 -0
  4. package/dist/auto-grow.d.cts +14 -0
  5. package/dist/auto-grow.d.ts +14 -0
  6. package/dist/auto-grow.js +9 -0
  7. package/dist/chunk-4PVVTQ5V.js +74 -0
  8. package/dist/chunk-5CA5RIMH.js +34 -0
  9. package/dist/chunk-5KIH2XLV.js +136 -0
  10. package/dist/chunk-5XRVPZHD.js +125 -0
  11. package/dist/chunk-6KCRVSLS.js +80 -0
  12. package/dist/chunk-DNU57XEJ.js +82 -0
  13. package/dist/chunk-DQBKU7HI.js +46 -0
  14. package/dist/chunk-EXEJI4FK.js +43 -0
  15. package/dist/chunk-FE3BQD2I.js +44 -0
  16. package/dist/chunk-HTVNN2NM.js +29 -0
  17. package/dist/chunk-IWQDAJEP.js +69 -0
  18. package/dist/chunk-JG3GPTMY.js +101 -0
  19. package/dist/chunk-LZKAGDER.js +22 -0
  20. package/dist/chunk-NKLEB5TR.js +98 -0
  21. package/dist/chunk-S4YKMGQE.js +23 -0
  22. package/dist/chunk-TNEJ34T5.js +86 -0
  23. package/dist/chunk-UJ366T3Y.js +146 -0
  24. package/dist/chunk-UPVNSGIK.js +72 -0
  25. package/dist/chunk-WVM2LTWQ.js +42 -0
  26. package/dist/chunk-WXL22EWK.js +21 -0
  27. package/dist/chunk-YFMMSLMB.js +41 -0
  28. package/dist/chunk-Z3X27PCA.js +108 -0
  29. package/dist/clipboard.cjs +18 -0
  30. package/dist/clipboard.d.cts +10 -1
  31. package/dist/clipboard.d.ts +10 -1
  32. package/dist/clipboard.js +8 -57
  33. package/dist/debounce.cjs +96 -0
  34. package/dist/debounce.d.cts +21 -0
  35. package/dist/debounce.d.ts +21 -0
  36. package/dist/debounce.js +11 -0
  37. package/dist/drawer-swipe.cjs +16 -4
  38. package/dist/drawer-swipe.d.cts +7 -3
  39. package/dist/drawer-swipe.d.ts +7 -3
  40. package/dist/drawer-swipe.js +16 -101
  41. package/dist/escape.cjs +1 -0
  42. package/dist/escape.js +3 -17
  43. package/dist/focus-trap.cjs +3 -1
  44. package/dist/focus-trap.js +6 -72
  45. package/dist/hotkey.cjs +127 -0
  46. package/dist/hotkey.d.cts +31 -0
  47. package/dist/hotkey.d.ts +31 -0
  48. package/dist/hotkey.js +15 -0
  49. package/dist/index.cjs +1207 -7
  50. package/dist/index.d.cts +20 -60
  51. package/dist/index.d.ts +20 -60
  52. package/dist/index.js +167 -247
  53. package/dist/keyboard-inset.cjs +113 -0
  54. package/dist/keyboard-inset.d.cts +25 -0
  55. package/dist/keyboard-inset.d.ts +25 -0
  56. package/dist/keyboard-inset.js +11 -0
  57. package/dist/media-query.cjs +90 -0
  58. package/dist/media-query.d.cts +12 -0
  59. package/dist/media-query.d.ts +12 -0
  60. package/dist/media-query.js +17 -0
  61. package/dist/online-status.cjs +46 -0
  62. package/dist/online-status.d.cts +3 -0
  63. package/dist/online-status.d.ts +3 -0
  64. package/dist/online-status.js +7 -0
  65. package/dist/outside-press.cjs +73 -0
  66. package/dist/outside-press.d.cts +17 -0
  67. package/dist/outside-press.d.ts +17 -0
  68. package/dist/outside-press.js +11 -0
  69. package/dist/push.cjs +201 -0
  70. package/dist/push.d.cts +37 -0
  71. package/dist/push.d.ts +37 -0
  72. package/dist/push.js +14 -0
  73. package/dist/pwa.cjs +198 -0
  74. package/dist/pwa.d.cts +35 -0
  75. package/dist/pwa.d.ts +35 -0
  76. package/dist/pwa.js +22 -0
  77. package/dist/scroll-lock.cjs +107 -0
  78. package/dist/scroll-lock.d.cts +8 -0
  79. package/dist/scroll-lock.d.ts +8 -0
  80. package/dist/scroll-lock.js +11 -0
  81. package/dist/service-worker.cjs +128 -0
  82. package/dist/service-worker.d.cts +23 -0
  83. package/dist/service-worker.d.ts +23 -0
  84. package/dist/service-worker.js +11 -0
  85. package/dist/share.cjs +139 -0
  86. package/dist/share.d.cts +23 -0
  87. package/dist/share.d.ts +23 -0
  88. package/dist/share.js +16 -0
  89. package/dist/storage-state.cjs +161 -0
  90. package/dist/storage-state.d.cts +14 -0
  91. package/dist/storage-state.d.ts +14 -0
  92. package/dist/storage-state.js +7 -0
  93. package/dist/unsaved-guard.cjs +48 -0
  94. package/dist/unsaved-guard.d.cts +5 -0
  95. package/dist/unsaved-guard.d.ts +5 -0
  96. package/dist/unsaved-guard.js +9 -0
  97. package/dist/visibility-polling.cjs +98 -0
  98. package/dist/visibility-polling.d.cts +14 -0
  99. package/dist/visibility-polling.d.ts +14 -0
  100. package/dist/visibility-polling.js +9 -0
  101. package/package.json +85 -2
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ // src/service-worker/use-service-worker.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+
6
+ // src/service-worker/update-state.ts
7
+ function deriveUpdateState({ hasController: hasController2, waiting, installingState }) {
8
+ if (!hasController2) return false;
9
+ return waiting || installingState === "installed";
10
+ }
11
+
12
+ // src/service-worker/register.ts
13
+ var reloaded = false;
14
+ function isServiceWorkerSupported() {
15
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator;
16
+ }
17
+ var hasController = () => Boolean(navigator.serviceWorker.controller);
18
+ async function unregisterAll() {
19
+ if (!isServiceWorkerSupported()) return;
20
+ try {
21
+ const registrations = await navigator.serviceWorker.getRegistrations();
22
+ await Promise.all(registrations.map((r) => r.unregister()));
23
+ } catch {
24
+ }
25
+ }
26
+ function registerServiceWorker({ url, scope, onUpdateAvailable }, { reload = () => window.location.reload() } = {}) {
27
+ let registration;
28
+ let disposed = false;
29
+ let applying = false;
30
+ const cleanups = [];
31
+ const controlledAtStart = hasController();
32
+ const onControllerChange = () => {
33
+ if (!(applying || controlledAtStart) || reloaded) return;
34
+ reloaded = true;
35
+ reload();
36
+ };
37
+ navigator.serviceWorker.addEventListener("controllerchange", onControllerChange);
38
+ cleanups.push(() => navigator.serviceWorker.removeEventListener("controllerchange", onControllerChange));
39
+ const watchInstalling = (reg) => {
40
+ const worker = reg.installing;
41
+ if (!worker) return;
42
+ const onState = () => {
43
+ if (deriveUpdateState({ hasController: hasController(), waiting: false, installingState: worker.state })) {
44
+ onUpdateAvailable();
45
+ }
46
+ };
47
+ worker.addEventListener("statechange", onState);
48
+ cleanups.push(() => worker.removeEventListener("statechange", onState));
49
+ };
50
+ navigator.serviceWorker.register(url, scope === void 0 ? void 0 : { scope }).then((reg) => {
51
+ if (disposed) return;
52
+ registration = reg;
53
+ if (deriveUpdateState({ hasController: hasController(), waiting: Boolean(reg.waiting) })) {
54
+ onUpdateAvailable();
55
+ }
56
+ const onUpdateFound = () => watchInstalling(reg);
57
+ reg.addEventListener("updatefound", onUpdateFound);
58
+ cleanups.push(() => reg.removeEventListener("updatefound", onUpdateFound));
59
+ }).catch(() => {
60
+ });
61
+ return {
62
+ applyUpdate() {
63
+ const waiting = registration?.waiting;
64
+ if (!waiting) return;
65
+ applying = true;
66
+ waiting.postMessage({ type: "SKIP_WAITING" });
67
+ },
68
+ dispose() {
69
+ disposed = true;
70
+ cleanups.splice(0).forEach((fn) => fn());
71
+ }
72
+ };
73
+ }
74
+
75
+ // src/service-worker/use-service-worker.ts
76
+ function useServiceWorker({ url, scope, enabled = true }) {
77
+ const [supported, setSupported] = useState(true);
78
+ const [updateAvailable, setUpdateAvailable] = useState(false);
79
+ const handle = useRef(void 0);
80
+ useEffect(() => {
81
+ if (!enabled) return;
82
+ if (!isServiceWorkerSupported()) {
83
+ setSupported(false);
84
+ return;
85
+ }
86
+ const registered = registerServiceWorker({ url, scope, onUpdateAvailable: () => setUpdateAvailable(true) });
87
+ handle.current = registered;
88
+ return () => {
89
+ registered.dispose();
90
+ handle.current = void 0;
91
+ };
92
+ }, [url, scope, enabled]);
93
+ const applyUpdate = useCallback(() => handle.current?.applyUpdate(), []);
94
+ return { supported, updateAvailable, applyUpdate };
95
+ }
96
+
97
+ export {
98
+ deriveUpdateState,
99
+ unregisterAll,
100
+ useServiceWorker
101
+ };
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ // src/unsaved-guard/handle-before-unload.ts
4
+ function handleBeforeUnload(event) {
5
+ event.preventDefault();
6
+ event.returnValue = "";
7
+ }
8
+
9
+ // src/unsaved-guard/use-unsaved-changes-guard.ts
10
+ import { useEffect } from "react";
11
+ function useUnsavedChangesGuard(dirty) {
12
+ useEffect(() => {
13
+ if (!dirty) return;
14
+ window.addEventListener("beforeunload", handleBeforeUnload);
15
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
16
+ }, [dirty]);
17
+ }
18
+
19
+ export {
20
+ handleBeforeUnload,
21
+ useUnsavedChangesGuard
22
+ };
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ // src/hotkey/parse-hotkey.ts
4
+ var KEY_ALIASES = {
5
+ esc: "escape",
6
+ space: " ",
7
+ plus: "+",
8
+ up: "arrowup",
9
+ down: "arrowdown",
10
+ left: "arrowleft",
11
+ right: "arrowright"
12
+ };
13
+ function parseHotkey(combo) {
14
+ const parsed = { key: "", mod: false, ctrl: false, meta: false, alt: false, shift: false };
15
+ const parts = combo.trim().toLowerCase().split(/\+(?=.)/);
16
+ for (const part of parts) {
17
+ const token = part.trim();
18
+ if (token === "mod" || token === "ctrl" || token === "meta" || token === "alt" || token === "shift") {
19
+ parsed[token] = true;
20
+ } else {
21
+ parsed.key = KEY_ALIASES[token] ?? token;
22
+ }
23
+ }
24
+ if (!parsed.key) throw new Error(`Invalid hotkey "${combo}": no key`);
25
+ return parsed;
26
+ }
27
+
28
+ // src/hotkey/match-hotkey.ts
29
+ var CODE_FOR_SYMBOL = {
30
+ "/": "Slash",
31
+ ".": "Period",
32
+ ",": "Comma",
33
+ ";": "Semicolon",
34
+ "'": "Quote",
35
+ "[": "BracketLeft",
36
+ "]": "BracketRight",
37
+ "\\": "Backslash",
38
+ "-": "Minus",
39
+ "=": "Equal",
40
+ "`": "Backquote"
41
+ };
42
+ function physicalCode(key) {
43
+ if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
44
+ if (/^[0-9]$/.test(key)) return `Digit${key}`;
45
+ return CODE_FOR_SYMBOL[key] ?? null;
46
+ }
47
+ function keyMatches(event, parsed) {
48
+ if (event.key.toLowerCase() === parsed.key) return true;
49
+ return parsed.shift && physicalCode(parsed.key) === event.code;
50
+ }
51
+ function matchHotkey(event, combo, isApple) {
52
+ const parsed = typeof combo === "string" ? parseHotkey(combo) : combo;
53
+ const wantMeta = parsed.meta || parsed.mod && isApple;
54
+ const wantCtrl = parsed.ctrl || parsed.mod && !isApple;
55
+ return event.metaKey === wantMeta && event.ctrlKey === wantCtrl && event.altKey === parsed.alt && event.shiftKey === parsed.shift && keyMatches(event, parsed);
56
+ }
57
+ function detectApple(nav) {
58
+ if (!nav) return false;
59
+ const platform = nav.userAgentData?.platform || nav.platform || "";
60
+ return /mac|iphone|ipad|ipod/i.test(platform);
61
+ }
62
+ function isEditableTarget(target) {
63
+ const el = target;
64
+ if (!el || typeof el.tagName !== "string") return false;
65
+ const tag = el.tagName.toLowerCase();
66
+ return tag === "input" || tag === "textarea" || tag === "select" || el.isContentEditable === true;
67
+ }
68
+
69
+ // src/hotkey/use-hotkey.ts
70
+ import { useEffect, useRef } from "react";
71
+ function useHotkey(combo, onHotkey, options = {}) {
72
+ const { enabled = true, allowInInputs = false, preventDefault = true } = options;
73
+ const handlerRef = useRef(onHotkey);
74
+ useEffect(() => {
75
+ handlerRef.current = onHotkey;
76
+ });
77
+ useEffect(() => {
78
+ if (!enabled) return;
79
+ const parsed = parseHotkey(combo);
80
+ function onKeyDown(event) {
81
+ if (event.isComposing || event.keyCode === 229) return;
82
+ if (!allowInInputs && isEditableTarget(event.target)) return;
83
+ if (!matchHotkey(event, parsed, detectApple(navigator))) return;
84
+ if (preventDefault) event.preventDefault();
85
+ handlerRef.current(event);
86
+ }
87
+ document.addEventListener("keydown", onKeyDown);
88
+ return () => document.removeEventListener("keydown", onKeyDown);
89
+ }, [combo, enabled, allowInInputs, preventDefault]);
90
+ }
91
+
92
+ export {
93
+ parseHotkey,
94
+ matchHotkey,
95
+ detectApple,
96
+ isEditableTarget,
97
+ useHotkey
98
+ };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ // src/escape/use-escape.ts
4
+ import { useEffect, useRef } from "react";
5
+ function useEscape(onEscape, active = true) {
6
+ const handlerRef = useRef(onEscape);
7
+ useEffect(() => {
8
+ handlerRef.current = onEscape;
9
+ });
10
+ useEffect(() => {
11
+ if (!active) return;
12
+ function onKeyDown(event) {
13
+ if (event.isComposing || event.keyCode === 229) return;
14
+ if (event.key === "Escape") handlerRef.current();
15
+ }
16
+ document.addEventListener("keydown", onKeyDown);
17
+ return () => document.removeEventListener("keydown", onKeyDown);
18
+ }, [active]);
19
+ }
20
+
21
+ export {
22
+ useEscape
23
+ };
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ // src/keyboard-inset/use-keyboard-inset.ts
4
+ import { useEffect, useState } from "react";
5
+
6
+ // src/keyboard-inset/compute-inset.ts
7
+ var DEFAULT_MIN_INSET = 150;
8
+ function computeKeyboardInset(r) {
9
+ if (!r.editableFocused) return 0;
10
+ const occluded = r.layoutHeight - r.visualHeight - r.visualOffsetTop;
11
+ return occluded >= (r.minInset ?? DEFAULT_MIN_INSET) ? Math.round(occluded) : 0;
12
+ }
13
+ var NON_TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
14
+ "button",
15
+ "checkbox",
16
+ "radio",
17
+ "range",
18
+ "submit",
19
+ "reset",
20
+ "file",
21
+ "color",
22
+ "image",
23
+ "hidden"
24
+ ]);
25
+ function isEditableElement(el) {
26
+ if (!el) return false;
27
+ const tag = el.tagName.toLowerCase();
28
+ if (tag === "select") return !el.disabled;
29
+ if (tag === "textarea" || tag === "input") {
30
+ const field = el;
31
+ if (field.readOnly || field.disabled) return false;
32
+ return tag === "textarea" || !NON_TEXT_INPUT_TYPES.has(field.type);
33
+ }
34
+ if (el.isContentEditable === true) return true;
35
+ const attr = el.getAttribute("contenteditable");
36
+ return attr !== null && attr !== "false";
37
+ }
38
+
39
+ // src/keyboard-inset/use-keyboard-inset.ts
40
+ function useKeyboardInset({ minInset = DEFAULT_MIN_INSET, enabled = true } = {}) {
41
+ const [inset, setInset] = useState(0);
42
+ useEffect(() => {
43
+ const vv = typeof window === "undefined" ? null : window.visualViewport;
44
+ if (!enabled || !vv) {
45
+ setInset(0);
46
+ return;
47
+ }
48
+ const update = () => setInset(
49
+ computeKeyboardInset({
50
+ layoutHeight: window.innerHeight,
51
+ visualHeight: vv.height,
52
+ visualOffsetTop: vv.offsetTop,
53
+ editableFocused: isEditableElement(document.activeElement),
54
+ minInset
55
+ })
56
+ );
57
+ let frame = 0;
58
+ const onFocusOut = () => {
59
+ cancelAnimationFrame(frame);
60
+ frame = requestAnimationFrame(update);
61
+ };
62
+ const onFocusIn = () => {
63
+ cancelAnimationFrame(frame);
64
+ update();
65
+ };
66
+ vv.addEventListener("resize", update);
67
+ vv.addEventListener("scroll", update);
68
+ document.addEventListener("focusin", onFocusIn);
69
+ document.addEventListener("focusout", onFocusOut);
70
+ update();
71
+ return () => {
72
+ cancelAnimationFrame(frame);
73
+ vv.removeEventListener("resize", update);
74
+ vv.removeEventListener("scroll", update);
75
+ document.removeEventListener("focusin", onFocusIn);
76
+ document.removeEventListener("focusout", onFocusOut);
77
+ };
78
+ }, [enabled, minInset]);
79
+ return inset;
80
+ }
81
+
82
+ export {
83
+ computeKeyboardInset,
84
+ isEditableElement,
85
+ useKeyboardInset
86
+ };
@@ -0,0 +1,146 @@
1
+ "use client";
2
+ import {
3
+ isIos,
4
+ isStandalone
5
+ } from "./chunk-YFMMSLMB.js";
6
+
7
+ // src/push/use-push-subscription.ts
8
+ import { useCallback, useEffect, useRef, useState } from "react";
9
+
10
+ // src/push/support.ts
11
+ function getPushSupport(env = readPushEnv()) {
12
+ if (env.isIos && !env.isStandalone) return "ios-needs-install";
13
+ if (!env.hasPushManager || !env.hasServiceWorker || !env.hasNotification) return "unsupported";
14
+ return "supported";
15
+ }
16
+ function readPushEnv() {
17
+ const browser = typeof window !== "undefined" && typeof navigator !== "undefined";
18
+ return {
19
+ hasPushManager: browser && "PushManager" in window,
20
+ hasServiceWorker: browser && "serviceWorker" in navigator,
21
+ hasNotification: browser && "Notification" in window,
22
+ isIos: browser && isIos(),
23
+ isStandalone: browser && isStandalone()
24
+ };
25
+ }
26
+ function readPermission() {
27
+ if (typeof window === "undefined" || !("Notification" in window)) return "unsupported";
28
+ return Notification.permission;
29
+ }
30
+ function shouldResubscribe({ permission, hasSubscription, resubscribeOnLoad }) {
31
+ return resubscribeOnLoad && permission === "granted" && !hasSubscription;
32
+ }
33
+ function isGone(error) {
34
+ return typeof error === "object" && error !== null && error.status === 410;
35
+ }
36
+
37
+ // src/push/vapid.ts
38
+ function urlBase64ToUint8Array(key) {
39
+ const padding = "=".repeat((4 - key.length % 4) % 4);
40
+ const base64 = (key + padding).replace(/-/g, "+").replace(/_/g, "/");
41
+ const raw = atob(base64);
42
+ const out = new Uint8Array(raw.length);
43
+ for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
44
+ return out;
45
+ }
46
+
47
+ // src/push/register.ts
48
+ function subscribeFresh(reg, vapidPublicKey) {
49
+ return reg.pushManager.subscribe({
50
+ userVisibleOnly: true,
51
+ // lib.dom wants ArrayBufferView<ArrayBuffer>; a Uint8Array satisfies the runtime contract.
52
+ applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
53
+ });
54
+ }
55
+ async function ensureSubscribed(reg, { vapidPublicKey, onSubscribe }, existing) {
56
+ let sub = existing ?? await reg.pushManager.getSubscription() ?? await subscribeFresh(reg, vapidPublicKey);
57
+ try {
58
+ await onSubscribe(sub.toJSON());
59
+ } catch (error) {
60
+ if (!isGone(error)) throw error;
61
+ await sub.unsubscribe();
62
+ sub = await subscribeFresh(reg, vapidPublicKey);
63
+ await onSubscribe(sub.toJSON());
64
+ }
65
+ return sub;
66
+ }
67
+
68
+ // src/push/use-push-subscription.ts
69
+ var messageOf = (error, fallback) => error instanceof Error ? error.message : fallback;
70
+ function usePushSubscription(options) {
71
+ const { resubscribeOnLoad = true } = options;
72
+ const [support, setSupport] = useState("unsupported");
73
+ const [permission, setPermission] = useState("unsupported");
74
+ const [subscribed, setSubscribed] = useState(false);
75
+ const [busy, setBusy] = useState(false);
76
+ const [error, setError] = useState(null);
77
+ const latest = useRef(options);
78
+ latest.current = options;
79
+ useEffect(() => {
80
+ const current = getPushSupport();
81
+ setSupport(current);
82
+ setPermission(readPermission());
83
+ if (current !== "supported") return;
84
+ let cancelled = false;
85
+ (async () => {
86
+ try {
87
+ const reg = await navigator.serviceWorker.ready;
88
+ const existing = await reg.pushManager.getSubscription();
89
+ if (cancelled) return;
90
+ const permissionNow = readPermission();
91
+ setPermission(permissionNow);
92
+ if (existing) return setSubscribed(true);
93
+ if (!shouldResubscribe({ permission: permissionNow, hasSubscription: false, resubscribeOnLoad })) return;
94
+ await ensureSubscribed(reg, latest.current);
95
+ if (!cancelled) setSubscribed(true);
96
+ } catch {
97
+ }
98
+ })();
99
+ return () => {
100
+ cancelled = true;
101
+ };
102
+ }, [resubscribeOnLoad]);
103
+ const subscribe = useCallback(async () => {
104
+ if (getPushSupport() !== "supported") return;
105
+ setError(null);
106
+ setBusy(true);
107
+ try {
108
+ const result = await Notification.requestPermission();
109
+ setPermission(result);
110
+ if (result !== "granted") return;
111
+ const reg = await navigator.serviceWorker.ready;
112
+ await ensureSubscribed(reg, latest.current);
113
+ setSubscribed(true);
114
+ } catch (e) {
115
+ setError(messageOf(e, "subscribe failed"));
116
+ } finally {
117
+ setBusy(false);
118
+ }
119
+ }, []);
120
+ const unsubscribe = useCallback(async () => {
121
+ if (getPushSupport() !== "supported") return;
122
+ setError(null);
123
+ setBusy(true);
124
+ try {
125
+ const reg = await navigator.serviceWorker.ready;
126
+ const sub = await reg.pushManager.getSubscription();
127
+ if (sub) {
128
+ await latest.current.onUnsubscribe(sub.endpoint);
129
+ await sub.unsubscribe();
130
+ }
131
+ setSubscribed(false);
132
+ } catch (e) {
133
+ setError(messageOf(e, "unsubscribe failed"));
134
+ } finally {
135
+ setBusy(false);
136
+ }
137
+ }, []);
138
+ return { support, permission, subscribed, busy, error, subscribe, unsubscribe };
139
+ }
140
+
141
+ export {
142
+ getPushSupport,
143
+ shouldResubscribe,
144
+ urlBase64ToUint8Array,
145
+ usePushSubscription
146
+ };
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ // src/visibility-polling/schedule.ts
4
+ function nextDelay(intervalMs, jitter, random) {
5
+ const fraction = Math.min(1, Math.max(0, jitter));
6
+ return intervalMs + random() * fraction * intervalMs;
7
+ }
8
+
9
+ // src/visibility-polling/use-visibility-polling.ts
10
+ import { useEffect, useRef } from "react";
11
+ var isHidden = () => document.visibilityState === "hidden";
12
+ function useVisibilityPolling(fn, intervalMs, { enabled = true, immediate = true, jitter = 0, runOnVisible = true } = {}) {
13
+ const fnRef = useRef(fn);
14
+ const optionsRef = useRef({ immediate, jitter, runOnVisible });
15
+ useEffect(() => {
16
+ fnRef.current = fn;
17
+ optionsRef.current = { immediate, jitter, runOnVisible };
18
+ });
19
+ const runningRef = useRef(false);
20
+ const scheduleRef = useRef(null);
21
+ useEffect(() => {
22
+ if (!enabled) return;
23
+ let active = true;
24
+ let timer = null;
25
+ function clearTimer() {
26
+ if (timer !== null) clearTimeout(timer);
27
+ timer = null;
28
+ }
29
+ function schedule() {
30
+ clearTimer();
31
+ if (!active || isHidden()) return;
32
+ timer = setTimeout(run, nextDelay(intervalMs, optionsRef.current.jitter, Math.random));
33
+ }
34
+ async function run() {
35
+ timer = null;
36
+ if (!active || runningRef.current || isHidden()) return;
37
+ runningRef.current = true;
38
+ try {
39
+ await fnRef.current();
40
+ } catch {
41
+ } finally {
42
+ runningRef.current = false;
43
+ }
44
+ scheduleRef.current?.();
45
+ }
46
+ function onVisibilityChange() {
47
+ if (isHidden()) {
48
+ clearTimer();
49
+ } else if (optionsRef.current.runOnVisible) {
50
+ clearTimer();
51
+ void run();
52
+ } else if (!runningRef.current) {
53
+ schedule();
54
+ }
55
+ }
56
+ scheduleRef.current = schedule;
57
+ if (optionsRef.current.immediate && !isHidden()) void run();
58
+ else schedule();
59
+ document.addEventListener("visibilitychange", onVisibilityChange);
60
+ return () => {
61
+ active = false;
62
+ scheduleRef.current = null;
63
+ clearTimer();
64
+ document.removeEventListener("visibilitychange", onVisibilityChange);
65
+ };
66
+ }, [enabled, intervalMs]);
67
+ }
68
+
69
+ export {
70
+ nextDelay,
71
+ useVisibilityPolling
72
+ };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ // src/media-query/media-query.ts
4
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
5
+ var COARSE_POINTER_QUERY = "(pointer: coarse)";
6
+ var lists = /* @__PURE__ */ new WeakMap();
7
+ function getList(query) {
8
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return null;
9
+ const owner = window.matchMedia;
10
+ let byQuery = lists.get(owner);
11
+ if (!byQuery) {
12
+ byQuery = /* @__PURE__ */ new Map();
13
+ lists.set(owner, byQuery);
14
+ }
15
+ let list = byQuery.get(query);
16
+ if (!list) {
17
+ list = owner.call(window, query);
18
+ byQuery.set(query, list);
19
+ }
20
+ return list;
21
+ }
22
+ function getMatches(query) {
23
+ return getList(query)?.matches ?? null;
24
+ }
25
+ function subscribe(query, onChange) {
26
+ const list = getList(query);
27
+ if (!list) return () => {
28
+ };
29
+ if (typeof list.addEventListener === "function") {
30
+ list.addEventListener("change", onChange);
31
+ return () => list.removeEventListener("change", onChange);
32
+ }
33
+ list.addListener(onChange);
34
+ return () => list.removeListener(onChange);
35
+ }
36
+
37
+ export {
38
+ REDUCED_MOTION_QUERY,
39
+ COARSE_POINTER_QUERY,
40
+ getMatches,
41
+ subscribe
42
+ };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ // src/online-status/use-online-status.ts
4
+ import { useSyncExternalStore } from "react";
5
+ function subscribe(callback) {
6
+ window.addEventListener("online", callback);
7
+ window.addEventListener("offline", callback);
8
+ return () => {
9
+ window.removeEventListener("online", callback);
10
+ window.removeEventListener("offline", callback);
11
+ };
12
+ }
13
+ var getSnapshot = () => navigator.onLine;
14
+ var getServerSnapshot = () => true;
15
+ function useOnlineStatus() {
16
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
17
+ }
18
+
19
+ export {
20
+ useOnlineStatus
21
+ };
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ // src/pwa/detect.ts
4
+ var IOS_UA = /iPhone|iPad|iPod/;
5
+ var IOS_OTHER_BROWSER = /CriOS|FxiOS|EdgiOS|OPiOS/;
6
+ var IN_APP_WEBVIEW = /FBAN|FBAV|Instagram/;
7
+ var STANDALONE_QUERY = "(display-mode: standalone)";
8
+ function detectPlatform(env) {
9
+ const isIos2 = IOS_UA.test(env.userAgent) || env.platform === "MacIntel" && env.maxTouchPoints > 1;
10
+ const isIosSafari2 = isIos2 && !IOS_OTHER_BROWSER.test(env.userAgent) && !IN_APP_WEBVIEW.test(env.userAgent);
11
+ return { isIos: isIos2, isIosSafari: isIosSafari2, isStandalone: env.standaloneMedia || env.navigatorStandalone };
12
+ }
13
+ function readStandaloneMedia() {
14
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
15
+ return window.matchMedia(STANDALONE_QUERY).matches;
16
+ }
17
+ function readNavigatorStandalone() {
18
+ if (typeof navigator === "undefined") return false;
19
+ return navigator.standalone === true;
20
+ }
21
+ function readPlatform() {
22
+ if (typeof navigator === "undefined") return { isIos: false, isIosSafari: false, isStandalone: false };
23
+ return detectPlatform({
24
+ userAgent: navigator.userAgent ?? "",
25
+ platform: navigator.platform ?? "",
26
+ maxTouchPoints: navigator.maxTouchPoints ?? 0,
27
+ standaloneMedia: readStandaloneMedia(),
28
+ navigatorStandalone: readNavigatorStandalone()
29
+ });
30
+ }
31
+ var isStandalone = () => readPlatform().isStandalone;
32
+ var isIos = () => readPlatform().isIos;
33
+ var isIosSafari = () => readPlatform().isIosSafari;
34
+
35
+ export {
36
+ STANDALONE_QUERY,
37
+ detectPlatform,
38
+ isStandalone,
39
+ isIos,
40
+ isIosSafari
41
+ };