@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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/index.d.mts +493 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/index.d.mts +3 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2 -0
- package/dist/server/index.mjs.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/LayProvider.tsx","../src/adapters/memory.ts","../src/adapters/hosted.ts","../src/lib/constants.ts","../src/lib/session.ts","../src/LayToggle.tsx","../src/hooks/useCommentMode.ts","../src/hooks/useLayContext.ts","../src/hooks/useComments.ts","../src/ArchivedThreadsPanel.tsx","../src/DetachedCommentsPanel.tsx","../src/utils/time.ts","../src/utils/humanizePath.ts","../src/hooks/useElementSelector.ts","../src/utils/domPath.ts","../src/utils/elementFingerprint.ts","../src/utils/computedStyles.ts","../src/utils/magneticTarget.ts","../src/ElementHighlighter.tsx","../src/ElementBreadcrumb.tsx","../src/CommentAnchor.tsx","../src/lib/guestAuthor.ts","../src/lib/authorResolver.ts","../src/utils/captureScreenshot.ts","../src/CommentLayer.tsx","../src/CommentDots.tsx","../src/CommentDot.tsx","../src/CommentThread.tsx","../src/CommentItem.tsx","../src/CommentActions.tsx","../src/ReplyInput.tsx","../src/utils/resolveElement.ts","../src/AIContextCard.tsx","../src/types/comment.ts"],"sourcesContent":["// Components\nexport { LayProvider } from './LayProvider';\nexport { LayToggle } from './LayToggle';\nexport { ElementHighlighter } from './ElementHighlighter';\nexport { CommentAnchor } from './CommentAnchor';\nexport { CommentLayer } from './CommentLayer';\nexport { CommentDot } from './CommentDot';\nexport { CommentDots } from './CommentDots';\nexport { CommentThread } from './CommentThread';\nexport { CommentItem } from './CommentItem';\nexport { CommentActions } from './CommentActions';\nexport { ArchivedThreadsPanel } from './ArchivedThreadsPanel';\nexport { DetachedCommentsPanel } from './DetachedCommentsPanel';\nexport { AIContextCard } from './AIContextCard';\nexport { ReplyInput } from './ReplyInput';\n\n// Hooks\nexport { useLayContext } from './hooks/useLayContext';\nexport { useCommentMode } from './hooks/useCommentMode';\nexport { useElementSelector } from './hooks/useElementSelector';\nexport { useComments } from './hooks/useComments';\n\n// Utils\nexport { generateDomPath, isCommentable, isGeneratedClassName } from './utils/domPath';\nexport { generateFingerprint, scoreFingerprintMatch, findByFingerprint } from './utils/elementFingerprint';\nexport { resolveElement } from './utils/resolveElement';\nexport type { ResolveMethod, ResolveResult } from './utils/resolveElement';\nexport { summarizeDomPath } from './utils/humanizePath';\nexport { formatRelativeTime } from './utils/time';\nexport { captureElementMetadata, computeContrastRatio, resolveEffectiveBackground, parseUserAgent } from './utils/computedStyles';\nexport { getGuestAuthor, saveGuestAuthor } from './lib/guestAuthor';\nexport { resolveAuthor, persistGuestName } from './lib/authorResolver';\n\n// Adapters\nexport { createMemoryAdapter } from './adapters/memory';\nexport { createHostedAdapter } from './adapters/hosted';\n\n// Types\nexport type { Comment, NewComment, Author, CommentStatus, ElementMetadata, ElementFingerprint, ElementBounds, AIContext, AIContextReview, AIContextSupport } from './types/comment';\nexport { isAIContextReview, isAIContextSupport } from './types/comment';\nexport type { LayConfig, ProjectMode, RemoteConfig, StarterChip } from './types/config';\nexport type { LayAdapter, AdapterOptions, CommentUpdate, CommentEvent, CommentEventType, Unsubscribe } from './adapters/types';\nexport type { LayContextValue } from './LayProvider';\nexport type { CommentGroup, ThreadItem, ThreadGroup } from './hooks/useComments';\n","'use client';\n\nimport React, {\n createContext,\n useReducer,\n useRef,\n useEffect,\n useState,\n useCallback,\n type ReactNode,\n} from 'react';\nimport type { Comment } from './types/comment';\nimport type { LayConfig } from './types/config';\nimport type { ProjectMode } from './types/config';\nimport type { LayAdapter, AdapterOptions, CommentEvent } from './adapters/types';\nimport { createMemoryAdapter } from './adapters/memory';\nimport { createHostedAdapter } from './adapters/hosted';\nimport { DATA_ATTRS } from './lib/constants';\nimport { getSessionToken, setSessionToken, clearSessionToken } from './lib/session';\nimport { LayToggle } from './LayToggle';\nimport { CommentLayer } from './CommentLayer';\nimport { CommentDots } from './CommentDots';\n\n// --- State ---\n\ninterface LayState {\n isCommentMode: boolean;\n comments: Comment[];\n}\n\ntype LayAction =\n | { type: 'TOGGLE_COMMENT_MODE' }\n | { type: 'SET_COMMENT_MODE'; payload: boolean }\n | { type: 'SET_COMMENTS'; payload: Comment[] }\n | { type: 'ADD_COMMENT'; payload: Comment }\n | { type: 'UPDATE_COMMENT'; payload: Comment };\n\nfunction layReducer(\n state: LayState,\n action: LayAction\n): LayState {\n switch (action.type) {\n case 'TOGGLE_COMMENT_MODE':\n return { ...state, isCommentMode: !state.isCommentMode };\n case 'SET_COMMENT_MODE':\n return { ...state, isCommentMode: action.payload };\n case 'SET_COMMENTS':\n return { ...state, comments: action.payload };\n case 'ADD_COMMENT':\n return { ...state, comments: [...state.comments, action.payload] };\n case 'UPDATE_COMMENT':\n return {\n ...state,\n comments: state.comments.map((c) =>\n c.id === action.payload.id ? action.payload : c\n ),\n };\n default:\n return state;\n }\n}\n\nconst initialState: LayState = {\n isCommentMode: false,\n comments: [],\n};\n\n// --- Context ---\n\nexport interface LayContextValue {\n isCommentMode: boolean;\n comments: Comment[];\n adapter: LayAdapter;\n config: LayConfig;\n dispatch: React.Dispatch<LayAction>;\n portalRoot: HTMLDivElement | null;\n mode: ProjectMode;\n remoteConfigLoaded: boolean;\n detachedDomPaths: Set<string>;\n setDetachedDomPaths: React.Dispatch<React.SetStateAction<Set<string>>>;\n}\n\nexport const LayContext = createContext<LayContextValue | null>(null);\n\n// --- Styles injection ---\n\nconst TOKENS_CSS = `[data-fl-root]{--fl-accent:#E8611A;--fl-accent-hover:#C85215;--fl-accent-subtle:rgba(232,97,26,0.1);--fl-accent-glow:rgba(232,97,26,0.2);--fl-surface:#FAFAF7;--fl-surface-raised:#FFFFFF;--fl-border:#E8E5DF;--fl-border-strong:#D4CFC6;--fl-text-primary:#1A1A18;--fl-text-secondary:#6B6860;--fl-text-tertiary:#9C978E;--fl-resolved:#9C978E;--fl-z-base:100000;--fl-duration-instant:80ms;--fl-duration-fast:120ms;--fl-duration-normal:150ms;--fl-duration-marker-enter:280ms;--fl-duration-marker-ripple:400ms;--fl-duration-resolve-burst:450ms;--fl-marker-height:22px;--fl-marker-min-width:20px;--fl-marker-pointer-size:4px;--fl-marker-font-size:11px;--fl-toggle-size:36px;--fl-border-radius:8px;--fl-border-radius-sm:6px;--fl-shadow:0 2px 8px rgba(26,26,24,0.08),0 1px 2px rgba(26,26,24,0.04);--fl-shadow-lg:0 4px 16px rgba(26,26,24,0.12),0 2px 4px rgba(26,26,24,0.06);--fl-shadow-popover:0 8px 30px rgba(26,26,24,0.16),0 2px 6px rgba(26,26,24,0.08);--fl-font-family:'Instrument Sans',-apple-system,BlinkMacSystemFont,sans-serif;--fl-font-size-sm:12px;--fl-font-size-base:14px;--fl-font-size-lg:16px;--fl-line-height:1.5;--fl-font-weight-normal:400;--fl-font-weight-medium:500;font-family:var(--fl-font-family);font-size:var(--fl-font-size-base);line-height:var(--fl-line-height);color:var(--fl-text-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}`;\n\nconst ANIMATIONS_CSS = `@keyframes fl-marker-enter{0%{opacity:0;transform:scale(0.4)}50%{opacity:1;transform:scale(1.08)}72%{transform:scale(0.97)}100%{transform:scale(1.0)}}@keyframes fl-marker-ripple{0%{transform:translate(-50%,-50%) scale(0.6);opacity:0.35}100%{transform:translate(-50%,-50%) scale(2.4);opacity:0}}@keyframes fl-resolve-particle{0%{opacity:0.9;transform:translate(var(--fl-dx),var(--fl-dy)) scale(1)}100%{opacity:0;transform:translate(var(--fl-end-dx),var(--fl-end-dy)) scale(0.3)}}@keyframes fl-toggle-beam{0%{box-shadow:0 2px 8px rgba(26,26,24,0.08),0 1px 2px rgba(26,26,24,0.04),0 0 0 0 rgba(232,97,26,0.25)}70%{box-shadow:0 2px 8px rgba(26,26,24,0.08),0 1px 2px rgba(26,26,24,0.04),0 0 0 10px transparent}100%{box-shadow:0 2px 8px rgba(26,26,24,0.08),0 1px 2px rgba(26,26,24,0.04),0 0 0 0 transparent}}`;\n\nconst FONT_CSS = `@font-face{font-family:'Instrument Sans';font-style:normal;font-weight:400;font-display:swap;src:url('https://fonts.gstatic.com/s/instrumentsans/v1/pximypc9vsFDm051Uf6KVwgkfoSxQ0GsQv8To18.woff2') format('woff2')}@font-face{font-family:'Instrument Sans';font-style:normal;font-weight:500;font-display:swap;src:url('https://fonts.gstatic.com/s/instrumentsans/v1/pximypc9vsFDm051Uf6KVwgkfoSxQ0GsQv8To18.woff2') format('woff2')}@font-face{font-family:'Instrument Sans';font-style:normal;font-weight:600;font-display:swap;src:url('https://fonts.gstatic.com/s/instrumentsans/v1/pximypc9vsFDm051Uf6KVwgkfoSxQ0GsQv8To18.woff2') format('woff2')}`;\n\n// --- Session-aware adapter wrapper ---\n\nfunction wrapAdapterWithSession(\n adapter: LayAdapter,\n getToken: () => string | null\n): LayAdapter {\n function opts(): AdapterOptions | undefined {\n const token = getToken();\n return token ? { sessionToken: token } : undefined;\n }\n\n return {\n getConfig: (projectId) => adapter.getConfig(projectId),\n getComments: (projectId, urlPath) =>\n adapter.getComments(projectId, urlPath, opts()),\n addComment: (comment) => adapter.addComment(comment, opts()),\n updateComment: (id, update) =>\n adapter.updateComment(id, update, opts()),\n uploadScreenshot: (projectId, commentId, blob, bounds) =>\n adapter.uploadScreenshot(projectId, commentId, blob, bounds, opts()),\n subscribe: (projectId, callback) =>\n adapter.subscribe(projectId, callback, opts()),\n };\n}\n\n// --- Session management ---\n\nconst LOG_PREFIX = 'Lay:';\n\ninterface SessionResponse {\n token: string;\n authorId: string;\n expiresAt: string;\n}\n\nasync function requestSession(\n baseUrl: string,\n projectId: string,\n user?: { id: string | null; name?: string | null } | null,\n userHash?: string\n): Promise<SessionResponse> {\n // Identified session (HMAC-verified)\n if (user?.id && userHash) {\n const res = await fetch(`${baseUrl}/api/v1/sessions/identify`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n projectId,\n userId: user.id,\n userHash,\n userName: user.name ?? null,\n }),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n throw new Error((body as { error?: string }).error ?? `Session error: ${res.status}`);\n }\n return res.json() as Promise<SessionResponse>;\n }\n\n // Anonymous session\n const res = await fetch(`${baseUrl}/api/v1/sessions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ projectId }),\n });\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n throw new Error((body as { error?: string }).error ?? `Session error: ${res.status}`);\n }\n return res.json() as Promise<SessionResponse>;\n}\n\n// --- Provider ---\n\ninterface LayProviderProps extends LayConfig {\n children: ReactNode;\n}\n\nfunction resolveAdapter(\n customAdapter: LayAdapter | undefined,\n projectId: string,\n apiUrl: string | undefined,\n ai: boolean | undefined\n): LayAdapter {\n if (customAdapter) return customAdapter;\n if (apiUrl) return createHostedAdapter({ apiUrl, ai });\n return createMemoryAdapter();\n}\n\nfunction resolveBaseUrl(apiUrl?: string): string {\n return (apiUrl ?? 'https://uselay.com').replace(/\\/$/, '');\n}\n\nexport function LayProvider({\n children,\n projectId,\n user,\n userHash,\n adapter: customAdapter,\n apiUrl,\n version,\n ai,\n mode: modeProp,\n active: activeProp,\n starterChips,\n screenshots,\n}: LayProviderProps) {\n const [state, dispatch] = useReducer(layReducer, initialState);\n const rawAdapterRef = useRef<LayAdapter>(\n resolveAdapter(customAdapter, projectId, apiUrl, ai)\n );\n const [portalRoot, setPortalRoot] = useState<HTMLDivElement | null>(null);\n\n // Session token state\n // Detached dom_paths (set by CommentDots when element resolution fails)\n const [detachedDomPaths, setDetachedDomPaths] = useState<Set<string>>(new Set());\n\n const [sessionToken, setSessionTokenState] = useState<string | null>(null);\n const sessionTokenRef = useRef<string | null>(null);\n const [sessionReady, setSessionReady] = useState(false);\n\n // Keep ref in sync with state (ref is used by the adapter wrapper for synchronous access)\n const updateSessionToken = useCallback((token: string | null) => {\n sessionTokenRef.current = token;\n setSessionTokenState(token);\n if (token) {\n setSessionToken(projectId, token);\n } else {\n clearSessionToken(projectId);\n }\n }, [projectId]);\n\n // Wrapped adapter that automatically injects session token\n const wrappedAdapterRef = useRef<LayAdapter>(\n wrapAdapterWithSession(rawAdapterRef.current, () => sessionTokenRef.current)\n );\n\n // Remote config state — defaults used until fetch resolves\n const [remoteMode, setRemoteMode] = useState<ProjectMode>('review');\n const [remoteActive, setRemoteActive] = useState(true);\n const [remoteConfigLoaded, setRemoteConfigLoaded] = useState(false);\n\n // Resolved values: prop > remote > default\n const resolvedMode: ProjectMode = modeProp ?? remoteMode;\n const resolvedActive = activeProp ?? remoteActive;\n\n // Fetch remote config on mount\n useEffect(() => {\n rawAdapterRef.current.getConfig(projectId).then(\n (config) => {\n setRemoteMode(config.mode);\n setRemoteActive(config.active);\n setRemoteConfigLoaded(true);\n },\n () => {\n // Config fetch failed — fall back to defaults.\n // Widget still works with mode: 'review', active: true.\n setRemoteConfigLoaded(true);\n }\n );\n }, [projectId]);\n\n // Session management: acquire session in support mode\n useEffect(() => {\n // Only support mode needs sessions\n if (resolvedMode !== 'support') {\n setSessionReady(true);\n return;\n }\n\n // Wait for remote config to load before deciding on session\n if (!remoteConfigLoaded) return;\n\n // Warn if user provided without userHash\n if (user?.id && !userHash) {\n console.warn(\n `${LOG_PREFIX} user.id provided without userHash in support mode. ` +\n 'Comments will be anonymous. Pass userHash from createUserHash() for verified identity.'\n );\n }\n\n // Check localStorage for existing token\n const existingToken = getSessionToken(projectId);\n if (existingToken) {\n updateSessionToken(existingToken);\n setSessionReady(true);\n return;\n }\n\n // Request new session\n const baseUrl = resolveBaseUrl(apiUrl);\n requestSession(baseUrl, projectId, user, userHash).then(\n (session) => {\n updateSessionToken(session.token);\n setSessionReady(true);\n },\n (err) => {\n console.warn(`${LOG_PREFIX} Failed to create session:`, err);\n // Widget still renders — just without session. API calls will fail with 401.\n setSessionReady(true);\n }\n );\n }, [projectId, resolvedMode, remoteConfigLoaded, user, userHash, apiUrl, updateSessionToken]);\n\n // Create portal root and inject styles\n useEffect(() => {\n // Create portal container\n const root = document.createElement('div');\n root.setAttribute(DATA_ATTRS.ROOT, '');\n root.setAttribute(DATA_ATTRS.IGNORE, '');\n document.body.appendChild(root);\n\n // Inject styles into portal root\n const styleEl = document.createElement('style');\n styleEl.textContent = FONT_CSS + TOKENS_CSS + ANIMATIONS_CSS;\n root.appendChild(styleEl);\n\n setPortalRoot(root);\n\n return () => {\n document.body.removeChild(root);\n };\n }, []);\n\n // Load initial comments (wait for session in support mode)\n useEffect(() => {\n if (!sessionReady) return;\n\n const urlPath = window.location.pathname;\n wrappedAdapterRef.current.getComments(projectId, urlPath).then(\n (comments) => {\n dispatch({ type: 'SET_COMMENTS', payload: comments });\n },\n (err) => {\n // Handle 401 — session may have expired\n if (err instanceof Error && err.message.includes('401')) {\n // Clear expired token and re-acquire\n updateSessionToken(null);\n setSessionReady(false);\n return;\n }\n // Adapter already logs specific warnings (403/404/network).\n // Comments array stays empty — widget still renders.\n }\n );\n }, [projectId, sessionReady, updateSessionToken]);\n\n // Subscribe to comment events (inserts and updates)\n useEffect(() => {\n if (!sessionReady) return;\n\n let unsubscribe: (() => void) | undefined;\n try {\n unsubscribe = wrappedAdapterRef.current.subscribe(projectId, (event: CommentEvent) => {\n switch (event.type) {\n case 'INSERT':\n dispatch({ type: 'ADD_COMMENT', payload: event.comment });\n break;\n case 'UPDATE':\n dispatch({ type: 'UPDATE_COMMENT', payload: event.comment });\n break;\n }\n });\n } catch {\n // Subscribe may fail if EventSource constructor throws (e.g. invalid URL).\n // Widget still works — just no live updates.\n }\n return () => unsubscribe?.();\n }, [projectId, sessionReady]);\n\n const config: LayConfig = { projectId, user, userHash, apiUrl, version, ai, mode: modeProp, active: activeProp, starterChips, screenshots };\n\n // Gate feedback UI: wait for config to load, session to be ready, and render nothing when inactive.\n const showFeedbackUI = portalRoot && remoteConfigLoaded && sessionReady && resolvedActive;\n\n const contextValue: LayContextValue = {\n isCommentMode: state.isCommentMode,\n comments: state.comments,\n adapter: wrappedAdapterRef.current,\n config,\n dispatch,\n portalRoot,\n mode: resolvedMode,\n remoteConfigLoaded,\n detachedDomPaths,\n setDetachedDomPaths,\n };\n\n return (\n <LayContext.Provider value={contextValue}>\n {children}\n {showFeedbackUI && <LayToggle />}\n {showFeedbackUI && <CommentLayer />}\n {showFeedbackUI && <CommentDots />}\n </LayContext.Provider>\n );\n}\n","import type { Comment, NewComment } from '../types/comment';\nimport type { RemoteConfig } from '../types/config';\nimport type { LayAdapter, AdapterOptions, CommentUpdate, CommentEvent, Unsubscribe } from './types';\n\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n}\n\nexport function createMemoryAdapter(): LayAdapter {\n const comments: Comment[] = [];\n const listeners: Set<(event: CommentEvent) => void> = new Set();\n\n function emit(event: CommentEvent): void {\n listeners.forEach((cb) => cb(event));\n }\n\n return {\n async getConfig(): Promise<RemoteConfig> {\n return { mode: 'review', active: true };\n },\n\n async getComments(projectId: string, urlPath: string, options?: AdapterOptions): Promise<Comment[]> {\n let filtered = comments.filter(\n (c) => c.project_id === projectId && c.url_path === urlPath\n );\n // In support mode (indicated by sessionToken presence), only return this author's comments.\n // The sessionToken doubles as the author_id filter in the memory adapter.\n if (options?.sessionToken) {\n filtered = filtered.filter((c) => c.author.id === options.sessionToken);\n }\n return filtered;\n },\n\n async addComment(newComment: NewComment): Promise<Comment> {\n const now = new Date().toISOString();\n const comment: Comment = {\n ...newComment,\n id: generateId(),\n created_at: now,\n updated_at: now,\n };\n comments.push(comment);\n emit({ type: 'INSERT', comment });\n return comment;\n },\n\n async updateComment(id: string, update: CommentUpdate): Promise<Comment> {\n const index = comments.findIndex((c) => c.id === id);\n if (index === -1) {\n throw new Error(`Comment not found: ${id}`);\n }\n const updated: Comment = {\n ...comments[index],\n ...update,\n updated_at: new Date().toISOString(),\n };\n comments[index] = updated;\n emit({ type: 'UPDATE', comment: updated });\n return updated;\n },\n\n async uploadScreenshot(): Promise<void> {\n // No-op in memory adapter\n },\n\n subscribe(\n _projectId: string,\n callback: (event: CommentEvent) => void\n ): Unsubscribe {\n listeners.add(callback);\n return () => {\n listeners.delete(callback);\n };\n },\n };\n}\n","import type { Comment, NewComment, ElementBounds } from '../types/comment';\nimport type { RemoteConfig } from '../types/config';\nimport type {\n LayAdapter,\n AdapterOptions,\n CommentUpdate,\n CommentEvent,\n Unsubscribe,\n} from './types';\n\nconst DEFAULT_API_URL = 'https://uselay.com';\nconst LOG_PREFIX = 'Lay:';\n\nfunction warn(message: string): void {\n console.warn(`${LOG_PREFIX} ${message}`);\n}\n\ninterface HostedAdapterOptions {\n apiUrl?: string;\n ai?: boolean;\n}\n\nexport function createHostedAdapter(options?: string | HostedAdapterOptions): LayAdapter {\n // Support legacy string-only signature: createHostedAdapter('http://...')\n const opts: HostedAdapterOptions =\n typeof options === 'string' ? { apiUrl: options } : options ?? {};\n const baseUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\\/$/, '');\n const aiEnabled = opts.ai !== false;\n\n function authHeaders(token?: string): Record<string, string> {\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function request<T>(\n path: string,\n fetchOptions?: RequestInit,\n sessionToken?: string\n ): Promise<T> {\n let res: Response;\n try {\n res = await fetch(`${baseUrl}${path}`, {\n ...fetchOptions,\n headers: {\n 'Content-Type': 'application/json',\n ...authHeaders(sessionToken),\n ...(fetchOptions?.headers ?? {}),\n },\n });\n } catch {\n warn('Unable to reach API. Comments won\\'t persist.');\n throw new Error('Network error');\n }\n\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n const message =\n (body as { error?: string }).error ?? `API error: ${res.status}`;\n\n if (res.status === 403) {\n warn(\n `This domain is not authorized for this project.`\n );\n } else if (res.status === 404) {\n warn('Invalid project ID. Comments won\\'t persist.');\n }\n\n throw new Error(message);\n }\n\n return res.json() as Promise<T>;\n }\n\n return {\n async getConfig(projectId: string): Promise<RemoteConfig> {\n const params = new URLSearchParams({ projectId });\n return request<RemoteConfig>(\n `/api/v1/config?${params.toString()}`\n );\n },\n\n async getComments(\n projectId: string,\n urlPath: string,\n options?: AdapterOptions\n ): Promise<Comment[]> {\n const params = new URLSearchParams({ projectId, urlPath });\n return request<Comment[]>(\n `/api/v1/comments?${params.toString()}`,\n undefined,\n options?.sessionToken\n );\n },\n\n async addComment(comment: NewComment, options?: AdapterOptions): Promise<Comment> {\n return request<Comment>('/api/v1/comments', {\n method: 'POST',\n body: JSON.stringify(comment),\n ...(!aiEnabled ? { headers: { 'X-FL-AI': 'false' } } : {}),\n }, options?.sessionToken);\n },\n\n async updateComment(\n id: string,\n update: CommentUpdate,\n options?: AdapterOptions\n ): Promise<Comment> {\n return request<Comment>(`/api/v1/comments/${id}`, {\n method: 'PATCH',\n body: JSON.stringify(update),\n }, options?.sessionToken);\n },\n\n async uploadScreenshot(\n projectId: string,\n commentId: string,\n blob: Blob,\n bounds: ElementBounds,\n options?: AdapterOptions\n ): Promise<void> {\n const formData = new FormData();\n formData.append('file', blob, `${commentId}.webp`);\n formData.append('projectId', projectId);\n formData.append('commentId', commentId);\n formData.append('elementBounds', JSON.stringify(bounds));\n\n const headers: Record<string, string> = {};\n if (options?.sessionToken) {\n headers['Authorization'] = `Bearer ${options.sessionToken}`;\n }\n\n let res: Response;\n try {\n res = await fetch(`${baseUrl}/api/v1/screenshots`, {\n method: 'POST',\n headers,\n body: formData,\n });\n } catch {\n // Silent failure — screenshot is best-effort\n return;\n }\n\n if (!res.ok) {\n // Silent failure\n return;\n }\n },\n\n subscribe(\n projectId: string,\n callback: (event: CommentEvent) => void,\n options?: AdapterOptions\n ): Unsubscribe {\n const params = new URLSearchParams({ projectId });\n if (options?.sessionToken) {\n params.set('token', options.sessionToken);\n }\n const url = `${baseUrl}/api/v1/stream?${params.toString()}`;\n const es = new EventSource(url);\n\n es.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data.type === 'INSERT' || data.type === 'UPDATE') {\n callback(data as CommentEvent);\n }\n } catch {\n // Ignore malformed messages\n }\n };\n\n es.onerror = () => {\n // EventSource auto-reconnects automatically.\n // No warning here — reconnection is expected behavior.\n };\n\n return () => {\n es.close();\n };\n },\n };\n}\n","/** Z-index base for all feedback layer elements */\nexport const Z_INDEX = {\n BASE: 100000,\n HIGHLIGHT: 100001,\n DOT: 100002,\n POPOVER: 100003,\n TOGGLE: 100004,\n} as const;\n\n/** Animation durations in ms */\nexport const ANIMATION = {\n INSTANT: 80,\n FAST: 120,\n NORMAL: 150,\n MARKER_ENTER: 280,\n MARKER_RIPPLE: 400,\n} as const;\n\n/** Resolve micro-confetti burst */\nexport const CONFETTI = {\n PARTICLE_COUNT: 5,\n PARTICLE_SIZE_MIN: 3,\n PARTICLE_SIZE_MAX: 4,\n DURATION: 450,\n SPREAD_ANGLE: 120,\n DISTANCE_MIN: 14,\n DISTANCE_MAX: 28,\n} as const;\n\n/** Sizing values in px */\nexport const SIZING = {\n MARKER_HEIGHT: 22,\n MARKER_MIN_WIDTH: 20,\n MARKER_POINTER: 4,\n MARKER_FONT_SIZE: 11,\n MARKER_MAX_COUNT: 99,\n TOGGLE_SIZE: 36,\n BORDER_RADIUS: 8,\n BORDER_RADIUS_SM: 6,\n HIGHLIGHT_OUTLINE: 2,\n} as const;\n\n/** Keyboard shortcuts */\nexport const SHORTCUTS = {\n TOGGLE_COMMENT_MODE: 'c',\n} as const;\n\n/** Data attributes used by the feedback layer */\nexport const DATA_ATTRS = {\n ROOT: 'data-fl-root',\n IGNORE: 'data-fl-ignore',\n} as const;\n\n/** Magnetic anchor resolution */\nexport const MAGNETIC = {\n ANCESTOR_DEPTH: 4,\n PROXIMITY_THRESHOLD: 30,\n} as const;\n\n/** Element breadcrumb label */\nexport const BREADCRUMB = {\n MAX_SEGMENTS: 3,\n MAX_LENGTH: 50,\n} as const;\n\n/** Two-Speed Composer sizing */\nexport const COMPOSER = {\n WIDTH: 320,\n COLLAPSED_HEIGHT: 40,\n GAP: 8,\n EXPAND_DURATION: 150,\n} as const;\n\n/** Default starter chips for quick feedback */\nexport const DEFAULT_STARTER_CHIPS: readonly { label: string; value: string }[] = [\n { label: 'Visual bug', value: 'Visual bug' },\n { label: 'Copy issue', value: 'Copy issue' },\n { label: 'Love this', value: 'Love this' },\n] as const;\n\n/** Thread Whisper tooltip */\nexport const WHISPER = {\n HOVER_DELAY: 300,\n MAX_CONTENT_LENGTH: 60,\n} as const;\n\n/** Default anonymous author */\nexport const ANONYMOUS_AUTHOR = {\n id: null,\n name: 'Anonymous',\n avatar: null,\n} as const;\n","const STORAGE_PREFIX = 'lay_session_';\n\n/** Get stored session token for a project */\nexport function getSessionToken(projectId: string): string | null {\n try {\n return localStorage.getItem(`${STORAGE_PREFIX}${projectId}`);\n } catch {\n // localStorage may be unavailable (SSR, iframe sandbox, etc.)\n return null;\n }\n}\n\n/** Store session token for a project */\nexport function setSessionToken(projectId: string, token: string): void {\n try {\n localStorage.setItem(`${STORAGE_PREFIX}${projectId}`, token);\n } catch {\n // localStorage may be unavailable\n }\n}\n\n/** Clear stored session token for a project */\nexport function clearSessionToken(projectId: string): void {\n try {\n localStorage.removeItem(`${STORAGE_PREFIX}${projectId}`);\n } catch {\n // localStorage may be unavailable\n }\n}\n","'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useCommentMode } from './hooks/useCommentMode';\nimport { useComments } from './hooks/useComments';\nimport { useLayContext } from './hooks/useLayContext';\nimport { ArchivedThreadsPanel } from './ArchivedThreadsPanel';\nimport { DetachedCommentsPanel } from './DetachedCommentsPanel';\nimport { Z_INDEX, DATA_ATTRS } from './lib/constants';\n\nconst toggleStyles: React.CSSProperties = {\n position: 'fixed',\n right: 16,\n top: '50%',\n transform: 'translateY(-50%)',\n width: 'var(--fl-toggle-size)',\n height: 'var(--fl-toggle-size)',\n borderRadius: '50%',\n borderWidth: 1.5,\n borderStyle: 'solid',\n borderColor: 'var(--fl-border)',\n backgroundColor: 'var(--fl-surface-raised)',\n color: 'var(--fl-text-secondary)',\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: Z_INDEX.TOGGLE,\n boxShadow: 'var(--fl-shadow)',\n animation: 'fl-toggle-beam 2s ease-out 1s infinite',\n transition: `all var(--fl-duration-normal) ease-in-out`,\n padding: 0,\n outline: 'none',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst toggleHoverStyles: React.CSSProperties = {\n ...toggleStyles,\n transform: 'translateY(-50%) translateX(-4px)',\n borderColor: 'var(--fl-border-strong)',\n color: 'var(--fl-text-primary)',\n boxShadow: 'var(--fl-shadow-lg)',\n};\n\nconst toggleActiveStyles: React.CSSProperties = {\n ...toggleStyles,\n backgroundColor: 'var(--fl-accent)',\n borderColor: 'var(--fl-accent)',\n color: '#FFFFFF',\n boxShadow: 'var(--fl-shadow-lg)',\n animation: 'none',\n};\n\nconst toggleActiveHoverStyles: React.CSSProperties = {\n ...toggleActiveStyles,\n transform: 'translateY(-50%) translateX(-4px)',\n};\n\nconst tooltipStyles: React.CSSProperties = {\n position: 'absolute',\n right: '100%',\n top: '50%',\n transform: 'translateY(-50%)',\n marginRight: 8,\n padding: '4px 8px',\n backgroundColor: 'var(--fl-text-primary)',\n color: 'var(--fl-surface)',\n fontSize: 'var(--fl-font-size-sm)',\n fontFamily: 'var(--fl-font-family)',\n fontWeight: 'var(--fl-font-weight-medium)',\n borderRadius: 'var(--fl-border-radius-sm)',\n whiteSpace: 'nowrap',\n pointerEvents: 'none',\n opacity: 0,\n transition: `opacity var(--fl-duration-fast) ease-out`,\n};\n\nconst tooltipVisibleStyles: React.CSSProperties = {\n ...tooltipStyles,\n opacity: 1,\n};\n\nconst chipStyles: React.CSSProperties = {\n position: 'absolute',\n right: '100%',\n top: '100%',\n marginRight: 8,\n marginTop: 4,\n padding: '3px 8px',\n backgroundColor: 'var(--fl-surface-raised)',\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: 'var(--fl-border)',\n borderRadius: 10,\n fontSize: 'var(--fl-font-size-sm)',\n fontFamily: 'var(--fl-font-family)',\n fontWeight: 'var(--fl-font-weight-medium)',\n color: 'var(--fl-text-secondary)',\n whiteSpace: 'nowrap',\n cursor: 'pointer',\n lineHeight: 1.3,\n transition: `color var(--fl-duration-fast) ease-out, border-color var(--fl-duration-fast) ease-out`,\n};\n\nconst chipHoverStyles: React.CSSProperties = {\n ...chipStyles,\n color: 'var(--fl-text-primary)',\n borderColor: 'var(--fl-border-strong)',\n};\n\nfunction ScanIcon({ active }: { active: boolean }) {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={active ? '#FFFFFF' : 'currentColor'}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 7V5a2 2 0 0 1 2-2h2\" />\n <path d=\"M17 3h2a2 2 0 0 1 2 2v2\" />\n <path d=\"M21 17v2a2 2 0 0 1-2 2h-2\" />\n <path d=\"M7 21H5a2 2 0 0 1-2-2v-2\" />\n </svg>\n );\n}\n\nexport function LayToggle() {\n const { isCommentMode, toggleCommentMode } = useCommentMode();\n const { portalRoot, mode } = useLayContext();\n const { archivedCount, detachedCount } = useComments();\n const showChips = mode !== 'support';\n const [isHovered, setIsHovered] = useState(false);\n const [chipHovered, setChipHovered] = useState(false);\n const [detachedChipHovered, setDetachedChipHovered] = useState(false);\n const [isArchivedPanelOpen, setIsArchivedPanelOpen] = useState(false);\n const [isDetachedPanelOpen, setIsDetachedPanelOpen] = useState(false);\n\n const handleChipClick = useCallback((e: React.MouseEvent) => {\n e.stopPropagation();\n setIsArchivedPanelOpen((prev) => !prev);\n setIsDetachedPanelOpen(false);\n }, []);\n\n const handleDetachedChipClick = useCallback((e: React.MouseEvent) => {\n e.stopPropagation();\n setIsDetachedPanelOpen((prev) => !prev);\n setIsArchivedPanelOpen(false);\n }, []);\n\n const handleCloseArchivedPanel = useCallback(() => {\n setIsArchivedPanelOpen(false);\n }, []);\n\n const handleCloseDetachedPanel = useCallback(() => {\n setIsDetachedPanelOpen(false);\n }, []);\n\n if (!portalRoot) return null;\n\n let buttonStyle: React.CSSProperties;\n if (isCommentMode) {\n buttonStyle = isHovered ? toggleActiveHoverStyles : toggleActiveStyles;\n } else {\n buttonStyle = isHovered ? toggleHoverStyles : toggleStyles;\n }\n\n return createPortal(\n <>\n <button\n type=\"button\"\n onClick={toggleCommentMode}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n style={buttonStyle}\n aria-label={isCommentMode ? 'Exit comment mode' : 'Enter comment mode'}\n {...{ [DATA_ATTRS.IGNORE]: '' }}\n >\n <ScanIcon active={isCommentMode} />\n <span style={isHovered ? tooltipVisibleStyles : tooltipStyles}>\n Comment mode (C)\n </span>\n {showChips && archivedCount > 0 && (\n <span\n role=\"button\"\n tabIndex={0}\n style={chipHovered ? chipHoverStyles : chipStyles}\n onMouseEnter={() => setChipHovered(true)}\n onMouseLeave={() => setChipHovered(false)}\n onClick={handleChipClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleChipClick(e as unknown as React.MouseEvent);\n }\n }}\n >\n {archivedCount} archived\n </span>\n )}\n {showChips && detachedCount > 0 && (\n <span\n role=\"button\"\n tabIndex={0}\n style={{\n ...(detachedChipHovered ? chipHoverStyles : chipStyles),\n // Stack below archived chip if both visible\n top: archivedCount > 0 ? 'calc(100% + 28px)' : '100%',\n }}\n onMouseEnter={() => setDetachedChipHovered(true)}\n onMouseLeave={() => setDetachedChipHovered(false)}\n onClick={handleDetachedChipClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleDetachedChipClick(e as unknown as React.MouseEvent);\n }\n }}\n >\n {detachedCount} detached\n </span>\n )}\n </button>\n {showChips && isArchivedPanelOpen && (\n <ArchivedThreadsPanel onClose={handleCloseArchivedPanel} />\n )}\n {showChips && isDetachedPanelOpen && (\n <DetachedCommentsPanel onClose={handleCloseDetachedPanel} />\n )}\n </>,\n portalRoot\n );\n}\n","import { useCallback, useEffect } from 'react';\nimport { useLayContext } from './useLayContext';\nimport { SHORTCUTS } from '../lib/constants';\n\nexport function useCommentMode() {\n const { isCommentMode, dispatch } = useLayContext();\n\n const toggleCommentMode = useCallback(() => {\n dispatch({ type: 'TOGGLE_COMMENT_MODE' });\n }, [dispatch]);\n\n const setCommentMode = useCallback(\n (active: boolean) => {\n dispatch({ type: 'SET_COMMENT_MODE', payload: active });\n },\n [dispatch]\n );\n\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n // Don't trigger when typing in an input, textarea, or contenteditable\n const target = e.target as HTMLElement;\n if (\n target.tagName === 'INPUT' ||\n target.tagName === 'TEXTAREA' ||\n target.tagName === 'SELECT' ||\n target.isContentEditable\n ) {\n return;\n }\n\n if (e.key.toLowerCase() === SHORTCUTS.TOGGLE_COMMENT_MODE && !e.metaKey && !e.ctrlKey && !e.altKey) {\n e.preventDefault();\n toggleCommentMode();\n }\n }\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [toggleCommentMode]);\n\n return { isCommentMode, toggleCommentMode, setCommentMode };\n}\n","import { useContext } from 'react';\nimport { LayContext } from '../LayProvider';\nimport type { LayContextValue } from '../LayProvider';\n\nexport function useLayContext(): LayContextValue {\n const context = useContext(LayContext);\n if (!context) {\n throw new Error(\n 'useLayContext must be used within a <LayProvider />. ' +\n 'Wrap your app in <LayProvider> to use uselay.'\n );\n }\n return context;\n}\n","import { useMemo } from 'react';\nimport { useLayContext } from './useLayContext';\nimport type { Comment } from '../types/comment';\n\nexport interface CommentGroup {\n domPath: string;\n comments: Comment[];\n}\n\nexport interface ThreadItem {\n root: Comment;\n replies: Comment[];\n}\n\nexport interface ThreadGroup {\n domPath: string;\n threads: ThreadItem[];\n allComments: Comment[];\n}\n\n/**\n * Convenience hook that reads comments from context and provides\n * grouping/filtering helpers. State is owned by LayProvider.\n */\nexport function useComments() {\n const { comments, detachedDomPaths } = useLayContext();\n\n /** Comments grouped by dom_path, derived during render (no effect needed) */\n const groupedByDomPath = useMemo<CommentGroup[]>(() => {\n const map = new Map<string, Comment[]>();\n for (const comment of comments) {\n const existing = map.get(comment.dom_path);\n if (existing) {\n existing.push(comment);\n } else {\n map.set(comment.dom_path, [comment]);\n }\n }\n return Array.from(map, ([domPath, comments]) => ({ domPath, comments }));\n }, [comments]);\n\n /**\n * Thread-aware grouping: groups by dom_path, then within each group\n * organizes into root comments each with their sorted replies.\n */\n const threadsByDomPath = useMemo<ThreadGroup[]>(() => {\n const pathMap = new Map<string, Comment[]>();\n for (const comment of comments) {\n const existing = pathMap.get(comment.dom_path);\n if (existing) {\n existing.push(comment);\n } else {\n pathMap.set(comment.dom_path, [comment]);\n }\n }\n\n return Array.from(pathMap, ([domPath, allComments]) => {\n const roots: Comment[] = [];\n const repliesByThreadId = new Map<string, Comment[]>();\n\n for (const comment of allComments) {\n if (comment.thread_id === null) {\n roots.push(comment);\n } else {\n const existing = repliesByThreadId.get(comment.thread_id);\n if (existing) {\n existing.push(comment);\n } else {\n repliesByThreadId.set(comment.thread_id, [comment]);\n }\n }\n }\n\n roots.sort(\n (a, b) =>\n new Date(a.created_at).getTime() - new Date(b.created_at).getTime()\n );\n\n const threads: ThreadItem[] = roots.map((root) => {\n const replies = repliesByThreadId.get(root.id) ?? [];\n replies.sort(\n (a, b) =>\n new Date(a.created_at).getTime() -\n new Date(b.created_at).getTime()\n );\n return { root, replies };\n });\n\n return { domPath, threads, allComments };\n });\n }, [comments]);\n\n /**\n * Archived root threads (thread_id === null, status === 'archived')\n * for the current page, with their replies.\n */\n const archivedThreads = useMemo<ThreadItem[]>(() => {\n const roots = comments.filter(\n (c) => c.thread_id === null && c.status === 'archived'\n );\n return roots.map((root) => {\n const replies = comments\n .filter((c) => c.thread_id === root.id)\n .sort(\n (a, b) =>\n new Date(a.created_at).getTime() - new Date(b.created_at).getTime()\n );\n return { root, replies };\n });\n }, [comments]);\n\n /** Count of archived root threads on the current page */\n const archivedCount = archivedThreads.length;\n\n /**\n * Detached root threads — comments whose dom_path couldn't be resolved\n * to any element on the page (failed all 3 resolution layers).\n */\n const detachedThreads = useMemo<ThreadItem[]>(() => {\n if (detachedDomPaths.size === 0) return [];\n\n const detachedRoots = comments.filter(\n (c) => c.thread_id === null && detachedDomPaths.has(c.dom_path) && c.status !== 'archived'\n );\n return detachedRoots.map((root) => {\n const replies = comments\n .filter((c) => c.thread_id === root.id)\n .sort(\n (a, b) =>\n new Date(a.created_at).getTime() - new Date(b.created_at).getTime()\n );\n return { root, replies };\n });\n }, [comments, detachedDomPaths]);\n\n /** Count of detached root threads on the current page */\n const detachedCount = detachedThreads.length;\n\n /**\n * Check whether all root comments on a given dom_path are resolved or archived.\n * Returns true if every root is resolved (none are open). Used for dot muting.\n */\n const isDomPathFullyResolved = useMemo(() => {\n const map = new Map<string, boolean>();\n for (const comment of comments) {\n if (comment.thread_id !== null) continue; // only roots\n const current = map.get(comment.dom_path);\n if (current === false) continue; // already found an open root\n map.set(\n comment.dom_path,\n comment.status === 'resolved' || comment.status === 'archived'\n );\n }\n return (domPath: string) => map.get(domPath) ?? false;\n }, [comments]);\n\n /**\n * Check whether all root comments on a given dom_path are archived.\n * Returns true if every root is archived. Used to hide dots entirely.\n */\n const isDomPathFullyArchived = useMemo(() => {\n const map = new Map<string, boolean>();\n for (const comment of comments) {\n if (comment.thread_id !== null) continue;\n const current = map.get(comment.dom_path);\n if (current === false) continue;\n map.set(comment.dom_path, comment.status === 'archived');\n }\n return (domPath: string) => map.get(domPath) ?? false;\n }, [comments]);\n\n return {\n comments,\n groupedByDomPath,\n threadsByDomPath,\n archivedThreads,\n archivedCount,\n detachedThreads,\n detachedCount,\n isDomPathFullyResolved,\n isDomPathFullyArchived,\n };\n}\n","'use client';\n\nimport React, { useEffect, useRef, useCallback, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { useComments } from './hooks/useComments';\nimport { resolveAuthor } from './lib/authorResolver';\nimport { Z_INDEX } from './lib/constants';\nimport type { Comment } from './types/comment';\nimport type { ThreadItem } from './hooks/useComments';\n\n// --- Styles ---\n\nconst panelStyles: React.CSSProperties = {\n position: 'fixed',\n top: '50%',\n right: 60,\n transform: 'translateY(-50%)',\n width: 300,\n maxHeight: 320,\n backgroundColor: 'var(--fl-surface-raised)',\n border: '1px solid var(--fl-border)',\n borderRadius: 'var(--fl-border-radius)',\n boxShadow: 'var(--fl-shadow-lg)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-base)',\n color: 'var(--fl-text-primary)',\n display: 'flex',\n flexDirection: 'column' as const,\n overflow: 'hidden',\n zIndex: Z_INDEX.POPOVER,\n};\n\nconst headerStyles: React.CSSProperties = {\n padding: '10px 12px',\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n color: 'var(--fl-text-secondary)',\n fontFamily: 'var(--fl-font-family)',\n borderBottom: '1px solid var(--fl-border)',\n flexShrink: 0,\n};\n\nconst scrollStyles: React.CSSProperties = {\n overflowY: 'auto',\n flexGrow: 1,\n minHeight: 0,\n};\n\nconst emptyStyles: React.CSSProperties = {\n padding: '20px 12px',\n textAlign: 'center' as const,\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst rowStyles: React.CSSProperties = {\n padding: '10px 12px',\n borderBottom: '1px solid var(--fl-border)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst rowContentStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n lineHeight: 'var(--fl-line-height)',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\n\nconst rowMetaStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginTop: 4,\n};\n\nconst rowAuthorStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst restoreButtonStyles: React.CSSProperties = {\n display: 'inline-flex',\n alignItems: 'center',\n padding: '2px 8px',\n border: 'none',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'transparent',\n color: 'var(--fl-text-tertiary)',\n cursor: 'pointer',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n transition: `color var(--fl-duration-fast) ease-out`,\n};\n\nconst restoreButtonHoverStyles: React.CSSProperties = {\n ...restoreButtonStyles,\n color: 'var(--fl-accent)',\n};\n\n// --- Archived row ---\n\nfunction ArchivedRow({\n thread,\n onRestore,\n}: {\n thread: ThreadItem;\n onRestore: (comment: Comment) => void;\n}) {\n const [hovered, setHovered] = useState(false);\n\n return (\n <div style={rowStyles}>\n <div style={rowContentStyles}>\n “{thread.root.content}”\n </div>\n <div style={rowMetaStyles}>\n <span style={rowAuthorStyles}>{thread.root.author.name}</span>\n <button\n type=\"button\"\n style={hovered ? restoreButtonHoverStyles : restoreButtonStyles}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n onClick={() => onRestore(thread.root)}\n >\n Restore\n </button>\n </div>\n </div>\n );\n}\n\n// --- Panel ---\n\ninterface ArchivedThreadsPanelProps {\n onClose: () => void;\n}\n\nexport function ArchivedThreadsPanel({ onClose }: ArchivedThreadsPanelProps) {\n const { portalRoot, adapter, config, dispatch } = useLayContext();\n const { archivedThreads } = useComments();\n const panelRef = useRef<HTMLDivElement>(null);\n\n const handleRestore = useCallback(\n (comment: Comment) => {\n const optimistic: Comment = {\n ...comment,\n status: 'open',\n resolved_by: null,\n resolved_at: null,\n archived_at: null,\n };\n\n dispatch({ type: 'UPDATE_COMMENT', payload: optimistic });\n\n adapter\n .updateComment(comment.id, {\n status: 'open',\n resolved_by: null,\n resolved_at: null,\n archived_at: null,\n })\n .catch(() => {\n dispatch({ type: 'UPDATE_COMMENT', payload: comment });\n });\n },\n [adapter, dispatch]\n );\n\n // Close on Escape\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n e.preventDefault();\n onClose();\n }\n }\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [onClose]);\n\n // Close on click outside\n useEffect(() => {\n function handleMouseDown(e: MouseEvent) {\n const target = e.target as Element;\n if (panelRef.current?.contains(target)) return;\n if (target.closest('[data-fl-ignore]')) return;\n onClose();\n }\n\n const timer = setTimeout(() => {\n document.addEventListener('mousedown', handleMouseDown, true);\n }, 50);\n\n return () => {\n clearTimeout(timer);\n document.removeEventListener('mousedown', handleMouseDown, true);\n };\n }, [onClose]);\n\n if (!portalRoot) return null;\n\n return createPortal(\n <div ref={panelRef} style={panelStyles} data-fl-ignore=\"\">\n <div style={headerStyles}>Archived Threads</div>\n <div style={scrollStyles}>\n {archivedThreads.length === 0 ? (\n <div style={emptyStyles}>No archived threads on this page.</div>\n ) : (\n archivedThreads.map((thread) => (\n <ArchivedRow\n key={thread.root.id}\n thread={thread}\n onRestore={handleRestore}\n />\n ))\n )}\n </div>\n </div>,\n portalRoot\n );\n}\n","'use client';\n\nimport React, { useEffect, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { useComments } from './hooks/useComments';\nimport { formatRelativeTime } from './utils/time';\nimport { summarizeDomPath } from './utils/humanizePath';\nimport { Z_INDEX } from './lib/constants';\nimport type { ThreadItem } from './hooks/useComments';\n\n// --- Styles ---\n\nconst panelStyles: React.CSSProperties = {\n position: 'fixed',\n top: '50%',\n right: 60,\n transform: 'translateY(-50%)',\n width: 300,\n maxHeight: 320,\n backgroundColor: 'var(--fl-surface-raised)',\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: 'var(--fl-border)',\n borderRadius: 'var(--fl-border-radius)',\n boxShadow: 'var(--fl-shadow-lg)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-base)',\n color: 'var(--fl-text-primary)',\n display: 'flex',\n flexDirection: 'column' as const,\n overflow: 'hidden',\n zIndex: Z_INDEX.POPOVER,\n};\n\nconst headerStyles: React.CSSProperties = {\n padding: '10px 12px',\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n color: 'var(--fl-text-secondary)',\n fontFamily: 'var(--fl-font-family)',\n borderBottomWidth: 1,\n borderBottomStyle: 'solid',\n borderBottomColor: 'var(--fl-border)',\n flexShrink: 0,\n};\n\nconst subheaderStyles: React.CSSProperties = {\n padding: '6px 12px 8px',\n fontSize: 11,\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n lineHeight: 1.4,\n};\n\nconst scrollStyles: React.CSSProperties = {\n overflowY: 'auto',\n flexGrow: 1,\n minHeight: 0,\n};\n\nconst emptyStyles: React.CSSProperties = {\n padding: '20px 12px',\n textAlign: 'center' as const,\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst rowStyles: React.CSSProperties = {\n padding: '10px 12px',\n borderBottomWidth: 1,\n borderBottomStyle: 'solid',\n borderBottomColor: 'var(--fl-border)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst rowContentStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n lineHeight: 'var(--fl-line-height)',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\n\nconst rowMetaStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginTop: 4,\n};\n\nconst rowAuthorStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst rowTimestampStyles: React.CSSProperties = {\n fontSize: 11,\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst wasOnStyles: React.CSSProperties = {\n marginTop: 4,\n fontSize: 11,\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n fontStyle: 'italic',\n};\n\n// --- Detached row ---\n\nfunction DetachedRow({ thread }: { thread: ThreadItem }) {\n const domPathSummary = summarizeDomPath(thread.root.dom_path);\n\n return (\n <div style={rowStyles}>\n <div style={rowContentStyles}>\n “{thread.root.content}”\n </div>\n <div style={rowMetaStyles}>\n <span style={rowAuthorStyles}>{thread.root.author.name}</span>\n <span style={rowTimestampStyles}>\n {formatRelativeTime(thread.root.created_at)}\n </span>\n </div>\n <div style={wasOnStyles}>\n Was on: {domPathSummary}\n </div>\n </div>\n );\n}\n\n// --- Panel ---\n\ninterface DetachedCommentsPanelProps {\n onClose: () => void;\n}\n\nexport function DetachedCommentsPanel({ onClose }: DetachedCommentsPanelProps) {\n const { portalRoot } = useLayContext();\n const { detachedThreads } = useComments();\n const panelRef = useRef<HTMLDivElement>(null);\n\n // Close on Escape\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n e.preventDefault();\n onClose();\n }\n }\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [onClose]);\n\n // Close on click outside\n useEffect(() => {\n function handleMouseDown(e: MouseEvent) {\n const target = e.target as Element;\n if (panelRef.current?.contains(target)) return;\n if (target.closest('[data-fl-ignore]')) return;\n onClose();\n }\n\n const timer = setTimeout(() => {\n document.addEventListener('mousedown', handleMouseDown, true);\n }, 50);\n\n return () => {\n clearTimeout(timer);\n document.removeEventListener('mousedown', handleMouseDown, true);\n };\n }, [onClose]);\n\n if (!portalRoot) return null;\n\n return createPortal(\n <div ref={panelRef} style={panelStyles} data-fl-ignore=\"\">\n <div style={headerStyles}>Detached Comments</div>\n <div style={subheaderStyles}>\n These comments can't find their original element.\n </div>\n <div style={scrollStyles}>\n {detachedThreads.length === 0 ? (\n <div style={emptyStyles}>No detached comments on this page.</div>\n ) : (\n detachedThreads.map((thread) => (\n <DetachedRow key={thread.root.id} thread={thread} />\n ))\n )}\n </div>\n </div>,\n portalRoot\n );\n}\n","/**\n * Format an ISO timestamp as a human-readable relative time string.\n *\n * Examples: \"just now\", \"2 min ago\", \"1 hr ago\", \"Yesterday\", \"Feb 12\"\n */\nexport function formatRelativeTime(isoTimestamp: string): string {\n const date = new Date(isoTimestamp);\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDays = Math.floor(diffHr / 24);\n\n if (diffSec < 60) return 'just now';\n if (diffMin < 60) return `${diffMin} min ago`;\n if (diffHr < 24) return `${diffHr} hr ago`;\n if (diffDays === 1) return 'Yesterday';\n\n const month = date.toLocaleString('en-US', { month: 'short' });\n const day = date.getDate();\n\n // Same year — show \"Feb 12\"\n if (date.getFullYear() === now.getFullYear()) {\n return `${month} ${day}`;\n }\n\n // Different year — show \"Feb 12, 2025\"\n return `${month} ${day}, ${date.getFullYear()}`;\n}\n","import { BREADCRUMB } from '../lib/constants';\n\nconst TAG_LABELS: Record<string, string> = {\n nav: 'navigation', main: 'main', header: 'header', footer: 'footer',\n section: 'section', article: 'article', aside: 'sidebar',\n form: 'form', table: 'table', ul: 'list', ol: 'list', li: 'item',\n h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading',\n h5: 'heading', h6: 'heading', p: 'text',\n img: 'image', figure: 'figure', video: 'video', picture: 'image',\n button: 'button', a: 'link', input: 'input',\n select: 'dropdown', textarea: 'text input',\n};\n\n/** Convert a single element to a human-readable label */\nexport function humanizeElement(el: Element): string {\n const tag = el.tagName.toLowerCase();\n\n // Use id if available\n if (el.id) return `#${el.id}`;\n\n // Use aria-label\n const ariaLabel = el.getAttribute('aria-label');\n if (ariaLabel) return `\"${truncate(ariaLabel, 20)}\"`;\n\n // Use visible text for interactive/heading elements\n if (['button', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {\n const text = el.textContent?.trim();\n if (text && text.length <= 25) {\n return `\"${truncate(text, 20)}\" ${TAG_LABELS[tag] ?? tag}`;\n }\n }\n\n // Use data-testid or data-feedback-id\n const testId = el.getAttribute('data-testid') ?? el.getAttribute('data-feedback-id');\n if (testId) return testId;\n\n // Fall back to semantic label or raw tag\n return TAG_LABELS[tag] ?? tag;\n}\n\n/** Build a human-readable path from element to its nearest id ancestor or body */\nexport function humanizeDomPath(element: Element): string[] {\n const segments: string[] = [];\n let current: Element | null = element;\n\n while (current && current !== document.body) {\n segments.unshift(humanizeElement(current));\n if (current.id) break; // stop at id — it's unique\n current = current.parentElement;\n }\n\n // Prepend \"page\" if we walked all the way to body\n if (!element.closest('[id]')) {\n segments.unshift('page');\n }\n\n // Keep last N segments\n return segments.slice(-BREADCRUMB.MAX_SEGMENTS);\n}\n\n/**\n * Summarize a stored CSS selector path into a human-readable string.\n * Used in the detached comments panel to show \"Was on: button in #pricing\".\n *\n * @param domPath - A CSS selector like \"body > div > main > section#pricing > button:nth-child(2)\"\n * @returns A human-readable summary like \"button in #pricing\"\n */\nexport function summarizeDomPath(domPath: string): string {\n const segments = domPath.split(/\\s*>\\s*/);\n if (segments.length === 0) return domPath;\n\n // Find the target (last segment) and the most meaningful ancestor\n const target = simplifySegment(segments[segments.length - 1]);\n\n // Walk backwards to find an id, data-feedback-id, or data-testid ancestor\n let context = '';\n for (let i = segments.length - 2; i >= 0; i--) {\n const seg = segments[i];\n if (seg.includes('#') || seg.includes('[data-feedback-id') || seg.includes('[data-testid')) {\n context = simplifySegment(seg);\n break;\n }\n // Use semantic tags as context\n const tag = seg.split(/[.:[\\s]/)[0];\n if (['main', 'nav', 'header', 'footer', 'section', 'article', 'aside', 'form'].includes(tag)) {\n context = tag;\n break;\n }\n }\n\n const result = context ? `${target} in ${context}` : target;\n return truncate(result, 30);\n}\n\n/**\n * Simplify a single CSS selector segment for display.\n * \"button:nth-child(2)\" → \"button\"\n * \"#pricing\" → \"#pricing\"\n * \"[data-testid=\\\"cta\\\"]\" → \"cta\"\n */\nfunction simplifySegment(segment: string): string {\n // data-feedback-id or data-testid\n const attrMatch = segment.match(/\\[data-(?:feedback-id|testid)=\"([^\"]+)\"\\]/);\n if (attrMatch) return attrMatch[1];\n\n // id\n const idMatch = segment.match(/#([a-zA-Z0-9_-]+)/);\n if (idMatch) return `#${idMatch[1]}`;\n\n // tag with class\n const classMatch = segment.match(/^([a-z]+)\\.([a-zA-Z0-9_-]+)/);\n if (classMatch) return `${classMatch[1]}.${classMatch[2]}`;\n\n // tag with nth-child — strip nth-child for display\n const nthMatch = segment.match(/^([a-z]+):nth-child\\(\\d+\\)/);\n if (nthMatch) return nthMatch[1];\n\n return segment;\n}\n\nfunction truncate(str: string, max: number): string {\n return str.length > max ? str.slice(0, max - 1) + '\\u2026' : str;\n}\n","import { useState, useEffect, useCallback, useRef } from 'react';\nimport { useLayContext } from './useLayContext';\nimport { isCommentable } from '../utils/domPath';\nimport { resolveMagneticTarget } from '../utils/magneticTarget';\n\ninterface ElementSelectorState {\n /** The element currently under the cursor (comment mode only) */\n hoveredElement: Element | null;\n /** The element the user clicked to comment on */\n selectedElement: Element | null;\n}\n\ninterface UseElementSelectorReturn extends ElementSelectorState {\n /** Clear the selected element (dismiss comment input) */\n clearSelection: () => void;\n}\n\n/**\n * Tracks hovered and selected elements via event delegation on document.body.\n * Only active when comment mode is on. Cleans up all listeners when mode is off.\n */\nexport function useElementSelector(): UseElementSelectorReturn {\n const { isCommentMode } = useLayContext();\n const [state, setState] = useState<ElementSelectorState>({\n hoveredElement: null,\n selectedElement: null,\n });\n\n // Use ref to avoid stale closure in event handlers\n const stateRef = useRef(state);\n stateRef.current = state;\n\n const clearSelection = useCallback(() => {\n setState((prev) => ({ ...prev, selectedElement: null }));\n }, []);\n\n useEffect(() => {\n if (!isCommentMode) {\n // Reset state when exiting comment mode\n setState({ hoveredElement: null, selectedElement: null });\n return;\n }\n\n function handleMouseOver(e: MouseEvent) {\n const target = e.target as Element;\n if (!target || !isCommentable(target)) {\n setState((prev) =>\n prev.hoveredElement !== null\n ? { ...prev, hoveredElement: null }\n : prev\n );\n return;\n }\n\n // Resolve magnetic target (may return null = use original)\n const finalTarget = resolveMagneticTarget(e, target) ?? target;\n\n setState((prev) =>\n prev.hoveredElement !== finalTarget\n ? { ...prev, hoveredElement: finalTarget }\n : prev\n );\n }\n\n function handleMouseOut(e: MouseEvent) {\n const relatedTarget = e.relatedTarget as Element | null;\n // Only clear if mouse left the document entirely\n if (!relatedTarget || relatedTarget === document.documentElement) {\n setState((prev) =>\n prev.hoveredElement !== null\n ? { ...prev, hoveredElement: null }\n : prev\n );\n }\n }\n\n function handleClick(e: MouseEvent) {\n const target = e.target as Element;\n if (!target || !isCommentable(target)) {\n return;\n }\n\n // Prevent host app click handlers from firing during comment mode\n e.preventDefault();\n e.stopPropagation();\n\n const finalTarget = resolveMagneticTarget(e, target) ?? target;\n\n setState((prev) => ({\n ...prev,\n selectedElement: finalTarget,\n hoveredElement: null,\n }));\n }\n\n document.body.addEventListener('mouseover', handleMouseOver, true);\n document.body.addEventListener('mouseout', handleMouseOut, true);\n document.body.addEventListener('click', handleClick, true);\n\n // Set comment-mode cursor on body\n const prevCursor = document.body.style.cursor;\n document.body.style.cursor = 'crosshair';\n\n return () => {\n document.body.removeEventListener('mouseover', handleMouseOver, true);\n document.body.removeEventListener('mouseout', handleMouseOut, true);\n document.body.removeEventListener('click', handleClick, true);\n document.body.style.cursor = prevCursor;\n };\n }, [isCommentMode]);\n\n return {\n hoveredElement: state.hoveredElement,\n selectedElement: state.selectedElement,\n clearSelection,\n };\n}\n","/**\n * Generate a CSS selector path for an element by walking up the DOM tree.\n *\n * Priority order for each segment (highest to lowest):\n * 1. data-feedback-id=\"X\" → [data-feedback-id=\"X\"] (stop walking)\n * 2. data-testid=\"X\" → [data-testid=\"X\"] (stop walking)\n * 3. id=\"X\" → #X (stop walking)\n * 4. Stable class names → tag.my-button (skip generated/hashed classes)\n * 5. Tag + nth-child → div:nth-child(3) (fallback)\n */\nexport function generateDomPath(element: Element): string {\n const segments: string[] = [];\n let current: Element | null = element;\n\n while (current && current !== document.documentElement) {\n if (current === document.body) {\n segments.unshift('body');\n break;\n }\n\n // 1. data-feedback-id — guaranteed stable anchor\n const feedbackId = current.getAttribute('data-feedback-id');\n if (feedbackId) {\n segments.unshift(`[data-feedback-id=\"${feedbackId}\"]`);\n break;\n }\n\n // 2. data-testid — developer-provided stable identifier\n const testId = current.getAttribute('data-testid');\n if (testId) {\n segments.unshift(`[data-testid=\"${testId}\"]`);\n break;\n }\n\n // 3. id — unique per page\n if (current.id) {\n segments.unshift(`#${current.id}`);\n break;\n }\n\n let segment = current.tagName.toLowerCase();\n\n // 4. Stable class names — filter out generated/hashed classes\n const stableClasses = getStableClassNames(current);\n if (stableClasses.length > 0) {\n // Use the first stable class for disambiguation\n segment += `.${stableClasses[0]}`;\n // Check if this selector is unique among siblings\n const parent = current.parentElement;\n if (parent) {\n const sameSelector = Array.from(parent.children).filter(\n (s) => s.matches(`${current!.tagName.toLowerCase()}.${stableClasses[0]}`)\n );\n if (sameSelector.length === 1) {\n segments.unshift(segment);\n current = current.parentElement;\n continue;\n }\n }\n }\n\n // 5. Tag + nth-child fallback\n const parent = current.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children);\n const sameTagSiblings = siblings.filter(\n (s) => s.tagName === current!.tagName\n );\n\n if (sameTagSiblings.length > 1) {\n const index = siblings.indexOf(current) + 1;\n segment = `${current.tagName.toLowerCase()}:nth-child(${index})`;\n }\n }\n\n segments.unshift(segment);\n current = current.parentElement;\n }\n\n return segments.join(' > ');\n}\n\n/**\n * Get stable (non-generated) class names from an element.\n * Filters out CSS Modules hashes, Emotion/styled-components prefixes,\n * single-letter classes, and underscore-prefixed classes.\n */\nfunction getStableClassNames(element: Element): string[] {\n const classList = Array.from(element.classList);\n return classList.filter((name) => !isGeneratedClassName(name));\n}\n\n/**\n * Heuristic to detect framework-generated class names.\n * Returns true for classes that are likely unstable across builds.\n */\nexport function isGeneratedClassName(name: string): boolean {\n // Single-letter or two-letter classes (Tailwind JIT internal, minifiers)\n if (name.length <= 2) return true;\n\n // Starts with underscore (CSS Modules convention)\n if (name.startsWith('_')) return true;\n\n // CSS Modules: component_hash, styles_abc123\n if (/^[a-zA-Z]+_[a-zA-Z0-9]{5,}$/.test(name)) return true;\n\n // Emotion/styled-components: css-1abc23\n if (/^css-[a-z0-9]+$/i.test(name)) return true;\n\n // Styled-components: sc-aBcDe (two-letter prefix + dash + hash)\n if (/^sc-[a-zA-Z0-9]+$/.test(name)) return true;\n\n // Generic hash patterns: 6+ char alphanumeric strings that look hashed\n if (/^[a-z]{1,3}[A-Z][a-zA-Z0-9]{4,}$/.test(name)) return true;\n\n return false;\n}\n\n/**\n * Check if an element should be excluded from the commentable surface.\n * Excludes Lay UI elements and non-visible elements.\n */\nexport function isCommentable(element: Element): boolean {\n // Skip non-visible elements\n const tag = element.tagName.toLowerCase();\n if (['html', 'head', 'script', 'style', 'link', 'meta', 'noscript', 'br'].includes(tag)) {\n return false;\n }\n\n // Skip Lay elements (anything inside [data-fl-root] or with data-fl-ignore)\n if (element.closest('[data-fl-root]') || element.hasAttribute('data-fl-ignore')) {\n return false;\n }\n\n return true;\n}\n","/**\n * Element fingerprinting for anchoring hardening (M6).\n *\n * Captures a structural fingerprint of a DOM element that can be used\n * to re-find the element when its CSS selector path breaks due to\n * DOM changes between deploys.\n */\n\n/** Key attributes to capture in fingerprints */\nconst FINGERPRINT_ATTRS = [\n 'id', 'class', 'role', 'type', 'name', 'href', 'src',\n 'data-testid', 'data-feedback-id',\n] as const;\n\nimport type { ElementFingerprint } from '../types/comment';\n\nconst TEXT_MAX_LENGTH = 80;\n\n/**\n * Generate a fingerprint for a DOM element.\n * Captures tag, text content, key attributes, and structural position.\n */\nexport function generateFingerprint(element: Element): ElementFingerprint {\n const tag = element.tagName.toLowerCase();\n\n // Text content: direct text only (not deeply nested), trimmed, truncated\n const textContent = (element.textContent?.trim() ?? '').slice(0, TEXT_MAX_LENGTH);\n\n // Key attributes\n const attributes: Record<string, string> = {};\n for (const attr of FINGERPRINT_ATTRS) {\n const value = element.getAttribute(attr);\n if (value !== null && value !== '') {\n attributes[attr] = value;\n }\n }\n\n // Sibling position\n const parent = element.parentElement;\n let siblingIndex = 0;\n let siblingCount = 1;\n if (parent) {\n const sameTagSiblings = Array.from(parent.children).filter(\n (s) => s.tagName === element.tagName\n );\n siblingCount = sameTagSiblings.length;\n siblingIndex = sameTagSiblings.indexOf(element);\n }\n\n // Ancestor tags\n const parentTag = parent?.tagName.toLowerCase() ?? '';\n const grandparentTag = parent?.parentElement?.tagName.toLowerCase() ?? '';\n\n return {\n tag,\n textContent,\n attributes,\n siblingIndex,\n siblingCount,\n parentTag,\n grandparentTag,\n };\n}\n\n/**\n * Score how well a stored fingerprint matches a candidate element.\n * Returns 0-100. Tag mismatch = 0 (required match).\n */\nexport function scoreFingerprintMatch(\n stored: ElementFingerprint,\n candidate: Element\n): number {\n // Tag match is required\n if (candidate.tagName.toLowerCase() !== stored.tag) return 0;\n\n let score = 20; // base score for tag match\n\n // Text content similarity\n const text = (candidate.textContent?.trim() ?? '').slice(0, TEXT_MAX_LENGTH);\n if (text === stored.textContent && text.length > 0) {\n score += 25;\n } else if (\n stored.textContent.length > 0 &&\n text.length > 0 &&\n (text.includes(stored.textContent) || stored.textContent.includes(text))\n ) {\n score += 15;\n }\n\n // Attribute matches\n const attrs = stored.attributes;\n if (attrs['data-feedback-id'] && candidate.getAttribute('data-feedback-id') === attrs['data-feedback-id']) {\n score += 30;\n }\n if (attrs['data-testid'] && candidate.getAttribute('data-testid') === attrs['data-testid']) {\n score += 25;\n }\n if (attrs.id && candidate.id === attrs.id) {\n score += 20;\n }\n if (attrs.role && candidate.getAttribute('role') === attrs.role) {\n score += 5;\n }\n if (attrs.type && candidate.getAttribute('type') === attrs.type) {\n score += 5;\n }\n if (attrs.href && candidate.getAttribute('href') === attrs.href) {\n score += 10;\n }\n if (attrs.name && candidate.getAttribute('name') === attrs.name) {\n score += 5;\n }\n\n // Structural position\n const parent = candidate.parentElement;\n if (parent && parent.tagName.toLowerCase() === stored.parentTag) {\n score += 10;\n }\n const grandparent = parent?.parentElement;\n if (grandparent && grandparent.tagName.toLowerCase() === stored.grandparentTag) {\n score += 5;\n }\n\n return Math.min(score, 100);\n}\n\nexport interface FingerprintMatchResult {\n element: Element | null;\n score: number;\n}\n\n/**\n * Find the best matching element on the page for a stored fingerprint.\n * Scans all elements of the same tag type and returns the highest-scoring match.\n *\n * @param fingerprint - The stored fingerprint to match against\n * @param threshold - Minimum score to consider a match (default: 40)\n * @returns The best matching element and its score, or null if below threshold\n */\nexport function findByFingerprint(\n fingerprint: ElementFingerprint,\n threshold: number = 40\n): FingerprintMatchResult {\n // Query all elements of the same tag type — much narrower than querying everything\n const candidates = document.querySelectorAll(fingerprint.tag);\n\n let bestElement: Element | null = null;\n let bestScore = 0;\n\n for (const candidate of candidates) {\n // Skip Lay UI elements\n if (candidate.closest('[data-fl-root]') || candidate.hasAttribute('data-fl-ignore')) {\n continue;\n }\n\n const score = scoreFingerprintMatch(fingerprint, candidate);\n if (score > bestScore) {\n bestScore = score;\n bestElement = candidate;\n }\n }\n\n if (bestScore >= threshold) {\n return { element: bestElement, score: bestScore };\n }\n\n return { element: null, score: bestScore };\n}\n","import type { ElementMetadata } from '../types/comment';\nimport { generateFingerprint } from './elementFingerprint';\n\n// --- Computed style keys to capture ---\n\nconst STYLE_KEYS = [\n 'font-family',\n 'font-size',\n 'font-weight',\n 'color',\n 'background-color',\n 'padding',\n 'margin',\n 'width',\n 'height',\n 'border-radius',\n 'display',\n 'position',\n 'line-height',\n 'letter-spacing',\n 'text-align',\n 'opacity',\n] as const;\n\n// --- Color parsing ---\n\ninterface RGB {\n r: number;\n g: number;\n b: number;\n}\n\n/**\n * Parse an rgb() or rgba() string into { r, g, b } (0-255).\n * Returns null if the string can't be parsed.\n */\nfunction parseRGB(color: string): RGB | null {\n const match = color.match(\n /rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/\n );\n if (!match) return null;\n return {\n r: Number(match[1]),\n g: Number(match[2]),\n b: Number(match[3]),\n };\n}\n\n/**\n * Returns true if the color string represents a fully transparent value.\n */\nfunction isTransparent(color: string): boolean {\n if (color === 'transparent' || color === 'rgba(0, 0, 0, 0)') return true;\n const match = color.match(\n /rgba\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*([\\d.]+)\\s*\\)/\n );\n return match ? Number(match[1]) === 0 : false;\n}\n\n// --- Visual boundary detection ---\n\n/**\n * Returns true if the element has computed styles that create a visible\n * boundary (background, border, shadow). Used by magnetic targeting to\n * distinguish styled containers (cards, tiles) from invisible wrappers.\n */\nexport function hasVisualBoundary(element: Element): boolean {\n const styles = window.getComputedStyle(element);\n\n // Non-transparent background\n const bg = styles.getPropertyValue('background-color');\n if (bg && !isTransparent(bg)) return true;\n\n // Background image (gradients, images)\n const bgImage = styles.getPropertyValue('background-image');\n if (bgImage && bgImage !== 'none') return true;\n\n // Box shadow\n const shadow = styles.getPropertyValue('box-shadow');\n if (shadow && shadow !== 'none') return true;\n\n // Visible border on any side\n for (const side of ['top', 'right', 'bottom', 'left']) {\n const width = styles.getPropertyValue(`border-${side}-width`);\n const style = styles.getPropertyValue(`border-${side}-style`);\n if (width !== '0px' && style !== 'none') return true;\n }\n\n return false;\n}\n\n// --- Effective background resolution ---\n\n/**\n * Walk up the DOM tree from `element` to find the first ancestor with\n * a non-transparent computed background-color. Returns the CSS color string.\n * Falls back to white (#FFFFFF) if all ancestors are transparent.\n */\nexport function resolveEffectiveBackground(element: Element): string {\n let current: Element | null = element;\n\n while (current && current !== document.documentElement) {\n const bg = window.getComputedStyle(current).backgroundColor;\n if (bg && !isTransparent(bg)) return bg;\n current = current.parentElement;\n }\n\n // Check <html> element itself\n if (document.documentElement) {\n const htmlBg =\n window.getComputedStyle(document.documentElement).backgroundColor;\n if (htmlBg && !isTransparent(htmlBg)) return htmlBg;\n }\n\n return 'rgb(255, 255, 255)';\n}\n\n// --- WCAG 2.1 contrast ratio ---\n\n/**\n * Convert an sRGB channel (0-255) to linear light value per WCAG 2.1.\n */\nfunction linearize(channel: number): number {\n const s = channel / 255;\n return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n}\n\n/**\n * Calculate relative luminance per WCAG 2.1.\n * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance\n */\nfunction relativeLuminance(rgb: RGB): number {\n return (\n 0.2126 * linearize(rgb.r) +\n 0.7152 * linearize(rgb.g) +\n 0.0722 * linearize(rgb.b)\n );\n}\n\n/**\n * Compute the WCAG 2.1 contrast ratio between a foreground and background color.\n *\n * Returns the ratio (≥1) and whether the combination passes WCAG AA.\n * AA threshold: 4.5:1 for normal text, 3:1 for large text (≥18px or bold ≥14px).\n * We default to the 4.5:1 threshold (normal text) since font-size context\n * is not always reliably available here.\n */\nexport function computeContrastRatio(\n fgColor: string,\n bgColor: string\n): { ratio: number; passesAA: boolean } {\n const fg = parseRGB(fgColor);\n const bg = parseRGB(bgColor);\n\n if (!fg || !bg) {\n return { ratio: 0, passesAA: false };\n }\n\n const lum1 = relativeLuminance(fg);\n const lum2 = relativeLuminance(bg);\n\n const lighter = Math.max(lum1, lum2);\n const darker = Math.min(lum1, lum2);\n\n const ratio = Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;\n\n return {\n ratio,\n passesAA: ratio >= 4.5,\n };\n}\n\n// --- User agent parsing ---\n\n/**\n * Parse a user agent string into a readable device string.\n * Returns \"iPhone\", \"iPad\", \"Android\", \"Chrome on macOS\", etc.\n */\nexport function parseUserAgent(ua: string): string {\n // Mobile devices\n if (/iPhone/i.test(ua)) return 'iPhone';\n if (/iPad/i.test(ua)) return 'iPad';\n if (/Android/i.test(ua)) {\n if (/Mobile/i.test(ua)) return 'Android Phone';\n return 'Android Tablet';\n }\n\n // Desktop — detect browser + OS\n let browser = 'Browser';\n if (/Edg\\//i.test(ua)) browser = 'Edge';\n else if (/Chrome\\//i.test(ua)) browser = 'Chrome';\n else if (/Firefox\\//i.test(ua)) browser = 'Firefox';\n else if (/Safari\\//i.test(ua) && !/Chrome/i.test(ua)) browser = 'Safari';\n\n let os = '';\n if (/Macintosh|Mac OS/i.test(ua)) os = 'macOS';\n else if (/Windows/i.test(ua)) os = 'Windows';\n else if (/Linux/i.test(ua)) os = 'Linux';\n else if (/CrOS/i.test(ua)) os = 'ChromeOS';\n\n return os ? `${browser} on ${os}` : browser;\n}\n\n// --- Main capture function ---\n\n/**\n * Capture technical metadata from a DOM element.\n *\n * Collects computed styles, accessibility data (contrast ratio, ARIA),\n * viewport dimensions, and device info. All reads are synchronous —\n * no network calls, no DOM mutations, no layout thrashing.\n */\nexport function captureElementMetadata(element: Element): ElementMetadata {\n const computed = window.getComputedStyle(element);\n\n // Capture key computed styles\n const computed_styles: Record<string, string> = {};\n for (const key of STYLE_KEYS) {\n computed_styles[key] = computed.getPropertyValue(key);\n }\n\n // Accessibility data\n const role = element.getAttribute('role');\n const aria_label = element.getAttribute('aria-label');\n\n const fgColor = computed_styles['color'];\n const bgColor = resolveEffectiveBackground(element);\n\n // Store the resolved background so the AI enrichment can use it\n if (bgColor !== computed_styles['background-color']) {\n computed_styles['effective-background-color'] = bgColor;\n }\n\n const { ratio, passesAA } = computeContrastRatio(fgColor, bgColor);\n\n // Viewport & device\n const viewport = {\n width: window.innerWidth,\n height: window.innerHeight,\n };\n const device = parseUserAgent(navigator.userAgent);\n\n // Element fingerprint for anchoring hardening\n let fingerprint;\n try {\n fingerprint = generateFingerprint(element);\n } catch {\n // Fingerprint generation is best-effort\n }\n\n return {\n computed_styles,\n accessibility: {\n role,\n aria_label,\n contrast_ratio: ratio,\n contrast_passes_aa: passesAA,\n },\n viewport,\n device,\n fingerprint,\n };\n}\n","import { isCommentable } from './domPath';\nimport { hasVisualBoundary } from './computedStyles';\nimport { MAGNETIC } from '../lib/constants';\n\n/** Element priority for magnetic targeting */\nexport function getElementPriority(el: Element): number {\n const tag = el.tagName.toLowerCase();\n\n // Tier 3 (highest): interactive elements + explicit anchors\n if (['button', 'a', 'input', 'select', 'textarea'].includes(tag)) return 3;\n if (el.getAttribute('role') === 'button' || el.getAttribute('role') === 'link')\n return 3;\n if (el.hasAttribute('data-feedback-id') || el.hasAttribute('data-testid'))\n return 3;\n\n // Tier 2: semantic block elements\n const semanticTags = [\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'p', 'img', 'figure', 'figcaption', 'picture', 'video',\n 'section', 'article', 'nav', 'header', 'footer', 'main', 'aside',\n 'form', 'table', 'ul', 'ol', 'li', 'blockquote', 'details',\n ];\n if (semanticTags.includes(tag)) return 2;\n\n // Tier 1 (lowest): generic containers\n return 1;\n}\n\nconst MAGNETIC_SELECTORS =\n 'button,a,input,select,textarea,' +\n 'h1,h2,h3,h4,h5,h6,p,img,figure,nav,section,article,header,footer,main,' +\n '[role=\"button\"],[role=\"link\"],[data-feedback-id],[data-testid]';\n\n/**\n * Resolve the best magnetic target for a mouseover/click event.\n *\n * Phase 1 — Ancestor Promotion: walk up from the target (max 4 levels)\n * looking for a higher-priority ancestor. Handles the common case of\n * cursor on <span> inside <button> → promote to <button>.\n *\n * Phase 2 — Proximity Search: if the target is still low-priority,\n * scan the parent's children for high-priority elements within 30px\n * of the cursor.\n *\n * Returns the promoted/snapped element, or null if the original target\n * should be used as-is.\n */\nexport function resolveMagneticTarget(\n event: MouseEvent,\n target: Element,\n): Element | null {\n const targetPriority = getElementPriority(target);\n\n // Already high priority — no override needed\n if (targetPriority >= 2) return null;\n\n // Styled containers (cards, tiles with bg/border/shadow) are visually\n // distinct — keep them as the target instead of promoting to a parent.\n if (hasVisualBoundary(target)) return null;\n\n // Phase 1: Ancestor promotion\n let bestAncestor: Element | null = null;\n let bestPriority = targetPriority;\n let walker: Element | null = target.parentElement;\n let depth = 0;\n\n while (walker && walker !== document.body && depth < MAGNETIC.ANCESTOR_DEPTH) {\n // Respect opt-out\n if (walker.getAttribute('data-fl-magnet') === 'off') break;\n\n const p = getElementPriority(walker);\n if (p > bestPriority) {\n bestAncestor = walker;\n bestPriority = p;\n }\n walker = walker.parentElement;\n depth++;\n }\n\n if (bestAncestor) return bestAncestor;\n\n // Phase 2: Proximity search among siblings\n const cursor = { x: event.clientX, y: event.clientY };\n const parent = target.parentElement;\n if (!parent) return null;\n\n const candidates = parent.querySelectorAll(MAGNETIC_SELECTORS);\n let closest: { el: Element; dist: number } | null = null;\n\n for (const candidate of candidates) {\n if (candidate === target) continue;\n if (candidate.getAttribute('data-fl-magnet') === 'off') continue;\n if (!isCommentable(candidate)) continue;\n\n const rect = candidate.getBoundingClientRect();\n const dist = distanceToRect(cursor, rect);\n if (dist <= MAGNETIC.PROXIMITY_THRESHOLD && (!closest || dist < closest.dist)) {\n closest = { el: candidate, dist };\n }\n }\n\n return closest?.el ?? null;\n}\n\n/** Point-to-rect distance. Returns 0 if the point is inside the rect. */\nfunction distanceToRect(\n point: { x: number; y: number },\n rect: DOMRect,\n): number {\n const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);\n const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);\n return Math.sqrt(dx * dx + dy * dy);\n}\n","'use client';\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { Z_INDEX } from './lib/constants';\n\ninterface HighlightRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\nconst highlightStyles: React.CSSProperties = {\n position: 'fixed',\n pointerEvents: 'none',\n border: '2px solid var(--fl-accent)',\n borderRadius: 'var(--fl-border-radius-sm)',\n transition: `all var(--fl-duration-instant) ease-out`,\n zIndex: Z_INDEX.HIGHLIGHT,\n boxSizing: 'border-box',\n};\n\ninterface ElementHighlighterProps {\n hoveredElement: Element | null;\n selectedElement: Element | null;\n}\n\nexport function ElementHighlighter({\n hoveredElement,\n selectedElement,\n}: ElementHighlighterProps) {\n const { isCommentMode, portalRoot } = useLayContext();\n const [rect, setRect] = useState<HighlightRect | null>(null);\n const rafRef = useRef<number>(0);\n\n const updateRect = useCallback(() => {\n // Don't show highlight when an element is selected (input is open)\n const target = selectedElement ? null : hoveredElement;\n if (!target) {\n setRect(null);\n return;\n }\n\n const domRect = target.getBoundingClientRect();\n setRect({\n top: domRect.top,\n left: domRect.left,\n width: domRect.width,\n height: domRect.height,\n });\n }, [hoveredElement, selectedElement]);\n\n // Update rect when hovered element changes\n useEffect(() => {\n updateRect();\n }, [updateRect]);\n\n // Recalculate on scroll and resize\n useEffect(() => {\n if (!isCommentMode || (!hoveredElement && !selectedElement)) {\n return;\n }\n\n function handleScrollOrResize() {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => {\n updateRect();\n });\n }\n\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n cancelAnimationFrame(rafRef.current);\n };\n }, [isCommentMode, hoveredElement, selectedElement, updateRect]);\n\n if (!isCommentMode || !portalRoot || !rect) {\n return null;\n }\n\n return createPortal(\n <div\n style={{\n ...highlightStyles,\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n }}\n data-fl-ignore=\"\"\n />,\n portalRoot\n );\n}\n","'use client';\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { Z_INDEX } from './lib/constants';\nimport { humanizeDomPath } from './utils/humanizePath';\n\ninterface BreadcrumbPosition {\n top: number;\n left: number;\n}\n\nconst breadcrumbStyles: React.CSSProperties = {\n position: 'fixed',\n pointerEvents: 'none',\n padding: '2px 8px',\n backgroundColor: 'var(--fl-surface)',\n border: '1px solid var(--fl-border)',\n borderRadius: 4,\n fontSize: 11,\n fontFamily: 'var(--fl-font-family)',\n color: 'var(--fl-text-secondary)',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n maxWidth: 320,\n zIndex: Z_INDEX.HIGHLIGHT,\n boxShadow: '0 1px 3px rgba(26,26,24,0.06)',\n transition: 'all var(--fl-duration-instant) ease-out',\n};\n\ninterface ElementBreadcrumbProps {\n hoveredElement: Element | null;\n selectedElement: Element | null;\n}\n\nexport function ElementBreadcrumb({\n hoveredElement,\n selectedElement,\n}: ElementBreadcrumbProps) {\n const { isCommentMode, portalRoot } = useLayContext();\n const [pos, setPos] = useState<BreadcrumbPosition | null>(null);\n const [label, setLabel] = useState('');\n const rafRef = useRef<number>(0);\n\n // Don't show breadcrumb when composer is open (element is selected)\n const target = selectedElement ? null : hoveredElement;\n\n const updatePosition = useCallback(() => {\n if (!target) {\n setPos(null);\n return;\n }\n\n const domRect = target.getBoundingClientRect();\n const segments = humanizeDomPath(target);\n setLabel(segments.join(' \\u203A '));\n\n // Position 8px above the element's top edge\n const top = Math.max(4, domRect.top - 28);\n // Left-aligned, clamped to viewport\n const left = Math.max(4, Math.min(domRect.left, window.innerWidth - 328));\n\n setPos({ top, left });\n }, [target]);\n\n // Update when target changes\n useEffect(() => {\n updatePosition();\n }, [updatePosition]);\n\n // Track scroll and resize\n useEffect(() => {\n if (!isCommentMode || !target) {\n return;\n }\n\n function handleScrollOrResize() {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => {\n updatePosition();\n });\n }\n\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n cancelAnimationFrame(rafRef.current);\n };\n }, [isCommentMode, target, updatePosition]);\n\n if (!isCommentMode || !portalRoot || !pos) {\n return null;\n }\n\n return createPortal(\n <div\n style={{\n ...breadcrumbStyles,\n top: pos.top,\n left: pos.left,\n }}\n data-fl-ignore=\"\"\n >\n {label}\n </div>,\n portalRoot,\n );\n}\n","'use client';\n\nimport React, {\n useState,\n useEffect,\n useCallback,\n useRef,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { generateDomPath } from './utils/domPath';\nimport { getGuestAuthor } from './lib/guestAuthor';\nimport { resolveAuthor, persistGuestName } from './lib/authorResolver';\nimport { Z_INDEX, COMPOSER, DEFAULT_STARTER_CHIPS } from './lib/constants';\nimport { captureElementMetadata } from './utils/computedStyles';\nimport { captureScreenshot } from './utils/captureScreenshot';\nimport type { NewComment } from './types/comment';\nimport type { StarterChip } from './types/config';\n\n// --- Styles ---\n\nconst popoverStyles: React.CSSProperties = {\n position: 'fixed',\n zIndex: Z_INDEX.POPOVER,\n width: COMPOSER.WIDTH,\n backgroundColor: 'var(--fl-surface-raised)',\n border: '1px solid var(--fl-border)',\n borderRadius: 'var(--fl-border-radius)',\n boxShadow: 'var(--fl-shadow-popover)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-base)',\n color: 'var(--fl-text-primary)',\n overflow: 'hidden',\n opacity: 0,\n transform: 'translateY(4px)',\n transition: `opacity var(--fl-duration-fast) ease-out, transform var(--fl-duration-fast) ease-out, max-height ${COMPOSER.EXPAND_DURATION}ms ease-out`,\n};\n\nconst popoverVisibleStyles: React.CSSProperties = {\n ...popoverStyles,\n opacity: 1,\n transform: 'translateY(0)',\n};\n\n// Expanded form inner wrapper\nconst expandedFormStyles: React.CSSProperties = {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: 8,\n padding: 12,\n};\n\nconst nameInputStyles: React.CSSProperties = {\n width: '100%',\n padding: '6px 8px',\n border: '1px solid var(--fl-border)',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'var(--fl-surface)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n lineHeight: 'var(--fl-line-height)',\n outline: 'none',\n boxSizing: 'border-box' as const,\n transition: `border-color var(--fl-duration-fast) ease-out`,\n};\n\nconst textareaStyles: React.CSSProperties = {\n width: '100%',\n minHeight: 64,\n padding: 8,\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: 'var(--fl-border)',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'var(--fl-surface)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-base)',\n lineHeight: 'var(--fl-line-height)',\n resize: 'vertical' as const,\n outline: 'none',\n boxSizing: 'border-box' as const,\n transition: `border-color var(--fl-duration-fast) ease-out`,\n};\n\nconst footerStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n gap: 8,\n};\n\nconst submitButtonStyles: React.CSSProperties = {\n padding: '4px 12px',\n backgroundColor: 'var(--fl-accent)',\n color: '#FFFFFF',\n border: 'none',\n borderRadius: 'var(--fl-border-radius-sm)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n cursor: 'pointer',\n outline: 'none',\n transition: `background-color var(--fl-duration-fast) ease-out`,\n};\n\nconst submitButtonDisabledStyles: React.CSSProperties = {\n ...submitButtonStyles,\n opacity: 0.4,\n cursor: 'default',\n};\n\nconst hintStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\n// Chip bar styles (collapsed state)\nconst chipBarStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n padding: '6px 8px',\n};\n\nconst chipStyles: React.CSSProperties = {\n padding: '4px 10px',\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: 'var(--fl-border)',\n borderRadius: 12,\n backgroundColor: 'var(--fl-surface)',\n color: 'var(--fl-text-secondary)',\n fontSize: 'var(--fl-font-size-sm)',\n fontFamily: 'var(--fl-font-family)',\n fontWeight: 'var(--fl-font-weight-medium)',\n cursor: 'pointer',\n outline: 'none',\n whiteSpace: 'nowrap' as const,\n transition: `background-color var(--fl-duration-fast) ease-out, border-color var(--fl-duration-fast) ease-out, color var(--fl-duration-fast) ease-out`,\n lineHeight: 1,\n flexShrink: 0,\n};\n\nconst chipHoverStyles: React.CSSProperties = {\n ...chipStyles,\n backgroundColor: 'var(--fl-accent-subtle)',\n borderColor: 'var(--fl-accent)',\n color: 'var(--fl-accent)',\n};\n\nconst typeHintStyles: React.CSSProperties = {\n flex: 1,\n padding: '4px 8px',\n color: 'var(--fl-text-tertiary)',\n fontSize: 'var(--fl-font-size-sm)',\n fontFamily: 'var(--fl-font-family)',\n cursor: 'text',\n minWidth: 0,\n};\n\n// --- Position calculation ---\n\nconst POPOVER_GAP = 8;\nconst POPOVER_MIN_HEIGHT = 120;\n\ninterface PopoverPosition {\n top: number;\n left: number;\n}\n\nfunction calculatePosition(\n elementRect: DOMRect,\n): PopoverPosition {\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n // Default: below the element, aligned to the left edge\n let top = elementRect.bottom + POPOVER_GAP;\n let left = elementRect.left;\n\n // Flip above if not enough room below\n if (top + POPOVER_MIN_HEIGHT > viewportHeight) {\n top = elementRect.top - POPOVER_MIN_HEIGHT - POPOVER_GAP;\n }\n\n // Keep within horizontal viewport bounds\n if (left + COMPOSER.WIDTH > viewportWidth) {\n left = viewportWidth - COMPOSER.WIDTH - POPOVER_GAP;\n }\n if (left < POPOVER_GAP) {\n left = POPOVER_GAP;\n }\n\n // Keep within vertical viewport bounds\n if (top < POPOVER_GAP) {\n top = POPOVER_GAP;\n }\n\n return { top, left };\n}\n\n// --- Component ---\n\ninterface CommentAnchorProps {\n selectedElement: Element | null;\n onClearSelection: () => void;\n}\n\nexport function CommentAnchor({\n selectedElement,\n onClearSelection,\n}: CommentAnchorProps) {\n const { portalRoot, adapter, config } = useLayContext();\n const [name, setName] = useState('');\n const [content, setContent] = useState('');\n const [position, setPosition] = useState<PopoverPosition | null>(null);\n const [isVisible, setIsVisible] = useState(false);\n const [isExpanded, setIsExpanded] = useState(false);\n const [hoveredChipIdx, setHoveredChipIdx] = useState<number | null>(null);\n const nameInputRef = useRef<HTMLInputElement>(null);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const rafRef = useRef<number>(0);\n\n const isIdentifiedMode = !!config.user;\n\n // Resolve chips: user-provided or default\n const chips: readonly StarterChip[] = config.starterChips ?? DEFAULT_STARTER_CHIPS;\n\n // Calculate position when selected element changes\n const updatePosition = useCallback(() => {\n if (!selectedElement) {\n setPosition(null);\n return;\n }\n const rect = selectedElement.getBoundingClientRect();\n setPosition(calculatePosition(rect));\n }, [selectedElement]);\n\n // Position and animate in when element is selected\n useEffect(() => {\n if (selectedElement) {\n setContent('');\n setIsExpanded(false);\n setHoveredChipIdx(null);\n updatePosition();\n\n // Pre-fill guest name from localStorage (only in guest mode)\n if (!isIdentifiedMode) {\n const guest = getGuestAuthor();\n setName(guest?.name ?? '');\n }\n\n // Trigger entrance animation on next frame\n requestAnimationFrame(() => {\n setIsVisible(true);\n });\n } else {\n setIsVisible(false);\n setPosition(null);\n }\n }, [selectedElement, updatePosition, isIdentifiedMode]);\n\n // Reposition on scroll/resize\n useEffect(() => {\n if (!selectedElement) return;\n\n function handleScrollOrResize() {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => {\n updatePosition();\n });\n }\n\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n cancelAnimationFrame(rafRef.current);\n };\n }, [selectedElement, updatePosition]);\n\n // Track expanded state in a ref so the Escape handler reads current value\n // without needing isExpanded in the effect dependency array.\n const isExpandedRef = useRef(isExpanded);\n isExpandedRef.current = isExpanded;\n\n // Handle Escape key — two-tier: expanded → collapse, collapsed → dismiss\n useEffect(() => {\n if (!selectedElement) return;\n\n function handleKeyDown(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n e.preventDefault();\n e.stopPropagation();\n if (isExpandedRef.current) {\n // First Escape: collapse back to chips\n setIsExpanded(false);\n setContent('');\n } else {\n // Second Escape: dismiss entirely\n onClearSelection();\n }\n }\n }\n\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [selectedElement, onClearSelection]);\n\n // Keyboard capture: printable key while collapsed → expand with first keystroke\n useEffect(() => {\n if (!selectedElement || isExpanded) return;\n\n function handleKeyDown(e: KeyboardEvent) {\n // Ignore modifier keys, Escape, Tab, etc.\n if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {\n e.preventDefault();\n setIsExpanded(true);\n setContent(e.key);\n setTimeout(() => {\n textareaRef.current?.focus();\n if (textareaRef.current) {\n textareaRef.current.selectionStart = textareaRef.current.value.length;\n }\n }, 10);\n }\n }\n\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [selectedElement, isExpanded]);\n\n // Handle click outside\n useEffect(() => {\n if (!selectedElement) return;\n\n function handleMouseDown(e: MouseEvent) {\n const target = e.target as Element;\n if (popoverRef.current?.contains(target)) return;\n if (target.closest('[data-fl-root]')) return;\n onClearSelection();\n }\n\n const timer = setTimeout(() => {\n document.addEventListener('mousedown', handleMouseDown, true);\n }, 50);\n\n return () => {\n clearTimeout(timer);\n document.removeEventListener('mousedown', handleMouseDown, true);\n };\n }, [selectedElement, onClearSelection]);\n\n const [hasError, setHasError] = useState(false);\n\n // --- Screenshot helper (fire-and-forget) ---\n const screenshotsEnabled = config.screenshots !== false;\n\n const fireScreenshotCapture = useCallback(\n (element: Element, commentId: string) => {\n if (!screenshotsEnabled) return;\n captureScreenshot(element).then((result) => {\n if (!result) return;\n adapter\n .uploadScreenshot(config.projectId, commentId, result.blob, result.bounds)\n .catch(() => {\n // Silent failure — screenshot is best-effort\n });\n });\n },\n [screenshotsEnabled, adapter, config.projectId]\n );\n\n // --- Chip submission ---\n const handleChipClick = useCallback(async (chip: StarterChip) => {\n if (!selectedElement) return;\n\n // Capture element ref before clearing selection\n const targetElement = selectedElement;\n const domPath = generateDomPath(targetElement);\n const urlPath = window.location.pathname;\n\n const storedGuest = getGuestAuthor();\n const author = resolveAuthor(config, storedGuest?.name ?? '');\n\n let elementMetadata = null;\n try {\n elementMetadata = captureElementMetadata(targetElement);\n } catch {\n // Metadata capture is best-effort\n }\n\n const newComment: NewComment = {\n project_id: config.projectId,\n thread_id: null,\n author,\n content: chip.value ?? chip.label,\n status: 'open',\n dom_path: domPath,\n url_path: urlPath,\n element_metadata: elementMetadata,\n resolved_by: null,\n resolved_at: null,\n archived_at: null,\n ai_context: null,\n screenshot_url: null,\n element_bounds: null,\n };\n\n try {\n const created = await adapter.addComment(newComment);\n // Fire-and-forget: capture before clearing so highlight/breadcrumb aren't visible\n fireScreenshotCapture(targetElement, created.id);\n onClearSelection();\n } catch {\n // On error, expand to full form with the chip text pre-filled\n setContent(chip.value ?? chip.label);\n setIsExpanded(true);\n setTimeout(() => textareaRef.current?.focus(), 10);\n }\n }, [selectedElement, config, adapter, onClearSelection, fireScreenshotCapture]);\n\n // --- Full form submission ---\n const handleSubmit = useCallback(async () => {\n if (!content.trim() || !selectedElement) return;\n\n // Capture element ref before clearing selection\n const targetElement = selectedElement;\n const domPath = generateDomPath(targetElement);\n const urlPath = window.location.pathname;\n\n const author = resolveAuthor(config, name);\n if (!isIdentifiedMode) {\n persistGuestName(name);\n }\n\n let elementMetadata = null;\n try {\n elementMetadata = captureElementMetadata(targetElement);\n } catch {\n console.warn('Lay: Could not capture element metadata.');\n }\n\n const newComment: NewComment = {\n project_id: config.projectId,\n thread_id: null,\n author,\n content: content.trim(),\n status: 'open',\n dom_path: domPath,\n url_path: urlPath,\n element_metadata: elementMetadata,\n resolved_by: null,\n resolved_at: null,\n archived_at: null,\n ai_context: null,\n screenshot_url: null,\n element_bounds: null,\n };\n\n try {\n const created = await adapter.addComment(newComment);\n setContent('');\n // Fire-and-forget: capture before clearing so highlight/breadcrumb aren't visible\n fireScreenshotCapture(targetElement, created.id);\n onClearSelection();\n } catch {\n setHasError(true);\n setTimeout(() => setHasError(false), 1200);\n }\n }, [content, name, selectedElement, config, adapter, onClearSelection, isIdentifiedMode, fireScreenshotCapture]);\n\n // Handle \"Type...\" click → expand\n const handleTypeHintClick = useCallback(() => {\n setIsExpanded(true);\n setTimeout(() => textareaRef.current?.focus(), 10);\n }, []);\n\n // Handle Enter to submit (Shift+Enter for newline)\n const handleTextareaKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n },\n [handleSubmit]\n );\n\n // Handle Enter on name field — move focus to textarea\n const handleNameKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n textareaRef.current?.focus();\n }\n },\n []\n );\n\n // Focus textarea when expanding\n useEffect(() => {\n if (isExpanded && selectedElement && !content) {\n // Auto-focus: name field if empty and in guest mode, otherwise textarea\n setTimeout(() => {\n if (!isIdentifiedMode) {\n const guest = getGuestAuthor();\n if (!guest?.name) {\n nameInputRef.current?.focus();\n return;\n }\n }\n textareaRef.current?.focus();\n }, 10);\n }\n }, [isExpanded, selectedElement, isIdentifiedMode, content]);\n\n if (!portalRoot || !selectedElement || !position) {\n return null;\n }\n\n const trimmedContent = content.trim();\n\n return createPortal(\n <div\n ref={popoverRef}\n style={{\n ...(isVisible ? popoverVisibleStyles : popoverStyles),\n top: position.top,\n left: position.left,\n maxHeight: isExpanded ? 400 : COMPOSER.COLLAPSED_HEIGHT,\n }}\n data-fl-ignore=\"\"\n >\n {isExpanded ? (\n <div style={expandedFormStyles}>\n {!isIdentifiedMode ? (\n <input\n ref={nameInputRef}\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n onKeyDown={handleNameKeyDown}\n placeholder=\"Your name (optional)\"\n style={nameInputStyles}\n />\n ) : null}\n <textarea\n ref={textareaRef}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n onKeyDown={handleTextareaKeyDown}\n placeholder=\"Leave feedback...\"\n style={\n hasError\n ? { ...textareaStyles, borderColor: '#D32F2F' }\n : textareaStyles\n }\n rows={3}\n />\n <div style={footerStyles}>\n <span style={hintStyles}>Esc ↩ collapse</span>\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={!trimmedContent}\n style={\n trimmedContent ? submitButtonStyles : submitButtonDisabledStyles\n }\n >\n Submit\n </button>\n </div>\n </div>\n ) : (\n <div style={chipBarStyles}>\n {chips.map((chip, i) => (\n <button\n key={chip.label}\n type=\"button\"\n onClick={() => handleChipClick(chip)}\n onMouseEnter={() => setHoveredChipIdx(i)}\n onMouseLeave={() => setHoveredChipIdx(null)}\n style={hoveredChipIdx === i ? chipHoverStyles : chipStyles}\n >\n {chip.label}\n </button>\n ))}\n <span\n style={typeHintStyles}\n onClick={handleTypeHintClick}\n role=\"button\"\n tabIndex={0}\n >\n Type...\n </span>\n </div>\n )}\n </div>,\n portalRoot\n );\n}\n","const STORAGE_KEY = 'fl-author';\n\ninterface GuestAuthor {\n name: string;\n}\n\n/**\n * Read the saved guest author name from localStorage.\n * Returns null if no name is saved or localStorage is unavailable.\n */\nexport function getGuestAuthor(): GuestAuthor | null {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed.name === 'string' && parsed.name.trim()) {\n return { name: parsed.name.trim() };\n }\n return null;\n } catch {\n // localStorage unavailable (private browsing, SSR, etc.)\n return null;\n }\n}\n\n/**\n * Persist a guest author name to localStorage.\n * Silently fails if localStorage is unavailable.\n */\nexport function saveGuestAuthor(name: string): void {\n try {\n const trimmed = name.trim();\n if (!trimmed) return;\n localStorage.setItem(STORAGE_KEY, JSON.stringify({ name: trimmed }));\n } catch {\n // localStorage unavailable — silently ignore\n }\n}\n","import { ANONYMOUS_AUTHOR } from './constants';\nimport { saveGuestAuthor } from './guestAuthor';\nimport type { Author } from '../types/comment';\nimport type { LayConfig } from '../types/config';\n\n/**\n * Resolve the author for a comment based on whether the developer\n * provided a user identity (identified mode) or not (guest mode).\n *\n * In identified mode, returns `config.user`.\n * In guest mode, returns a guest author from the name field,\n * or ANONYMOUS_AUTHOR if the name is blank.\n */\nexport function resolveAuthor(\n config: LayConfig,\n guestName: string\n): Author {\n if (config.user) {\n return config.user;\n }\n\n const trimmedName = guestName.trim();\n if (trimmedName) {\n return { id: null, name: trimmedName, avatar: null };\n }\n\n return { ...ANONYMOUS_AUTHOR };\n}\n\n/**\n * Persist the guest name to localStorage if non-empty.\n * No-op in identified mode — the caller decides when to call this.\n */\nexport function persistGuestName(name: string): void {\n const trimmed = name.trim();\n if (trimmed) {\n saveGuestAuthor(trimmed);\n }\n}\n","import type { ElementBounds } from '../types/comment';\nimport { DATA_ATTRS } from '../lib/constants';\n\nconst WEBP_QUALITY = 0.65;\n\nexport interface CaptureResult {\n blob: Blob;\n bounds: ElementBounds;\n}\n\n/**\n * Capture a viewport screenshot using html-to-image (dynamic import).\n * Returns the WebP blob and the target element's bounding box.\n *\n * Silent failure: returns null if html-to-image is not installed,\n * the import fails, or the capture errors.\n */\nexport async function captureScreenshot(\n element: Element\n): Promise<CaptureResult | null> {\n // Record bounding box before capture (viewport-relative from getBoundingClientRect)\n const rect = element.getBoundingClientRect();\n const bounds: ElementBounds = {\n x: Math.round(rect.x),\n y: Math.round(rect.y),\n width: Math.round(rect.width),\n height: Math.round(rect.height),\n };\n\n // Dynamic import — zero bundle cost if html-to-image is not installed\n let toCanvas: (\n node: HTMLElement,\n options?: Record<string, unknown>\n ) => Promise<HTMLCanvasElement>;\n\n try {\n const mod = await import('html-to-image');\n toCanvas = mod.toCanvas;\n } catch {\n console.info(\n 'Lay: Screenshot capture unavailable. This may be caused by a CSP restriction or bundler issue.'\n );\n return null;\n }\n\n // Capture the full page, then crop to viewport\n let croppedCanvas: HTMLCanvasElement;\n try {\n const fullCanvas = await toCanvas(document.body, {\n // Exclude our own feedback layer UI\n filter: (el: Element) => !el.hasAttribute?.(DATA_ATTRS.IGNORE),\n // Force 1:1 CSS-to-canvas pixel mapping so crop coordinates match\n pixelRatio: 1,\n });\n\n // Crop to the visible viewport\n croppedCanvas = document.createElement('canvas');\n croppedCanvas.width = window.innerWidth;\n croppedCanvas.height = window.innerHeight;\n const ctx = croppedCanvas.getContext('2d');\n if (!ctx) return null;\n\n ctx.drawImage(\n fullCanvas,\n window.scrollX,\n window.scrollY,\n window.innerWidth,\n window.innerHeight,\n 0,\n 0,\n window.innerWidth,\n window.innerHeight\n );\n } catch {\n return null;\n }\n\n // Compress to WebP\n return new Promise((resolve) => {\n croppedCanvas.toBlob(\n (blob) => {\n if (!blob) {\n resolve(null);\n return;\n }\n resolve({ blob, bounds });\n },\n 'image/webp',\n WEBP_QUALITY\n );\n });\n}\n","'use client';\n\nimport { useElementSelector } from './hooks/useElementSelector';\nimport { ElementHighlighter } from './ElementHighlighter';\nimport { ElementBreadcrumb } from './ElementBreadcrumb';\nimport { CommentAnchor } from './CommentAnchor';\n\n/**\n * Owns the shared element selector state and renders both the\n * highlight overlay and the comment input popover.\n */\nexport function CommentLayer() {\n const { hoveredElement, selectedElement, clearSelection } =\n useElementSelector();\n\n return (\n <>\n <ElementHighlighter\n hoveredElement={hoveredElement}\n selectedElement={selectedElement}\n />\n <ElementBreadcrumb\n hoveredElement={hoveredElement}\n selectedElement={selectedElement}\n />\n <CommentAnchor\n selectedElement={selectedElement}\n onClearSelection={clearSelection}\n />\n </>\n );\n}\n","'use client';\n\nimport { useRef, useEffect, useMemo } from 'react';\nimport { useComments } from './hooks/useComments';\nimport { useLayContext } from './hooks/useLayContext';\nimport { CommentDot } from './CommentDot';\nimport { resolveElement } from './utils/resolveElement';\nimport { generateDomPath } from './utils/domPath';\nimport type { ElementFingerprint } from './types/comment';\n\n/** Self-healing threshold: only PATCH dom_path when fingerprint score >= 60 */\nconst SELF_HEAL_THRESHOLD = 60;\n\n/**\n * Renders a CommentDot for each unique dom_path that has comments.\n * Hides dots for fully-archived dom_paths. Mutes dots for fully-resolved ones.\n * Runs 3-layer element resolution and tracks detached comments.\n * Always visible — dots show regardless of comment mode.\n */\nexport function CommentDots() {\n const {\n groupedByDomPath,\n isDomPathFullyResolved,\n isDomPathFullyArchived,\n } = useComments();\n const { adapter, setDetachedDomPaths } = useLayContext();\n\n // Track dom_paths present on first render — dots appearing later are \"new\"\n const initialPathsRef = useRef<Set<string> | null>(null);\n if (initialPathsRef.current === null) {\n initialPathsRef.current = new Set(groupedByDomPath.map((g) => g.domPath));\n }\n\n // Track which dom_paths we've already self-healed to avoid duplicate PATCHes\n const healedRef = useRef<Set<string>>(new Set());\n\n // Resolve elements for all non-archived groups\n const activeGroups = useMemo(\n () => groupedByDomPath.filter((group) => !isDomPathFullyArchived(group.domPath)),\n [groupedByDomPath, isDomPathFullyArchived]\n );\n\n // Run element resolution and collect results\n const resolutionResults = useMemo(() => {\n const results = new Map<string, { element: Element | null; method: string; score?: number }>();\n\n for (const group of activeGroups) {\n // Extract fingerprint from the first comment that has one\n let fingerprint: ElementFingerprint | null = null;\n for (const comment of group.comments) {\n const fp = comment.element_metadata?.fingerprint;\n if (fp) {\n fingerprint = fp;\n break;\n }\n }\n\n const result = resolveElement(group.domPath, fingerprint);\n results.set(group.domPath, result);\n }\n\n return results;\n }, [activeGroups]);\n\n // Update detached dom_paths in context\n useEffect(() => {\n const detached = new Set<string>();\n for (const [domPath, result] of resolutionResults) {\n if (result.method === 'detached') {\n detached.add(domPath);\n }\n }\n setDetachedDomPaths(detached);\n }, [resolutionResults, setDetachedDomPaths]);\n\n // Self-healing: when fingerprint match succeeds with score >= 60, PATCH dom_path\n useEffect(() => {\n for (const group of activeGroups) {\n const result = resolutionResults.get(group.domPath);\n if (\n result &&\n result.method === 'fingerprint' &&\n result.element &&\n result.score !== undefined &&\n result.score >= SELF_HEAL_THRESHOLD &&\n !healedRef.current.has(group.domPath)\n ) {\n healedRef.current.add(group.domPath);\n const newDomPath = generateDomPath(result.element);\n\n // Fire-and-forget: PATCH each root comment's dom_path\n for (const comment of group.comments) {\n if (comment.thread_id === null) {\n adapter.updateComment(comment.id, { dom_path: newDomPath }).catch(() => {\n // Silent failure — next page load will retry\n });\n }\n }\n }\n }\n }, [activeGroups, resolutionResults, adapter]);\n\n if (activeGroups.length === 0) {\n return null;\n }\n\n return (\n <>\n {activeGroups.map((group) => {\n const result = resolutionResults.get(group.domPath);\n // Skip detached comments — they'll appear in the detached panel\n if (!result || result.method === 'detached') return null;\n\n return (\n <CommentDot\n key={group.domPath}\n domPath={group.domPath}\n comments={group.comments}\n isResolved={isDomPathFullyResolved(group.domPath)}\n isNew={!initialPathsRef.current!.has(group.domPath)}\n resolvedElement={result.element}\n />\n );\n })}\n </>\n );\n}\n","'use client';\n\nimport React, {\n useState,\n useEffect,\n useCallback,\n useRef,\n memo,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { CommentThread } from './CommentThread';\nimport { Z_INDEX, SIZING, CONFETTI, WHISPER } from './lib/constants';\nimport { formatRelativeTime } from './utils/time';\nimport type { Comment } from './types/comment';\n\n// --- Particle generation for resolve burst ---\n\ninterface ParticleConfig {\n endX: number;\n endY: number;\n size: number;\n color: string;\n delay: number;\n}\n\nfunction generateParticles(count: number): ParticleConfig[] {\n const particles: ParticleConfig[] = [];\n const centerAngle = 90; // straight up\n const halfSpread = CONFETTI.SPREAD_ANGLE / 2;\n\n for (let i = 0; i < count; i++) {\n const baseAngle =\n centerAngle - halfSpread + (CONFETTI.SPREAD_ANGLE / (count - 1)) * i;\n const angle = baseAngle + (Math.random() - 0.5) * 15; // ±7.5° jitter\n const distance =\n CONFETTI.DISTANCE_MIN +\n Math.random() * (CONFETTI.DISTANCE_MAX - CONFETTI.DISTANCE_MIN);\n const radians = (angle * Math.PI) / 180;\n\n const endX = Math.cos(radians) * distance;\n const endY = -Math.sin(radians) * distance; // negative = upward\n\n const size =\n CONFETTI.PARTICLE_SIZE_MIN +\n Math.random() * (CONFETTI.PARTICLE_SIZE_MAX - CONFETTI.PARTICLE_SIZE_MIN);\n\n const colors = ['var(--fl-accent)', 'var(--fl-accent-hover)', '#F4A261'];\n const color = colors[i % colors.length];\n\n particles.push({ endX, endY, size, color, delay: i * 20 });\n }\n return particles;\n}\n\n// --- Styles ---\n\nconst markerStyles: React.CSSProperties = {\n position: 'fixed',\n minWidth: SIZING.MARKER_MIN_WIDTH,\n height: SIZING.MARKER_HEIGHT,\n padding: '0 5px',\n borderRadius: 4,\n border: 'none',\n backgroundColor: 'var(--fl-accent)',\n color: '#FFFFFF',\n fontSize: SIZING.MARKER_FONT_SIZE,\n fontFamily: 'var(--fl-font-family)',\n fontWeight: 600,\n lineHeight: `${SIZING.MARKER_HEIGHT}px`,\n textAlign: 'center' as const,\n cursor: 'pointer',\n zIndex: Z_INDEX.DOT,\n outline: 'none',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n animation: `fl-marker-enter var(--fl-duration-marker-enter) ease-out`,\n transition: `transform var(--fl-duration-fast) ease-out`,\n boxSizing: 'border-box' as const,\n};\n\nconst markerHoverStyles: React.CSSProperties = {\n ...markerStyles,\n transform: 'scale(1.15)',\n};\n\nconst markerResolvedStyles: React.CSSProperties = {\n ...markerStyles,\n backgroundColor: 'var(--fl-resolved)',\n};\n\nconst markerResolvedHoverStyles: React.CSSProperties = {\n ...markerResolvedStyles,\n transform: 'scale(1.15)',\n};\n\nconst pointerStyles: React.CSSProperties = {\n position: 'absolute',\n bottom: -SIZING.MARKER_POINTER,\n left: '50%',\n transform: 'translateX(-50%)',\n width: 0,\n height: 0,\n borderLeft: `${SIZING.MARKER_POINTER}px solid transparent`,\n borderRight: `${SIZING.MARKER_POINTER}px solid transparent`,\n borderTopWidth: SIZING.MARKER_POINTER,\n borderTopStyle: 'solid',\n borderTopColor: 'var(--fl-accent)',\n pointerEvents: 'none' as const,\n};\n\nconst pointerResolvedStyles: React.CSSProperties = {\n ...pointerStyles,\n borderTopColor: 'var(--fl-resolved)',\n};\n\n// --- Whisper tooltip ---\n\nconst whisperStyles: React.CSSProperties = {\n position: 'absolute',\n top: `calc(100% + ${SIZING.MARKER_POINTER + 6}px)`,\n left: '50%',\n transform: 'translateX(-50%)',\n backgroundColor: 'var(--fl-text-primary)',\n color: 'var(--fl-surface)',\n padding: '6px 10px',\n borderRadius: 'var(--fl-border-radius-sm)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n lineHeight: 1.4,\n whiteSpace: 'nowrap',\n pointerEvents: 'none',\n zIndex: Z_INDEX.POPOVER,\n boxShadow: 'var(--fl-shadow)',\n maxWidth: 260,\n opacity: 0,\n transition: `opacity var(--fl-duration-fast) ease-out`,\n};\n\nconst whisperVisibleStyles: React.CSSProperties = {\n ...whisperStyles,\n opacity: 1,\n};\n\nconst whisperMetaStyles: React.CSSProperties = {\n display: 'flex',\n gap: 6,\n alignItems: 'baseline',\n};\n\nconst whisperAuthorStyles: React.CSSProperties = {\n fontWeight: 'var(--fl-font-weight-medium)' as unknown as number,\n color: 'var(--fl-surface)',\n};\n\nconst whisperTimeStyles: React.CSSProperties = {\n color: 'rgba(255, 255, 255, 0.55)',\n};\n\nconst whisperContentStyles: React.CSSProperties = {\n color: 'rgba(255, 255, 255, 0.8)',\n whiteSpace: 'normal',\n marginTop: 2,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n display: '-webkit-box',\n WebkitLineClamp: 1,\n WebkitBoxOrient: 'vertical',\n};\n\nfunction truncateContent(text: string): string {\n if (text.length <= WHISPER.MAX_CONTENT_LENGTH) return text;\n return text.slice(0, WHISPER.MAX_CONTENT_LENGTH).trimEnd() + '\\u2026';\n}\n\n// --- Position ---\n\ninterface MarkerPosition {\n top: number;\n left: number;\n visible: boolean;\n}\n\nfunction calculateMarkerPosition(element: Element): MarkerPosition {\n const rect = element.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n // Off-screen check\n if (\n rect.bottom < 0 ||\n rect.top > viewportHeight ||\n rect.right < 0 ||\n rect.left > viewportWidth\n ) {\n return { top: 0, left: 0, visible: false };\n }\n\n // Position badge above element's top-right, with pointer touching down\n const top = rect.top - SIZING.MARKER_HEIGHT - SIZING.MARKER_POINTER;\n const left = rect.right - SIZING.MARKER_MIN_WIDTH / 2;\n\n return { top, left, visible: true };\n}\n\n// --- Component ---\n\ninterface CommentDotProps {\n domPath: string;\n comments: Comment[];\n isResolved?: boolean;\n isNew?: boolean;\n onDidResolve?: () => void;\n /** Pre-resolved element from 3-layer resolution (M6). Falls back to querySelector if not provided. */\n resolvedElement?: Element | null;\n}\n\nexport const CommentDot = memo(function CommentDot({\n domPath,\n comments,\n isResolved = false,\n isNew = false,\n resolvedElement,\n}: CommentDotProps) {\n const { portalRoot } = useLayContext();\n const [position, setPosition] = useState<MarkerPosition | null>(null);\n const [isHovered, setIsHovered] = useState(false);\n const [showWhisper, setShowWhisper] = useState(false);\n const [isPopoverOpen, setIsPopoverOpen] = useState(false);\n const [showRipple, setShowRipple] = useState(isNew);\n const [showBurst, setShowBurst] = useState(false);\n const [burstParticles, setBurstParticles] = useState<ParticleConfig[]>([]);\n const dotRef = useRef<HTMLButtonElement>(null);\n const rafRef = useRef<number>(0);\n const whisperTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Find the target element and calculate position\n const updatePosition = useCallback(() => {\n // Use pre-resolved element (M6 3-layer resolution) or fall back to querySelector\n const element = resolvedElement ?? document.querySelector(domPath);\n if (!element) {\n setPosition(null);\n return;\n }\n setPosition(calculateMarkerPosition(element));\n }, [domPath, resolvedElement]);\n\n // Initial position calculation\n useEffect(() => {\n updatePosition();\n }, [updatePosition]);\n\n // Recalculate on scroll and resize\n useEffect(() => {\n function handleScrollOrResize() {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => {\n updatePosition();\n });\n }\n\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n cancelAnimationFrame(rafRef.current);\n };\n }, [updatePosition]);\n\n // Self-clean ripple element after animation completes\n useEffect(() => {\n if (!showRipple) return;\n const timer = setTimeout(() => setShowRipple(false), CONFETTI.DURATION); // 400ms ripple ≈ 450 safe\n return () => clearTimeout(timer);\n }, [showRipple]);\n\n // Callback for CommentThread to signal a local resolve action — directly fires burst\n const handleDidResolve = useCallback(() => {\n setBurstParticles(generateParticles(CONFETTI.PARTICLE_COUNT));\n setShowBurst(true);\n\n const timer = setTimeout(() => {\n setShowBurst(false);\n setBurstParticles([]);\n }, CONFETTI.DURATION + 100);\n return () => clearTimeout(timer);\n }, []);\n\n const handleMouseEnter = useCallback(() => {\n setIsHovered(true);\n whisperTimerRef.current = setTimeout(() => {\n setShowWhisper(true);\n }, WHISPER.HOVER_DELAY);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n if (whisperTimerRef.current) {\n clearTimeout(whisperTimerRef.current);\n whisperTimerRef.current = null;\n }\n setShowWhisper(false);\n }, []);\n\n // Clean up whisper timer on unmount\n useEffect(() => {\n return () => {\n if (whisperTimerRef.current) {\n clearTimeout(whisperTimerRef.current);\n }\n };\n }, []);\n\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n setShowWhisper(false);\n if (whisperTimerRef.current) {\n clearTimeout(whisperTimerRef.current);\n whisperTimerRef.current = null;\n }\n setIsPopoverOpen((prev) => !prev);\n },\n []\n );\n\n const handleClosePopover = useCallback(() => {\n setIsPopoverOpen(false);\n }, []);\n\n if (!portalRoot || !position || !position.visible) {\n return null;\n }\n\n const count = comments.length;\n\n // Determine marker content: initial letter for 1 comment, count for 2+\n let markerContent: string;\n if (count === 1) {\n const lastAuthor = comments[comments.length - 1].author.name;\n markerContent = lastAuthor.charAt(0).toUpperCase();\n } else {\n markerContent = count > SIZING.MARKER_MAX_COUNT ? '99+' : `${count}`;\n }\n\n // Pick style based on resolved status + hover\n let currentStyle: React.CSSProperties;\n if (isResolved) {\n currentStyle = isHovered ? markerResolvedHoverStyles : markerResolvedStyles;\n } else {\n currentStyle = isHovered ? markerHoverStyles : markerStyles;\n }\n\n return createPortal(\n <>\n <button\n ref={dotRef}\n type=\"button\"\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n style={{\n ...currentStyle,\n top: position.top,\n left: position.left,\n }}\n aria-label={`${count} comment${count > 1 ? 's' : ''}${isResolved ? ' (resolved)' : ''}`}\n data-fl-ignore=\"\"\n >\n {markerContent}\n <span style={isResolved ? pointerResolvedStyles : pointerStyles} />\n\n {/* Arrival ripple — only for newly placed comments */}\n {showRipple ? (\n <span\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n width: SIZING.MARKER_HEIGHT,\n height: SIZING.MARKER_HEIGHT,\n borderRadius: '50%',\n border: '2px solid var(--fl-accent)',\n animation:\n 'fl-marker-ripple var(--fl-duration-marker-ripple) ease-out forwards',\n pointerEvents: 'none' as const,\n }}\n />\n ) : null}\n\n {/* Resolve micro-confetti burst */}\n {showBurst\n ? burstParticles.map((p, i) => (\n <span\n key={i}\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n width: p.size,\n height: p.size,\n borderRadius: '50%',\n backgroundColor: p.color,\n pointerEvents: 'none' as const,\n '--fl-end-dx': `${p.endX}px`,\n '--fl-end-dy': `${p.endY}px`,\n '--fl-dx': '0px',\n '--fl-dy': '0px',\n animation: `fl-resolve-particle var(--fl-duration-resolve-burst) ease-out ${p.delay}ms forwards`,\n } as React.CSSProperties}\n />\n ))\n : null}\n\n {/* Thread Whisper tooltip */}\n {!isPopoverOpen && comments.length > 0 && (\n <span style={showWhisper ? whisperVisibleStyles : whisperStyles}>\n <span style={whisperMetaStyles}>\n <span style={whisperAuthorStyles}>{comments[0].author.name}</span>\n <span style={whisperTimeStyles}>{formatRelativeTime(comments[0].created_at)}</span>\n </span>\n <div style={whisperContentStyles}>\n {truncateContent(comments[0].content)}\n </div>\n </span>\n )}\n </button>\n {isPopoverOpen && dotRef.current ? (\n <CommentThread\n comments={comments}\n dotRect={dotRef.current.getBoundingClientRect()}\n onClose={handleClosePopover}\n onDidResolve={handleDidResolve}\n />\n ) : null}\n </>,\n portalRoot\n );\n});\n","'use client';\n\nimport React, { useEffect, useRef, useMemo, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useLayContext } from './hooks/useLayContext';\nimport { CommentItem } from './CommentItem';\nimport { CommentActions } from './CommentActions';\nimport { ReplyInput } from './ReplyInput';\nimport { resolveAuthor } from './lib/authorResolver';\nimport { Z_INDEX } from './lib/constants';\nimport type { Comment } from './types/comment';\n\n// --- Styles ---\n\nconst panelStyles: React.CSSProperties = {\n position: 'fixed',\n zIndex: Z_INDEX.POPOVER,\n width: 320,\n maxHeight: 400,\n backgroundColor: 'var(--fl-surface-raised)',\n border: '1px solid var(--fl-border)',\n borderRadius: 'var(--fl-border-radius)',\n boxShadow: 'var(--fl-shadow-popover)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-base)',\n color: 'var(--fl-text-primary)',\n display: 'flex',\n flexDirection: 'column' as const,\n overflow: 'hidden',\n opacity: 0,\n transform: 'translateY(4px)',\n transition: `opacity var(--fl-duration-fast) ease-out, transform var(--fl-duration-fast) ease-out`,\n};\n\nconst panelVisibleStyles: React.CSSProperties = {\n ...panelStyles,\n opacity: 1,\n transform: 'translateY(0)',\n};\n\nconst scrollContainerStyles: React.CSSProperties = {\n overflowY: 'auto',\n padding: 12,\n flexGrow: 1,\n minHeight: 0,\n};\n\nconst dividerStyles: React.CSSProperties = {\n height: 1,\n backgroundColor: 'var(--fl-border)',\n margin: '0',\n border: 'none',\n flexShrink: 0,\n};\n\nconst replyDividerStyles: React.CSSProperties = {\n ...dividerStyles,\n backgroundColor: 'var(--fl-border-strong)',\n};\n\n// --- Position ---\n\nconst PANEL_GAP = 8;\nconst PANEL_WIDTH = 320;\nconst PANEL_EST_HEIGHT = 200;\n\ninterface PanelPosition {\n top: number;\n left: number;\n}\n\nfunction calculatePanelPosition(dotRect: DOMRect): PanelPosition {\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n // Default: below and to the left of the dot\n let top = dotRect.bottom + PANEL_GAP;\n let left = dotRect.right - PANEL_WIDTH;\n\n // Flip above if not enough room below\n if (top + PANEL_EST_HEIGHT > viewportHeight) {\n top = dotRect.top - PANEL_EST_HEIGHT - PANEL_GAP;\n }\n\n // Clamp horizontal\n if (left < PANEL_GAP) {\n left = PANEL_GAP;\n }\n if (left + PANEL_WIDTH > viewportWidth - PANEL_GAP) {\n left = viewportWidth - PANEL_WIDTH - PANEL_GAP;\n }\n\n // Clamp vertical\n if (top < PANEL_GAP) {\n top = PANEL_GAP;\n }\n\n return { top, left };\n}\n\n// --- Thread organization ---\n\ninterface ThreadGroup {\n root: Comment;\n replies: Comment[];\n}\n\nfunction organizeThreads(comments: Comment[]): ThreadGroup[] {\n const roots: Comment[] = [];\n const repliesByThreadId = new Map<string, Comment[]>();\n\n for (const comment of comments) {\n if (comment.thread_id === null) {\n roots.push(comment);\n } else {\n const existing = repliesByThreadId.get(comment.thread_id);\n if (existing) {\n existing.push(comment);\n } else {\n repliesByThreadId.set(comment.thread_id, [comment]);\n }\n }\n }\n\n // Sort roots chronologically (oldest first)\n roots.sort(\n (a, b) =>\n new Date(a.created_at).getTime() - new Date(b.created_at).getTime()\n );\n\n return roots.map((root) => {\n const replies = repliesByThreadId.get(root.id) ?? [];\n // Sort replies chronologically (oldest first)\n replies.sort(\n (a, b) =>\n new Date(a.created_at).getTime() - new Date(b.created_at).getTime()\n );\n return { root, replies };\n });\n}\n\n// --- Component ---\n\ninterface CommentThreadProps {\n comments: Comment[];\n dotRect: DOMRect;\n onClose: () => void;\n onDidResolve?: () => void;\n}\n\nexport function CommentThread({\n comments,\n dotRect,\n onClose,\n onDidResolve,\n}: CommentThreadProps) {\n const { portalRoot, adapter, config, dispatch } = useLayContext();\n const panelRef = useRef<HTMLDivElement>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n const [isVisible, setIsVisible] = React.useState(false);\n\n const position = calculatePanelPosition(dotRect);\n const threads = useMemo(() => organizeThreads(comments), [comments]);\n\n // Determine the root comment for the reply input.\n // Use the first root comment (thread_id: null); if all are roots, use the first.\n const replyTarget = threads.length > 0 ? threads[0].root : null;\n\n // --- Status transition handlers (optimistic) ---\n\n const handleResolve = useCallback(\n (comment: Comment) => {\n const author = resolveAuthor(config, '');\n const now = new Date().toISOString();\n const optimistic: Comment = {\n ...comment,\n status: 'resolved',\n resolved_by: author,\n resolved_at: now,\n };\n\n // Optimistic update\n dispatch({ type: 'UPDATE_COMMENT', payload: optimistic });\n\n // Signal local resolve to parent (triggers confetti burst) and close panel\n onDidResolve?.();\n onClose();\n\n // Persist\n adapter\n .updateComment(comment.id, {\n status: 'resolved',\n resolved_by: author,\n resolved_at: now,\n })\n .catch(() => {\n // Revert on failure\n dispatch({ type: 'UPDATE_COMMENT', payload: comment });\n });\n },\n [adapter, config, dispatch, onDidResolve, onClose]\n );\n\n const handleReopen = useCallback(\n (comment: Comment) => {\n const optimistic: Comment = {\n ...comment,\n status: 'open',\n resolved_by: null,\n resolved_at: null,\n };\n\n dispatch({ type: 'UPDATE_COMMENT', payload: optimistic });\n\n adapter\n .updateComment(comment.id, {\n status: 'open',\n resolved_by: null,\n resolved_at: null,\n })\n .catch(() => {\n dispatch({ type: 'UPDATE_COMMENT', payload: comment });\n });\n },\n [adapter, dispatch]\n );\n\n const handleArchive = useCallback(\n (comment: Comment) => {\n const now = new Date().toISOString();\n const optimistic: Comment = {\n ...comment,\n status: 'archived',\n archived_at: now,\n };\n\n dispatch({ type: 'UPDATE_COMMENT', payload: optimistic });\n\n adapter\n .updateComment(comment.id, {\n status: 'archived',\n archived_at: now,\n })\n .catch(() => {\n dispatch({ type: 'UPDATE_COMMENT', payload: comment });\n });\n },\n [adapter, dispatch]\n );\n\n // Animate in\n useEffect(() => {\n requestAnimationFrame(() => {\n setIsVisible(true);\n });\n }, []);\n\n // Scroll to bottom on open so most recent comments are visible\n useEffect(() => {\n if (isVisible && scrollRef.current) {\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\n }\n }, [isVisible]);\n\n // Scroll to bottom when new comments arrive\n const prevCountRef = useRef(comments.length);\n useEffect(() => {\n if (comments.length > prevCountRef.current && scrollRef.current) {\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\n }\n prevCountRef.current = comments.length;\n }, [comments.length]);\n\n const handleReplySubmitted = useCallback(() => {\n // Scroll will happen via the comments.length effect above\n }, []);\n\n // Handle Escape\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n e.preventDefault();\n onClose();\n }\n }\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [onClose]);\n\n // Handle click outside\n useEffect(() => {\n function handleMouseDown(e: MouseEvent) {\n const target = e.target as Element;\n if (panelRef.current?.contains(target)) return;\n if (target.closest('[data-fl-ignore]')) return;\n onClose();\n }\n\n const timer = setTimeout(() => {\n document.addEventListener('mousedown', handleMouseDown, true);\n }, 50);\n\n return () => {\n clearTimeout(timer);\n document.removeEventListener('mousedown', handleMouseDown, true);\n };\n }, [onClose]);\n\n if (!portalRoot) return null;\n\n return createPortal(\n <div\n ref={panelRef}\n style={{\n ...(isVisible ? panelVisibleStyles : panelStyles),\n top: position.top,\n left: position.left,\n }}\n data-fl-ignore=\"\"\n >\n <div ref={scrollRef} style={scrollContainerStyles}>\n {threads.map((thread, threadIndex) => (\n <React.Fragment key={thread.root.id}>\n {threadIndex > 0 ? <hr style={dividerStyles} /> : null}\n <CommentItem\n comment={thread.root}\n actions={\n <CommentActions\n comment={thread.root}\n onResolve={handleResolve}\n onReopen={handleReopen}\n onArchive={handleArchive}\n />\n }\n />\n {thread.replies.map((reply) => (\n <CommentItem key={reply.id} comment={reply} isReply />\n ))}\n </React.Fragment>\n ))}\n </div>\n {replyTarget ? (\n <>\n <hr style={replyDividerStyles} />\n <ReplyInput\n rootComment={replyTarget}\n onReplySubmitted={handleReplySubmitted}\n />\n </>\n ) : null}\n </div>,\n portalRoot\n );\n}\n","import React, { type ReactNode } from 'react';\nimport { formatRelativeTime } from './utils/time';\nimport type { Comment } from './types/comment';\n\n// --- Styles ---\n\nconst itemStyles: React.CSSProperties = {\n padding: '10px 0',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst replyItemStyles: React.CSSProperties = {\n ...itemStyles,\n marginLeft: 16,\n paddingLeft: 12,\n borderLeft: '2px solid var(--fl-border)',\n};\n\nconst headerStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: 4,\n};\n\nconst headerLeftStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n minWidth: 0,\n};\n\nconst authorStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n color: 'var(--fl-text-secondary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst timestampStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst contentStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-base)',\n lineHeight: 'var(--fl-line-height)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n};\n\nconst resolvedIndicatorStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 4,\n marginTop: 6,\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst resolvedCheckStyles: React.CSSProperties = {\n color: 'var(--fl-accent)',\n fontSize: 12,\n};\n\n// --- Component ---\n\ninterface CommentItemProps {\n comment: Comment;\n isReply?: boolean;\n actions?: ReactNode;\n}\n\nexport function CommentItem({ comment, isReply = false, actions }: CommentItemProps) {\n return (\n <div style={isReply ? replyItemStyles : itemStyles}>\n <div style={headerStyles}>\n <div style={headerLeftStyles}>\n <span style={authorStyles}>{comment.author.name}</span>\n <span style={timestampStyles}>\n {formatRelativeTime(comment.created_at)}\n </span>\n </div>\n {actions}\n </div>\n <div style={contentStyles}>{comment.content}</div>\n {comment.status === 'resolved' && comment.resolved_by && !isReply && (\n <div style={resolvedIndicatorStyles}>\n <span style={resolvedCheckStyles}>✓</span>\n <span>\n Resolved by {comment.resolved_by.name}\n {comment.resolved_at\n ? ` \\u00B7 ${formatRelativeTime(comment.resolved_at)}`\n : ''}\n </span>\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport React, { useState } from 'react';\nimport type { Comment, Author } from './types/comment';\n\n// --- Styles ---\n\nconst actionsContainerStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 4,\n};\n\nconst iconButtonStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: 24,\n height: 24,\n padding: 0,\n border: 'none',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'transparent',\n color: 'var(--fl-text-tertiary)',\n cursor: 'pointer',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 14,\n lineHeight: 1,\n transition: `color var(--fl-duration-fast) ease-out, background-color var(--fl-duration-fast) ease-out`,\n};\n\nconst iconButtonHoverStyles: React.CSSProperties = {\n ...iconButtonStyles,\n color: 'var(--fl-accent)',\n backgroundColor: 'var(--fl-accent-subtle)',\n};\n\nconst textButtonStyles: React.CSSProperties = {\n display: 'inline-flex',\n alignItems: 'center',\n padding: '2px 6px',\n border: 'none',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'transparent',\n color: 'var(--fl-text-tertiary)',\n cursor: 'pointer',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n lineHeight: 1,\n transition: `color var(--fl-duration-fast) ease-out`,\n};\n\nconst textButtonHoverStyles: React.CSSProperties = {\n ...textButtonStyles,\n color: 'var(--fl-text-secondary)',\n};\n\n// --- Component ---\n\ninterface CommentActionsProps {\n comment: Comment;\n onResolve: (comment: Comment) => void;\n onReopen: (comment: Comment) => void;\n onArchive: (comment: Comment) => void;\n}\n\nexport function CommentActions({\n comment,\n onResolve,\n onReopen,\n onArchive,\n}: CommentActionsProps) {\n const [resolveHovered, setResolveHovered] = useState(false);\n const [reopenHovered, setReopenHovered] = useState(false);\n const [archiveHovered, setArchiveHovered] = useState(false);\n\n if (comment.status === 'open') {\n return (\n <div style={actionsContainerStyles}>\n <button\n type=\"button\"\n title=\"Resolve\"\n style={resolveHovered ? iconButtonHoverStyles : iconButtonStyles}\n onMouseEnter={() => setResolveHovered(true)}\n onMouseLeave={() => setResolveHovered(false)}\n onClick={(e) => {\n e.stopPropagation();\n onResolve(comment);\n }}\n >\n ✓\n </button>\n </div>\n );\n }\n\n if (comment.status === 'resolved') {\n return (\n <div style={actionsContainerStyles}>\n <button\n type=\"button\"\n title=\"Reopen\"\n style={reopenHovered ? iconButtonHoverStyles : iconButtonStyles}\n onMouseEnter={() => setReopenHovered(true)}\n onMouseLeave={() => setReopenHovered(false)}\n onClick={(e) => {\n e.stopPropagation();\n onReopen(comment);\n }}\n >\n ↩\n </button>\n <button\n type=\"button\"\n title=\"Archive\"\n style={archiveHovered ? textButtonHoverStyles : textButtonStyles}\n onMouseEnter={() => setArchiveHovered(true)}\n onMouseLeave={() => setArchiveHovered(false)}\n onClick={(e) => {\n e.stopPropagation();\n onArchive(comment);\n }}\n >\n Archive\n </button>\n </div>\n );\n }\n\n // Archived — no actions shown in thread (restore is in ArchivedThreadsPanel)\n return null;\n}\n","'use client';\n\nimport React, { useState, useCallback, useRef, useEffect } from 'react';\nimport { useLayContext } from './hooks/useLayContext';\nimport { getGuestAuthor } from './lib/guestAuthor';\nimport { resolveAuthor, persistGuestName } from './lib/authorResolver';\nimport type { Comment, NewComment } from './types/comment';\n\n// --- Styles ---\n\nconst containerStyles: React.CSSProperties = {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: 6,\n padding: 12,\n};\n\nconst nameInputStyles: React.CSSProperties = {\n width: '100%',\n padding: '4px 8px',\n border: '1px solid var(--fl-border)',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'var(--fl-surface)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n lineHeight: 'var(--fl-line-height)',\n outline: 'none',\n boxSizing: 'border-box' as const,\n transition: `border-color var(--fl-duration-fast) ease-out`,\n};\n\nconst textareaStyles: React.CSSProperties = {\n width: '100%',\n minHeight: 48,\n padding: '6px 8px',\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: 'var(--fl-border)',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'var(--fl-surface)',\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-base)',\n lineHeight: 'var(--fl-line-height)',\n resize: 'vertical' as const,\n outline: 'none',\n boxSizing: 'border-box' as const,\n transition: `border-color var(--fl-duration-fast) ease-out`,\n};\n\nconst footerStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n gap: 8,\n};\n\nconst replyButtonStyles: React.CSSProperties = {\n padding: '4px 12px',\n backgroundColor: 'var(--fl-accent)',\n color: '#FFFFFF',\n border: 'none',\n borderRadius: 'var(--fl-border-radius-sm)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 'var(--fl-font-size-sm)',\n fontWeight: 'var(--fl-font-weight-medium)',\n cursor: 'pointer',\n outline: 'none',\n transition: `background-color var(--fl-duration-fast) ease-out`,\n};\n\nconst replyButtonDisabledStyles: React.CSSProperties = {\n ...replyButtonStyles,\n opacity: 0.4,\n cursor: 'default',\n};\n\nconst hintStyles: React.CSSProperties = {\n fontSize: 'var(--fl-font-size-sm)',\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\n// --- Component ---\n\ninterface ReplyInputProps {\n /** The root comment of the thread to reply to */\n rootComment: Comment;\n /** Called after a reply is successfully submitted */\n onReplySubmitted?: () => void;\n}\n\nexport function ReplyInput({ rootComment, onReplySubmitted }: ReplyInputProps) {\n const { adapter, config } = useLayContext();\n const [name, setName] = useState('');\n const [content, setContent] = useState('');\n const [hasError, setHasError] = useState(false);\n const nameInputRef = useRef<HTMLInputElement>(null);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const isIdentifiedMode = !!config.user;\n\n // Pre-fill guest name from localStorage on mount\n useEffect(() => {\n if (!isIdentifiedMode) {\n const guest = getGuestAuthor();\n setName(guest?.name ?? '');\n }\n }, [isIdentifiedMode]);\n\n const handleSubmit = useCallback(async () => {\n if (!content.trim()) return;\n\n const author = resolveAuthor(config, name);\n if (!isIdentifiedMode) {\n persistGuestName(name);\n }\n\n const newReply: NewComment = {\n project_id: config.projectId,\n thread_id: rootComment.id,\n author,\n content: content.trim(),\n status: 'open',\n dom_path: rootComment.dom_path,\n url_path: rootComment.url_path,\n element_metadata: null,\n resolved_by: null,\n resolved_at: null,\n archived_at: null,\n ai_context: null,\n screenshot_url: null,\n element_bounds: null,\n };\n\n try {\n await adapter.addComment(newReply);\n setContent('');\n onReplySubmitted?.();\n\n // Re-focus textarea after submit\n setTimeout(() => {\n textareaRef.current?.focus();\n }, 10);\n } catch {\n // Flash border red — content is preserved so user can retry\n setHasError(true);\n setTimeout(() => setHasError(false), 1200);\n }\n }, [content, name, config, adapter, rootComment, isIdentifiedMode, onReplySubmitted]);\n\n const handleTextareaKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n },\n [handleSubmit]\n );\n\n const handleNameKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n textareaRef.current?.focus();\n }\n },\n []\n );\n\n const trimmedContent = content.trim();\n\n return (\n <div style={containerStyles}>\n {!isIdentifiedMode ? (\n <input\n ref={nameInputRef}\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n onKeyDown={handleNameKeyDown}\n placeholder=\"Your name (optional)\"\n style={nameInputStyles}\n />\n ) : null}\n <textarea\n ref={textareaRef}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n onKeyDown={handleTextareaKeyDown}\n placeholder=\"Reply...\"\n style={\n hasError\n ? { ...textareaStyles, borderColor: '#D32F2F' }\n : textareaStyles\n }\n rows={2}\n />\n <div style={footerStyles}>\n <span style={hintStyles}>Esc</span>\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={!trimmedContent}\n style={\n trimmedContent ? replyButtonStyles : replyButtonDisabledStyles\n }\n >\n Reply\n </button>\n </div>\n </div>\n );\n}\n","/**\n * Three-layer element resolution for anchoring hardening (M6).\n *\n * Resolution order:\n * 1. data-feedback-id lookup (if dom_path contains one)\n * 2. CSS selector (document.querySelector)\n * 3. Fingerprint matching (fuzzy structural match)\n *\n * Returns the resolved element and which method succeeded.\n */\n\nimport type { ElementFingerprint } from '../types/comment';\nimport { findByFingerprint } from './elementFingerprint';\n\nexport type ResolveMethod = 'feedback-id' | 'selector' | 'fingerprint' | 'detached';\n\nexport interface ResolveResult {\n element: Element | null;\n method: ResolveMethod;\n /** Fingerprint match score (only set when method is 'fingerprint') */\n score?: number;\n}\n\n/**\n * Resolve a DOM element using 3-layer strategy.\n *\n * @param domPath - The stored CSS selector path\n * @param fingerprint - Optional element fingerprint for fuzzy matching\n * @returns The resolved element and the method that found it\n */\nexport function resolveElement(\n domPath: string,\n fingerprint?: ElementFingerprint | null\n): ResolveResult {\n // Layer 1: data-feedback-id — extract from domPath if present\n const feedbackIdMatch = domPath.match(/\\[data-feedback-id=\"([^\"]+)\"\\]/);\n if (feedbackIdMatch) {\n const el = document.querySelector(`[data-feedback-id=\"${feedbackIdMatch[1]}\"]`);\n if (el) return { element: el, method: 'feedback-id' };\n }\n\n // Layer 2: CSS selector — try the stored dom_path directly\n try {\n const el = document.querySelector(domPath);\n if (el) return { element: el, method: 'selector' };\n } catch {\n // Invalid selector syntax — fall through to fingerprint\n }\n\n // Layer 3: Fingerprint matching — fuzzy structural match\n if (fingerprint) {\n const result = findByFingerprint(fingerprint);\n if (result.element) {\n return { element: result.element, method: 'fingerprint', score: result.score };\n }\n }\n\n // All layers failed\n return { element: null, method: 'detached' };\n}\n","'use client';\n\nimport React, { useState } from 'react';\nimport type { ElementMetadata, AIContext } from './types/comment';\nimport { isAIContextReview } from './types/comment';\n\n// --- Styles ---\n\nconst cardStyles: React.CSSProperties = {\n marginTop: 6,\n border: '1px solid var(--fl-border)',\n borderRadius: 'var(--fl-border-radius-sm)',\n backgroundColor: 'var(--fl-surface)',\n fontFamily: 'var(--fl-font-family)',\n fontSize: 12,\n overflow: 'hidden',\n};\n\nconst summaryRowStyles: React.CSSProperties = {\n display: 'flex',\n alignItems: 'flex-start',\n gap: 6,\n padding: '6px 8px',\n cursor: 'pointer',\n userSelect: 'none',\n color: 'var(--fl-text-secondary)',\n lineHeight: 1.4,\n};\n\nconst disclosureStyles: React.CSSProperties = {\n flexShrink: 0,\n fontSize: 10,\n lineHeight: '16.8px',\n color: 'var(--fl-text-tertiary)',\n};\n\nconst summaryTextStyles: React.CSSProperties = {\n minWidth: 0,\n wordBreak: 'break-word' as const,\n};\n\nconst expandedBodyStyles: React.CSSProperties = {\n padding: '0 8px 8px',\n display: 'flex',\n flexDirection: 'column' as const,\n gap: 6,\n};\n\nconst sectionLabelStyles: React.CSSProperties = {\n fontSize: 11,\n fontWeight: 600,\n color: 'var(--fl-text-tertiary)',\n textTransform: 'uppercase' as const,\n letterSpacing: 0.5,\n marginBottom: 2,\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst badgeStyles: React.CSSProperties = {\n display: 'inline-block',\n fontSize: 11,\n fontWeight: 500,\n padding: '1px 6px',\n borderRadius: 3,\n backgroundColor: 'var(--fl-border)',\n color: 'var(--fl-text-secondary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst urgencyHighStyles: React.CSSProperties = {\n ...badgeStyles,\n backgroundColor: '#D32F2F',\n color: '#FFFFFF',\n};\n\nconst urgencyMediumStyles: React.CSSProperties = {\n ...badgeStyles,\n backgroundColor: '#F9A825',\n color: '#1A1A18',\n};\n\nconst proseStyles: React.CSSProperties = {\n fontSize: 12,\n lineHeight: 1.5,\n color: 'var(--fl-text-primary)',\n fontFamily: 'var(--fl-font-family)',\n};\n\nconst analyzingStyles: React.CSSProperties = {\n fontSize: 12,\n color: 'var(--fl-text-tertiary)',\n fontFamily: 'var(--fl-font-family)',\n fontStyle: 'italic',\n animation: 'fl-analyzing-pulse 1.5s ease-in-out infinite',\n};\n\nconst badgeRowStyles: React.CSSProperties = {\n display: 'flex',\n gap: 6,\n flexWrap: 'wrap' as const,\n marginBottom: 4,\n};\n\n// Inject keyframes for the analyzing pulse\nconst ANALYZING_CSS = `@keyframes fl-analyzing-pulse{0%,100%{opacity:0.4}50%{opacity:1}}`;\n\n// --- Helpers ---\n\nfunction buildSummary(aiContext: AIContext | null): string {\n if (!aiContext) return 'Analyzing...';\n\n if (isAIContextReview(aiContext)) {\n return aiContext.category;\n }\n\n const label = aiContext.intent.replace('_', ' ');\n return `${label} \\u00B7 ${aiContext.urgency} urgency`;\n}\n\nfunction urgencyBadgeStyle(urgency: string): React.CSSProperties {\n if (urgency === 'high') return urgencyHighStyles;\n if (urgency === 'medium') return urgencyMediumStyles;\n return badgeStyles;\n}\n\n// --- Component ---\n\ninterface AIContextCardProps {\n elementMetadata: ElementMetadata | null;\n aiContext: AIContext | null;\n aiEnabled?: boolean;\n}\n\nexport function AIContextCard({\n elementMetadata,\n aiContext,\n aiEnabled = true,\n}: AIContextCardProps) {\n const [isExpanded, setIsExpanded] = useState(false);\n\n if (!elementMetadata) return null;\n if (!aiEnabled) return null;\n // Skip comments with pre-v2 ai_context (no mode field)\n if (aiContext && !('mode' in aiContext)) return null;\n\n const summary = buildSummary(aiContext);\n const isLoading = !aiContext;\n\n return (\n <div style={cardStyles}>\n {isLoading && <style>{ANALYZING_CSS}</style>}\n\n {/* Collapsed summary row */}\n <div\n style={summaryRowStyles}\n onClick={() => setIsExpanded((prev) => !prev)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setIsExpanded((prev) => !prev);\n }\n }}\n >\n <span style={disclosureStyles}>{isExpanded ? '\\u25BE' : '\\u25B8'}</span>\n <span style={summaryTextStyles}>\n {isExpanded ? 'AI Interpretation' : summary}\n </span>\n </div>\n\n {/* Expanded body */}\n {isExpanded && (\n <div style={expandedBodyStyles}>\n {isLoading ? (\n <div>\n <div style={sectionLabelStyles}>AI Interpretation</div>\n <div style={analyzingStyles}>Analyzing...</div>\n </div>\n ) : aiContext && isAIContextReview(aiContext) ? (\n /* --- Review mode --- */\n <div>\n {aiContext.interpretation ? (\n <div style={proseStyles}>{aiContext.interpretation}</div>\n ) : (\n <div style={proseStyles}>{aiContext.category}</div>\n )}\n </div>\n ) : aiContext ? (\n /* --- Support mode (widget — no suggested_response) --- */\n <div>\n <div style={badgeRowStyles}>\n <span style={badgeStyles}>{aiContext.intent.replace('_', ' ')}</span>\n <span style={urgencyBadgeStyle(aiContext.urgency)}>\n {aiContext.urgency} urgency\n </span>\n </div>\n <div style={proseStyles}>{aiContext.summary}</div>\n </div>\n ) : null}\n </div>\n )}\n </div>\n );\n}\n","export type CommentStatus = 'open' | 'resolved' | 'archived';\n\nexport interface Author {\n id: string | null;\n name: string;\n avatar: string | null;\n}\n\nexport interface ElementFingerprint {\n tag: string;\n textContent: string;\n attributes: Record<string, string>;\n siblingIndex: number;\n siblingCount: number;\n parentTag: string;\n grandparentTag: string;\n}\n\nexport interface ElementMetadata {\n computed_styles: Record<string, string>;\n accessibility: {\n role: string | null;\n aria_label: string | null;\n contrast_ratio: number | null;\n contrast_passes_aa: boolean | null;\n };\n viewport: { width: number; height: number };\n device: string;\n fingerprint?: ElementFingerprint;\n}\n\nexport interface AIContextReview {\n ai_context_version: 2 | 3;\n mode: 'review';\n category: 'visual' | 'accessibility' | 'layout' | 'copy' | 'interaction';\n /** v3: single interpretation sentence replacing suggestions + accessibility_issues */\n interpretation?: string;\n /** @deprecated v2 only — replaced by interpretation in v3 */\n suggestions?: string[];\n /** @deprecated v2 only — replaced by interpretation in v3 */\n accessibility_issues?: string[];\n /** @deprecated v2 only — removed in v3 */\n confidence?: number;\n enriched_at: string;\n}\n\nexport interface AIContextSupport {\n ai_context_version: 2 | 3;\n mode: 'support';\n intent: 'confusion' | 'bug_report' | 'feature_request' | 'complaint' | 'question' | 'praise' | 'other';\n urgency: 'low' | 'medium' | 'high';\n summary: string;\n suggested_response: string;\n /** @deprecated v2 only — removed in v3 */\n confidence?: number;\n enriched_at: string;\n}\n\nexport type AIContext = AIContextReview | AIContextSupport;\n\nexport function isAIContextReview(ctx: AIContext): ctx is AIContextReview {\n return ctx.mode === 'review';\n}\n\nexport function isAIContextSupport(ctx: AIContext): ctx is AIContextSupport {\n return ctx.mode === 'support';\n}\n\nexport interface ElementBounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface Comment {\n id: string;\n project_id: string;\n thread_id: string | null;\n author: Author;\n content: string;\n status: CommentStatus;\n dom_path: string;\n url_path: string;\n element_metadata: ElementMetadata | null;\n resolved_by: Author | null;\n resolved_at: string | null;\n archived_at: string | null;\n ai_context: AIContext | null;\n screenshot_url: string | null;\n element_bounds: ElementBounds | null;\n created_at: string;\n updated_at: string;\n}\n\nexport type NewComment = Omit<\n Comment,\n 'id' | 'created_at' | 'updated_at'\n>;\n"],"mappings":"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","names":["index_exports","__export","AIContextCard","ArchivedThreadsPanel","CommentActions","CommentAnchor","CommentDot","CommentDots","CommentItem","CommentLayer","CommentThread","DetachedCommentsPanel","ElementHighlighter","LayProvider","LayToggle","ReplyInput","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","__toCommonJS","import_react","generateId","createMemoryAdapter","comments","listeners","emit","event","cb","projectId","urlPath","options","filtered","c","newComment","now","comment","id","update","index","updated","_projectId","callback","DEFAULT_API_URL","LOG_PREFIX","warn","message","createHostedAdapter","options","opts","baseUrl","aiEnabled","authHeaders","token","request","path","fetchOptions","sessionToken","res","projectId","params","urlPath","comment","id","update","commentId","blob","bounds","formData","headers","callback","url","es","msg","data","Z_INDEX","CONFETTI","SIZING","SHORTCUTS","DATA_ATTRS","MAGNETIC","BREADCRUMB","COMPOSER","DEFAULT_STARTER_CHIPS","WHISPER","ANONYMOUS_AUTHOR","STORAGE_PREFIX","getSessionToken","projectId","setSessionToken","token","clearSessionToken","import_react","import_react_dom","import_react","import_react","useLayContext","context","LayContext","useCommentMode","isCommentMode","dispatch","useLayContext","toggleCommentMode","setCommentMode","active","handleKeyDown","e","target","SHORTCUTS","import_react","useComments","comments","detachedDomPaths","useLayContext","groupedByDomPath","map","comment","existing","domPath","threadsByDomPath","pathMap","allComments","roots","repliesByThreadId","a","b","threads","root","replies","archivedThreads","c","archivedCount","detachedThreads","detachedCount","isDomPathFullyResolved","isDomPathFullyArchived","import_react","import_react_dom","import_jsx_runtime","panelStyles","Z_INDEX","headerStyles","scrollStyles","emptyStyles","rowStyles","rowContentStyles","rowMetaStyles","rowAuthorStyles","restoreButtonStyles","restoreButtonHoverStyles","ArchivedRow","thread","onRestore","hovered","setHovered","ArchivedThreadsPanel","onClose","portalRoot","adapter","config","dispatch","useLayContext","archivedThreads","useComments","panelRef","handleRestore","comment","optimistic","handleKeyDown","e","handleMouseDown","target","timer","import_react","import_react_dom","formatRelativeTime","isoTimestamp","date","now","diffMs","diffSec","diffMin","diffHr","diffDays","month","day","TAG_LABELS","humanizeElement","el","tag","ariaLabel","truncate","text","testId","humanizeDomPath","element","segments","current","BREADCRUMB","summarizeDomPath","domPath","target","simplifySegment","context","i","seg","result","segment","attrMatch","idMatch","classMatch","nthMatch","str","max","import_jsx_runtime","panelStyles","Z_INDEX","headerStyles","subheaderStyles","scrollStyles","emptyStyles","rowStyles","rowContentStyles","rowMetaStyles","rowAuthorStyles","rowTimestampStyles","wasOnStyles","DetachedRow","thread","domPathSummary","summarizeDomPath","formatRelativeTime","DetachedCommentsPanel","onClose","portalRoot","useLayContext","detachedThreads","useComments","panelRef","handleKeyDown","e","handleMouseDown","target","timer","import_jsx_runtime","toggleStyles","Z_INDEX","toggleHoverStyles","toggleActiveStyles","toggleActiveHoverStyles","tooltipStyles","tooltipVisibleStyles","chipStyles","chipHoverStyles","ScanIcon","active","LayToggle","isCommentMode","toggleCommentMode","useCommentMode","portalRoot","mode","useLayContext","archivedCount","detachedCount","useComments","showChips","isHovered","setIsHovered","chipHovered","setChipHovered","detachedChipHovered","setDetachedChipHovered","isArchivedPanelOpen","setIsArchivedPanelOpen","isDetachedPanelOpen","setIsDetachedPanelOpen","handleChipClick","e","prev","handleDetachedChipClick","handleCloseArchivedPanel","handleCloseDetachedPanel","buttonStyle","DATA_ATTRS","ArchivedThreadsPanel","DetachedCommentsPanel","import_react","generateDomPath","element","segments","current","feedbackId","testId","segment","stableClasses","getStableClassNames","parent","s","siblings","index","name","isGeneratedClassName","isCommentable","tag","FINGERPRINT_ATTRS","generateFingerprint","element","tag","textContent","attributes","attr","FINGERPRINT_ATTRS","value","parent","siblingIndex","siblingCount","sameTagSiblings","s","parentTag","grandparentTag","scoreFingerprintMatch","stored","candidate","score","text","attrs","grandparent","findByFingerprint","fingerprint","threshold","candidates","bestElement","bestScore","STYLE_KEYS","parseRGB","color","match","isTransparent","hasVisualBoundary","element","styles","bg","bgImage","shadow","side","width","style","resolveEffectiveBackground","current","htmlBg","linearize","channel","s","relativeLuminance","rgb","computeContrastRatio","fgColor","bgColor","fg","lum1","lum2","lighter","darker","ratio","parseUserAgent","ua","browser","os","captureElementMetadata","computed","computed_styles","key","role","aria_label","passesAA","viewport","device","fingerprint","generateFingerprint","getElementPriority","el","tag","MAGNETIC_SELECTORS","resolveMagneticTarget","event","target","targetPriority","hasVisualBoundary","bestAncestor","bestPriority","walker","depth","MAGNETIC","p","cursor","parent","candidates","closest","candidate","isCommentable","rect","dist","distanceToRect","point","dx","dy","useElementSelector","isCommentMode","useLayContext","state","setState","stateRef","clearSelection","prev","handleMouseOver","e","target","isCommentable","finalTarget","resolveMagneticTarget","handleMouseOut","relatedTarget","handleClick","prevCursor","import_react","import_react_dom","import_jsx_runtime","highlightStyles","Z_INDEX","ElementHighlighter","hoveredElement","selectedElement","isCommentMode","portalRoot","useLayContext","rect","setRect","rafRef","updateRect","target","domRect","handleScrollOrResize","import_react","import_react_dom","import_jsx_runtime","breadcrumbStyles","Z_INDEX","ElementBreadcrumb","hoveredElement","selectedElement","isCommentMode","portalRoot","useLayContext","pos","setPos","label","setLabel","rafRef","target","updatePosition","domRect","segments","humanizeDomPath","top","left","handleScrollOrResize","import_react","import_react_dom","STORAGE_KEY","getGuestAuthor","raw","parsed","saveGuestAuthor","name","trimmed","resolveAuthor","config","guestName","trimmedName","ANONYMOUS_AUTHOR","persistGuestName","name","trimmed","saveGuestAuthor","WEBP_QUALITY","captureScreenshot","element","rect","bounds","toCanvas","croppedCanvas","fullCanvas","el","DATA_ATTRS","ctx","resolve","blob","import_jsx_runtime","popoverStyles","Z_INDEX","COMPOSER","popoverVisibleStyles","expandedFormStyles","nameInputStyles","textareaStyles","footerStyles","submitButtonStyles","submitButtonDisabledStyles","hintStyles","chipBarStyles","chipStyles","chipHoverStyles","typeHintStyles","POPOVER_GAP","POPOVER_MIN_HEIGHT","calculatePosition","elementRect","viewportWidth","viewportHeight","top","left","CommentAnchor","selectedElement","onClearSelection","portalRoot","adapter","config","useLayContext","name","setName","content","setContent","position","setPosition","isVisible","setIsVisible","isExpanded","setIsExpanded","hoveredChipIdx","setHoveredChipIdx","nameInputRef","textareaRef","popoverRef","rafRef","isIdentifiedMode","chips","DEFAULT_STARTER_CHIPS","updatePosition","rect","guest","getGuestAuthor","handleScrollOrResize","isExpandedRef","handleKeyDown","e","handleMouseDown","target","timer","hasError","setHasError","screenshotsEnabled","fireScreenshotCapture","element","commentId","captureScreenshot","result","handleChipClick","chip","targetElement","domPath","generateDomPath","urlPath","storedGuest","author","resolveAuthor","elementMetadata","captureElementMetadata","newComment","created","handleSubmit","persistGuestName","handleTypeHintClick","handleTextareaKeyDown","handleNameKeyDown","trimmedContent","i","import_jsx_runtime","CommentLayer","hoveredElement","selectedElement","clearSelection","useElementSelector","ElementHighlighter","ElementBreadcrumb","CommentAnchor","import_react","import_react","import_react_dom","import_react","import_react_dom","import_jsx_runtime","itemStyles","replyItemStyles","headerStyles","headerLeftStyles","authorStyles","timestampStyles","contentStyles","resolvedIndicatorStyles","resolvedCheckStyles","CommentItem","comment","isReply","actions","formatRelativeTime","import_react","import_jsx_runtime","actionsContainerStyles","iconButtonStyles","iconButtonHoverStyles","textButtonStyles","textButtonHoverStyles","CommentActions","comment","onResolve","onReopen","onArchive","resolveHovered","setResolveHovered","reopenHovered","setReopenHovered","archiveHovered","setArchiveHovered","e","import_react","import_jsx_runtime","containerStyles","nameInputStyles","textareaStyles","footerStyles","replyButtonStyles","replyButtonDisabledStyles","hintStyles","ReplyInput","rootComment","onReplySubmitted","adapter","config","useLayContext","name","setName","content","setContent","hasError","setHasError","nameInputRef","textareaRef","isIdentifiedMode","guest","getGuestAuthor","handleSubmit","author","resolveAuthor","persistGuestName","newReply","handleTextareaKeyDown","e","handleNameKeyDown","trimmedContent","import_jsx_runtime","panelStyles","Z_INDEX","panelVisibleStyles","scrollContainerStyles","dividerStyles","replyDividerStyles","PANEL_GAP","PANEL_WIDTH","PANEL_EST_HEIGHT","calculatePanelPosition","dotRect","viewportWidth","viewportHeight","top","left","organizeThreads","comments","roots","repliesByThreadId","comment","existing","a","b","root","replies","CommentThread","onClose","onDidResolve","portalRoot","adapter","config","dispatch","useLayContext","panelRef","scrollRef","isVisible","setIsVisible","React","position","threads","replyTarget","handleResolve","author","resolveAuthor","now","optimistic","handleReopen","handleArchive","prevCountRef","handleReplySubmitted","handleKeyDown","e","handleMouseDown","target","timer","thread","threadIndex","CommentItem","CommentActions","reply","ReplyInput","import_jsx_runtime","generateParticles","count","particles","halfSpread","CONFETTI","i","angle","distance","radians","endX","endY","size","colors","color","markerStyles","SIZING","Z_INDEX","markerHoverStyles","markerResolvedStyles","markerResolvedHoverStyles","pointerStyles","pointerResolvedStyles","whisperStyles","whisperVisibleStyles","whisperMetaStyles","whisperAuthorStyles","whisperTimeStyles","whisperContentStyles","truncateContent","text","WHISPER","calculateMarkerPosition","element","rect","viewportWidth","viewportHeight","top","left","CommentDot","domPath","comments","isResolved","isNew","resolvedElement","portalRoot","useLayContext","position","setPosition","isHovered","setIsHovered","showWhisper","setShowWhisper","isPopoverOpen","setIsPopoverOpen","showRipple","setShowRipple","showBurst","setShowBurst","burstParticles","setBurstParticles","dotRef","rafRef","whisperTimerRef","updatePosition","handleScrollOrResize","timer","handleDidResolve","handleMouseEnter","handleMouseLeave","handleClick","e","prev","handleClosePopover","markerContent","currentStyle","p","formatRelativeTime","CommentThread","resolveElement","domPath","fingerprint","feedbackIdMatch","el","result","findByFingerprint","import_jsx_runtime","SELF_HEAL_THRESHOLD","CommentDots","groupedByDomPath","isDomPathFullyResolved","isDomPathFullyArchived","useComments","adapter","setDetachedDomPaths","useLayContext","initialPathsRef","g","healedRef","activeGroups","group","resolutionResults","results","fingerprint","comment","fp","result","resolveElement","detached","domPath","newDomPath","generateDomPath","CommentDot","import_jsx_runtime","layReducer","state","action","c","initialState","LayContext","TOKENS_CSS","ANIMATIONS_CSS","FONT_CSS","wrapAdapterWithSession","adapter","getToken","opts","token","projectId","urlPath","comment","id","update","commentId","blob","bounds","callback","LOG_PREFIX","requestSession","baseUrl","user","userHash","res","body","resolveAdapter","customAdapter","apiUrl","ai","createHostedAdapter","createMemoryAdapter","resolveBaseUrl","LayProvider","children","version","modeProp","activeProp","starterChips","screenshots","dispatch","rawAdapterRef","portalRoot","setPortalRoot","detachedDomPaths","setDetachedDomPaths","sessionToken","setSessionTokenState","sessionTokenRef","sessionReady","setSessionReady","updateSessionToken","setSessionToken","clearSessionToken","wrappedAdapterRef","remoteMode","setRemoteMode","remoteActive","setRemoteActive","remoteConfigLoaded","setRemoteConfigLoaded","resolvedMode","resolvedActive","config","existingToken","getSessionToken","session","err","root","DATA_ATTRS","styleEl","comments","unsubscribe","event","showFeedbackUI","contextValue","LayToggle","CommentLayer","CommentDots","import_react","isAIContextReview","ctx","isAIContextSupport","import_jsx_runtime","cardStyles","summaryRowStyles","disclosureStyles","summaryTextStyles","expandedBodyStyles","sectionLabelStyles","badgeStyles","urgencyHighStyles","urgencyMediumStyles","proseStyles","analyzingStyles","badgeRowStyles","ANALYZING_CSS","buildSummary","aiContext","isAIContextReview","urgencyBadgeStyle","urgency","AIContextCard","elementMetadata","aiEnabled","isExpanded","setIsExpanded","summary","isLoading","prev","e"]}
|