@unhingged/vizu-react 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zoltán Balogh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @vizu/react
2
+
3
+ React bindings for [@vizu/core](../vizu). Drop a provider in your tree, use hooks to read comments / set identity / register actions.
4
+
5
+ ```bash
6
+ npm install @vizu/core @vizu/react
7
+ ```
8
+
9
+ ## Use it
10
+
11
+ ```tsx
12
+ 'use client';
13
+ import { VizuProvider, useVizu, useComments, useVizuUser, useVizuEvent, useVizuAction } from '@vizu/react';
14
+
15
+ export default function App() {
16
+ return (
17
+ <VizuProvider options={{
18
+ namespace: 'my-site',
19
+ pageVersion: 'v1',
20
+ shortcut: 'mod+shift+e',
21
+ user: { name: 'Anonymous' },
22
+ startEnabled: true,
23
+ }}>
24
+ <YourPage />
25
+ </VizuProvider>
26
+ );
27
+ }
28
+
29
+ function YourPage() {
30
+ // 1. Read current user (re-renders on setUser)
31
+ const [user, setUser] = useVizuUser();
32
+
33
+ // 2. Hydrate identity from your auth/session
34
+ useEffect(() => {
35
+ setUser({ id: session.userId, name: session.name, avatarUrl: session.avatar });
36
+ }, [session]);
37
+
38
+ // 3. Read live comments (re-renders on add/remove/clear/set)
39
+ const comments = useComments();
40
+
41
+ // 4. Listen to events — persist to your backend
42
+ useVizuEvent('comment:added', ({ comment }) => {
43
+ fetch('/api/comments', { method: 'POST', body: JSON.stringify(comment) });
44
+ });
45
+
46
+ // 5. Register a pill action
47
+ useVizuAction({
48
+ id: 'send-to-api',
49
+ label: 'Send to API',
50
+ variant: 'primary',
51
+ onClick: (ctx) => fetch('/api/iterate', {
52
+ method: 'POST',
53
+ body: JSON.stringify({ comments: ctx.comments, pageHtml: ctx.pageHtml }),
54
+ }),
55
+ visibleWhen: ({ commentsCount }) => commentsCount > 0,
56
+ });
57
+
58
+ // 6. Imperative access
59
+ const vizu = useVizu();
60
+ useEffect(() => {
61
+ // Pre-load comments from your backend
62
+ fetch(`/api/comments?ns=my-site`).then(r => r.json()).then(c => vizu.setComments(c, { persist: false }));
63
+ }, [vizu]);
64
+
65
+ return <div>Your page content…</div>;
66
+ }
67
+ ```
68
+
69
+ ## What you get
70
+
71
+ - `<VizuProvider>` — mounts a single Vizu instance for the subtree (lazy, destroyed on unmount)
72
+ - `useVizu()` — the instance itself for imperative calls (`vizu.enable()`, `vizu.clearAll()`, etc.)
73
+ - `useComments()` — subscribes to all comment-list events, returns `VizuComment[]`
74
+ - `useVizuUser()` — `[user, setUser]` tuple, re-renders on `user:changed`
75
+ - `useVizuEvent('event', handler)` — subscribe to any Vizu event for the component lifetime
76
+ - `useVizuAction({...})` — register an action while mounted; auto-removed on unmount
77
+
78
+ ## Storage defaults
79
+
80
+ Programmatic mode defaults to **in-memory** storage. Provide `options.storage` if you want persistence:
81
+
82
+ ```tsx
83
+ <VizuProvider options={{ storage: 'local' }}>...
84
+ <VizuProvider options={{ storage: myCustomAdapter }}>...
85
+ ```
86
+
87
+ Or skip storage entirely and own it in your effects — listen to `comment:added` and POST to your API.
88
+
89
+ ## Next.js (App Router)
90
+
91
+ `VizuProvider` is a client component (it uses refs + effects). Put it inside a layout that opts in:
92
+
93
+ ```tsx
94
+ // app/(commentable)/layout.tsx
95
+ 'use client';
96
+ import { VizuProvider } from '@vizu/react';
97
+ export default function Layout({ children }) {
98
+ return <VizuProvider options={{ namespace: 'my-app' }}>{children}</VizuProvider>;
99
+ }
100
+ ```
101
+
102
+ Page-level usage stays as Server Components; only the provider and the components that call its hooks need the `'use client'` directive.
package/dist/index.cjs ADDED
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.tsx
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ VizuProvider: () => VizuProvider,
25
+ useComments: () => useComments,
26
+ useVizu: () => useVizu,
27
+ useVizuAction: () => useVizuAction,
28
+ useVizuEvent: () => useVizuEvent,
29
+ useVizuUser: () => useVizuUser
30
+ });
31
+ module.exports = __toCommonJS(src_exports);
32
+ var import_react = require("react");
33
+ var import_vizu_core = require("@unhingged/vizu-core");
34
+ var import_jsx_runtime = require("react/jsx-runtime");
35
+ var VizuContext = (0, import_react.createContext)(null);
36
+ function VizuProvider({ options, children }) {
37
+ const ref = (0, import_react.useRef)(null);
38
+ if (ref.current === null) {
39
+ ref.current = new import_vizu_core.Vizu(options ?? {});
40
+ }
41
+ const vizu = ref.current;
42
+ (0, import_react.useEffect)(() => {
43
+ return () => {
44
+ vizu.destroy();
45
+ ref.current = null;
46
+ };
47
+ }, []);
48
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VizuContext.Provider, { value: vizu, children });
49
+ }
50
+ function useVizu() {
51
+ const v = (0, import_react.useContext)(VizuContext);
52
+ if (!v) {
53
+ throw new Error("useVizu must be called inside a <VizuProvider>.");
54
+ }
55
+ return v;
56
+ }
57
+ function useVizuEvent(event, handler) {
58
+ const vizu = useVizu();
59
+ const handlerRef = (0, import_react.useRef)(handler);
60
+ handlerRef.current = handler;
61
+ (0, import_react.useEffect)(() => {
62
+ const off = vizu.on(event, (payload) => handlerRef.current(payload));
63
+ return off;
64
+ }, [vizu, event]);
65
+ }
66
+ function useComments() {
67
+ const vizu = useVizu();
68
+ const subscribe = (0, import_react.useMemo)(
69
+ () => (cb) => {
70
+ const offs = [
71
+ vizu.on("comment:added", cb),
72
+ vizu.on("comment:removed", cb),
73
+ vizu.on("comments:cleared", cb),
74
+ vizu.on("comments:set", cb),
75
+ vizu.on("comments:loaded", cb)
76
+ ];
77
+ return () => offs.forEach((off) => off());
78
+ },
79
+ [vizu]
80
+ );
81
+ const cacheRef = (0, import_react.useRef)(vizu.getComments());
82
+ const getSnapshot = () => {
83
+ const next = vizu.getComments();
84
+ const last = cacheRef.current;
85
+ if (last.length === next.length && last.every((c, i) => c === next[i] || c.id === next[i].id && c.text === next[i].text)) {
86
+ return last;
87
+ }
88
+ cacheRef.current = next;
89
+ return next;
90
+ };
91
+ return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
92
+ }
93
+ function useVizuUser() {
94
+ const vizu = useVizu();
95
+ const subscribe = (0, import_react.useMemo)(
96
+ () => (cb) => vizu.on("user:changed", cb),
97
+ [vizu]
98
+ );
99
+ const getSnapshot = () => vizu.getUser();
100
+ const user = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
101
+ return [user, (u) => vizu.setUser(u)];
102
+ }
103
+ function useVizuAction(action) {
104
+ const vizu = useVizu();
105
+ const actionRef = (0, import_react.useRef)(action);
106
+ actionRef.current = action;
107
+ (0, import_react.useEffect)(() => {
108
+ vizu.addAction({
109
+ ...action,
110
+ onClick: (ctx) => actionRef.current.onClick(ctx),
111
+ visibleWhen: action.visibleWhen ? ({ commentsCount }) => actionRef.current.visibleWhen?.({ commentsCount }) ?? true : void 0
112
+ });
113
+ return () => vizu.removeAction(action.id);
114
+ }, [vizu, action.id]);
115
+ }
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ VizuProvider,
119
+ useComments,
120
+ useVizu,
121
+ useVizuAction,
122
+ useVizuEvent,
123
+ useVizuUser
124
+ });
125
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport {\n Vizu,\n type VizuOptions,\n type VizuComment,\n type VizuUser,\n type VizuAction,\n type VizuEventName,\n type VizuEventHandler,\n} from '@unhingged/vizu-core';\n\nexport type {\n Vizu,\n VizuOptions,\n VizuComment,\n VizuUser,\n VizuAction,\n VizuEventName,\n VizuEventHandler,\n};\n\nconst VizuContext = createContext<Vizu | null>(null);\n\nexport interface VizuProviderProps {\n options?: VizuOptions;\n children?: ReactNode;\n}\n\n/**\n * Mounts a single Vizu instance for the subtree. The instance is created lazily\n * once and persists across re-renders. Pass options once at construction;\n * imperative changes go through `useVizu()`.\n */\nexport function VizuProvider({ options, children }: VizuProviderProps) {\n // We need a stable instance across renders. Use a ref so re-renders don't construct new ones.\n const ref = useRef<Vizu | null>(null);\n if (ref.current === null) {\n ref.current = new Vizu(options ?? {});\n }\n const vizu = ref.current;\n\n useEffect(() => {\n return () => {\n vizu.destroy();\n ref.current = null;\n };\n // Intentionally only run on unmount. Recreating Vizu mid-tree would lose listeners + state.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return <VizuContext.Provider value={vizu}>{children}</VizuContext.Provider>;\n}\n\n/** Returns the Vizu instance. Throws if used outside a <VizuProvider>. */\nexport function useVizu(): Vizu {\n const v = useContext(VizuContext);\n if (!v) {\n throw new Error('useVizu must be called inside a <VizuProvider>.');\n }\n return v;\n}\n\n/** Subscribe to a Vizu event for the component's lifetime. Fire-and-forget. */\nexport function useVizuEvent<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void {\n const vizu = useVizu();\n // Stable handler ref — re-subscribe only when the event name changes.\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => {\n const off = vizu.on(event, (payload) => handlerRef.current(payload));\n return off;\n }, [vizu, event]);\n}\n\n/** Returns the current comments array and re-renders when it changes. */\nexport function useComments(): VizuComment[] {\n const vizu = useVizu();\n // Stable subscribe + snapshot for useSyncExternalStore.\n const subscribe = useMemo(\n () => (cb: () => void) => {\n const offs = [\n vizu.on('comment:added', cb),\n vizu.on('comment:removed', cb),\n vizu.on('comments:cleared', cb),\n vizu.on('comments:set', cb),\n vizu.on('comments:loaded', cb),\n ];\n return () => offs.forEach((off) => off());\n },\n [vizu],\n );\n // getSnapshot must return a referentially stable result when the data hasn't changed.\n // We re-derive only when the subscribe fires; meanwhile memoize the last array.\n const cacheRef = useRef<VizuComment[]>(vizu.getComments());\n const getSnapshot = () => {\n const next = vizu.getComments();\n const last = cacheRef.current;\n if (last.length === next.length && last.every((c, i) => c === next[i] || (c.id === next[i].id && c.text === next[i].text))) {\n return last;\n }\n cacheRef.current = next;\n return next;\n };\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/** Returns the current user and re-renders when it changes. Also exposes a setter. */\nexport function useVizuUser(): [VizuUser | null, (u: VizuUser | null) => void] {\n const vizu = useVizu();\n const subscribe = useMemo(\n () => (cb: () => void) => vizu.on('user:changed', cb),\n [vizu],\n );\n const getSnapshot = () => vizu.getUser();\n const user = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n return [user, (u) => vizu.setUser(u)];\n}\n\n/**\n * Register an action while the component is mounted. The action is removed\n * automatically on unmount. Re-renders DON'T re-register unless `action.id` changes.\n */\nexport function useVizuAction(action: VizuAction): void {\n const vizu = useVizu();\n const actionRef = useRef(action);\n actionRef.current = action;\n useEffect(() => {\n // Wrap so we always call the latest action.onClick / visibleWhen\n vizu.addAction({\n ...action,\n onClick: (ctx) => actionRef.current.onClick(ctx),\n visibleWhen: action.visibleWhen\n ? ({ commentsCount }) => actionRef.current.visibleWhen?.({ commentsCount }) ?? true\n : undefined,\n });\n return () => vizu.removeAction(action.id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [vizu, action.id]);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { VizuOptions, VizuComment, Vizu, VizuAction, VizuEventName, VizuEventHandler, VizuUser } from '@unhingged/vizu-core';
4
+ export { Vizu, VizuAction, VizuComment, VizuEventHandler, VizuEventName, VizuOptions, VizuUser } from '@unhingged/vizu-core';
5
+
6
+ interface VizuProviderProps {
7
+ options?: VizuOptions;
8
+ children?: ReactNode;
9
+ }
10
+ /**
11
+ * Mounts a single Vizu instance for the subtree. The instance is created lazily
12
+ * once and persists across re-renders. Pass options once at construction;
13
+ * imperative changes go through `useVizu()`.
14
+ */
15
+ declare function VizuProvider({ options, children }: VizuProviderProps): react_jsx_runtime.JSX.Element;
16
+ /** Returns the Vizu instance. Throws if used outside a <VizuProvider>. */
17
+ declare function useVizu(): Vizu;
18
+ /** Subscribe to a Vizu event for the component's lifetime. Fire-and-forget. */
19
+ declare function useVizuEvent<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void;
20
+ /** Returns the current comments array and re-renders when it changes. */
21
+ declare function useComments(): VizuComment[];
22
+ /** Returns the current user and re-renders when it changes. Also exposes a setter. */
23
+ declare function useVizuUser(): [VizuUser | null, (u: VizuUser | null) => void];
24
+ /**
25
+ * Register an action while the component is mounted. The action is removed
26
+ * automatically on unmount. Re-renders DON'T re-register unless `action.id` changes.
27
+ */
28
+ declare function useVizuAction(action: VizuAction): void;
29
+
30
+ export { VizuProvider, type VizuProviderProps, useComments, useVizu, useVizuAction, useVizuEvent, useVizuUser };
@@ -0,0 +1,30 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { VizuOptions, VizuComment, Vizu, VizuAction, VizuEventName, VizuEventHandler, VizuUser } from '@unhingged/vizu-core';
4
+ export { Vizu, VizuAction, VizuComment, VizuEventHandler, VizuEventName, VizuOptions, VizuUser } from '@unhingged/vizu-core';
5
+
6
+ interface VizuProviderProps {
7
+ options?: VizuOptions;
8
+ children?: ReactNode;
9
+ }
10
+ /**
11
+ * Mounts a single Vizu instance for the subtree. The instance is created lazily
12
+ * once and persists across re-renders. Pass options once at construction;
13
+ * imperative changes go through `useVizu()`.
14
+ */
15
+ declare function VizuProvider({ options, children }: VizuProviderProps): react_jsx_runtime.JSX.Element;
16
+ /** Returns the Vizu instance. Throws if used outside a <VizuProvider>. */
17
+ declare function useVizu(): Vizu;
18
+ /** Subscribe to a Vizu event for the component's lifetime. Fire-and-forget. */
19
+ declare function useVizuEvent<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void;
20
+ /** Returns the current comments array and re-renders when it changes. */
21
+ declare function useComments(): VizuComment[];
22
+ /** Returns the current user and re-renders when it changes. Also exposes a setter. */
23
+ declare function useVizuUser(): [VizuUser | null, (u: VizuUser | null) => void];
24
+ /**
25
+ * Register an action while the component is mounted. The action is removed
26
+ * automatically on unmount. Re-renders DON'T re-register unless `action.id` changes.
27
+ */
28
+ declare function useVizuAction(action: VizuAction): void;
29
+
30
+ export { VizuProvider, type VizuProviderProps, useComments, useVizu, useVizuAction, useVizuEvent, useVizuUser };
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ // src/index.tsx
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useSyncExternalStore
11
+ } from "react";
12
+ import {
13
+ Vizu
14
+ } from "@unhingged/vizu-core";
15
+ import { jsx } from "react/jsx-runtime";
16
+ var VizuContext = createContext(null);
17
+ function VizuProvider({ options, children }) {
18
+ const ref = useRef(null);
19
+ if (ref.current === null) {
20
+ ref.current = new Vizu(options ?? {});
21
+ }
22
+ const vizu = ref.current;
23
+ useEffect(() => {
24
+ return () => {
25
+ vizu.destroy();
26
+ ref.current = null;
27
+ };
28
+ }, []);
29
+ return /* @__PURE__ */ jsx(VizuContext.Provider, { value: vizu, children });
30
+ }
31
+ function useVizu() {
32
+ const v = useContext(VizuContext);
33
+ if (!v) {
34
+ throw new Error("useVizu must be called inside a <VizuProvider>.");
35
+ }
36
+ return v;
37
+ }
38
+ function useVizuEvent(event, handler) {
39
+ const vizu = useVizu();
40
+ const handlerRef = useRef(handler);
41
+ handlerRef.current = handler;
42
+ useEffect(() => {
43
+ const off = vizu.on(event, (payload) => handlerRef.current(payload));
44
+ return off;
45
+ }, [vizu, event]);
46
+ }
47
+ function useComments() {
48
+ const vizu = useVizu();
49
+ const subscribe = useMemo(
50
+ () => (cb) => {
51
+ const offs = [
52
+ vizu.on("comment:added", cb),
53
+ vizu.on("comment:removed", cb),
54
+ vizu.on("comments:cleared", cb),
55
+ vizu.on("comments:set", cb),
56
+ vizu.on("comments:loaded", cb)
57
+ ];
58
+ return () => offs.forEach((off) => off());
59
+ },
60
+ [vizu]
61
+ );
62
+ const cacheRef = useRef(vizu.getComments());
63
+ const getSnapshot = () => {
64
+ const next = vizu.getComments();
65
+ const last = cacheRef.current;
66
+ if (last.length === next.length && last.every((c, i) => c === next[i] || c.id === next[i].id && c.text === next[i].text)) {
67
+ return last;
68
+ }
69
+ cacheRef.current = next;
70
+ return next;
71
+ };
72
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
73
+ }
74
+ function useVizuUser() {
75
+ const vizu = useVizu();
76
+ const subscribe = useMemo(
77
+ () => (cb) => vizu.on("user:changed", cb),
78
+ [vizu]
79
+ );
80
+ const getSnapshot = () => vizu.getUser();
81
+ const user = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
82
+ return [user, (u) => vizu.setUser(u)];
83
+ }
84
+ function useVizuAction(action) {
85
+ const vizu = useVizu();
86
+ const actionRef = useRef(action);
87
+ actionRef.current = action;
88
+ useEffect(() => {
89
+ vizu.addAction({
90
+ ...action,
91
+ onClick: (ctx) => actionRef.current.onClick(ctx),
92
+ visibleWhen: action.visibleWhen ? ({ commentsCount }) => actionRef.current.visibleWhen?.({ commentsCount }) ?? true : void 0
93
+ });
94
+ return () => vizu.removeAction(action.id);
95
+ }, [vizu, action.id]);
96
+ }
97
+ export {
98
+ VizuProvider,
99
+ useComments,
100
+ useVizu,
101
+ useVizuAction,
102
+ useVizuEvent,
103
+ useVizuUser
104
+ };
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport {\n Vizu,\n type VizuOptions,\n type VizuComment,\n type VizuUser,\n type VizuAction,\n type VizuEventName,\n type VizuEventHandler,\n} from '@unhingged/vizu-core';\n\nexport type {\n Vizu,\n VizuOptions,\n VizuComment,\n VizuUser,\n VizuAction,\n VizuEventName,\n VizuEventHandler,\n};\n\nconst VizuContext = createContext<Vizu | null>(null);\n\nexport interface VizuProviderProps {\n options?: VizuOptions;\n children?: ReactNode;\n}\n\n/**\n * Mounts a single Vizu instance for the subtree. The instance is created lazily\n * once and persists across re-renders. Pass options once at construction;\n * imperative changes go through `useVizu()`.\n */\nexport function VizuProvider({ options, children }: VizuProviderProps) {\n // We need a stable instance across renders. Use a ref so re-renders don't construct new ones.\n const ref = useRef<Vizu | null>(null);\n if (ref.current === null) {\n ref.current = new Vizu(options ?? {});\n }\n const vizu = ref.current;\n\n useEffect(() => {\n return () => {\n vizu.destroy();\n ref.current = null;\n };\n // Intentionally only run on unmount. Recreating Vizu mid-tree would lose listeners + state.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return <VizuContext.Provider value={vizu}>{children}</VizuContext.Provider>;\n}\n\n/** Returns the Vizu instance. Throws if used outside a <VizuProvider>. */\nexport function useVizu(): Vizu {\n const v = useContext(VizuContext);\n if (!v) {\n throw new Error('useVizu must be called inside a <VizuProvider>.');\n }\n return v;\n}\n\n/** Subscribe to a Vizu event for the component's lifetime. Fire-and-forget. */\nexport function useVizuEvent<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void {\n const vizu = useVizu();\n // Stable handler ref — re-subscribe only when the event name changes.\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => {\n const off = vizu.on(event, (payload) => handlerRef.current(payload));\n return off;\n }, [vizu, event]);\n}\n\n/** Returns the current comments array and re-renders when it changes. */\nexport function useComments(): VizuComment[] {\n const vizu = useVizu();\n // Stable subscribe + snapshot for useSyncExternalStore.\n const subscribe = useMemo(\n () => (cb: () => void) => {\n const offs = [\n vizu.on('comment:added', cb),\n vizu.on('comment:removed', cb),\n vizu.on('comments:cleared', cb),\n vizu.on('comments:set', cb),\n vizu.on('comments:loaded', cb),\n ];\n return () => offs.forEach((off) => off());\n },\n [vizu],\n );\n // getSnapshot must return a referentially stable result when the data hasn't changed.\n // We re-derive only when the subscribe fires; meanwhile memoize the last array.\n const cacheRef = useRef<VizuComment[]>(vizu.getComments());\n const getSnapshot = () => {\n const next = vizu.getComments();\n const last = cacheRef.current;\n if (last.length === next.length && last.every((c, i) => c === next[i] || (c.id === next[i].id && c.text === next[i].text))) {\n return last;\n }\n cacheRef.current = next;\n return next;\n };\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/** Returns the current user and re-renders when it changes. Also exposes a setter. */\nexport function useVizuUser(): [VizuUser | null, (u: VizuUser | null) => void] {\n const vizu = useVizu();\n const subscribe = useMemo(\n () => (cb: () => void) => vizu.on('user:changed', cb),\n [vizu],\n );\n const getSnapshot = () => vizu.getUser();\n const user = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n return [user, (u) => vizu.setUser(u)];\n}\n\n/**\n * Register an action while the component is mounted. The action is removed\n * automatically on unmount. Re-renders DON'T re-register unless `action.id` changes.\n */\nexport function useVizuAction(action: VizuAction): void {\n const vizu = useVizu();\n const actionRef = useRef(action);\n actionRef.current = action;\n useEffect(() => {\n // Wrap so we always call the latest action.onClick / visibleWhen\n vizu.addAction({\n ...action,\n onClick: (ctx) => actionRef.current.onClick(ctx),\n visibleWhen: action.visibleWhen\n ? ({ commentsCount }) => actionRef.current.visibleWhen?.({ commentsCount }) ?? true\n : undefined,\n });\n return () => vizu.removeAction(action.id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [vizu, action.id]);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@unhingged/vizu-react",
3
+ "version": "0.1.0",
4
+ "description": "React bindings for @unhingged/vizu-core — VizuProvider, useVizu, useComments, useVizuUser, useVizuEvent.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch"
27
+ },
28
+ "peerDependencies": {
29
+ "react": ">=18"
30
+ },
31
+ "dependencies": {
32
+ "@unhingged/vizu-core": "workspace:*"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^18.3.0",
36
+ "react": "^18.3.1",
37
+ "tsup": "^8.3.5",
38
+ "typescript": "^5.6.3"
39
+ },
40
+ "keywords": [
41
+ "vizu",
42
+ "react",
43
+ "annotation",
44
+ "comments",
45
+ "feedback"
46
+ ],
47
+ "license": "MIT",
48
+ "author": "Zoltán Balogh",
49
+ "homepage": "https://github.com/unhingedshoobydoo/vizu/tree/main/packages/vizu-react",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/unhingedshoobydoo/vizu.git",
53
+ "directory": "packages/vizu-react"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/unhingedshoobydoo/vizu/issues"
57
+ }
58
+ }