Add ProviderCard molecule — first molecule in design system
First molecule component. Listing card for funeral providers on the provider select screen (map + scrollable list layout). - Verified providers: hero image, 48px logo overlay, "Trusted Partner" badge, name, location, reviews, capability badge, footer with price - Unverified providers: text-only with same content alignment - 7 component tokens (image height, logo size, footer/content spacing) - Composes Card (interactive, padding="none") + Badge + Typography - Capability badges accept arbitrary label + colour (categories may change) - Footer bar with warm brand.100 bg, "Packages from $X >" - 9 Storybook stories including mixed list layout demo - Decisions D026-D030 logged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,9 @@ duplicates) and MUST update it after completing one.
|
|||||||
| Component | Status | Composed of | Notes |
|
| Component | Status | Composed of | Notes |
|
||||||
|-----------|--------|-------------|-------|
|
|-----------|--------|-------------|-------|
|
||||||
| FormField | planned | Input + Typography (label) + Typography (helper) | Standard form field with label and validation |
|
| 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 |
|
| ServiceOption | planned | Card + Typography + Chip + Icon | Selectable service item |
|
||||||
| SearchBar | planned | Input + Icon + Button | Search with submit |
|
| SearchBar | planned | Input + Icon + Button | Search with submit |
|
||||||
| StepIndicator | planned | Typography + Badge + Divider | Multi-step flow progress |
|
| StepIndicator | planned | Typography + Badge + Divider | Multi-step flow progress |
|
||||||
|
|||||||
@@ -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`.
|
**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
|
**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).
|
**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 `<img>`.
|
||||||
|
**Rationale:** Simpler API for the common case (CDN URLs). CSS background-image handles aspect ratio cropping automatically. `<img>` 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).
|
||||||
|
|||||||
@@ -496,6 +496,48 @@ Each entry follows this structure:
|
|||||||
- **Planned (5 organisms):** ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer
|
- **Planned (5 organisms):** ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer
|
||||||
|
|
||||||
**Next steps:**
|
**Next steps:**
|
||||||
- User to review Switch and Radio in Storybook
|
- ~~User to review Switch and Radio in Storybook~~ ✓ Approved
|
||||||
- Begin PriceCard molecule
|
- ~~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 (<img>)
|
||||||
|
- 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
|
- Address P2 audit issues in a future cleanup pass
|
||||||
|
|||||||
@@ -264,3 +264,17 @@ the correct token for any design property.
|
|||||||
|-----------|-----------|---------|-------------|
|
|-----------|-----------|---------|-------------|
|
||||||
| radio.size.default | 20px | Radio | Outer circle size |
|
| radio.size.default | 20px | Radio | Outer circle size |
|
||||||
| radio.dotSize.default | 10px | Radio | Inner selected dot 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 |
|
||||||
|
|||||||
420
src/components/molecules/ProviderCard/ProviderCard.stories.tsx
Normal file
420
src/components/molecules/ProviderCard/ProviderCard.stories.tsx
Normal file
@@ -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(
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96"><circle cx="48" cy="48" r="48" fill="%23E8E8E8"/><text x="48" y="54" text-anchor="middle" font-family="sans-serif" font-size="14" fill="%23737373">Logo</text></svg>',
|
||||||
|
);
|
||||||
|
|
||||||
|
const meta: Meta<typeof ProviderCard> = {
|
||||||
|
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) => (
|
||||||
|
<Box sx={{ width: 380 }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ProviderCard>;
|
||||||
|
|
||||||
|
// ─── 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: () => (
|
||||||
|
<ProviderCard
|
||||||
|
name="Parsons Ladies Funeral Directors"
|
||||||
|
location="Wollongong"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.8}
|
||||||
|
reviewCount={127}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={900}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Unverified Provider ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Unverified provider — text only, no image/logo/trusted badge */
|
||||||
|
export const UnverifiedProvider: Story = {
|
||||||
|
name: 'Unverified Provider',
|
||||||
|
render: () => (
|
||||||
|
<ProviderCard
|
||||||
|
name="Rankins Funeral's Heathcote"
|
||||||
|
location="Heathcote"
|
||||||
|
rating={4.2}
|
||||||
|
reviewCount={43}
|
||||||
|
capabilityLabel="Outsourced"
|
||||||
|
capabilityColor="default"
|
||||||
|
startingPrice={1200}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── 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) => (
|
||||||
|
<Box sx={{ width: 400, maxHeight: 700, overflow: 'auto' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<ProviderCard
|
||||||
|
name="Parsons Ladies Funeral Directors"
|
||||||
|
location="Wollongong"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.8}
|
||||||
|
reviewCount={127}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={900}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="H.Parsons Wollongong"
|
||||||
|
location="Wollongong"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER_2}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.6}
|
||||||
|
reviewCount={89}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={1100}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="Rankins Funeral's Heathcote"
|
||||||
|
location="Heathcote"
|
||||||
|
rating={4.2}
|
||||||
|
reviewCount={43}
|
||||||
|
capabilityLabel="Outsourced"
|
||||||
|
capabilityColor="default"
|
||||||
|
startingPrice={1200}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="Stella Armois Funerals"
|
||||||
|
location="Corrimal"
|
||||||
|
rating={3.9}
|
||||||
|
reviewCount={18}
|
||||||
|
capabilityLabel="Partial Arrangement"
|
||||||
|
capabilityColor="warning"
|
||||||
|
startingPrice={950}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="St. Angelo of God Funeral Directors"
|
||||||
|
location="Fairy Meadow"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER_3}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.9}
|
||||||
|
reviewCount={203}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={1500}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Capability Variants ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Three capability badge colours */
|
||||||
|
export const CapabilityVariants: Story = {
|
||||||
|
name: 'Capability Variants',
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ width: 380 }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<ProviderCard
|
||||||
|
name="Online Provider"
|
||||||
|
location="Sydney"
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={900}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="Partial Provider"
|
||||||
|
location="Melbourne"
|
||||||
|
capabilityLabel="Partial Arrangement"
|
||||||
|
capabilityColor="warning"
|
||||||
|
startingPrice={1100}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="Outsourced Provider"
|
||||||
|
location="Brisbane"
|
||||||
|
capabilityLabel="Outsourced"
|
||||||
|
capabilityColor="default"
|
||||||
|
startingPrice={800}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Edge Cases ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Edge cases: long name, missing fields, extremes */
|
||||||
|
export const EdgeCases: Story = {
|
||||||
|
name: 'Edge Cases',
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ width: 380 }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
{/* Long name — tests maxLines truncation */}
|
||||||
|
<ProviderCard
|
||||||
|
name="The Most Honourable and Distinguished Funeral Directors of the Greater Wollongong Metropolitan Area"
|
||||||
|
location="Wollongong"
|
||||||
|
rating={4.5}
|
||||||
|
reviewCount={67}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={2500}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
{/* No reviews */}
|
||||||
|
<ProviderCard
|
||||||
|
name="New Provider"
|
||||||
|
location="Perth"
|
||||||
|
capabilityLabel="Partial Arrangement"
|
||||||
|
capabilityColor="warning"
|
||||||
|
startingPrice={750}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
{/* No capability badge */}
|
||||||
|
<ProviderCard
|
||||||
|
name="Basic Listing"
|
||||||
|
location="Adelaide"
|
||||||
|
rating={3.2}
|
||||||
|
reviewCount={5}
|
||||||
|
startingPrice={600}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
{/* No price (footer hidden) */}
|
||||||
|
<ProviderCard
|
||||||
|
name="Price Unavailable"
|
||||||
|
location="Darwin"
|
||||||
|
rating={4.0}
|
||||||
|
reviewCount={12}
|
||||||
|
capabilityLabel="Outsourced"
|
||||||
|
capabilityColor="default"
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
{/* Minimal — just name and location */}
|
||||||
|
<ProviderCard
|
||||||
|
name="Minimal Card"
|
||||||
|
location="Hobart"
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Responsive ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Cards at different viewport widths */
|
||||||
|
export const Responsive: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ width: 'auto' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', gap: 3, alignItems: 'start', flexWrap: 'wrap' }}>
|
||||||
|
{[280, 340, 420].map((width) => (
|
||||||
|
<Box key={width} sx={{ width }}>
|
||||||
|
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>{width}px</Box>
|
||||||
|
<ProviderCard
|
||||||
|
name="H.Parsons Funeral Directors"
|
||||||
|
location="Wollongong"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.8}
|
||||||
|
reviewCount={127}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={900}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── On Different Backgrounds ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Cards on white vs grey surfaces */
|
||||||
|
export const OnDifferentBackgrounds: Story = {
|
||||||
|
name: 'On Different Backgrounds',
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ width: 'auto' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||||
|
<Box sx={{ width: 360, p: 3, backgroundColor: 'background.default' }}>
|
||||||
|
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>White surface</Box>
|
||||||
|
<ProviderCard
|
||||||
|
name="H.Parsons"
|
||||||
|
location="Wollongong"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.8}
|
||||||
|
reviewCount={127}
|
||||||
|
startingPrice={900}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ width: 360, p: 3, backgroundColor: 'var(--fa-color-surface-subtle)' }}>
|
||||||
|
<Box sx={{ mb: 1, fontSize: 12, color: 'text.secondary' }}>Grey surface (neutral.50)</Box>
|
||||||
|
<ProviderCard
|
||||||
|
name="Rankins Funerals"
|
||||||
|
location="Heathcote"
|
||||||
|
rating={4.2}
|
||||||
|
reviewCount={43}
|
||||||
|
capabilityLabel="Outsourced"
|
||||||
|
capabilityColor="default"
|
||||||
|
startingPrice={1200}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Interactive Demo ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Click any card — fires onClick in Storybook actions panel */
|
||||||
|
export const InteractiveDemo: Story = {
|
||||||
|
name: 'Interactive — Click to Navigate',
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ width: 380 }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<ProviderCard
|
||||||
|
name="Parsons Ladies Funeral Directors"
|
||||||
|
location="Wollongong"
|
||||||
|
verified
|
||||||
|
imageUrl={HERO_PLACEHOLDER}
|
||||||
|
logoUrl={LOGO_PLACEHOLDER}
|
||||||
|
rating={4.8}
|
||||||
|
reviewCount={127}
|
||||||
|
capabilityLabel="Online Arrangement"
|
||||||
|
capabilityColor="success"
|
||||||
|
startingPrice={900}
|
||||||
|
onClick={() => alert('Navigate to Parsons Ladies packages')}
|
||||||
|
/>
|
||||||
|
<ProviderCard
|
||||||
|
name="Rankins Funeral's Heathcote"
|
||||||
|
location="Heathcote"
|
||||||
|
rating={4.2}
|
||||||
|
reviewCount={43}
|
||||||
|
capabilityLabel="Outsourced"
|
||||||
|
capabilityColor="default"
|
||||||
|
startingPrice={1200}
|
||||||
|
onClick={() => alert('Navigate to Rankins packages')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
260
src/components/molecules/ProviderCard/ProviderCard.tsx
Normal file
260
src/components/molecules/ProviderCard/ProviderCard.tsx
Normal file
@@ -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<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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
|
||||||
|
* <ProviderCard
|
||||||
|
* name="H.Parsons Funeral Directors"
|
||||||
|
* location="Wollongong"
|
||||||
|
* verified
|
||||||
|
* imageUrl="/images/parsons-hero.jpg"
|
||||||
|
* logoUrl="/images/parsons-logo.png"
|
||||||
|
* rating={4.8}
|
||||||
|
* reviewCount={127}
|
||||||
|
* capabilityLabel="Online Arrangement"
|
||||||
|
* capabilityColor="success"
|
||||||
|
* startingPrice={900}
|
||||||
|
* onClick={() => navigate(`/providers/parsons`)}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
location,
|
||||||
|
verified = false,
|
||||||
|
imageUrl,
|
||||||
|
logoUrl,
|
||||||
|
rating,
|
||||||
|
reviewCount,
|
||||||
|
capabilityLabel,
|
||||||
|
capabilityColor = 'default',
|
||||||
|
startingPrice,
|
||||||
|
onClick,
|
||||||
|
sx,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const showImage = verified && imageUrl;
|
||||||
|
const showLogo = verified && logoUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
interactive
|
||||||
|
padding="none"
|
||||||
|
onClick={onClick}
|
||||||
|
sx={[
|
||||||
|
{ overflow: 'hidden' },
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* ── Image area (verified only) ── */}
|
||||||
|
{showImage && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
height: IMAGE_HEIGHT,
|
||||||
|
backgroundImage: `url(${imageUrl})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundColor: 'action.hover', // fallback if image fails
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Trusted Partner badge */}
|
||||||
|
<Box sx={{ position: 'absolute', top: 12, right: 12 }}>
|
||||||
|
<Badge
|
||||||
|
variant="filled"
|
||||||
|
color="brand"
|
||||||
|
size="small"
|
||||||
|
icon={<VerifiedOutlinedIcon />}
|
||||||
|
>
|
||||||
|
Trusted Partner
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Logo overlay */}
|
||||||
|
{showLogo && (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={logoUrl}
|
||||||
|
alt={`${name} logo`}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -LOGO_OVERLAP,
|
||||||
|
left: CONTENT_PADDING,
|
||||||
|
width: LOGO_SIZE,
|
||||||
|
height: LOGO_SIZE,
|
||||||
|
borderRadius: '50%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
boxShadow: 1,
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: 'background.paper',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Content area ── */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: CONTENT_GAP,
|
||||||
|
p: CONTENT_PADDING,
|
||||||
|
// Extra top padding when logo overlaps into content
|
||||||
|
...(showLogo && { pt: `calc(${CONTENT_PADDING} + ${LOGO_OVERLAP}px)` }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Provider name */}
|
||||||
|
<Typography variant="h5" maxLines={1}>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Meta row: location + reviews */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Location */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<LocationOnOutlinedIcon
|
||||||
|
sx={{ fontSize: 16, color: 'text.secondary' }}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{location}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Reviews */}
|
||||||
|
{rating != null && (
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
||||||
|
aria-label={`Rated ${rating} out of 5${reviewCount != null ? `, ${reviewCount} reviews` : ''}`}
|
||||||
|
>
|
||||||
|
<StarRoundedIcon
|
||||||
|
sx={{ fontSize: 16, color: 'warning.main' }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{rating}
|
||||||
|
{reviewCount != null && ` (${reviewCount.toLocaleString('en-AU')})`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Capability badge */}
|
||||||
|
{capabilityLabel && (
|
||||||
|
<Box>
|
||||||
|
<Badge color={capabilityColor} size="small">
|
||||||
|
{capabilityLabel}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ── Footer bar ── */}
|
||||||
|
{startingPrice != null && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 0.5,
|
||||||
|
backgroundColor: FOOTER_BG,
|
||||||
|
px: FOOTER_PX,
|
||||||
|
py: FOOTER_PY,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="label" color="text.secondary">
|
||||||
|
Packages from
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="label" sx={{ fontWeight: 700 }}>
|
||||||
|
${startingPrice.toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
<ChevronRightIcon
|
||||||
|
sx={{ fontSize: 20, color: 'text.secondary' }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ProviderCard.displayName = 'ProviderCard';
|
||||||
|
export default ProviderCard;
|
||||||
2
src/components/molecules/ProviderCard/index.ts
Normal file
2
src/components/molecules/ProviderCard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ProviderCard, default } from './ProviderCard';
|
||||||
|
export type { ProviderCardProps } from './ProviderCard';
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
--fa-input-height-sm: 40px; /** Small — compact forms, admin layouts, matches Button medium height */
|
--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-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-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-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-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 */
|
--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-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-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-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-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-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-secondary: var(--fa-color-neutral-600); /** Secondary text — helper text, descriptions, metadata, less prominent content */
|
||||||
|
|||||||
@@ -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 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 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 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 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 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
|
export const SwitchTrackWidth = "44px"; // Track width — slightly narrower than Figma 52px for better proportion with 44px touch target
|
||||||
|
|||||||
46
tokens/component/providerCard.json
Normal file
46
tokens/component/providerCard.json
Normal file
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user