Skip to content

M8 Item Cards

Read the following context files first:

  • docs/superpowers/specs/2026-03-16-m08-item-cards-design.md
  • 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/ui/src/CardRoom/ (all files — component this module consumes)
  • packages/ui/src/TabFilter/ (all files — tab filter component, if already created)
  • packages/modules/src/m07-slider-categories/ (reference for interactive module pattern)
  • packages/modules/src/registry.ts
  • packages/ui/src/ (scan for available UI components)

Now create a new module with the following details:

Module ID: M08 Module Name: Item Cards (Accommodations) Figma Desktop: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1047-7702&m=dev Figma Mobile: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1048-8850&m=dev Full Page Template: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1396-74862&m=dev Component Type: INTERACTIVE

Description: Displays a grid of hotel room cards with tab-based category filtering and “Load More” pagination. Reads room data dynamically from child roomDetailPage pages. Each room renders using the CardRoom UI component from @savoy/ui. Includes a TabFilter component for category filtering (All / Rooms / Suites) and a background variation (half-light / white).

Prerequisites:

  • TabFilter UI component must be created in packages/ui/src/TabFilter/ BEFORE implementing this module
  • CardRoom UI component already exists in packages/ui/src/CardRoom/
  • roomDetailPage document type must be enhanced with gallery, bedding, amenities, CTAs (Phase C migration)

Props:

  • active?: boolean — show/hide module (default true)
  • rooms: RoomCardData[] — room card data (all rooms, client-side filtering)
  • tabs?: TabFilterTab[] — category filter tabs (derived from roomCategory)
  • initialCount?: number — cards shown before “Load More” (default 4)
  • background?: ‘white’ | ‘half-light’ — first row background style
  • showTabs?: boolean — show/hide tab filter (default true)
  • siteKey: string, locale: string, moduleId?: string

UI Components to Use:

  • CardRoom from @savoy/ui (room card with gallery, content, CTAs)
  • TabFilter from @savoy/ui (tab filter bar — create first if not existing)
  • CtaLink from @savoy/ui (for Load More button if needed)

Layout:

  • Desktop: 2-column grid, 102px gap, max-width 1200px, 80px vertical padding
  • Mobile: single column, 64px gap, 312px width, 64px vertical padding
  • Tab bar: full-width bg-alt background, container-padded content
  • Background: “half-light” = 30% beige top strip on desktop (10% mobile), “white” = no strip

--- INTERACTIVE ---

Interaction Specification:

  • Triggers: Tab click (filter cards), Load More button click (reveal cards)
  • State Changes: activeTab (filters visible cards), visibleCount (how many cards shown)
  • Animations: Card fade-in on reveal (respect prefers-reduced-motion)
  • Keyboard Navigation: ArrowLeft/ArrowRight between tabs, Enter/Space to select tab, Tab to Load More button
  • Touch Gestures: N/A (individual CardRoom handles its own swipe)
  • Autoplay: N/A

--- END INTERACTIVE ---

This module requires BOTH a UI component (TabFilter) and a module (M08). Follow this order:

Before starting the module, create the TabFilter component in packages/ui/src/TabFilter/:

  1. TabFilter.types.ts — TabFilterTab, TabFilterProps
  2. TabFilter.test.tsx — rendering + interaction tests
  3. TabFilter.tsx — client component (‘use client’) with tab state management
  4. TabFilter.scss — BEM styles with theme tokens
  5. TabFilter.stories.tsx — Default, ManyTabs, SingleTab, FigmaFidelity
  6. index.ts — exports
  7. Update packages/ui/src/index.ts — add TabFilter exports
  1. Define types in .types.ts first — RoomCardData, ItemCardsProps extending ModuleLayoutProps
  2. Scaffold test file — create .test.tsx with describe blocks for mapper + rendering → 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 (4 cards), OneCard, TwoCards, EightCards (with Load More), WithoutTabs, WhiteBackground, 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 and feature bullet list
  1. Implement the component:
    • ItemCards.tsx — Server Component wrapper
    • ItemCards.client.tsx — Client component (‘use client’) with tab filtering + load more state
    • ItemCards.scss — BEM styles (grid, background variation, load more button)
  2. Write mapper — ItemCards.mapper.ts — maps Umbraco element + pre-fetched room children to props → Map active from element.properties.active — default true when undefined → Map rooms from context (pre-fetched roomDetailPage children) → Extract unique categories for tab filter → Map initialCount, background, showTabs from element properties
  3. Write play() interaction tests: tab click filters cards, Load More reveals cards
  4. Run unit tests — pnpm --filter modules test — all must pass before continuing
  5. Register in packages/modules/src/registry.ts with { component: ItemCards, mapper: mapItemCards, moduleId: "M08" }
  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)”
  1. Fetch Figma baselines — desktop (1440×900) node 1047:7702 + mobile (375×812) node 1048:8850
  2. Register Figma mapping in .storybook/visual-testing/figma-mapping.ts
  3. Run visual tests against FigmaFidelity story — desktop ≤5%, mobile ≤10%
  4. Iterate — fix CSS until thresholds met
  5. Promote regression baselines — pnpm --filter storybook visual:update
  6. Commit baselines
  1. Create migration: EnhanceRoomDetailPageMigration — add bedding, amenities, gallery Block List, CTAs to roomDetailPage
  2. Create galleryImageItem Element Type (imageDesktop + imageMobile + alt)
  3. Create migration: AddItemCardsMigration — itemCards element type with sourceNode (Content Picker), initialCount, background, showTabs
  4. Create SVG thumbnail — apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m08-item-cards.svg
  5. Register in Page Modules Block List with label Item Cards - {{blockLabel}}
  6. Apply layoutComposition
  7. Create test content — add rooms under roomsListPage, add M08 module to the page
  8. Verify Content Delivery API output
  9. Validate mapper against real API response
  10. Test in at least 2 sites (default + themed)
  1. TabFilter.types.ts
  2. TabFilter.tsx
  3. TabFilter.scss
  4. TabFilter.stories.tsx
  5. TabFilter.test.tsx
  6. index.ts

M08 Module (packages/modules/src/m08-item-cards/)

Section titled “M08 Module (packages/modules/src/m08-item-cards/)”
  1. index.ts (Server Component wrapper)
  2. ItemCards.tsx (Server Component shell)
  3. ItemCards.client.tsx (‘use client’ — tabs + grid + load more)
  4. ItemCards.scss
  5. ItemCards.types.ts
  6. ItemCards.mapper.ts
  7. ItemCards.stories.tsx
  8. ItemCards.test.tsx
  • apps/cms/Savoy.Cms/Migrations/EnhanceRoomDetailPageMigration.cs
  • apps/cms/Savoy.Cms/Migrations/AddItemCardsMigration.cs
  • apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m08-item-cards.svg
  • apps/cms/Savoy.Cms/Migrations/SavoyMigrationPlan.cs (add registrations)

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

  • 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 extends ModuleLayoutProps (marginTop, marginBottom, paddingTop, paddingBottom)
  • Mapper signature: (data: UmbracoElement, context: { rooms }) => Omit<Props, ‘siteKey’ | ‘locale’>
  • Mapper maps active: element.properties.active !== false (default true)
  • Root element: <section data-module="itemCards" data-module-id={moduleId}>
  • CardRoom imported from @savoy/ui (not relative path)
  • TabFilter imported from @savoy/ui (not relative path)
  • Responsive: mobile-first with min-width breakpoints
  • Stories use realistic hotel data (room names, descriptions in PT)
  • Images from /storybook/ directory
  • ‘use client’ ONLY in ItemCards.client.tsx
  • index.ts is a Server Component wrapper — pass only serializable data
  • Tab filtering: client-side filter on rooms array by category key
  • Load More: client-side show/hide with visibleCount state
  • Keyboard: ArrowLeft/ArrowRight between tabs, Enter/Space to select
  • ARIA: role=“tablist” on tabs, aria-selected on active tab, aria-live on card count
  • Focus: after Load More, focus first newly revealed card
  • prefers-reduced-motion: disable card reveal animation
  1. Hardcoding colors — use var(—token-name)
  2. Putting all rooms in a Block List on the module — rooms come from child pages, not module config
  3. Client-side API calls for rooms — all data is pre-fetched server-side
  4. Not passing siteKey/locale to each CardRoom — they need it for theming
  5. Fixed card widths — use fluid calc(50% - gap/2) on desktop
  6. Missing tab keyboard navigation — ArrowLeft/ArrowRight must work
  7. Load More not hiding when all cards visible
  8. Missing background variation — half-light needs the beige strip
  9. Not extracting categories from rooms for tabs — tabs are dynamic, not hardcoded
  10. Missing LAYOUT_ARG_TYPES in stories