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 ---
Development Process
Section titled “Development Process”This module requires BOTH a UI component (TabFilter) and a module (M08). Follow this order:
Pre-Phase: TabFilter UI Component
Section titled “Pre-Phase: TabFilter UI Component”Before starting the module, create the TabFilter component in packages/ui/src/TabFilter/:
- TabFilter.types.ts — TabFilterTab, TabFilterProps
- TabFilter.test.tsx — rendering + interaction tests
- TabFilter.tsx — client component (‘use client’) with tab state management
- TabFilter.scss — BEM styles with theme tokens
- TabFilter.stories.tsx — Default, ManyTabs, SingleTab, FigmaFidelity
- index.ts — exports
- Update packages/ui/src/index.ts — add TabFilter exports
Phase A — Storybook
Section titled “Phase A — Storybook”- Define types in
.types.tsfirst — RoomCardData, ItemCardsProps extending ModuleLayoutProps - Scaffold test file — create
.test.tsxwith describe blocks for mapper + rendering → 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 (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
- Stories must include
tags: ['autodocs', 'vitest'], full EN autodocs documentation with Figma desktop + mobile links and feature bullet list
Phase B — React/Next.js
Section titled “Phase B — React/Next.js”- Implement the component:
ItemCards.tsx— Server Component wrapperItemCards.client.tsx— Client component (‘use client’) with tab filtering + load more stateItemCards.scss— BEM styles (grid, background variation, load more button)
- Write mapper —
ItemCards.mapper.ts— maps Umbraco element + pre-fetched room children to props → Mapactivefromelement.properties.active— defaulttruewhen undefined → Map rooms from context (pre-fetched roomDetailPage children) → Extract unique categories for tab filter → Map initialCount, background, showTabs from element properties - Write
play()interaction tests: tab click filters cards, Load More reveals cards - Run unit tests —
pnpm --filter modules test— all must pass before continuing - Register in
packages/modules/src/registry.tswith{ component: ItemCards, mapper: mapItemCards, moduleId: "M08" } - 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)”- Fetch Figma baselines — desktop (1440×900) node
1047:7702+ mobile (375×812) node1048:8850 - Register Figma mapping in
.storybook/visual-testing/figma-mapping.ts - Run visual tests against FigmaFidelity story — desktop ≤5%, mobile ≤10%
- Iterate — fix CSS until thresholds met
- Promote regression baselines —
pnpm --filter storybook visual:update - Commit baselines
Phase C — Umbraco
Section titled “Phase C — Umbraco”- Create migration: EnhanceRoomDetailPageMigration — add bedding, amenities, gallery Block List, CTAs to roomDetailPage
- Create galleryImageItem Element Type (imageDesktop + imageMobile + alt)
- Create migration: AddItemCardsMigration — itemCards element type with sourceNode (Content Picker), initialCount, background, showTabs
- Create SVG thumbnail —
apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m08-item-cards.svg - Register in Page Modules Block List with label
Item Cards - {{blockLabel}} - Apply layoutComposition
- Create test content — add rooms under roomsListPage, add M08 module to the page
- Verify Content Delivery API output
- Validate mapper against real API response
- Test in at least 2 sites (default + themed)
Files to Create
Section titled “Files to Create”TabFilter (packages/ui/src/TabFilter/)
Section titled “TabFilter (packages/ui/src/TabFilter/)”- TabFilter.types.ts
- TabFilter.tsx
- TabFilter.scss
- TabFilter.stories.tsx
- TabFilter.test.tsx
- index.ts
M08 Module (packages/modules/src/m08-item-cards/)
Section titled “M08 Module (packages/modules/src/m08-item-cards/)”- index.ts (Server Component wrapper)
- ItemCards.tsx (Server Component shell)
- ItemCards.client.tsx (‘use client’ — tabs + grid + load more)
- ItemCards.scss
- ItemCards.types.ts
- ItemCards.mapper.ts
- ItemCards.stories.tsx
- ItemCards.test.tsx
CMS (Phase C)
Section titled “CMS (Phase C)”- 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.
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 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
Interactive Module Conventions
Section titled “Interactive Module Conventions”- ‘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
Common Pitfalls to Avoid
Section titled “Common Pitfalls to Avoid”- Hardcoding colors — use var(—token-name)
- Putting all rooms in a Block List on the module — rooms come from child pages, not module config
- Client-side API calls for rooms — all data is pre-fetched server-side
- Not passing siteKey/locale to each CardRoom — they need it for theming
- Fixed card widths — use fluid calc(50% - gap/2) on desktop
- Missing tab keyboard navigation — ArrowLeft/ArrowRight must work
- Load More not hiding when all cards visible
- Missing background variation — half-light needs the beige strip
- Not extracting categories from rooms for tabs — tabs are dynamic, not hardcoded
- Missing LAYOUT_ARG_TYPES in stories