UNPKG

@preprio/prepr-nextjs

Version:

Next.js package for Prepr CMS preview functionality with advanced debugging and visual editing capabilities

1 lines 112 kB
{"version":3,"sources":["../../src/react/index.tsx","../../src/react/components/toolbar/toolbar-provider.tsx","../../src/react/contexts/segment-context.tsx","../../src/utils/errors.ts","../../src/utils/index.ts","../../src/utils/dom.ts","../../src/utils/debug.ts","../../src/utils/performance.ts","../../src/react/contexts/variant-context.tsx","../../src/react/contexts/edit-mode-context.tsx","../../src/react/components/error-boundary.tsx","../../src/react/hooks/use-scroll-position.tsx","../../src/react/components/toolbar/prepr-toolbar.tsx","../../src/react/hooks/use-stega-scan.tsx","../../src/react/hooks/use-stega-overlay.tsx","../../src/react/hooks/use-stega-proximity.tsx","../../src/react/hooks/use-stega-elements.tsx","../../src/react/components/toolbar/toolbar-wrapper.tsx","../../src/react/components/toolbar/toolbar.tsx","../../src/react/hooks/use-modal.ts","../../src/react/components/toolbar/toolbar-content.tsx","../../src/react/components/ui/status-indicator-pill.tsx","../../src/react/components/icons/xmark.tsx","../../src/react/components/ui/close-edit-mode-pill.tsx","../../src/react/components/ui/reset-button.tsx","../../src/react/components/icons/rotate.tsx","../../src/react/components/ui/icon.tsx","../../src/react/components/ui/logo.tsx","../../src/react/components/ui/prepr-tracking-pixel.tsx","../../src/react/components/selectors/segment-selector.tsx","../../src/react/components/icons/sort-down.tsx","../../src/react/components/selectors/variant-selector.tsx","../../src/react/components/selectors/radio-selector.tsx","../../src/react/components/selectors/edit-mode-selector.tsx","../../src/react/components/toolbar/toolbar-button.tsx","../../src/react/components/toolbar/toolbar-indicator-wrapper.tsx"],"sourcesContent":["'use client';\n\nexport {\n PreprToolbarProvider,\n usePreprToolbar,\n} from './components/toolbar/toolbar-provider';\n\nexport { default as PreprToolbar } from './components/toolbar/prepr-toolbar';\nexport { default as PreprTrackingPixel } from './components/ui/prepr-tracking-pixel';\n","'use client';\n\nimport React, { ReactNode, useEffect, useCallback, useMemo } from 'react';\nimport { PreprToolbarOptions, PreprToolbarProps } from '../../../types';\nimport {\n SegmentProvider,\n VariantProvider,\n EditModeProvider,\n useSegmentContext,\n useVariantContext,\n useEditModeContext,\n} from '../../contexts';\nimport { StegaErrorBoundary } from '../error-boundary';\nimport { initDebugLogger } from '../../../utils/debug';\nimport useScrollPosition from '../../hooks/use-scroll-position';\n\ninterface PreprToolbarProviderProps {\n children: ReactNode;\n props: PreprToolbarProps;\n options?: PreprToolbarOptions;\n}\n\nexport const PreprToolbarProvider: React.FC<PreprToolbarProviderProps> = ({\n children,\n props,\n options,\n}) => {\n const { activeSegment, activeVariant, data } = props;\n\n // Initialize debug logger with options\n useEffect(() => {\n const debugEnabled = options?.debug ?? false;\n initDebugLogger(debugEnabled);\n }, [options?.debug]);\n\n // Initialize scroll position handling for iframe communication\n useScrollPosition();\n\n return (\n <StegaErrorBoundary>\n <SegmentProvider initialSegments={data} activeSegment={activeSegment}>\n <VariantProvider activeVariant={activeVariant}>\n <EditModeProvider>{children}</EditModeProvider>\n </VariantProvider>\n </SegmentProvider>\n </StegaErrorBoundary>\n );\n};\n\n// Legacy hook for backward compatibility\nexport const usePreprToolbar = () => {\n // This will be deprecated in favor of specific context hooks\n // but kept for backward compatibility\n const segmentContext = useSegmentContext();\n const variantContext = useVariantContext();\n const editModeContext = useEditModeContext();\n\n const resetPersonalization = useCallback(() => {\n segmentContext.setSelectedSegment(segmentContext.emptySegment);\n variantContext.setSelectedVariant(variantContext.emptyVariant);\n }, [segmentContext, variantContext]);\n\n const resetAll = useCallback(() => {\n segmentContext.setSelectedSegment(segmentContext.emptySegment);\n variantContext.setSelectedVariant(variantContext.emptyVariant);\n editModeContext.setEditMode(false);\n }, [segmentContext, variantContext, editModeContext]);\n\n return useMemo(\n () => ({\n isPreviewMode: false,\n activeSegment: segmentContext.selectedSegment._id,\n activeVariant: variantContext.selectedVariant,\n data: segmentContext.segments,\n emptySegment: segmentContext.emptySegment,\n segmentList: segmentContext.segments,\n selectedSegment: segmentContext.selectedSegment,\n setSelectedSegment: segmentContext.setSelectedSegment,\n emptyVariant: variantContext.emptyVariant,\n selectedVariant: variantContext.selectedVariant,\n setSelectedVariant: variantContext.setSelectedVariant,\n editMode: editModeContext.editMode,\n setEditMode: editModeContext.setEditMode,\n isIframe: editModeContext.isIframe,\n resetPersonalization,\n resetAll,\n }),\n [\n segmentContext.selectedSegment,\n segmentContext.segments,\n segmentContext.emptySegment,\n segmentContext.setSelectedSegment,\n variantContext.selectedVariant,\n variantContext.emptyVariant,\n variantContext.setSelectedVariant,\n editModeContext.editMode,\n editModeContext.setEditMode,\n editModeContext.isIframe,\n resetPersonalization,\n resetAll,\n ]\n );\n};\n","'use client';\n\nimport React, { createContext, useContext, useState, ReactNode } from 'react';\nimport { PreprSegment } from '../../types';\nimport { handleContextError } from '../../utils/errors';\nimport { sendPreprEvent } from '../../utils';\n\ninterface SegmentContextValue {\n segments: PreprSegment[];\n selectedSegment: PreprSegment;\n setSelectedSegment: (segment: PreprSegment) => void;\n emptySegment: PreprSegment;\n}\n\nconst SegmentContext = createContext<SegmentContextValue | undefined>(\n undefined\n);\n\ninterface SegmentProviderProps {\n children: ReactNode;\n initialSegments: readonly PreprSegment[];\n activeSegment?: string | null;\n}\n\nexport function SegmentProvider({\n children,\n initialSegments,\n activeSegment,\n}: SegmentProviderProps) {\n const segmentList: PreprSegment[] = [\n {\n _id: 'all_other_users',\n name: 'All other users',\n },\n ...initialSegments,\n ];\n\n const emptySegment: PreprSegment = {\n name: 'Choose segment',\n _id: 'null',\n };\n\n const [selectedSegment, setSelectedSegment] = useState<PreprSegment>(\n (segmentList &&\n segmentList.filter(\n (segmentData: PreprSegment) => segmentData._id === activeSegment\n )[0]) ||\n emptySegment\n );\n\n const handleSetSelectedSegment = (segment: PreprSegment) => {\n setSelectedSegment(segment);\n sendPreprEvent('segment_changed', { segment: segment._id });\n };\n\n const value: SegmentContextValue = {\n segments: segmentList,\n selectedSegment,\n setSelectedSegment: handleSetSelectedSegment,\n emptySegment,\n };\n\n return (\n <SegmentContext.Provider value={value}>{children}</SegmentContext.Provider>\n );\n}\n\nexport function useSegmentContext(): SegmentContextValue {\n const context = useContext(SegmentContext);\n if (!context) {\n handleContextError('useSegmentContext');\n }\n return context!;\n}\n","export const StegaError = {\n DECODE_FAILED: 'STEGA_DECODE_FAILED',\n INVALID_FORMAT: 'STEGA_INVALID_FORMAT',\n DOM_MANIPULATION_FAILED: 'DOM_MANIPULATION_FAILED',\n CONTEXT_NOT_FOUND: 'CONTEXT_NOT_FOUND',\n} as const;\n\nexport type StegaErrorType = (typeof StegaError)[keyof typeof StegaError];\n\n// Define specific types for error additional data\nexport interface ErrorAdditionalData {\n input?: string;\n element?: HTMLElement;\n context?: string;\n [key: string]: string | HTMLElement | undefined;\n}\n\nexport interface ErrorInfo {\n type: StegaErrorType;\n context: string;\n message: string;\n timestamp: string;\n stack?: string;\n additionalData?: ErrorAdditionalData;\n}\n\nexport function createErrorInfo(\n type: StegaErrorType,\n context: string,\n error: Error,\n additionalData?: ErrorAdditionalData\n): ErrorInfo {\n return {\n type,\n context,\n message: error.message,\n timestamp: new Date().toISOString(),\n stack: error.stack,\n additionalData,\n };\n}\n\nexport function handleStegaError(\n error: Error,\n context: string,\n additionalData?: ErrorAdditionalData\n) {\n const errorInfo = createErrorInfo(\n StegaError.DECODE_FAILED,\n context,\n error,\n additionalData\n );\n\n console.error('Stega Error:', errorInfo);\n\n // In production, you might want to send this to an error tracking service\n if (process.env.NODE_ENV === 'production') {\n // sendToErrorTrackingService(errorInfo);\n }\n\n return errorInfo;\n}\n\nexport function handleDOMError(error: Error, context: string) {\n const errorInfo = createErrorInfo(\n StegaError.DOM_MANIPULATION_FAILED,\n context,\n error\n );\n\n console.error('DOM Error:', errorInfo);\n return errorInfo;\n}\n\nexport function handleContextError(contextName: string) {\n const error = new Error(`${contextName} must be used within its provider`);\n const errorInfo = createErrorInfo(\n StegaError.CONTEXT_NOT_FOUND,\n contextName,\n error\n );\n\n console.error('Context Error:', errorInfo);\n throw error;\n}\n","import { ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\nimport { PreprEventType } from '../types';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n// Define specific types for Prepr events\nexport interface PreprEventData {\n readonly segment?: string;\n readonly variant?: string;\n readonly editMode?: boolean;\n readonly [key: string]: string | boolean | number | undefined;\n}\n\n/**\n * Sends a Prepr event to both the current window and parent window\n * @param event - The event type to send\n * @param data - Optional event data\n */\nexport function sendPreprEvent(\n event: PreprEventType,\n data?: PreprEventData\n): void {\n if (typeof window !== 'undefined') {\n const message = {\n name: 'prepr_preview_bar',\n event,\n ...data,\n };\n\n // Send to current window for local event handling\n window.dispatchEvent(\n new CustomEvent('prepr_preview_bar', { detail: message })\n );\n\n // Send to parent window if available\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n}\n\n// Export error handling utilities\nexport * from './errors';\n\n// Export DOM service\nexport * from './dom';\n\n// Export debug utilities\nexport * from './debug';\n\n// Export performance utilities\nexport * from './performance';\n","import { handleDOMError } from './errors';\n\nexport class DOMService {\n /**\n * Creates an HTML element with specified tag and class name\n */\n static createElement(tag: string, className: string): HTMLElement {\n try {\n const element = document.createElement(tag);\n element.className = className;\n return element;\n } catch (error) {\n handleDOMError(error as Error, 'createElement');\n throw error;\n }\n }\n\n /**\n * Appends an element to the document body\n */\n static appendToBody(element: HTMLElement): void {\n try {\n document.body.appendChild(element);\n } catch (error) {\n handleDOMError(error as Error, 'appendToBody');\n throw error;\n }\n }\n\n /**\n * Removes an element from the document body\n */\n static removeFromBody(element: HTMLElement): void {\n try {\n if (element.parentNode) {\n element.parentNode.removeChild(element);\n }\n } catch (error) {\n handleDOMError(error as Error, 'removeFromBody');\n throw error;\n }\n }\n\n /**\n * Sets multiple CSS properties on an element\n */\n static setElementStyles(\n element: HTMLElement,\n styles: Record<string, string>\n ): void {\n try {\n Object.entries(styles).forEach(([property, value]) => {\n element.style.setProperty(property, value);\n });\n } catch (error) {\n handleDOMError(error as Error, 'setElementStyles');\n throw error;\n }\n }\n\n /**\n * Gets the bounding rectangle of an element\n */\n static getElementRect(element: HTMLElement): DOMRect {\n try {\n return element.getBoundingClientRect();\n } catch (error) {\n handleDOMError(error as Error, 'getElementRect');\n throw error;\n }\n }\n\n /**\n * Checks if an element is in the viewport\n */\n static isElementInViewport(element: HTMLElement): boolean {\n try {\n const rect = this.getElementRect(element);\n return (\n rect.top >= 0 &&\n rect.left >= 0 &&\n rect.bottom <=\n (window.innerHeight || document.documentElement.clientHeight) &&\n rect.right <=\n (window.innerWidth || document.documentElement.clientWidth)\n );\n } catch (error) {\n handleDOMError(error as Error, 'isElementInViewport');\n return false;\n }\n }\n\n /**\n * Calculates distance between two points\n */\n static calculateDistance(\n x1: number,\n y1: number,\n x2: number,\n y2: number\n ): number {\n return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));\n }\n\n /**\n * Finds the closest element to a point from a list of elements\n */\n static findClosestElement(\n pointX: number,\n pointY: number,\n elements: NodeListOf<Element>\n ): HTMLElement | null {\n try {\n let closestElement: HTMLElement | null = null;\n let minDistance = Infinity;\n\n elements.forEach(element => {\n const rect = this.getElementRect(element as HTMLElement);\n const distance = this.calculateDistance(\n pointX,\n pointY,\n rect.left + rect.width / 2,\n rect.top + rect.height / 2\n );\n\n if (distance < minDistance) {\n minDistance = distance;\n closestElement = element as HTMLElement;\n }\n });\n\n return closestElement;\n } catch (error) {\n handleDOMError(error as Error, 'findClosestElement');\n return null;\n }\n }\n\n /**\n * Safely adds event listeners\n */\n static addEventListener(\n element: EventTarget,\n event: string,\n handler: EventListener,\n options?: AddEventListenerOptions\n ): void {\n try {\n element.addEventListener(event, handler, options);\n } catch (error) {\n handleDOMError(error as Error, 'addEventListener');\n throw error;\n }\n }\n\n /**\n * Safely removes event listeners\n */\n static removeEventListener(\n element: EventTarget,\n event: string,\n handler: EventListener,\n options?: EventListenerOptions\n ): void {\n try {\n element.removeEventListener(event, handler, options);\n } catch (error) {\n handleDOMError(error as Error, 'removeEventListener');\n throw error;\n }\n }\n}\n","/**\n * Debug utility for Prepr Next.js package\n * Provides centralized debug logging with performance optimizations\n */\n\n// Define specific types for debug arguments\nexport type DebugArg = string | number | boolean | null | undefined | object;\n\ninterface DebugOptions {\n enabled?: boolean;\n prefix?: string;\n}\n\nclass DebugLogger {\n private options: DebugOptions;\n\n constructor(options: DebugOptions) {\n this.options = {\n prefix: '[Prepr]',\n ...options,\n };\n }\n\n /**\n * Check if debug is enabled - checks both local and global state\n */\n private isEnabled(): boolean {\n // If this logger has a local enabled state, use it\n if (this.options.enabled !== undefined) {\n return this.options.enabled;\n }\n\n // Otherwise, check the global logger state\n return globalDebugLogger?.options?.enabled ?? false;\n }\n\n /**\n * Log a debug message if debug is enabled\n */\n log(message: string, ...args: DebugArg[]): void {\n if (!this.isEnabled()) return;\n\n const prefix = this.options.prefix;\n console.log(`${prefix} ${message}`, ...args);\n }\n\n /**\n * Log a debug warning if debug is enabled\n */\n warn(message: string, ...args: DebugArg[]): void {\n if (!this.isEnabled()) return;\n\n const prefix = this.options.prefix;\n console.warn(`${prefix} ${message}`, ...args);\n }\n\n /**\n * Log a debug error if debug is enabled\n */\n error(message: string, ...args: DebugArg[]): void {\n if (!this.isEnabled()) return;\n\n const prefix = this.options.prefix;\n console.error(`${prefix} ${message}`, ...args);\n }\n\n /**\n * Create a scoped logger with additional context\n */\n scope(scopeName: string): DebugLogger {\n return new DebugLogger({\n ...this.options,\n prefix: `${this.options.prefix}[${scopeName}]`,\n });\n }\n}\n\n// Global debug instance\nlet globalDebugLogger: DebugLogger | null = null;\n\n/**\n * Initialize the debug logger\n */\nexport function initDebugLogger(enabled: boolean = false): void {\n globalDebugLogger = new DebugLogger({ enabled });\n}\n\n/**\n * Get the debug logger instance\n */\nexport function getDebugLogger(): DebugLogger {\n if (!globalDebugLogger) {\n // Fallback to disabled logger if not initialized\n globalDebugLogger = new DebugLogger({ enabled: false });\n }\n return globalDebugLogger;\n}\n\n/**\n * Convenience function for logging\n */\nexport function debugLog(message: string, ...args: DebugArg[]): void {\n getDebugLogger().log(message, ...args);\n}\n\n/**\n * Convenience function for warning\n */\nexport function debugWarn(message: string, ...args: DebugArg[]): void {\n getDebugLogger().warn(message, ...args);\n}\n\n/**\n * Convenience function for errors\n */\nexport function debugError(message: string, ...args: DebugArg[]): void {\n getDebugLogger().error(message, ...args);\n}\n\n/**\n * Create a scoped debug logger that dynamically checks global debug state\n */\nexport function createScopedLogger(scopeName: string): DebugLogger {\n // Create a scoped logger without its own enabled state\n // This allows it to dynamically check the global logger state\n return new DebugLogger({\n prefix: `[Prepr][${scopeName}]`,\n });\n}\n","// Performance utilities\n\n/**\n * Throttled function with cancellation support\n */\nexport interface ThrottledFunction<T extends (...args: any[]) => any> {\n (...args: Parameters<T>): void;\n cancel(): void;\n}\n\n/**\n * Improved throttle function with better memory management and cancellation\n * @param func - The function to throttle\n * @param delay - The delay in milliseconds\n * @returns Throttled function with cancel method\n */\nexport function throttle<T extends (...args: any[]) => any>(\n func: T,\n delay: number\n): ThrottledFunction<T> {\n let timeoutId: NodeJS.Timeout | null = null;\n let lastExecTime = 0;\n\n const throttledFunc = ((...args: Parameters<T>) => {\n const currentTime = Date.now();\n const timeSinceLastExec = currentTime - lastExecTime;\n\n if (timeSinceLastExec >= delay) {\n func(...args);\n lastExecTime = currentTime;\n } else {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n timeoutId = setTimeout(() => {\n func(...args);\n lastExecTime = Date.now();\n timeoutId = null;\n }, delay - timeSinceLastExec);\n }\n }) as ThrottledFunction<T>;\n\n throttledFunc.cancel = () => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n };\n\n return throttledFunc;\n}\n\n/**\n * Debounce function with cancellation support\n * @param func - The function to debounce\n * @param delay - The delay in milliseconds\n * @returns Debounced function with cancel method\n */\nexport function debounce<T extends (...args: any[]) => any>(\n func: T,\n delay: number\n): ThrottledFunction<T> {\n let timeoutId: NodeJS.Timeout | null = null;\n\n const debouncedFunc = ((...args: Parameters<T>) => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n timeoutId = setTimeout(() => {\n func(...args);\n timeoutId = null;\n }, delay);\n }) as ThrottledFunction<T>;\n\n debouncedFunc.cancel = () => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n };\n\n return debouncedFunc;\n}\n\n// Simple DOM element cache for querySelectorAll\nexport function createElementCache<T extends Element = Element>(\n query: string,\n ttl: number = 1000\n) {\n let cache: NodeListOf<T> | null = null;\n let lastCacheTime = 0;\n return () => {\n const now = Date.now();\n if (!cache || now - lastCacheTime > ttl) {\n cache = document.querySelectorAll<T>(query);\n lastCacheTime = now;\n }\n return cache;\n };\n}\n","'use client';\n\nimport React, { createContext, useContext, useState, ReactNode } from 'react';\nimport { handleContextError } from '../../utils/errors';\nimport { sendPreprEvent } from '../../utils';\n\ninterface VariantContextValue {\n selectedVariant: string | null;\n setSelectedVariant: (variant: string | null) => void;\n emptyVariant: string;\n}\n\nconst VariantContext = createContext<VariantContextValue | undefined>(\n undefined\n);\n\ninterface VariantProviderProps {\n children: ReactNode;\n activeVariant?: string | null;\n}\n\nexport function VariantProvider({\n children,\n activeVariant,\n}: VariantProviderProps) {\n const emptyVariant = 'null';\n\n const [selectedVariant, setSelectedVariant] = useState<string | null>(\n activeVariant || 'null'\n );\n\n const handleSetSelectedVariant = (variant: string | null) => {\n setSelectedVariant(variant);\n sendPreprEvent('variant_changed', { variant: variant ?? undefined });\n };\n\n const value: VariantContextValue = {\n selectedVariant,\n setSelectedVariant: handleSetSelectedVariant,\n emptyVariant,\n };\n\n return (\n <VariantContext.Provider value={value}>{children}</VariantContext.Provider>\n );\n}\n\nexport function useVariantContext(): VariantContextValue {\n const context = useContext(VariantContext);\n if (!context) {\n handleContextError('useVariantContext');\n }\n return context!;\n}\n","'use client';\n\nimport React, {\n createContext,\n useContext,\n useState,\n useEffect,\n ReactNode,\n} from 'react';\nimport { handleContextError } from '../../utils/errors';\nimport { sendPreprEvent } from '../../utils';\n\ninterface EditModeContextValue {\n editMode: boolean;\n setEditMode: (mode: boolean) => void;\n isIframe: boolean;\n}\n\nconst EditModeContext = createContext<EditModeContextValue | undefined>(\n undefined\n);\n\ninterface EditModeProviderProps {\n children: ReactNode;\n}\n\nexport function EditModeProvider({ children }: EditModeProviderProps) {\n const [editMode, setEditMode] = useState(false);\n const [isIframe, setIsIframe] = useState(false);\n\n const handleSetEditMode = (mode: boolean) => {\n setEditMode(mode);\n sendPreprEvent('edit_mode_toggled', { editMode: mode });\n };\n\n useEffect(() => {\n if (window.parent !== self) {\n setIsIframe(true);\n }\n }, []);\n\n // Handle escape key to turn off edit mode\n useEffect(() => {\n const handleEscapeKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && editMode) {\n handleSetEditMode(false);\n }\n };\n\n if (editMode) {\n document.addEventListener('keydown', handleEscapeKey);\n }\n\n return () => {\n document.removeEventListener('keydown', handleEscapeKey);\n };\n }, [editMode]);\n\n const value: EditModeContextValue = {\n editMode,\n setEditMode: handleSetEditMode,\n isIframe,\n };\n\n return (\n <EditModeContext.Provider value={value}>\n {children}\n </EditModeContext.Provider>\n );\n}\n\nexport function useEditModeContext(): EditModeContextValue {\n const context = useContext(EditModeContext);\n if (!context) {\n handleContextError('useEditModeContext');\n }\n return context!;\n}\n","'use client';\n\nimport React, { Component, ReactNode } from 'react';\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n}\n\ninterface State {\n hasError: boolean;\n error: Error | null;\n}\n\nexport class StegaErrorBoundary extends Component<Props, State> {\n constructor(props: Props) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): State {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n console.error('Stega Error Boundary caught an error:', error, errorInfo);\n\n // In production, you might want to send this to an error tracking service\n if (process.env.NODE_ENV === 'production') {\n // sendToErrorTrackingService({ error, errorInfo });\n }\n }\n\n render() {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div className=\"p-rounded-lg p-border p-border-red-200 p-bg-red-50 p-p-4 p-text-sm p-text-red-800\">\n <div className=\"p-mb-2 p-font-semibold\">Preview mode unavailable</div>\n <div className=\"p-text-red-600\">\n {this.state.error?.message || 'An unexpected error occurred'}\n </div>\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { useEffect } from 'react';\nimport { sendPreprEvent } from '../../utils';\nimport { createScopedLogger } from '../../utils';\n\n// Mark this hook as having side effects to prevent tree shaking\n\nexport default function useScrollPosition() {\n const debug = createScopedLogger('useScrollPosition');\n\n useEffect(() => {\n sendPreprEvent('getScrollPosition', {\n value: 0,\n });\n\n if (window.parent !== self) {\n let parentOrigin: string | null = null; //Get origin of parent outside iframe\n sendPreprEvent('loaded');\n\n const handleMessage = (evt: MessageEvent) => {\n debug.log('received message:', evt.data);\n\n if (evt?.data?.event === 'prepr:initVE' && !parentOrigin) {\n parentOrigin = evt.origin;\n\n if (evt.data?.scrollPosition) {\n debug.log('scrolling to position:', evt.data.scrollPosition);\n //Timeout needed in order to scroll to position\n setTimeout(() => {\n window.scrollTo(0, evt.data?.scrollPosition);\n }, 1);\n }\n }\n if (evt.origin !== parentOrigin) return;\n\n if (evt?.data?.event === 'prepr:getScrollPosition') {\n const currentScrollY =\n window.scrollY || document.documentElement.scrollTop;\n debug.log('sending scroll position:', currentScrollY);\n sendPreprEvent('getScrollPosition', {\n value: currentScrollY,\n });\n }\n };\n\n window.addEventListener('message', handleMessage);\n\n debug.log('set up iframe message listener');\n\n return () => {\n window.removeEventListener('message', handleMessage);\n debug.log('cleaned up iframe message listener');\n };\n } else {\n debug.log('not in iframe, skipping iframe setup');\n return undefined;\n }\n }, [debug]);\n\n // Return something to prevent tree shaking\n return true;\n}\n","'use client';\n\nimport React from 'react';\nimport { useEditModeContext } from '../../contexts';\nimport useStegaScan from '../../hooks/use-stega-scan';\nimport ToolbarWrapper from './toolbar-wrapper';\nimport ToolbarIndicatorWrapper from './toolbar-indicator-wrapper';\n\nexport default function PreprToolbar() {\n const { editMode } = useEditModeContext();\n\n useStegaScan(editMode);\n\n return (\n <>\n <ToolbarWrapper />\n <ToolbarIndicatorWrapper />\n </>\n );\n}\n","import { useEffect, useRef, useCallback, useMemo } from 'react';\nimport { DOMService } from '../../utils/dom';\nimport { createScopedLogger } from '../../utils/debug';\nimport { throttle } from '../../utils/performance';\nimport { useStegaOverlay } from './use-stega-overlay';\nimport { useStegaProximity } from './use-stega-proximity';\nimport { useStegaElements } from './use-stega-elements';\n\nexport default function useStegaScan(editMode: boolean): void {\n const debug = createScopedLogger('useStegaScan');\n const isInitializedRef = useRef(false);\n\n const {\n currentElementRef,\n hideTimeoutRef,\n createOverlay,\n showOverlay,\n hideOverlay,\n cleanup: cleanupOverlay,\n decode,\n } = useStegaOverlay();\n\n const { updateElementGradients, clearAllHighlights } = useStegaProximity();\n\n const {\n getElements,\n scanDocument,\n setupMutationObserver,\n cleanup: cleanupElements,\n } = useStegaElements();\n\n // Memoize the throttled mouse move handler\n const throttledMouseMove = useMemo(\n () =>\n throttle((e: Event) => {\n const mouseEvent = e as MouseEvent;\n const target = mouseEvent.target as HTMLElement;\n // Early return if hovering over tooltip\n if (target.closest('.prepr-tooltip')) {\n return;\n }\n updateElementGradients(mouseEvent.clientX, mouseEvent.clientY);\n const encodedElement = target.closest('[data-prepr-encoded]');\n if (encodedElement) {\n showOverlay(encodedElement as HTMLElement);\n } else {\n hideOverlay();\n }\n }, 16),\n [updateElementGradients, showOverlay, hideOverlay]\n );\n\n // Memoize tooltip handlers\n const handleTooltipMouseEnter = useCallback(() => {\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n hideTimeoutRef.current = null;\n }\n }, [hideTimeoutRef]);\n\n const handleTooltipMouseLeave = useCallback(() => {\n if (!currentElementRef.current) {\n hideOverlay();\n }\n }, [currentElementRef, hideOverlay]);\n\n useEffect(() => {\n debug.log('editMode changed to', editMode);\n if (!editMode) {\n debug.log('editMode is false, cleaning up');\n if (isInitializedRef.current) {\n DOMService.removeEventListener(\n document,\n 'mousemove',\n throttledMouseMove\n );\n cleanupOverlay();\n clearAllHighlights();\n cleanupElements();\n isInitializedRef.current = false;\n }\n return;\n }\n if (isInitializedRef.current) {\n debug.log('already initialized, skipping setup');\n return;\n }\n debug.log('editMode is true, setting up scanning');\n // Create overlay and tooltip elements\n const { tooltip } = createOverlay();\n debug.log('created overlay and tooltip');\n DOMService.addEventListener(tooltip, 'mouseenter', handleTooltipMouseEnter);\n DOMService.addEventListener(tooltip, 'mouseleave', handleTooltipMouseLeave);\n debug.log('starting document scan');\n scanDocument(decode);\n const elements = getElements();\n debug.log('found', elements.length, 'encoded elements after scan');\n setupMutationObserver(decode);\n debug.log('set up mutation observer');\n DOMService.addEventListener(document, 'mousemove', throttledMouseMove);\n debug.log('added throttled mousemove handler');\n isInitializedRef.current = true;\n return () => {\n debug.log('cleaning up');\n DOMService.removeEventListener(document, 'mousemove', throttledMouseMove);\n DOMService.removeEventListener(\n tooltip,\n 'mouseenter',\n handleTooltipMouseEnter\n );\n DOMService.removeEventListener(\n tooltip,\n 'mouseleave',\n handleTooltipMouseLeave\n );\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n cleanupOverlay();\n clearAllHighlights();\n cleanupElements();\n isInitializedRef.current = false;\n };\n }, [editMode]);\n}\n","import { useRef, useCallback } from 'react';\nimport { DOMService } from '../../utils/dom';\nimport { handleStegaError } from '../../utils/errors';\nimport { createScopedLogger } from '../../utils/debug';\nimport { vercelStegaDecode } from '@vercel/stega';\n\ninterface DecodedData {\n origin: string;\n href: string;\n}\n\nfunction decode(str: string | null): DecodedData | null {\n if (!str) return null;\n\n const debug = createScopedLogger('decode');\n\n try {\n // First, try to decode the string directly\n debug.log('attempting to decode stega data');\n\n const decoded = vercelStegaDecode(str) as DecodedData;\n debug.log('vercelStegaDecode result:', decoded);\n\n if (decoded?.href) {\n debug.log('successfully decoded', decoded);\n return decoded;\n }\n } catch (e) {\n debug.log('error decoding stega data:', e as Error);\n // If it fails, it might be because of trailing characters.\n // Regex to find the JSON string\n const regex = /{\"origin.*?}/;\n const match = str.match(regex);\n\n if (match) {\n try {\n // Now, try to decode the matched JSON string\n const decodedMatch = vercelStegaDecode(match[0]) as DecodedData;\n if (decodedMatch?.href) {\n debug.log('successfully decoded with regex match', decodedMatch);\n return decodedMatch;\n }\n } catch (e) {\n handleStegaError(e as Error, 'decode', { input: str });\n }\n }\n }\n\n return null;\n}\n\nexport function useStegaOverlay() {\n const debug = createScopedLogger('useStegaOverlay');\n const overlayRef = useRef<HTMLDivElement | null>(null);\n const tooltipRef = useRef<HTMLDivElement | null>(null);\n const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n const currentElementRef = useRef<HTMLElement | null>(null);\n\n const createOverlay = useCallback(() => {\n const overlay = DOMService.createElement(\n 'div',\n 'prepr-overlay'\n ) as HTMLDivElement;\n overlay.style.display = 'none';\n\n const tooltip = DOMService.createElement(\n 'div',\n 'prepr-tooltip'\n ) as HTMLDivElement;\n tooltip.style.display = 'none';\n\n DOMService.appendToBody(overlay);\n DOMService.appendToBody(tooltip);\n\n overlayRef.current = overlay;\n tooltipRef.current = tooltip;\n\n debug.log('created overlay and tooltip elements');\n\n return { overlay, tooltip };\n }, [debug]);\n\n const showOverlay = useCallback(\n (element: HTMLElement) => {\n if (!overlayRef.current || !tooltipRef.current) return;\n\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n hideTimeoutRef.current = null;\n }\n\n const rect = DOMService.getElementRect(element);\n const href = element.getAttribute('data-prepr-href');\n const origin = element.getAttribute('data-prepr-origin');\n\n debug.log('showing overlay for element:', { href, origin, rect });\n\n // Position overlay\n const overlay = overlayRef.current;\n overlay.style.display = 'block';\n overlay.style.top = `${rect.top + window.scrollY - 2}px`;\n overlay.style.left = `${rect.left + window.scrollX - 4}px`;\n overlay.style.width = `${rect.width + 8}px`;\n overlay.style.height = `${rect.height + 4}px`;\n\n // Position and show tooltip\n const tooltip = tooltipRef.current;\n if (tooltip && href && origin) {\n const MIN_WIDTH_FOR_TEXT = 80;\n const isCompact = rect.width < MIN_WIDTH_FOR_TEXT;\n tooltip.textContent = isCompact ? '↗' : `${origin} ↗`;\n tooltip.style.display = 'block';\n\n // Remove min-width constraint for compact tooltips\n if (isCompact) {\n tooltip.style.minWidth = 'auto';\n } else {\n tooltip.style.minWidth = '80px';\n }\n\n // Use requestAnimationFrame to ensure the DOM has updated before calculating position\n requestAnimationFrame(() => {\n if (tooltip) {\n tooltip.style.top = `${rect.top + window.scrollY - tooltip.clientHeight - 2}px`;\n tooltip.style.left = `${rect.right + 4 - tooltip.clientWidth}px`;\n }\n });\n\n tooltip.onclick = () =>\n window.open(href, '_blank', 'noopener,noreferrer');\n }\n\n currentElementRef.current = element;\n },\n [debug]\n );\n\n const hideOverlay = useCallback(() => {\n if (!overlayRef.current || !tooltipRef.current) return;\n\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n\n hideTimeoutRef.current = setTimeout(() => {\n if (overlayRef.current) overlayRef.current.style.display = 'none';\n if (tooltipRef.current) tooltipRef.current.style.display = 'none';\n currentElementRef.current = null;\n debug.log('hidden overlay and tooltip');\n }, 100);\n }, [debug]);\n\n const cleanup = useCallback(() => {\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n\n if (overlayRef.current) {\n DOMService.removeFromBody(overlayRef.current);\n }\n if (tooltipRef.current) {\n DOMService.removeFromBody(tooltipRef.current);\n }\n\n debug.log('cleaned up overlay and tooltip');\n }, [debug]);\n\n return {\n overlayRef,\n tooltipRef,\n currentElementRef,\n hideTimeoutRef,\n createOverlay,\n showOverlay,\n hideOverlay,\n cleanup,\n decode,\n };\n}\n","import { useRef, useCallback } from 'react';\nimport { createScopedLogger } from '../../utils/debug';\nimport { createElementCache } from '../../utils/performance';\n\nexport function useStegaProximity() {\n const debug = createScopedLogger('useStegaProximity');\n const highlightedElementsRef = useRef<Set<HTMLElement>>(new Set());\n const getEncodedElements = createElementCache<HTMLElement>(\n '[data-prepr-encoded]',\n 500\n );\n\n const updateElementGradients = useCallback(\n (cursorX: number, cursorY: number) => {\n const encodedElements = getEncodedElements();\n const newHighlightedElements = new Set<HTMLElement>();\n let highlightedCount = 0;\n\n encodedElements.forEach(element => {\n const rect = element.getBoundingClientRect();\n\n // Calculate shortest distance from cursor to element edges\n const distanceLeft = Math.abs(cursorX - rect.left);\n const distanceRight = Math.abs(cursorX - rect.right);\n const distanceTop = Math.abs(cursorY - rect.top);\n const distanceBottom = Math.abs(cursorY - rect.bottom);\n\n // Use minimum distance to any edge\n const distance = Math.min(\n distanceLeft,\n distanceRight,\n distanceTop,\n distanceBottom\n );\n\n const el = element as HTMLElement;\n if (distance < 150) {\n // Calculate relative cursor position within the element\n const relativeX = cursorX - rect.left;\n const relativeY = cursorY - rect.top;\n\n el.style.setProperty('--cursor-x', `${relativeX}px`);\n el.style.setProperty('--cursor-y', `${relativeY}px`);\n\n // Set gradient size based on element dimensions\n // Calculate base gradient size based on element dimensions\n const baseGradientSize = Math.max(\n 150,\n Math.max(rect.width, rect.height) * 1.1\n );\n // Scale gradient size based on distance (400 is max distance, closer = larger gradient)\n const distanceScale = Math.max(0, (400 - distance) / 400);\n const gradientSize = baseGradientSize * distanceScale;\n\n el.style.setProperty('--gradient-size', `${gradientSize}px`);\n el.classList.add('prepr-proximity-highlight');\n newHighlightedElements.add(el);\n highlightedCount++;\n } else {\n el.classList.remove('prepr-proximity-highlight');\n }\n });\n\n // Update the highlighted elements reference\n highlightedElementsRef.current = newHighlightedElements;\n\n if (highlightedCount > 0) {\n debug.log('highlighted', highlightedCount, 'elements near cursor');\n }\n },\n [debug, getEncodedElements]\n );\n\n const clearAllHighlights = useCallback(() => {\n const highlightedElements = highlightedElementsRef.current;\n let clearedCount = 0;\n\n highlightedElements.forEach(element => {\n element.classList.remove('prepr-proximity-highlight');\n clearedCount++;\n });\n\n highlightedElementsRef.current.clear();\n\n if (clearedCount > 0) {\n debug.log('cleared highlights from', clearedCount, 'elements');\n }\n }, [debug]);\n\n return {\n updateElementGradients,\n clearAllHighlights,\n highlightedElementsRef,\n };\n}\n","import { useRef, useCallback } from 'react';\nimport { createScopedLogger } from '../../utils/debug';\n\n// Define the expected structure for decoded data\ninterface DecodedData {\n href: string;\n origin: string;\n}\n\nexport function useStegaElements() {\n const debug = createScopedLogger('useStegaElements');\n const elementsRef = useRef<NodeListOf<Element> | undefined>(undefined);\n const observerRef = useRef<MutationObserver | null>(null);\n\n const getElements = useCallback(() => {\n if (!elementsRef.current) {\n elementsRef.current = document.querySelectorAll('[data-prepr-encoded]');\n }\n return elementsRef.current;\n }, []);\n\n const scanNode = useCallback(\n (node: Node, decode: (str: string | null) => DecodedData | null) => {\n if (node.nodeType === Node.TEXT_NODE) {\n if (!node.textContent?.trim()) return;\n if (node.parentElement?.closest('script, style, noscript')) return;\n const decoded = decode(node.textContent);\n if (decoded?.href) {\n const target = node.parentElement;\n if (target && !target.hasAttribute('data-prepr-encoded')) {\n target.setAttribute('data-prepr-encoded', '');\n target.setAttribute('data-prepr-href', decoded.href);\n target.setAttribute('data-prepr-origin', decoded.origin);\n debug.log('encoded element found:', {\n href: decoded.href,\n origin: decoded.origin,\n });\n }\n }\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n for (let i = 0; i < node.childNodes.length; i++) {\n scanNode(node.childNodes[i], decode);\n }\n }\n },\n [debug]\n );\n\n const scanDocument = useCallback(\n (decode: (str: string | null) => DecodedData | null) => {\n debug.log('starting document scan');\n const walker = document.createTreeWalker(\n document.body,\n NodeFilter.SHOW_TEXT,\n {\n acceptNode: node => {\n if (node.parentElement?.closest('script, style, noscript')) {\n return NodeFilter.FILTER_REJECT;\n }\n if (!node.textContent?.trim()) {\n return NodeFilter.FILTER_REJECT;\n }\n return NodeFilter.FILTER_ACCEPT;\n },\n }\n );\n let textNode;\n let encodedCount = 0;\n while ((textNode = walker.nextNode())) {\n const decoded = decode(textNode.textContent);\n if (decoded?.href) {\n const target = textNode.parentElement;\n if (target && !target.hasAttribute('data-prepr-encoded')) {\n target.setAttribute('data-prepr-encoded', '');\n target.setAttribute('data-prepr-href', decoded.href);\n target.setAttribute('data-prepr-origin', decoded.origin);\n encodedCount++;\n }\n }\n }\n debug.log('document scan complete, encoded', encodedCount, 'elements');\n elementsRef.current = document.querySelectorAll('[data-prepr-encoded]');\n },\n [debug]\n );\n\n const setupMutationObserver = useCallback(\n (decode: (str: string | null) => DecodedData | null) => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n }\n let pendingMutations: MutationRecord[] = [];\n let debounceTimeout: NodeJS.Timeout | null = null;\n const processMutations = () => {\n const allAddedNodes = new Set<Node>();\n pendingMutations.forEach(mutation => {\n mutation.addedNodes.forEach(node => allAddedNodes.add(node));\n });\n allAddedNodes.forEach(node => scanNode(node, decode));\n pendingMutations = [];\n elementsRef.current = document.querySelectorAll('[data-prepr-encoded]');\n };\n observerRef.current = new MutationObserver(mutations => {\n pendingMutations.push(...mutations);\n if (debounceTimeout) clearTimeout(debounceTimeout);\n debounceTimeout = setTimeout(processMutations, 100);\n });\n observerRef.current.observe(document.body, {\n childList: true,\n subtree: true,\n });\n debug.log('mutation observer set up');\n },\n [scanNode, debug]\n );\n\n const cleanup = useCallback(() => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n const encodedElements = document.querySelectorAll('[data-prepr-encoded]');\n encodedElements.forEach(element => {\n element.removeAttribute('data-prepr-encoded');\n element.removeAttribute('data-prepr-href');\n element.removeAttribute('data-prepr-origin');\n });\n debug.log('cleaned up', encodedElements.length, 'encoded elements');\n elementsRef.current = undefined;\n }, [debug]);\n\n return {\n getElements,\n scanDocument,\n setupMutationObserver,\n cleanup,\n };\n}\n","import React, { useEffect, useState } from 'react';\nimport { useSearchParams } from 'next/navigation';\nimport Toolbar from './toolbar';\n\nexport default function ToolbarWrapper() {\n const searchParams = useSearchParams();\n const [isIframe, setIsIframe] = useState<boolean>(false);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const key = event.key.toLowerCase();\n // Check for the blocked shortcuts\n const isSaveShortcut = (event.ctrlKey || event.metaKey) && key === 's';\n const isPrintShortcut = (event.ctrlKey || event.metaKey) && key === 'p';\n const isAddressBarShortcut =\n (event.ctrlKey || event.metaKey) && key === 'l';\n if (isSaveShortcut || isPrintShortcut || isAddressBarShortcut) {\n event.preventDefault(); // Prevent the browser's default action\n }\n };\n\n useEffect(() => {\n const isIframe =\n typeof window !== 'undefined' && window?.parent !== window.self;\n if (isIframe) {\n setIsIframe(true);\n const previewBarMessage = {\n name: 'prepr_preview_bar',\n event: 'loaded',\n };\n window.parent.postMessage(previewBarMessage, '*');\n window.addEventListener('keydown', handleKeyDown);\n }\n return () => {\n if (isIframe) {\n window.removeEventListener('keydown', handleKeyDown);\n }\n };\n }, []);\n\n if (searchParams.get('prepr_hide_bar') === 'true' || isIframe) {\n return null;\n }\n\n return <Toolbar />;\n}\n","import React, { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../../utils';\nimport { useEditModeContext } from '../../contexts';\nimport { useModal } from '../../hooks/use-modal';\nimport { ToolbarContent } from './toolbar-content';\nimport { ToolbarButton } from './toolbar-button';\n\ninterface ToolbarProps {\n children?: React.ReactNode;\n}\n\nexport default function Toolbar({ children }: ToolbarProps) {\n const [isMounted, setIsMounted] = useState(false);\n const [isBarVisible, setIsBarVisible] = React.useState(false);\n const { editMode } = useEditModeContext();\n const { contentRef, triggerRef } = useModal({\n isVisible: isBarVisible,\n onClose: () => setIsBarVisible(false),\n });\n\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n // Ref for the popup box\n const popupBoxRef = React.useRef<HTMLDivElement>(null);\n const [popupTop, setPopupTop] = React.useState<string | number>('');\n\n const updatePopupPosition = React.useCallback(() => {\n if (popupBoxRef.current && triggerRef.current) {\n const popupHeight = popupBoxRef.current.offsetHeight;\n const windowHeight = window.innerHeight;\n const triggerRect = triggerRef.current.getBoundingClientRect();\n // Center popup relative to the icon (trigger)\n const triggerCenter = triggerRect.top + triggerRect.height / 2;\n let top = triggerCenter - popupHeight / 2;\n // Clamp to leave at least 32px top and bottom\n top = Math.max(32, Math.min(top, windowHeight - popupHeight - 32));\n setPopupTop(top);\n }\n }, [triggerRef]);\n\n useEffect(() => {\n if (isBarVisible) {\n updatePopupPosition();\n window.addEventListener('resize', updatePopupPosition);\n return () => window.removeEventListener('resize', updatePopupPosition);\n }\n return undefined;\n }, [isBarVisible, updatePopupPosition]);\n\n const handleClick = () => {\n setIsBarVisible(!isBarVisible);\n };\n\n useEffect(() => {\n if (editMode) {\n setTimeout(() => {\n setIsBarVisible(false);\n }, 150);\n }\n }, [editMode]);\n\n const previewBarContent = (\n <>\n {isBarVisible && <div className=\"preview-bar-backdrop\" />}\n <div className={cn('preview-bar-container')}>\n {/* Button holder*/}\n <div className=\"p-pr-2\" ref={triggerRef}>\n <ToolbarButton onClick={handleClick} />\n </div>\n\n {/* Box holder */}\n <div\n ref={popupBoxRef}\n style={\n pop