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 ---
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
.types.tsfirst, aligned with expected content structure - Scaffold test file — create
.test.tsxwith 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 - Write Storybook stories with realistic mock data BEFORE implementing the component
- 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
- Stories must include
tags: ['autodocs', 'vitest'], full EN autodocs documentation with Figma desktop + mobile links, User Story link, and feature bullet list
Phase B — React/Next.js
Section titled “Phase B — React/Next.js”- 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) - Write mapper —
.mapper.tstransforming Umbraco JSON → Omit<Props, ‘siteKey’ | ‘locale’> → Mapactivefromelement.properties.active— defaulttruewhen undefined - (Interactive only) Write
play()interaction tests in stories - Run unit tests —
pnpm --filter modules test— all must pass before continuing - Register in
packages/modules/src/registry.tswith{ component, mapper, moduleId } - Export via
index.tswith named exports - 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.
- Fetch Figma baselines — Use Figma MCP
get_screenshotto capture desktop (1440×900) + mobile (375×812) PNGs to__figma_baselines__/{storyId}/ - Register Figma mapping — Add story ID → Figma node in
.storybook/visual-testing/figma-mapping.ts - Run visual tests against FigmaFidelity story —
pnpm --filter storybook visual:test -- --story {moduleId}-figma-fidelity - Iterate — View diff in Pixel Perfect panel, fix CSS until both viewport thresholds are met (loop back to Phase B if needed)
- Promote regression baselines —
pnpm --filter storybook visual:update - 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)”- Define Element Type schema — use
.claude/rules/backend-umbraco.mdmigration templateactiveToggle is always the first property in the Content tab (default ON)- Add
blockLabelTextstring (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)
- Create SVG thumbnail —
apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m{XX}-{kebab-name}.svg(400×250, wireframe, light background) - Create Element Type in Umbraco via migration — set
Icon(e.g.,icon-banner), never leave default - Configure Block List label expression —
{Module Name} - {{title}}or{Module Name} - {{blockLabel}} - Add as allowed block on target page Document Types (include thumbnail path in Block List config)
- Create test content and verify Content Delivery API output
- Verify
active: falsehides the module on the frontend (no HTML output) - Validate mapper against real API response
- Test in at least 2 sites (default + one themed)
- Verify cache purge on content publish
Files to Create
Section titled “Files to Create”For STATIC modules:
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/index.ts
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.tsx
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.scss
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.types.ts
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.mapper.ts
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.stories.tsx
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.test.tsx
For INTERACTIVE modules (add .client.tsx):
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/index.ts (Server Component wrapper — no ‘use client’)
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.tsx (Server Component shell, if needed)
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.client.tsx (‘use client’ — all state/effects here)
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.scss
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.types.ts
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.mapper.ts
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.stories.tsx
- 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)
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: 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.)
Common Pitfalls to Avoid
Section titled “Common Pitfalls to Avoid”- Hardcoding colors, fonts, or spacing — use var(—token-name) from packages/themes/src/
- Forgetting imageMobile — every image needs both desktop and mobile variants
- Mapper crash on null — Umbraco sends null for optional fields, always use ?. and fallbacks
- Wrong BEM nesting — use &__element inside the block, not standalone .block__element
- Missing registry entry — module won’t render on any page without it
- Desktop-first media queries — use min-width (mobile-first), never max-width
- Importing from wrong package — UI components from @savoy/ui, not relative paths
- Lorem ipsum in stories — use realistic hotel context (FigmaFidelity is the only exception)
- Missing
activeToggle in Element Type — always the first property in Content tab, default ON - Block List label not configured — always set
Module Name - {{title}}orModule Name - {{blockLabel}} - Using
Defaultstory as visual test target — always useFigmaFidelityfor Phase B7 - Wrong visual test threshold — FigmaFidelity: desktop ≤5% / mobile ≤10% (not 0.5%)
- Advancing to Phase C before Phase B7 passes — B7 is a hard gate
- Missing
tags: ['vitest']in story meta — unit tests won’t appear in Storybook “Unit Tests” panel - (Interactive) Putting ‘use client’ in index.ts — only .client.tsx
- (Interactive) Non-serializable props crossing Server-Client boundary
- (Interactive) Missing keyboard navigation or ARIA updates on state change
- (Interactive) No prefers-reduced-motion handling on animations
- (Interactive) Forgetting focus trap cleanup on unmount
- Skipping Pixel Perfect visual tests — No module is complete without passing visual tests
- Not fetching Figma baselines — New modules must have Figma baselines for both desktop and mobile
- Not committing baselines —
__figma_baselines__/and__visual_snapshots__/must be in git - Skipping Phase C — A module is NOT complete without full Umbraco integration