Skip to content

M25 Slider

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/m07-slider-categories/ (all files — reference interactive module with Swiper)
  • packages/modules/src/registry.ts
  • packages/ui/src/ (scan for available UI components)
  • docs/superpowers/specs/2026-03-12-m25-slider-design.md (authoritative design spec)

Now create a new module with the following details:

Module ID: M25 Module Name: Slider Figma Desktop: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=915-24367&m=dev Figma Mobile: https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=925-10953&m=dev Component Type: INTERACTIVE

Description: Full-width media slider displaying a sequence of images or native HTML5 videos. On desktop, the active media fills ~83% of the container width while a thumbnail of the next slide is shown in a fixed side panel (180px) with the caption, slide counter, progress bar and navigation controls. On mobile, active slide is centred and larger (312px), with adjacent slides partially visible (234px — peek effect). Supports two background modes: light (white) and medium (beige).

Props:

  • active: boolean — module visibility toggle (default true)
  • moduleId?: string — injected by ModuleRenderer
  • siteKey: string — injected by ModuleRenderer
  • locale: string — forwarded to SliderClient for ARIA label localisation
  • mode: 'light' | 'medium' — background colour variant
  • label?: HtmlHeading — eyebrow/label text (uppercase)
  • title?: HtmlHeading — main headline
  • body?: string — supporting text (indented 102px on desktop)
  • slides: SliderSlide[] — array of slide items (each with mediaType, imageDesktop, imageMobile, optional videoUrl, caption, captionCta)
  • cta?: CtaLinkWithElement — optional bottom call-to-action button

UI Components to Use:

  • CtaLink from @savoy/ui — for captionCta and bottom cta
  • ResponsiveImage is NOT used (desktop/mobile Swipers each use <img> or <video> directly)

Layout:

  • Desktop (≥1024px): two-column flex — main slide area (flex: 1) + side panel (width: var(--slider-panel-width, 180px)), aligned to bottom. Side panel: next-slide thumbnail (aspect-ratio 1/2) + meta (counter, caption, captionCta) + progress bar + prev/next nav buttons. Above the two-column area: optional heading section (label + title + body, body indented via --slider-body-indent: 102px). Below: optional CTA button.
  • Mobile (<1024px): stacked. Swiper with centeredSlides + slidesPerView auto, active slide var(--slider-slide-active-size: 312px), inactive var(--slider-slide-inactive-size: 234px). Below: meta (counter, caption, captionCta) + progress bar + prev/next nav (spread full width) + optional CTA button (full width).

--- INTERACTIVE ONLY ---

Interaction Specification:

  • Triggers: Prev/Next button click; swipe left/right (Swiper native, all viewports); ArrowLeft/ArrowRight keyboard when section is focused (tabIndex={0})
  • State Changes: activeIndex (0-based), playingIndex (null | number for video inline play), reducedMotion (boolean, SSR-safe via useEffect + matchMedia)
  • Animations: Swiper EffectFade (crossFade: true) on desktop Swiper, speed: reducedMotion ? 0 : 300; progress bar width transition var(--transition-normal). Both disabled when prefers-reduced-motion: reduce.
  • Keyboard Navigation: ArrowLeft → slidePrev, ArrowRight → slideNext; onKeyDown on root <section tabIndex={0}>
  • Touch Gestures: Swiper native swipe on mobile; allowTouchMove: false on desktop Swiper
  • Autoplay: No autoplay. Manual navigation only.
  • Video: mediaType: 'video' shows poster image + play button overlay. Click plays native <video autoPlay controls playsInline preload="metadata"> inline. Video stops on slide change (setPlayingIndex(null)).
  • Two Swipers: One desktop Swiper (effect: 'fade', hidden <1024px), one mobile Swiper (centeredSlides, hidden ≥1024px). Both share activeIndex state; navigation calls slideTo(index, speed) on both refs to keep them in sync.

--- END INTERACTIVE ONLY ---

Follow a frontend-first approach, in this exact order:

  1. Define types in .types.ts first, aligned with expected content structure
  2. Scaffold test file — create .test.tsx with describe blocks for mapper + rendering → Interactive: 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, MediumMode, WithVideo, MinimalContent, EmptyState, SingleSlide, AllOptions, LongCaption, 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, User Story link, and feature bullet list
  1. Implement the component — Slider.tsx (Server wrapper) + Slider.client.tsx ('use client') + .scss (BEM + CSS custom properties)
  2. Write mapper — Slider.mapper.ts transforming Umbraco JSON → Omit<SliderProps, 'siteKey' | 'locale'> → Map active from element.properties.active — default true when undefined → Filter inactive slides: .filter(item => item.content.properties.active !== false)
  3. Write play() interaction tests in stories (WithInteraction story)
  4. Run unit tests — pnpm --filter modules test — all must pass before continuing
  5. Register in packages/modules/src/registry.ts with { component, mapper, moduleId: 'M25' }
  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) + mobile (375×812) PNGs to __figma_baselines__/
  2. Register Figma mapping in .storybook/visual-testing/figma-mapping.ts
  3. Run visual tests: pnpm --filter storybook visual:test -- --story m25-slider-figma-fidelity
  4. Iterate — fix CSS until desktop ≤5% and mobile ≤10% (max 10 iterations / 30 min)
  5. Promote regression baselines: pnpm --filter storybook visual:update
  6. Commit baselines

Phase C — Umbraco (hard gate: Phase B7 must pass first)

Section titled “Phase C — Umbraco (hard gate: Phase B7 must pass first)”
  1. Define Element Types: sliderItem (nested) + slider (module) — see spec for full schema
  2. Create SVG thumbnail — apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m25-slider.svg
  3. Write AddSliderMigration.cs — bottom-up: sliderItemSlider Items BLslider → register in Page Modules BL
  4. Register in SavoyMigrationPlan.cs
  5. Test in Umbraco — create content, verify Content Delivery API output
  6. Validate mapper against real API response
  7. Test in ≥2 sites (default + one themed)
  8. Verify active: false hides module; inactive slides are filtered from output

Interactive module:

  1. packages/modules/src/m25-slider/index.ts
  2. packages/modules/src/m25-slider/Slider.tsx (Server Component wrapper)
  3. packages/modules/src/m25-slider/Slider.client.tsx (‘use client’)
  4. packages/modules/src/m25-slider/Slider.scss
  5. packages/modules/src/m25-slider/Slider.types.ts
  6. packages/modules/src/m25-slider/Slider.mapper.ts
  7. packages/modules/src/m25-slider/Slider.stories.tsx
  8. packages/modules/src/m25-slider/Slider.test.tsx

Phase C: 9. apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m25-slider.svg 10. apps/cms/Savoy.Cms/Migrations/AddSliderMigration.cs 11. apps/cms/Savoy.Cms/Migrations/SavoyMigrationPlan.cs (add migration registration)

Modify: 12. packages/modules/src/registry.ts (add slider entry)

See docs/superpowers/specs/2026-03-12-m25-slider-design.md for authoritative type definitions, mapper code, Umbraco schema, and ARIA specifications.