@vezham/hooks 26.0.3 → 26.2.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/README.md CHANGED
@@ -1 +1,48 @@
1
1
  # @vezham/hooks
2
+
3
+ ## useLocalStore
4
+
5
+ ```tsx
6
+ import { useLocalStore } from '@vezham/hooks'
7
+
8
+ const DEFAULTS = { sounds: true }
9
+ const Preferences = () => {
10
+ const { value, setValue, reset, isReady, isSaved, error } = useLocalStore(
11
+ 'my-app:preferences:v1',
12
+ DEFAULTS,
13
+ { debounceMs: 150 }
14
+ )
15
+ // Use setValue(next) or setValue(current => next).
16
+ }
17
+ ```
18
+
19
+ Provide a stable default and an optional `parse(unknown)` function to validate
20
+ or migrate stored JSON. One key represents one schema. Reads start after mount,
21
+ so static/prerendered HTML has a deterministic initial value. Failed reads and
22
+ writes retain a usable in-memory value and expose `error`/`isSaved`. Defaults
23
+ are not saved during hydration; successful parser migrations are persisted.
24
+
25
+ Updates notify consumers in the current document and listen to `storage`
26
+ events from other tabs. Cross-tab conflicts use the latest storage event.
27
+ `reset()` removes only this key. Debounced writes flush on pagehide and final
28
+ unsubscribe; `flush()` can explicitly retry a failed write. Scope keys by host,
29
+ workspace, and user where needed. Do not store credentials.
30
+
31
+ ## useCopyToClipboard
32
+
33
+ ```tsx
34
+ import { useCopyToClipboard } from '@vezham/hooks'
35
+
36
+ const CopyButton = ({ text }: { text: string }) => {
37
+ const { copy, isCopied, isUnavailable } = useCopyToClipboard()
38
+ return (
39
+ <button onClick={() => void copy(text)}>
40
+ {isCopied ? 'Copied' : isUnavailable ? 'Copy unavailable' : 'Copy'}
41
+ </button>
42
+ )
43
+ }
44
+ ```
45
+
46
+ Uses the Clipboard API when available and falls back to a temporary selection for
47
+ restricted origins. The hook exposes the last copy result as `status`, `isCopied`,
48
+ and `isUnavailable`.
package/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
+ export { AppContext, useApp, type AppIdentity } from './lib/use-app';
1
2
  export { CommandContext, useCommand } from './lib/use-command';
3
+ export { useLocalStore, type LocalStoreOptions } from './lib/use-local-store';
4
+ export { useCopyToClipboard, type CopyToClipboardStatus } from './lib/use-copy-to-clipboard';
2
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EACL,kBAAkB,EAClB,KAAK,qBAAqB,EAC3B,MAAM,6BAA6B,CAAA"}
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
- import { CommandContext, useCommand } from "./lib/use-command.js";
2
- export { CommandContext, useCommand };
1
+ import { AppContext, useApp } from "./lib/use-app/index.js";
2
+ import { CommandContext, useCommand } from "./lib/use-command/index.js";
3
+ import { useLocalStore } from "./lib/use-local-store/index.js";
4
+ import { useCopyToClipboard } from "./lib/use-copy-to-clipboard/index.js";
5
+ export { AppContext, CommandContext, useApp, useCommand, useCopyToClipboard, useLocalStore };
@@ -0,0 +1,10 @@
1
+ export interface AppIdentity {
2
+ readonly id: string;
3
+ readonly name: string;
4
+ readonly version: string;
5
+ readonly environment: string;
6
+ readonly runtime: string;
7
+ }
8
+ export declare const AppContext: import("react").Context<AppIdentity | undefined>;
9
+ export declare const useApp: () => AppIdentity | undefined;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-app/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,eAAO,MAAM,UAAU,kDAAoD,CAAA;AAE3E,eAAO,MAAM,MAAM,+BAA+B,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/lib/use-app/index.ts
3
+ var AppContext = createContext(void 0);
4
+ var useApp = () => useContext(AppContext);
5
+ //#endregion
6
+ export { AppContext, useApp };
@@ -7,4 +7,4 @@ interface CommandContextValue {
7
7
  export declare const CommandContext: import("react").Context<CommandContextValue | null>;
8
8
  export declare const useCommand: () => CommandContextValue;
9
9
  export {};
10
- //# sourceMappingURL=use-command.d.ts.map
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-command/index.ts"],"names":[],"mappings":"AAEA,UAAU,mBAAmB;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B;AAED,eAAO,MAAM,cAAc,qDAAkD,CAAA;AAE7E,eAAO,MAAM,UAAU,2BAQtB,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext } from "react";
2
- //#region src/lib/use-command.ts
2
+ //#region src/lib/use-command/index.ts
3
3
  var CommandContext = createContext(null);
4
4
  var useCommand = () => {
5
5
  const context = useContext(CommandContext);
@@ -0,0 +1,8 @@
1
+ export type CopyToClipboardStatus = 'idle' | 'copied' | 'unavailable';
2
+ export declare const useCopyToClipboard: () => {
3
+ copy: (text: string) => Promise<boolean>;
4
+ status: CopyToClipboardStatus;
5
+ isCopied: boolean;
6
+ isUnavailable: boolean;
7
+ };
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-copy-to-clipboard/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,aAAa,CAAA;AAErE,eAAO,MAAM,kBAAkB;iBAES,MAAM;;;;CAiB7C,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { writeClipboardText } from "./write-clipboard-text.js";
2
+ import { useCallback, useState } from "react";
3
+ //#region src/lib/use-copy-to-clipboard/index.ts
4
+ var useCopyToClipboard = () => {
5
+ const [status, setStatus] = useState("idle");
6
+ return {
7
+ copy: useCallback(async (text) => {
8
+ try {
9
+ await writeClipboardText(text);
10
+ setStatus("copied");
11
+ return true;
12
+ } catch {
13
+ setStatus("unavailable");
14
+ return false;
15
+ }
16
+ }, []),
17
+ status,
18
+ isCopied: status === "copied",
19
+ isUnavailable: status === "unavailable"
20
+ };
21
+ };
22
+ //#endregion
23
+ export { useCopyToClipboard };
@@ -0,0 +1,2 @@
1
+ export declare const writeClipboardText: (text: string) => Promise<void>;
2
+ //# sourceMappingURL=write-clipboard-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-clipboard-text.d.ts","sourceRoot":"","sources":["../../../src/lib/use-copy-to-clipboard/write-clipboard-text.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,GAAU,MAAM,MAAM,kBAkCpD,CAAA"}
@@ -0,0 +1,26 @@
1
+ //#region src/lib/use-copy-to-clipboard/write-clipboard-text.ts
2
+ var writeClipboardText = async (text) => {
3
+ if (typeof navigator !== "undefined" && typeof navigator.clipboard?.writeText === "function") try {
4
+ await navigator.clipboard.writeText(text);
5
+ return;
6
+ } catch {}
7
+ if (typeof document === "undefined" || !document.body) throw new Error("Clipboard copy is unavailable");
8
+ const input = document.createElement("textarea");
9
+ input.value = text;
10
+ input.setAttribute("readonly", "");
11
+ input.setAttribute("aria-hidden", "true");
12
+ input.tabIndex = -1;
13
+ input.style.position = "fixed";
14
+ input.style.top = "0";
15
+ input.style.left = "-9999px";
16
+ input.style.fontSize = "16px";
17
+ document.body.appendChild(input);
18
+ input.focus();
19
+ input.select();
20
+ input.setSelectionRange(0, text.length);
21
+ const copied = document.execCommand("copy");
22
+ input.remove();
23
+ if (!copied) throw new Error("Clipboard copy is unavailable");
24
+ };
25
+ //#endregion
26
+ export { writeClipboardText };
@@ -0,0 +1,12 @@
1
+ import type { LocalStoreOptions } from './local-store';
2
+ export declare const useLocalStore: <T>(key: string, initialValue: T, options?: LocalStoreOptions<T>) => {
3
+ setValue: (update: T | ((current: T) => T)) => void;
4
+ reset: () => void;
5
+ flush: () => void;
6
+ value: T;
7
+ isReady: boolean;
8
+ isSaved: boolean;
9
+ error: unknown;
10
+ };
11
+ export type { LocalStoreOptions } from './local-store';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-local-store/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAItD,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,KAAK,MAAM,EACX,cAAc,CAAC,EACf,UAAS,iBAAiB,CAAC,CAAC,CAAM;;;;;;;;CAkBnC,CAAA;AAED,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { getLocalStore } from "./local-store.js";
2
+ import { useMemo, useSyncExternalStore } from "react";
3
+ //#region src/lib/use-local-store/index.ts
4
+ var useLocalStore = (key, initialValue, options = {}) => {
5
+ const { parse, debounceMs } = options;
6
+ const store = useMemo(() => getLocalStore(key, initialValue, {
7
+ parse,
8
+ debounceMs
9
+ }), [
10
+ key,
11
+ initialValue,
12
+ parse,
13
+ debounceMs
14
+ ]);
15
+ return {
16
+ ...useSyncExternalStore(store.subscribe, store.getSnapshot, store.getInitialSnapshot),
17
+ setValue: store.setValue,
18
+ reset: store.reset,
19
+ flush: store.flush
20
+ };
21
+ };
22
+ //#endregion
23
+ export { useLocalStore };
@@ -0,0 +1,20 @@
1
+ export interface LocalStoreOptions<T> {
2
+ parse?: (value: unknown) => T;
3
+ debounceMs?: number;
4
+ }
5
+ interface Snapshot<T> {
6
+ value: T;
7
+ isReady: boolean;
8
+ isSaved: boolean;
9
+ error: unknown;
10
+ }
11
+ export declare const getLocalStore: <T>(key: string, initialValue: T, options: LocalStoreOptions<T>) => {
12
+ getSnapshot: () => Snapshot<T>;
13
+ getInitialSnapshot: () => Snapshot<T>;
14
+ setValue: (update: T | ((current: T) => T)) => void;
15
+ reset: () => void;
16
+ flush: () => void;
17
+ subscribe: (listener: () => void) => () => void;
18
+ };
19
+ export {};
20
+ //# sourceMappingURL=local-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["../../../src/lib/use-local-store/local-store.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,UAAU,QAAQ,CAAC,CAAC;IAClB,KAAK,EAAE,CAAC,CAAA;IACR,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAsHD,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,KAAK,MAAM,EACX,cAAc,CAAC,EACf,SAAS,iBAAiB,CAAC,CAAC,CAAC;;;;;;gCA5BC,IAAI;CAmCnC,CAAA"}
@@ -0,0 +1,141 @@
1
+ //#region src/lib/use-local-store/local-store.ts
2
+ var createLocalStore = (key, initialValue, { parse, debounceMs = 0 }) => {
3
+ const initial = {
4
+ value: initialValue,
5
+ isReady: false,
6
+ isSaved: true,
7
+ error: null
8
+ };
9
+ let snapshot = initial;
10
+ let dirty = false;
11
+ let timer;
12
+ const listeners = /* @__PURE__ */ new Set();
13
+ const emit = () => listeners.forEach((listener) => listener());
14
+ const cancel = () => {
15
+ clearTimeout(timer);
16
+ timer = void 0;
17
+ };
18
+ const flush = () => {
19
+ cancel();
20
+ if (!dirty || typeof window === "undefined") return;
21
+ try {
22
+ window.localStorage.setItem(key, JSON.stringify(snapshot.value));
23
+ dirty = false;
24
+ snapshot = {
25
+ ...snapshot,
26
+ isSaved: true,
27
+ error: null
28
+ };
29
+ } catch (error) {
30
+ snapshot = {
31
+ ...snapshot,
32
+ isSaved: false,
33
+ error
34
+ };
35
+ }
36
+ emit();
37
+ };
38
+ const read = () => {
39
+ try {
40
+ const raw = window.localStorage.getItem(key);
41
+ const decoded = raw === null ? initialValue : JSON.parse(raw);
42
+ const value = raw === null ? initialValue : parse ? parse(decoded) : decoded;
43
+ snapshot = {
44
+ value,
45
+ isReady: true,
46
+ isSaved: true,
47
+ error: null
48
+ };
49
+ if (raw !== null && parse && JSON.stringify(value) !== raw) {
50
+ dirty = true;
51
+ flush();
52
+ }
53
+ } catch (error) {
54
+ snapshot = {
55
+ value: initialValue,
56
+ isReady: true,
57
+ isSaved: false,
58
+ error
59
+ };
60
+ }
61
+ emit();
62
+ };
63
+ const onStorage = (event) => {
64
+ if (event.key !== null && event.key !== key) return;
65
+ if (event.storageArea && event.storageArea !== window.localStorage) return;
66
+ cancel();
67
+ dirty = false;
68
+ read();
69
+ };
70
+ const setValue = (update) => {
71
+ if (typeof window === "undefined") return;
72
+ if (!snapshot.isReady) read();
73
+ snapshot = {
74
+ value: typeof update === "function" ? update(snapshot.value) : update,
75
+ isReady: true,
76
+ isSaved: false,
77
+ error: null
78
+ };
79
+ dirty = true;
80
+ emit();
81
+ cancel();
82
+ if (debounceMs > 0) timer = setTimeout(flush, debounceMs);
83
+ else flush();
84
+ };
85
+ const reset = () => {
86
+ cancel();
87
+ dirty = false;
88
+ snapshot = {
89
+ value: initialValue,
90
+ isReady: true,
91
+ isSaved: true,
92
+ error: null
93
+ };
94
+ try {
95
+ window.localStorage.removeItem(key);
96
+ } catch (error) {
97
+ snapshot = {
98
+ ...snapshot,
99
+ isSaved: false,
100
+ error
101
+ };
102
+ }
103
+ emit();
104
+ };
105
+ const store = {
106
+ getSnapshot: () => snapshot,
107
+ getInitialSnapshot: () => initial,
108
+ setValue,
109
+ reset,
110
+ flush,
111
+ subscribe: (listener) => {
112
+ const first = listeners.size === 0;
113
+ listeners.add(listener);
114
+ if (first) {
115
+ stores.set(key, store);
116
+ if (!dirty) read();
117
+ window.addEventListener("storage", onStorage);
118
+ window.addEventListener("pagehide", flush);
119
+ }
120
+ return () => {
121
+ listeners.delete(listener);
122
+ if (listeners.size) return;
123
+ flush();
124
+ window.removeEventListener("storage", onStorage);
125
+ window.removeEventListener("pagehide", flush);
126
+ if (stores.get(key) === store) stores.delete(key);
127
+ };
128
+ }
129
+ };
130
+ return store;
131
+ };
132
+ var stores = /* @__PURE__ */ new Map();
133
+ var getLocalStore = (key, initialValue, options) => {
134
+ const existing = stores.get(key);
135
+ if (existing) return existing;
136
+ const store = createLocalStore(key, initialValue, options);
137
+ stores.set(key, store);
138
+ return store;
139
+ };
140
+ //#endregion
141
+ export { getLocalStore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vezham/hooks",
3
- "version": "26.0.3",
3
+ "version": "26.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/vezham/vx-fe-pkgs.git",
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-command.d.ts","sourceRoot":"","sources":["../../src/lib/use-command.ts"],"names":[],"mappings":"AAEA,UAAU,mBAAmB;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B;AAED,eAAO,MAAM,cAAc,qDAAkD,CAAA;AAE7E,eAAO,MAAM,UAAU,2BAQtB,CAAA"}