11'use client'
22
3- import { useState } from 'react'
4- import { AnimatePresence , domAnimation , LazyMotion , m } from 'framer-motion'
3+ import { useId , useState } from 'react'
4+ import { domAnimation , LazyMotion , m } from 'framer-motion'
55import { ChevronDown } from '@/components/emcn'
66import { cn } from '@/lib/core/utils/cn'
77
@@ -14,7 +14,13 @@ interface LandingFAQProps {
1414 faqs : LandingFAQItem [ ]
1515}
1616
17+ /**
18+ * Accordion FAQ for landing pages. Answers stay mounted (collapsed via
19+ * animated height) so non-JS crawlers see the full Q&A text and FAQPage
20+ * JSON-LD always matches visible content.
21+ */
1722export function LandingFAQ ( { faqs } : LandingFAQProps ) {
23+ const baseId = useId ( )
1824 const [ openIndex , setOpenIndex ] = useState < number | null > ( 0 )
1925 const [ hoveredIndex , setHoveredIndex ] = useState < number | null > ( null )
2026
@@ -23,8 +29,8 @@ export function LandingFAQ({ faqs }: LandingFAQProps) {
2329 < div >
2430 { faqs . map ( ( { question, answer } , index ) => {
2531 const isOpen = openIndex === index
26- const isHovered = hoveredIndex === index
2732 const showDivider = index > 0 && hoveredIndex !== index && hoveredIndex !== index - 1
33+ const panelId = `${ baseId } -faq-panel-${ index } `
2834
2935 return (
3036 < div key = { question } >
@@ -34,50 +40,50 @@ export function LandingFAQ({ faqs }: LandingFAQProps) {
3440 index === 0 || ! showDivider ? 'invisible' : 'visible'
3541 ) }
3642 />
37- < button
38- type = 'button'
39- onClick = { ( ) => setOpenIndex ( isOpen ? null : index ) }
40- onMouseEnter = { ( ) => setHoveredIndex ( index ) }
41- onMouseLeave = { ( ) => setHoveredIndex ( null ) }
42- className = '-mx-6 flex w-[calc(100%+3rem)] items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-[var(--landing-bg-elevated)]'
43- aria-expanded = { isOpen }
44- >
45- < span
46- className = { cn (
47- 'text-[15px] leading-snug tracking-[-0.02em] transition-colors' ,
48- isOpen
49- ? 'text-[var(--landing-text)]'
50- : 'text-[var(--landing-text-body)] hover:text-[var(--landing-text)]'
51- ) }
43+ < h3 >
44+ < button
45+ type = 'button'
46+ onClick = { ( ) => setOpenIndex ( isOpen ? null : index ) }
47+ onMouseEnter = { ( ) => setHoveredIndex ( index ) }
48+ onMouseLeave = { ( ) => setHoveredIndex ( null ) }
49+ className = '-mx-6 flex w-[calc(100%+3rem)] items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-[var(--landing-bg-elevated)]'
50+ aria-expanded = { isOpen }
51+ aria-controls = { panelId }
5252 >
53- { question }
54- </ span >
55- < ChevronDown
56- className = { cn (
57- 'h-3 w-3 shrink-0 text-[var(--landing-text-subtle)] transition-transform duration-200' ,
58- isOpen ? 'rotate-180' : 'rotate-0'
59- ) }
60- aria-hidden = 'true'
61- />
62- </ button >
63-
64- < AnimatePresence initial = { false } >
65- { isOpen && (
66- < m . div
67- initial = { { height : 0 , opacity : 0 } }
68- animate = { { height : 'auto' , opacity : 1 } }
69- exit = { { height : 0 , opacity : 0 } }
70- transition = { { duration : 0.25 , ease : [ 0.4 , 0 , 0.2 , 1 ] } }
71- className = 'overflow-hidden'
53+ < span
54+ className = { cn (
55+ 'text-[15px] leading-snug tracking-[-0.02em] transition-colors' ,
56+ isOpen
57+ ? 'text-[var(--landing-text)]'
58+ : 'text-[var(--landing-text-body)] hover:text-[var(--landing-text)]'
59+ ) }
7260 >
73- < div className = 'pt-2 pb-4' >
74- < p className = 'text-[14px] text-[var(--landing-text-body)] leading-[1.75]' >
75- { answer }
76- </ p >
77- </ div >
78- </ m . div >
79- ) }
80- </ AnimatePresence >
61+ { question }
62+ </ span >
63+ < ChevronDown
64+ className = { cn (
65+ 'h-3 w-3 shrink-0 text-[var(--landing-text-subtle)] transition-transform duration-200' ,
66+ isOpen ? 'rotate-180' : 'rotate-0'
67+ ) }
68+ aria-hidden = 'true'
69+ />
70+ </ button >
71+ </ h3 >
72+
73+ < m . div
74+ id = { panelId }
75+ initial = { false }
76+ animate = { { height : isOpen ? 'auto' : 0 , opacity : isOpen ? 1 : 0 } }
77+ transition = { { duration : 0.25 , ease : [ 0.4 , 0 , 0.2 , 1 ] } }
78+ className = 'overflow-hidden'
79+ aria-hidden = { ! isOpen }
80+ >
81+ < div className = 'pt-2 pb-4' >
82+ < p className = 'text-[14px] text-[var(--landing-text-body)] leading-[1.75]' >
83+ { answer }
84+ </ p >
85+ </ div >
86+ </ m . div >
8187 </ div >
8288 )
8389 } ) }
0 commit comments