85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import * as React from "react"
|
||
import { Plus } from "lucide-react"
|
||
|
||
import { cn } from "@/lib/utils"
|
||
import {
|
||
Accordion,
|
||
AccordionContent,
|
||
AccordionItem,
|
||
AccordionTrigger,
|
||
} from "@/components/ui/accordion"
|
||
|
||
type FaqItem = {
|
||
question: React.ReactNode
|
||
answer: React.ReactNode
|
||
}
|
||
|
||
/**
|
||
* Toggle disc: hairline teal plus that fills in and turns into an × once its
|
||
* item opens. Driven by the trigger's own `group`, so hovering the answer body
|
||
* below leaves the disc alone.
|
||
*/
|
||
function FaqIndicator() {
|
||
return (
|
||
<span
|
||
aria-hidden="true"
|
||
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-brand-100 bg-card text-primary shadow-pill transition-[transform,color,background-color,border-color] duration-200 group-hover:border-brand-200 group-hover:bg-accent group-data-[state=open]:rotate-45 group-data-[state=open]:border-transparent group-data-[state=open]:bg-primary group-data-[state=open]:text-primary-foreground"
|
||
>
|
||
<Plus className="size-4" strokeWidth={2.5} />
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* FAQ list: one white card per question, plus disc as the toggle, and the open
|
||
* card tinted brand-50 behind a mint hairline so the answer reads as its own
|
||
* panel. Single-open and collapsible — the block only ever moves one card's
|
||
* worth of height, and every answer can be closed again.
|
||
*
|
||
* Sized for a reading column (centred `max-w-3xl`) instead of the full 6xl
|
||
* container; override the width via `className`.
|
||
*/
|
||
function FaqAccordion({
|
||
items,
|
||
defaultOpen = 0,
|
||
className,
|
||
}: {
|
||
items: FaqItem[]
|
||
/** index of the item open on first paint; `null` starts fully collapsed */
|
||
defaultOpen?: number | null
|
||
className?: string
|
||
}) {
|
||
return (
|
||
<Accordion
|
||
type="single"
|
||
collapsible
|
||
defaultValue={defaultOpen == null ? undefined : `faq-${defaultOpen}`}
|
||
className={cn("mx-auto flex w-full max-w-3xl flex-col gap-3", className)}
|
||
>
|
||
{items.map((item, i) => (
|
||
<AccordionItem
|
||
key={i}
|
||
value={`faq-${i}`}
|
||
/* the all-round `border` supersedes the primitive's divider width,
|
||
which leaves `last:border-b-0` to strip the final card's bottom
|
||
edge — `last:border-b` puts it back. */
|
||
className="rounded-2xl border border-border/70 bg-card px-5 shadow-card transition-colors duration-200 last:border-b hover:border-brand-200 data-[state=open]:border-brand-200 data-[state=open]:bg-brand-50/50 sm:px-6"
|
||
>
|
||
<AccordionTrigger
|
||
className="group items-center gap-5 rounded-xl py-5 text-base leading-snug font-bold text-navy"
|
||
indicator={<FaqIndicator />}
|
||
>
|
||
{item.question}
|
||
</AccordionTrigger>
|
||
<AccordionContent className="pb-6 leading-relaxed text-pretty sm:pr-13">
|
||
{item.answer}
|
||
</AccordionContent>
|
||
</AccordionItem>
|
||
))}
|
||
</Accordion>
|
||
)
|
||
}
|
||
|
||
export { FaqAccordion }
|
||
export type { FaqItem }
|