@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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single rung of the ancestor chain: the element's tag and its
|
|
3
|
+
* `nth-of-type` index inside its parent (1-indexed). Captured root-near
|
|
4
|
+
* → closer-to-target so the matcher can shift-search through wrapper
|
|
5
|
+
* additions.
|
|
6
|
+
*/
|
|
7
|
+
interface AncestorStep {
|
|
8
|
+
tag: string;
|
|
9
|
+
nthOfType: number;
|
|
10
|
+
}
|
|
11
|
+
interface ElementFingerprint {
|
|
12
|
+
selector: string;
|
|
13
|
+
parentSelector: string;
|
|
14
|
+
tagName: string;
|
|
15
|
+
textSnippet: string;
|
|
16
|
+
siblingIndex: number;
|
|
17
|
+
attributes: {
|
|
18
|
+
id?: string;
|
|
19
|
+
classList?: string[];
|
|
20
|
+
role?: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
dataKey?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 3-step ancestor chain. Added in algorithm v2 (Phase 11.4.2). Order:
|
|
26
|
+
* immediate parent first, then grandparent, then great-grandparent.
|
|
27
|
+
* Legacy fingerprints written before this field lack it; the matcher
|
|
28
|
+
* falls back to the parent+sibling-index+tag heuristic for those.
|
|
29
|
+
*/
|
|
30
|
+
ancestorChain?: {
|
|
31
|
+
steps: AncestorStep[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Algorithm version that produced this fingerprint. Absent (or `1`) on
|
|
35
|
+
* legacy captures; new captures stamp `2` (Phase 11.4 hardened matcher
|
|
36
|
+
* — class-signature rung, multi-ancestor chain, Levenshtein text scan).
|
|
37
|
+
* The matcher routes on the presence of {@link ancestorChain} today, so
|
|
38
|
+
* this field is informational; it gives us a stable handle to migrate
|
|
39
|
+
* data when v3 ships without re-introspecting fingerprint shape.
|
|
40
|
+
*/
|
|
41
|
+
algorithmVersion?: 1 | 2;
|
|
42
|
+
}
|
|
43
|
+
interface VizuUser {
|
|
44
|
+
id?: string;
|
|
45
|
+
name: string;
|
|
46
|
+
avatarUrl?: string;
|
|
47
|
+
email?: string;
|
|
48
|
+
/** Free-form host-defined extras (team, role, etc.) preserved on each comment. */
|
|
49
|
+
meta?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** Current schema version. Bump + add a migration when the shape changes. */
|
|
52
|
+
declare const SCHEMA_VERSION: 2;
|
|
53
|
+
type SchemaVersion = typeof SCHEMA_VERSION;
|
|
54
|
+
/**
|
|
55
|
+
* Triage state. Cloud workspaces use this for filtering;
|
|
56
|
+
* the local sidebar treats `resolved` / `wontfix` as "dim".
|
|
57
|
+
*/
|
|
58
|
+
type CommentStatus = 'open' | 'resolved' | 'wontfix';
|
|
59
|
+
/**
|
|
60
|
+
* Outcome of resolving a comment's fingerprint against the current DOM.
|
|
61
|
+
*
|
|
62
|
+
* - `exact` : `data-vizu-key` or `id` matched. We trust this 100%.
|
|
63
|
+
* - `likely` : The full CSS selector path matched. Probably the right
|
|
64
|
+
* element but not guaranteed if the page has dynamic
|
|
65
|
+
* duplicates.
|
|
66
|
+
* - `drifted` : Only the structural fallback (parent + sibling-index)
|
|
67
|
+
* or the page-wide text snippet matched. The page has
|
|
68
|
+
* changed since the comment was written. Render with a
|
|
69
|
+
* visible warning so visitors don't act on a stale match.
|
|
70
|
+
* - `orphaned` : Nothing matched. The element is gone. Comment is shown
|
|
71
|
+
* in a separate sidebar section.
|
|
72
|
+
*
|
|
73
|
+
* Returned by `findByFingerprint`; emitted via the `anchor:resolved` event.
|
|
74
|
+
*/
|
|
75
|
+
type MatchConfidence = 'exact' | 'likely' | 'drifted' | 'orphaned';
|
|
76
|
+
/** Result of a fingerprint resolution. `element` is null iff `confidence === 'orphaned'`. */
|
|
77
|
+
interface FingerprintMatch {
|
|
78
|
+
element: Element | null;
|
|
79
|
+
confidence: MatchConfidence;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Embedded reply in a comment thread. Threading UI ships in v1.1 — the
|
|
83
|
+
* shape is stamped in v2 so writes are already future-correct.
|
|
84
|
+
*/
|
|
85
|
+
interface Reply {
|
|
86
|
+
id: string;
|
|
87
|
+
text: string;
|
|
88
|
+
createdAt: number;
|
|
89
|
+
author?: VizuUser;
|
|
90
|
+
/** Identity provider's user id (Clerk, etc.) when known. */
|
|
91
|
+
authorId?: string;
|
|
92
|
+
/** Clerk user ids mentioned in this reply. */
|
|
93
|
+
mentions?: string[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* File attachment metadata. Storage backend (Vercel Blob, S3, …) is
|
|
97
|
+
* decided by the host / cloud adapter; the comment only carries the
|
|
98
|
+
* resolved URL.
|
|
99
|
+
*/
|
|
100
|
+
interface Attachment {
|
|
101
|
+
id: string;
|
|
102
|
+
url: string;
|
|
103
|
+
mimeType: string;
|
|
104
|
+
sizeBytes: number;
|
|
105
|
+
uploadedAt: number;
|
|
106
|
+
/** Original filename the user uploaded. */
|
|
107
|
+
filename?: string;
|
|
108
|
+
}
|
|
109
|
+
interface VizuComment {
|
|
110
|
+
id: string;
|
|
111
|
+
/** Schema version. Set to {@link SCHEMA_VERSION} on every new write. */
|
|
112
|
+
schemaVersion: SchemaVersion;
|
|
113
|
+
/**
|
|
114
|
+
* One or more elements this comment is anchored to. Length 1 = single-anchor
|
|
115
|
+
* (the common case); >1 = "grouped" comment that applies to multiple
|
|
116
|
+
* elements together (e.g., "align these three CTAs to the same baseline").
|
|
117
|
+
*
|
|
118
|
+
* Always has length ≥ 1. Legacy v1 comments (with a singular `fingerprint`
|
|
119
|
+
* field) are auto-migrated to `[fingerprint]` on load — see `migrateComment`.
|
|
120
|
+
*/
|
|
121
|
+
fingerprints: ElementFingerprint[];
|
|
122
|
+
/** @deprecated v1 only — kept on the type so older payloads parse cleanly; migrated at load time. */
|
|
123
|
+
fingerprint?: ElementFingerprint;
|
|
124
|
+
text: string;
|
|
125
|
+
createdAt: number;
|
|
126
|
+
pageVersion?: string;
|
|
127
|
+
/** @deprecated use `pageUrl` — kept for backwards-compat read of v1 payloads. */
|
|
128
|
+
url?: string;
|
|
129
|
+
/** Page URL where the comment was anchored. Cloud reads filter on this. */
|
|
130
|
+
pageUrl?: string;
|
|
131
|
+
/** Captured from `Vizu.setUser` / `options.user` at the moment the comment was created. */
|
|
132
|
+
author?: VizuUser;
|
|
133
|
+
/** Identity provider's user id (Clerk, etc.) when known. Cloud writes set this from the session. */
|
|
134
|
+
authorId?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Element references the user inserted via `#` mentions. Keyed by short refId.
|
|
137
|
+
* The comment text contains tokens like `[#label](vizu-ref:refId)` that resolve
|
|
138
|
+
* to these fingerprints; clicking a rendered reference jumps to the element.
|
|
139
|
+
*/
|
|
140
|
+
references?: Record<string, ElementFingerprint>;
|
|
141
|
+
/** Triage state. Defaults to `'open'` on new comments. */
|
|
142
|
+
status: CommentStatus;
|
|
143
|
+
/** Threaded replies. UI ships in v1.1; the array is always present. */
|
|
144
|
+
replies: Reply[];
|
|
145
|
+
/** File attachments. UI ships in v1.2; the array is always present. */
|
|
146
|
+
attachments: Attachment[];
|
|
147
|
+
/** Clerk user ids mentioned in this comment. */
|
|
148
|
+
mentions: string[];
|
|
149
|
+
/**
|
|
150
|
+
* Timestamp (ms) of the last self-healing re-fingerprint pass. Set by
|
|
151
|
+
* the Vizu class when a `drifted` resolution lands on an existing
|
|
152
|
+
* element and the fingerprint is rewritten in place. Reads as null
|
|
153
|
+
* for comments that have never drifted. Surface in the dashboard if
|
|
154
|
+
* you want to flag "auto-healed" rows; ignore otherwise.
|
|
155
|
+
*/
|
|
156
|
+
fingerprintsRefreshedAt?: number | null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Storage event emitted by adapters that support push (cloud adapters).
|
|
160
|
+
* Local / memory adapters never emit these.
|
|
161
|
+
*/
|
|
162
|
+
type StorageEvent = {
|
|
163
|
+
type: 'added';
|
|
164
|
+
comment: VizuComment;
|
|
165
|
+
} | {
|
|
166
|
+
type: 'updated';
|
|
167
|
+
comment: VizuComment;
|
|
168
|
+
} | {
|
|
169
|
+
type: 'removed';
|
|
170
|
+
id: string;
|
|
171
|
+
} | {
|
|
172
|
+
type: 'cleared';
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Optional authorization context an adapter may surface to the host.
|
|
176
|
+
* Cloud adapters set this after the user signs in; the Vizu class
|
|
177
|
+
* forwards it via `vizu.getAuthContext()` for hosts that need to call
|
|
178
|
+
* the same backend with the same session.
|
|
179
|
+
*/
|
|
180
|
+
interface AuthContext {
|
|
181
|
+
/** Identity provider's user id (e.g. Clerk user id). */
|
|
182
|
+
userId: string;
|
|
183
|
+
/** Short-lived bearer token. Adapter is responsible for refresh. */
|
|
184
|
+
token: string;
|
|
185
|
+
/** ISO timestamp the token expires; informational. */
|
|
186
|
+
expiresAt?: string;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Storage contract v2. Per-comment operations replace v1's full-array
|
|
190
|
+
* `save(comments[])`. Cloud-friendly: each mutation is one network call,
|
|
191
|
+
* the adapter can do optimistic-concurrency / conflict resolution
|
|
192
|
+
* without re-shipping the whole list.
|
|
193
|
+
*
|
|
194
|
+
* Hosts that wrote against v1 (`load / save / clear`) are still accepted —
|
|
195
|
+
* see `wrapV1Adapter` in `storage.ts` for the back-compat shim.
|
|
196
|
+
*
|
|
197
|
+
* Reply ops (`addReply` / `removeReply`) are optional: adapters that don't
|
|
198
|
+
* implement them fall back to `updateComment` with a manually-patched
|
|
199
|
+
* `replies` array. Wrapping logic lives in the Vizu class, not here.
|
|
200
|
+
*/
|
|
201
|
+
interface StorageAdapter {
|
|
202
|
+
/** Load all comments for this namespace. Called once at boot, plus on `setComments`. */
|
|
203
|
+
load(namespace: string): Promise<VizuComment[]>;
|
|
204
|
+
/** Persist a new comment. */
|
|
205
|
+
addComment(namespace: string, comment: VizuComment): Promise<void>;
|
|
206
|
+
/** Patch an existing comment by id. Adapter returns the updated doc or `null` if not found. */
|
|
207
|
+
updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
208
|
+
/** Remove a comment by id. */
|
|
209
|
+
removeComment(namespace: string, id: string): Promise<void>;
|
|
210
|
+
/** Replace the entire list (used by hosts that hydrate from their own backend). */
|
|
211
|
+
setAll(namespace: string, comments: VizuComment[]): Promise<void>;
|
|
212
|
+
/** Wipe the namespace. */
|
|
213
|
+
clear(namespace: string): Promise<void>;
|
|
214
|
+
/** Append a reply to a comment. Adapter returns the parent or null if not found. */
|
|
215
|
+
addReply?(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
|
|
216
|
+
/** Remove a reply by id from a comment. Adapter returns the parent or null if not found. */
|
|
217
|
+
removeReply?(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
|
|
218
|
+
/**
|
|
219
|
+
* Upload a file attachment. Returns the resolved {@link Attachment} with
|
|
220
|
+
* a public URL the host can persist on a comment. Adapters that don't
|
|
221
|
+
* implement this surface report `vizu.canUploadAttachments() === false`
|
|
222
|
+
* and the popover hides the upload UI.
|
|
223
|
+
*/
|
|
224
|
+
uploadAttachment?(namespace: string, file: File): Promise<Attachment>;
|
|
225
|
+
/** Optional remote-change subscription. Returns an unsubscribe function. */
|
|
226
|
+
subscribe?(namespace: string, handler: (event: StorageEvent) => void): () => void;
|
|
227
|
+
/** Optional auth context (cloud adapters). */
|
|
228
|
+
getAuthContext?(): AuthContext | null;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Legacy v1 adapter shape. Kept exported so hosts can still type their
|
|
232
|
+
* existing adapters; the resolver wraps them automatically.
|
|
233
|
+
*/
|
|
234
|
+
interface StorageAdapterV1 {
|
|
235
|
+
load(namespace: string): Promise<VizuComment[]>;
|
|
236
|
+
save(namespace: string, comments: VizuComment[]): Promise<void>;
|
|
237
|
+
clear(namespace: string): Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
type StorageOption = 'local' | 'memory' | 'none' | StorageAdapter | StorageAdapterV1;
|
|
240
|
+
/** Context handed to every action's onClick. Has the data and the side-effect helpers. */
|
|
241
|
+
interface ActionContext {
|
|
242
|
+
comments: VizuComment[];
|
|
243
|
+
pageHtml: string;
|
|
244
|
+
pageUrl?: string;
|
|
245
|
+
pageVersion?: string;
|
|
246
|
+
user?: VizuUser;
|
|
247
|
+
timestamp: number;
|
|
248
|
+
/** Quick copy-to-clipboard helper. */
|
|
249
|
+
copyToClipboard: (text: string) => void;
|
|
250
|
+
/** Briefly show a toast above the pill. */
|
|
251
|
+
toast: (message: string) => void;
|
|
252
|
+
}
|
|
253
|
+
interface VizuAction {
|
|
254
|
+
id: string;
|
|
255
|
+
label: string;
|
|
256
|
+
/** Tooltip / aria-label */
|
|
257
|
+
title?: string;
|
|
258
|
+
variant?: 'primary' | 'default' | 'ghost';
|
|
259
|
+
/** Placement in the pill. Defaults to 'main'. */
|
|
260
|
+
position?: 'main';
|
|
261
|
+
onClick: (ctx: ActionContext) => void | Promise<void>;
|
|
262
|
+
/** Only render the action when this returns true. */
|
|
263
|
+
visibleWhen?: (ctx: {
|
|
264
|
+
commentsCount: number;
|
|
265
|
+
}) => boolean;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Cloud connection options. When set, the resolver constructs the built-in
|
|
269
|
+
* CloudStorageAdapter (Phase 3) and uses it instead of `storage`. If both
|
|
270
|
+
* `cloud` and `storage` are provided, `cloud` wins and a `console.warn`
|
|
271
|
+
* is emitted at construction time.
|
|
272
|
+
*/
|
|
273
|
+
interface VizuCloudOptions {
|
|
274
|
+
/** Workspace slug. Required. Created in the Vizu dashboard. */
|
|
275
|
+
workspace: string;
|
|
276
|
+
/** Override the API base URL. Defaults to `'https://vizu.unhingged.com'`. */
|
|
277
|
+
apiUrl?: string;
|
|
278
|
+
/**
|
|
279
|
+
* Auto-open the popup sign-in window when a write is attempted by an
|
|
280
|
+
* unauthenticated user. Defaults to `true`.
|
|
281
|
+
*/
|
|
282
|
+
autoSignIn?: boolean;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Live collaboration config. Both keys are needed — the public key
|
|
286
|
+
* identifies the Liveblocks project to the browser SDK; the auth
|
|
287
|
+
* endpoint mints scoped tokens that gate room access against the
|
|
288
|
+
* workspace's role + plan.
|
|
289
|
+
*
|
|
290
|
+
* Wired in Slice 1 of plans/live-collab.md; cursor + follow rendering
|
|
291
|
+
* arrives in Slice 2+.
|
|
292
|
+
*/
|
|
293
|
+
interface VizuLiveblocksOptions {
|
|
294
|
+
/** Liveblocks public key from the dashboard (pk_dev_… / pk_prod_…). */
|
|
295
|
+
publicKey: string;
|
|
296
|
+
/**
|
|
297
|
+
* URL of the auth endpoint that mints room tokens. Same-origin path
|
|
298
|
+
* (`/api/liveblocks-auth`) for dashboard usage; full URL for hosts
|
|
299
|
+
* embedding @vizu/core cross-origin.
|
|
300
|
+
*/
|
|
301
|
+
authEndpoint: string;
|
|
302
|
+
}
|
|
303
|
+
interface VizuOptions {
|
|
304
|
+
shortcut?: string;
|
|
305
|
+
namespace?: string;
|
|
306
|
+
pageVersion?: string;
|
|
307
|
+
/**
|
|
308
|
+
* Storage strategy. Default behavior:
|
|
309
|
+
* - **Programmatic** (`new Vizu()`): defaults to `'memory'`; host should listen to events to persist.
|
|
310
|
+
* - **Script-tag auto-init**: defaults to `'local'` for zero-config drop-in (override via `data-storage="memory"` or `data-storage="none"`).
|
|
311
|
+
*
|
|
312
|
+
* When `cloud` is set this option is ignored.
|
|
313
|
+
*/
|
|
314
|
+
storage?: StorageOption;
|
|
315
|
+
/** Cloud workspace connection. Overrides `storage`. See {@link VizuCloudOptions}. */
|
|
316
|
+
cloud?: VizuCloudOptions;
|
|
317
|
+
/**
|
|
318
|
+
* Optional live collaboration config. When set AND the workspace's
|
|
319
|
+
* plan carries the `live_collab` feature, @vizu/core opens a
|
|
320
|
+
* Liveblocks room scoped to (workspaceSlug, pageUrl) and renders
|
|
321
|
+
* presence + cursors + follow mode. Without this, all live-collab
|
|
322
|
+
* machinery stays inert — standalone usage is unaffected.
|
|
323
|
+
*
|
|
324
|
+
* See plans/live-collab.md.
|
|
325
|
+
*/
|
|
326
|
+
liveblocks?: VizuLiveblocksOptions;
|
|
327
|
+
ignoreSelectors?: string[];
|
|
328
|
+
accent?: string;
|
|
329
|
+
startEnabled?: boolean;
|
|
330
|
+
user?: VizuUser;
|
|
331
|
+
/** Actions to register on construction. You can also call `vizu.addAction(...)` later. */
|
|
332
|
+
actions?: VizuAction[];
|
|
333
|
+
/**
|
|
334
|
+
* Convenience callbacks. These are aliases for `.on('comment:added', ...)` etc.
|
|
335
|
+
* The event API is preferred for new code.
|
|
336
|
+
*/
|
|
337
|
+
onCommentAdded?: (c: VizuComment) => void;
|
|
338
|
+
onCommentRemoved?: (id: string) => void;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Typed event map. Every event payload is an object (never bare values) so the
|
|
342
|
+
* shape is forward-compatible.
|
|
343
|
+
*/
|
|
344
|
+
interface VizuEventMap {
|
|
345
|
+
'enabled': Record<string, never>;
|
|
346
|
+
'disabled': Record<string, never>;
|
|
347
|
+
'mounted': Record<string, never>;
|
|
348
|
+
'unmounted': Record<string, never>;
|
|
349
|
+
'comment:added': {
|
|
350
|
+
comment: VizuComment;
|
|
351
|
+
};
|
|
352
|
+
'comment:removed': {
|
|
353
|
+
id: string;
|
|
354
|
+
comment: VizuComment;
|
|
355
|
+
};
|
|
356
|
+
'comment:updated': {
|
|
357
|
+
comment: VizuComment;
|
|
358
|
+
};
|
|
359
|
+
'comments:cleared': {
|
|
360
|
+
previous: VizuComment[];
|
|
361
|
+
};
|
|
362
|
+
'comments:set': {
|
|
363
|
+
comments: VizuComment[];
|
|
364
|
+
};
|
|
365
|
+
'comments:loaded': {
|
|
366
|
+
comments: VizuComment[];
|
|
367
|
+
};
|
|
368
|
+
'element:selected': {
|
|
369
|
+
target: Element;
|
|
370
|
+
fingerprint: ElementFingerprint;
|
|
371
|
+
};
|
|
372
|
+
'element:deselected': Record<string, never>;
|
|
373
|
+
'sidebar:opened': Record<string, never>;
|
|
374
|
+
'sidebar:closed': Record<string, never>;
|
|
375
|
+
'action:invoked': {
|
|
376
|
+
id: string;
|
|
377
|
+
};
|
|
378
|
+
'user:changed': {
|
|
379
|
+
user: VizuUser | null;
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* Emitted once per comment when the lib resolves (or fails to resolve)
|
|
383
|
+
* its fingerprint against the current DOM. `confidence: 'orphaned'`
|
|
384
|
+
* means the comment is in the sidebar's orphaned section.
|
|
385
|
+
*/
|
|
386
|
+
'anchor:resolved': {
|
|
387
|
+
comment: VizuComment;
|
|
388
|
+
confidence: MatchConfidence;
|
|
389
|
+
element: Element | null;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
type VizuEventName = keyof VizuEventMap;
|
|
393
|
+
type VizuEventHandler<E extends VizuEventName> = (payload: VizuEventMap[E]) => void;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Persist comments to the host page's localStorage. One JSON document per
|
|
397
|
+
* namespace. Migrations are applied on read.
|
|
398
|
+
*
|
|
399
|
+
* v2 per-comment ops are emulated via read-modify-write of the underlying
|
|
400
|
+
* array — fine for localStorage's <1ms cost. Cloud adapters do real
|
|
401
|
+
* single-document writes.
|
|
402
|
+
*/
|
|
403
|
+
declare class LocalStorageAdapter implements StorageAdapter {
|
|
404
|
+
load(namespace: string): Promise<VizuComment[]>;
|
|
405
|
+
addComment(namespace: string, comment: VizuComment): Promise<void>;
|
|
406
|
+
updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
407
|
+
removeComment(namespace: string, id: string): Promise<void>;
|
|
408
|
+
setAll(namespace: string, comments: VizuComment[]): Promise<void>;
|
|
409
|
+
clear(namespace: string): Promise<void>;
|
|
410
|
+
addReply(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
|
|
411
|
+
removeReply(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Stores comments in RAM only — wipes on reload. Useful when the host owns
|
|
415
|
+
* persistence via events (the integrator listens to `comment:added` and
|
|
416
|
+
* forwards to its own backend).
|
|
417
|
+
*/
|
|
418
|
+
declare class InMemoryStorageAdapter implements StorageAdapter {
|
|
419
|
+
private store;
|
|
420
|
+
load(namespace: string): Promise<VizuComment[]>;
|
|
421
|
+
addComment(namespace: string, comment: VizuComment): Promise<void>;
|
|
422
|
+
updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
423
|
+
removeComment(namespace: string, id: string): Promise<void>;
|
|
424
|
+
setAll(namespace: string, comments: VizuComment[]): Promise<void>;
|
|
425
|
+
clear(namespace: string): Promise<void>;
|
|
426
|
+
addReply(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
|
|
427
|
+
removeReply(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* No-op adapter — never persists. The host hydrates via `setComments` and
|
|
431
|
+
* listens to events to drive its own storage.
|
|
432
|
+
*/
|
|
433
|
+
declare class NullStorageAdapter implements StorageAdapter {
|
|
434
|
+
load(): Promise<VizuComment[]>;
|
|
435
|
+
addComment(): Promise<void>;
|
|
436
|
+
updateComment(): Promise<VizuComment | null>;
|
|
437
|
+
removeComment(): Promise<void>;
|
|
438
|
+
setAll(): Promise<void>;
|
|
439
|
+
clear(): Promise<void>;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* CloudStorageAdapter — talks to the Vizu hosted backend.
|
|
444
|
+
*
|
|
445
|
+
* Lifecycle:
|
|
446
|
+
* - Reads are unauthenticated-attempted first (the dashboard's own
|
|
447
|
+
* reads are same-origin Clerk session). If the backend says 401, we
|
|
448
|
+
* open the popup once and retry.
|
|
449
|
+
* - Writes require a host-token. First write opens the popup; the
|
|
450
|
+
* resulting JWT is stored in sessionStorage keyed by workspace slug
|
|
451
|
+
* and reused for the tab's lifetime (or until `exp` lapses).
|
|
452
|
+
*
|
|
453
|
+
* Token storage = sessionStorage (per-tab). Cross-tab is intentional:
|
|
454
|
+
* a stolen token from one tab can't leak to another via storage events.
|
|
455
|
+
* The cost is one popup per tab; cheap for the safety win.
|
|
456
|
+
*
|
|
457
|
+
* The popup is hosted at `${apiUrl}/connect?workspace=&origin=`. It
|
|
458
|
+
* handles the Clerk sign-in handshake and posts the token back via
|
|
459
|
+
* `postMessage` targeted at the host origin (never `*`).
|
|
460
|
+
*/
|
|
461
|
+
interface CloudStorageOptions {
|
|
462
|
+
/** Workspace slug — required. */
|
|
463
|
+
workspace: string;
|
|
464
|
+
/** API base URL (defaults to https://vizu.unhingged.com). */
|
|
465
|
+
apiUrl?: string;
|
|
466
|
+
/** Open the sign-in popup automatically on the first auth-needed write. Defaults to true. */
|
|
467
|
+
autoSignIn?: boolean;
|
|
468
|
+
}
|
|
469
|
+
declare class CloudStorageAdapter implements StorageAdapter {
|
|
470
|
+
private readonly apiUrl;
|
|
471
|
+
private readonly workspace;
|
|
472
|
+
private readonly autoSignIn;
|
|
473
|
+
/** Cached current-tab token. Mirrored to sessionStorage. */
|
|
474
|
+
private cachedToken;
|
|
475
|
+
/** Single-flight: at most one popup at a time. Subsequent requests await this. */
|
|
476
|
+
private pendingAuth;
|
|
477
|
+
constructor(opts: CloudStorageOptions);
|
|
478
|
+
load(_namespace: string): Promise<VizuComment[]>;
|
|
479
|
+
addComment(_namespace: string, comment: VizuComment): Promise<void>;
|
|
480
|
+
updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
481
|
+
removeComment(_namespace: string, id: string): Promise<void>;
|
|
482
|
+
setAll(namespace: string, comments: VizuComment[]): Promise<void>;
|
|
483
|
+
clear(_namespace: string): Promise<void>;
|
|
484
|
+
addReply(_namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
|
|
485
|
+
removeReply(_namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
|
|
486
|
+
/**
|
|
487
|
+
* Upload a file as multipart/form-data to the host backend, which
|
|
488
|
+
* forwards to Vercel Blob server-side. Stays under the 4.5 MB Vercel
|
|
489
|
+
* function body cap (our own 5 MB attachment cap will be enforced
|
|
490
|
+
* server-side before that's hit).
|
|
491
|
+
*
|
|
492
|
+
* Deliberately doesn't use `@vercel/blob/client`'s direct-upload flow:
|
|
493
|
+
* - keeps `@vizu/core` free of any `@vercel/blob` dependency
|
|
494
|
+
* (script-tag and local-only consumers don't pull blob code)
|
|
495
|
+
* - the multipart path is one round-trip; the direct-upload flow
|
|
496
|
+
* is two (signed URL + upload). For attachments capped at 5 MB,
|
|
497
|
+
* the savings don't justify the dep cost.
|
|
498
|
+
*/
|
|
499
|
+
uploadAttachment(_namespace: string, file: File): Promise<Attachment>;
|
|
500
|
+
getAuthContext(): AuthContext | null;
|
|
501
|
+
private isExpired;
|
|
502
|
+
private readStoredToken;
|
|
503
|
+
private writeStoredToken;
|
|
504
|
+
private clearStoredToken;
|
|
505
|
+
private sessionKey;
|
|
506
|
+
/**
|
|
507
|
+
* Resolve a valid token, opening the popup if needed.
|
|
508
|
+
*
|
|
509
|
+
* Single-flight: simultaneous requests await one popup, not N.
|
|
510
|
+
* Caller-friendly: throws a Error with `.code === 'auth_canceled'`
|
|
511
|
+
* if the user closes the popup; hosts can catch this and present
|
|
512
|
+
* a friendly message instead of a blank failure.
|
|
513
|
+
*/
|
|
514
|
+
private resolveToken;
|
|
515
|
+
private openSignInPopup;
|
|
516
|
+
/**
|
|
517
|
+
* Perform an authenticated fetch with one-shot retry on 401 (re-opens
|
|
518
|
+
* the popup if the token expired during the request). 4xx other than
|
|
519
|
+
* 401 / 404 throw; 5xx throws; 2xx returns.
|
|
520
|
+
*/
|
|
521
|
+
private fetchAuthed;
|
|
522
|
+
private parseJson;
|
|
523
|
+
}
|
|
524
|
+
type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found' | 'http_error';
|
|
525
|
+
interface CloudError extends Error {
|
|
526
|
+
code: CloudErrorCode;
|
|
527
|
+
status?: number;
|
|
528
|
+
body?: unknown;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
declare function fingerprint(el: Element): ElementFingerprint;
|
|
532
|
+
/**
|
|
533
|
+
* Resolve a fingerprint to a live DOM element with a confidence tag.
|
|
534
|
+
*
|
|
535
|
+
* Two-phase ladder:
|
|
536
|
+
*
|
|
537
|
+
* **Strong identifiers (short-circuit).** `data-vizu-key` and `#id`
|
|
538
|
+
* are host-explicit anchors — if the captured key exists in the DOM
|
|
539
|
+
* we trust it 100% and return **exact**. For `data-vizu-key`, the
|
|
540
|
+
* *absence* of the captured key is itself evidence: the host marked
|
|
541
|
+
* that element explicitly and the mark is gone, so we **orphan**
|
|
542
|
+
* without consulting weaker rungs. `id` gets the gentler treatment
|
|
543
|
+
* (fall through on miss) because ids get renamed without the element
|
|
544
|
+
* itself going away.
|
|
545
|
+
*
|
|
546
|
+
* **Fuzzy rungs vote (Phase 11.4.4 / #135).** When no strong
|
|
547
|
+
* identifier resolves, every fuzzy rung (class signature, CSS selector,
|
|
548
|
+
* ancestor chain, text snippet) returns its best candidate. We tally
|
|
549
|
+
* votes per element and require ≥ 2 rungs to agree on the same target
|
|
550
|
+
* before returning a match. 3+ votes → **likely**, 2 votes → **drifted**,
|
|
551
|
+
* ≤ 1 vote → **orphaned**.
|
|
552
|
+
*
|
|
553
|
+
* The vote requirement is what catches `element_deleted`: the actual
|
|
554
|
+
* element is gone, so each fuzzy rung finds a *different* nearby
|
|
555
|
+
* element. Votes scatter, no consensus, orphan. Before this, the
|
|
556
|
+
* matcher returned the first rung's hit and confidently mis-anchored
|
|
557
|
+
* the comment to a similar element nearby — the false-positive
|
|
558
|
+
* surfaced by the Phase 11.4.3 stress harness.
|
|
559
|
+
*/
|
|
560
|
+
declare function findByFingerprint(fp: ElementFingerprint, root?: ParentNode): FingerprintMatch;
|
|
561
|
+
/** Backwards-compat: callers that only need the element. */
|
|
562
|
+
declare function findElementByFingerprint(fp: ElementFingerprint, root?: ParentNode): Element | null;
|
|
563
|
+
declare function fingerprintKey(fp: ElementFingerprint): string;
|
|
564
|
+
/** Short human-readable label for an element, used in `#` reference chips. */
|
|
565
|
+
declare function fingerprintLabel(fp: ElementFingerprint): string;
|
|
566
|
+
|
|
567
|
+
declare class Vizu {
|
|
568
|
+
private opts;
|
|
569
|
+
private parsedShortcut;
|
|
570
|
+
private storage;
|
|
571
|
+
private root;
|
|
572
|
+
private highlighter;
|
|
573
|
+
private popover;
|
|
574
|
+
private pill;
|
|
575
|
+
private sidebar;
|
|
576
|
+
/** Saved-comment markers: keyed by `${commentId}:${anchorIndex}` */
|
|
577
|
+
private markers;
|
|
578
|
+
/** Persistent outlines for commented elements: keyed by fingerprintKey */
|
|
579
|
+
private outlines;
|
|
580
|
+
/** Temporary "active anchor" outlines shown while popover is open OR
|
|
581
|
+
* while a multi-select session is pending. Stores the fp so we can
|
|
582
|
+
* reposition without needing to consult popover/pending state. */
|
|
583
|
+
private activeAnchorOutlines;
|
|
584
|
+
/** Transient spotlights rendered on jump (click sidebar item / anchor chip / # ref). */
|
|
585
|
+
private spotlights;
|
|
586
|
+
private comments;
|
|
587
|
+
/**
|
|
588
|
+
* Best confidence each comment was resolved at on the most recent
|
|
589
|
+
* marker render. Used by the sidebar to surface orphaned + drifted
|
|
590
|
+
* comments separately, and exposed publicly via {@link getCommentConfidence}.
|
|
591
|
+
*/
|
|
592
|
+
private resolvedConfidence;
|
|
593
|
+
/**
|
|
594
|
+
* Comment ids the matcher has already re-fingerprinted in this session.
|
|
595
|
+
* Phase 11.4.4.3 self-healing: when a comment lands as `drifted` and
|
|
596
|
+
* we have a live element to re-fingerprint, we rewrite the stored
|
|
597
|
+
* fingerprint so the next load resolves as exact/likely. Tracked
|
|
598
|
+
* here so the same render loop doesn't re-heal on every redraw.
|
|
599
|
+
* Cleared on `destroy()`; survives `disable()`/`enable()`.
|
|
600
|
+
*/
|
|
601
|
+
private selfHealedThisSession;
|
|
602
|
+
private actions;
|
|
603
|
+
private user;
|
|
604
|
+
/** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
|
|
605
|
+
private liveCollab;
|
|
606
|
+
private enabled;
|
|
607
|
+
private rafScheduled;
|
|
608
|
+
private bus;
|
|
609
|
+
/** Picker: next element click is consumed as a reference instead of opening a popover. */
|
|
610
|
+
private picking;
|
|
611
|
+
private pickingBanner;
|
|
612
|
+
/** Multi-select state — pending selection NOT yet attached to a popover. */
|
|
613
|
+
private multiSelectMode;
|
|
614
|
+
private pendingFingerprints;
|
|
615
|
+
private pendingTargets;
|
|
616
|
+
constructor(options?: VizuOptions, _defaultStorage?: 'memory' | 'local');
|
|
617
|
+
on<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): () => void;
|
|
618
|
+
off<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void;
|
|
619
|
+
setUser(user: VizuUser | null): void;
|
|
620
|
+
getUser(): VizuUser | null;
|
|
621
|
+
enable(): void;
|
|
622
|
+
disable(): void;
|
|
623
|
+
toggle(): void;
|
|
624
|
+
isEnabled(): boolean;
|
|
625
|
+
destroy(): void;
|
|
626
|
+
addAction(action: VizuAction): void;
|
|
627
|
+
removeAction(id: string): void;
|
|
628
|
+
getActions(): VizuAction[];
|
|
629
|
+
invokeAction(id: string): void;
|
|
630
|
+
getComments(): VizuComment[];
|
|
631
|
+
setComments(comments: VizuComment[], opts?: {
|
|
632
|
+
persist?: boolean;
|
|
633
|
+
}): Promise<void>;
|
|
634
|
+
/**
|
|
635
|
+
* Patch a single comment. Used by status changes (resolve / reopen),
|
|
636
|
+
* future threading mutations, etc. Returns the updated comment or
|
|
637
|
+
* `null` if no comment with that id exists.
|
|
638
|
+
*/
|
|
639
|
+
updateComment(id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
640
|
+
/** Auth context surfaced by the active storage adapter, if any (cloud adapters set this). */
|
|
641
|
+
getAuthContext(): AuthContext | null;
|
|
642
|
+
/**
|
|
643
|
+
* Re-fingerprint a comment's anchors against the live DOM and persist.
|
|
644
|
+
* Called from the render loop when a comment lands `drifted` with a
|
|
645
|
+
* matched element — rewriting the fingerprint here makes the next
|
|
646
|
+
* load resolve via a cleaner rung (data-vizu-key / id / class) instead
|
|
647
|
+
* of falling back to fuzzy text.
|
|
648
|
+
*
|
|
649
|
+
* Marks the comment as healed for this session so the same render
|
|
650
|
+
* doesn't kick the same write twice. Failures are logged; the next
|
|
651
|
+
* page load gets another chance.
|
|
652
|
+
*/
|
|
653
|
+
private selfHealComment;
|
|
654
|
+
/**
|
|
655
|
+
* Resolution confidence for a comment as of the last marker render.
|
|
656
|
+
* `'orphaned'` means the comment's fingerprints didn't resolve to any
|
|
657
|
+
* live DOM element — the sidebar surfaces it in the orphaned section.
|
|
658
|
+
* Returns `undefined` for unknown comment ids.
|
|
659
|
+
*/
|
|
660
|
+
getCommentConfidence(commentId: string): MatchConfidence | undefined;
|
|
661
|
+
/** Returns the full Map of comment id → confidence. Useful for the dashboard. */
|
|
662
|
+
getResolvedConfidence(): Map<string, MatchConfidence>;
|
|
663
|
+
/**
|
|
664
|
+
* True iff the active storage adapter implements `uploadAttachment`.
|
|
665
|
+
* The popover hides its upload UI when this returns false (local /
|
|
666
|
+
* memory modes don't support attachments today).
|
|
667
|
+
*/
|
|
668
|
+
canUploadAttachments(): boolean;
|
|
669
|
+
/**
|
|
670
|
+
* Upload an attachment via the active storage adapter. Throws if the
|
|
671
|
+
* adapter doesn't implement uploads. Caller is responsible for adding
|
|
672
|
+
* the returned Attachment to a comment (popover does this automatically).
|
|
673
|
+
*/
|
|
674
|
+
uploadAttachment(file: File): Promise<Attachment>;
|
|
675
|
+
/**
|
|
676
|
+
* Append a reply to a comment. Returns the parent comment or null if
|
|
677
|
+
* the comment id doesn't exist. Routes through the storage adapter's
|
|
678
|
+
* native `addReply` when available, falling back to an updateComment
|
|
679
|
+
* patch for adapters that don't implement reply ops.
|
|
680
|
+
*/
|
|
681
|
+
addReply(commentId: string, text: string, mentions?: string[]): Promise<VizuComment | null>;
|
|
682
|
+
/**
|
|
683
|
+
* Remove a reply from a comment. Returns the parent comment or null
|
|
684
|
+
* if either id is unknown.
|
|
685
|
+
*/
|
|
686
|
+
removeReply(commentId: string, replyId: string): Promise<VizuComment | null>;
|
|
687
|
+
clearAll(): Promise<void>;
|
|
688
|
+
/** Toggle multi-select mode. When on, plain clicks accumulate into the selection. */
|
|
689
|
+
toggleMultiSelectMode(): void;
|
|
690
|
+
/** Discard the in-progress selection AND exit multi-select mode. */
|
|
691
|
+
clearSelection(): void;
|
|
692
|
+
/** Open the popover anchored to the current pending selection so the user can write the comment. */
|
|
693
|
+
commitSelection(): void;
|
|
694
|
+
getSelection(): ElementFingerprint[];
|
|
695
|
+
private loadComments;
|
|
696
|
+
/**
|
|
697
|
+
* Subscribe to remote-change notifications from the active storage
|
|
698
|
+
* adapter (cloud adapters only). Local adapters never emit. The
|
|
699
|
+
* subscription is torn down on `destroy()`.
|
|
700
|
+
*/
|
|
701
|
+
private unsubscribeStorage;
|
|
702
|
+
private subscribeStorage;
|
|
703
|
+
private deferred;
|
|
704
|
+
private onKeyDown;
|
|
705
|
+
private mount;
|
|
706
|
+
private unmount;
|
|
707
|
+
private onElementClick;
|
|
708
|
+
private openPopoverFor;
|
|
709
|
+
private closePopover;
|
|
710
|
+
private addToSelection;
|
|
711
|
+
private commentsForElement;
|
|
712
|
+
private addCommentFromPopover;
|
|
713
|
+
private removeComment;
|
|
714
|
+
private refreshUi;
|
|
715
|
+
private toggleSidebar;
|
|
716
|
+
private closeSidebar;
|
|
717
|
+
startPicking(onSelect: (fp: ElementFingerprint) => void, onCancel: () => void): void;
|
|
718
|
+
cancelPicking(): void;
|
|
719
|
+
private showPickingBanner;
|
|
720
|
+
private hidePickingBanner;
|
|
721
|
+
private jumpToReference;
|
|
722
|
+
jumpToFingerprint(fp: ElementFingerprint): void;
|
|
723
|
+
private renderAllMarkers;
|
|
724
|
+
/** Pulse outlines of OTHER elements that share a multi-anchor comment with this one. */
|
|
725
|
+
private pulseRelatedElements;
|
|
726
|
+
private positionOutline;
|
|
727
|
+
private positionMarker;
|
|
728
|
+
private syncActiveAnchorOutlines;
|
|
729
|
+
private clearActiveAnchorOutlines;
|
|
730
|
+
private scheduleReposition;
|
|
731
|
+
private actionContext;
|
|
732
|
+
snapshotHtml(): string;
|
|
733
|
+
private copyToClipboard;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export { type ActionContext, type Attachment, type AuthContext, type CloudError, CloudStorageAdapter, type ElementFingerprint, InMemoryStorageAdapter, LocalStorageAdapter, type MatchConfidence, NullStorageAdapter, type Reply, SCHEMA_VERSION, type StorageAdapter, type StorageOption, Vizu, type VizuAction, type VizuCloudOptions, type VizuComment, type VizuEventHandler, type VizuEventName, type VizuOptions, type VizuUser, findByFingerprint, findElementByFingerprint, fingerprint, fingerprintKey, fingerprintLabel };
|