diff --git a/src/components/pages/PackagesStep/PackagesStep.stories.tsx b/src/components/pages/PackagesStep/PackagesStep.stories.tsx
new file mode 100644
index 0000000..be4f085
--- /dev/null
+++ b/src/components/pages/PackagesStep/PackagesStep.stories.tsx
@@ -0,0 +1,303 @@
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { PackagesStep } from './PackagesStep';
+import type { PackageData, PackagesStepProvider } from './PackagesStep';
+import { Navigation } from '../../organisms/Navigation';
+import Box from '@mui/material/Box';
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+const FALogo = () => (
+
+
+
+
+);
+
+const nav = (
+ }
+ items={[
+ { label: 'FAQ', href: '/faq' },
+ { label: 'Contact Us', href: '/contact' },
+ { label: 'Log in', href: '/login' },
+ ]}
+ />
+);
+
+const mockProvider: PackagesStepProvider = {
+ name: 'H.Parsons Funeral Directors',
+ location: 'Wentworth, NSW',
+ imageUrl: 'https://placehold.co/120x80/E8E0D6/8B6F47?text=H.Parsons',
+ rating: 4.6,
+ reviewCount: 7,
+};
+
+const mockPackages: PackageData[] = [
+ {
+ id: 'everyday',
+ name: 'Everyday Funeral Package',
+ price: 2700,
+ description:
+ 'This package includes a funeral service at a chapel or a church with a funeral procession. It includes many of the most commonly selected funeral options.',
+ popular: true,
+ sections: [
+ {
+ heading: 'Essentials',
+ items: [
+ { name: 'Accommodation', price: 500 },
+ { name: 'Death registration certificate', price: 150 },
+ { name: 'Doctor fee for Cremation', price: 150 },
+ { name: 'NSW Government Levy - Cremation', price: 83 },
+ { name: 'Professional Mortuary Care', price: 1200 },
+ { name: 'Professional Service Fee', price: 1120 },
+ ],
+ },
+ {
+ heading: 'Complimentary Items',
+ items: [
+ { name: 'Dressing Fee', price: 0 },
+ { name: 'Viewing Fee', price: 0 },
+ ],
+ },
+ ],
+ total: 2700,
+ extras: {
+ heading: 'Extras',
+ items: [
+ { name: 'Allowance for Flowers', price: 150, isAllowance: true },
+ { name: 'Allowance for Master of Ceremonies', price: 500, isAllowance: true },
+ { name: 'After Business Hours Service Surcharge', price: 150 },
+ { name: 'After Hours Prayers', price: 1920 },
+ { name: 'Coffin Bearing by Funeral Directors', price: 1500 },
+ { name: 'Digital Recording', price: 500 },
+ ],
+ },
+ terms:
+ 'This package includes a funeral service at a chapel or a church with a funeral procession. Pricing may vary based on additional selections.',
+ },
+ {
+ id: 'deluxe',
+ name: 'Deluxe Funeral Package',
+ price: 4900,
+ description:
+ 'A comprehensive package with premium inclusions, higher-quality coffin selection, and expanded service options for families wanting a more personalised farewell.',
+ sections: [
+ {
+ heading: 'Essentials',
+ items: [
+ { name: 'Accommodation', price: 750 },
+ { name: 'Death registration certificate', price: 150 },
+ { name: 'Professional Mortuary Care', price: 1500 },
+ { name: 'Professional Service Fee', price: 1500 },
+ { name: 'Premium Coffin', price: 1000 },
+ ],
+ },
+ ],
+ total: 4900,
+ },
+ {
+ id: 'essential',
+ name: 'Essential Funeral Package',
+ price: 1800,
+ description:
+ 'A simple, dignified option covering the essential requirements for a cremation service.',
+ sections: [
+ {
+ heading: 'Essentials',
+ items: [
+ { name: 'Death registration certificate', price: 150 },
+ { name: 'Professional Mortuary Care', price: 800 },
+ { name: 'Professional Service Fee', price: 850 },
+ ],
+ },
+ ],
+ total: 1800,
+ },
+ {
+ id: 'catholic',
+ name: 'Catholic Service',
+ price: 3200,
+ description:
+ 'Tailored for Catholic funeral traditions including a Requiem Mass, graveside prayers, and coordination with parish requirements.',
+ sections: [
+ {
+ heading: 'Essentials',
+ items: [
+ { name: 'Accommodation', price: 500 },
+ { name: 'Professional Mortuary Care', price: 1200 },
+ { name: 'Professional Service Fee', price: 1200 },
+ { name: 'Church coordination', price: 300 },
+ ],
+ },
+ ],
+ total: 3200,
+ },
+];
+
+// ─── Meta ────────────────────────────────────────────────────────────────────
+
+const meta: Meta = {
+ title: 'Pages/PackagesStep',
+ component: PackagesStep,
+ tags: ['autodocs'],
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+// ─── Interactive (default) ──────────────────────────────────────────────────
+
+/** Fully interactive — browse, filter, select a package, see detail */
+export const Default: Story = {
+ render: () => {
+ const [selectedId, setSelectedId] = useState(null);
+ const [budget, setBudget] = useState('all');
+ const [error, setError] = useState();
+
+ const filtered =
+ budget === 'all'
+ ? mockPackages
+ : mockPackages.filter((p) => {
+ const [min, max] = budget.split('-').map(Number);
+ return p.price >= min && p.price <= (max || Infinity);
+ });
+
+ const handleContinue = () => {
+ if (!selectedId) {
+ setError('Please choose a package to continue.');
+ return;
+ }
+ setError(undefined);
+ alert(`Continue with package: ${selectedId}`);
+ };
+
+ return (
+ {
+ setSelectedId(id);
+ setError(undefined);
+ }}
+ budgetFilter={budget}
+ onBudgetFilterChange={setBudget}
+ onContinue={handleContinue}
+ onBack={() => alert('Back')}
+ error={error}
+ navigation={nav}
+ />
+ );
+ },
+};
+
+// ─── With selection ─────────────────────────────────────────────────────────
+
+/** Package already selected — detail panel visible */
+export const WithSelection: Story = {
+ render: () => {
+ const [selectedId, setSelectedId] = useState('everyday');
+ const [budget, setBudget] = useState('all');
+
+ return (
+ alert('Continue')}
+ onBack={() => alert('Back')}
+ navigation={nav}
+ />
+ );
+ },
+};
+
+// ─── Pre-planning ───────────────────────────────────────────────────────────
+
+/** Pre-planning flow — softer helper text */
+export const PrePlanning: Story = {
+ render: () => {
+ const [selectedId, setSelectedId] = useState(null);
+ const [budget, setBudget] = useState('all');
+
+ return (
+ alert('Continue')}
+ onBack={() => alert('Back')}
+ navigation={nav}
+ isPrePlanning
+ />
+ );
+ },
+};
+
+// ─── Filtered empty ─────────────────────────────────────────────────────────
+
+/** Budget filter yielding no results */
+export const FilteredEmpty: Story = {
+ render: () => {
+ const [budget, setBudget] = useState('7000-10000');
+
+ return (
+ {}}
+ budgetFilter={budget}
+ onBudgetFilterChange={setBudget}
+ onContinue={() => {}}
+ onBack={() => alert('Back')}
+ navigation={nav}
+ />
+ );
+ },
+};
+
+// ─── Validation error ───────────────────────────────────────────────────────
+
+/** Error shown when no package selected */
+export const WithError: Story = {
+ render: () => {
+ const [selectedId, setSelectedId] = useState(null);
+ const [budget, setBudget] = useState('all');
+
+ return (
+ {}}
+ onBack={() => alert('Back')}
+ error="Please choose a package to continue."
+ navigation={nav}
+ />
+ );
+ },
+};
diff --git a/src/components/pages/PackagesStep/PackagesStep.tsx b/src/components/pages/PackagesStep/PackagesStep.tsx
new file mode 100644
index 0000000..ab68528
--- /dev/null
+++ b/src/components/pages/PackagesStep/PackagesStep.tsx
@@ -0,0 +1,289 @@
+import React from 'react';
+import Box from '@mui/material/Box';
+import TextField from '@mui/material/TextField';
+import MenuItem from '@mui/material/MenuItem';
+import type { SxProps, Theme } from '@mui/material/styles';
+import { WizardLayout } from '../../templates/WizardLayout';
+import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
+import { ServiceOption } from '../../molecules/ServiceOption';
+import { PackageDetail } from '../../organisms/PackageDetail';
+import type { PackageSection } from '../../organisms/PackageDetail';
+import { Typography } from '../../atoms/Typography';
+import { Badge } from '../../atoms/Badge';
+import { Button } from '../../atoms/Button';
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+/** Provider summary for the compact card */
+export interface PackagesStepProvider {
+ /** Provider name */
+ name: string;
+ /** Location */
+ location: string;
+ /** Image URL */
+ imageUrl?: string;
+ /** Rating */
+ rating?: number;
+ /** Review count */
+ reviewCount?: number;
+}
+
+/** Package data for the selection list */
+export interface PackageData {
+ /** Unique package ID */
+ id: string;
+ /** Package display name */
+ name: string;
+ /** Package price in dollars */
+ price: number;
+ /** Short description */
+ description?: string;
+ /** Whether this is a "Most Popular" package */
+ popular?: boolean;
+ /** Line item sections for the detail panel */
+ sections: PackageSection[];
+ /** Total price (may differ from base price with extras) */
+ total?: number;
+ /** Extra items section (after total) */
+ extras?: PackageSection;
+ /** Terms and conditions */
+ terms?: string;
+}
+
+/** Budget filter option */
+export interface BudgetOption {
+ /** Option value */
+ value: string;
+ /** Display label */
+ label: string;
+}
+
+/** Props for the PackagesStep page component */
+export interface PackagesStepProps {
+ /** Provider summary shown at top of the list panel */
+ provider: PackagesStepProvider;
+ /** Available packages */
+ packages: PackageData[];
+ /** Currently selected package ID */
+ selectedPackageId: string | null;
+ /** Callback when a package is selected */
+ onSelectPackage: (id: string) => void;
+ /** Current budget filter value */
+ budgetFilter: string;
+ /** Callback when budget filter changes */
+ onBudgetFilterChange: (value: string) => void;
+ /** Budget filter options */
+ budgetOptions?: BudgetOption[];
+ /** Callback for the Continue button */
+ onContinue: () => void;
+ /** Callback for the Back button */
+ onBack: () => void;
+ /** Validation error */
+ error?: string;
+ /** Whether Continue is loading */
+ loading?: boolean;
+ /** Navigation bar */
+ navigation?: React.ReactNode;
+ /** Whether this is a pre-planning flow */
+ isPrePlanning?: boolean;
+ /** MUI sx prop */
+ sx?: SxProps;
+}
+
+// ─── Constants ───────────────────────────────────────────────────────────────
+
+const DEFAULT_BUDGET_OPTIONS: BudgetOption[] = [
+ { value: 'all', label: 'All packages' },
+ { value: '2000-4000', label: '$2,000 \u2013 $4,000' },
+ { value: '4000-7000', label: '$4,000 \u2013 $7,000' },
+ { value: '7000-10000', label: '$7,000 \u2013 $10,000+' },
+];
+
+// ─── Component ───────────────────────────────────────────────────────────────
+
+/**
+ * Step 3 — Package selection page for the FA arrangement wizard.
+ *
+ * List + Detail split layout. Left panel shows the selected provider
+ * (compact), a budget filter, and selectable package cards. Right panel
+ * shows the full detail breakdown of the selected package.
+ *
+ * Packages are displayed as ServiceOption cards in a radiogroup pattern.
+ * "Most Popular" badge on qualifying packages reduces decision paralysis.
+ *
+ * Pure presentation component — props in, callbacks out.
+ *
+ * Spec: documentation/steps/steps/03_packages.yaml
+ */
+export const PackagesStep: React.FC = ({
+ provider,
+ packages,
+ selectedPackageId,
+ onSelectPackage,
+ budgetFilter,
+ onBudgetFilterChange,
+ budgetOptions = DEFAULT_BUDGET_OPTIONS,
+ onContinue,
+ onBack,
+ error,
+ loading = false,
+ navigation,
+ isPrePlanning = false,
+ sx,
+}) => {
+ const selectedPackage = packages.find((p) => p.id === selectedPackageId);
+
+ const subheading =
+ 'Each package includes a set of services. You can customise your selections in the next steps.';
+ const helperText = isPrePlanning
+ ? 'Compare packages to find what suits your wishes. Nothing is committed until you confirm.'
+ : 'Prices shown include the base services listed. Additional options may change the total.';
+
+ return (
+
+ ) : (
+
+
+ Select a package to see what's included.
+
+
+ )
+ }
+ >
+ {/* Provider compact card */}
+
+
+
+
+ {/* Heading */}
+
+ Choose a funeral package
+
+
+ {subheading}
+
+
+ {helperText}
+
+
+ {/* Budget filter */}
+
+ onBudgetFilterChange(e.target.value)}
+ label="Budget range"
+ sx={{ width: { xs: '100%', sm: 240 } }}
+ >
+ {budgetOptions.map((opt) => (
+
+ ))}
+
+
+
+ {/* Error message */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Package list — radiogroup pattern */}
+
+ {packages.map((pkg) => (
+
+ {pkg.popular && (
+
+ Most Popular
+
+ )}
+ onSelectPackage(pkg.id)}
+ />
+
+ ))}
+
+ {packages.length === 0 && (
+
+
+ No packages match the selected budget range.
+
+
+ )}
+
+
+ {/* Mobile: Continue button (desktop uses PackageDetail's CTA) */}
+
+
+
+
+ );
+};
+
+PackagesStep.displayName = 'PackagesStep';
+export default PackagesStep;
diff --git a/src/components/pages/PackagesStep/index.ts b/src/components/pages/PackagesStep/index.ts
new file mode 100644
index 0000000..6805537
--- /dev/null
+++ b/src/components/pages/PackagesStep/index.ts
@@ -0,0 +1,2 @@
+export { default } from './PackagesStep';
+export * from './PackagesStep';