M9 Quotes
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: M9 - Quotes Module Name: M9 - Quotes Figma Desktop: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=908-27272&m=dev Figma Mobile: {FIGMA_MOBILE_URL} Component Type: STATIC
Description:
Section titled “Description:”Props:
Section titled “Props:”UI Components to Use:
Section titled “UI Components to Use:”Layout:
Section titled “Layout:”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 - Write Storybook stories with realistic mock data BEFORE implementing the component
- Create these story variants: Default, MinimalContent, EmptyState, LongContent, AllOptions
- 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 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’> - Write tests —
.test.tsxcovering mapper logic and component rendering - Register in
packages/modules/src/registry.tswith{ component, mapper, moduleId } - Export via
index.tswith named exports
Phase C — Umbraco (when ready for CMS integration)
Section titled “Phase C — Umbraco (when ready for CMS integration)”- Define Element Type schema (use prompt template 16)
- Create Element Type in Umbraco backoffice
- 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”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)
- packages/modules/src/m{MODULE_NUMBER}-{KEBAB_NAME}/{PascalName}.client.tsx (‘use client’)
- 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.
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 and locale: string
- Mapper signature: (data: UmbracoElement) => Omit<Props, ‘siteKey’ | ‘locale’>
- Mapper handles missing/null fields gracefully with optional chaining and defaults
- Root element:
<section data-module="{camelCaseAlias}" data-module-id="M{XX}"> - Props interface includes
moduleId?: string(injected by page renderer) - 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’], Figma link in parameters.design, and full EN documentation
- Must render correctly for all 8 themes
- No TypeScript errors (pnpm tsc —noEmit 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
- (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