feat: iPad portrait responsive layout — min-[820px] breakpoints across all blocks + tablet slide-in nav
This commit is contained in:
parent
7010e6519b
commit
313cfd31fb
@ -56,7 +56,6 @@ function groupChildren(children: NavChild[]): { heading: string | null; links: N
|
||||
return result
|
||||
}
|
||||
|
||||
/* ── Priority #4: Focus trap hook for mobile menu ──────────────────── */
|
||||
function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, active: boolean) {
|
||||
useEffect(() => {
|
||||
if (!active || !containerRef.current) return
|
||||
@ -68,10 +67,8 @@ function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, active:
|
||||
const getFocusableElements = () =>
|
||||
Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
|
||||
|
||||
// Focus the first focusable element when trap activates
|
||||
const elements = getFocusableElements()
|
||||
if (elements.length > 0) {
|
||||
// Small delay to ensure the menu transition has started
|
||||
setTimeout(() => elements[0]?.focus(), 100)
|
||||
}
|
||||
|
||||
@ -102,16 +99,13 @@ function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, active:
|
||||
}, [active, containerRef])
|
||||
}
|
||||
|
||||
/* ── Priority #10: Check if a nav item matches current path ────────── */
|
||||
function isActiveSection(item: NavItem, pathname: string | null): boolean {
|
||||
if (!pathname || !item.children?.length) {
|
||||
// For top-level links without children, check direct href match
|
||||
const href = resolveHref(item)
|
||||
if (href === '/') return pathname === '/'
|
||||
return pathname?.startsWith(href) || false
|
||||
}
|
||||
|
||||
// For dropdown parents, check if any child href matches
|
||||
return item.children.some((child) => {
|
||||
const childHref = resolveHref(child)
|
||||
if (childHref === '/' || childHref === '#') return false
|
||||
@ -119,7 +113,6 @@ function isActiveSection(item: NavItem, pathname: string | null): boolean {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const MegaMenuPanel: React.FC<{
|
||||
item: NavItem
|
||||
headerRef: React.RefObject<HTMLElement | null>
|
||||
@ -155,7 +148,7 @@ const MegaMenuPanel: React.FC<{
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Panel — white in light, navy in dark */}
|
||||
{/* Panel */}
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="fixed left-0 right-0 z-40 bg-white dark:bg-fd-navy border-b border-fd-navy/10 dark:border-white/10"
|
||||
@ -222,7 +215,6 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
const megaMenuRef = useRef<HTMLDivElement | null>(null)
|
||||
const mobileMenuRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
// Priority #4: Focus trap
|
||||
useFocusTrap(mobileMenuRef, mobileOpen)
|
||||
|
||||
useEffect(() => {
|
||||
@ -275,13 +267,12 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Desktop nav ── */}
|
||||
<nav ref={navRef} className="hidden md:flex gap-6 items-center">
|
||||
{/* ── Desktop nav — shows at lg (1024px+) ── */}
|
||||
<nav ref={navRef} className="hidden lg:flex gap-6 items-center">
|
||||
{navItems.map((item, i) => {
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
const isOpen = openDropdown === item.label
|
||||
const isMega = item.megaMenu && hasChildren
|
||||
// Priority #10: Active section detection
|
||||
const isActive = isActiveSection(item, pathname)
|
||||
|
||||
return hasChildren ? (
|
||||
@ -302,7 +293,6 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
<ChevronDownIcon
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
{/* Active indicator dot */}
|
||||
{isActive && !isOpen && (
|
||||
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-fd-yellow" />
|
||||
)}
|
||||
@ -361,27 +351,42 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Mobile hamburger ── */}
|
||||
{/* ── Hamburger — shows below lg (mobile + tablet) ── */}
|
||||
<button
|
||||
className="md:hidden text-fd-navy dark:text-white p-2"
|
||||
className="lg:hidden text-fd-navy dark:text-white p-2"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label={mobileOpen ? 'Stäng meny' : 'Öppna meny'}
|
||||
>
|
||||
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
|
||||
</button>
|
||||
|
||||
{/* ── Mobile full-screen overlay (Priority #4: has focus trap via ref) ── */}
|
||||
{/* ── Tablet backdrop — blur overlay left of panel on md+ ── */}
|
||||
<div
|
||||
className={`fixed inset-0 z-40 lg:hidden transition-opacity duration-300 ${
|
||||
mobileOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
style={{ backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)', backgroundColor: 'rgba(14, 35, 56, 0.45)' }}
|
||||
onClick={closeMobile}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* ── Mobile/tablet slide-in panel ──
|
||||
Mobile (<768px): full-screen overlay
|
||||
Tablet (768-1023px): right-side panel, 420px wide, with backdrop above
|
||||
── */}
|
||||
<div
|
||||
ref={mobileMenuRef}
|
||||
className={`fixed inset-0 z-50 bg-fd-navy flex flex-col transition-transform duration-300 md:hidden ${
|
||||
mobileOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
className={`fixed inset-y-0 right-0 z-50 flex flex-col bg-fd-navy lg:hidden
|
||||
w-full md:w-[420px]
|
||||
transition-transform duration-300
|
||||
${mobileOpen ? 'translate-x-0 md:shadow-[-24px_0_60px_rgba(0,0,0,0.25)]' : 'translate-x-full'}
|
||||
`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Navigeringsmeny"
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-white/10">
|
||||
<div className="flex items-center justify-between px-6 md:px-8 py-5 border-b border-white/10">
|
||||
<Link href="/" onClick={closeMobile}>
|
||||
<span className="font-joey-heavy text-fd-yellow text-xl">Fiber Direkt</span>
|
||||
</Link>
|
||||
@ -395,7 +400,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
</div>
|
||||
|
||||
{/* Nav links */}
|
||||
<nav className="flex-1 overflow-y-auto px-6 py-6 flex flex-col">
|
||||
<nav className="flex-1 overflow-y-auto px-6 md:px-8 py-6 flex flex-col">
|
||||
{navItems.map((item, i) => {
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
const isExpanded = mobileOpenSection === item.label
|
||||
@ -404,7 +409,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
<div key={i} className="border-b border-white/10">
|
||||
<button
|
||||
onClick={() => setMobileOpenSection(isExpanded ? null : item.label ?? null)}
|
||||
className="w-full flex justify-between items-center py-4 text-white font-joey-medium text-xl hover:text-fd-yellow transition-colors"
|
||||
className="w-full flex justify-between items-center py-4 md:py-5 text-white font-joey-medium text-xl md:text-2xl hover:text-fd-yellow transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
<ChevronRightIcon
|
||||
@ -421,7 +426,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
key={j}
|
||||
href={resolveHref(child)}
|
||||
onClick={closeMobile}
|
||||
className="block pl-4 py-3 text-fd-yellow font-joey text-lg border-b border-white/5 hover:text-white transition-colors"
|
||||
className="block pl-4 py-3 md:py-3.5 text-fd-yellow font-joey text-lg md:text-xl border-b border-white/5 hover:text-white transition-colors"
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
@ -433,7 +438,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
key={i}
|
||||
href={resolveHref(item)}
|
||||
onClick={closeMobile}
|
||||
className="py-4 text-white font-joey-medium text-xl border-b border-white/10 hover:text-fd-yellow transition-colors block"
|
||||
className="py-4 md:py-5 text-white font-joey-medium text-xl md:text-2xl border-b border-white/10 hover:text-fd-yellow transition-colors block"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
@ -441,8 +446,8 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* ── Mobile menu footer: flag left, social icons right ── */}
|
||||
<div className="px-6 py-5 border-t border-white/10 flex items-center justify-between">
|
||||
{/* Footer: Swedish flag left, social icons right */}
|
||||
<div className="px-6 md:px-8 py-5 border-t border-white/10 flex items-center justify-between">
|
||||
<SwedishFlag />
|
||||
{socialLinks.length > 0 && (
|
||||
<SocialIconsRow
|
||||
|
||||
@ -50,10 +50,10 @@ const sectionBgMap: Record<string, string> = {
|
||||
}
|
||||
|
||||
const layoutGridMap: Record<string, string> = {
|
||||
'1-2': 'lg:grid-cols-[1fr_2fr]',
|
||||
'2-1': 'lg:grid-cols-[2fr_1fr]',
|
||||
'1-1-1': 'lg:grid-cols-3',
|
||||
'1-1': 'lg:grid-cols-2',
|
||||
'1-2': 'min-[820px]:grid-cols-[1fr_2fr]',
|
||||
'2-1': 'min-[820px]:grid-cols-[2fr_1fr]',
|
||||
'1-1-1': 'min-[820px]:grid-cols-3',
|
||||
'1-1': 'min-[820px]:grid-cols-2',
|
||||
}
|
||||
|
||||
const styleClassMap: Record<string, string> = {
|
||||
@ -80,7 +80,7 @@ export const FDCardGridBlockComponent: React.FC<FDCardGridBlockProps> = ({
|
||||
return (
|
||||
<section id={anchorId || undefined} className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
|
||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
|
||||
<div className={`grid grid-cols-1 md:grid-cols-2 ${gridCols} gap-4 md:gap-6`}>
|
||||
<div className={`grid grid-cols-1 ${gridCols} gap-4 md:gap-6`}>
|
||||
{cards?.map((card, index) => {
|
||||
const mode = card.displayMode || 'content'
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ export const FDCtaSideImageBlockComponent: React.FC<FDCtaSideImageBlockProps> =
|
||||
: 'text-fd-navy dark:text-white'
|
||||
|
||||
const textContent = (
|
||||
<div className={`flex flex-col flex-1 items-start gap-8 lg:gap-[41px] ${!hasImage ? 'max-w-2xl' : ''}`}>
|
||||
<div className={`flex flex-col flex-1 items-start gap-8 min-[820px]:gap-[41px] ${!hasImage ? 'max-w-2xl' : ''}`}>
|
||||
<div className="flex flex-col items-start gap-4 w-full">
|
||||
<h2 className={`w-full font-joey-heavy text-fd-h1 leading-tight ${headingClass}`}>
|
||||
{heading}
|
||||
@ -67,7 +67,7 @@ export const FDCtaSideImageBlockComponent: React.FC<FDCtaSideImageBlockProps> =
|
||||
)
|
||||
|
||||
const imageContent = hasImage ? (
|
||||
<div className={`relative w-full lg:w-[575px] lg:h-[479px] flex-shrink-0 overflow-hidden ${imageRadius}`}>
|
||||
<div className={`relative w-full min-[820px]:w-[45%] lg:w-[575px] lg:h-[479px] flex-shrink-0 overflow-hidden ${imageRadius}`}>
|
||||
<FDImage
|
||||
media={image as Media}
|
||||
size="large"
|
||||
@ -81,7 +81,7 @@ export const FDCtaSideImageBlockComponent: React.FC<FDCtaSideImageBlockProps> =
|
||||
|
||||
return (
|
||||
<section id={anchorId || undefined} className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
|
||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8 flex flex-col lg:flex-row items-center gap-10 lg:gap-16">
|
||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8 flex flex-col min-[820px]:flex-row items-center gap-10 min-[820px]:gap-16">
|
||||
{imagePosition === 'left' ? (
|
||||
<>{imageContent}{textContent}</>
|
||||
) : (
|
||||
|
||||
@ -56,7 +56,7 @@ export const FDLocationsGridBlockComponent: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 min-[820px]:grid-cols-3 gap-4 md:gap-6">
|
||||
{(cards ?? []).map((card, i) => {
|
||||
const media = card.image as Media | undefined
|
||||
const isLink = Boolean(card.link)
|
||||
|
||||
@ -64,9 +64,9 @@ const buttonVariantMap: Record<string, { variant: 'primary' | 'outline' }> = {
|
||||
}
|
||||
|
||||
const gridColsMap: Record<number, string> = {
|
||||
1: 'lg:grid-cols-1 max-w-[500px] mx-auto',
|
||||
2: 'lg:grid-cols-2',
|
||||
3: 'lg:grid-cols-3',
|
||||
1: 'min-[820px]:grid-cols-1 max-w-[500px] mx-auto',
|
||||
2: 'min-[820px]:grid-cols-2',
|
||||
3: 'min-[820px]:grid-cols-3',
|
||||
}
|
||||
|
||||
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
|
||||
@ -96,11 +96,11 @@ export const FDPricingCardBlockComponent: React.FC<FDPricingCardBlockProps> = ({
|
||||
{sectionTitle}
|
||||
</h2>
|
||||
)}
|
||||
<div className={`grid grid-cols-1 ${gridCols} gap-6 md:gap-8`}>
|
||||
<div className={`grid grid-cols-1 ${gridCols} gap-4 min-[820px]:gap-5 lg:gap-8`}>
|
||||
{cards?.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${style.bg} ${style.border} ${cardRadius} px-8 md:px-12 py-10 md:py-14 flex flex-col gap-5`}
|
||||
className={`${style.bg} ${style.border} ${cardRadius} px-6 min-[820px]:px-8 lg:px-12 py-8 min-[820px]:py-10 lg:py-14 flex flex-col gap-5`}
|
||||
>
|
||||
<h3 className={`font-joey-heavy text-fd-h2 leading-tight ${style.title}`}>
|
||||
{card.title}
|
||||
|
||||
@ -219,10 +219,10 @@ export const FDServiceCalculatorBlockComponent: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 lg:gap-8">
|
||||
<div className="grid grid-cols-1 min-[820px]:grid-cols-5 gap-6 min-[820px]:gap-8">
|
||||
|
||||
{/* ── Left: Configuration ─────────────────────────────────── */}
|
||||
<div className={`lg:col-span-3 p-7 md:p-9 ${cardClass}`}>
|
||||
<div className={`min-[820px]:col-span-3 p-7 md:p-9 ${cardClass}`}>
|
||||
|
||||
{/* Option groups */}
|
||||
{(optionGroups ?? []).map((group, gi) => (
|
||||
@ -312,7 +312,7 @@ export const FDServiceCalculatorBlockComponent: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{/* ── Right: Summary ──────────────────────────────────────── */}
|
||||
<div className={`lg:col-span-2 p-7 md:p-9 flex flex-col ${cardClass}`}>
|
||||
<div className={`min-[820px]:col-span-2 p-7 md:p-9 flex flex-col ${cardClass}`}>
|
||||
<h3 className={`font-joey-heavy text-fd-h2 mb-6 ${headingColor}`}>{summaryHeading}</h3>
|
||||
|
||||
<div className="flex-1 space-y-0">
|
||||
|
||||
@ -100,7 +100,7 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 min-[820px]:grid-cols-3 gap-4 md:gap-6">
|
||||
{activeCategory?.services?.map((service, i) => (
|
||||
<div
|
||||
key={i}
|
||||
|
||||
@ -4,8 +4,8 @@ import { FDImage } from '@/components/FDImage'
|
||||
|
||||
const columnClasses: Record<string, string> = {
|
||||
'2': 'grid-cols-1 md:grid-cols-2',
|
||||
'3': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
'4': 'grid-cols-2 lg:grid-cols-4',
|
||||
'3': 'grid-cols-1 md:grid-cols-2 min-[820px]:grid-cols-3',
|
||||
'4': 'grid-cols-2 min-[820px]:grid-cols-4',
|
||||
}
|
||||
|
||||
/* Priority #5: Responsive radius for service images */
|
||||
|
||||
@ -34,8 +34,8 @@ const cardMap: Record<string, { bg: string; name: string; role: string; bio: str
|
||||
|
||||
const colsMap: Record<string, string> = {
|
||||
'2': 'sm:grid-cols-2',
|
||||
'3': 'sm:grid-cols-2 lg:grid-cols-3',
|
||||
'4': 'sm:grid-cols-2 lg:grid-cols-4',
|
||||
'3': 'sm:grid-cols-2 min-[820px]:grid-cols-3',
|
||||
'4': 'sm:grid-cols-2 min-[820px]:grid-cols-4',
|
||||
}
|
||||
|
||||
/* Priority #5: Responsive radius for team member cards */
|
||||
|
||||
@ -121,7 +121,7 @@ export const FDTestimonialBlockComponent: React.FC<FDTestimonialBlockProps> = ({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 min-[820px]:grid-cols-3 gap-6">
|
||||
{testimonials?.map((t, i) => {
|
||||
const avatar = t.avatar as Media | undefined
|
||||
return (
|
||||
|
||||
@ -78,7 +78,7 @@ export const FDUspChecklistBlockComponent: React.FC<FDUspChecklistBlockProps> =
|
||||
|
||||
return (
|
||||
<section id={anchorId || undefined} className={`w-full py-16 md:py-20 lg:py-[99px] ${bg}`}>
|
||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8 flex flex-col lg:flex-row items-center gap-10 lg:gap-16">
|
||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8 flex flex-col min-[820px]:flex-row items-center gap-10 min-[820px]:gap-16">
|
||||
{imagePosition === 'left' ? <>{imageContent}{textContent}</> : <>{textContent}{imageContent}</>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -169,10 +169,10 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 lg:gap-8">
|
||||
<div className="grid grid-cols-1 min-[820px]:grid-cols-5 gap-6 min-[820px]:gap-8">
|
||||
|
||||
{/* Left: Config */}
|
||||
<div className={`lg:col-span-3 p-7 md:p-9 ${cardClass}`}>
|
||||
<div className={`min-[820px]:col-span-3 p-7 md:p-9 ${cardClass}`}>
|
||||
|
||||
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-3 ${sectionLabel}`}>
|
||||
Operativsystem
|
||||
@ -225,7 +225,7 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
|
||||
</div>
|
||||
|
||||
{/* Right: Summary */}
|
||||
<div className={`lg:col-span-2 p-7 md:p-9 flex flex-col ${cardClass}`}>
|
||||
<div className={`min-[820px]:col-span-2 p-7 md:p-9 flex flex-col ${cardClass}`}>
|
||||
<h3 className={`font-joey-heavy text-fd-h2 mb-6 ${headingColor}`}>Kostnadsöversikt</h3>
|
||||
|
||||
<div className="flex-1 space-y-0">
|
||||
|
||||
@ -44,7 +44,7 @@ export const FDWideCardBlockComponent: React.FC<FDWideCardBlockProps> = ({
|
||||
return (
|
||||
<section id={anchorId || undefined} className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
|
||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
|
||||
<div className={`${card.bg} ${cardRadius} overflow-hidden flex flex-col lg:flex-row`}>
|
||||
<div className={`${card.bg} ${cardRadius} overflow-hidden flex flex-col min-[820px]:flex-row`}>
|
||||
<div className="flex-1 flex flex-col justify-center gap-5 md:gap-6 px-8 md:px-14 lg:px-16 py-12 md:py-16">
|
||||
<h2 className={`font-joey-heavy text-fd-h1 leading-tight ${card.heading}`}>
|
||||
{heading}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user