@siteping/widget 0.10.2 → 0.10.4

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.
@@ -0,0 +1,799 @@
1
+ import { type Prettify } from "./type-utils.cjs";
2
+ /** FAB anchor — bottom-corner placement supported by the widget. */
3
+ export type SitepingPosition = "bottom-right" | "bottom-left";
4
+ /** Visual theme — `auto` resolves to `light` or `dark` via system preference. */
5
+ export type SitepingTheme = "light" | "dark" | "auto";
6
+ /** Built-in UI locales shipped with the widget. */
7
+ export declare const BUILTIN_LOCALES: readonly ["en", "fr", "de", "es", "it", "pt", "ru"];
8
+ export type BuiltinLocale = (typeof BUILTIN_LOCALES)[number];
9
+ /**
10
+ * Locale identifier accepted by the widget. Built-in locales are kept as
11
+ * literal strings so editors auto-complete them, but arbitrary BCP-47 tags
12
+ * are also accepted (custom dictionaries registered via `registerLocale`).
13
+ */
14
+ export type SitepingLocale = BuiltinLocale | (string & {});
15
+ /**
16
+ * Reasons reported through `SitepingConfig.onSkip` — production environment,
17
+ * mobile viewport, or server-side rendering (no `window`/`document`).
18
+ */
19
+ export type SitepingSkipReason = "production" | "mobile" | "ssr";
20
+ /** Per-channel + per-buffer-size diagnostics configuration. */
21
+ export interface DiagnosticsCaptureOptions {
22
+ console?: boolean;
23
+ network?: boolean;
24
+ maxConsoleEntries?: number;
25
+ maxNetworkEntries?: number;
26
+ }
27
+ /** Identity payload supplied by the host application — bypasses the modal. */
28
+ export interface SitepingIdentity {
29
+ name: string;
30
+ email: string;
31
+ }
32
+ /** Deep-link configuration — controls how a feedback id is read from the URL. */
33
+ export interface SitepingDeepLinkOptions {
34
+ /** Query parameter name carrying the feedback id. Defaults to `"siteping"`. */
35
+ param?: string;
36
+ }
37
+ /**
38
+ * Extra request headers for HTTP mode — a static map, or a factory (sync or
39
+ * async) invoked once per request to produce fresh values (e.g. a short-lived
40
+ * session token).
41
+ */
42
+ export type SitepingHeadersOption = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
43
+ /** Configuration options for the Siteping widget. */
44
+ export interface SitepingConfig {
45
+ /** HTTP endpoint that receives feedbacks (e.g. '/api/siteping'). Required unless `store` is provided. */
46
+ endpoint?: string | undefined;
47
+ /**
48
+ * Convenience auth for HTTP mode — sent as `Authorization: Bearer <apiKey>`
49
+ * on every request to `endpoint`.
50
+ *
51
+ * **WARNING: the widget runs in every visitor's browser, so a static key
52
+ * configured here is public** — anyone can read it from your page source
53
+ * and replay it against your API. Only use `apiKey` for internal tools
54
+ * already behind your own login. On public sites, prefer `headers` with a
55
+ * per-request factory returning a short-lived session token.
56
+ *
57
+ * Ignored in store mode.
58
+ */
59
+ apiKey?: string | undefined;
60
+ /**
61
+ * Extra headers for every HTTP-mode request — a static map, or a factory
62
+ * (sync or async) called once per request (e.g. to fetch a fresh session
63
+ * token). Merged over the widget's generated headers, so an explicit
64
+ * `Authorization` entry overrides `apiKey`. A throwing/rejecting factory
65
+ * fails the request like a network error.
66
+ *
67
+ * Ignored in store mode.
68
+ */
69
+ headers?: SitepingHeadersOption | undefined;
70
+ /** Required — project identifier used to scope feedbacks */
71
+ projectName: string;
72
+ /** Direct store for client-side mode. When set, bypasses HTTP and uses the store directly in the browser. */
73
+ store?: SitepingStore | undefined;
74
+ /** FAB position — defaults to 'bottom-right' */
75
+ position?: SitepingPosition;
76
+ /**
77
+ * Show the "toggle markers visibility" item in the FAB radial menu.
78
+ * Defaults to `true` (current behavior). Set to `false` to hide that
79
+ * item entirely — useful for hosts that always want markers visible
80
+ * (e.g. dedicated review tools) or that find the eye icon redundant
81
+ * when no marker is on screen.
82
+ *
83
+ * Hiding the item also removes its keyboard navigation slot — the
84
+ * remaining two items still respond to ArrowUp/ArrowDown/Home/End.
85
+ * The marker-visibility state itself is unaffected; markers stay
86
+ * visible (the previous default state) and `annotations:toggle` is
87
+ * simply never emitted from the FAB.
88
+ */
89
+ showAnnotationsToggle?: boolean | undefined;
90
+ /** Accent color for the widget UI — defaults to '#0066ff' */
91
+ accentColor?: string;
92
+ /**
93
+ * Render the widget even when it would normally be skipped — this bypasses
94
+ * BOTH the production-environment guard AND the mobile-viewport guard.
95
+ * It does NOT bypass the SSR guard: without `window`/`document` the widget
96
+ * never renders and `onSkip("ssr")` fires instead.
97
+ * Defaults to false. Use it for dedicated review tools, staging environments,
98
+ * or responsive testing where you always want the widget present.
99
+ */
100
+ forceShow?: boolean;
101
+ /**
102
+ * Minimum viewport width (px) at or above which the widget renders. Below it,
103
+ * the widget is skipped and `onSkip("mobile")` fires. Defaults to `768`.
104
+ *
105
+ * Set lower (e.g. `0`) to allow narrow/mobile viewports, or use `forceShow`
106
+ * to bypass the viewport check entirely.
107
+ */
108
+ minViewportWidth?: number | undefined;
109
+ /** Enable debug logging of lifecycle events — defaults to false */
110
+ debug?: boolean;
111
+ /** Color theme — defaults to 'light' */
112
+ theme?: SitepingTheme;
113
+ /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru. Any other string falls back to English. */
114
+ locale?: SitepingLocale | undefined;
115
+ /**
116
+ * Returns the current page scope for annotations and panel filtering.
117
+ * Called on initial markers load and on `instance.refresh()`.
118
+ *
119
+ * Default: `{ url: window.location.pathname, urlPattern: null }` — annotations
120
+ * are scoped strictly to the current pathname.
121
+ *
122
+ * Apps with parameterized routes (e.g. React Router) should return both the
123
+ * concrete URL and the route template (e.g. `/orders/:orderId`) so the panel
124
+ * can offer a "this type of page" filter that groups feedbacks by template.
125
+ */
126
+ getPageScope?: (() => PageScope) | undefined;
127
+ /**
128
+ * When true (default), the widget filters initial markers and panel results
129
+ * by `feedback.url === scope.url`, so annotations created on one page never
130
+ * leak to other pages — even if their CSS selector accidentally matches.
131
+ * Set to `false` to revert to the legacy project-wide behavior.
132
+ */
133
+ scopeAnnotationsByUrl?: boolean | undefined;
134
+ /**
135
+ * Capture a JPEG screenshot of the annotated area on submit. Defaults to
136
+ * `false` — opt-in because:
137
+ *
138
+ * - it adds runtime weight (~40 KB gzip dynamic chunk for html2canvas,
139
+ * loaded only on first capture),
140
+ * - it embeds page content in the feedback (privacy/GDPR consideration —
141
+ * inform end users in your widget host UI when enabling).
142
+ *
143
+ * `html2canvas` ships as a regular dependency of `@siteping/widget` so the
144
+ * dynamic import always resolves; you don't need to install anything extra.
145
+ *
146
+ * **Masking sensitive elements:** add `data-siteping-ignore="true"` to any
147
+ * element you do NOT want captured (password fields, credit-card forms,
148
+ * API tokens shown in the UI, etc.). The capture predicate skips matching
149
+ * elements *and their descendants*. Do this BEFORE turning on screenshots
150
+ * in production — once a feedback is saved, the screenshot is in your DB
151
+ * (or object storage) regardless of what was on the page.
152
+ */
153
+ enableScreenshot?: boolean | undefined;
154
+ /**
155
+ * Enable right-click (`contextmenu`) to instantly open the comment composer
156
+ * at the cursor location. When enabled, a document-level listener intercepts
157
+ * right-clicks, prevents the browser's native context menu, and enters the
158
+ * annotation flow anchored to the element under the cursor. Defaults to
159
+ * `false` — the browser's native context menu is never hijacked unless the
160
+ * host explicitly opts in.
161
+ *
162
+ * **Modifier-key escape hatch:** holding Shift, Ctrl, Alt, or Meta while
163
+ * right-clicking always falls through to the native context menu, giving
164
+ * users (and devtools) an escape hatch regardless of this setting.
165
+ *
166
+ * Right-clicks on SitePing's own UI (FAB, panel, markers, popup) are
167
+ * ignored — the native menu is shown as expected.
168
+ *
169
+ * Note: on Android, `contextmenu` fires on long-press. The widget already
170
+ * hides below `minViewportWidth` (default 768 px), but tablets above that
171
+ * threshold will trigger this flow on long-press.
172
+ */
173
+ enableRightClickComment?: boolean | undefined;
174
+ /**
175
+ * Capture the last few `console.*` calls and failed network requests
176
+ * (HTTP >= 400 or network error) at the moment a feedback is submitted.
177
+ *
178
+ * Lets reviewers replay the technical context that led to the report —
179
+ * stack traces, 500 responses, dead third-party scripts. Great for the
180
+ * "the page just doesn't work" feedback that contains zero detail.
181
+ *
182
+ * - `true` — capture with defaults (50 console / 20 network entries).
183
+ * - `false` (default) — no capture, no monkey-patching.
184
+ * - object — per-channel toggles + custom buffer sizes.
185
+ *
186
+ * **Privacy considerations:** console messages may contain anything the
187
+ * host page logs, including user data. Failed network requests record the
188
+ * URL (with query string) but never the response body. Inform end users
189
+ * before enabling in environments where they might log sensitive values.
190
+ */
191
+ captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;
192
+ /** Called when the widget is skipped (production mode, mobile viewport, SSR — no DOM) */
193
+ onSkip?: (reason: SitepingSkipReason) => void;
194
+ /**
195
+ * Auto-focus a specific annotation when its ID appears in the URL query
196
+ * string. Lets hosts deeplink directly into a feedback from external
197
+ * systems (Zammad tickets, Slack notifications, dashboard rows).
198
+ *
199
+ * When enabled, the widget reads the configured query parameter from
200
+ * `window.location.search` right after the initial markers load. If the
201
+ * value matches a visible feedback ID, the widget scrolls the annotation
202
+ * into view, pins its highlight, and pulses the marker — the same visual
203
+ * affordance a marker click produces.
204
+ *
205
+ * - `false` / `undefined` (default): no URL parsing. Existing behavior
206
+ * unchanged, no host URL inspection.
207
+ * - `true`: enabled with default query parameter name `siteping`.
208
+ * - object: enabled with a custom parameter name. Use this to avoid
209
+ * clashes with host-app query keys.
210
+ *
211
+ * Only the initial load triggers focus. Subsequent URL changes (SPA
212
+ * navigation, `history.pushState`, hash updates) are ignored —
213
+ * deliberate, to avoid surprising re-scrolls during normal browsing.
214
+ * Hosts that need re-focus on route change can call
215
+ * `instance.focusFeedback(id)` explicitly.
216
+ */
217
+ deepLink?: boolean | SitepingDeepLinkOptions | undefined;
218
+ /**
219
+ * Automatically re-fetch feedbacks when the page changes during client-side
220
+ * (SPA) navigation. Enabled by default.
221
+ *
222
+ * The widget is normally mounted once (singleton) inside a persistent layout
223
+ * — e.g. a Next.js App Router `layout.tsx`, which does NOT remount on
224
+ * client-side navigation. Without this, init runs a single time and both the
225
+ * panel list and the page markers stay frozen on the page where the widget
226
+ * first mounted. With it on, the widget patches the History API
227
+ * (`pushState`/`replaceState`, which SPA routers call instead of triggering
228
+ * `popstate`) and listens for `popstate`/`hashchange`, then re-fetches when
229
+ * the scope key (`getPageScope().url` + template) actually changes.
230
+ *
231
+ * This re-fetches data only — it deliberately does NOT re-focus or re-scroll
232
+ * to an annotation (deep-link focus stays initial-load only; see `deepLink`),
233
+ * so normal browsing is never interrupted by a surprise scroll.
234
+ *
235
+ * - `true` (default) — watch navigation and re-fetch on route change.
236
+ * - `false` — never touch the History API; hosts drive updates manually via
237
+ * `instance.refresh()`.
238
+ */
239
+ watchNavigation?: boolean | undefined;
240
+ /**
241
+ * Pre-fill author identity from the host application — typically the
242
+ * currently signed-in user. When set, the widget uses these values
243
+ * directly and never shows the identity modal, even on first feedback.
244
+ *
245
+ * Use case: SSO-integrated apps where the end user is already
246
+ * authenticated by the host. Avoids the awkward "enter your name and
247
+ * email" prompt for users the host already knows.
248
+ *
249
+ * When unset (default), the widget falls back to localStorage and shows
250
+ * the modal on first feedback as before — existing behavior unchanged.
251
+ *
252
+ * Note: `config.identity` is **not** persisted to localStorage. It is
253
+ * read at widget init time, not on every render. Hosts that need live
254
+ * identity updates after sign-in/sign-out should currently remount the
255
+ * widget (e.g. via a React `key` on the wrapping component). See
256
+ * https://github.com/NeosiaNexus/SitePing/issues/85 for tracking a
257
+ * future enhancement that propagates identity updates without a remount.
258
+ */
259
+ identity?: SitepingIdentity | undefined;
260
+ /** Called when the feedback panel is opened. */
261
+ onOpen?: () => void;
262
+ /** Called when the feedback panel is closed. */
263
+ onClose?: () => void;
264
+ onFeedbackSent?: (feedback: FeedbackResponse) => void;
265
+ /**
266
+ * Called when a feedback API call fails.
267
+ *
268
+ * The widget always emits a `SitepingError` (or a subclass:
269
+ * `SitepingNetworkError`, `SitepingValidationError`, `SitepingAuthError`)
270
+ * for HTTP-mode failures — host apps can `instanceof` to drive retry
271
+ * logic, or read `error.code` (`"NETWORK" | "VALIDATION" | "AUTH" |
272
+ * "SERVER"`) and `error.retryable`. The type is widened to `Error` so
273
+ * direct-store callers can still surface raw errors without breaking the
274
+ * contract.
275
+ */
276
+ onError?: (error: Error) => void;
277
+ /** Called when the user starts drawing an annotation. */
278
+ onAnnotationStart?: () => void;
279
+ /** Called when the user finishes drawing an annotation. */
280
+ onAnnotationEnd?: () => void;
281
+ }
282
+ /** Instance returned by initSiteping() with lifecycle methods. */
283
+ export interface SitepingInstance {
284
+ /** Remove the widget from the DOM and clean up all listeners. */
285
+ destroy: () => void;
286
+ /** Open the panel programmatically */
287
+ open: () => void;
288
+ /** Close the panel */
289
+ close: () => void;
290
+ /** Reload feedbacks from server */
291
+ refresh: () => void;
292
+ /**
293
+ * Scroll the matching annotation into view, pin its highlight, and
294
+ * pulse its marker. Returns `true` when a visible feedback matched the
295
+ * given ID, `false` otherwise (unknown ID, feedback on another URL when
296
+ * `scopeAnnotationsByUrl` filtered it out, or markers not yet loaded).
297
+ *
298
+ * Counterpart to the `deepLink` config option for hosts that prefer to
299
+ * drive focus from JS (e.g., a notification click handler) instead of a
300
+ * URL query parameter.
301
+ */
302
+ focusFeedback: (feedbackId: string) => boolean;
303
+ /** Subscribe to a public widget event */
304
+ on: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => SitepingUnsubscribe;
305
+ /** Unsubscribe from a public widget event */
306
+ off: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => void;
307
+ }
308
+ /** Listener signature for a single `SitepingPublicEvents` key. */
309
+ export type SitepingPublicEventListener<K extends keyof SitepingPublicEvents> = (...args: SitepingPublicEvents[K]) => void;
310
+ /** Disposer returned by `SitepingInstance.on` — call once to detach the listener. */
311
+ export type SitepingUnsubscribe = () => void;
312
+ /** Events exposed to consumers via SitepingInstance.on / .off */
313
+ export interface SitepingPublicEvents {
314
+ "feedback:sent": [FeedbackResponse];
315
+ "feedback:deleted": [FeedbackResponse["id"]];
316
+ "panel:open": [];
317
+ "panel:close": [];
318
+ }
319
+ /** Single source of truth for feedback types — used by both TS types and Zod schemas. */
320
+ export declare const FEEDBACK_TYPES: readonly ["question", "change", "bug", "other"];
321
+ export type FeedbackType = (typeof FEEDBACK_TYPES)[number];
322
+ /** Single source of truth for feedback statuses. */
323
+ export declare const FEEDBACK_STATUSES: readonly ["open", "in_progress", "resolved", "wont_fix"];
324
+ export type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];
325
+ /**
326
+ * Terminal statuses — the feedback needs no further action. `resolvedAt` is
327
+ * the closure timestamp: set when a feedback enters a closed status, null
328
+ * while it is open or in progress. The derivation happens at the edge (HTTP
329
+ * handler, dashboard) — store adapters persist whatever they are given.
330
+ */
331
+ export declare const CLOSED_FEEDBACK_STATUSES: readonly ["resolved", "wont_fix"];
332
+ /** Whether a status is terminal (`resolved` or `wont_fix`). */
333
+ export declare function isClosedStatus(status: FeedbackStatus): boolean;
334
+ /**
335
+ * Page scope returned by `SitepingConfig.getPageScope()`.
336
+ *
337
+ * - `url`: concrete page identifier — usually `window.location.pathname`,
338
+ * used as the strict scope for marker rendering.
339
+ * - `urlPattern`: optional parameterized template (e.g. `/orders/:orderId`)
340
+ * used by the panel's "this type of page" filter to group feedbacks across
341
+ * instances of the same page kind.
342
+ */
343
+ export interface PageScope {
344
+ url: string;
345
+ urlPattern: string | null;
346
+ }
347
+ /** Input for creating a feedback record in the store. */
348
+ export interface FeedbackCreateInput {
349
+ projectName: string;
350
+ type: FeedbackType;
351
+ message: string;
352
+ status: FeedbackStatus;
353
+ url: string;
354
+ /**
355
+ * Optional parameterized URL template (e.g. `/orders/:orderId`) for the page
356
+ * where the feedback was created. Allows the panel to filter feedbacks by
357
+ * "this type of page" across different instances. Null when the host did not
358
+ * provide a `getPageScope` callback or the route has no template.
359
+ */
360
+ urlPattern?: string | null | undefined;
361
+ viewport: string;
362
+ userAgent: string;
363
+ authorName: string;
364
+ authorEmail: string;
365
+ clientId: string;
366
+ annotations: AnnotationCreateInput[];
367
+ /**
368
+ * Base64 JPEG `data:` URL captured by the widget at submit time.
369
+ *
370
+ * Adapters with a configured `ScreenshotStorage` are expected to upload
371
+ * this and persist the returned URL on `FeedbackRecord.screenshotUrl`.
372
+ * Adapters without storage may persist the data URL inline (memory /
373
+ * localStorage / dev) — the widget then renders it directly.
374
+ */
375
+ screenshotDataUrl?: string | null | undefined;
376
+ /**
377
+ * Where the client's annotation rect sits within the screenshot image,
378
+ * as fractions [0, 1] of the image dimensions. Present when the widget
379
+ * captured context around the drawn rect; null for legacy captures that
380
+ * were cropped exactly to the rect (dashboards then render the image
381
+ * without an overlay).
382
+ */
383
+ screenshotRegion?: ScreenshotRegion | null | undefined;
384
+ /**
385
+ * Optional console + failed-network snapshot captured by the widget when
386
+ * `SitepingConfig.captureDiagnostics` is enabled. Stored as JSON on
387
+ * `FeedbackRecord.diagnostics` so reviewers can replay the context.
388
+ */
389
+ diagnostics?: DiagnosticsSnapshot | null | undefined;
390
+ }
391
+ /** Input for a single annotation when creating a feedback. */
392
+ export interface AnnotationCreateInput {
393
+ cssSelector: string;
394
+ xpath: string;
395
+ textSnippet: string;
396
+ elementTag: string;
397
+ elementId?: string | undefined;
398
+ textPrefix: string;
399
+ textSuffix: string;
400
+ fingerprint: string;
401
+ neighborText: string;
402
+ /**
403
+ * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`
404
+ * attribute. When set, this is the most stable re-anchoring signal because
405
+ * hosts deliberately place these on layout/section roots that survive DOM
406
+ * refactors and viewport changes. Null when no semantic ancestor exists.
407
+ */
408
+ anchorKey?: string | null | undefined;
409
+ xPct: number;
410
+ yPct: number;
411
+ wPct: number;
412
+ hPct: number;
413
+ scrollX: number;
414
+ scrollY: number;
415
+ viewportW: number;
416
+ viewportH: number;
417
+ devicePixelRatio: number;
418
+ }
419
+ /** Query parameters for fetching feedbacks. */
420
+ export interface FeedbackQuery {
421
+ projectName: string;
422
+ type?: FeedbackType | undefined;
423
+ /** Exact single-status filter. For "any of a set" (bucket) semantics, use `statuses`. */
424
+ status?: FeedbackStatus | undefined;
425
+ /**
426
+ * Filter to feedbacks whose status is any of the listed values — bucket
427
+ * semantics used by the panel's binary tabs (e.g. "Open" passes
428
+ * `["open", "in_progress"]`). When both `status` and `statuses` are set,
429
+ * `statuses` wins. An empty array is treated as absent (no status filter).
430
+ */
431
+ statuses?: readonly FeedbackStatus[] | undefined;
432
+ search?: string | undefined;
433
+ page?: number | undefined;
434
+ limit?: number | undefined;
435
+ /**
436
+ * Filter to feedbacks created on this exact URL (path). Used by the panel's
437
+ * "this page" filter and by the markers loader to keep page scopes isolated.
438
+ */
439
+ url?: string | undefined;
440
+ /**
441
+ * Filter to feedbacks created on this URL pattern (e.g. `/orders/:orderId`).
442
+ * Used by the panel's "this type of page" filter to group feedbacks across
443
+ * different concrete instances of the same template.
444
+ */
445
+ urlPattern?: string | undefined;
446
+ }
447
+ /** Update payload for patching a feedback. */
448
+ export interface FeedbackUpdateInput {
449
+ status: FeedbackStatus;
450
+ resolvedAt: Date | null;
451
+ }
452
+ /** A persisted feedback record returned by the store. */
453
+ export interface FeedbackRecord {
454
+ id: string;
455
+ type: FeedbackType;
456
+ message: string;
457
+ status: FeedbackStatus;
458
+ projectName: string;
459
+ url: string;
460
+ /**
461
+ * Parameterized URL template the feedback was created on.
462
+ * Null for legacy records or hosts without `getPageScope`.
463
+ */
464
+ urlPattern: string | null;
465
+ authorName: string;
466
+ authorEmail: string;
467
+ viewport: string;
468
+ userAgent: string;
469
+ clientId: string;
470
+ resolvedAt: Date | null;
471
+ createdAt: Date;
472
+ updatedAt: Date;
473
+ annotations: AnnotationRecord[];
474
+ /**
475
+ * URL the widget renders as `<img src>`. Either an `https://...` from a
476
+ * configured `ScreenshotStorage`, or a `data:image/jpeg;base64,...` URL
477
+ * inline-persisted by adapters without storage. Null when no screenshot
478
+ * was captured (legacy records, capture failed, or host disabled it).
479
+ */
480
+ screenshotUrl: string | null;
481
+ /**
482
+ * Annotation rect position within the screenshot image, as fractions of
483
+ * its dimensions. Null for legacy captures cropped exactly to the rect.
484
+ */
485
+ screenshotRegion: ScreenshotRegion | null;
486
+ /**
487
+ * Console + failed-network snapshot captured at submit time. Null when
488
+ * diagnostics weren't enabled on the widget side.
489
+ */
490
+ diagnostics: DiagnosticsSnapshot | null;
491
+ }
492
+ /** A persisted annotation record returned by the store. */
493
+ export interface AnnotationRecord {
494
+ id: string;
495
+ feedbackId: string;
496
+ cssSelector: string;
497
+ xpath: string;
498
+ textSnippet: string;
499
+ elementTag: string;
500
+ elementId: string | null;
501
+ textPrefix: string;
502
+ textSuffix: string;
503
+ fingerprint: string;
504
+ neighborText: string;
505
+ /**
506
+ * Semantic anchor identifier from `data-feedback-anchor`. Null for legacy
507
+ * annotations or those drawn outside any anchored region.
508
+ */
509
+ anchorKey: string | null;
510
+ xPct: number;
511
+ yPct: number;
512
+ wPct: number;
513
+ hPct: number;
514
+ scrollX: number;
515
+ scrollY: number;
516
+ viewportW: number;
517
+ viewportH: number;
518
+ devicePixelRatio: number;
519
+ createdAt: Date;
520
+ }
521
+ /**
522
+ * Thrown when a record is not found during update or delete.
523
+ *
524
+ * Handlers translate this to HTTP 404. Adapters MUST throw this (not
525
+ * ORM-specific errors) so the handler layer remains ORM-agnostic.
526
+ */
527
+ export declare class StoreNotFoundError extends Error {
528
+ readonly code: "STORE_NOT_FOUND";
529
+ constructor(message?: string);
530
+ }
531
+ /**
532
+ * Thrown when a unique constraint is violated (e.g. duplicate `clientId`).
533
+ *
534
+ * Handlers use this to return the existing record instead of failing.
535
+ */
536
+ export declare class StoreDuplicateError extends Error {
537
+ readonly code: "STORE_DUPLICATE";
538
+ constructor(message?: string);
539
+ }
540
+ /**
541
+ * Thrown when a store accepts a mutation but cannot persist it — e.g.
542
+ * `localStorage` is full (QuotaExceededError). Adapters MUST throw this rather
543
+ * than swallow the failure, so callers learn the write was lost instead of
544
+ * seeing a phantom success.
545
+ */
546
+ export declare class StorePersistenceError extends Error {
547
+ readonly code: "STORE_PERSISTENCE";
548
+ constructor(message?: string, options?: ErrorOptions);
549
+ }
550
+ /** Shape of any ORM error that carries a Prisma-style `code` field. */
551
+ type CodedError<C extends string = string> = {
552
+ code: C;
553
+ };
554
+ /** Type guard — works for `StoreNotFoundError` and ORM-specific equivalents (e.g. Prisma P2025). */
555
+ export declare function isStoreNotFound(error: unknown): error is StoreNotFoundError | CodedError<"P2025">;
556
+ /** Type guard — works for `StoreDuplicateError` and ORM-specific equivalents (e.g. Prisma P2002). */
557
+ export declare function isStoreDuplicate(error: unknown): error is StoreDuplicateError | CodedError<"P2002">;
558
+ /**
559
+ * Type guard for `StorePersistenceError`. Matches on the stable `code` field
560
+ * in addition to `instanceof`: every consumer package bundles its own copy of
561
+ * core (tsup `noExternal`), so an instance thrown by one package fails an
562
+ * `instanceof` check against another package's class identity.
563
+ */
564
+ export declare function isStorePersistence(error: unknown): error is StorePersistenceError | CodedError<"STORE_PERSISTENCE">;
565
+ /** Flatten a widget `AnnotationPayload` (nested anchor + rect) into a flat `AnnotationCreateInput`. */
566
+ export declare function flattenAnnotation(ann: AnnotationPayload): Prettify<AnnotationCreateInput>;
567
+ /** Paginated result returned by `SitepingStore.getFeedbacks`. */
568
+ export interface FeedbackPage {
569
+ feedbacks: FeedbackRecord[];
570
+ total: number;
571
+ }
572
+ /**
573
+ * Abstract storage interface for Siteping.
574
+ *
575
+ * Any adapter (Prisma, Drizzle, raw SQL, localStorage, etc.) implements this
576
+ * interface. The HTTP handler and widget `StoreClient` operate against
577
+ * `SitepingStore`, decoupled from the storage backend.
578
+ *
579
+ * ## Error contract
580
+ *
581
+ * - **`updateFeedback` / `deleteFeedback`**: throw `StoreNotFoundError` when
582
+ * the record does not exist.
583
+ * - **`createFeedback`**: either return the existing record on duplicate
584
+ * `clientId` (idempotent) or throw `StoreDuplicateError`. The handler
585
+ * handles both patterns.
586
+ * - **All mutations**: when a write is accepted but cannot be persisted
587
+ * (e.g. storage quota), throw `StorePersistenceError` instead of reporting
588
+ * a phantom success. Detect it with `isStorePersistence`.
589
+ * - Other methods should not throw on empty results — return empty arrays or `null`.
590
+ */
591
+ export interface SitepingStore {
592
+ /** Create a feedback with its annotations. Idempotent on `clientId` — return existing record on duplicate, or throw `StoreDuplicateError`. Throws `StorePersistenceError` when the write cannot be persisted. */
593
+ createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;
594
+ /** Paginated query with optional filters. Returns empty array (not error) when no results. */
595
+ getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;
596
+ /** Lookup by client-generated UUID. Returns `null` (not error) when not found. */
597
+ findByClientId(clientId: string): Promise<FeedbackRecord | null>;
598
+ /** Update status/resolvedAt. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */
599
+ updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord>;
600
+ /** Delete a single record. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */
601
+ deleteFeedback(id: string): Promise<void>;
602
+ /** Bulk delete all feedbacks for a project. No-op (not error) if none exist. Throws `StorePersistenceError` when the write cannot be persisted. */
603
+ deleteAllFeedbacks(projectName: string): Promise<void>;
604
+ }
605
+ /** Payload sent from the widget to the server when submitting feedback. */
606
+ export interface FeedbackPayload {
607
+ projectName: string;
608
+ type: FeedbackType;
609
+ message: string;
610
+ url: string;
611
+ /**
612
+ * Parameterized URL template (e.g. `/orders/:orderId`) supplied by
613
+ * `SitepingConfig.getPageScope()`. Null when the host did not provide one.
614
+ */
615
+ urlPattern?: string | null | undefined;
616
+ viewport: string;
617
+ userAgent: string;
618
+ authorName: string;
619
+ authorEmail: string;
620
+ annotations: AnnotationPayload[];
621
+ /** Client-generated UUID for deduplication */
622
+ clientId: string;
623
+ /**
624
+ * Base64 JPEG `data:` URL of the annotated area. Captured by the widget
625
+ * when `enableScreenshot: true` is set in `SitepingConfig`. Null when
626
+ * disabled or when capture failed silently.
627
+ */
628
+ screenshotDataUrl?: string | null | undefined;
629
+ /**
630
+ * Annotation rect position within the screenshot image — see
631
+ * `ScreenshotRegion`. Null/absent when no screenshot was captured or the
632
+ * capture predates contextual framing.
633
+ */
634
+ screenshotRegion?: ScreenshotRegion | null | undefined;
635
+ /**
636
+ * Snapshot of the last few console messages and failed network requests
637
+ * captured at submit time when `captureDiagnostics` is enabled.
638
+ */
639
+ diagnostics?: DiagnosticsSnapshot | null | undefined;
640
+ }
641
+ /** Severity levels persisted in `ConsoleDiagnosticEntry`. */
642
+ export type ConsoleDiagnosticLevel = "log" | "info" | "warn" | "error";
643
+ /** A single console entry captured by `ConsoleBuffer`. */
644
+ export interface ConsoleDiagnosticEntry {
645
+ level: ConsoleDiagnosticLevel;
646
+ /** ISO 8601 timestamp captured at log time. */
647
+ timestamp: string;
648
+ /** Best-effort string representation of the original console args. */
649
+ message: string;
650
+ }
651
+ /** A single failed network request captured by `NetworkBuffer`. */
652
+ export interface NetworkDiagnosticEntry {
653
+ url: string;
654
+ method: string;
655
+ /** HTTP status; 0 when the request never reached the server. */
656
+ status: number;
657
+ /** End-to-end duration in ms. */
658
+ durationMs: number;
659
+ /** ISO 8601 timestamp at the moment the request was initiated. */
660
+ timestamp: string;
661
+ }
662
+ /**
663
+ * Diagnostics captured by the widget when `captureDiagnostics` is enabled.
664
+ *
665
+ * Both arrays are bounded (default: 50 console / 20 network). Adapters that
666
+ * support diagnostics should persist this as a JSON blob alongside the
667
+ * feedback so reviewers can replay the context that led to the report.
668
+ */
669
+ export interface DiagnosticsSnapshot {
670
+ console: ConsoleDiagnosticEntry[];
671
+ network: NetworkDiagnosticEntry[];
672
+ }
673
+ /** DOM anchoring data for re-attaching annotations to page elements. */
674
+ export interface AnchorData {
675
+ /** CSS selector generated by @medv/finder — primary anchor */
676
+ cssSelector: string;
677
+ /** XPath — fallback 1 */
678
+ xpath: string;
679
+ /** First ~120 chars of element innerText — empty string if none */
680
+ textSnippet: string;
681
+ /** Tag name for validation (e.g. "DIV", "SECTION") */
682
+ elementTag: string;
683
+ /** Element id attribute if available — most stable */
684
+ elementId?: string | undefined;
685
+ /** ~32 chars of text before this element in document flow (disambiguation) */
686
+ textPrefix: string;
687
+ /** ~32 chars of text after this element in document flow (disambiguation) */
688
+ textSuffix: string;
689
+ /** Structural fingerprint: "childCount:siblingIdx:attrHash" */
690
+ fingerprint: string;
691
+ /** Text content of adjacent sibling elements (context) */
692
+ neighborText: string;
693
+ /**
694
+ * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`
695
+ * attribute. When set, this is the highest-priority re-anchoring signal —
696
+ * hosts deliberately place these on layout/section roots that survive
697
+ * viewport changes and DOM refactors.
698
+ */
699
+ anchorKey?: string | null | undefined;
700
+ }
701
+ /**
702
+ * Where the client's annotation rect sits within the captured screenshot,
703
+ * as fractions [0, 1] of the image dimensions. The widget captures context
704
+ * around the drawn rect and records the rect's position here so dashboards
705
+ * can re-render the annotation on top of the image. Survives downscaling
706
+ * (fractions are resolution-independent).
707
+ */
708
+ export interface ScreenshotRegion {
709
+ /** X offset of the rect as fraction of image width — [0, 1] */
710
+ xPct: number;
711
+ /** Y offset of the rect as fraction of image height — [0, 1] */
712
+ yPct: number;
713
+ /** Rect width as fraction of image width — [0, 1] */
714
+ wPct: number;
715
+ /** Rect height as fraction of image height — [0, 1] */
716
+ hPct: number;
717
+ }
718
+ /** Drawn rectangle coordinates as percentages relative to the anchor element. */
719
+ export interface RectData {
720
+ /** X offset as fraction of anchor element width — must be in range [0, 1] */
721
+ xPct: number;
722
+ /** Y offset as fraction of anchor element height — must be in range [0, 1] */
723
+ yPct: number;
724
+ /** Width as fraction of anchor element width — must be in range [0, 1] */
725
+ wPct: number;
726
+ /** Height as fraction of anchor element height — must be in range [0, 1] */
727
+ hPct: number;
728
+ }
729
+ /** Annotation data sent as part of a feedback submission. */
730
+ export interface AnnotationPayload {
731
+ anchor: AnchorData;
732
+ rect: RectData;
733
+ scrollX: number;
734
+ scrollY: number;
735
+ viewportW: number;
736
+ viewportH: number;
737
+ devicePixelRatio: number;
738
+ }
739
+ /** Feedback record as returned by the API (dates serialized as strings). */
740
+ export interface FeedbackResponse {
741
+ id: string;
742
+ projectName: string;
743
+ type: FeedbackType;
744
+ message: string;
745
+ status: FeedbackStatus;
746
+ url: string;
747
+ /** Parameterized URL template the feedback was created on, or null. */
748
+ urlPattern: string | null;
749
+ viewport: string;
750
+ userAgent: string;
751
+ authorName: string;
752
+ /**
753
+ * May be an empty string — HTTP adapters redact it for unauthenticated
754
+ * requests. The full value requires a Bearer-authenticated request.
755
+ */
756
+ authorEmail: string;
757
+ resolvedAt: string | null;
758
+ createdAt: string;
759
+ updatedAt: string;
760
+ annotations: AnnotationResponse[];
761
+ /** Screenshot URL (data: or http:) — see `FeedbackRecord.screenshotUrl`. */
762
+ screenshotUrl: string | null;
763
+ /** Annotation rect within the screenshot image, or null — see `ScreenshotRegion`. */
764
+ screenshotRegion: ScreenshotRegion | null;
765
+ /** Console + failed-network snapshot, or null when diagnostics weren't captured. */
766
+ diagnostics: DiagnosticsSnapshot | null;
767
+ }
768
+ /** Annotation record as returned by the API. */
769
+ export interface AnnotationResponse {
770
+ id: string;
771
+ feedbackId: string;
772
+ cssSelector: string;
773
+ xpath: string;
774
+ textSnippet: string;
775
+ elementTag: string;
776
+ elementId: string | null;
777
+ textPrefix: string;
778
+ textSuffix: string;
779
+ fingerprint: string;
780
+ neighborText: string;
781
+ /** Semantic anchor identifier from `data-feedback-anchor`, or null. */
782
+ anchorKey: string | null;
783
+ xPct: number;
784
+ yPct: number;
785
+ wPct: number;
786
+ hPct: number;
787
+ scrollX: number;
788
+ scrollY: number;
789
+ viewportW: number;
790
+ viewportH: number;
791
+ devicePixelRatio: number;
792
+ createdAt: string;
793
+ }
794
+ /** Paginated `FeedbackResponse` shape returned by the API. */
795
+ export interface FeedbackResponseList {
796
+ feedbacks: FeedbackResponse[];
797
+ total: number;
798
+ }
799
+ export {};