From 9323b523766995a321830f5d09b15c0684863e95 Mon Sep 17 00:00:00 2001 From: Richie Date: Wed, 25 Mar 2026 19:51:53 +1100 Subject: [PATCH] =?UTF-8?q?Add=20VenueCard=20molecule=20=E2=80=94=20venue?= =?UTF-8?q?=20listing=20card=20for=20service=20venue=20select=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3 component tokens (image.height, content.padding, content.gap) - Composes Card (interactive) + Typography, consistent with ProviderCard patterns - Hero image with role="img" aria-label for screen readers - Meta row: location (pin icon) + capacity with "guests" suffix for clarity - Price with "From" qualifier for transparency (split typography like ProviderCard) - 6 Storybook stories: Default, ListLayout, EdgeCases, Responsive, OnDifferentBackgrounds, InteractiveDemo - Critique score: 33/40 (Good) — P2 fixes applied (capacity label, price context, image a11y) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../molecules/VenueCard/VenueCard.stories.tsx | 281 ++++++++++++++++++ .../molecules/VenueCard/VenueCard.tsx | 163 ++++++++++ src/components/molecules/VenueCard/index.ts | 2 + src/theme/generated/tokens.css | 3 + src/theme/generated/tokens.js | 3 + tokens/component/venueCard.json | 23 ++ 6 files changed, 475 insertions(+) create mode 100644 src/components/molecules/VenueCard/VenueCard.stories.tsx create mode 100644 src/components/molecules/VenueCard/VenueCard.tsx create mode 100644 src/components/molecules/VenueCard/index.ts create mode 100644 tokens/component/venueCard.json diff --git a/src/components/molecules/VenueCard/VenueCard.stories.tsx b/src/components/molecules/VenueCard/VenueCard.stories.tsx new file mode 100644 index 0000000..3f0af08 --- /dev/null +++ b/src/components/molecules/VenueCard/VenueCard.stories.tsx @@ -0,0 +1,281 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { VenueCard } from './VenueCard'; +import Box from '@mui/material/Box'; + +// Venue photo placeholders +const VENUE_CHAPEL = + 'https://images.unsplash.com/photo-1545128485-c400e7702796?w=600&h=300&fit=crop&auto=format'; +const VENUE_GARDEN = + 'https://images.unsplash.com/photo-1510076857177-7470076d4098?w=600&h=300&fit=crop&auto=format'; +const VENUE_HALL = + 'https://images.unsplash.com/photo-1519167758481-83f550bb49b3?w=600&h=300&fit=crop&auto=format'; +const VENUE_CHURCH = + 'https://images.unsplash.com/photo-1438032005730-c779502df39b?w=600&h=300&fit=crop&auto=format'; +const VENUE_BEACH = + 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=600&h=300&fit=crop&auto=format'; + +const meta: Meta = { + title: 'Molecules/VenueCard', + component: VenueCard, + tags: ['autodocs'], + parameters: { + layout: 'centered', + design: { + type: 'figma', + url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=2997-83018', + }, + }, + argTypes: { + name: { control: 'text' }, + location: { control: 'text' }, + imageUrl: { control: 'text' }, + capacity: { control: 'number' }, + price: { control: 'number' }, + onClick: { action: 'clicked' }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ──────────────────────────────────────────────────────────────── + +/** Default — venue with all fields */ +export const Default: Story = { + args: { + name: 'West Chapel', + imageUrl: VENUE_CHAPEL, + location: 'Strathfield', + capacity: 100, + price: 900, + }, +}; + +// ─── List Layout ──────────────────────────────────────────────────────────── + +/** + * Multiple venue cards in a scrollable list on neutral.50 background. + * This is the primary use case — the venue select screen's left panel. + */ +export const ListLayout: Story = { + name: 'List Layout', + decorators: [ + (Story) => ( + + + + ), + ], + render: () => ( + + {}} + /> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + + ), +}; + +// ─── Edge Cases ───────────────────────────────────────────────────────────── + +/** Edge cases: long name, missing optional fields, extremes */ +export const EdgeCases: Story = { + name: 'Edge Cases', + render: () => ( + + {/* Long name — tests maxLines truncation */} + {}} + /> + {/* No capacity */} + {}} + /> + {/* No price */} + {}} + /> + {/* Minimal — just name, image, 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 Select', + render: () => ( + + alert('Selected: West Chapel')} + /> + alert('Selected: Macquarie Park')} + /> + + ), +}; diff --git a/src/components/molecules/VenueCard/VenueCard.tsx b/src/components/molecules/VenueCard/VenueCard.tsx new file mode 100644 index 0000000..6a5b44c --- /dev/null +++ b/src/components/molecules/VenueCard/VenueCard.tsx @@ -0,0 +1,163 @@ +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 PeopleOutlinedIcon from '@mui/icons-material/PeopleOutlined'; +import { Card } from '../../atoms/Card'; +import { Typography } from '../../atoms/Typography'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA VenueCard molecule */ +export interface VenueCardProps { + /** Venue display name */ + name: string; + /** Venue photo URL — always present for venues */ + imageUrl: string; + /** Location text (suburb, city) */ + location: string; + /** Venue capacity (seating/standing) */ + capacity?: number; + /** Venue hire price in dollars */ + price?: number; + /** Click handler — entire card is clickable */ + onClick?: () => void; + /** MUI sx prop for style overrides */ + sx?: SxProps; +} + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const IMAGE_HEIGHT = 'var(--fa-venue-card-image-height)'; +const CONTENT_PADDING = 'var(--fa-venue-card-content-padding)'; +const CONTENT_GAP = 'var(--fa-venue-card-content-gap)'; + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Venue listing card for the FA design system. + * + * Displays a service venue in the venue select screen's scrollable list. + * Venues always have a photo, location, and typically capacity + price. + * Simpler than ProviderCard — no verification tiers, no logo, no + * capability badges. + * + * Composes: Card (interactive, padding="none"), Typography. + * + * Usage: + * ```tsx + * navigate(`/venues/west-chapel`)} + * /> + * ``` + */ +export const VenueCard = React.forwardRef( + ({ name, imageUrl, location, capacity, price, onClick, sx }, ref) => { + return ( + + {/* ── Image area ── */} + + + {/* ── Content area ── */} + + {/* Venue name */} + + {name} + + + {/* Meta row: location + capacity */} + + {/* Location */} + + + + {location} + + + + {/* Capacity */} + {capacity != null && ( + + + + {capacity} guests + + + )} + + + {/* Price */} + {price != null && ( + + + From + + + ${price.toLocaleString('en-AU')} + + + )} + + + ); + }, +); + +VenueCard.displayName = 'VenueCard'; +export default VenueCard; diff --git a/src/components/molecules/VenueCard/index.ts b/src/components/molecules/VenueCard/index.ts new file mode 100644 index 0000000..3a70e5c --- /dev/null +++ b/src/components/molecules/VenueCard/index.ts @@ -0,0 +1,2 @@ +export { VenueCard, default } from './VenueCard'; +export type { VenueCardProps } from './VenueCard'; diff --git a/src/theme/generated/tokens.css b/src/theme/generated/tokens.css index 1257372..ea2fb0b 100644 --- a/src/theme/generated/tokens.css +++ b/src/theme/generated/tokens.css @@ -33,6 +33,7 @@ --fa-switch-track-width: 44px; /** Track width — slightly narrower than Figma 52px for better proportion with 44px touch target */ --fa-switch-track-height: 24px; /** Track height */ --fa-switch-thumb-size: 18px; /** Thumb diameter — sits inside the track with 3px inset */ + --fa-venue-card-image-height: 180px; /** Fixed image height — matches ProviderCard for consistent list layout when both card types appear in search results */ --fa-color-brand-50: #fef9f5; /** Lightest warm tint — warm section backgrounds */ --fa-color-brand-100: #f7ecdf; /** Light warm — hover backgrounds, subtle fills */ --fa-color-brand-200: #ebdac8; /** Warm light — secondary backgrounds, divider tones */ @@ -271,6 +272,8 @@ --fa-provider-card-content-padding: var(--fa-spacing-3); /** 12px content padding — tight to keep card compact in listing layout */ --fa-provider-card-content-gap: var(--fa-spacing-1); /** 4px vertical gap between content rows — tight for compact listing cards */ --fa-switch-track-border-radius: var(--fa-border-radius-full); /** Pill shape */ + --fa-venue-card-content-padding: var(--fa-spacing-3); /** 12px content padding — matches ProviderCard for visual consistency */ + --fa-venue-card-content-gap: var(--fa-spacing-1); /** 4px vertical gap between content rows — tight for compact listing cards */ --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 */ --fa-color-text-tertiary: var(--fa-color-neutral-500); /** Tertiary text — placeholders, timestamps, attribution, meta information */ diff --git a/src/theme/generated/tokens.js b/src/theme/generated/tokens.js index 0c3dfd1..988c527 100644 --- a/src/theme/generated/tokens.js +++ b/src/theme/generated/tokens.js @@ -83,6 +83,9 @@ export const SwitchTrackWidth = "44px"; // Track width — slightly narrower tha export const SwitchTrackHeight = "24px"; // Track height export const SwitchTrackBorderRadius = "9999px"; // Pill shape export const SwitchThumbSize = "18px"; // Thumb diameter — sits inside the track with 3px inset +export const VenueCardImageHeight = "180px"; // Fixed image height — matches ProviderCard for consistent list layout when both card types appear in search results +export const VenueCardContentPadding = "12px"; // 12px content padding — matches ProviderCard for visual consistency +export const VenueCardContentGap = "4px"; // 4px vertical gap between content rows — tight for compact listing cards export const ColorBrand50 = "#fef9f5"; // Lightest warm tint — warm section backgrounds export const ColorBrand100 = "#f7ecdf"; // Light warm — hover backgrounds, subtle fills export const ColorBrand200 = "#ebdac8"; // Warm light — secondary backgrounds, divider tones diff --git a/tokens/component/venueCard.json b/tokens/component/venueCard.json new file mode 100644 index 0000000..896676c --- /dev/null +++ b/tokens/component/venueCard.json @@ -0,0 +1,23 @@ +{ + "venueCard": { + "$description": "VenueCard molecule tokens — listing card for service venues on the venue select screen. Always has a photo, location, capacity, and price. Simpler than ProviderCard — no verification tiers or logo.", + "image": { + "$type": "dimension", + "$description": "Hero image area dimensions.", + "height": { "$value": "180px", "$description": "Fixed image height — matches ProviderCard for consistent list layout when both card types appear in search results" } + }, + "content": { + "$description": "Content area spacing.", + "padding": { + "$type": "dimension", + "$value": "{spacing.3}", + "$description": "12px content padding — matches ProviderCard for visual consistency" + }, + "gap": { + "$type": "dimension", + "$value": "{spacing.1}", + "$description": "4px vertical gap between content rows — tight for compact listing cards" + } + } + } +}