diff --git a/docs/memory/component-registry.md b/docs/memory/component-registry.md index b31453d..16d133f 100644 --- a/docs/memory/component-registry.md +++ b/docs/memory/component-registry.md @@ -37,7 +37,9 @@ duplicates) and MUST update it after completing one. | Component | Status | Composed of | Notes | |-----------|--------|-------------|-------| | FormField | planned | Input + Typography (label) + Typography (helper) | Standard form field with label and validation | -| PriceCard | planned | Card + Typography + Badge + Button | Service pricing display | +| ProviderCard | review | Card + Typography + Badge | Provider listing card. Verified: image + logo + "Trusted Partner" badge. Unverified: text-only. Capability badges (arbitrary label + colour). Footer with "Packages from $X >". 7 component tokens. | +| VenueCard | planned | Card + Typography + Badge | Venue listing card. Always has photo, location, capacity, price. Simpler than ProviderCard. | +| MapCard | planned | Card + Typography + Badge | Compact horizontal map popup card. Deferred until map integration. | | ServiceOption | planned | Card + Typography + Chip + Icon | Selectable service item | | SearchBar | planned | Input + Icon + Button | Search with submit | | StepIndicator | planned | Typography + Badge + Divider | Multi-step flow progress | diff --git a/docs/memory/decisions-log.md b/docs/memory/decisions-log.md index ac5ade7..22b94cc 100644 --- a/docs/memory/decisions-log.md +++ b/docs/memory/decisions-log.md @@ -219,3 +219,43 @@ contradict a previous one. **Rationale:** Leading icons (email, phone, dollar) are essential for FA's arrangement forms. Success state provides positive feedback after validation. The Figma only had trailing icon, but leading icons are a near-universal production need. The `startIcon`/`endIcon` props are simpler than MUI's `InputAdornment` pattern while remaining compatible with raw adornments via `startAdornment`/`endAdornment`. **Affects:** Input component API, InputAdornment usage, Storybook stories **Alternatives considered:** Only supporting MUI's raw adornment API — rejected as too verbose for the common case. The convenience props are ergonomic while the raw props remain available for complex cases (e.g., password toggle with IconButton). + +### D026 — ProviderCard establishes the molecule pattern +**Date:** 2026-03-25 +**Category:** architecture +**Decision:** ProviderCard is the first molecule. Molecules compose existing atoms via `sx` props — no MUI theme overrides. All styling from token CSS variables and theme accessors. +**Rationale:** Keeps molecules lightweight and composable. Theme overrides are reserved for atoms where global consistency matters. Molecules are higher-level compositions with more context-specific layouts. +**Affects:** All future molecules (VenueCard, MapCard, ServiceOption, etc.) +**Alternatives considered:** Adding MuiProviderCard theme overrides — rejected as molecules are page-specific compositions, not reusable primitives. + +### D027 — ProviderCard image is a URL string, not a ReactNode +**Date:** 2026-03-25 +**Category:** component +**Decision:** `imageUrl` prop accepts a string rendered as CSS `background-image` with `cover`. Logo is also a URL string rendered as ``. +**Rationale:** Simpler API for the common case (CDN URLs). CSS background-image handles aspect ratio cropping automatically. `` for logo supports `alt` text natively for accessibility. +**Affects:** ProviderCard, VenueCard, MapCard APIs +**Alternatives considered:** ReactNode for image — rejected as over-flexible. A ReactNode slot would allow video/carousel but adds API complexity for a feature we don't need yet. + +### D028 — Logo is 48px (not 75px from Figma) +**Date:** 2026-03-25 +**Category:** component +**Decision:** Logo overlay is 48px diameter, not the 75px shown in Figma. +**Rationale:** On a ~340-400px card in a list panel, a 75px logo dominates the content area. 48px provides clear brand visibility while leaving sufficient space for the provider name and meta row. +**Affects:** providerCard.logo.size token +**Alternatives considered:** 75px from Figma — too large for the card width. 64px — still large relative to content. + +### D029 — Footer is built into ProviderCard, not a slot +**Date:** 2026-03-25 +**Category:** component +**Decision:** The "Packages from $X >" footer is rendered via a `startingPrice` number prop, not a children/slot pattern. +**Rationale:** The footer is structurally identical across all provider cards — warm background, "Packages from" text, price, chevron. Only the price value changes. A slot would add API complexity for no flexibility gain. If footer is omitted (no startingPrice), the bar is simply absent. +**Affects:** ProviderCard API, VenueCard may use a similar pattern +**Alternatives considered:** children/render prop for footer — rejected as over-engineered for a fixed layout. + +### D030 — Verified is an explicit boolean, not derived from imageUrl +**Date:** 2026-03-25 +**Category:** component +**Decision:** `verified` boolean prop controls the visual treatment independently from `imageUrl`/`logoUrl`. +**Rationale:** A provider could be verified but have a broken/missing image URL. The boolean is the source of truth from the business layer. Image and logo are only rendered when `verified` is also true, preventing accidental display of unverified providers with images. +**Affects:** ProviderCard API, data model expectations +**Alternatives considered:** Deriving verified from imageUrl presence — rejected as it couples business logic (partner status) to content availability (image uploaded). diff --git a/docs/memory/session-log.md b/docs/memory/session-log.md index c06371f..806eeb8 100644 --- a/docs/memory/session-log.md +++ b/docs/memory/session-log.md @@ -496,6 +496,48 @@ Each entry follows this structure: - **Planned (5 organisms):** ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer **Next steps:** -- User to review Switch and Radio in Storybook -- Begin PriceCard molecule +- ~~User to review Switch and Radio in Storybook~~ ✓ Approved +- ~~Begin PriceCard molecule~~ ✓ Replaced with ProviderCard (see below) +- Address P2 audit issues in a future cleanup pass + +### Session 2026-03-25l — ProviderCard molecule (first molecule) + +**Agent(s):** Claude Opus (via conversation) + +**Work completed:** +- Reviewed 3 Figma designs: Provider Cards (5503:44422), Service Venue (2997:83018), Map Pin Card (5369:140263) +- Captured business context: verified vs unverified providers, Parsons partner strategy, map + list layout +- Replaced generic "PriceCard" in registry with 3 specific molecules: ProviderCard, VenueCard, MapCard +- Saved business context to auto-memory (project_listing_cards.md) +- Created plan via /plan mode — component API, card structure, token plan, story plan +- Created providerCard component tokens (`tokens/component/providerCard.json`): image.height, logo.size, footer.background/paddingX/paddingY, content.padding/gap — 7 tokens +- Built ProviderCard molecule (`src/components/molecules/ProviderCard/ProviderCard.tsx`): + - Composes Card (interactive, padding="none") + Badge + Typography + - Verified variant: hero image (CSS bg-image cover), logo overlay (48px circle, absolute positioned, overlaps content), "Trusted Partner" badge (filled brand), capability badge + - Unverified variant: text-only, same content area + footer + - Footer bar: warm brand.100 bg, "Packages from $X" + ChevronRight + - Accessibility: aria-label on reviews, aria-hidden on decorative icons, alt text on logo + - All values from token CSS variables, no hardcoded hex +- Created 9 Storybook stories: Default, VerifiedProvider, UnverifiedProvider, ListLayout (mixed), CapabilityVariants, EdgeCases, Responsive, OnDifferentBackgrounds, InteractiveDemo +- Preflight passed all 5 checks +- Logged D026-D030 in decisions log + +**Decisions made:** +- D026: Molecules compose atoms via sx — no MUI theme overrides +- D027: Image as URL string (CSS bg-image), logo as URL string () +- D028: Logo 48px (not 75px Figma) for card width proportion +- D029: Footer built in via startingPrice prop, not a slot +- D030: verified is explicit boolean, not derived from imageUrl + +**Component status at end of session:** +- **Done (7):** Button, Typography, Input, Card, Badge, Chip, Switch, Radio +- **Review (1 molecule):** ProviderCard +- **Planned (7 atoms):** IconButton, Icon, Avatar, Divider, ColourToggle, Slider, Link +- **Planned (5 molecules):** VenueCard, MapCard, ServiceOption, SearchBar, StepIndicator, FormField +- **Planned (5 organisms):** ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer + +**Next steps:** +- User to review ProviderCard in Storybook — especially ListLayout story for verified/unverified alignment +- Build VenueCard molecule (simpler — always has photo, location, capacity, price) +- Consider MapCard as a deferred item until map integration - Address P2 audit issues in a future cleanup pass diff --git a/docs/memory/token-registry.md b/docs/memory/token-registry.md index b92f121..9765532 100644 --- a/docs/memory/token-registry.md +++ b/docs/memory/token-registry.md @@ -264,3 +264,17 @@ the correct token for any design property. |-----------|-----------|---------|-------------| | radio.size.default | 20px | Radio | Outer circle size | | radio.dotSize.default | 10px | Radio | Inner selected dot size | + +### ProviderCard + +`tokens/component/providerCard.json` + +| Token path | Value / Reference | Used by | Description | +|-----------|-----------|---------|-------------| +| providerCard.image.height | 180px | ProviderCard | Hero image fixed height | +| providerCard.logo.size | 48px | ProviderCard | Logo circle diameter | +| providerCard.footer.background | → color.brand.100 (#F7ECDF) | ProviderCard | Warm beige footer bar | +| providerCard.footer.paddingX | → spacing.4 (16px) | ProviderCard | Footer horizontal padding | +| providerCard.footer.paddingY | → spacing.3 (12px) | ProviderCard | Footer vertical padding | +| providerCard.content.padding | → spacing.4 (16px) | ProviderCard | Content area padding | +| providerCard.content.gap | → spacing.2 (8px) | ProviderCard | Gap between content rows | diff --git a/src/components/molecules/ProviderCard/ProviderCard.stories.tsx b/src/components/molecules/ProviderCard/ProviderCard.stories.tsx new file mode 100644 index 0000000..57745f7 --- /dev/null +++ b/src/components/molecules/ProviderCard/ProviderCard.stories.tsx @@ -0,0 +1,420 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ProviderCard } from './ProviderCard'; +import Box from '@mui/material/Box'; + +// Placeholder images for stories (grey boxes via data URIs) +const HERO_PLACEHOLDER = + 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=600&h=300&fit=crop&auto=format'; +const HERO_PLACEHOLDER_2 = + 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=600&h=300&fit=crop&auto=format'; +const HERO_PLACEHOLDER_3 = + 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=600&h=300&fit=crop&auto=format'; +// Simple grey circle for logo placeholder +const LOGO_PLACEHOLDER = + 'data:image/svg+xml,' + + encodeURIComponent( + 'Logo', + ); + +const meta: Meta = { + title: 'Molecules/ProviderCard', + component: ProviderCard, + tags: ['autodocs'], + parameters: { + layout: 'centered', + design: { + type: 'figma', + url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=5369-140263', + }, + }, + argTypes: { + name: { control: 'text' }, + location: { control: 'text' }, + verified: { control: 'boolean' }, + rating: { control: { type: 'number', min: 0, max: 5, step: 0.1 } }, + reviewCount: { control: 'number' }, + capabilityLabel: { control: 'text' }, + capabilityColor: { + control: 'select', + options: ['default', 'success', 'warning', 'error', 'info'], + }, + startingPrice: { control: 'number' }, + onClick: { action: 'clicked' }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ──────────────────────────────────────────────────────────────── + +/** Default — verified provider with all fields */ +export const Default: Story = { + args: { + name: 'H.Parsons Funeral Directors', + location: 'Wollongong', + verified: true, + imageUrl: HERO_PLACEHOLDER, + logoUrl: LOGO_PLACEHOLDER, + rating: 4.8, + reviewCount: 127, + capabilityLabel: 'Online Arrangement', + capabilityColor: 'success', + startingPrice: 900, + }, +}; + +// ─── Verified Provider ────────────────────────────────────────────────────── + +/** Full verified provider card with all elements */ +export const VerifiedProvider: Story = { + name: 'Verified Provider', + render: () => ( + {}} + /> + ), +}; + +// ─── Unverified Provider ──────────────────────────────────────────────────── + +/** Unverified provider — text only, no image/logo/trusted badge */ +export const UnverifiedProvider: Story = { + name: 'Unverified Provider', + render: () => ( + {}} + /> + ), +}; + +// ─── List Layout ──────────────────────────────────────────────────────────── + +/** + * Mixed verified and unverified providers in a scrollable list. + * This is the primary use case — text alignment should be consistent + * across both card types for scan readability. + */ +export const ListLayout: Story = { + name: 'List Layout — Mixed', + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + {}} + /> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + + ), +}; + +// ─── Capability Variants ──────────────────────────────────────────────────── + +/** Three capability badge colours */ +export const CapabilityVariants: Story = { + name: 'Capability Variants', + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + {}} + /> + {}} + /> + {}} + /> + + ), +}; + +// ─── Edge Cases ───────────────────────────────────────────────────────────── + +/** Edge cases: long name, missing fields, extremes */ +export const EdgeCases: Story = { + name: 'Edge Cases', + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + {/* Long name — tests maxLines truncation */} + {}} + /> + {/* No reviews */} + {}} + /> + {/* No capability badge */} + {}} + /> + {/* No price (footer hidden) */} + {}} + /> + {/* Minimal — just name and location */} + {}} + /> + + ), +}; + +// ─── Responsive ───────────────────────────────────────────────────────────── + +/** Cards at different viewport widths */ +export const Responsive: Story = { + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + {[280, 340, 420].map((width) => ( + + {width}px + {}} + /> + + ))} + + ), +}; + +// ─── On Different Backgrounds ─────────────────────────────────────────────── + +/** Cards on white vs grey surfaces */ +export const OnDifferentBackgrounds: Story = { + name: 'On Different Backgrounds', + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + + White surface + {}} + /> + + + Grey surface (neutral.50) + {}} + /> + + + ), +}; + +// ─── Interactive Demo ─────────────────────────────────────────────────────── + +/** Click any card — fires onClick in Storybook actions panel */ +export const InteractiveDemo: Story = { + name: 'Interactive — Click to Navigate', + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + alert('Navigate to Parsons Ladies packages')} + /> + alert('Navigate to Rankins packages')} + /> + + ), +}; diff --git a/src/components/molecules/ProviderCard/ProviderCard.tsx b/src/components/molecules/ProviderCard/ProviderCard.tsx new file mode 100644 index 0000000..2ed1c35 --- /dev/null +++ b/src/components/molecules/ProviderCard/ProviderCard.tsx @@ -0,0 +1,260 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import type { SxProps, Theme } from '@mui/material/styles'; +import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; +import StarRoundedIcon from '@mui/icons-material/StarRounded'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; +import { Card } from '../../atoms/Card'; +import { Badge } from '../../atoms/Badge'; +import type { BadgeColor } from '../../atoms/Badge/Badge'; +import { Typography } from '../../atoms/Typography'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA ProviderCard molecule */ +export interface ProviderCardProps { + /** Provider display name */ + name: string; + /** Location text (suburb, city) */ + location: string; + /** Whether this provider is a verified/trusted partner */ + verified?: boolean; + /** Hero image URL — only rendered when verified */ + imageUrl?: string; + /** Provider logo URL — circular overlay on image, only rendered when verified */ + logoUrl?: string; + /** Average rating (e.g. 4.8). Omit to hide reviews. */ + rating?: number; + /** Number of reviews (e.g. 127). Omit to hide review count. */ + reviewCount?: number; + /** Capability badge label (e.g. "Online Arrangement") */ + capabilityLabel?: string; + /** Capability badge colour intent — maps to Badge colour */ + capabilityColor?: BadgeColor; + /** Starting price in dollars (shown in footer as "Packages from $X") */ + startingPrice?: number; + /** Click handler — entire card is clickable */ + onClick?: () => void; + /** MUI sx prop for style overrides */ + sx?: SxProps; +} + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const LOGO_SIZE = 'var(--fa-provider-card-logo-size)'; +const LOGO_OVERLAP = 24; // half of 48px logo, in px +const IMAGE_HEIGHT = 'var(--fa-provider-card-image-height)'; +const CONTENT_PADDING = 'var(--fa-provider-card-content-padding)'; +const CONTENT_GAP = 'var(--fa-provider-card-content-gap)'; +const FOOTER_BG = 'var(--fa-provider-card-footer-background)'; +const FOOTER_PX = 'var(--fa-provider-card-footer-padding-x)'; +const FOOTER_PY = 'var(--fa-provider-card-footer-padding-y)'; + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Provider listing card for the FA design system. + * + * Displays a funeral provider in the provider select screen's scrollable + * list. Supports verified (paid partner) and unverified (scraped listing) + * providers with consistent text alignment for scan readability. + * + * **Verified providers** get a hero image, logo overlay, and "Trusted Partner" + * badge. **Unverified providers** show text content only — no image, logo, + * or verification badge. + * + * Composes: Card (interactive, padding="none"), Badge, Typography. + * + * Usage: + * ```tsx + * navigate(`/providers/parsons`)} + * /> + * ``` + */ +export const ProviderCard = React.forwardRef( + ( + { + name, + location, + verified = false, + imageUrl, + logoUrl, + rating, + reviewCount, + capabilityLabel, + capabilityColor = 'default', + startingPrice, + onClick, + sx, + }, + ref, + ) => { + const showImage = verified && imageUrl; + const showLogo = verified && logoUrl; + + return ( + + {/* ── Image area (verified only) ── */} + {showImage && ( + + {/* Trusted Partner badge */} + + } + > + Trusted Partner + + + + {/* Logo overlay */} + {showLogo && ( + + )} + + )} + + {/* ── Content area ── */} + + {/* Provider name */} + + {name} + + + {/* Meta row: location + reviews */} + + {/* Location */} + + + + {location} + + + + {/* Reviews */} + {rating != null && ( + + + + {rating} + {reviewCount != null && ` (${reviewCount.toLocaleString('en-AU')})`} + + + )} + + + {/* Capability badge */} + {capabilityLabel && ( + + + {capabilityLabel} + + + )} + + + {/* ── Footer bar ── */} + {startingPrice != null && ( + + + Packages from + + + ${startingPrice.toLocaleString('en-AU')} + + + + )} + + ); + }, +); + +ProviderCard.displayName = 'ProviderCard'; +export default ProviderCard; diff --git a/src/components/molecules/ProviderCard/index.ts b/src/components/molecules/ProviderCard/index.ts new file mode 100644 index 0000000..dcea37d --- /dev/null +++ b/src/components/molecules/ProviderCard/index.ts @@ -0,0 +1,2 @@ +export { ProviderCard, default } from './ProviderCard'; +export type { ProviderCardProps } from './ProviderCard'; diff --git a/src/theme/generated/tokens.css b/src/theme/generated/tokens.css index 45fedc4..b3bdb1b 100644 --- a/src/theme/generated/tokens.css +++ b/src/theme/generated/tokens.css @@ -26,6 +26,8 @@ --fa-input-height-sm: 40px; /** Small — compact forms, admin layouts, matches Button medium height */ --fa-input-height-md: 48px; /** Medium (default) — standard forms, matches Button large for alignment */ --fa-input-icon-size-default: 20px; /** 20px — icon size inside input field, matches Figma trailing icon */ + --fa-provider-card-image-height: 180px; /** Fixed image height for consistent card sizing in list layouts */ + --fa-provider-card-logo-size: 48px; /** Logo circle diameter — positioned bottom-left of image, overlapping content area */ --fa-radio-size-default: 20px; /** Default radio size — matches Figma 16px + padding for 44px touch target area */ --fa-radio-dot-size-default: 10px; /** Selected indicator dot — 50% of outer size */ --fa-switch-track-width: 44px; /** Track width — slightly narrower than Figma 52px for better proportion with 44px touch target */ @@ -265,6 +267,11 @@ --fa-input-font-size-default: var(--fa-font-size-base); /** 16px — prevents iOS auto-zoom on focus, matches Figma */ --fa-input-border-radius-default: var(--fa-border-radius-sm); /** 4px — subtle rounding, consistent with Figma design */ --fa-input-gap-default: var(--fa-spacing-2); /** 8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability */ + --fa-provider-card-footer-background: var(--fa-color-brand-100); /** Warm beige footer — brand.100 provides subtle brand warmth */ + --fa-provider-card-footer-padding-x: var(--fa-spacing-4); /** 16px horizontal padding — matches compact card padding */ + --fa-provider-card-footer-padding-y: var(--fa-spacing-3); /** 12px vertical padding — slightly tighter than content area */ + --fa-provider-card-content-padding: var(--fa-spacing-4); /** 16px content padding — compact to maximise text area on listing cards */ + --fa-provider-card-content-gap: var(--fa-spacing-2); /** 8px vertical gap between content rows (name, meta, capability) */ --fa-switch-track-border-radius: var(--fa-border-radius-full); /** Pill shape */ --fa-color-text-primary: var(--fa-color-neutral-800); /** Primary text — body content, headings. Cool charcoal (#2C2E35) for comfortable extended reading */ --fa-color-text-secondary: var(--fa-color-neutral-600); /** Secondary text — helper text, descriptions, metadata, less prominent content */ diff --git a/src/theme/generated/tokens.js b/src/theme/generated/tokens.js index 32523cb..dad18f9 100644 --- a/src/theme/generated/tokens.js +++ b/src/theme/generated/tokens.js @@ -72,6 +72,13 @@ export const InputFontSizeDefault = "1rem"; // 16px — prevents iOS auto-zoom o export const InputBorderRadiusDefault = "4px"; // 4px — subtle rounding, consistent with Figma design export const InputGapDefault = "8px"; // 8px — vertical rhythm between label/input/helper, slightly more generous than Figma's 6px for readability export const InputIconSizeDefault = "20px"; // 20px — icon size inside input field, matches Figma trailing icon +export const ProviderCardImageHeight = "180px"; // Fixed image height for consistent card sizing in list layouts +export const ProviderCardLogoSize = "48px"; // Logo circle diameter — positioned bottom-left of image, overlapping content area +export const ProviderCardFooterBackground = "#f7ecdf"; // Warm beige footer — brand.100 provides subtle brand warmth +export const ProviderCardFooterPaddingX = "16px"; // 16px horizontal padding — matches compact card padding +export const ProviderCardFooterPaddingY = "12px"; // 12px vertical padding — slightly tighter than content area +export const ProviderCardContentPadding = "16px"; // 16px content padding — compact to maximise text area on listing cards +export const ProviderCardContentGap = "8px"; // 8px vertical gap between content rows (name, meta, capability) export const RadioSizeDefault = "20px"; // Default radio size — matches Figma 16px + padding for 44px touch target area export const RadioDotSizeDefault = "10px"; // Selected indicator dot — 50% of outer size export const SwitchTrackWidth = "44px"; // Track width — slightly narrower than Figma 52px for better proportion with 44px touch target diff --git a/tokens/component/providerCard.json b/tokens/component/providerCard.json new file mode 100644 index 0000000..fb348a7 --- /dev/null +++ b/tokens/component/providerCard.json @@ -0,0 +1,46 @@ +{ + "providerCard": { + "$description": "ProviderCard molecule tokens — listing card for funeral providers on the select screen. Supports verified (image + logo + badge) and unverified (text-only) variants.", + "image": { + "$type": "dimension", + "$description": "Hero image area dimensions.", + "height": { "$value": "180px", "$description": "Fixed image height for consistent card sizing in list layouts" } + }, + "logo": { + "$type": "dimension", + "$description": "Provider logo overlay dimensions.", + "size": { "$value": "48px", "$description": "Logo circle diameter — positioned bottom-left of image, overlapping content area" } + }, + "footer": { + "$description": "Footer bar styling — warm beige bar with package pricing.", + "background": { + "$type": "color", + "$value": "{color.brand.100}", + "$description": "Warm beige footer — brand.100 provides subtle brand warmth" + }, + "paddingX": { + "$type": "dimension", + "$value": "{spacing.4}", + "$description": "16px horizontal padding — matches compact card padding" + }, + "paddingY": { + "$type": "dimension", + "$value": "{spacing.3}", + "$description": "12px vertical padding — slightly tighter than content area" + } + }, + "content": { + "$description": "Content area spacing.", + "padding": { + "$type": "dimension", + "$value": "{spacing.4}", + "$description": "16px content padding — compact to maximise text area on listing cards" + }, + "gap": { + "$type": "dimension", + "$value": "{spacing.2}", + "$description": "8px vertical gap between content rows (name, meta, capability)" + } + } + } +}