Skip to content

MODULE

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/m04-page-hero/ (all files in this directory)
  • packages/modules/src/registry.ts
  • packages/ui/src/ (scan for available UI components)

Now create a new module with the following details:

Module ID: M{MODULE_NUMBER} Module Name: {MODULE_NAME} Figma Desktop: {FIGMA_DESKTOP_URL} Figma Mobile: {FIGMA_MOBILE_URL} Component Type: {STATIC or INTERACTIVE}

Description: {BRIEF_DESCRIPTION_OF_MODULE_PURPOSE}

Props: {LIST_EACH_PROP_WITH_TYPE_AND_DESCRIPTION}

UI Components to Use: {LIST_UI_COMPONENTS_FROM_PACKAGES_UI — e.g., Heading, BodyText, ResponsiveImage, LinkButton}

Layout: {DESCRIBE_LAYOUT — e.g., 2-column on desktop, stacked on mobile, max-width container}

--- INTERACTIVE ONLY (remove this section if Static) ---

Interaction Specification:

  • Triggers: {WHAT_TRIGGERS_INTERACTION — e.g., click, swipe, scroll, hover, autoplay timer}
  • State Changes: {WHAT_CHANGES — e.g., active slide index, open/closed panel, selected tab}
  • Animations: {TRANSITIONS_AND_ANIMATIONS — e.g., slide left 300ms ease, fade 200ms, height auto}
  • Keyboard Navigation: {KEY_BINDINGS — e.g., ArrowLeft/ArrowRight for slides, Enter/Space to toggle, Escape to close}
  • Touch Gestures: {MOBILE_GESTURES — e.g., swipe left/right, pinch to zoom}
  • Autoplay: {IF_APPLICABLE — interval, pause on hover, pause on focus, pause control button}

--- END INTERACTIVE ONLY ---

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

  1. Define types in .types.ts first, aligned with expected content structure
  2. Scaffold test file — create .test.tsx with describe blocks for mapper + rendering → Static: write full mapper tests + rendering tests NOW (TDD — before implementation) → Interactive: write mapper tests + rendering tests now; play() tests after Phase B
  3. Write Storybook stories with realistic mock data BEFORE implementing the component
  4. Create these story variants: Default, MinimalContent, EmptyState, LongContent, AllOptions, FigmaFidelity → FigmaFidelity must mirror Figma content exactly (Lorem Ipsum OK if Figma uses it) → All other variants use realistic hotel context data — never Lorem Ipsum
  5. Stories must include tags: ['autodocs', 'vitest'], full EN autodocs documentation with Figma desktop + mobile links, User Story link, and feature bullet list
  1. Implement the component — .tsx + .scss (BEM + CSS custom properties) → Static: Server Component (no ‘use client’) → Interactive: Server wrapper .tsx + .client.tsx (‘use client’ ONLY in .client.tsx)
  2. Write mapper — .mapper.ts transforming Umbraco JSON → Omit<Props, ‘siteKey’ | ‘locale’> → Map active from element.properties.active — default true when undefined
  3. (Interactive only) Write play() interaction tests in stories
  4. Run unit tests — pnpm --filter modules test — all must pass before continuing
  5. Register in packages/modules/src/registry.ts with { component, mapper, moduleId }
  6. Export via index.ts with named exports
  7. Pre-gate checks — pnpm typecheck + pnpm lint — both must pass before visual testing

Phase B7 — Pixel Perfect Visual Testing (HARD GATE)

Section titled “Phase B7 — Pixel Perfect Visual Testing (HARD GATE)”

This phase is MANDATORY. Phase C cannot begin until both thresholds are met (or the fallback limit is reached).

Thresholds (both viewports must pass):

  • Desktop (1440×900): diff ≤ 5%
  • Mobile (375×812): diff ≤ 10%

Fallback: If after 10 fix iterations or 30 minutes thresholds are still not met, stop and notify the user explicitly with current diff values, then proceed to Phase C. Manual visual review is required before merge.

  1. Fetch Figma baselines — Use Figma MCP get_screenshot to capture desktop (1440×900) + mobile (375×812) PNGs to __figma_baselines__/{storyId}/
  2. Register Figma mapping — Add story ID → Figma node in .storybook/visual-testing/figma-mapping.ts
  3. Run visual tests against FigmaFidelity story — pnpm --filter storybook visual:test -- --story {moduleId}-figma-fidelity
  4. Iterate — View diff in Pixel Perfect panel, fix CSS until both viewport thresholds are met (loop back to Phase B if needed)
  5. Promote regression baselines — pnpm --filter storybook visual:update
  6. Commit baselines — __figma_baselines__/ and __visual_snapshots__/ are git-tracked

Phase C — Umbraco (hard gate: Phase B7 must pass first)

Section titled “Phase C — Umbraco (hard gate: Phase B7 must pass first)”
  1. Define Element Type schema — use .claude/rules/backend-umbraco.md migration template
    • active Toggle is always the first property in the Content tab (default ON)
    • Add blockLabel Textstring (invariant) if module has no plain-text title field
    • Split into tabs: Content (0) → Settings (1, if 3+ config fields) → CTAs (1 or 2) → Images (2 or 3)
  2. Create SVG thumbnail — apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m{XX}-{kebab-name}.svg (400×250, wireframe, light background)
  3. Create Element Type in Umbraco via migration — set Icon (e.g., icon-banner), never leave default
  4. Configure Block List label expression — {Module Name} - {{title}} or {Module Name} - {{blockLabel}}
  5. Add as allowed block on target page Document Types (include thumbnail path in Block List config)
  6. Create test content and verify Content Delivery API output
  7. Verify active: false hides the module on the frontend (no HTML output)
  8. Validate mapper against real API response
  9. Test in at least 2 sites (default + one themed)
  10. Verify cache purge on content publish

For STATIC modules:

  1. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/index.ts
  2. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.tsx
  3. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.scss
  4. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.types.ts
  5. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.mapper.ts
  6. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.stories.tsx
  7. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.test.tsx

For INTERACTIVE modules (add .client.tsx):

  1. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/index.ts (Server Component wrapper — no ‘use client’)
  2. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.tsx (Server Component shell, if needed)
  3. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.client.tsx (‘use client’ — all state/effects here)
  4. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.scss
  5. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.types.ts
  6. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.mapper.ts
  7. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.stories.tsx
  8. packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.test.tsx

Then register the module in packages/modules/src/registry.ts.

Phase C also requires:

  • apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m{MODULE_NUMBER}-{KEBAB_NAME}.svg
  • apps/cms/Savoy.Cms/Migrations/Add{PascalName}Migration.cs
  • apps/cms/Savoy.Cms/Migrations/SavoyMigrationPlan.cs (add migration registration)
  • 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: SiteKey, locale: string, and moduleId?: string
  • Mapper signature: (data: UmbracoElement) => Omit<Props, ‘siteKey’ | ‘locale’>
  • Mapper maps active: element.properties.active !== false (default true)
  • Mapper handles missing/null fields gracefully with optional chaining and defaults
  • Root element: <section data-module="{camelCaseAlias}" data-module-id={moduleId}>
  • Semantic HTML (section, article, nav, h2-h4)
  • Every image uses imageDesktop + imageMobile with ResponsiveImage from @savoy/ui
  • Responsive: mobile-first with min-width breakpoints (sm=640, md=768, lg=1024, xl=1280, 2xl=1440)
  • Fluid layouts — no fixed pixel widths, no horizontal scroll
  • Storybook stories use realistic hotel context data (room names, restaurants, prices — never lorem ipsum)
  • Stories include tags: ['autodocs', 'vitest'], Figma link in parameters.design, and full EN documentation
  • Must render correctly for all 8 themes
  • No TypeScript errors (pnpm typecheck passes)

Interactive Module Conventions (skip if Static)

Section titled “Interactive Module Conventions (skip if Static)”
  • ‘use client’ ONLY in .client.tsx — never in index.ts
  • index.ts is a Server Component wrapper — pass only serializable data (no functions, Date, Map/Set)
  • No useState/useEffect in index.ts — only in .client.tsx
  • Keyboard navigation fully implemented (Arrow keys, Enter, Space, Escape as appropriate)
  • ARIA attributes set and updated on state change (aria-expanded, aria-selected, aria-hidden, aria-live, role)
  • Focus management — logical focus order, focus trap for modals/overlays, visible focus ring via :focus-visible
  • Autoplay (if applicable) pauses on hover and keyboard focus, with visible pause/play button
  • Touch gestures (if applicable) work on iOS Safari and Android Chrome
  • Animations respect prefers-reduced-motion media query
  • Storybook stories include interaction state variants (open/closed, active slide, etc.)
  1. Hardcoding colors, fonts, or spacing — use var(—token-name) from packages/themes/src/
  2. Forgetting imageMobile — every image needs both desktop and mobile variants
  3. Mapper crash on null — Umbraco sends null for optional fields, always use ?. and fallbacks
  4. Wrong BEM nesting — use &__element inside the block, not standalone .block__element
  5. Missing registry entry — module won’t render on any page without it
  6. Desktop-first media queries — use min-width (mobile-first), never max-width
  7. Importing from wrong package — UI components from @savoy/ui, not relative paths
  8. Lorem ipsum in stories — use realistic hotel context (FigmaFidelity is the only exception)
  9. Missing active Toggle in Element Type — always the first property in Content tab, default ON
  10. Block List label not configured — always set Module Name - {{title}} or Module Name - {{blockLabel}}
  11. Using Default story as visual test target — always use FigmaFidelity for Phase B7
  12. Wrong visual test threshold — FigmaFidelity: desktop ≤5% / mobile ≤10% (not 0.5%)
  13. Advancing to Phase C before Phase B7 passes — B7 is a hard gate
  14. Missing tags: ['vitest'] in story meta — unit tests won’t appear in Storybook “Unit Tests” panel
  15. (Interactive) Putting ‘use client’ in index.ts — only .client.tsx
  16. (Interactive) Non-serializable props crossing Server-Client boundary
  17. (Interactive) Missing keyboard navigation or ARIA updates on state change
  18. (Interactive) No prefers-reduced-motion handling on animations
  19. (Interactive) Forgetting focus trap cleanup on unmount
  20. Skipping Pixel Perfect visual tests — No module is complete without passing visual tests
  21. Not fetching Figma baselines — New modules must have Figma baselines for both desktop and mobile
  22. Not committing baselines__figma_baselines__/ and __visual_snapshots__/ must be in git
  23. Skipping Phase C — A module is NOT complete without full Umbraco integration