Skip to content

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:

  • CtaLink from @savoy/ui — for contactCta and cta
  • No ResponsiveImage needed (no images in this module)

Layout:

  • Desktop: full-width white section, 120px lateral padding, 80px vertical padding. Header row flex with title left and contact area right. Sections area indented (~102px left padding). Accordion items stacked vertically with dividers.
  • Mobile: 24px lateral padding, 64px vertical 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 item i in section s: if openIndexes[s] === i → set to null (close); otherwise → set to i (open, closing previous in same section). Other sections’ state unchanged.
  • Animations: grid-template-rows: 0fr → 1fr on answer panel wrapper with transition: grid-template-rows var(--transition-normal). Icon vertical bar: transform: scaleY(0) when open, scaleY(1) when closed, with transition: var(--transition-fast).
  • Keyboard Navigation: Enter/Space to toggle (native button behaviour). Tab to move between triggers. No Escape needed (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 ---

The + / icon is built with CSS pseudo-elements on .faqs__icon:

  • ::before — horizontal bar (always visible, colour var(--color-primary))
  • ::after — vertical bar (scaleY(1) closed, scaleY(0) open)
  • No SVG or image assets required

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
<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>

Follow a frontend-first approach, in this exact order:

  1. Define types in Faqs.types.ts first
  2. Write Storybook stories with realistic mock data BEFORE implementing the component
  3. Create these story variants: Default, MinimalContent, EmptyState, LongContent, AllOptions, MultipleSections, WithInteraction
  4. Stories must include full EN autodocs documentation with Figma links and feature bullet list
  5. Validate visual design against Figma in all 8 themes at breakpoints 375px, 768px, 1024px, 1440px
  1. Implement Faqs.client.tsx with 'use client' and useState for accordion state
  2. Implement Faqs.scss with BEM + CSS custom properties
  3. Write mapper Faqs.mapper.ts transforming Umbraco JSON → props
  4. Write tests Faqs.test.tsx covering mapper + rendering + play() interaction story
  5. Register in packages/modules/src/registry.ts
  6. Export via index.ts

Phase B.5 — Pixel Perfect Visual Testing (MANDATORY)

Section titled “Phase B.5 — Pixel Perfect Visual Testing (MANDATORY)”
  1. Fetch Figma baselines — Use Figma MCP get_screenshot to capture desktop (1440x900) + mobile (375x812) PNGs to __figma_baselines__/{storyId}/
  2. Register Figma mapping in .storybook/visual-testing/figma-mapping.ts
  3. Run visual tests — pnpm --filter storybook visual:test
  4. Iterate — fix CSS until diff < 0.5%
  5. Promote baselines — pnpm --filter storybook visual:update
  6. Commit baselines

Phase C — Umbraco (when ready for CMS integration)

Section titled “Phase C — Umbraco (when ready for CMS integration)”
  1. Create Element Types: faqs, faqSection, faqItem (see design spec for schema)
  2. Add as allowed block on target page Document Types
  3. Create test content and verify API output
  4. Validate mapper against real API response
  1. packages/modules/src/m13-faqs/index.ts
  2. packages/modules/src/m13-faqs/Faqs.client.tsx ('use client')
  3. packages/modules/src/m13-faqs/Faqs.scss
  4. packages/modules/src/m13-faqs/Faqs.types.ts
  5. packages/modules/src/m13-faqs/Faqs.mapper.ts
  6. packages/modules/src/m13-faqs/Faqs.stories.tsx
  7. packages/modules/src/m13-faqs/Faqs.test.tsx

Then register in packages/modules/src/registry.ts:

faqs: { component: Faqs, mapper: mapFaqs, moduleId: "M13" }
  • 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: string and locale: 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 --noEmit passes)
  • 'use client' ONLY in Faqs.client.tsx — never in index.ts
  • No useState/useEffect in index.ts
  • Answer RTE HTML rendered via trusted CMS content pattern (same as other RTE fields in the project)
  1. Hardcoding colors — use var(--color-primary) for gold, var(--color-accent) for purple
  2. Using max-width media queries — always mobile-first with min-width
  3. Putting 'use client' in index.ts — only in Faqs.client.tsx
  4. Passing non-serializable props (functions, Date) across the server/client boundary
  5. Missing aria-expanded update on state change
  6. Missing aria-controls/id pairing on trigger/panel
  7. Skipping prefers-reduced-motion handling
  8. Using lorem ipsum in stories — use realistic hotel FAQ content
  9. Skipping Pixel Perfect visual tests — mandatory before completion