Skip to content

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:

card-content:

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-motion disables slide animation

--- END INTERACTIVE ---

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.

  1. Define types in .types.ts first for all components (shared primitives, CardRoom, CardContent, Icon)
  2. Scaffold test files — create .test.tsx with 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
  3. Write Storybook stories with realistic mock data BEFORE implementing the component
  4. 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
  5. Stories must include tags: ['autodocs', 'vitest'], full EN autodocs documentation with Figma desktop + mobile links and feature bullet list
  1. 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
  2. Implement Icon component: Icon/Icon.tsx + Icon/icons/ SVG registry
  3. Implement CardContent: CardContent/CardContent.tsx + .scss (Server Component, composes CardImage + CardCtas)
  4. Implement CardRoom: CardRoom/CardRoom.tsx (server wrapper) + CardRoom.client.tsx (composes CardImageGallery + CardCtas)
  5. (CardRoom) Write play() interaction tests in stories
  6. Run unit tests — pnpm --filter ui test — all must pass before continuing
  7. Export via index.ts with named exports — add CardRoom, CardContent, CardImage, CardImageGallery, CardCtas, Icon to packages/ui/src/index.ts
  8. 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.

  1. Fetch Figma baselines — Use Figma MCP get_screenshot to capture desktop (1440×900) + mobile (375×812) PNGs to __figma_baselines__/{storyId}/
  2. Register Figma mapping — Add story ID → Figma node in .storybook/visual-testing/figma-mapping.ts
  3. Run visual tests against FigmaFidelity stories — pnpm --filter storybook visual:test -- --story {storyId}-figma-fidelity
  4. Iterate — View diff in Pixel Perfect panel, fix CSS until both viewport thresholds are met (loop back to Phase B if needed)
  5. Promote regression baselines — pnpm --filter storybook visual:update
  6. Commit baselines — __figma_baselines__/ and __visual_snapshots__/ are git-tracked

No Phase C — these are UI components consumed by parent modules.

  1. packages/ui/src/_card-shared/CardImage.tsx
  2. packages/ui/src/_card-shared/CardImage.types.ts
  3. packages/ui/src/_card-shared/CardImageGallery.client.tsx
  4. packages/ui/src/_card-shared/CardImageGallery.types.ts
  5. packages/ui/src/_card-shared/CardCtas.tsx
  6. packages/ui/src/_card-shared/CardCtas.types.ts
  7. packages/ui/src/_card-shared/_card-shared.scss
  1. packages/ui/src/CardRoom/index.ts
  2. packages/ui/src/CardRoom/CardRoom.tsx (Server wrapper)
  3. packages/ui/src/CardRoom/CardRoom.client.tsx (‘use client’)
  4. packages/ui/src/CardRoom/CardRoom.types.ts
  5. packages/ui/src/CardRoom/CardRoom.scss
  6. packages/ui/src/CardRoom/CardRoom.stories.tsx
  7. packages/ui/src/CardRoom/CardRoom.test.tsx
  1. packages/ui/src/CardContent/index.ts
  2. packages/ui/src/CardContent/CardContent.tsx
  3. packages/ui/src/CardContent/CardContent.types.ts
  4. packages/ui/src/CardContent/CardContent.scss
  5. packages/ui/src/CardContent/CardContent.stories.tsx
  6. packages/ui/src/CardContent/CardContent.test.tsx
  1. packages/ui/src/Icon/index.ts
  2. packages/ui/src/Icon/Icon.tsx
  3. packages/ui/src/Icon/Icon.types.ts
  4. packages/ui/src/Icon/Icon.scss
  5. packages/ui/src/Icon/icons/ (directory with SVG icon files)
  6. packages/ui/src/Icon/Icon.stories.tsx
  7. packages/ui/src/Icon/Icon.test.tsx
  1. packages/themes/src/_base.css (add —radius-card-image: 0)
  2. packages/themes/src/calheta-beach.css (add —radius-card-image: 16px)
  3. packages/themes/src/hotel-next.css (add —radius-card-image: 24px)

Then export all components from packages/ui/src/index.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 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
  1. Hardcoding colors, fonts, or spacing — use var(—token-name) from packages/themes/src/
  2. Forgetting imageMobile — every image needs both desktop and mobile variants
  3. Wrong BEM nesting — use &__element inside the block, not standalone .block__element
  4. Desktop-first media queries — use min-width (mobile-first), never max-width
  5. Importing from wrong package — UI components from @savoy/ui, not relative paths to modules
  6. Lorem ipsum in stories — use realistic hotel context (FigmaFidelity is the only exception)
  7. Using Default story as visual test target — always use FigmaFidelity for Phase B7
  8. Wrong visual test threshold — FigmaFidelity: desktop ≤5% / mobile ≤10%
  9. Missing tags: ['vitest'] in story meta — unit tests won’t appear in Storybook “Unit Tests” panel
  10. (CardRoom) Putting ‘use client’ in index.ts — only .client.tsx
  11. (CardRoom) Non-serializable props crossing Server-Client boundary
  12. (CardRoom) Missing keyboard navigation or ARIA updates on state change
  13. (CardRoom) No prefers-reduced-motion handling on Swiper animations
  14. Skipping Pixel Perfect visual tests — No component is complete without passing visual tests
  15. Not fetching Figma baselines — Both cards need baselines for desktop and mobile
  16. Not committing baselines — __figma_baselines__/ and __visual_snapshots__/ must be in git
  17. Adding Swiper dependency to CardContent — only CardRoom/CardImageGallery needs it
  18. Fixed pixel widths on cards — cards are fluid, width determined by parent container
  19. Missing —radius-card-image token on image containers — must use theme token for border-radius
  20. Rendering both highlights and iconLinks — only one should render; iconLinks takes priority