@unhingged/vizu-core 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 +428 -0
- package/dist/auto.cjs +5005 -0
- package/dist/auto.cjs.map +1 -0
- package/dist/auto.js +3626 -0
- package/dist/auto.js.map +1 -0
- package/dist/chunk-OMIFOSQ2.js +295 -0
- package/dist/chunk-OMIFOSQ2.js.map +1 -0
- package/dist/index.cjs +4989 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +736 -0
- package/dist/index.d.ts +736 -0
- package/dist/index.js +3600 -0
- package/dist/index.js.map +1 -0
- package/dist/live-collab-IRUNFAPE.js +1033 -0
- package/dist/live-collab-IRUNFAPE.js.map +1 -0
- package/dist/vizu.min.js +1281 -0
- package/dist/vizu.min.js.map +1 -0
- package/package.json +60 -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,428 @@
|
|
|
1
|
+
# @vizu/core
|
|
2
|
+
|
|
3
|
+
A drop-in DOM commenting tool. Press a shortcut on any page → highlight any element → leave a comment → the host application handles persistence and downstream actions via events.
|
|
4
|
+
|
|
5
|
+
**Vizu is a UI for collecting annotations, not a storage system or an LLM-prompt builder.** It emits events; you decide what to do with them.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vizu/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install via `<script>`
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script
|
|
15
|
+
src="https://unpkg.com/@vizu/core/dist/vizu.min.js"
|
|
16
|
+
data-shortcut="mod+shift+e"
|
|
17
|
+
data-namespace="my-site"
|
|
18
|
+
data-version="v1"
|
|
19
|
+
data-start-enabled="true"
|
|
20
|
+
data-user-name="Jane Doe"
|
|
21
|
+
data-user-avatar="https://example.com/jane.jpg">
|
|
22
|
+
</script>
|
|
23
|
+
<script>
|
|
24
|
+
// Vizu is at window.__vizu — register your actions here
|
|
25
|
+
window.__vizu.addAction({
|
|
26
|
+
id: 'copy',
|
|
27
|
+
label: 'Copy',
|
|
28
|
+
variant: 'primary',
|
|
29
|
+
onClick: (ctx) => ctx.copyToClipboard(JSON.stringify(ctx.comments, null, 2)),
|
|
30
|
+
});
|
|
31
|
+
window.__vizu.on('comment:added', ({ comment }) => {
|
|
32
|
+
fetch('/api/comments', { method: 'POST', body: JSON.stringify(comment) });
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Defaults in `<script>` mode: `data-storage="local"` (localStorage), no actions registered.
|
|
38
|
+
|
|
39
|
+
## What it does — and what it doesn't
|
|
40
|
+
|
|
41
|
+
| Vizu does | Vizu does NOT |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Renders the highlight, marker, popover, sidebar, pill | Persist comments anywhere by default (you wire that) |
|
|
44
|
+
| Captures DOM-anchored comments with fingerprints that survive edits | Format prompts / payloads (you build whatever shape you want) |
|
|
45
|
+
| Emits typed events for everything | Send anything to a backend |
|
|
46
|
+
| Exposes a `setUser` / `setComments` API for hydration | Manage auth or sessions |
|
|
47
|
+
| Lets you register pill actions | Ship "Copy as prompt" / "JSON" / "Clear" buttons |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Integration
|
|
52
|
+
|
|
53
|
+
### Plain JS / Vanilla
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { Vizu } from '@vizu/core';
|
|
57
|
+
|
|
58
|
+
const vizu = new Vizu({
|
|
59
|
+
namespace: 'my-site',
|
|
60
|
+
pageVersion: 'home-v1',
|
|
61
|
+
shortcut: 'mod+shift+e',
|
|
62
|
+
user: { id: '123', name: 'Jane Doe', avatarUrl: '/jane.jpg' },
|
|
63
|
+
// storage defaults to 'memory' in the programmatic API — host owns persistence
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 1. Listen for new comments → POST to your backend
|
|
67
|
+
vizu.on('comment:added', async ({ comment }) => {
|
|
68
|
+
await fetch('/api/comments', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify(comment),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vizu.on('comment:removed', async ({ id }) => {
|
|
76
|
+
await fetch(`/api/comments/${id}`, { method: 'DELETE' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 2. Hydrate from your backend on load
|
|
80
|
+
const existing = await fetch('/api/comments?ns=my-site').then((r) => r.json());
|
|
81
|
+
await vizu.setComments(existing, { persist: false });
|
|
82
|
+
|
|
83
|
+
// 3. Register a pill action that builds your own prompt format
|
|
84
|
+
vizu.addAction({
|
|
85
|
+
id: 'send-to-llm',
|
|
86
|
+
label: 'Send to LLM',
|
|
87
|
+
variant: 'primary',
|
|
88
|
+
onClick: async (ctx) => {
|
|
89
|
+
const res = await fetch('/api/iterate', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
comments: ctx.comments,
|
|
93
|
+
pageHtml: ctx.pageHtml,
|
|
94
|
+
version: ctx.pageVersion,
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
const { newHtml } = await res.json();
|
|
98
|
+
document.documentElement.innerHTML = newHtml;
|
|
99
|
+
ctx.toast('Page iterated');
|
|
100
|
+
},
|
|
101
|
+
visibleWhen: ({ commentsCount }) => commentsCount > 0,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
vizu.enable();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### React (via [@vizu/react](../vizu-react))
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
'use client';
|
|
111
|
+
import {
|
|
112
|
+
VizuProvider, useVizu, useComments, useVizuUser,
|
|
113
|
+
useVizuEvent, useVizuAction,
|
|
114
|
+
} from '@vizu/react';
|
|
115
|
+
|
|
116
|
+
function App() {
|
|
117
|
+
return (
|
|
118
|
+
<VizuProvider options={{ namespace: 'my-app', startEnabled: true }}>
|
|
119
|
+
<Annotations />
|
|
120
|
+
<YourPage />
|
|
121
|
+
</VizuProvider>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Annotations() {
|
|
126
|
+
const [, setUser] = useVizuUser();
|
|
127
|
+
const session = useSession();
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (session) setUser({ id: session.userId, name: session.name, avatarUrl: session.avatar });
|
|
130
|
+
}, [session, setUser]);
|
|
131
|
+
|
|
132
|
+
useVizuEvent('comment:added', ({ comment }) =>
|
|
133
|
+
fetch('/api/comments', { method: 'POST', body: JSON.stringify(comment) }),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
useVizuAction({
|
|
137
|
+
id: 'send',
|
|
138
|
+
label: 'Send to backend',
|
|
139
|
+
variant: 'primary',
|
|
140
|
+
onClick: (ctx) => fetch('/api/iterate', { method: 'POST', body: JSON.stringify(ctx) }),
|
|
141
|
+
visibleWhen: ({ commentsCount }) => commentsCount > 0,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const comments = useComments();
|
|
145
|
+
return <CommentsBadge count={comments.length} />;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Vue 3
|
|
150
|
+
|
|
151
|
+
A thin composable is the cleanest shape. Until `@vizu/vue` ships, write your own:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// composables/useVizu.ts
|
|
155
|
+
import { onMounted, onUnmounted, ref, type Ref } from 'vue';
|
|
156
|
+
import { Vizu, type VizuOptions, type VizuComment } from '@vizu/core';
|
|
157
|
+
|
|
158
|
+
export function useVizu(options: VizuOptions) {
|
|
159
|
+
const vizu = new Vizu(options);
|
|
160
|
+
const comments: Ref<VizuComment[]> = ref([]);
|
|
161
|
+
|
|
162
|
+
const refresh = () => { comments.value = vizu.getComments(); };
|
|
163
|
+
const offs: Array<() => void> = [];
|
|
164
|
+
|
|
165
|
+
onMounted(() => {
|
|
166
|
+
vizu.enable();
|
|
167
|
+
offs.push(
|
|
168
|
+
vizu.on('comment:added', refresh),
|
|
169
|
+
vizu.on('comment:removed', refresh),
|
|
170
|
+
vizu.on('comments:cleared', refresh),
|
|
171
|
+
vizu.on('comments:set', refresh),
|
|
172
|
+
vizu.on('comments:loaded', refresh),
|
|
173
|
+
);
|
|
174
|
+
refresh();
|
|
175
|
+
});
|
|
176
|
+
onUnmounted(() => {
|
|
177
|
+
offs.forEach((o) => o());
|
|
178
|
+
vizu.destroy();
|
|
179
|
+
});
|
|
180
|
+
return { vizu, comments };
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use in a component:
|
|
185
|
+
```vue
|
|
186
|
+
<script setup lang="ts">
|
|
187
|
+
import { useVizu } from './composables/useVizu';
|
|
188
|
+
const { vizu, comments } = useVizu({ namespace: 'my-vue-app', startEnabled: true });
|
|
189
|
+
vizu.on('comment:added', ({ comment }) => fetch('/api/comments', { method: 'POST', body: JSON.stringify(comment) }));
|
|
190
|
+
</script>
|
|
191
|
+
<template>
|
|
192
|
+
<div>{{ comments.length }} comments</div>
|
|
193
|
+
</template>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Angular
|
|
197
|
+
|
|
198
|
+
Provide Vizu via a service so identity + listeners are global:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// vizu.service.ts
|
|
202
|
+
import { Injectable, NgZone, OnDestroy } from '@angular/core';
|
|
203
|
+
import { Vizu, type VizuOptions } from '@vizu/core';
|
|
204
|
+
|
|
205
|
+
@Injectable({ providedIn: 'root' })
|
|
206
|
+
export class VizuService implements OnDestroy {
|
|
207
|
+
readonly vizu: Vizu;
|
|
208
|
+
private offs: Array<() => void> = [];
|
|
209
|
+
|
|
210
|
+
constructor(private zone: NgZone) {
|
|
211
|
+
// Construct outside the zone so Vizu's own DOM listeners don't trigger CD on every mouse move
|
|
212
|
+
this.vizu = this.zone.runOutsideAngular(() => new Vizu({
|
|
213
|
+
namespace: 'my-ng-app',
|
|
214
|
+
startEnabled: true,
|
|
215
|
+
} satisfies VizuOptions));
|
|
216
|
+
|
|
217
|
+
// Pipe events back into the zone if your handlers update Angular state
|
|
218
|
+
this.offs.push(this.vizu.on('comment:added', (payload) =>
|
|
219
|
+
this.zone.run(() => fetch('/api/comments', { method: 'POST', body: JSON.stringify(payload.comment) })),
|
|
220
|
+
));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setUser(name: string, avatarUrl?: string) {
|
|
224
|
+
this.vizu.setUser({ name, avatarUrl });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
ngOnDestroy() {
|
|
228
|
+
this.offs.forEach((o) => o());
|
|
229
|
+
this.vizu.destroy();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Use it in a component:
|
|
235
|
+
```ts
|
|
236
|
+
@Component({...})
|
|
237
|
+
export class AppComponent {
|
|
238
|
+
constructor(private vizuService: VizuService) {
|
|
239
|
+
this.vizuService.vizu.addAction({
|
|
240
|
+
id: 'send',
|
|
241
|
+
label: 'Send to API',
|
|
242
|
+
variant: 'primary',
|
|
243
|
+
onClick: (ctx) => fetch('/api/iterate', { method: 'POST', body: JSON.stringify(ctx) }),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Other frameworks
|
|
250
|
+
|
|
251
|
+
Anything with a DOM works. The package is framework-agnostic — `new Vizu(...)`, subscribe to events, call `setUser`/`setComments`/`addAction`. Lifecycle hook → `vizu.destroy()` on teardown.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Events
|
|
256
|
+
|
|
257
|
+
Fire-and-forget. Handlers throw silently (errors are logged but don't break Vizu's state). Every payload is an object so the shape stays forward-compatible.
|
|
258
|
+
|
|
259
|
+
| Event | Payload | When it fires |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `enabled` | `{}` | After `vizu.enable()` |
|
|
262
|
+
| `disabled` | `{}` | After `vizu.disable()` |
|
|
263
|
+
| `mounted` | `{}` | After the UI is in the DOM |
|
|
264
|
+
| `unmounted` | `{}` | After the UI is removed |
|
|
265
|
+
| `comment:added` | `{ comment: VizuComment }` | User saved a new comment |
|
|
266
|
+
| `comment:removed` | `{ id: string, comment: VizuComment }` | User clicked delete |
|
|
267
|
+
| `comments:cleared` | `{ previous: VizuComment[] }` | `clearAll()` resolved |
|
|
268
|
+
| `comments:set` | `{ comments: VizuComment[] }` | `setComments(...)` resolved (hydration) |
|
|
269
|
+
| `comments:loaded` | `{ comments: VizuComment[] }` | Storage adapter finished initial `load()` |
|
|
270
|
+
| `element:selected` | `{ target: Element, fingerprint: ElementFingerprint }` | User clicked a commentable element |
|
|
271
|
+
| `element:deselected` | `{}` | Popover closed |
|
|
272
|
+
| `sidebar:opened` | `{}` | Sidebar drawer opened |
|
|
273
|
+
| `sidebar:closed` | `{}` | Sidebar drawer closed |
|
|
274
|
+
| `action:invoked` | `{ id: string }` | Pill action clicked (before its `onClick` runs) |
|
|
275
|
+
| `user:changed` | `{ user: VizuUser \| null }` | `setUser(...)` called |
|
|
276
|
+
|
|
277
|
+
### Subscription API
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const off = vizu.on('comment:added', ({ comment }) => { /* ... */ });
|
|
281
|
+
off(); // unsubscribe
|
|
282
|
+
vizu.off('comment:added', handler); // alternative
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`on()` returns an unsubscribe function. Use it in cleanup paths (React `useEffect` return, Vue `onUnmounted`, Angular `ngOnDestroy`).
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Supplying current user data
|
|
290
|
+
|
|
291
|
+
Three ways:
|
|
292
|
+
|
|
293
|
+
**1. At construction**
|
|
294
|
+
```ts
|
|
295
|
+
new Vizu({ user: { id: '123', name: 'Jane Doe', avatarUrl: '/jane.jpg' } });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**2. Imperatively (re-renders the pill chip + popover author line)**
|
|
299
|
+
```ts
|
|
300
|
+
vizu.setUser({ id: '123', name: 'Jane Doe', avatarUrl: '/jane.jpg' });
|
|
301
|
+
// or clear
|
|
302
|
+
vizu.setUser(null);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**3. Script-tag data attributes**
|
|
306
|
+
```html
|
|
307
|
+
<script src="vizu.min.js" data-user-name="Jane" data-user-id="123" data-user-avatar="/jane.jpg"></script>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The current user is captured into every new comment as `comment.author` — that snapshot is preserved even if the user logs out or changes name later.
|
|
311
|
+
|
|
312
|
+
`VizuUser` shape:
|
|
313
|
+
```ts
|
|
314
|
+
{
|
|
315
|
+
id?: string;
|
|
316
|
+
name: string;
|
|
317
|
+
avatarUrl?: string;
|
|
318
|
+
email?: string;
|
|
319
|
+
meta?: Record<string, unknown>; // free-form host extras (team, role, etc.)
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Loading existing comments
|
|
326
|
+
|
|
327
|
+
Vizu doesn't ship a server adapter — bring your own. Two patterns:
|
|
328
|
+
|
|
329
|
+
### Listen + push (recommended for live apps)
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
// On load, hydrate from your API
|
|
333
|
+
const initial = await fetch('/api/comments?ns=my-site').then(r => r.json());
|
|
334
|
+
await vizu.setComments(initial, { persist: false });
|
|
335
|
+
|
|
336
|
+
// Then mirror every change to the backend
|
|
337
|
+
vizu.on('comment:added', ({ comment }) => fetch('/api/comments', { method: 'POST', body: JSON.stringify(comment) }));
|
|
338
|
+
vizu.on('comment:removed', ({ id }) => fetch(`/api/comments/${id}`, { method: 'DELETE' }));
|
|
339
|
+
vizu.on('comments:cleared', () => fetch('/api/comments?ns=my-site', { method: 'DELETE' }));
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Custom `StorageAdapter`
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { Vizu, type StorageAdapter, type VizuComment } from '@vizu/core';
|
|
346
|
+
|
|
347
|
+
const remote: StorageAdapter = {
|
|
348
|
+
async load(ns) { const r = await fetch(`/api/comments?ns=${ns}`); return r.json(); },
|
|
349
|
+
async save(ns, c) { await fetch(`/api/comments?ns=${ns}`, { method: 'PUT', body: JSON.stringify(c) }); },
|
|
350
|
+
async clear(ns) { await fetch(`/api/comments?ns=${ns}`, { method: 'DELETE' }); },
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
new Vizu({ storage: remote, namespace: 'my-site' });
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The adapter is called transparently on add/remove/clear and once at startup. Mix and match: built-in `'local'` adapter for fast iteration, custom one for production.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Storage modes
|
|
361
|
+
|
|
362
|
+
| `options.storage` | Behavior | Default in |
|
|
363
|
+
|---|---|---|
|
|
364
|
+
| `'local'` | Persist to `localStorage` | Script-tag mode |
|
|
365
|
+
| `'memory'` | RAM-only — wipes on reload | Programmatic API (`new Vizu()`) |
|
|
366
|
+
| `'none'` | No-op storage; host owns hydration via events + `setComments` | (opt-in) |
|
|
367
|
+
| `StorageAdapter` | Your own load/save/clear | (opt-in) |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## API quick reference
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
// Lifecycle
|
|
375
|
+
vizu.enable(); vizu.disable(); vizu.toggle(); vizu.isEnabled();
|
|
376
|
+
vizu.destroy(); // full teardown + clear all listeners
|
|
377
|
+
|
|
378
|
+
// Comments
|
|
379
|
+
vizu.getComments(): VizuComment[];
|
|
380
|
+
vizu.setComments(comments, { persist?: boolean }): Promise<void>;
|
|
381
|
+
vizu.clearAll(): Promise<void>;
|
|
382
|
+
|
|
383
|
+
// User
|
|
384
|
+
vizu.setUser(user | null);
|
|
385
|
+
vizu.getUser(): VizuUser | null;
|
|
386
|
+
|
|
387
|
+
// Actions
|
|
388
|
+
vizu.addAction({ id, label, onClick, variant?, title?, visibleWhen? });
|
|
389
|
+
vizu.removeAction(id);
|
|
390
|
+
vizu.invokeAction(id); // programmatically trigger
|
|
391
|
+
vizu.getActions(): VizuAction[];
|
|
392
|
+
|
|
393
|
+
// Events
|
|
394
|
+
vizu.on(event, handler) → off();
|
|
395
|
+
vizu.off(event, handler);
|
|
396
|
+
|
|
397
|
+
// Helpers
|
|
398
|
+
vizu.snapshotHtml(): string; // page HTML with Vizu's own UI stripped (for sending to LLMs)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Element fingerprints
|
|
404
|
+
|
|
405
|
+
Each comment carries an `ElementFingerprint` that survives edits:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
{
|
|
409
|
+
selector: string; // CSS selector path
|
|
410
|
+
parentSelector: string;
|
|
411
|
+
tagName: string;
|
|
412
|
+
textSnippet: string; // first 80 chars of innerText
|
|
413
|
+
siblingIndex: number;
|
|
414
|
+
attributes: {
|
|
415
|
+
id?: string;
|
|
416
|
+
classList?: string[];
|
|
417
|
+
role?: string;
|
|
418
|
+
ariaLabel?: string;
|
|
419
|
+
dataKey?: string; // from data-vizu-key="…" — explicit pin
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Re-anchoring tries: `data-vizu-key` → `id` → full selector → parent + tag + sibling-index → fuzzy text match. Add `data-vizu-key="something-stable"` to elements you want pinned permanently — it wins all matching.
|
|
425
|
+
|
|
426
|
+
## License
|
|
427
|
+
|
|
428
|
+
MIT
|