M11 — Banner: Development Prompt
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/m04-page-hero/ (reference implementation — similar full-bleed image module)
- packages/modules/src/registry.ts
- packages/themes/src/_base.css
- packages/themes/src/savoy-palace.css (reference for module token pattern)
- packages/ui/src/ (scan for available UI components, especially ResponsiveImage and CtaLink)
1. Module Overview
Section titled “1. Module Overview”| Field | Value |
|---|---|
| Module ID | M11 |
| Module Name | Banner |
| Folder | m11-banner |
| PascalCase | Banner |
| camelCase (Registry Key / Umbraco Alias) | banner |
| BEM Block | .banner |
| Component Type | Static (Server Component — no interactivity, no client-side state) |
Description: Full-width banner module with a background image and dark overlay (Light variant) or a solid tinted background (Medium variant). Content sits in a left or right aligned text column containing an optional etiqueta (pill-shaped badge image), headline, body text, and up to two CTAs. An optional decorative symbol image appears on the side opposite the text column, desktop only. Supports two height sizes (Small / Big) that also change the headline tag (H6 / H5) and layout column width. All fields are optional.
Figma Links:
| Variant | URL |
|---|---|
| Desktop (default — Small, Light, Left) | https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1110-1817&m=dev |
| Mobile | https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1112-1930&m=dev |
| All desktop variants (8 combinations) | https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1110-1818&m=dev |
| Additional variants | https://www.figma.com/design/VFdCObxYyZjSRFdK5pU1df/01_Savoy_library?node-id=1721-97850&m=dev |
2. Props
Section titled “2. Props”All props are optional. siteKey and locale are always injected by ModuleRenderer.
import type { HtmlHeading, CtaLinkWithElement } from '@savoy/cms-client';import type { ResponsiveImageProps } from '@savoy/ui';import type { SiteKey } from '@savoy/cms-client';
export interface BannerProps { // --- Layout / variant selectors --- background?: 'light' | 'medium'; // 'light' = image+overlay | 'medium' = solid tinted bg. Default: 'light' size?: 'small' | 'big'; // 'small' = 608px / H6 | 'big' = 790px / H5. Default: 'small' textPosition?: 'left' | 'right'; // Position of the text column. Default: 'left'
// --- Content fields (all optional) --- etiqueta?: ResponsiveImageProps; // Pill-shaped badge/logo image (134x50px display size) headline?: HtmlHeading; // Heading text — H6 for Small, H5 for Big (editor controls tag) body?: string; // Body paragraph text (Body M) cta?: CtaLinkWithElement; // Primary CTA (filled button) ctaSecondary?: CtaLinkWithElement; // Secondary CTA (link/underline style) symbolImage?: ResponsiveImageProps; // Decorative image opposite text column (desktop only)
// --- Background image (used when background = 'light') --- imageDesktop?: ResponsiveImageProps['imageDesktop']; imageMobile?: ResponsiveImageProps['imageMobile'];
// --- Standard module props --- siteKey: SiteKey; locale: string; moduleId?: string; // injected by page renderer}Prop notes:
background: 'medium'replaces the image+overlay withvar(--color-bg-alt)(tinted surface). Whenmedium,imageDesktop/imageMobileare ignored.size: 'big'increases module height, widens the layout grid (12 columns vs 10), and changes the headline font from--text-5xl(H6) to--text-6xl(H5). The editor controls the semantic HTML tag viaheadline.html.etiquetais a Media Picker image displayed as a pill (border-radius: 100px). The image itself provides the visual (logo, award badge, etc.).symbolImageis decorative — hidden on mobile, shown on the side oppositetextPositionon desktop.- When
ctais absent,ctaSecondaryshould also not render (secondary CTA cannot stand alone without primary).
3. Visual Variants
Section titled “3. Visual Variants”Desktop (1440px)
Section titled “Desktop (1440px)”| Size | Background | Text Position | Height | Headline | Columns |
|---|---|---|---|---|---|
| Small | Light | Left | 608px | H6 (40px) | 10 col (996px) |
| Small | Light | Right | 608px | H6 (40px) | 10 col (996px) |
| Small | Medium | Left | 608px | H6 (40px) | 10 col (996px) |
| Small | Medium | Right | 608px | H6 (40px) | 10 col (996px) |
| Big | Light | Left | 790px | H5 (48px) | 12 col (1360px) |
| Big | Light | Right | 790px | H5 (48px) | 12 col (1360px) |
| Big | Medium | Left | 790px | H5 (48px) | 12 col (1360px) |
| Big | Medium | Right | 790px | H5 (48px) | 12 col (1360px) |
Mobile (375px)
Section titled “Mobile (375px)”- Height: 600px
- No
symbolImage— hidden on mobile (SCSSdisplay: none, not conditional render) - CTAs stack vertically (primary full-width, secondary centered below)
- Padding: 24px horizontal
- Content max-width: 312px (5 columns)
4. Layout
Section titled “4. Layout”Desktop structure (BEM):
<section class="banner banner--light banner--small banner--text-left"> <div class="banner__background"> ← full-bleed image (light) or solid bg (medium) <div class="banner__overlay"> ← dark 40% overlay (light variant only) <div class="banner__container"> ← centered, 10 or 12 columns wide <div class="banner__inner"> ← flex row, gap 102px (small) / 126px (big) <div class="banner__content"> ← text column, ~486px (small) / ~588px (big) <img class="banner__etiqueta"> ← optional pill image <h6 class="banner__headline"> ← or h5 when size=big <p class="banner__body"> <div class="banner__ctas"> <a class="banner__cta"> ← filled <a class="banner__cta-secondary"> ← link <div class="banner__symbol"> ← optional decorative image (opposite side)SCSS modifier classes on root <section>:
banner--light/banner--medium— background variantbanner--small/banner--big— size variantbanner--text-left/banner--text-right— text position
Mobile:
banner__symbolhidden viadisplay: noneat all breakpoints belowlgbanner__ctasswitches toflex-direction: column; align-items: center- Primary CTA becomes
width: 100% - Content column takes full width (no side-by-side layout)
5. CSS Tokens to Use
Section titled “5. CSS Tokens to Use”// Background--color-bg // Light variant: page background fallback--color-bg-alt // Medium variant: tinted surface (e.g. beige)
// Text (white on Light; dark on Medium)--color-text--color-text-on-dark // use for light variant text
// Typography--font-heading--font-body--text-5xl // H6 font-size (small, ~40px)--text-6xl // H5 font-size (big, ~48px)--text-base // Body M--leading-tight--leading-normal
// Spacing--space-6 // 24px — mobile padding, cta gap--space-10 // 40px — gap between content groups--container-max--container-padding--container-padding-md--container-padding-lg6. UI Components to Use
Section titled “6. UI Components to Use”| Component | Source | Usage |
|---|---|---|
ResponsiveImage | @savoy/ui | Background image (light), etiqueta, symbolImage |
CtaLink | @savoy/ui | Primary and secondary CTAs |
HtmlHeading rendering pattern | inline (see frontend-module rules) | Renders headline with dynamic tag from CMS. Content is trusted CMS output — see .claude/rules/frontend-module.md for the safe rendering pattern. |
Do NOT create custom button styles — use CtaLink from @savoy/ui with appropriate as and class props.
7. Mapper
Section titled “7. Mapper”import { mapHtmlHeading } from '@savoy/cms-client';import type { BannerProps } from './Banner.types';
export function mapBanner( element: UmbracoElement): Omit<BannerProps, 'siteKey' | 'locale'> { const p = element.properties;
const ctaLink = p.cta?.[0]; const ctaSecondaryLink = p.ctaSecondary?.[0];
return { active: p.active !== false, background: p.background ?? 'light', size: p.size ?? 'small', textPosition: p.textPosition ?? 'left',
headline: p.headline ? mapHtmlHeading(p.headline) : undefined, body: p.body ?? undefined,
etiqueta: p.etiquetaDesktop ? { imageDesktop: { url: p.etiquetaDesktop.url, width: 134, height: 50, alt: p.etiquetaDesktop.name ?? '' }, imageMobile: { url: (p.etiquetaMobile ?? p.etiquetaDesktop).url, width: 134, height: 50, alt: '' }, } : undefined,
imageDesktop: p.imageDesktop ? { url: p.imageDesktop.url, width: 1440, height: 790, alt: p.imageDesktop.name ?? '' } : undefined, imageMobile: p.imageMobile ? { url: p.imageMobile.url, width: 375, height: 600, alt: '' } : undefined,
symbolImage: p.symbolImageDesktop ? { imageDesktop: { url: p.symbolImageDesktop.url, width: 501, height: 626, alt: p.symbolImageDesktop.name ?? '' }, imageMobile: { url: (p.symbolImageMobile ?? p.symbolImageDesktop).url, width: 375, height: 400, alt: '' }, } : undefined,
cta: ctaLink ? { label: ctaLink.name, href: ctaLink.url, as: 'a' } : undefined, ctaSecondary: ctaSecondaryLink ? { label: ctaSecondaryLink.name, href: ctaSecondaryLink.url, as: 'a' } : undefined, };}8. Storybook Stories
Section titled “8. Storybook Stories”Required variants:
| Story | Purpose |
|---|---|
Default | background: ‘light’, size: ‘small’, textPosition: ‘left’, all fields, realistic hotel content |
TextRight | textPosition: ‘right’, all fields |
SizeBig | size: ‘big’, textPosition: ‘left’, all fields |
BackgroundMedium | background: ‘medium’, all fields |
MinimalContent | headline + cta only — no etiqueta, no body, no symbolImage |
NoCta | headline + body, no CTAs, no symbolImage |
FigmaFidelity | Exact Figma content verbatim (Lorem ipsum as-is) — visual test target |
FigmaFidelity story parameters:
parameters: { pixelPerfect: { threshold: { desktop: 0.05, mobile: 0.10 }, viewports: ['desktop', 'mobile'], themes: ['savoy-palace'], },},Mock data guidelines:
- Use hotel / restaurant / room descriptions as realistic content
- siteKey:
'savoy-palace', locale:'pt' - Image URLs: use Unsplash or placeholder services with correct aspect ratios
9. Umbraco Element Type Schema
Section titled “9. Umbraco Element Type Schema”Alias: banner
Icon: icon-banner
Tab: Content (sortOrder 1)
| # | Property Name | Alias | Editor | Notes |
|---|---|---|---|---|
| 0 | Active | active | Toggle | Default ON. First property. |
| 1 | Background | background | Dropdown (light, medium) | Default: light |
| 2 | Size | size | Dropdown (small, big) | Default: small |
| 3 | Text Position | textPosition | Dropdown (left, right) | Default: left |
| 4 | Headline | headline | Single Html Heading Block List | Optional |
| 5 | Body | body | Textarea | Optional |
| 6 | CTA | cta | URL Picker | Optional |
| 7 | CTA Secondary | ctaSecondary | URL Picker | Optional |
| 8 | Etiqueta Desktop | etiquetaDesktop | Media Picker | Optional — pill badge image |
| 9 | Etiqueta Mobile | etiquetaMobile | Media Picker | Optional — falls back to desktop |
| 10 | Image Desktop | imageDesktop | Media Picker | Used when background = ‘light’ |
| 11 | Image Mobile | imageMobile | Media Picker | Used when background = ‘light’ |
| 12 | Symbol Image Desktop | symbolImageDesktop | Media Picker | Optional |
| 13 | Symbol Image Mobile | symbolImageMobile | Media Picker | Optional — falls back to desktop |
Block List label expression: Banner - {{headline}}
(If editor leaves headline blank, add a blockLabel Textstring property at position 1 and use Banner - {{blockLabel}}.)
SVG Thumbnail: apps/cms/Savoy.Cms/wwwroot/assets/thumbnails/m11-banner.svg
10. Files to Create
Section titled “10. Files to Create”packages/modules/src/m11-banner/ index.ts Banner.tsx Banner.scss Banner.types.ts Banner.mapper.ts Banner.stories.tsx Banner.test.tsxRegister in packages/modules/src/registry.ts:
banner: { component: Banner, mapper: mapBanner, moduleId: 'M11' },11. Common Pitfalls for This Module
Section titled “11. Common Pitfalls for This Module”symbolImageon mobile — usedisplay: nonein SCSS (not conditional render) so the DOM element exists for potential JS hooks but is hidden- Background Medium ignores images — when
background === 'medium', do not render<img>background or overlay element - Size modifies layout AND typography —
banner--bigmust change both column width AND font-size token (--text-5xl→--text-6xl) - Text colour on Medium — headline and body are dark-coloured on Medium variant (no image overlay), use
--color-textnot--color-text-on-dark - Etiqueta border-radius —
border-radius: var(--radius-full)(100px) — use the token, not a hardcoded value - Do not hardcode heights — use
min-height+ padding so content is never clipped if text overflows - CTA secondary guard — if
ctaprop is absent,ctaSecondarymust also be hidden (checkcta &&before renderingctaSecondary)