@meui-creative/dev-tools
Version:
Professional responsive DevTools for React applications with device preview, performance testing, and accessibility auditing
1 lines • 141 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/DevToolsAuto.tsx","../src/components/DevToolsLauncher.tsx","../src/components/DevTools/DevTools.tsx","../src/data/devices.ts","../src/hooks/useDevToolsTests.ts","../src/utils/helpers.ts","../src/components/DevTools/DevicePreview.tsx","../src/components/DevTools/MetricsPanel.tsx","../src/components/DevTools/DeviceSelector.tsx","../src/components/DevTools/styles.ts","../src/hooks/useDevTools.ts"],"sourcesContent":["// src/components/DevToolsAuto.tsx\n\n'use client'\n\nimport React, { useEffect, useState } from 'react'\nimport { DevToolsTrigger } from './DevToolsLauncher'\nimport { DevTools as DevToolsCore } from './DevTools'\nimport { DevToolsProps } from '../types'\n\ninterface DevToolsAutoProps {\n url?: string\n hideInProduction?: boolean\n children?: React.ReactNode\n}\n\n// Automatická detekce development environment (browser-only)\nfunction isDevelopment(): boolean {\n // Check pro browser environment\n if (typeof window === 'undefined') {\n return false // SSR safe\n }\n\n // Check NODE_ENV pokud je dostupné\n if (typeof globalThis !== 'undefined') {\n const globalProcess = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process\n if (globalProcess?.env?.NODE_ENV) {\n return globalProcess.env.NODE_ENV === 'development'\n }\n }\n\n // Fallback checks pro browser\n const port = window.location.port\n const devPorts = ['3000', '3001', '5173', '8080', '4200', '8000', '9000']\n\n // Check hostname\n const hostname = window.location.hostname\n const devHosts = ['localhost', '127.0.0.1', '0.0.0.0']\n\n // Check for dev indicators\n const isDevHost = devHosts.includes(hostname)\n const isDevPort = devPorts.includes(port)\n const hasDevQuery = window.location.search.includes('dev=true')\n\n return isDevHost || isDevPort || hasDevQuery\n}\n\n/**\n * DevTools komponenta s automatickou detekcí development environment\n * Použití: <DevTools /> - automaticky se skryje v produkci\n */\nexport function DevTools(props: DevToolsAutoProps = {}) {\n const { hideInProduction = true, children } = props\n const [shouldShow, setShouldShow] = useState(false)\n\n useEffect(() => {\n const isDevEnv = isDevelopment()\n const shouldHide = hideInProduction && !isDevEnv\n const hasNoDevToolsParam =\n new URLSearchParams(window.location.search).get('no-dev-tools') === 'true'\n\n setShouldShow(!shouldHide && !hasNoDevToolsParam)\n }, [hideInProduction])\n\n // Pokud má children, použij jako wrapper\n if (children) {\n return (\n <>\n {children}\n {shouldShow && <DevToolsTrigger />}\n </>\n )\n }\n\n // Pokud nemá children, použij jen trigger\n if (!shouldShow) {\n return null\n }\n\n return <DevToolsTrigger />\n}\n\n/**\n * DevTools launcher komponenta - vždy viditelná wrapper\n * Použití: <DevToolsLauncher>{children}</DevToolsLauncher>\n */\nexport function DevToolsLauncher({ children }: { children: React.ReactNode }) {\n return (\n <>\n {children}\n <DevTools />\n </>\n )\n}\n\n/**\n * Manuální DevTools komponenta - pro pokročilé použití\n * Použití: const {isOpen, toggle} = useDevTools(); <DevToolsManual isOpen={isOpen} onToggle={toggle} />\n */\nexport function DevToolsManual(props: DevToolsProps) {\n const [shouldShow, setShouldShow] = useState(false)\n\n useEffect(() => {\n const hasNoDevToolsParam =\n new URLSearchParams(window.location.search).get('no-dev-tools') === 'true'\n setShouldShow(!hasNoDevToolsParam)\n }, [])\n\n if (!shouldShow) {\n return null\n }\n\n return <DevToolsCore {...props} />\n}\n\n/**\n * Force DevTools - vždy viditelná bez kontrol\n * Použití: <DevToolsForce /> - pro testování\n */\nexport function DevToolsForce(props: Omit<DevToolsAutoProps, 'hideInProduction'>) {\n return <DevTools {...props} hideInProduction={false} />\n}\n","// src/components/DevToolsLauncher.tsx\n\n'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { DevTools } from './DevTools'\nimport { useDevTools } from '../hooks/useDevTools'\nimport { Monitor, Settings2 } from 'lucide-react'\n\n// Browser-safe production check\nfunction isProduction(): boolean {\n if (typeof window === 'undefined') {\n return true // SSR safe - assume production\n }\n\n // Check NODE_ENV pokud je dostupné\n if (typeof globalThis !== 'undefined') {\n const globalProcess = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process\n if (globalProcess?.env?.NODE_ENV) {\n return globalProcess.env.NODE_ENV === 'production'\n }\n }\n\n // Fallback - check por development indicators\n const port = window.location.port\n const hostname = window.location.hostname\n const devPorts = ['3000', '3001', '5173', '8080', '4200', '8000', '9000']\n const devHosts = ['localhost', '127.0.0.1', '0.0.0.0']\n\n // If on dev host/port, assume development\n const isDev = devHosts.includes(hostname) || devPorts.includes(port)\n\n return !isDev // Return true for production if NOT on dev host/port\n}\n\nexport function DevToolsTrigger() {\n const { isOpen, toggle } = useDevTools()\n const [showTooltip, setShowTooltip] = useState(false)\n const [shouldHide, setShouldHide] = useState(false)\n\n // Skrýt pokud je no-dev-tools parametr\n useEffect(() => {\n const checkParams = () => {\n const urlParams = new URLSearchParams(window.location.search)\n setShouldHide(urlParams.get('no-dev-tools') === 'true')\n }\n\n checkParams()\n window.addEventListener('popstate', checkParams)\n\n return () => window.removeEventListener('popstate', checkParams)\n }, [])\n\n // Zobrazit pouze v development mode\n if (isProduction()) {\n return null\n }\n\n if (shouldHide) {\n return null\n }\n\n const triggerStyles: React.CSSProperties = {\n position: 'fixed',\n bottom: '20px',\n right: '20px',\n zIndex: 999997,\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }\n\n const buttonStyles: React.CSSProperties = {\n width: '56px',\n height: '56px',\n borderRadius: '50%',\n background: isOpen\n ? 'linear-gradient(135deg, #3b82f6, #1d4ed8)'\n : 'linear-gradient(135deg, #ffffff, #f8fafc)',\n border: '1px solid rgba(0, 0, 0, 0.1)',\n boxShadow: isOpen\n ? '0 20px 25px -5px rgba(59, 130, 246, 0.4), 0 10px 10px -5px rgba(59, 130, 246, 0.2)'\n : '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n transform: isOpen ? 'scale(1.1)' : 'scale(1)',\n color: isOpen ? '#ffffff' : '#374151',\n outline: 'none',\n }\n\n const tooltipStyles: React.CSSProperties = {\n position: 'absolute',\n bottom: '100%',\n right: '0',\n marginBottom: '12px',\n padding: '12px 16px',\n background: 'rgba(17, 24, 39, 0.95)',\n color: '#ffffff',\n fontSize: '12px',\n borderRadius: '8px',\n whiteSpace: 'nowrap',\n backdropFilter: 'blur(8px)',\n border: '1px solid rgba(255, 255, 255, 0.1)',\n boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',\n opacity: showTooltip ? 1 : 0,\n transform: showTooltip ? 'translateY(0)' : 'translateY(4px)',\n transition: 'all 0.2s ease-out',\n pointerEvents: 'none',\n zIndex: 999998,\n }\n\n return (\n <>\n <div style={triggerStyles}>\n <button\n onClick={toggle}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={buttonStyles}\n title=\"Open DevTools\"\n >\n {isOpen ? (\n <Settings2 size={24} style={{ animation: 'spin 2s linear infinite' }} />\n ) : (\n <Monitor size={24} />\n )}\n </button>\n\n <div style={tooltipStyles}>\n <div style={{ fontWeight: 600, marginBottom: '4px' }}>DevTools</div>\n <div style={{ opacity: 0.8, fontSize: '11px' }}>\n {isOpen ? 'Click to close' : 'Ctrl+Shift+D'}\n </div>\n </div>\n\n {isOpen && (\n <div\n style={{\n position: 'absolute',\n top: '-2px',\n right: '-2px',\n width: '12px',\n height: '12px',\n background: '#10b981',\n borderRadius: '50%',\n border: '2px solid #ffffff',\n animation: 'pulse 2s ease-in-out infinite',\n }}\n />\n )}\n </div>\n\n <DevTools isOpen={isOpen} onToggle={toggle} />\n\n <style>{`\n @keyframes spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.5; }\n }\n `}</style>\n </>\n )\n}\n\n// Quick integration component\nexport function DevToolsLauncher({ children }: { children: React.ReactNode }) {\n return (\n <>\n {children}\n <DevToolsTrigger />\n </>\n )\n}\n\n// HOC pro snadnou integraci\nexport function withDevTools<T extends object>(Component: React.ComponentType<T>) {\n return function DevToolsWrapper(props: T) {\n return (\n <DevToolsLauncher>\n <Component {...props} />\n </DevToolsLauncher>\n )\n }\n}\n\n// Context pro pokročilé použití\ninterface DevToolsContextType {\n isOpen: boolean\n toggle: () => void\n open: () => void\n close: () => void\n}\n\nconst DevToolsContext = React.createContext<DevToolsContextType | undefined>(undefined)\n\nexport function DevToolsProvider({ children }: { children: React.ReactNode }) {\n const devTools = useDevTools()\n\n return (\n <DevToolsContext.Provider value={devTools}>\n {children}\n <DevTools isOpen={devTools.isOpen} onToggle={devTools.toggle} />\n </DevToolsContext.Provider>\n )\n}\n\nexport function useDevToolsContext() {\n const context = React.useContext(DevToolsContext)\n if (context === undefined) {\n throw new Error('useDevToolsContext must be used within a DevToolsProvider')\n }\n return context\n}\n\n// Utility pro testování konkrétních komponent\nexport function DevToolsStandalone({ url }: { url?: string }) {\n const [isOpen, setIsOpen] = useState(true)\n\n if (isProduction()) {\n return null\n }\n\n return <DevTools url={url} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} />\n}\n\n// Re-export hook\nexport { useDevTools } from '../hooks/useDevTools'\n","// src/components/DevTools/DevTools.tsx\n\n'use client'\n\nimport React, { useState, useEffect, useRef } from 'react'\nimport {\n Monitor,\n Smartphone,\n RotateCcw,\n X,\n Moon,\n Sun,\n Grid,\n Minimize2,\n MoreHorizontal,\n RefreshCw,\n Accessibility,\n} from 'lucide-react'\n\nimport {\n Device,\n DevToolsProps,\n EnhancedLighthouseMetrics,\n DetailedAccessibilityIssue,\n ComprehensiveSEOMetrics,\n SecurityMetrics,\n} from '../../types'\nimport { devices, essentialDevices } from '../../data/devices'\nimport { useDevToolsTests } from '../../hooks/useDevToolsTests'\nimport { getCleanUrl, refreshAllFrames } from '../../utils/helpers'\nimport { DevicePreview } from './DevicePreview'\nimport { MetricsPanel } from './MetricsPanel'\nimport { DeviceSelector } from './DeviceSelector'\nimport { devToolsStyles } from './styles'\n\nexport function DevTools({\n url = typeof window !== 'undefined' ? window.location.href : '/',\n isOpen,\n onToggle,\n}: DevToolsProps) {\n // State\n const [selectedDevices, setSelectedDevices] = useState<Device[]>(essentialDevices)\n const [darkMode, setDarkMode] = useState(false)\n const [showGrid, setShowGrid] = useState(false)\n const [deviceRotations, setDeviceRotations] = useState<Map<string, boolean>>(new Map())\n const [fullscreenDevice, setFullscreenDevice] = useState<Device | null>(null)\n const [showMoreControls, setShowMoreControls] = useState(false)\n const [realDeviceBehavior, setRealDeviceBehavior] = useState(false)\n const [accessibilityMode, setAccessibilityMode] = useState<'none' | 'contrast' | 'colorblind'>(\n 'none'\n )\n\n // Metrics state\n const [performanceData, setPerformanceData] = useState<Map<string, EnhancedLighthouseMetrics>>(\n new Map()\n )\n const [accessibilityData, setAccessibilityData] = useState<\n Map<string, DetailedAccessibilityIssue[]>\n >(new Map())\n const [seoData, setSeoData] = useState<Map<string, ComprehensiveSEOMetrics>>(new Map())\n const [securityData, setSecurityData] = useState<Map<string, SecurityMetrics>>(new Map())\n const [runningTests, setRunningTests] = useState<Set<string>>(new Set())\n const [activeMetricsDevice, setActiveMetricsDevice] = useState<string | null>(null)\n const [metricsExpanded, setMetricsExpanded] = useState(true)\n const [activeTab, setActiveTab] = useState<'performance' | 'accessibility' | 'seo' | 'security'>(\n 'performance'\n )\n\n const moreControlsRef = useRef<HTMLDivElement>(null)\n\n // Custom hooks\n const { runPerformanceTest } = useDevToolsTests(\n setPerformanceData,\n setAccessibilityData,\n setSeoData,\n setSecurityData,\n setRunningTests,\n setActiveMetricsDevice,\n setActiveTab\n )\n\n // Effects\n useEffect(() => {\n if (isOpen) {\n document.body.style.overflow = 'hidden'\n } else {\n document.body.style.overflow = 'unset'\n }\n return () => {\n document.body.style.overflow = 'unset'\n }\n }, [isOpen])\n\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (moreControlsRef.current && !moreControlsRef.current.contains(event.target as Node)) {\n setShowMoreControls(false)\n }\n }\n\n if (showMoreControls) {\n document.addEventListener('mousedown', handleClickOutside)\n }\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n }\n }, [showMoreControls])\n\n // Handlers\n const toggleDeviceRotation = (deviceName: string) => {\n setDeviceRotations(prev => {\n const newMap = new Map(prev)\n newMap.set(deviceName, !prev.get(deviceName))\n return newMap\n })\n }\n\n const toggleDevice = (device: Device) => {\n setSelectedDevices(prev => {\n const isSelected = prev.find(d => d.name === device.name)\n if (isSelected) {\n return prev.filter(d => d.name !== device.name)\n } else {\n return [...prev, device]\n }\n })\n }\n\n const loadEssentials = () => {\n setSelectedDevices(essentialDevices)\n }\n\n if (!isOpen) return null\n\n const currentMetrics = activeMetricsDevice ? performanceData.get(activeMetricsDevice) : null\n const currentAccessibility = activeMetricsDevice\n ? accessibilityData.get(activeMetricsDevice)\n : null\n const currentSEO = activeMetricsDevice ? seoData.get(activeMetricsDevice) : null\n const currentSecurity = activeMetricsDevice ? securityData.get(activeMetricsDevice) : null\n\n return (\n <div>\n <style>{devToolsStyles(darkMode)}</style>\n\n <div className=\"devtools-container\">\n {/* Header */}\n <div className=\"devtools-header\">\n <div className=\"header-title\">DevTools • {selectedDevices.length} devices</div>\n\n <div className=\"header-controls\">\n <div className=\"control-group\">\n <button onClick={refreshAllFrames} className=\"control-btn\" title=\"Refresh all\">\n <RefreshCw className=\"w-4 h-4\" />\n </button>\n </div>\n\n <div className=\"more-controls\" ref={moreControlsRef}>\n <button\n onClick={() => setShowMoreControls(!showMoreControls)}\n className={`control-btn ${showMoreControls ? 'active' : ''}`}\n title=\"More options\"\n >\n <MoreHorizontal className=\"w-4 h-4\" />\n </button>\n\n {showMoreControls && (\n <div className=\"more-dropdown\">\n <div\n className={`more-item ${darkMode ? 'active' : ''}`}\n role=\"button\"\n tabIndex={0}\n onClick={() => setDarkMode(!darkMode)}\n onKeyDown={(e) => e.key === 'Enter' && setDarkMode(!darkMode)}\n >\n {darkMode ? <Sun className=\"w-4 h-4\" /> : <Moon className=\"w-4 h-4\" />}\n Dark Mode\n </div>\n <div\n className={`more-item ${showGrid ? 'active' : ''}`}\n role=\"button\"\n tabIndex={0}\n onClick={() => setShowGrid(!showGrid)}\n onKeyDown={(e) => e.key === 'Enter' && setShowGrid(!showGrid)}\n >\n <Grid className=\"w-4 h-4\" />\n Grid Overlay\n </div>\n <div\n className={`more-item ${realDeviceBehavior ? 'active' : ''}`}\n role=\"button\"\n tabIndex={0}\n onClick={() => setRealDeviceBehavior(!realDeviceBehavior)}\n onKeyDown={(e) => e.key === 'Enter' && setRealDeviceBehavior(!realDeviceBehavior)}\n >\n <Smartphone className=\"w-4 h-4\" />\n Real Device UI\n </div>\n <div\n className={`more-item ${accessibilityMode !== 'none' ? 'active' : ''}`}\n role=\"button\"\n tabIndex={0}\n onClick={() =>\n setAccessibilityMode(\n accessibilityMode === 'none'\n ? 'contrast'\n : accessibilityMode === 'contrast'\n ? 'colorblind'\n : 'none'\n )\n }\n onKeyDown={(e) => e.key === 'Enter' && setAccessibilityMode(\n accessibilityMode === 'none'\n ? 'contrast'\n : accessibilityMode === 'contrast'\n ? 'colorblind'\n : 'none'\n )}\n >\n <Accessibility className=\"w-4 h-4\" />\n A11y Filters ({accessibilityMode})\n </div>\n <div\n className=\"more-item\"\n role=\"button\"\n tabIndex={0}\n onClick={() => {\n setSelectedDevices([])\n setShowMoreControls(false)\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n setSelectedDevices([])\n setShowMoreControls(false)\n }\n }}\n >\n <X className=\"w-4 h-4\" />\n Clear All\n </div>\n </div>\n )}\n </div>\n\n <button onClick={onToggle} className=\"control-btn\" title=\"Close DevTools\">\n <X className=\"w-4 h-4\" />\n </button>\n </div>\n </div>\n\n <div className=\"content-wrapper\">\n {/* Sidebar */}\n <DeviceSelector\n devices={devices}\n selectedDevices={selectedDevices}\n deviceRotations={deviceRotations}\n onToggleDevice={toggleDevice}\n onLoadEssentials={loadEssentials}\n onFullscreen={setFullscreenDevice}\n />\n\n {/* Preview Area */}\n <div className=\"preview-area\">\n <div className=\"preview-content\">\n {selectedDevices.length === 0 ? (\n <div className=\"empty-state\">\n <Monitor className=\"w-12 h-12\" style={{ marginBottom: '12px', opacity: 0.3 }} />\n <p style={{ fontSize: '16px', marginBottom: '6px', fontWeight: 500 }}>\n No devices selected\n </p>\n <p style={{ fontSize: '13px', opacity: 0.7, marginBottom: '12px' }}>\n Choose devices or load essentials\n </p>\n <button onClick={loadEssentials} className=\"preset-btn\">\n Load Essentials\n </button>\n </div>\n ) : (\n <div className=\"preview-grid\">\n {selectedDevices.map(device => (\n <DevicePreview\n key={device.name}\n device={device}\n url={url}\n deviceRotations={deviceRotations}\n performanceData={performanceData}\n runningTests={runningTests}\n realDeviceBehavior={realDeviceBehavior}\n accessibilityMode={accessibilityMode}\n showGrid={showGrid}\n onToggleRotation={toggleDeviceRotation}\n onRemove={() => toggleDevice(device)}\n onFullscreen={() => setFullscreenDevice(device)}\n onRunTest={runPerformanceTest}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Metrics Panel */}\n {(currentMetrics || currentAccessibility || currentSEO || currentSecurity) && activeMetricsDevice && (\n <MetricsPanel\n deviceName={activeMetricsDevice}\n isExpanded={metricsExpanded}\n activeTab={activeTab}\n performanceData={currentMetrics || undefined}\n accessibilityData={currentAccessibility || undefined}\n seoData={currentSEO || undefined}\n securityData={currentSecurity || undefined}\n onToggleExpand={() => setMetricsExpanded(!metricsExpanded)}\n onClose={() => setActiveMetricsDevice(null)}\n onTabChange={setActiveTab}\n />\n )}\n\n {/* Fullscreen Modal */}\n {fullscreenDevice && (\n <div className=\"fullscreen-overlay\">\n <div className=\"fullscreen-header\">\n <div>\n <div className=\"device-title\">\n {fullscreenDevice.icon}\n {fullscreenDevice.name}\n <span style={{ marginLeft: '12px', opacity: 0.7, fontSize: '13px' }}>\n {deviceRotations.get(fullscreenDevice.name)\n ? fullscreenDevice.height\n : fullscreenDevice.width}{' '}\n ×{' '}\n {deviceRotations.get(fullscreenDevice.name)\n ? fullscreenDevice.width\n : fullscreenDevice.height}\n </span>\n </div>\n </div>\n\n <div className=\"header-controls\">\n <button\n onClick={() => toggleDeviceRotation(fullscreenDevice.name)}\n className={`control-btn ${deviceRotations.get(fullscreenDevice.name) ? 'active' : ''}`}\n >\n <RotateCcw className=\"w-4 h-4\" />\n </button>\n <button onClick={() => setFullscreenDevice(null)} className=\"control-btn\">\n <Minimize2 className=\"w-4 h-4\" />\n </button>\n </div>\n </div>\n\n <div className=\"fullscreen-viewport\">\n {(() => {\n const isRotated = deviceRotations.get(fullscreenDevice.name) || false\n const deviceWidth = isRotated ? fullscreenDevice.height : fullscreenDevice.width\n const deviceHeight = isRotated ? fullscreenDevice.width : fullscreenDevice.height\n\n const viewportWidth = window.innerWidth - 40\n const viewportHeight = window.innerHeight - 100\n\n const scaleX = viewportWidth / deviceWidth\n const scaleY = viewportHeight / deviceHeight\n const scale = Math.min(scaleX, scaleY)\n\n const finalWidth = deviceWidth * scale\n const finalHeight = deviceHeight * scale\n\n return (\n <iframe\n src={getCleanUrl(url)}\n title={`${fullscreenDevice.name} preview in fullscreen mode`}\n style={{\n width: finalWidth,\n height: finalHeight,\n border: 'none',\n borderRadius: '8px',\n boxShadow: '0 20px 40px -12px rgba(0, 0, 0, 0.25)',\n }}\n />\n )\n })()}\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n","// src/data/devices.ts\n\nimport React from 'react'\nimport { Smartphone, Tablet, Laptop, Monitor, MonitorSpeaker } from 'lucide-react'\nimport { Device } from '../types'\n\nexport const devices: Device[] = [\n // Phones\n {\n name: 'iPhone SE',\n width: 375,\n height: 667,\n icon: React.createElement(Smartphone, { className: 'w-4 h-4' }),\n category: 'phone',\n pixelRatio: 2,\n statusBar: true,\n browserUI: true,\n os: 'ios',\n },\n {\n name: 'iPhone 15 Pro',\n width: 393,\n height: 852,\n icon: React.createElement(Smartphone, { className: 'w-4 h-4' }),\n category: 'phone',\n pixelRatio: 3,\n hasNotch: true,\n statusBar: true,\n browserUI: true,\n os: 'ios',\n },\n {\n name: 'Samsung S24 Ultra',\n width: 412,\n height: 915,\n icon: React.createElement(Smartphone, { className: 'w-4 h-4' }),\n category: 'phone',\n pixelRatio: 3,\n statusBar: true,\n browserUI: true,\n os: 'android',\n },\n\n // Tablets\n {\n name: 'iPad Mini',\n width: 768,\n height: 1024,\n icon: React.createElement(Tablet, { className: 'w-4 h-4' }),\n category: 'tablet',\n pixelRatio: 2,\n statusBar: true,\n browserUI: true,\n os: 'ios',\n },\n {\n name: 'iPad Air',\n width: 820,\n height: 1180,\n icon: React.createElement(Tablet, { className: 'w-4 h-4' }),\n category: 'tablet',\n pixelRatio: 2,\n statusBar: true,\n browserUI: true,\n os: 'ios',\n },\n {\n name: 'iPad Pro 12.9\"',\n width: 1024,\n height: 1366,\n icon: React.createElement(Tablet, { className: 'w-4 h-4' }),\n category: 'tablet',\n pixelRatio: 2,\n statusBar: true,\n browserUI: true,\n os: 'ios',\n },\n\n // Laptops\n {\n name: 'MacBook Air 13\"',\n width: 1280,\n height: 800,\n icon: React.createElement(Laptop, { className: 'w-4 h-4' }),\n category: 'laptop',\n browserUI: true,\n os: 'macos',\n },\n {\n name: 'MacBook Pro 14\"',\n width: 1512,\n height: 982,\n icon: React.createElement(Laptop, { className: 'w-4 h-4' }),\n category: 'laptop',\n browserUI: true,\n os: 'macos',\n },\n {\n name: 'MacBook Pro 16\"',\n width: 1728,\n height: 1117,\n icon: React.createElement(Laptop, { className: 'w-4 h-4' }),\n category: 'laptop',\n browserUI: true,\n os: 'macos',\n },\n\n // Desktops\n {\n name: 'Small Desktop',\n width: 1366,\n height: 768,\n icon: React.createElement(Monitor, { className: 'w-4 h-4' }),\n category: 'desktop',\n browserUI: true,\n os: 'windows',\n },\n {\n name: 'Standard Desktop',\n width: 1920,\n height: 1080,\n icon: React.createElement(Monitor, { className: 'w-4 h-4' }),\n category: 'desktop',\n browserUI: true,\n os: 'windows',\n },\n {\n name: '4K Desktop',\n width: 3840,\n height: 2160,\n icon: React.createElement(Monitor, { className: 'w-4 h-4' }),\n category: 'desktop',\n browserUI: true,\n os: 'windows',\n },\n\n // Special Monitors\n {\n name: 'Ultrawide 21:9',\n width: 3440,\n height: 1440,\n icon: React.createElement(MonitorSpeaker, { className: 'w-4 h-4' }),\n category: 'special',\n browserUI: true,\n os: 'windows',\n },\n {\n name: 'Super Ultrawide 32:9',\n width: 5120,\n height: 1440,\n icon: React.createElement(MonitorSpeaker, { className: 'w-4 h-4' }),\n category: 'special',\n browserUI: true,\n os: 'windows',\n },\n {\n name: 'Vertical Coding',\n width: 1440,\n height: 2560,\n icon: React.createElement(MonitorSpeaker, { className: 'w-4 h-4' }),\n category: 'special',\n browserUI: true,\n os: 'windows',\n },\n]\n\nexport const essentialDevices = [\n devices.find(d => d.name === 'iPhone 15 Pro'),\n devices.find(d => d.name === 'MacBook Air 13\"'),\n devices.find(d => d.name === '4K Desktop'),\n].filter((device): device is Device => device !== undefined)\n\nexport const deviceCategories = {\n phone: { title: 'Phones', icon: React.createElement(Smartphone, { className: 'w-4 h-4' }) },\n tablet: { title: 'Tablets', icon: React.createElement(Tablet, { className: 'w-4 h-4' }) },\n laptop: { title: 'Laptops', icon: React.createElement(Laptop, { className: 'w-4 h-4' }) },\n desktop: { title: 'Desktops', icon: React.createElement(Monitor, { className: 'w-4 h-4' }) },\n special: {\n title: 'Special',\n icon: React.createElement(MonitorSpeaker, { className: 'w-4 h-4' }),\n },\n}\n","// src/hooks/useDevToolsTests.ts\n\nimport { useCallback } from 'react'\nimport { devices } from '../data/devices'\nimport {\n EnhancedLighthouseMetrics,\n DetailedAccessibilityIssue,\n ComprehensiveSEOMetrics,\n SecurityMetrics,\n} from '../types'\n\nexport const useDevToolsTests = (\n setPerformanceData: React.Dispatch<React.SetStateAction<Map<string, EnhancedLighthouseMetrics>>>,\n setAccessibilityData: React.Dispatch<\n React.SetStateAction<Map<string, DetailedAccessibilityIssue[]>>\n >,\n setSeoData: React.Dispatch<React.SetStateAction<Map<string, ComprehensiveSEOMetrics>>>,\n setSecurityData: React.Dispatch<React.SetStateAction<Map<string, SecurityMetrics>>>,\n setRunningTests: React.Dispatch<React.SetStateAction<Set<string>>>,\n setActiveMetricsDevice: React.Dispatch<React.SetStateAction<string | null>>,\n setActiveTab: React.Dispatch<\n React.SetStateAction<'performance' | 'accessibility' | 'seo' | 'security'>\n >\n) => {\n const runAccessibilityTest = useCallback(\n async (deviceName: string) => {\n await new Promise(resolve => setTimeout(resolve, 2000))\n\n const possibleIssues: DetailedAccessibilityIssue[] = [\n {\n type: 'contrast',\n severity: 'serious',\n message: 'Text elements have insufficient color contrast ratio',\n element: 'button.primary, .text-muted',\n count: 8,\n impact: 'high',\n helpUrl: 'https://webaim.org/articles/contrast/',\n },\n {\n type: 'alt',\n severity: 'critical',\n message: 'Images missing alternative text',\n element: 'img',\n count: 5,\n impact: 'high',\n helpUrl: 'https://webaim.org/techniques/alttext/',\n },\n {\n type: 'heading',\n severity: 'moderate',\n message: 'Heading structure is not properly nested',\n element: 'h1, h3',\n count: 2,\n impact: 'medium',\n helpUrl: 'https://webaim.org/techniques/semanticstructure/',\n },\n {\n type: 'keyboard',\n severity: 'serious',\n message: 'Interactive elements not keyboard accessible',\n element: 'div[onclick]',\n count: 3,\n impact: 'high',\n helpUrl: 'https://webaim.org/techniques/keyboard/',\n },\n {\n type: 'aria',\n severity: 'moderate',\n message: 'Form elements missing proper labels',\n element: 'input, select',\n count: 4,\n impact: 'medium',\n helpUrl: 'https://webaim.org/techniques/forms/',\n },\n ]\n\n const issues = possibleIssues.filter(() => Math.random() > 0.3)\n setAccessibilityData(prev => new Map(prev).set(deviceName, issues))\n },\n [setAccessibilityData]\n )\n\n const runSEOTest = useCallback(\n async (deviceName: string) => {\n await new Promise(resolve => setTimeout(resolve, 2500))\n\n const titleLength = Math.round(35 + Math.random() * 30)\n const descLength = Math.round(120 + Math.random() * 40)\n const imageTotal = Math.round(8 + Math.random() * 12)\n const imageMissing = Math.round(Math.random() * 5)\n\n const seoMetrics: ComprehensiveSEOMetrics = {\n score: Math.round(70 + Math.random() * 25),\n // Basic SEO\n title: {\n exists: Math.random() > 0.1,\n length: titleLength,\n optimal: titleLength >= 30 && titleLength <= 60,\n },\n metaDescription: {\n exists: Math.random() > 0.2,\n length: descLength,\n optimal: descLength >= 120 && descLength <= 160,\n },\n headings: {\n h1Count: Math.round(Math.random() * 3),\n structure: Math.random() > 0.3,\n optimal: Math.random() > 0.4,\n },\n imageAlts: {\n total: imageTotal,\n missing: imageMissing,\n percentage: Math.round(((imageTotal - imageMissing) / imageTotal) * 100),\n },\n // Technical SEO\n canonicalUrl: Math.random() > 0.3,\n viewport: Math.random() > 0.1,\n https: Math.random() > 0.05,\n mobileFriendly: Math.random() > 0.2,\n pageSpeed: Math.round(60 + Math.random() * 35),\n // Content SEO\n wordCount: Math.round(300 + Math.random() * 1200),\n readabilityScore: Math.round(60 + Math.random() * 35),\n internalLinks: Math.round(3 + Math.random() * 12),\n externalLinks: Math.round(1 + Math.random() * 6),\n // Schema & Structure\n structuredData: Math.random() > 0.4,\n openGraph: Math.random() > 0.3,\n twitterCards: Math.random() > 0.5,\n breadcrumbs: Math.random() > 0.6,\n // Advanced\n xmlSitemap: Math.random() > 0.3,\n robotsTxt: Math.random() > 0.2,\n hreflang: Math.random() > 0.7,\n coreWebVitals: Math.random() > 0.4,\n issues: [\n 'Meta description too short',\n 'Missing H1 tag',\n 'Images without alt text',\n 'No canonical URL specified',\n 'Missing OpenGraph tags',\n 'Low text-to-HTML ratio',\n 'Too many internal links',\n 'Missing schema markup',\n ].filter(() => Math.random() > 0.6),\n recommendations: [\n 'Optimize meta description length (120-160 characters)',\n 'Add structured data markup',\n 'Improve internal linking structure',\n 'Compress and optimize images',\n 'Add breadcrumb navigation',\n 'Implement lazy loading for images',\n 'Minify CSS and JavaScript',\n 'Use descriptive anchor text',\n ].filter(() => Math.random() > 0.5),\n }\n\n setSeoData(prev => new Map(prev).set(deviceName, seoMetrics))\n },\n [setSeoData]\n )\n\n const runSecurityTest = useCallback(\n async (deviceName: string) => {\n await new Promise(resolve => setTimeout(resolve, 1800))\n\n const securityMetrics: SecurityMetrics = {\n score: Math.round(70 + Math.random() * 25),\n https: Math.random() > 0.1,\n hsts: Math.random() > 0.4,\n contentSecurityPolicy: Math.random() > 0.6,\n xFrameOptions: Math.random() > 0.3,\n mixedContent: Math.random() < 0.2,\n vulnerabilities: Math.round(Math.random() * 3),\n }\n\n setSecurityData(prev => new Map(prev).set(deviceName, securityMetrics))\n },\n [setSecurityData]\n )\n\n const runPerformanceTest = useCallback(\n async (deviceName: string) => {\n setRunningTests(prev => new Set(prev).add(deviceName))\n setActiveMetricsDevice(deviceName)\n setActiveTab('performance')\n\n await new Promise(resolve => setTimeout(resolve, 3000 + Math.random() * 2000))\n\n const device = devices.find(d => d.name === deviceName)\n const isMobile = device?.category === 'phone'\n const isTablet = device?.category === 'tablet'\n\n const basePerformance = isMobile ? 60 + Math.random() * 30 : 70 + Math.random() * 25\n const loadTimeBase = isMobile ? 3200 : isTablet ? 2800 : 2200\n\n const metrics: EnhancedLighthouseMetrics = {\n performance: Math.round(basePerformance),\n accessibility: Math.round(75 + Math.random() * 20),\n bestPractices: Math.round(80 + Math.random() * 15),\n seo: Math.round(85 + Math.random() * 12),\n pwa: Math.round(40 + Math.random() * 30),\n // Core Web Vitals\n loadTime: Math.round(loadTimeBase + Math.random() * 1500),\n firstContentfulPaint: Math.round(loadTimeBase * 0.4 + Math.random() * 800),\n largestContentfulPaint: Math.round(loadTimeBase * 0.7 + Math.random() * 1200),\n firstInputDelay: Math.round(80 + Math.random() * 150),\n cumulativeLayoutShift: Math.round((0.05 + Math.random() * 0.2) * 1000) / 1000,\n speedIndex: Math.round(loadTimeBase * 0.8 + Math.random() * 1000),\n totalBlockingTime: Math.round(100 + Math.random() * 300),\n // Additional metrics\n timeToInteractive: Math.round(loadTimeBase * 0.9 + Math.random() * 1000),\n maxPotentialFID: Math.round(120 + Math.random() * 200),\n serverResponseTime: Math.round(150 + Math.random() * 300),\n // Resource analysis\n totalSize: Math.round(1200 + Math.random() * 2800), // KB\n imageOptimization: Math.round(60 + Math.random() * 35),\n textCompression: Math.round(70 + Math.random() * 25),\n unusedCode: Math.round(15 + Math.random() * 35),\n renderBlocking: Math.round(5 + Math.random() * 15),\n // Network\n requestCount: Math.round(25 + Math.random() * 45),\n cacheHitRatio: Math.round(60 + Math.random() * 35),\n cdnUsage: Math.random() > 0.3,\n }\n\n setPerformanceData(prev => new Map(prev).set(deviceName, metrics))\n setRunningTests(prev => {\n const newSet = new Set(prev)\n newSet.delete(deviceName)\n return newSet\n })\n\n // Auto-run other tests\n setTimeout(() => runAccessibilityTest(deviceName), 500)\n setTimeout(() => runSEOTest(deviceName), 1000)\n setTimeout(() => runSecurityTest(deviceName), 1500)\n },\n [\n setPerformanceData,\n setRunningTests,\n setActiveMetricsDevice,\n setActiveTab,\n runAccessibilityTest,\n runSEOTest,\n runSecurityTest\n ]\n )\n\n return {\n runPerformanceTest,\n runAccessibilityTest,\n runSEOTest,\n runSecurityTest,\n }\n}\n","// src/utils/helpers.ts\n\nimport { CheckCircle, AlertTriangle, AlertCircle } from 'lucide-react'\nimport { Device, PerformanceClass, VitalStatus } from '../types'\n\nexport const getScaleFactor = (device: Device, deviceRotations: Map<string, boolean>) => {\n const isRotated = deviceRotations.get(device.name) || false\n const displayWidth = isRotated ? device.height : device.width\n const displayHeight = isRotated ? device.width : device.height\n\n let maxWidth: number, maxHeight: number\n\n switch (device.category) {\n case 'phone':\n maxWidth = 260\n maxHeight = 480\n break\n case 'tablet':\n maxWidth = 380\n maxHeight = 520\n break\n case 'laptop':\n maxWidth = 500\n maxHeight = 320\n break\n case 'desktop':\n maxWidth = 600\n maxHeight = 360\n break\n case 'special':\n maxWidth = 800\n maxHeight = 300\n break\n default:\n maxWidth = 600\n maxHeight = 360\n break\n }\n\n const scaleX = maxWidth / displayWidth\n const scaleY = maxHeight / displayHeight\n return Math.min(scaleX, scaleY, 0.75)\n}\n\nexport const getPerformanceClass = (score: number): PerformanceClass => {\n if (score >= 90) return { class: 'excellent', color: '#10b981', label: 'Excellent' }\n if (score >= 75) return { class: 'good', color: '#22c55e', label: 'Good' }\n if (score >= 50) return { class: 'average', color: '#f59e0b', label: 'Needs Improvement' }\n return { class: 'poor', color: '#ef4444', label: 'Poor' }\n}\n\nexport const getVitalStatus = (metric: string, value: number): VitalStatus => {\n const thresholds: Record<string, { good: number; needsImprovement: number }> = {\n firstContentfulPaint: { good: 1800, needsImprovement: 3000 },\n largestContentfulPaint: { good: 2500, needsImprovement: 4000 },\n firstInputDelay: { good: 100, needsImprovement: 300 },\n cumulativeLayoutShift: { good: 0.1, needsImprovement: 0.25 },\n speedIndex: { good: 3400, needsImprovement: 5800 },\n totalBlockingTime: { good: 200, needsImprovement: 600 },\n timeToInteractive: { good: 3800, needsImprovement: 7300 },\n loadTime: { good: 2000, needsImprovement: 4000 },\n }\n\n const threshold = thresholds[metric]\n if (!threshold) return { color: '#6b7280', status: 'unknown' }\n\n if (value <= threshold.good) {\n return { color: '#10b981', status: 'good', icon: CheckCircle }\n } else if (value <= threshold.needsImprovement) {\n return { color: '#f59e0b', status: 'needs-improvement', icon: AlertTriangle }\n } else {\n return { color: '#ef4444', status: 'poor', icon: AlertCircle }\n }\n}\n\nexport const getCleanUrl = (originalUrl: string): string => {\n try {\n const url = new URL(originalUrl)\n url.searchParams.set('no-dev-tools', 'true')\n return url.toString()\n } catch {\n return originalUrl + (originalUrl.includes('?') ? '&' : '?') + 'no-dev-tools=true'\n }\n}\n\nexport const refreshAllFrames = (): void => {\n const iframes = document.querySelectorAll('.device-iframe')\n iframes.forEach(iframe => {\n const src = (iframe as HTMLIFrameElement).src\n ;(iframe as HTMLIFrameElement).src = ''\n setTimeout(() => {\n ;(iframe as HTMLIFrameElement).src = src\n }, 50)\n })\n}\n","// src/components/DevTools/DevicePreview.tsx\n\nimport React from 'react'\nimport {\n Play,\n RotateCcw,\n Maximize2,\n X,\n Wifi,\n Battery,\n Signal,\n Lock,\n ArrowLeft,\n ArrowRight,\n RotateCw,\n} from 'lucide-react'\nimport { Device, EnhancedLighthouseMetrics } from '../../types'\nimport { getScaleFactor, getPerformanceClass, getCleanUrl } from '../../utils/helpers'\n\ninterface DevicePreviewProps {\n device: Device\n url: string\n deviceRotations: Map<string, boolean>\n performanceData: Map<string, EnhancedLighthouseMetrics>\n runningTests: Set<string>\n realDeviceBehavior: boolean\n accessibilityMode: 'none' | 'contrast' | 'colorblind'\n showGrid: boolean\n onToggleRotation: (deviceName: string) => void\n onRemove: () => void\n onFullscreen: () => void\n onRunTest: (deviceName: string) => void\n}\n\nexport function DevicePreview({\n device,\n url,\n deviceRotations,\n performanceData,\n runningTests,\n realDeviceBehavior,\n accessibilityMode,\n showGrid,\n onToggleRotation,\n onRemove,\n onFullscreen,\n onRunTest,\n}: DevicePreviewProps) {\n const isRotated = deviceRotations.get(device.name) || false\n const scaleFactor = getScaleFactor(device, deviceRotations)\n const displayWidth = isRotated ? device.height : device.width\n const displayHeight = isRotated ? device.width : device.height\n const metrics = performanceData.get(device.name)\n const isRunningTest = runningTests.has(device.name)\n\n // Calculate chrome heights\n let chromeHeight = 0\n if (realDeviceBehavior) {\n if (device.hasNotch) chromeHeight += 32\n if (device.statusBar) {\n chromeHeight += device.os === 'android' ? 24 : 44\n }\n if (device.browserUI) {\n chromeHeight += device.category === 'phone' ? 0 : 72\n }\n }\n\n const viewportHeight = displayHeight - chromeHeight\n\n return (\n <div className=\"device-preview\">\n <div className=\"device-header\">\n <div>\n <div className=\"device-title\">\n {device.icon}\n {device.name}\n </div>\n <div className=\"device-info\">\n <span>\n {displayWidth} × {displayHeight}\n </span>\n <span>{Math.round(scaleFactor * 100)}%</span>\n {metrics && (\n <span\n style={{ color: getPerformanceClass(metrics.performance).color, fontWeight: 500 }}\n >\n {metrics.performance}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"device-actions\">\n <button\n onClick={() => onRunTest(device.name)}\n className=\"test-btn\"\n disabled={isRunningTest}\n title=\"Run Comprehensive Test\"\n >\n <Play className=\"w-2 h-2\" />\n </button>\n <button\n onClick={() => onToggleRotation(device.name)}\n className={`action-btn ${isRotated ? 'active' : ''}`}\n title=\"Rotate\"\n >\n <RotateCcw className=\"w-3 h-3\" />\n </button>\n <button onClick={onFullscreen} className=\"action-btn\" title=\"Fullscreen\">\n <Maximize2 className=\"w-3 h-3\" />\n </button>\n <button onClick={onRemove} className=\"action-btn remove-btn\" title=\"Remove device\">\n <X className=\"w-3 h-3\" />\n </button>\n </div>\n </div>\n\n <div className=\"device-body\">\n <div\n className=\"device-chrome\"\n style={{\n width: displayWidth * scaleFactor,\n height: displayHeight * scaleFactor,\n }}\n >\n {/* Enhanced Real Device UI */}\n {realDeviceBehavior && (\n <>\n {/* iOS Notch */}\n {device.hasNotch && (\n <div\n className=\"ios-notch\"\n style={{ transform: `translateX(-50%) scale(${scaleFactor})` }}\n />\n )}\n\n {/* Status Bars */}\n {device.statusBar && (\n <div\n className={device.os === 'android' ? 'android-status-bar' : 'ios-status-bar'}\n style={{\n height: (device.os === 'android' ? 24 : 44) * scaleFactor,\n fontSize: (device.os === 'android' ? 12 : 15) * scaleFactor,\n padding: `0 ${(device.os === 'android' ? 16 : 20) * scaleFactor}px`,\n }}\n >\n {device.os === 'android' ? (\n <>\n <div className=\"android-status-left\">\n <span>12:34</span>\n </div>\n <div className=\"android-status-right\">\n <Signal style={{ width: 10 * scaleFactor, height: 10 * scaleFactor }} />\n <Wifi style={{ width: 10 * scaleFactor, height: 10 * scaleFactor }} />\n <Battery style={{ width: 10 * scaleFactor, height: 10 * scaleFactor }} />\n <span>100%</span>\n </div>\n </>\n ) : (\n <>\n <div className=\"ios-status-left\">\n <Signal style={{ width: 12 * scaleFactor, height: 12 * scaleFactor }} />\n <Wifi style={{ width: 12 * scaleFactor, height: 12 * scaleFactor }} />\n </div>\n <span>9:41</span>\n <div className=\"ios-status-right\">\n <span>100%</span>\n <Battery style={{ width: 14 * scaleFactor, height: 14 * scaleFactor }} />\n </div>\n </>\n )}\n </div>\n )}\n\n {/* Desktop/Laptop Browser Chrome */}\n {device.browserUI &&\n (device.category === 'laptop' ||\n device.category === 'desktop' ||\n device.category === 'special') && (\n <div\n className={device.os === 'macos' ? 'macos-chrome' : 'windows-chrome'}\n style={{ height: 72 * scaleFactor }}\n >\n <div\n className=\"titlebar\"\n style={{ height: 32 * scaleFactor, padding: `0 ${12 * scaleFactor}px` }}\n >\n <div className=\"browser-controls\">\n <div\n className=\"browser-btn btn-close\"\n style={{\n width: 12 * scaleFactor,\n height: 12 * scaleFactor,\n }}\n />\n <div\n className=\"browser-btn btn-minimize\"\n style={{\n width: 12 * scaleFactor,\n height: 12 * scaleFactor,\n }}\n />\n