migration wip

This commit is contained in:
2025-12-29 18:18:48 +01:00
parent 292975299d
commit f86785bfb0
182 changed files with 30131 additions and 9321 deletions

View File

@@ -0,0 +1,163 @@
import Link from 'next/link';
import { Container } from '@/components/ui/Container';
import { Navigation } from './Navigation';
interface FooterProps {
locale: string;
siteName?: string;
}
export function Footer({ locale, siteName = 'KLZ Cables' }: FooterProps) {
const currentYear = new Date().getFullYear();
// Quick links
const quickLinks = [
{ title: 'About Us', path: `/${locale}/about` },
{ title: 'Blog', path: `/${locale}/blog` },
{ title: 'Products', path: `/${locale}/products` },
{ title: 'Contact', path: `/${locale}/contact` }
];
// Product categories
const productCategories = [
{ title: 'Medium Voltage Cables', path: `/${locale}/product-category/medium-voltage` },
{ title: 'Low Voltage Cables', path: `/${locale}/product-category/low-voltage` },
{ title: 'Cable Accessories', path: `/${locale}/product-category/accessories` },
{ title: 'Special Solutions', path: `/${locale}/product-category/special` }
];
// Legal links
const legalLinks = [
{ title: 'Privacy Policy', path: `/${locale}/privacy` },
{ title: 'Terms of Service', path: `/${locale}/terms` },
{ title: 'Imprint', path: `/${locale}/imprint` }
];
return (
<footer className="bg-gray-900 text-gray-300 border-t border-gray-800">
<Container maxWidth="6xl" padding="lg">
{/* Main Footer Content */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
{/* Company Info */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">KLZ</span>
</div>
<span className="font-bold text-white text-lg">{siteName}</span>
</div>
<p className="text-sm leading-relaxed text-gray-400">
Professional cable solutions for industrial applications.
Quality, reliability, and innovation since 1990.
</p>
<div className="flex gap-3">
{/* Social Media Links */}
<a href="#" className="text-gray-400 hover:text-white transition-colors" aria-label="LinkedIn">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors" aria-label="Twitter">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors" aria-label="Facebook">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"/>
</svg>
</a>
</div>
</div>
{/* Quick Links */}
<div>
<h3 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">
Quick Links
</h3>
<ul className="space-y-2">
{quickLinks.map((link) => (
<li key={link.path}>
<Link
href={link.path}
className="text-sm hover:text-white transition-colors"
>
{link.title}
</Link>
</li>
))}
</ul>
</div>
{/* Product Categories */}
<div>
<h3 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">
Products
</h3>
<ul className="space-y-2">
{productCategories.map((link) => (
<li key={link.path}>
<Link
href={link.path}
className="text-sm hover:text-white transition-colors"
>
{link.title}
</Link>
</li>
))}
</ul>
</div>
{/* Contact Info */}
<div>
<h3 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">
Contact
</h3>
<ul className="space-y-3 text-sm">
<li className="flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span>info@klz-cables.com</span>
</li>
<li className="flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span>+49 (0) 123 456 789</span>
</li>
<li className="flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>
Industrial Street 123<br />
12345 Berlin, Germany
</span>
</li>
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-gray-800 pt-6 flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-sm text-gray-400">
© {currentYear} {siteName}. All rights reserved.
</div>
<div className="flex items-center gap-4 text-sm">
{legalLinks.map((link) => (
<Link
key={link.path}
href={link.path}
className="hover:text-white transition-colors"
>
{link.title}
</Link>
))}
</div>
</div>
</Container>
</footer>
);
}

View File

@@ -0,0 +1,60 @@
import Link from 'next/link';
import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button';
import { Navigation } from './Navigation';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { MobileMenu } from './MobileMenu';
interface HeaderProps {
locale: string;
siteName?: string;
logo?: string;
}
export function Header({ locale, siteName = 'KLZ Cables', logo }: HeaderProps) {
return (
<header className="sticky top-0 z-50 bg-white border-b border-gray-200 shadow-sm">
<Container maxWidth="6xl" padding="md">
<div className="flex items-center justify-between h-16">
{/* Logo and Branding */}
<div className="flex items-center gap-3">
<Link
href={`/${locale}`}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
{logo ? (
<img src={logo} alt={siteName} className="h-8 w-auto" />
) : (
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">KLZ</span>
</div>
)}
<span className="hidden sm:block font-bold text-lg text-gray-900">
{siteName}
</span>
</Link>
</div>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-6">
<Navigation locale={locale} variant="header" />
<LocaleSwitcher />
<Link href={`/${locale}/contact`}>
<Button
variant="primary"
size="sm"
>
Contact Us
</Button>
</Link>
</div>
{/* Mobile Menu */}
<div className="flex items-center gap-2">
<MobileMenu locale={locale} siteName={siteName} />
</div>
</div>
</Container>
</header>
);
}

View File

@@ -0,0 +1,78 @@
import { ReactNode } from 'react';
import Link from 'next/link';
import { Header } from './Header';
import { Footer } from './Footer';
import { Container } from '@/components/ui/Container';
interface LayoutProps {
children: ReactNode;
locale: string;
siteName?: string;
logo?: string;
showSidebar?: boolean;
breadcrumb?: Array<{ title: string; path: string }>;
}
export function Layout({
children,
locale,
siteName = 'KLZ Cables',
logo,
showSidebar = false,
breadcrumb
}: LayoutProps) {
return (
<div className="min-h-screen flex flex-col">
{/* Header */}
<Header
locale={locale}
siteName={siteName}
logo={logo}
/>
{/* Main Content Area */}
<main className="flex-1">
{/* Breadcrumb */}
{breadcrumb && breadcrumb.length > 0 && (
<div className="bg-gray-50 border-b border-gray-200">
<Container maxWidth="6xl" padding="md">
<nav className="flex items-center gap-2 text-sm text-gray-600 py-3" aria-label="Breadcrumb">
<Link
href={`/${locale}`}
className="hover:text-primary transition-colors"
>
Home
</Link>
{breadcrumb.map((item, index) => (
<div key={item.path} className="flex items-center gap-2">
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
{index === breadcrumb.length - 1 ? (
<span className="text-gray-900 font-medium">{item.title}</span>
) : (
<Link
href={item.path}
className="hover:text-primary transition-colors"
>
{item.title}
</Link>
)}
</div>
))}
</nav>
</Container>
</div>
)}
{/* Content */}
<Container maxWidth="6xl" padding="md" className="py-8 md:py-12">
{children}
</Container>
</main>
{/* Footer */}
<Footer locale={locale} siteName={siteName} />
</div>
);
}

View File

@@ -0,0 +1,213 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { getLocaleFromPath } from '@/lib/i18n';
interface MobileMenuProps {
locale: string;
siteName: string;
onClose?: () => void;
}
export function MobileMenu({ locale, siteName, onClose }: MobileMenuProps) {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const currentLocale = getLocaleFromPath(pathname);
// Main navigation menu
const mainMenu = [
{ title: 'Home', path: `/${locale}` },
{ title: 'Blog', path: `/${locale}/blog` },
{ title: 'Products', path: `/${locale}/products` },
{ title: 'Contact', path: `/${locale}/contact` }
];
// Product categories (could be dynamic from data)
const productCategories = [
{ title: 'Medium Voltage', path: `/${locale}/product-category/medium-voltage` },
{ title: 'Low Voltage', path: `/${locale}/product-category/low-voltage` },
{ title: 'Accessories', path: `/${locale}/product-category/accessories` }
];
// Close on route change
useEffect(() => {
setIsOpen(false);
if (onClose) onClose();
}, [pathname, onClose]);
const toggleMenu = () => {
setIsOpen(!isOpen);
if (!isOpen && onClose) onClose();
};
const closeMenu = () => {
setIsOpen(false);
if (onClose) onClose();
};
return (
<>
{/* Mobile Toggle Button */}
<button
onClick={toggleMenu}
className="md:hidden p-3 rounded-lg hover:bg-gray-100 active:bg-gray-200 transition-colors touch-target-sm"
aria-label="Toggle mobile menu"
aria-expanded={isOpen}
>
<svg
className="w-6 h-6 text-gray-700"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{isOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
{/* Mobile Menu Overlay */}
{isOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-50 md:hidden"
onClick={closeMenu}
aria-hidden="true"
/>
)}
{/* Mobile Menu Drawer */}
<div
className={`fixed top-0 right-0 h-full w-full max-w-md bg-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out md:hidden safe-area-p ${
isOpen ? 'translate-x-0' : 'translate-x-full'
}`}
role="dialog"
aria-modal="true"
aria-label="Mobile navigation menu"
>
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 safe-area-p">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center shadow-sm">
<span className="text-white font-bold text-sm">KLZ</span>
</div>
<span className="font-semibold text-gray-900 text-lg">{siteName}</span>
</div>
<button
onClick={closeMenu}
className="p-3 rounded-lg hover:bg-gray-100 active:bg-gray-200 transition-colors touch-target-sm"
aria-label="Close menu"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Navigation */}
<div className="flex-1 overflow-y-auto p-4">
{/* Main Navigation */}
<div className="mb-6">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
Navigation
</h3>
<nav className="space-y-1">
{mainMenu.map((item) => (
<Link
key={item.path}
href={item.path}
className="flex items-center justify-between px-4 py-3 rounded-lg text-gray-700 hover:bg-gray-100 hover:text-primary active:bg-gray-200 transition-colors touch-target-md"
onClick={closeMenu}
>
<span className="font-medium text-base">{item.title}</span>
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
))}
</nav>
</div>
{/* Product Categories */}
<div className="mb-6">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
Product Categories
</h3>
<nav className="space-y-1">
{productCategories.map((item) => (
<Link
key={item.path}
href={item.path}
className="flex items-center justify-between px-4 py-3 rounded-lg text-gray-700 hover:bg-gray-100 hover:text-primary active:bg-gray-200 transition-colors touch-target-md"
onClick={closeMenu}
>
<span className="font-medium text-base">{item.title}</span>
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
))}
</nav>
</div>
{/* Language Switcher */}
<div className="mb-6">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
Language
</h3>
<div className="px-3">
<LocaleSwitcher />
</div>
</div>
{/* Contact Information */}
<div className="mb-6">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
Contact
</h3>
<div className="space-y-2 px-4 text-sm text-gray-600">
<a
href="mailto:info@klz-cables.com"
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors touch-target-sm"
>
<svg className="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span className="font-medium">info@klz-cables.com</span>
</a>
<a
href="tel:+490123456789"
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors touch-target-sm"
>
<svg className="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span className="font-medium">+49 (0) 123 456 789</span>
</a>
</div>
</div>
</div>
{/* Footer CTA */}
<div className="p-4 border-t border-gray-200 bg-gray-50">
<Link href={`/${locale}/contact`} onClick={closeMenu} className="block w-full">
<Button
variant="primary"
size="md"
fullWidth
>
Get in Touch
</Button>
</Link>
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,59 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { getLocaleFromPath } from '@/lib/i18n';
interface NavigationProps {
locale: string;
variant?: 'header' | 'footer';
}
export function Navigation({ locale, variant = 'header' }: NavigationProps) {
const pathname = usePathname();
const currentLocale = getLocaleFromPath(pathname);
// Main navigation menu
const mainMenu = [
{ title: 'Home', path: `/${locale}` },
{ title: 'Blog', path: `/${locale}/blog` },
{ title: 'Products', path: `/${locale}/products` },
{ title: 'Contact', path: `/${locale}/contact` }
];
// Determine styles based on variant
const isHeader = variant === 'header';
const baseClasses = isHeader
? 'hidden md:flex items-center gap-1'
: 'flex flex-col gap-2';
const linkClasses = isHeader
? 'px-3 py-2 text-sm font-medium text-gray-700 hover:text-primary hover:bg-primary-light rounded-lg transition-colors relative'
: 'text-sm text-gray-600 hover:text-primary transition-colors';
const activeClasses = isHeader
? 'text-primary bg-primary-light font-semibold'
: 'text-primary font-medium';
return (
<nav className={baseClasses}>
{mainMenu.map((item) => {
const isActive = pathname === item.path ||
(item.path !== `/${locale}` && pathname.startsWith(item.path));
return (
<Link
key={item.path}
href={item.path}
className={`${linkClasses} ${isActive ? activeClasses : ''}`}
>
{item.title}
{isActive && isHeader && (
<span className="absolute bottom-0 left-3 right-3 h-0.5 bg-primary rounded-full" />
)}
</Link>
);
})}
</nav>
);
}

View File

@@ -0,0 +1,337 @@
import React, { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { getViewport } from '@/lib/responsive';
interface ResponsiveWrapperProps {
children: ReactNode;
className?: string;
// Visibility control
showOn?: ('mobile' | 'tablet' | 'desktop' | 'largeDesktop')[];
hideOn?: ('mobile' | 'tablet' | 'desktop' | 'largeDesktop')[];
// Mobile-specific behavior
stackOnMobile?: boolean;
centerOnMobile?: boolean;
// Padding control
padding?: 'none' | 'sm' | 'md' | 'lg' | 'responsive';
// Container control
container?: boolean;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
}
/**
* ResponsiveWrapper Component
* Provides comprehensive responsive behavior for any content
*/
export function ResponsiveWrapper({
children,
className = '',
showOn,
hideOn,
stackOnMobile = false,
centerOnMobile = false,
padding = 'md',
container = false,
maxWidth = 'xl',
}: ResponsiveWrapperProps) {
// Get visibility classes
const getVisibilityClasses = () => {
let classes = '';
if (showOn) {
// Hide by default, show only on specified breakpoints
classes += 'hidden ';
if (showOn.includes('mobile')) classes += 'xs:block ';
if (showOn.includes('tablet')) classes += 'md:block ';
if (showOn.includes('desktop')) classes += 'lg:block ';
if (showOn.includes('largeDesktop')) classes += 'xl:block ';
}
if (hideOn) {
// Show by default, hide on specified breakpoints
if (hideOn.includes('mobile')) classes += 'xs:hidden ';
if (hideOn.includes('tablet')) classes += 'md:hidden ';
if (hideOn.includes('desktop')) classes += 'lg:hidden ';
if (hideOn.includes('largeDesktop')) classes += 'xl:hidden ';
}
return classes;
};
// Get mobile-specific classes
const getMobileClasses = () => {
let classes = '';
if (stackOnMobile) {
classes += 'flex-col xs:flex-row ';
}
if (centerOnMobile) {
classes += 'items-center xs:items-start text-center xs:text-left ';
}
return classes;
};
// Get padding classes
const getPaddingClasses = () => {
switch (padding) {
case 'none':
return '';
case 'sm':
return 'px-3 py-2 xs:px-4 xs:py-3';
case 'md':
return 'px-4 py-3 xs:px-6 xs:py-4';
case 'lg':
return 'px-5 py-4 xs:px-8 xs:py-6';
case 'responsive':
return 'px-4 py-3 xs:px-6 xs:py-4 md:px-8 md:py-6 lg:px-10 lg:py-8';
default:
return 'px-4 py-3';
}
};
// Get container classes if needed
const getContainerClasses = () => {
if (!container) return '';
const maxWidthClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl',
full: 'max-w-full',
};
return `mx-auto ${maxWidthClasses[maxWidth]} w-full`;
};
const wrapperClasses = cn(
// Base classes
'responsive-wrapper',
// Visibility
getVisibilityClasses(),
// Mobile behavior
getMobileClasses(),
// Padding
getPaddingClasses(),
// Container
getContainerClasses(),
// Custom classes
className
);
return <div className={wrapperClasses}>{children}</div>;
}
/**
* ResponsiveGrid Wrapper
* Creates responsive grid layouts with mobile-first approach
*/
interface ResponsiveGridProps {
children: ReactNode;
className?: string;
// Column configuration
columns?: {
mobile?: number;
tablet?: number;
desktop?: number;
largeDesktop?: number;
};
gap?: 'none' | 'sm' | 'md' | 'lg' | 'responsive';
// Mobile stacking
stackMobile?: boolean;
// Alignment
alignItems?: 'start' | 'center' | 'end' | 'stretch';
justifyItems?: 'start' | 'center' | 'end' | 'stretch';
}
export function ResponsiveGrid({
children,
className = '',
columns = {},
gap = 'md',
stackMobile = false,
alignItems = 'start',
justifyItems = 'start',
}: ResponsiveGridProps) {
const getGridColumns = () => {
if (stackMobile) {
return `grid-cols-1 sm:grid-cols-2 md:grid-cols-${columns.tablet || 3} lg:grid-cols-${columns.desktop || 4}`;
}
const mobile = columns.mobile || 1;
const tablet = columns.tablet || 2;
const desktop = columns.desktop || 3;
const largeDesktop = columns.largeDesktop || 4;
return `grid-cols-${mobile} sm:grid-cols-${tablet} md:grid-cols-${desktop} lg:grid-cols-${largeDesktop}`;
};
const getGapClasses = () => {
switch (gap) {
case 'none':
return 'gap-0';
case 'sm':
return 'gap-2 sm:gap-3 md:gap-4';
case 'md':
return 'gap-3 sm:gap-4 md:gap-6 lg:gap-8';
case 'lg':
return 'gap-4 sm:gap-6 md:gap-8 lg:gap-12';
case 'responsive':
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8 xl:gap-12';
default:
return 'gap-4';
}
};
return (
<div
className={cn(
'grid',
'w-full',
getGridColumns(),
getGapClasses(),
alignItems && `items-${alignItems}`,
justifyItems && `justify-items-${justifyItems}`,
className
)}
>
{children}
</div>
);
}
/**
* ResponsiveStack Wrapper
* Creates vertical stack that becomes horizontal on larger screens
*/
interface ResponsiveStackProps {
children: ReactNode;
className?: string;
gap?: 'none' | 'sm' | 'md' | 'lg' | 'responsive';
reverseOnMobile?: boolean;
wrap?: boolean;
}
export function ResponsiveStack({
children,
className = '',
gap = 'md',
reverseOnMobile = false,
wrap = false,
}: ResponsiveStackProps) {
const getGapClasses = () => {
switch (gap) {
case 'none':
return 'gap-0';
case 'sm':
return 'gap-2 sm:gap-3 md:gap-4';
case 'md':
return 'gap-3 sm:gap-4 md:gap-6 lg:gap-8';
case 'lg':
return 'gap-4 sm:gap-6 md:gap-8 lg:gap-12';
case 'responsive':
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8';
default:
return 'gap-4';
}
};
return (
<div
className={cn(
'flex',
// Mobile-first: column, then row
'flex-col',
'xs:flex-row',
// Gap
getGapClasses(),
// Wrap
wrap ? 'flex-wrap xs:flex-nowrap' : 'flex-nowrap',
// Reverse on mobile
reverseOnMobile && 'flex-col-reverse xs:flex-row',
// Ensure proper spacing
'w-full',
className
)}
>
{children}
</div>
);
}
/**
* ResponsiveSection Wrapper
* Optimized section with responsive padding and max-width
*/
interface ResponsiveSectionProps {
children: ReactNode;
className?: string;
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'responsive';
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | 'full';
centered?: boolean;
safeArea?: boolean;
}
export function ResponsiveSection({
children,
className = '',
padding = 'responsive',
maxWidth = '6xl',
centered = true,
safeArea = false,
}: ResponsiveSectionProps) {
const getPaddingClasses = () => {
switch (padding) {
case 'none':
return '';
case 'sm':
return 'px-3 py-4 xs:px-4 xs:py-6 md:px-6 md:py-8';
case 'md':
return 'px-4 py-6 xs:px-6 xs:py-8 md:px-8 md:py-12';
case 'lg':
return 'px-5 py-8 xs:px-8 xs:py-12 md:px-12 md:py-16';
case 'xl':
return 'px-6 py-10 xs:px-10 xs:py-14 md:px-16 md:py-20';
case 'responsive':
return 'px-4 py-6 xs:px-6 xs:py-8 md:px-8 md:py-12 lg:px-12 lg:py-16 xl:px-16 xl:py-20';
default:
return 'px-4 py-6';
}
};
const getMaxWidthClasses = () => {
const maxWidthMap = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
'6xl': 'max-w-6xl',
full: 'max-w-full',
};
return maxWidthMap[maxWidth];
};
return (
<section
className={cn(
'w-full',
centered && 'mx-auto',
getMaxWidthClasses(),
getPaddingClasses(),
safeArea && 'safe-area-p',
className
)}
>
{children}
</section>
);
}
export default ResponsiveWrapper;

View File

@@ -0,0 +1,6 @@
// Layout Components
export { Header } from './Header';
export { Footer } from './Footer';
export { Layout } from './Layout';
export { MobileMenu } from './MobileMenu';
export { Navigation } from './Navigation';