From 87ad82509b7b929b265a90413df3b57bed55ff95 Mon Sep 17 00:00:00 2001 From: Felipe Dias Date: Sat, 28 Jun 2025 15:29:42 -0300 Subject: [PATCH 01/10] fix: resolve translation key errors and PWA hook infinite loop - Fixed translation key paths in mobile-layout.tsx to use correct 'common.navigation.*' namespace - Fixed infinite loop in usePWAFeatures hook by properly handling CSS environment variables - Added safe fallbacks for CSS safe area insets in globals.css - Added TypeScript declarations for Navigator PWA APIs - Fixed ESLint errors in mobile components The translation errors were caused by incorrect key paths - they needed the 'common' prefix. The infinite loop was due to parsing 'env(safe-area-inset-*)' strings as integers, which returned NaN. --- messages/en.json | 12 +- messages/es.json | 12 +- messages/pt.json | 12 +- package.json | 2 + pnpm-lock.yaml | 30 ++ src/app/[locale]/(app)/mobile-demo/page.tsx | 200 +++++++++ src/app/globals.css | 219 +++++++++- .../dashboard/page-transition-wrapper.tsx | 4 +- src/components/layout/AdaptiveLoader.tsx | 253 +++++++++++ src/components/layout/LayoutDebugger.tsx | 340 +++++++++++++++ src/components/layout/PWALayout.tsx | 280 +++++++++++++ src/components/layout/README.md | 396 ++++++++++++++++++ src/components/layout/ResponsiveImage.tsx | 284 +++++++++++++ src/components/layout/ResponsiveUtils.tsx | 141 +++++++ src/components/layout/Section.tsx | 114 +++++ src/components/layout/Stack.tsx | 138 ++++++ src/components/layout/Typography.tsx | 34 +- src/components/layout/WebVitals.tsx | 215 ++++++++++ src/components/layout/index.ts | 48 ++- src/components/mobile/mobile-layout.tsx | 210 ++++++++++ src/components/mobile/pull-to-refresh.tsx | 208 +++++++++ src/components/mobile/swipe-navigation.tsx | 159 +++++++ .../components/DrawerNavigation/index.tsx | 4 +- src/hooks/use-navigation-persistence.tsx | 202 +++++++++ src/hooks/use-pwa-features.tsx | 211 ++++++++++ src/lib/theme-provider.tsx | 1 - src/types/navigator.d.ts | 14 + tailwind.config.ts | 3 +- 28 files changed, 3709 insertions(+), 37 deletions(-) create mode 100644 src/app/[locale]/(app)/mobile-demo/page.tsx create mode 100644 src/components/layout/AdaptiveLoader.tsx create mode 100644 src/components/layout/LayoutDebugger.tsx create mode 100644 src/components/layout/PWALayout.tsx create mode 100644 src/components/layout/README.md create mode 100644 src/components/layout/ResponsiveImage.tsx create mode 100644 src/components/layout/ResponsiveUtils.tsx create mode 100644 src/components/layout/Section.tsx create mode 100644 src/components/layout/Stack.tsx create mode 100644 src/components/layout/WebVitals.tsx create mode 100644 src/components/mobile/mobile-layout.tsx create mode 100644 src/components/mobile/pull-to-refresh.tsx create mode 100644 src/components/mobile/swipe-navigation.tsx create mode 100644 src/hooks/use-navigation-persistence.tsx create mode 100644 src/hooks/use-pwa-features.tsx create mode 100644 src/types/navigator.d.ts diff --git a/messages/en.json b/messages/en.json index 67d2663..6929a5c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -23,7 +23,14 @@ "feedback": "Feedback", "search": "Search", "create": "Create", - "favorites": "Favorites" + "favorites": "Favorites", + "notifications": "Notifications", + "menu": "Menu", + "main": "Main", + "account": "Account", + "support": "Support", + "help": "Help", + "logout": "Logout" }, "actions": { "save": "Save", @@ -49,6 +56,9 @@ "info": "Information" } }, + "app": { + "name": "Works" + }, "auth": { "signin": "Sign in", "signup": "Sign up", diff --git a/messages/es.json b/messages/es.json index 32805cb..9a15865 100644 --- a/messages/es.json +++ b/messages/es.json @@ -23,7 +23,14 @@ "feedback": "Comentarios", "search": "Buscar", "create": "Crear", - "favorites": "Favoritos" + "favorites": "Favoritos", + "notifications": "Notificaciones", + "menu": "Menú", + "main": "Principal", + "account": "Cuenta", + "support": "Soporte", + "help": "Ayuda", + "logout": "Cerrar sesión" }, "actions": { "save": "Guardar", @@ -49,6 +56,9 @@ "info": "Información" } }, + "app": { + "name": "Works" + }, "auth": { "signin": "Iniciar sesión", "signup": "Registrarse", diff --git a/messages/pt.json b/messages/pt.json index 06b3d9c..318fa62 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -23,7 +23,14 @@ "feedback": "Feedback", "search": "Buscar", "create": "Criar", - "favorites": "Favoritos" + "favorites": "Favoritos", + "notifications": "Notificações", + "menu": "Menu", + "main": "Principal", + "account": "Conta", + "support": "Suporte", + "help": "Ajuda", + "logout": "Sair" }, "actions": { "save": "Salvar", @@ -49,6 +56,9 @@ "info": "Informação" } }, + "app": { + "name": "Works" + }, "auth": { "signin": "Entrar", "signup": "Cadastrar", diff --git a/package.json b/package.json index 116eea8..1214ae7 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,8 @@ "@radix-ui/react-tabs": "1.1.12", "@radix-ui/react-toggle": "1.1.9", "@radix-ui/react-tooltip": "1.2.7", + "@tailwindcss/container-queries": "^0.1.1", + "@use-gesture/react": "^10.3.1", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce8fdd3..0091cad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,12 @@ importers: '@radix-ui/react-tooltip': specifier: 1.2.7 version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tailwindcss/container-queries': + specifier: ^0.1.1 + version: 0.1.1(tailwindcss@4.1.11) + '@use-gesture/react': + specifier: ^10.3.1 + version: 10.3.1(react@19.1.0) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -2895,6 +2901,11 @@ packages: zod: optional: true + '@tailwindcss/container-queries@0.1.1': + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} + peerDependencies: + tailwindcss: '>=3.2.0' + '@tailwindcss/node@4.1.11': resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} @@ -3263,6 +3274,14 @@ packages: cpu: [x64] os: [win32] + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -11516,6 +11535,10 @@ snapshots: typescript: 5.8.3 zod: 3.25.67 + '@tailwindcss/container-queries@0.1.1(tailwindcss@4.1.11)': + dependencies: + tailwindcss: 4.1.11 + '@tailwindcss/node@4.1.11': dependencies: '@ampproject/remapping': 2.3.0 @@ -11869,6 +11892,13 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.9.2': optional: true + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.1.0)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.1.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 diff --git a/src/app/[locale]/(app)/mobile-demo/page.tsx b/src/app/[locale]/(app)/mobile-demo/page.tsx new file mode 100644 index 0000000..4fb3c48 --- /dev/null +++ b/src/app/[locale]/(app)/mobile-demo/page.tsx @@ -0,0 +1,200 @@ +'use client' + +import { + Activity, + CreditCard, + DollarSign, + Download, + Users, + TrendingUp, + ShoppingCart, + Package, +} from 'lucide-react' +import { useTranslations } from 'next-intl' + +import { MobileLayout } from '@/components/mobile/mobile-layout' +import { SwipeNavigation } from '@/components/mobile/swipe-navigation' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + +export default function MobileDemoPage() { + const t = useTranslations() + + // Sample data for demo + const stats = [ + { + title: 'Total Revenue', + value: '$45,231.89', + description: '+20.1% from last month', + icon: DollarSign, + trend: 'up', + }, + { + title: 'Subscriptions', + value: '+2350', + description: '+180.1% from last month', + icon: Users, + trend: 'up', + }, + { + title: 'Sales', + value: '+12,234', + description: '+19% from last month', + icon: CreditCard, + trend: 'up', + }, + { + title: 'Active Now', + value: '+573', + description: '+201 since last hour', + icon: Activity, + trend: 'up', + }, + ] + + const recentSales = [ + { + name: 'Olivia Martin', + email: 'olivia.martin@email.com', + amount: '+$1,999.00', + avatar: '/avatars/01.png', + }, + { + name: 'Jackson Lee', + email: 'jackson.lee@email.com', + amount: '+$39.00', + avatar: '/avatars/02.png', + }, + { + name: 'Isabella Nguyen', + email: 'isabella.nguyen@email.com', + amount: '+$299.00', + avatar: '/avatars/03.png', + }, + { + name: 'William Kim', + email: 'will@email.com', + amount: '+$99.00', + avatar: '/avatars/04.png', + }, + { + name: 'Sofia Davis', + email: 'sofia.davis@email.com', + amount: '+$39.00', + avatar: '/avatars/05.png', + }, + ] + + const handleRefresh = async () => { + // Simulate data refresh + await new Promise(resolve => setTimeout(resolve, 2000)) + } + + return ( + +
+ {/* Welcome Section */} +
+

{t('dashboard.welcome')}

+

Here's what's happening with your business today.

+
+ + {/* Stats Grid - Swipeable on mobile */} +
+ + {stats.map((stat, index) => ( +
+ + + {stat.title} + + + +
{stat.value}
+

{stat.description}

+
+
+
+ ))} +
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + + + +
+
+ + {/* Recent Sales */} + + + Recent Sales + You made 265 sales this month. + + +
+ {recentSales.map((sale, index) => ( +
+ + + + {sale.name + .split(' ') + .map(n => n[0]) + .join('')} + + +
+

{sale.name}

+

{sale.email}

+
+
{sale.amount}
+
+ ))} +
+
+
+ + {/* Notifications Example */} +
+

Recent Activity

+
+
+ +
+

New subscription

+

2 minutes ago

+
+
+
+ +
+

Payment received

+

1 hour ago

+
+
+
+
+
+
+ ) +} diff --git a/src/app/globals.css b/src/app/globals.css index 66cbbed..24ff958 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -100,10 +100,16 @@ /* Mobile-specific variables */ --touch-target-min: 2.75rem; /* 44px */ --touch-target-comfortable: 3rem; /* 48px */ - --safe-area-inset-top: env(safe-area-inset-top); - --safe-area-inset-right: env(safe-area-inset-right); - --safe-area-inset-bottom: env(safe-area-inset-bottom); - --safe-area-inset-left: env(safe-area-inset-left); + --safe-area-inset-top: env(safe-area-inset-top, 0px); + --safe-area-inset-right: env(safe-area-inset-right, 0px); + --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); + --safe-area-inset-left: env(safe-area-inset-left, 0px); + + /* Shorthand variables for safe area insets */ + --sat: var(--safe-area-inset-top); + --sar: var(--safe-area-inset-right); + --sab: var(--safe-area-inset-bottom); + --sal: var(--safe-area-inset-left); } .dark { @@ -390,17 +396,220 @@ opacity: 1; } } + + /* Viewport-aware spacing utilities */ + .p-fluid { + padding: clamp(1rem, 2vw + 0.5rem, 2rem); + } + + .px-fluid { + padding-left: clamp(1rem, 3vw + 0.5rem, 3rem); + padding-right: clamp(1rem, 3vw + 0.5rem, 3rem); + } + + .py-fluid { + padding-top: clamp(1rem, 2vw + 0.5rem, 2rem); + padding-bottom: clamp(1rem, 2vw + 0.5rem, 2rem); + } + + .m-fluid { + margin: clamp(1rem, 2vw + 0.5rem, 2rem); + } + + .mx-fluid { + margin-left: clamp(1rem, 3vw + 0.5rem, 3rem); + margin-right: clamp(1rem, 3vw + 0.5rem, 3rem); + } + + .my-fluid { + margin-top: clamp(1rem, 2vw + 0.5rem, 2rem); + margin-bottom: clamp(1rem, 2vw + 0.5rem, 2rem); + } + + .gap-fluid { + gap: clamp(0.5rem, 1.5vw + 0.25rem, 1.5rem); + } + + /* Container queries utility */ + @container { + .container-queries { + container-type: inline-size; + } + } + + /* Safe area container for PWA */ + .safe-area-container { + padding-top: env(safe-area-inset-top); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + padding-bottom: env(safe-area-inset-bottom); + } + + /* Responsive touch targets */ + .touch-responsive { + min-height: clamp(2.75rem, 5vw, 3.5rem); + min-width: clamp(2.75rem, 5vw, 3.5rem); + } + + /* Viewport units with fallback */ + .vw-100 { + width: 100vw; + width: 100dvw; + } + + .vh-100 { + height: 100vh; + height: 100dvh; + } + + /* Dynamic padding based on viewport */ + .px-viewport { + padding-left: max(1rem, env(safe-area-inset-left)); + padding-right: max(1rem, env(safe-area-inset-right)); + } + + .py-viewport { + padding-top: max(1rem, env(safe-area-inset-top)); + padding-bottom: max(1rem, env(safe-area-inset-bottom)); + } } /* Print styles */ @media print { - .no-print { + /* Hide non-essential elements */ + .no-print, + nav, + aside, + footer, + .btn-touch, + .glass, + [role="navigation"], + [role="banner"]:not(.print-header), + [aria-label*="menu"], + [aria-label*="navigation"] { display: none !important; } + /* Reset colors for better print contrast */ * { + color-adjust: exact; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + /* Base text and background */ + body { color: black !important; background: white !important; + font-size: 12pt; + line-height: 1.5; + } + + /* Headings */ + h1, h2, h3, h4, h5, h6 { + color: black !important; + page-break-after: avoid; + page-break-inside: avoid; + } + + h1 { font-size: 20pt; } + h2 { font-size: 18pt; } + h3 { font-size: 16pt; } + h4 { font-size: 14pt; } + h5, h6 { font-size: 12pt; } + + /* Links */ + a { + color: black !important; + text-decoration: underline; + } + + /* Show link URLs */ + a[href^="http"]:after { + content: " (" attr(href) ")"; + font-size: 0.8em; + color: #666; + } + + /* Images */ + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + /* Tables */ + table { + border-collapse: collapse !important; + width: 100%; + } + + table, th, td { + border: 1px solid #ddd !important; + } + + th, td { + padding: 8px !important; + } + + /* Page breaks */ + .page-break { + page-break-after: always; + } + + .avoid-break { + page-break-inside: avoid; + } + + /* Margins for print */ + @page { + margin: 2cm; + size: A4; + } + + /* Container adjustments */ + .container, + main, + article { + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + /* Grid and flex adjustments */ + .grid { + display: block !important; + } + + .grid > * { + display: block !important; + width: 100% !important; + margin-bottom: 1em; + } + + /* Remove shadows and effects */ + * { + box-shadow: none !important; + text-shadow: none !important; + } + + /* Ensure content is visible */ + .print-visible { + display: block !important; + visibility: visible !important; + } + + /* Typography adjustments */ + p, li { + orphans: 3; + widows: 3; + } + + /* Code blocks */ + pre, code { + border: 1px solid #ddd; + page-break-inside: avoid; + font-family: monospace; + font-size: 11pt; } } diff --git a/src/components/dashboard/page-transition-wrapper.tsx b/src/components/dashboard/page-transition-wrapper.tsx index 0a56051..8f9b8fa 100644 --- a/src/components/dashboard/page-transition-wrapper.tsx +++ b/src/components/dashboard/page-transition-wrapper.tsx @@ -20,8 +20,8 @@ const pageVariants = { } const pageTransition = { - type: 'tween', - ease: 'easeInOut', + type: 'tween' as const, + ease: 'easeInOut' as const, duration: 0.3, } diff --git a/src/components/layout/AdaptiveLoader.tsx b/src/components/layout/AdaptiveLoader.tsx new file mode 100644 index 0000000..9dded54 --- /dev/null +++ b/src/components/layout/AdaptiveLoader.tsx @@ -0,0 +1,253 @@ +'use client' + +import dynamic from 'next/dynamic' +import { forwardRef, useEffect, useRef, useState } from 'react' +import type React from 'react' + +import { useDeviceCapabilities } from './ResponsiveUtils' + +/** + * Adaptive component loader that loads components based on device capabilities + */ +interface AdaptiveLoaderProps { + children?: React.ReactNode + fallback?: React.ReactNode + loadingComponent?: React.ReactNode + errorComponent?: React.ReactNode + loadOn?: 'immediate' | 'interaction' | 'idle' | 'visible' + threshold?: number + rootMargin?: string + loadCondition?: () => boolean + className?: string +} + +export const AdaptiveLoader = forwardRef( + ( + { + children, + fallback, + loadingComponent =
, + errorComponent, + loadOn = 'visible', + threshold = 0.1, + rootMargin = '50px', + loadCondition, + className, + }, + ref + ) => { + const [shouldLoad, setShouldLoad] = useState(loadOn === 'immediate') + const elementRef = useRef(null) + + const { networkSpeed, deviceMemory } = useDeviceCapabilities() + + // Check if component should load based on conditions + useEffect(() => { + if (loadCondition && !loadCondition()) { + return undefined + } + + switch (loadOn) { + case 'immediate': + setShouldLoad(true) + break + + case 'idle': + if ('requestIdleCallback' in window) { + const id = requestIdleCallback(() => setShouldLoad(true)) + return () => cancelIdleCallback(id) + } else { + // Fallback for browsers without requestIdleCallback + const timeout = setTimeout(() => setShouldLoad(true), 1) + return () => clearTimeout(timeout) + } + break + + case 'interaction': + const handleInteraction = () => { + setShouldLoad(true) + cleanup() + } + + const events = ['click', 'touchstart', 'mouseenter', 'focus'] + const cleanup = () => { + events.forEach(event => { + document.removeEventListener(event, handleInteraction) + }) + } + + events.forEach(event => { + document.addEventListener(event, handleInteraction, { once: true }) + }) + + return cleanup + + case 'visible': + if (!elementRef.current) return + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + setShouldLoad(true) + observer.disconnect() + } + }, + { threshold, rootMargin } + ) + + observer.observe(elementRef.current) + return () => observer.disconnect() + + default: + break + } + + return undefined + }, [loadOn, loadCondition, threshold, rootMargin]) + + // Show fallback for low-end devices or slow networks + if (networkSpeed === 'slow' || deviceMemory < 4) { + return fallback ? <>{fallback} : null + } + + if (!shouldLoad) { + return ( +
+ {loadingComponent} +
+ ) + } + + return ( +
+ {children} +
+ ) + } +) + +AdaptiveLoader.displayName = 'AdaptiveLoader' + +/** + * Component for dynamically importing heavy components + */ +interface DynamicComponentProps

{ + loader: () => Promise<{ default: React.ComponentType

}> + props?: P + fallback?: React.ReactNode + loadOn?: 'immediate' | 'interaction' | 'idle' | 'visible' + ssr?: boolean +} + +export function DynamicComponent

= {}>({ + loader, + props = {} as P, + fallback =

, + loadOn = 'visible', + ssr = false, +}: DynamicComponentProps

) { + const Component = dynamic(loader, { + loading: () => <>{fallback}, + ssr, + }) + + return ( + + + + ) +} + +/** + * Progressive enhancement wrapper + */ +interface ProgressiveEnhancementProps { + basic: React.ReactNode + enhanced: React.ReactNode + enhanceOn?: 'immediate' | 'interaction' | 'idle' + className?: string +} + +export const ProgressiveEnhancement: React.FC = ({ + basic, + enhanced, + enhanceOn = 'idle', + className, +}) => { + const [isEnhanced, setIsEnhanced] = useState(false) + const { isTouch, networkSpeed } = useDeviceCapabilities() + + useEffect(() => { + // Don't enhance on slow networks or low-end devices + if (networkSpeed === 'slow') { + return undefined + } + + switch (enhanceOn) { + case 'immediate': + setIsEnhanced(true) + break + + case 'idle': + if ('requestIdleCallback' in window) { + const id = requestIdleCallback(() => setIsEnhanced(true)) + return () => cancelIdleCallback(id) + } else { + const timeout = setTimeout(() => setIsEnhanced(true), 1) + return () => clearTimeout(timeout) + } + break + + case 'interaction': + const handleInteraction = () => setIsEnhanced(true) + const event = isTouch ? 'touchstart' : 'mouseenter' + + document.addEventListener(event, handleInteraction, { once: true }) + return () => document.removeEventListener(event, handleInteraction) + + default: + break + } + + return undefined + }, [enhanceOn, networkSpeed, isTouch]) + + return

{isEnhanced ? enhanced : basic}
+} + +/** + * Resource hint component for preloading/prefetching + */ +interface ResourceHintProps { + href: string + as?: 'script' | 'style' | 'image' | 'font' | 'document' + type?: string + crossOrigin?: 'anonymous' | 'use-credentials' + rel?: 'preload' | 'prefetch' | 'preconnect' | 'dns-prefetch' +} + +export const ResourceHint: React.FC = ({ + href, + as, + type, + crossOrigin, + rel = 'preload', +}) => { + useEffect(() => { + const link = document.createElement('link') + link.rel = rel + link.href = href + + if (as) link.as = as + if (type) link.type = type + if (crossOrigin) link.crossOrigin = crossOrigin + + document.head.appendChild(link) + + return () => { + document.head.removeChild(link) + } + }, [href, as, type, crossOrigin, rel]) + + return null +} diff --git a/src/components/layout/LayoutDebugger.tsx b/src/components/layout/LayoutDebugger.tsx new file mode 100644 index 0000000..f6441eb --- /dev/null +++ b/src/components/layout/LayoutDebugger.tsx @@ -0,0 +1,340 @@ +'use client' + +import { useEffect, useState } from 'react' + +import { cn } from '@/lib/utils' + +import { useViewport } from './ResponsiveUtils' + +interface LayoutDebuggerProps { + showGrid?: boolean + showBreakpoints?: boolean + showContainerQueries?: boolean + showTouchTargets?: boolean + showViewportInfo?: boolean + showPerformance?: boolean + className?: string +} + +export const LayoutDebugger: React.FC = ({ + showGrid = true, + showBreakpoints = true, + showContainerQueries = true, + showTouchTargets = true, + showViewportInfo = true, + showPerformance = true, + className, +}) => { + const [isVisible, setIsVisible] = useState(false) + const [performanceMetrics, setPerformanceMetrics] = useState({ + fps: 0, + memory: 0, + layoutShifts: 0, + }) + const viewport = useViewport() + + // Toggle debugger visibility with keyboard shortcut + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.ctrlKey && e.shiftKey && e.key === 'D') { + setIsVisible(prev => !prev) + } + } + + window.addEventListener('keydown', handleKeyPress) + return () => window.removeEventListener('keydown', handleKeyPress) + }, []) + + // Monitor performance metrics + useEffect(() => { + if (!showPerformance || !isVisible) return + + let frameCount = 0 + let lastTime = performance.now() + let rafId: number + + const measureFPS = () => { + frameCount++ + const currentTime = performance.now() + + if (currentTime >= lastTime + 1000) { + setPerformanceMetrics(prev => ({ + ...prev, + fps: Math.round((frameCount * 1000) / (currentTime - lastTime)), + })) + frameCount = 0 + lastTime = currentTime + } + + rafId = requestAnimationFrame(measureFPS) + } + + rafId = requestAnimationFrame(measureFPS) + + // Monitor memory usage + const memoryInterval = setInterval(() => { + if ('memory' in performance) { + const memory = (performance as any).memory + setPerformanceMetrics(prev => ({ + ...prev, + memory: Math.round(memory.usedJSHeapSize / 1048576), // Convert to MB + })) + } + }, 1000) + + // Monitor layout shifts + let layoutShiftCount = 0 + const observer = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + if (entry.entryType === 'layout-shift') { + layoutShiftCount++ + setPerformanceMetrics(prev => ({ + ...prev, + layoutShifts: layoutShiftCount, + })) + } + } + }) + + observer.observe({ entryTypes: ['layout-shift'] }) + + return () => { + cancelAnimationFrame(rafId) + clearInterval(memoryInterval) + observer.disconnect() + } + }, [showPerformance, isVisible]) + + if (!isVisible) { + return ( + + ) + } + + return ( + <> + {/* Grid overlay */} + {showGrid && ( +
+ )} + + {/* Touch target indicators */} + {showTouchTargets && ( +