M13 Faqs
Read the following context files first:
- docs/PRD/07_Modules_and_Templates.md
- docs/dev-frontend-guides/03_MODULE_DEVELOPMENT_LIFECYCLE.md
- docs/dev-frontend-guides/05_BEM_SASS_THEMING.md
- docs/dev-frontend-guides/01_FIGMA_TO_CODE_WORKFLOW.md
- docs/dev-frontend-guides/04_STORYBOOK_FIRST_DEVELOPMENT.md
- docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md
- packages/modules/src/m10-highlight/ (all files in this directory — static module reference)
- packages/modules/src/m07-slider-categories/ (all files — interactive module reference)
- packages/modules/src/registry.ts
- packages/ui/src/ (scan for available UI components)
- docs/superpowers/specs/2026-03-12-m13-faqs-design.md (full design spec — read this carefully)
Now create a new module with the following details:
Module ID: M13 Module Name: FAQs Figma Desktop: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=884-4866&m=dev Figma Mobile: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=884-5110&m=dev Figma Open state: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=884-7264&m=dev Component Type: INTERACTIVE
Description: FAQ accordion module. Displays frequently asked questions organised in one or more thematic sections. Each section has an optional heading. Items expand/collapse on click; only one item can be open at a time within each section. Multiple sections can have different items open simultaneously.
Props:
import type { HtmlHeading, CtaLinkWithElement } from '@savoy/cms-client';
interface FaqItem { question: string // plain text, rendered as <button> trigger answer: string // HTML string from RTE (trusted CMS content)}
interface FaqSection { subtitle?: HtmlHeading // optional H7 section heading (gold) items: FaqItem[]}
interface FaqsProps { title?: HtmlHeading // H5 module heading (gold) contactLabel?: string // e.g. "Got any more questions?" (purple) contactCta?: CtaLinkWithElement // "CONTACT US" filled pill button sections: FaqSection[] // min 1 section cta?: CtaLinkWithElement // link-style CTA at bottom of sections siteKey: string locale: string moduleId?: string}UI Components to Use:
CtaLinkfrom@savoy/ui— forcontactCtaandcta- No
ResponsiveImageneeded (no images in this module)
Layout:
- Desktop: full-width white section,
120pxlateral padding,80pxvertical padding. Header row flex with title left and contact area right. Sections area indented (~102pxleft padding). Accordion items stacked vertically with dividers. - Mobile:
24pxlateral padding,64pxvertical padding. Header stacks vertically and centres (title centred, contact label + full-width button below). No left indent on sections area.
--- INTERACTIVE ONLY ---
Interaction Specification:
- Triggers: Click (or Enter/Space) on accordion trigger button
- State Changes:
openIndexes: (number | null)[]— one index per section. Clicking itemiin sections: ifopenIndexes[s] === i→ set tonull(close); otherwise → set toi(open, closing previous in same section). Other sections’ state unchanged. - Animations:
grid-template-rows: 0fr → 1fron answer panel wrapper withtransition: grid-template-rows var(--transition-normal). Icon vertical bar:transform: scaleY(0)when open,scaleY(1)when closed, withtransition: var(--transition-fast). - Keyboard Navigation:
Enter/Spaceto toggle (native button behaviour).Tabto move between triggers. NoEscapeneeded (no modal/overlay). - Touch Gestures: None — tap is equivalent to click, handled natively.
- Autoplay: Not applicable.
- prefers-reduced-motion: Disable all transitions when
@media (prefers-reduced-motion: reduce).
--- END INTERACTIVE ONLY ---
Accordion Icon
Section titled “Accordion Icon”The + / — icon is built with CSS pseudo-elements on .faqs__icon:
::before— horizontal bar (always visible, colourvar(--color-primary))::after— vertical bar (scaleY(1)closed,scaleY(0)open)- No SVG or image assets required
Answer Text Styling
Section titled “Answer Text Styling”When expanded, the answer uses:
- Font:
var(--text-sm)(14px),var(--font-body), weight 400 - Colour:
var(--color-accent)(purple — same token as label/eyebrow text) - RTE prose styles needed:
p,ul,ol,li,strong,a
Accordion ARIA Pattern
Section titled “Accordion ARIA Pattern”<button className="faqs__trigger" aria-expanded={isOpen} aria-controls={`faq-${sectionIdx}-${itemIdx}`} onClick={() => toggle(sectionIdx, itemIdx)}> <span className="faqs__question">{item.question}</span> <span className="faqs__icon" aria-hidden="true" /></button><div className={`faqs__body${isOpen ? ' faqs__body--open' : ''}`} id={`faq-${sectionIdx}-${itemIdx}`} role="region"> <div className="faqs__body-inner"> <div className="faqs__answer" /* render RTE HTML here */ /> </div></div>Development Process
Section titled “Development Process”Follow a frontend-first approach, in this exact order:
Phase A — Storybook
Section titled “Phase A — Storybook”- Define types in
Faqs.types.tsfirst - Write Storybook stories with realistic mock data BEFORE implementing the component
- Create these story variants: Default, MinimalContent, EmptyState, LongContent, AllOptions, MultipleSections, WithInteraction
- Stories must include full EN autodocs documentation with Figma links and feature bullet list
- Validate visual design against Figma in all 8 themes at breakpoints 375px, 768px, 1024px, 1440px
Phase B — React/Next.js
Section titled “Phase B — React/Next.js”- Implement
Faqs.client.tsxwith'use client'anduseStatefor accordion state - Implement
Faqs.scsswith BEM + CSS custom properties - Write mapper
Faqs.mapper.tstransforming Umbraco JSON → props - Write tests
Faqs.test.tsxcovering mapper + rendering +play()interaction story - Register in
packages/modules/src/registry.ts - Export via
index.ts
Phase B.5 — Pixel Perfect Visual Testing (MANDATORY)
Section titled “Phase B.5 — Pixel Perfect Visual Testing (MANDATORY)”- Fetch Figma baselines — Use Figma MCP
get_screenshotto capture desktop (1440x900) + mobile (375x812) PNGs to__figma_baselines__/{storyId}/ - Register Figma mapping in
.storybook/visual-testing/figma-mapping.ts - Run visual tests —
pnpm --filter storybook visual:test - Iterate — fix CSS until diff < 0.5%
- Promote baselines —
pnpm --filter storybook visual:update - Commit baselines
Phase C — Umbraco (when ready for CMS integration)
Section titled “Phase C — Umbraco (when ready for CMS integration)”- Create Element Types:
faqs,faqSection,faqItem(see design spec for schema) - Add as allowed block on target page Document Types
- Create test content and verify API output
- Validate mapper against real API response
Files to Create
Section titled “Files to Create”packages/modules/src/m13-faqs/index.tspackages/modules/src/m13-faqs/Faqs.client.tsx('use client')packages/modules/src/m13-faqs/Faqs.scsspackages/modules/src/m13-faqs/Faqs.types.tspackages/modules/src/m13-faqs/Faqs.mapper.tspackages/modules/src/m13-faqs/Faqs.stories.tsxpackages/modules/src/m13-faqs/Faqs.test.tsx
Then register in packages/modules/src/registry.ts:
faqs: { component: Faqs, mapper: mapFaqs, moduleId: "M13" }Conventions
Section titled “Conventions”- BEM class names with SASS (no CSS Modules, no Tailwind)
- All colors, fonts, spacing via CSS custom properties (var(—token-name))
- SCSS uses BEM nesting with
&__element,&--modifier(max 3 levels) - Props interface includes
siteKey: stringandlocale: string - Mapper signature:
(data: UmbracoElement) => Omit<FaqsProps, 'siteKey' | 'locale'> - Mapper handles missing/null fields with optional chaining and defaults
- Root element:
<section data-module="faqs" data-module-id={moduleId}> - Storybook stories use realistic hotel FAQ context (check-in times, amenities, policies — never lorem ipsum)
- Must render correctly for all 8 themes
- No TypeScript errors (
pnpm tsc --noEmitpasses) 'use client'ONLY inFaqs.client.tsx— never inindex.ts- No
useState/useEffectinindex.ts - Answer RTE HTML rendered via trusted CMS content pattern (same as other RTE fields in the project)
Common Pitfalls to Avoid
Section titled “Common Pitfalls to Avoid”- Hardcoding colors — use
var(--color-primary)for gold,var(--color-accent)for purple - Using max-width media queries — always mobile-first with min-width
- Putting
'use client'inindex.ts— only inFaqs.client.tsx - Passing non-serializable props (functions, Date) across the server/client boundary
- Missing
aria-expandedupdate on state change - Missing
aria-controls/idpairing on trigger/panel - Skipping
prefers-reduced-motionhandling - Using lorem ipsum in stories — use realistic hotel FAQ content
- Skipping Pixel Perfect visual tests — mandatory before completion