@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 +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +125 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|