From 34cd87c85aa81a343eedb58a9d6dc0140a199463 Mon Sep 17 00:00:00 2001 From: Richie Date: Tue, 31 Mar 2026 10:36:23 +1100 Subject: [PATCH] CoffinsStep: rewrite to grid-sidebar ecommerce layout - Switch from wide-form to grid-sidebar (viewport-locked, independent scroll) - Sidebar: heading, allowance info bubble (conditional), category menu with expandable subcategories, dual-knob price slider with editable inputs, sort by dropdown, clear all filters, save-and-exit link - Grid: coffin cards with thumbnail hover preview, equal-height cards, subtle bg for white-bg product photos, colour count, Most Popular badge - Card click navigates to CoffinDetailsStep (no Continue button) - 20 coffins per page max before pagination - WizardLayout grid-sidebar: wider sidebar (28%), overflowX hidden, vertical scroll at all breakpoints, viewport-lock on desktop only Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pages/CoffinsStep/CoffinsStep.stories.tsx | 117 +-- .../pages/CoffinsStep/CoffinsStep.tsx | 717 ++++++++++++------ src/components/pages/CoffinsStep/index.ts | 3 +- .../templates/WizardLayout/WizardLayout.tsx | 69 +- 4 files changed, 600 insertions(+), 306 deletions(-) diff --git a/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx b/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx index 763908a..9c0adeb 100644 --- a/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx +++ b/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { CoffinsStep } from './CoffinsStep'; -import type { CoffinsStepValues, CoffinsStepErrors, Coffin } from './CoffinsStep'; +import type { CoffinsStepValues, Coffin } from './CoffinsStep'; import { Navigation } from '../../organisms/Navigation'; import Box from '@mui/material/Box'; @@ -34,21 +34,29 @@ const nav = ( /> ); +// Unsplash images used as stand-ins for coffin product photos const sampleCoffins: Coffin[] = [ { id: 'cedar-classic', name: 'Cedar Classic', imageUrl: 'https://images.unsplash.com/photo-1618220179428-22790b461013?w=400&h=300&fit=crop', + thumbnails: [ + 'https://images.unsplash.com/photo-1555041469-a586c1029ad9?w=80&h=60&fit=crop', + 'https://images.unsplash.com/photo-1449247709967-d4461a6a6103?w=80&h=60&fit=crop', + ], price: 2800, category: 'Solid Timber', isPopular: true, + colourCount: 3, }, { id: 'oak-heritage', name: 'Oak Heritage', imageUrl: 'https://images.unsplash.com/photo-1555041469-a586c1029ad9?w=400&h=300&fit=crop', + thumbnails: ['https://images.unsplash.com/photo-1618220179428-22790b461013?w=80&h=60&fit=crop'], price: 3500, category: 'Solid Timber', + colourCount: 2, }, { id: 'eco-willow', @@ -62,15 +70,23 @@ const sampleCoffins: Coffin[] = [ id: 'maple-serenity', name: 'Maple Serenity', imageUrl: 'https://images.unsplash.com/photo-1449247709967-d4461a6a6103?w=400&h=300&fit=crop', + thumbnails: [ + 'https://images.unsplash.com/photo-1490750967868-88aa4f44baee?w=80&h=60&fit=crop', + 'https://images.unsplash.com/photo-1432462770865-65b70566d673?w=80&h=60&fit=crop', + 'https://images.unsplash.com/photo-1618220179428-22790b461013?w=80&h=60&fit=crop', + ], price: 4200, category: 'Solid Timber', + colourCount: 4, }, { id: 'custom-rose', name: 'Custom Rose Garden', imageUrl: 'https://images.unsplash.com/photo-1490750967868-88aa4f44baee?w=400&h=300&fit=crop', + thumbnails: ['https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=80&h=60&fit=crop'], price: 1800, category: 'Custom Board', + colourCount: 2, }, { id: 'bronze-eternal', @@ -82,9 +98,9 @@ const sampleCoffins: Coffin[] = [ ]; const defaultValues: CoffinsStepValues = { - selectedCoffinId: null, categoryFilter: 'all', - priceFilter: 'all', + priceRange: [0, 15000], + sortBy: 'popularity', page: 1, }; @@ -104,56 +120,22 @@ type Story = StoryObj; // ─── Interactive (default) ────────────────────────────────────────────────── -/** Full catalogue with filters */ +/** Grid-sidebar layout with filters, subcategories, price slider, and thumbnails */ export const Default: Story = { render: () => { const [values, setValues] = useState({ ...defaultValues }); - const [errors, setErrors] = useState({}); - const handleContinue = () => { - if (!values.selectedCoffinId) { - setErrors({ selectedCoffinId: 'Please choose a coffin to continue.' }); - return; - } - alert(`Selected: ${values.selectedCoffinId}`); - }; - - return ( - { - setValues(v); - setErrors({}); - }} - onContinue={handleContinue} - onBack={() => alert('Back')} - onSaveAndExit={() => alert('Save')} - errors={errors} - coffins={sampleCoffins} - totalCount={19} - totalPages={2} - navigation={nav} - /> - ); - }, -}; - -// ─── Coffin selected ──────────────────────────────────────────────────────── - -/** A coffin is already selected */ -export const CoffinSelected: Story = { - render: () => { - const [values, setValues] = useState({ - ...defaultValues, - selectedCoffinId: 'cedar-classic', - }); return ( alert('Continue')} + onSelectCoffin={(id) => alert(`Navigate to details: ${id}`)} onBack={() => alert('Back')} + onSaveAndExit={() => alert('Save')} coffins={sampleCoffins} + totalCount={19} + totalPages={2} + allowanceAmount={500} navigation={nav} /> ); @@ -162,7 +144,7 @@ export const CoffinSelected: Story = { // ─── Pre-planning ─────────────────────────────────────────────────────────── -/** Pre-planning variant */ +/** Pre-planning variant (no allowance — browsing only) */ export const PrePlanning: Story = { render: () => { const [values, setValues] = useState({ ...defaultValues }); @@ -170,7 +152,7 @@ export const PrePlanning: Story = { alert('Continue')} + onSelectCoffin={(id) => alert(`Navigate to details: ${id}`)} onBack={() => alert('Back')} coffins={sampleCoffins} isPrePlanning @@ -180,19 +162,50 @@ export const PrePlanning: Story = { }, }; -// ─── Validation error ─────────────────────────────────────────────────────── +// ─── With subcategory selected ────────────────────────────────────────────── -/** No coffin selected with error */ -export const WithError: Story = { +/** Subcategory selected — parent expanded, child highlighted */ +export const SubcategorySelected: Story = { render: () => { - const [values, setValues] = useState({ ...defaultValues }); + const [values, setValues] = useState({ + ...defaultValues, + categoryFilter: 'cedar', + priceRange: [2000, 5000], + }); return ( {}} - errors={{ selectedCoffinId: 'Please choose a coffin to continue.' }} - coffins={sampleCoffins} + onSelectCoffin={(id) => alert(`Navigate to details: ${id}`)} + onBack={() => alert('Back')} + coffins={sampleCoffins.filter((c) => c.category === 'Solid Timber')} + totalCount={3} + allowanceAmount={500} + navigation={nav} + /> + ); + }, +}; + +// ─── Empty results ────────────────────────────────────────────────────────── + +/** No coffins match filters */ +export const EmptyResults: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + categoryFilter: 'protective_metal', + priceRange: [0, 1000], + }); + return ( + alert(`Navigate to details: ${id}`)} + onBack={() => alert('Back')} + coffins={[]} + totalCount={0} + allowanceAmount={500} navigation={nav} /> ); diff --git a/src/components/pages/CoffinsStep/CoffinsStep.tsx b/src/components/pages/CoffinsStep/CoffinsStep.tsx index 138ef55..0ff2655 100644 --- a/src/components/pages/CoffinsStep/CoffinsStep.tsx +++ b/src/components/pages/CoffinsStep/CoffinsStep.tsx @@ -2,15 +2,20 @@ import React from 'react'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import MenuItem from '@mui/material/MenuItem'; +import Paper from '@mui/material/Paper'; +import Slider from '@mui/material/Slider'; import Pagination from '@mui/material/Pagination'; +import InputAdornment from '@mui/material/InputAdornment'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import type { SxProps, Theme } from '@mui/material/styles'; import { WizardLayout } from '../../templates/WizardLayout'; -import { FilterPanel } from '../../molecules/FilterPanel'; import { Card } from '../../atoms/Card'; import { Badge } from '../../atoms/Badge'; +import { Collapse } from '../../atoms/Collapse'; import { Typography } from '../../atoms/Typography'; -import { Button } from '../../atoms/Button'; import { Divider } from '../../atoms/Divider'; +import { Link } from '../../atoms/Link'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -19,57 +24,51 @@ export interface Coffin { id: string; name: string; imageUrl: string; + /** Additional images (handles, fixtures, interior) shown as thumbnails */ + thumbnails?: string[]; price: number; category: string; isPopular?: boolean; + /** Number of colour/finish variants available */ + colourCount?: number; } -/** Filter category option */ +/** Filter category option (supports one level of subcategories) */ export interface CoffinCategory { value: string; label: string; + /** Subcategories — shown when parent is selected */ + children?: CoffinCategory[]; } -/** Price range filter option */ -export interface CoffinPriceRange { - value: string; - label: string; -} +/** Sort direction */ +export type CoffinSortBy = 'popularity' | 'price_asc' | 'price_desc'; /** Form values for the coffins step */ export interface CoffinsStepValues { - /** Selected coffin ID */ - selectedCoffinId: string | null; /** Active category filter */ categoryFilter: string; - /** Active price filter */ - priceFilter: string; + /** Price range [min, max] for slider filter */ + priceRange: [number, number]; + /** Sort order */ + sortBy: CoffinSortBy; /** Current page (1-indexed) */ page: number; } -/** Field-level error messages */ -export interface CoffinsStepErrors { - selectedCoffinId?: string; -} - /** Props for the CoffinsStep page component */ export interface CoffinsStepProps { - /** Current form values */ + /** Current filter/sort values */ values: CoffinsStepValues; - /** Callback when any field value changes */ + /** Callback when any filter/sort value changes */ onChange: (values: CoffinsStepValues) => void; - /** Callback when the Continue button is clicked */ - onContinue: () => void; + /** Called when a coffin card is clicked — navigates to CoffinDetailsStep */ + onSelectCoffin: (coffinId: string) => void; /** Callback for back navigation */ onBack?: () => void; /** Callback for save-and-exit */ onSaveAndExit?: () => void; - /** Field-level validation errors */ - errors?: CoffinsStepErrors; - /** Whether the Continue button is in a loading state */ - loading?: boolean; - /** Available coffins (already filtered by parent) */ + /** Available coffins (already filtered/sorted/paginated by parent) */ coffins: Coffin[]; /** Total count before pagination (for display) */ totalCount?: number; @@ -77,8 +76,14 @@ export interface CoffinsStepProps { totalPages?: number; /** Category filter options */ categories?: CoffinCategory[]; - /** Price range filter options */ - priceRanges?: CoffinPriceRange[]; + /** Min price for the range slider */ + minPrice?: number; + /** Max price for the range slider */ + maxPrice?: number; + /** Package coffin allowance amount (shown in info bubble, omit if no allowance) */ + allowanceAmount?: number; + /** Max coffins per page before pagination (default 20) */ + pageSize?: number; /** Whether this is a pre-planning flow */ isPrePlanning?: boolean; /** Navigation bar */ @@ -93,48 +98,197 @@ export interface CoffinsStepProps { sx?: SxProps; } +// ─── Category menu item style ─────────────────────────────────────────────── + +const categoryItemSx = (isActive: boolean): SxProps => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + textAlign: 'left', + border: 'none', + background: 'none', + cursor: 'pointer', + py: 0.75, + px: 1.5, + borderRadius: 1, + fontSize: '0.875rem', + fontWeight: isActive ? 600 : 400, + color: isActive ? 'primary.main' : 'text.primary', + bgcolor: isActive ? 'var(--fa-color-brand-50)' : 'transparent', + '&:hover': { + bgcolor: isActive ? 'var(--fa-color-brand-50)' : 'var(--fa-color-surface-subtle)', + }, + fontFamily: 'inherit', +}); + +// ─── Coffin card with thumbnail hover ─────────────────────────────────────── + +const CoffinCard: React.FC<{ + coffin: Coffin; + onClick: () => void; +}> = ({ coffin, onClick }) => { + const [previewImage, setPreviewImage] = React.useState(coffin.imageUrl); + + const allImages = [coffin.imageUrl, ...(coffin.thumbnails || [])]; + const hasThumbnails = allImages.length > 1; + + return ( + + {/* Main image on subtle background — handles white-bg product photos */} + + + {coffin.isPopular && ( + + + Most Popular + + + )} + + + {/* Thumbnail strip — hover swaps main preview */} + {hasThumbnails && ( + setPreviewImage(coffin.imageUrl)} + > + {allImages.map((img, i) => ( + setPreviewImage(img)} + sx={{ + width: 48, + height: 36, + objectFit: 'cover', + borderRadius: 0.5, + border: 2, + borderColor: previewImage === img ? 'primary.main' : 'transparent', + opacity: previewImage === img ? 1 : 0.7, + transition: 'border-color 0.15s, opacity 0.15s', + '&:hover': { opacity: 1 }, + }} + /> + ))} + + )} + + {/* Content — flex: 1 ensures all cards are the same height */} + + + {coffin.name} + + + ${coffin.price.toLocaleString('en-AU')} + + {coffin.colourCount != null && coffin.colourCount > 0 && ( + + {coffin.colourCount} Colour option{coffin.colourCount !== 1 ? 's' : ''} available + + )} + + + ); +}; + // ─── Component ─────────────────────────────────────────────────────────────── /** - * Step 10 — Coffin selection for the FA arrangement wizard. + * Step 10 — Coffin browsing for the FA arrangement wizard. * - * Grid-sidebar layout: filter sidebar (left/top on mobile) + coffin - * card grid (main area). Cards show image, name, price, and optional - * "Most Popular" badge (Rec #10). Pagination at the bottom. + * Grid-sidebar layout: filter sidebar (left, stacked on mobile) + coffin + * card grid (right). Both panels scroll independently on desktop; sidebar + * scrollbar appears on hover. * - * Uses Australian terminology: "coffin" not "casket" (both types may - * appear in the catalogue — the product type is shown per card). + * Clicking a coffin card navigates to CoffinDetailsStep — no Continue + * button. Sidebar contains heading, allowance info, category menu with + * expandable subcategories, price range slider with editable inputs, + * sort control, and save-and-exit link. + * + * Cards show product image on subtle background (for white-bg photos), + * thumbnail strip with hover preview, name, price, and colour count. + * All cards are equal height regardless of thumbnail/colour presence. + * + * Uses Australian terminology: "coffin" not "casket". * * Pure presentation component — props in, callbacks out. - * Filtering and pagination logic handled by parent. + * Filtering, sorting, and pagination logic handled by parent. * * Spec: documentation/steps/steps/10_coffins.yaml */ export const CoffinsStep: React.FC = ({ values, onChange, - onContinue, + onSelectCoffin, onBack, onSaveAndExit, - errors, - loading = false, coffins, totalCount, totalPages = 1, categories = [ - { value: 'all', label: 'All categories' }, - { value: 'solid_timber', label: 'Solid Timber' }, - { value: 'custom_board', label: 'Custom Board' }, + { value: 'all', label: 'All' }, + { + value: 'solid_timber', + label: 'Solid Timber', + children: [ + { value: 'cedar', label: 'Cedar' }, + { value: 'oak', label: 'Oak' }, + { value: 'mahogany', label: 'Mahogany' }, + { value: 'teak', label: 'Teak' }, + ], + }, + { + value: 'custom_board', + label: 'Custom Board', + children: [ + { value: 'printed', label: 'Printed' }, + { value: 'painted', label: 'Painted' }, + ], + }, { value: 'environmental', label: 'Environmental' }, { value: 'designer', label: 'Designer' }, { value: 'protective_metal', label: 'Protective Metal' }, ], - priceRanges = [ - { value: 'all', label: 'All prices' }, - { value: 'under_2000', label: 'Under $2,000' }, - { value: '2000_4000', label: '$2,000 – $4,000' }, - { value: 'over_4000', label: 'Over $4,000' }, - ], + minPrice = 0, + maxPrice = 15000, + allowanceAmount, + pageSize = 20, isPrePlanning = false, navigation, progressStepper, @@ -142,21 +296,63 @@ export const CoffinsStep: React.FC = ({ hideHelpBar, sx, }) => { + // Cap displayed coffins to pageSize (default 20 per page) + const visibleCoffins = coffins.slice(0, pageSize); const displayCount = totalCount ?? coffins.length; - const activeFilterCount = - (values.categoryFilter !== 'all' ? 1 : 0) + (values.priceFilter !== 'all' ? 1 : 0); + const derivedTotalPages = totalPages > 1 ? totalPages : Math.ceil(coffins.length / pageSize); - const handleFilterChange = (field: 'categoryFilter' | 'priceFilter', value: string) => { - onChange({ ...values, [field]: value, page: 1 }); + // ─── Price input local state (commits on blur / Enter) ─── + const [priceMinInput, setPriceMinInput] = React.useState(String(values.priceRange[0])); + const [priceMaxInput, setPriceMaxInput] = React.useState(String(values.priceRange[1])); + + // Sync local state when the slider (or clear) changes the value + const rangeMin = values.priceRange[0]; + const rangeMax = values.priceRange[1]; + React.useEffect(() => { + setPriceMinInput(String(rangeMin)); + setPriceMaxInput(String(rangeMax)); + }, [rangeMin, rangeMax]); + + const commitPriceRange = () => { + let lo = parseInt(priceMinInput, 10); + let hi = parseInt(priceMaxInput, 10); + if (isNaN(lo)) lo = minPrice; + if (isNaN(hi)) hi = maxPrice; + lo = Math.max(minPrice, Math.min(lo, maxPrice)); + hi = Math.max(minPrice, Math.min(hi, maxPrice)); + if (lo > hi) [lo, hi] = [hi, lo]; + const newRange: [number, number] = [lo, hi]; + if (newRange[0] !== values.priceRange[0] || newRange[1] !== values.priceRange[1]) { + onChange({ ...values, priceRange: newRange, page: 1 }); + } }; - const handleFilterClear = () => { - onChange({ ...values, categoryFilter: 'all', priceFilter: 'all', page: 1 }); + // ─── Derived state ─── + const hasActiveFilters = + values.categoryFilter !== 'all' || + values.priceRange[0] !== minPrice || + values.priceRange[1] !== maxPrice; + + // Determine which parent category is expanded (if filter matches parent or child) + const expandedParent = + categories.find( + (cat) => + cat.value === values.categoryFilter || + cat.children?.some((child) => child.value === values.categoryFilter), + )?.value ?? null; + + const handleClearFilters = () => { + onChange({ + ...values, + categoryFilter: 'all', + priceRange: [minPrice, maxPrice], + page: 1, + }); }; return ( = ({ onBack={onBack} hideHelpBar={hideHelpBar} sx={sx} - > - { - e.preventDefault(); - if (!loading) onContinue(); - }} - > - {/* Page heading */} - - Choose a coffin - - - - {isPrePlanning - ? 'Browse the range to get an idea of styles and pricing. You can change your selection later.' - : 'Browse the range available with your selected provider. Use the filters to narrow your options.'} - - - - Selecting a coffin within your package allowance won't change your total. Coffins - outside the allowance will adjust the price. - - - {/* Filter button + results count */} - - - handleFilterChange('categoryFilter', e.target.value)} - fullWidth - > - {categories.map((cat) => ( - - {cat.label} - - ))} - - - handleFilterChange('priceFilter', e.target.value)} - fullWidth - > - {priceRanges.map((range) => ( - - {range.label} - - ))} - - - - + secondaryPanel={ + /* ─── Grid area (right panel) ─── */ + + {/* Results count */} + Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''} - - {/* Coffin card grid — full width (D-F) */} - - {coffins.map((coffin, index) => ( - onChange({ ...values, selectedCoffinId: coffin.id })} - role="radio" - aria-checked={coffin.id === values.selectedCoffinId} - tabIndex={ - values.selectedCoffinId === null - ? index === 0 - ? 0 - : -1 - : coffin.id === values.selectedCoffinId - ? 0 - : -1 - } - sx={{ overflow: 'hidden' }} - > - {/* Image */} - - {coffin.isPopular && ( - - - Most Popular - - - )} + {/* Coffin card grid */} + + {visibleCoffins.map((coffin) => ( + + onSelectCoffin(coffin.id)} /> + ))} - {/* Content */} - - - {coffin.name} + {visibleCoffins.length === 0 && ( + + + No coffins match your selected filters. - - {coffin.category} - - - ${coffin.price.toLocaleString('en-AU')} + + Try adjusting the category or price range. - - ))} + )} + - {coffins.length === 0 && ( - - - No coffins match your selected filters. - - - Try adjusting the category or price range. - + {/* Pagination */} + {derivedTotalPages > 1 && ( + + onChange({ ...values, page })} + color="primary" + /> )} + } + > + {/* ─── Sidebar (left panel) ─── */} - {/* Validation error */} - {errors?.selectedCoffinId && ( - - {errors.selectedCoffinId} - - )} + {/* Heading */} + + Choose a coffin + - {/* Pagination */} - {totalPages > 1 && ( - - onChange({ ...values, page })} - color="primary" - /> - - )} + + {isPrePlanning + ? 'Browse the range to get an idea of styles and pricing.' + : 'Browse our selection of bespoke designer coffins.'} + - - - {/* CTAs */} - - {onSaveAndExit ? ( - - ) : ( - - )} - - + + + Your package includes a ${allowanceAmount.toLocaleString('en-AU')}{' '} + allowance for coffins which will be applied to your order after you have made a + selection. + + + )} + + + + {/* ─── Categories menu — single-select with expandable subcategories ─── */} + + Categories + + + {categories.map((cat) => { + const isParentActive = + cat.value === values.categoryFilter || + cat.children?.some((child) => child.value === values.categoryFilter); + const isExactActive = cat.value === values.categoryFilter; + const isExpanded = expandedParent === cat.value; + + return ( + + {/* Parent category */} + onChange({ ...values, categoryFilter: cat.value, page: 1 })} + sx={categoryItemSx(!!isExactActive)} + > + {cat.label} + {cat.children && cat.children.length > 0 && ( + + )} + + + {/* Subcategories — expand when parent is active */} + {cat.children && cat.children.length > 0 && ( + + + {cat.children.map((child) => ( + + onChange({ ...values, categoryFilter: child.value, page: 1 }) + } + sx={categoryItemSx(child.value === values.categoryFilter)} + > + {child.label} + + ))} + + + )} + + ); + })} + + + + {/* ─── Price range slider with editable inputs ─── */} + + Price + + + + onChange({ ...values, priceRange: newValue as [number, number], page: 1 }) + } + min={minPrice} + max={maxPrice} + step={100} + valueLabelDisplay="auto" + valueLabelFormat={(v) => `$${v.toLocaleString('en-AU')}`} + color="primary" + /> + + + setPriceMinInput(e.target.value.replace(/[^0-9]/g, ''))} + onBlur={commitPriceRange} + onKeyDown={(e) => e.key === 'Enter' && commitPriceRange()} + InputProps={{ + startAdornment: $, + }} + inputProps={{ inputMode: 'numeric', 'aria-label': 'Minimum price' }} + sx={{ flex: 1 }} + /> + + – + + setPriceMaxInput(e.target.value.replace(/[^0-9]/g, ''))} + onBlur={commitPriceRange} + onKeyDown={(e) => e.key === 'Enter' && commitPriceRange()} + InputProps={{ + startAdornment: $, + }} + inputProps={{ inputMode: 'numeric', 'aria-label': 'Maximum price' }} + sx={{ flex: 1 }} + /> + + + + + {/* ─── Sort by ─── */} + + Sort by + + onChange({ ...values, sortBy: e.target.value as CoffinSortBy })} + fullWidth + size="small" + > + Popularity + Price: Low to High + Price: High to Low + + + {/* Clear filters — at bottom of sidebar, under sort */} + {hasActiveFilters && ( + <> + + + Clear all filters + + + )} + + {/* Save and continue later — bottom of sidebar */} + {onSaveAndExit && ( + <> + + + Save and continue later + + + )} ); }; diff --git a/src/components/pages/CoffinsStep/index.ts b/src/components/pages/CoffinsStep/index.ts index 6f16c39..b8e1b07 100644 --- a/src/components/pages/CoffinsStep/index.ts +++ b/src/components/pages/CoffinsStep/index.ts @@ -2,8 +2,7 @@ export { CoffinsStep, default } from './CoffinsStep'; export type { CoffinsStepProps, CoffinsStepValues, - CoffinsStepErrors, Coffin, CoffinCategory, - CoffinPriceRange, + CoffinSortBy, } from './CoffinsStep'; diff --git a/src/components/templates/WizardLayout/WizardLayout.tsx b/src/components/templates/WizardLayout/WizardLayout.tsx index 0cf6523..38fad78 100644 --- a/src/components/templates/WizardLayout/WizardLayout.tsx +++ b/src/components/templates/WizardLayout/WizardLayout.tsx @@ -230,31 +230,64 @@ const ListDetailLayout: React.FC<{ ); -/** Grid + Sidebar: ~25% filter sidebar (left) / ~75% card grid (right) */ +/** Grid + Sidebar: ~25% filter sidebar (left) / ~75% card grid (right). + * Viewport-locked on desktop — both panels scroll independently. + * On mobile, stacks vertically and scrolls as a single page. */ const GridSidebarLayout: React.FC<{ children: React.ReactNode; secondaryPanel?: React.ReactNode; }> = ({ children, secondaryPanel }) => ( - + + {/* Left sidebar — scrollbar visible on hover */} - - {children} - - {secondaryPanel} + {children} - + {/* Right panel — always scrollable */} + + {secondaryPanel} + + ); /** Detail + Toggles: scrollable left (image/desc) / sticky right (info/CTA) */ @@ -401,6 +434,10 @@ export const WizardLayout = React.forwardRef( height: '100vh', overflow: 'hidden', }), + ...(variant === 'grid-sidebar' && { + height: { xs: 'auto', md: '100vh' }, + overflow: { xs: 'visible', md: 'hidden' }, + }), }, ...(Array.isArray(sx) ? sx : [sx]), ]}