Card Components
Read the following context files first:
- docs/superpowers/specs/2026-03-15-card-component-system-design.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/ResponsiveImage/ (all files — reference pattern)
- packages/ui/src/CtaLink/ (all files — reference pattern)
- packages/modules/src/m25-slider/ (all files — Swiper reference)
- packages/themes/src/_base.css
Now create the Card Component System in packages/ui/ with the following details:
Component Name: Card Components (CardRoom + CardContent + shared primitives + Icon)
Package: packages/ui/
Component Type: MIXED (CardRoom = Interactive, CardContent = Static)
Figma References:
card-room:
- Desktop: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1024-6632&m=dev
- Mobile: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1029-519&m=dev
- Savoy Palace: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1029-1583&m=dev
- Saccharum: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1029-1702&m=dev
- Calheta Beach: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=2079-80980&m=dev
- Next: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=2079-81086&m=dev
card-content:
- Desktop (cross-content): https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1019-22448&m=dev
- Desktop (documents): https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1642-125719&m=dev
- Mobile: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1019-24065&m=dev
- Savoy Palace: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1642-125719&m=dev
- Saccharum: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1034-25777&m=dev
- Calheta Beach: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1480-69912&m=dev
- Beach: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1989-24853&m=dev
Description: Reusable card component system with two primary formats (CardRoom and CardContent) that will be composed into page modules. Shares common primitives for image rendering and CTAs. CardRoom has an interactive Swiper gallery; CardContent is a static server component with configurable layout variations.
Architecture: Shared Primitives (CardImage, CardImageGallery, CardCtas) + Two Cards (CardRoom, CardContent) + Icon component. See design spec for full file structure.
UI Components to Use:
- ResponsiveImage from @savoy/ui (for CardImage)
- CtaLink from @savoy/ui (for CardCtas)
Layout:
- CardRoom: vertical stack — gallery (1:1) → content → CTAs. Width determined by parent.
- CardContent: vertical stack with 3 variation axes — imageSize (small/big), imagePosition (above/below), textAlign (left/right). All 8 combinations valid.
--- INTERACTIVE (CardRoom only) ---
Interaction Specification:
- Triggers: Click on prev/next buttons, swipe gesture on mobile, ArrowLeft/ArrowRight keyboard
- State Changes: Active slide index, counter text (“1/4” → “2/4”)
- Animations: Horizontal slide transition (Swiper default)
- Keyboard Navigation: ArrowLeft = previous, ArrowRight = next (when gallery is focused)
- Touch Gestures: Swipe left/right on mobile
- Loop: Enabled — last slide wraps to first
- Reduced Motion:
prefers-reduced-motiondisables slide animation
--- END INTERACTIVE ---
Development Process
Section titled “Development Process”Follow a frontend-first approach, in this exact order.
IMPORTANT: This is a UI component system (not a module). There is NO Phase C (Umbraco).
There is NO mapper (no .mapper.ts). There is NO registry entry. Props are passed directly by parent modules.
Phase A — Storybook
Section titled “Phase A — Storybook”- Define types in
.types.tsfirst for all components (shared primitives, CardRoom, CardContent, Icon) - Scaffold test files — create
.test.tsxwith describe blocks for rendering tests → CardContent (Static): write rendering tests NOW (TDD — before implementation) → CardRoom (Interactive): write rendering tests now;play()tests after Phase B → Icon: write rendering tests now - Write Storybook stories with realistic mock data BEFORE implementing the component
- Create these story variants for each card: CardRoom: Default, MinimalContent, WithoutAmenities, ManyImages, LongContent, FigmaFidelity (+ per-theme FigmaFidelity) CardContent: Default, ImageBig, ImageBelow, AlignRight, WithHighlights, WithIconLinks, MinimalContent, LongContent, FigmaFidelity (+ per-theme FigmaFidelity) Icon: AllIcons, Sizes, ThemeColors → 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 shared primitives first:
_card-shared/CardImage.tsx(Server Component)_card-shared/CardImageGallery.client.tsx(‘use client’ — Swiper)_card-shared/CardCtas.tsx(Server Component)_card-shared/_card-shared.scss
- Implement Icon component:
Icon/Icon.tsx+Icon/icons/SVG registry - Implement CardContent:
CardContent/CardContent.tsx+.scss(Server Component, composes CardImage + CardCtas) - Implement CardRoom:
CardRoom/CardRoom.tsx(server wrapper) +CardRoom.client.tsx(composes CardImageGallery + CardCtas) - (CardRoom) Write
play()interaction tests in stories - Run unit tests —
pnpm --filter ui test— all must pass before continuing - Export via
index.tswith named exports — add CardRoom, CardContent, CardImage, CardImageGallery, CardCtas, Icon topackages/ui/src/index.ts - 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.
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. 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 stories —
pnpm --filter storybook visual:test -- --story {storyId}-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
No Phase C — these are UI components consumed by parent modules.
Files to Create
Section titled “Files to Create”Shared Primitives
Section titled “Shared Primitives”- packages/ui/src/_card-shared/CardImage.tsx
- packages/ui/src/_card-shared/CardImage.types.ts
- packages/ui/src/_card-shared/CardImageGallery.client.tsx
- packages/ui/src/_card-shared/CardImageGallery.types.ts
- packages/ui/src/_card-shared/CardCtas.tsx
- packages/ui/src/_card-shared/CardCtas.types.ts
- packages/ui/src/_card-shared/_card-shared.scss
Card Room (Interactive)
Section titled “Card Room (Interactive)”- packages/ui/src/CardRoom/index.ts
- packages/ui/src/CardRoom/CardRoom.tsx (Server wrapper)
- packages/ui/src/CardRoom/CardRoom.client.tsx (‘use client’)
- packages/ui/src/CardRoom/CardRoom.types.ts
- packages/ui/src/CardRoom/CardRoom.scss
- packages/ui/src/CardRoom/CardRoom.stories.tsx
- packages/ui/src/CardRoom/CardRoom.test.tsx
Card Content (Static)
Section titled “Card Content (Static)”- packages/ui/src/CardContent/index.ts
- packages/ui/src/CardContent/CardContent.tsx
- packages/ui/src/CardContent/CardContent.types.ts
- packages/ui/src/CardContent/CardContent.scss
- packages/ui/src/CardContent/CardContent.stories.tsx
- packages/ui/src/CardContent/CardContent.test.tsx
- packages/ui/src/Icon/index.ts
- packages/ui/src/Icon/Icon.tsx
- packages/ui/src/Icon/Icon.types.ts
- packages/ui/src/Icon/Icon.scss
- packages/ui/src/Icon/icons/ (directory with SVG icon files)
- packages/ui/src/Icon/Icon.stories.tsx
- packages/ui/src/Icon/Icon.test.tsx
Theme Tokens
Section titled “Theme Tokens”- packages/themes/src/_base.css (add —radius-card-image: 0)
- packages/themes/src/calheta-beach.css (add —radius-card-image: 16px)
- packages/themes/src/hotel-next.css (add —radius-card-image: 24px)
Then export all components from packages/ui/src/index.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: string, locale: string (injected by parent module)
- No mapper — UI components receive props directly
- No registry — UI components are imported directly by modules
- Root element uses
<article>for cards (semantic HTML) - 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 links in parameters.design, and full EN documentation - Must render correctly for all 8 themes
- No TypeScript errors (pnpm typecheck passes)
Interactive Conventions (CardRoom / CardImageGallery only)
Section titled “Interactive Conventions (CardRoom / CardImageGallery only)”- ‘use client’ ONLY in .client.tsx — never in index.ts or CardRoom.tsx
- CardRoom.tsx is a Server Component wrapper — pass only serializable data (no functions, Date, Map/Set)
- No useState/useEffect in CardRoom.tsx — only in .client.tsx
- Keyboard navigation: ArrowLeft/ArrowRight when gallery is focused
- ARIA: aria-roledescription=“carousel”, aria-label on gallery, aria-live=“polite” on counter
- Focus management: visible focus ring via :focus-visible on controls
- Animations respect prefers-reduced-motion media query
- Storybook stories include interaction test via play() function
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
- Wrong BEM nesting — use &__element inside the block, not standalone .block__element
- Desktop-first media queries — use min-width (mobile-first), never max-width
- Importing from wrong package — UI components from @savoy/ui, not relative paths to modules
- Lorem ipsum in stories — use realistic hotel context (FigmaFidelity is the only exception)
- Using
Defaultstory as visual test target — always useFigmaFidelityfor Phase B7 - Wrong visual test threshold — FigmaFidelity: desktop ≤5% / mobile ≤10%
- Missing
tags: ['vitest']in story meta — unit tests won’t appear in Storybook “Unit Tests” panel - (CardRoom) Putting ‘use client’ in index.ts — only .client.tsx
- (CardRoom) Non-serializable props crossing Server-Client boundary
- (CardRoom) Missing keyboard navigation or ARIA updates on state change
- (CardRoom) No prefers-reduced-motion handling on Swiper animations
- Skipping Pixel Perfect visual tests — No component is complete without passing visual tests
- Not fetching Figma baselines — Both cards need baselines for desktop and mobile
- Not committing baselines —
__figma_baselines__/and__visual_snapshots__/must be in git - Adding Swiper dependency to CardContent — only CardRoom/CardImageGallery needs it
- Fixed pixel widths on cards — cards are fluid, width determined by parent container
- Missing —radius-card-image token on image containers — must use theme token for border-radius
- Rendering both highlights and iconLinks — only one should render; iconLinks takes priority