@uselay/sdk 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.
@@ -0,0 +1,493 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React, { ReactNode } from 'react';
3
+
4
+ type CommentStatus = 'open' | 'resolved' | 'archived';
5
+ interface Author {
6
+ id: string | null;
7
+ name: string;
8
+ avatar: string | null;
9
+ }
10
+ interface ElementFingerprint {
11
+ tag: string;
12
+ textContent: string;
13
+ attributes: Record<string, string>;
14
+ siblingIndex: number;
15
+ siblingCount: number;
16
+ parentTag: string;
17
+ grandparentTag: string;
18
+ }
19
+ interface ElementMetadata {
20
+ computed_styles: Record<string, string>;
21
+ accessibility: {
22
+ role: string | null;
23
+ aria_label: string | null;
24
+ contrast_ratio: number | null;
25
+ contrast_passes_aa: boolean | null;
26
+ };
27
+ viewport: {
28
+ width: number;
29
+ height: number;
30
+ };
31
+ device: string;
32
+ fingerprint?: ElementFingerprint;
33
+ }
34
+ interface AIContextReview {
35
+ ai_context_version: 2 | 3;
36
+ mode: 'review';
37
+ category: 'visual' | 'accessibility' | 'layout' | 'copy' | 'interaction';
38
+ /** v3: single interpretation sentence replacing suggestions + accessibility_issues */
39
+ interpretation?: string;
40
+ /** @deprecated v2 only — replaced by interpretation in v3 */
41
+ suggestions?: string[];
42
+ /** @deprecated v2 only — replaced by interpretation in v3 */
43
+ accessibility_issues?: string[];
44
+ /** @deprecated v2 only — removed in v3 */
45
+ confidence?: number;
46
+ enriched_at: string;
47
+ }
48
+ interface AIContextSupport {
49
+ ai_context_version: 2 | 3;
50
+ mode: 'support';
51
+ intent: 'confusion' | 'bug_report' | 'feature_request' | 'complaint' | 'question' | 'praise' | 'other';
52
+ urgency: 'low' | 'medium' | 'high';
53
+ summary: string;
54
+ suggested_response: string;
55
+ /** @deprecated v2 only — removed in v3 */
56
+ confidence?: number;
57
+ enriched_at: string;
58
+ }
59
+ type AIContext = AIContextReview | AIContextSupport;
60
+ declare function isAIContextReview(ctx: AIContext): ctx is AIContextReview;
61
+ declare function isAIContextSupport(ctx: AIContext): ctx is AIContextSupport;
62
+ interface ElementBounds {
63
+ x: number;
64
+ y: number;
65
+ width: number;
66
+ height: number;
67
+ }
68
+ interface Comment {
69
+ id: string;
70
+ project_id: string;
71
+ thread_id: string | null;
72
+ author: Author;
73
+ content: string;
74
+ status: CommentStatus;
75
+ dom_path: string;
76
+ url_path: string;
77
+ element_metadata: ElementMetadata | null;
78
+ resolved_by: Author | null;
79
+ resolved_at: string | null;
80
+ archived_at: string | null;
81
+ ai_context: AIContext | null;
82
+ screenshot_url: string | null;
83
+ element_bounds: ElementBounds | null;
84
+ created_at: string;
85
+ updated_at: string;
86
+ }
87
+ type NewComment = Omit<Comment, 'id' | 'created_at' | 'updated_at'>;
88
+
89
+ type Unsubscribe = () => void;
90
+ interface CommentUpdate {
91
+ status?: CommentStatus;
92
+ resolved_by?: Author | null;
93
+ resolved_at?: string | null;
94
+ archived_at?: string | null;
95
+ /** Updated CSS selector path (used by M6 self-healing) */
96
+ dom_path?: string;
97
+ }
98
+ type CommentEventType = 'INSERT' | 'UPDATE';
99
+ interface CommentEvent {
100
+ type: CommentEventType;
101
+ comment: Comment;
102
+ }
103
+ interface AdapterOptions {
104
+ /** Session token for support-mode authentication */
105
+ sessionToken?: string;
106
+ }
107
+ interface LayAdapter {
108
+ getConfig(projectId: string): Promise<RemoteConfig>;
109
+ getComments(projectId: string, urlPath: string, options?: AdapterOptions): Promise<Comment[]>;
110
+ addComment(comment: NewComment, options?: AdapterOptions): Promise<Comment>;
111
+ updateComment(id: string, update: CommentUpdate, options?: AdapterOptions): Promise<Comment>;
112
+ uploadScreenshot(projectId: string, commentId: string, blob: Blob, bounds: ElementBounds, options?: AdapterOptions): Promise<void>;
113
+ subscribe(projectId: string, callback: (event: CommentEvent) => void, options?: AdapterOptions): Unsubscribe;
114
+ }
115
+
116
+ /** Project mode: review (team feedback) or support (end-user help) */
117
+ type ProjectMode = 'review' | 'support';
118
+ /** Starter chip for the Two-Speed Composer */
119
+ interface StarterChip {
120
+ /** Display label on the chip */
121
+ label: string;
122
+ /** Comment content submitted when chip is tapped (defaults to label if omitted) */
123
+ value?: string;
124
+ }
125
+ /** Remote configuration fetched from the dashboard */
126
+ interface RemoteConfig {
127
+ mode: ProjectMode;
128
+ active: boolean;
129
+ }
130
+ interface LayConfig {
131
+ /** Unique identifier for the project */
132
+ projectId: string;
133
+ /** Optional user identity. If not provided, comments are anonymous. */
134
+ user?: Author;
135
+ /** HMAC-SHA256 hash of user.id, created server-side with LAY_SECRET_KEY. Required for verified identity in support mode. */
136
+ userHash?: string;
137
+ /** Optional custom adapter. If not provided, auto-selects based on projectId. */
138
+ adapter?: LayAdapter;
139
+ /** Optional API base URL for the hosted backend. Defaults to https://uselay.com. Used for local development. */
140
+ apiUrl?: string;
141
+ /** Optional version string. Changing this archives previous comments. */
142
+ version?: string;
143
+ /** Enable AI enrichment on comments. Defaults to true. Set false to show metadata only (no AI interpretation). */
144
+ ai?: boolean;
145
+ /** Override mode from dashboard. If omitted, uses dashboard setting. */
146
+ mode?: ProjectMode;
147
+ /** Override active state. If omitted, uses dashboard setting. false = widget renders nothing. */
148
+ active?: boolean;
149
+ /** Starter chips for quick feedback. Defaults to ["Visual bug", "Copy issue", "Love this"]. Set to [] to disable chips. */
150
+ starterChips?: StarterChip[];
151
+ /** Capture viewport screenshots on comment creation. Defaults to true. Set false to disable. */
152
+ screenshots?: boolean;
153
+ }
154
+
155
+ type LayAction = {
156
+ type: 'TOGGLE_COMMENT_MODE';
157
+ } | {
158
+ type: 'SET_COMMENT_MODE';
159
+ payload: boolean;
160
+ } | {
161
+ type: 'SET_COMMENTS';
162
+ payload: Comment[];
163
+ } | {
164
+ type: 'ADD_COMMENT';
165
+ payload: Comment;
166
+ } | {
167
+ type: 'UPDATE_COMMENT';
168
+ payload: Comment;
169
+ };
170
+ interface LayContextValue {
171
+ isCommentMode: boolean;
172
+ comments: Comment[];
173
+ adapter: LayAdapter;
174
+ config: LayConfig;
175
+ dispatch: React.Dispatch<LayAction>;
176
+ portalRoot: HTMLDivElement | null;
177
+ mode: ProjectMode;
178
+ remoteConfigLoaded: boolean;
179
+ detachedDomPaths: Set<string>;
180
+ setDetachedDomPaths: React.Dispatch<React.SetStateAction<Set<string>>>;
181
+ }
182
+ interface LayProviderProps extends LayConfig {
183
+ children: ReactNode;
184
+ }
185
+ declare function LayProvider({ children, projectId, user, userHash, adapter: customAdapter, apiUrl, version, ai, mode: modeProp, active: activeProp, starterChips, screenshots, }: LayProviderProps): react_jsx_runtime.JSX.Element;
186
+
187
+ declare function LayToggle(): React.ReactPortal | null;
188
+
189
+ interface ElementHighlighterProps {
190
+ hoveredElement: Element | null;
191
+ selectedElement: Element | null;
192
+ }
193
+ declare function ElementHighlighter({ hoveredElement, selectedElement, }: ElementHighlighterProps): React.ReactPortal | null;
194
+
195
+ interface CommentAnchorProps {
196
+ selectedElement: Element | null;
197
+ onClearSelection: () => void;
198
+ }
199
+ declare function CommentAnchor({ selectedElement, onClearSelection, }: CommentAnchorProps): React.ReactPortal | null;
200
+
201
+ /**
202
+ * Owns the shared element selector state and renders both the
203
+ * highlight overlay and the comment input popover.
204
+ */
205
+ declare function CommentLayer(): react_jsx_runtime.JSX.Element;
206
+
207
+ interface CommentDotProps {
208
+ domPath: string;
209
+ comments: Comment[];
210
+ isResolved?: boolean;
211
+ isNew?: boolean;
212
+ onDidResolve?: () => void;
213
+ /** Pre-resolved element from 3-layer resolution (M6). Falls back to querySelector if not provided. */
214
+ resolvedElement?: Element | null;
215
+ }
216
+ declare const CommentDot: React.NamedExoticComponent<CommentDotProps>;
217
+
218
+ /**
219
+ * Renders a CommentDot for each unique dom_path that has comments.
220
+ * Hides dots for fully-archived dom_paths. Mutes dots for fully-resolved ones.
221
+ * Runs 3-layer element resolution and tracks detached comments.
222
+ * Always visible — dots show regardless of comment mode.
223
+ */
224
+ declare function CommentDots(): react_jsx_runtime.JSX.Element | null;
225
+
226
+ interface CommentThreadProps {
227
+ comments: Comment[];
228
+ dotRect: DOMRect;
229
+ onClose: () => void;
230
+ onDidResolve?: () => void;
231
+ }
232
+ declare function CommentThread({ comments, dotRect, onClose, onDidResolve, }: CommentThreadProps): React.ReactPortal | null;
233
+
234
+ interface CommentItemProps {
235
+ comment: Comment;
236
+ isReply?: boolean;
237
+ actions?: ReactNode;
238
+ }
239
+ declare function CommentItem({ comment, isReply, actions }: CommentItemProps): react_jsx_runtime.JSX.Element;
240
+
241
+ interface CommentActionsProps {
242
+ comment: Comment;
243
+ onResolve: (comment: Comment) => void;
244
+ onReopen: (comment: Comment) => void;
245
+ onArchive: (comment: Comment) => void;
246
+ }
247
+ declare function CommentActions({ comment, onResolve, onReopen, onArchive, }: CommentActionsProps): react_jsx_runtime.JSX.Element | null;
248
+
249
+ interface ArchivedThreadsPanelProps {
250
+ onClose: () => void;
251
+ }
252
+ declare function ArchivedThreadsPanel({ onClose }: ArchivedThreadsPanelProps): React.ReactPortal | null;
253
+
254
+ interface DetachedCommentsPanelProps {
255
+ onClose: () => void;
256
+ }
257
+ declare function DetachedCommentsPanel({ onClose }: DetachedCommentsPanelProps): React.ReactPortal | null;
258
+
259
+ interface AIContextCardProps {
260
+ elementMetadata: ElementMetadata | null;
261
+ aiContext: AIContext | null;
262
+ aiEnabled?: boolean;
263
+ }
264
+ declare function AIContextCard({ elementMetadata, aiContext, aiEnabled, }: AIContextCardProps): react_jsx_runtime.JSX.Element | null;
265
+
266
+ interface ReplyInputProps {
267
+ /** The root comment of the thread to reply to */
268
+ rootComment: Comment;
269
+ /** Called after a reply is successfully submitted */
270
+ onReplySubmitted?: () => void;
271
+ }
272
+ declare function ReplyInput({ rootComment, onReplySubmitted }: ReplyInputProps): react_jsx_runtime.JSX.Element;
273
+
274
+ declare function useLayContext(): LayContextValue;
275
+
276
+ declare function useCommentMode(): {
277
+ isCommentMode: boolean;
278
+ toggleCommentMode: () => void;
279
+ setCommentMode: (active: boolean) => void;
280
+ };
281
+
282
+ interface ElementSelectorState {
283
+ /** The element currently under the cursor (comment mode only) */
284
+ hoveredElement: Element | null;
285
+ /** The element the user clicked to comment on */
286
+ selectedElement: Element | null;
287
+ }
288
+ interface UseElementSelectorReturn extends ElementSelectorState {
289
+ /** Clear the selected element (dismiss comment input) */
290
+ clearSelection: () => void;
291
+ }
292
+ /**
293
+ * Tracks hovered and selected elements via event delegation on document.body.
294
+ * Only active when comment mode is on. Cleans up all listeners when mode is off.
295
+ */
296
+ declare function useElementSelector(): UseElementSelectorReturn;
297
+
298
+ interface CommentGroup {
299
+ domPath: string;
300
+ comments: Comment[];
301
+ }
302
+ interface ThreadItem {
303
+ root: Comment;
304
+ replies: Comment[];
305
+ }
306
+ interface ThreadGroup {
307
+ domPath: string;
308
+ threads: ThreadItem[];
309
+ allComments: Comment[];
310
+ }
311
+ /**
312
+ * Convenience hook that reads comments from context and provides
313
+ * grouping/filtering helpers. State is owned by LayProvider.
314
+ */
315
+ declare function useComments(): {
316
+ comments: Comment[];
317
+ groupedByDomPath: CommentGroup[];
318
+ threadsByDomPath: ThreadGroup[];
319
+ archivedThreads: ThreadItem[];
320
+ archivedCount: number;
321
+ detachedThreads: ThreadItem[];
322
+ detachedCount: number;
323
+ isDomPathFullyResolved: (domPath: string) => boolean;
324
+ isDomPathFullyArchived: (domPath: string) => boolean;
325
+ };
326
+
327
+ /**
328
+ * Generate a CSS selector path for an element by walking up the DOM tree.
329
+ *
330
+ * Priority order for each segment (highest to lowest):
331
+ * 1. data-feedback-id="X" → [data-feedback-id="X"] (stop walking)
332
+ * 2. data-testid="X" → [data-testid="X"] (stop walking)
333
+ * 3. id="X" → #X (stop walking)
334
+ * 4. Stable class names → tag.my-button (skip generated/hashed classes)
335
+ * 5. Tag + nth-child → div:nth-child(3) (fallback)
336
+ */
337
+ declare function generateDomPath(element: Element): string;
338
+ /**
339
+ * Heuristic to detect framework-generated class names.
340
+ * Returns true for classes that are likely unstable across builds.
341
+ */
342
+ declare function isGeneratedClassName(name: string): boolean;
343
+ /**
344
+ * Check if an element should be excluded from the commentable surface.
345
+ * Excludes Lay UI elements and non-visible elements.
346
+ */
347
+ declare function isCommentable(element: Element): boolean;
348
+
349
+ /**
350
+ * Element fingerprinting for anchoring hardening (M6).
351
+ *
352
+ * Captures a structural fingerprint of a DOM element that can be used
353
+ * to re-find the element when its CSS selector path breaks due to
354
+ * DOM changes between deploys.
355
+ */
356
+
357
+ /**
358
+ * Generate a fingerprint for a DOM element.
359
+ * Captures tag, text content, key attributes, and structural position.
360
+ */
361
+ declare function generateFingerprint(element: Element): ElementFingerprint;
362
+ /**
363
+ * Score how well a stored fingerprint matches a candidate element.
364
+ * Returns 0-100. Tag mismatch = 0 (required match).
365
+ */
366
+ declare function scoreFingerprintMatch(stored: ElementFingerprint, candidate: Element): number;
367
+ interface FingerprintMatchResult {
368
+ element: Element | null;
369
+ score: number;
370
+ }
371
+ /**
372
+ * Find the best matching element on the page for a stored fingerprint.
373
+ * Scans all elements of the same tag type and returns the highest-scoring match.
374
+ *
375
+ * @param fingerprint - The stored fingerprint to match against
376
+ * @param threshold - Minimum score to consider a match (default: 40)
377
+ * @returns The best matching element and its score, or null if below threshold
378
+ */
379
+ declare function findByFingerprint(fingerprint: ElementFingerprint, threshold?: number): FingerprintMatchResult;
380
+
381
+ /**
382
+ * Three-layer element resolution for anchoring hardening (M6).
383
+ *
384
+ * Resolution order:
385
+ * 1. data-feedback-id lookup (if dom_path contains one)
386
+ * 2. CSS selector (document.querySelector)
387
+ * 3. Fingerprint matching (fuzzy structural match)
388
+ *
389
+ * Returns the resolved element and which method succeeded.
390
+ */
391
+
392
+ type ResolveMethod = 'feedback-id' | 'selector' | 'fingerprint' | 'detached';
393
+ interface ResolveResult {
394
+ element: Element | null;
395
+ method: ResolveMethod;
396
+ /** Fingerprint match score (only set when method is 'fingerprint') */
397
+ score?: number;
398
+ }
399
+ /**
400
+ * Resolve a DOM element using 3-layer strategy.
401
+ *
402
+ * @param domPath - The stored CSS selector path
403
+ * @param fingerprint - Optional element fingerprint for fuzzy matching
404
+ * @returns The resolved element and the method that found it
405
+ */
406
+ declare function resolveElement(domPath: string, fingerprint?: ElementFingerprint | null): ResolveResult;
407
+
408
+ /**
409
+ * Summarize a stored CSS selector path into a human-readable string.
410
+ * Used in the detached comments panel to show "Was on: button in #pricing".
411
+ *
412
+ * @param domPath - A CSS selector like "body > div > main > section#pricing > button:nth-child(2)"
413
+ * @returns A human-readable summary like "button in #pricing"
414
+ */
415
+ declare function summarizeDomPath(domPath: string): string;
416
+
417
+ /**
418
+ * Format an ISO timestamp as a human-readable relative time string.
419
+ *
420
+ * Examples: "just now", "2 min ago", "1 hr ago", "Yesterday", "Feb 12"
421
+ */
422
+ declare function formatRelativeTime(isoTimestamp: string): string;
423
+
424
+ /**
425
+ * Walk up the DOM tree from `element` to find the first ancestor with
426
+ * a non-transparent computed background-color. Returns the CSS color string.
427
+ * Falls back to white (#FFFFFF) if all ancestors are transparent.
428
+ */
429
+ declare function resolveEffectiveBackground(element: Element): string;
430
+ /**
431
+ * Compute the WCAG 2.1 contrast ratio between a foreground and background color.
432
+ *
433
+ * Returns the ratio (≥1) and whether the combination passes WCAG AA.
434
+ * AA threshold: 4.5:1 for normal text, 3:1 for large text (≥18px or bold ≥14px).
435
+ * We default to the 4.5:1 threshold (normal text) since font-size context
436
+ * is not always reliably available here.
437
+ */
438
+ declare function computeContrastRatio(fgColor: string, bgColor: string): {
439
+ ratio: number;
440
+ passesAA: boolean;
441
+ };
442
+ /**
443
+ * Parse a user agent string into a readable device string.
444
+ * Returns "iPhone", "iPad", "Android", "Chrome on macOS", etc.
445
+ */
446
+ declare function parseUserAgent(ua: string): string;
447
+ /**
448
+ * Capture technical metadata from a DOM element.
449
+ *
450
+ * Collects computed styles, accessibility data (contrast ratio, ARIA),
451
+ * viewport dimensions, and device info. All reads are synchronous —
452
+ * no network calls, no DOM mutations, no layout thrashing.
453
+ */
454
+ declare function captureElementMetadata(element: Element): ElementMetadata;
455
+
456
+ interface GuestAuthor {
457
+ name: string;
458
+ }
459
+ /**
460
+ * Read the saved guest author name from localStorage.
461
+ * Returns null if no name is saved or localStorage is unavailable.
462
+ */
463
+ declare function getGuestAuthor(): GuestAuthor | null;
464
+ /**
465
+ * Persist a guest author name to localStorage.
466
+ * Silently fails if localStorage is unavailable.
467
+ */
468
+ declare function saveGuestAuthor(name: string): void;
469
+
470
+ /**
471
+ * Resolve the author for a comment based on whether the developer
472
+ * provided a user identity (identified mode) or not (guest mode).
473
+ *
474
+ * In identified mode, returns `config.user`.
475
+ * In guest mode, returns a guest author from the name field,
476
+ * or ANONYMOUS_AUTHOR if the name is blank.
477
+ */
478
+ declare function resolveAuthor(config: LayConfig, guestName: string): Author;
479
+ /**
480
+ * Persist the guest name to localStorage if non-empty.
481
+ * No-op in identified mode — the caller decides when to call this.
482
+ */
483
+ declare function persistGuestName(name: string): void;
484
+
485
+ declare function createMemoryAdapter(): LayAdapter;
486
+
487
+ interface HostedAdapterOptions {
488
+ apiUrl?: string;
489
+ ai?: boolean;
490
+ }
491
+ declare function createHostedAdapter(options?: string | HostedAdapterOptions): LayAdapter;
492
+
493
+ export { type AIContext, AIContextCard, type AIContextReview, type AIContextSupport, type AdapterOptions, ArchivedThreadsPanel, type Author, type Comment, CommentActions, CommentAnchor, CommentDot, CommentDots, type CommentEvent, type CommentEventType, type CommentGroup, CommentItem, CommentLayer, type CommentStatus, CommentThread, type CommentUpdate, DetachedCommentsPanel, type ElementBounds, type ElementFingerprint, ElementHighlighter, type ElementMetadata, type LayAdapter, type LayConfig, type LayContextValue, LayProvider, LayToggle, type NewComment, type ProjectMode, type RemoteConfig, ReplyInput, type ResolveMethod, type ResolveResult, type StarterChip, type ThreadGroup, type ThreadItem, type Unsubscribe, captureElementMetadata, computeContrastRatio, createHostedAdapter, createMemoryAdapter, findByFingerprint, formatRelativeTime, generateDomPath, generateFingerprint, getGuestAuthor, isAIContextReview, isAIContextSupport, isCommentable, isGeneratedClassName, parseUserAgent, persistGuestName, resolveAuthor, resolveEffectiveBackground, resolveElement, saveGuestAuthor, scoreFingerprintMatch, summarizeDomPath, useCommentMode, useComments, useElementSelector, useLayContext };