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 ModuleRenderersiteKey: string— injected by ModuleRendererlocale: string— forwarded to SliderClient for ARIA label localisationmode: 'light' | 'medium'— background colour variantlabel?: HtmlHeading— eyebrow/label text (uppercase)title?: HtmlHeading— main headlinebody?: 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:
CtaLinkfrom@savoy/ui— for captionCta and bottom ctaResponsiveImageis 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), inactivevar(--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 viauseEffect+matchMedia) - Animations: Swiper
EffectFade(crossFade: true) on desktop Swiper,speed: reducedMotion ? 0 : 300; progress barwidthtransitionvar(--transition-normal). Both disabled whenprefers-reduced-motion: reduce. - Keyboard Navigation:
ArrowLeft→ slidePrev,ArrowRight→ slideNext;onKeyDownon root<section tabIndex={0}> - Touch Gestures: Swiper native swipe on mobile;
allowTouchMove: falseon 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 shareactiveIndexstate; navigation callsslideTo(index, speed)on both refs to keep them in sync.
--- END INTERACTIVE ONLY ---
Development Process
Section titled “Development Process”Follow a frontend-first approach, in this exact order:
Phase A — Storybook
Section titled “Phase A — Storybook”- Define types in
.types.tsfirst, aligned with expected content structure - Scaffold test file — create
.test.tsxwith describe blocks for mapper + rendering → Interactive: 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, 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
- Stories must include
tags: ['autodocs', 'vitest'], full EN autodocs documentation with Figma desktop + mobile links, User Story link, and feature bullet list
Phase B — React/Next.js
Section titled “Phase B — React/Next.js”- Implement the component —
Slider.tsx(Server wrapper) +Slider.client.tsx('use client') +.scss(BEM + CSS custom properties) - Write mapper —
Slider.mapper.tstransforming Umbraco JSON →Omit<SliderProps, 'siteKey' | 'locale'>→ Mapactivefromelement.properties.active— defaulttruewhen undefined → Filter inactive slides:.filter(item => item.content.properties.active !== false) - Write
play()interaction tests in stories (WithInteraction story) - Run unit tests —
pnpm --filter modules test— all must pass before continuing - Register in
packages/modules/src/registry.tswith{ component, mapper, moduleId: 'M25' } - 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) + mobile (375×812) PNGs to
__figma_baselines__/ - Register Figma mapping in
.storybook/visual-testing/figma-mapping.ts - Run visual tests:
pnpm --filter storybook visual:test -- --story m25-slider-figma-fidelity - Iterate — fix CSS until desktop ≤5% and mobile ≤10% (max 10 iterations / 30 min)
- Promote regression baselines:
pnpm --filter storybook visual:update - Commit baselines
Phase C — Umbraco (hard gate: Phase B7 must pass first)
Section titled “Phase C — Umbraco (hard gate: Phase B7 must pass first)”- Define Element Types:
sliderItem(nested) +slider(module) — see spec for full schema - Create SVG thumbnail —
apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m25-slider.svg - Write
AddSliderMigration.cs— bottom-up:sliderItem→Slider Items BL→slider→ register inPage Modules BL - Register in
SavoyMigrationPlan.cs - Test in Umbraco — create content, verify Content Delivery API output
- Validate mapper against real API response
- Test in ≥2 sites (default + one themed)
- Verify
active: falsehides module; inactive slides are filtered from output
Files to Create
Section titled “Files to Create”Interactive module:
- packages/modules/src/m25-slider/index.ts
- packages/modules/src/m25-slider/Slider.tsx (Server Component wrapper)
- packages/modules/src/m25-slider/Slider.client.tsx (‘use client’)
- packages/modules/src/m25-slider/Slider.scss
- packages/modules/src/m25-slider/Slider.types.ts
- packages/modules/src/m25-slider/Slider.mapper.ts
- packages/modules/src/m25-slider/Slider.stories.tsx
- 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)
Conventions
Section titled “Conventions”See docs/superpowers/specs/2026-03-12-m25-slider-design.md for authoritative type definitions, mapper code, Umbraco schema, and ARIA specifications.