From b454911314bcca16f0b5841b8a57fd06877b3a3c Mon Sep 17 00:00:00 2001 From: Richie Date: Mon, 30 Mar 2026 18:27:21 +1100 Subject: [PATCH] Checkbox atom: new FA wrapper with brand theming - New Checkbox atom wrapping MUI Checkbox (forwardRef, displayName) - MuiCheckbox theme overrides: warm gold checked, focus ring, disabled muted - Stories: Default, States, TermsAgreement, Checklist - PaymentStep: now imports Checkbox from atom instead of MUI directly Co-Authored-By: Claude Opus 4.6 (1M context) --- .../atoms/Checkbox/Checkbox.stories.tsx | 144 ++++++++++++++++++ src/components/atoms/Checkbox/Checkbox.tsx | 35 +++++ src/components/atoms/Checkbox/index.ts | 2 + .../pages/PaymentStep/PaymentStep.tsx | 2 +- src/theme/index.ts | 26 ++++ 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/components/atoms/Checkbox/Checkbox.stories.tsx create mode 100644 src/components/atoms/Checkbox/Checkbox.tsx create mode 100644 src/components/atoms/Checkbox/index.ts diff --git a/src/components/atoms/Checkbox/Checkbox.stories.tsx b/src/components/atoms/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000..23ad173 --- /dev/null +++ b/src/components/atoms/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Checkbox } from './Checkbox'; +import { Typography } from '../Typography'; +import Box from '@mui/material/Box'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; + +const meta: Meta = { + title: 'Atoms/Checkbox', + component: Checkbox, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + checked: { + control: 'boolean', + description: 'Whether the checkbox is checked', + }, + disabled: { + control: 'boolean', + description: 'Disable the checkbox', + table: { defaultValue: { summary: 'false' } }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ──────────────────────────────────────────────────────────────── + +/** Default checkbox — unchecked */ +export const Default: Story = { + args: {}, +}; + +// ─── States ───────────────────────────────────────────────────────────────── + +/** All visual states */ +export const States: Story = { + render: () => ( + + } label="Unchecked" /> + } label="Checked" /> + } label="Disabled unchecked" /> + } label="Disabled checked" /> + } label="Indeterminate" /> + + ), +}; + +// ─── Terms Agreement ──────────────────────────────────────────────────────── + +/** + * Realistic pattern — terms and conditions checkbox in a payment form. + */ +export const TermsAgreement: Story = { + name: 'Terms Agreement', + render: () => { + const TermsDemo = () => { + const [accepted, setAccepted] = useState(false); + + return ( + + setAccepted(e.target.checked)} /> + } + label={ + + I agree to the service agreement and privacy policy + + } + sx={{ alignItems: 'flex-start', '& .MuiCheckbox-root': { pt: 0.5 } }} + /> + {!accepted && ( + + You must accept the terms to continue + + )} + + ); + }; + + return ; + }, +}; + +// ─── Checklist ────────────────────────────────────────────────────────────── + +/** + * Checklist pattern — multiple independent options. + */ +export const Checklist: Story = { + render: () => { + const ChecklistDemo = () => { + const [items, setItems] = useState({ + dressing: true, + viewing: false, + prayers: false, + announcement: true, + }); + + const toggle = (key: keyof typeof items) => { + setItems((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + return ( + + + Complimentary inclusions + + + toggle('dressing')} />} + label="Dressing and preparation" + /> + toggle('viewing')} />} + label="Viewing for family and friends" + /> + toggle('prayers')} />} + label="Prayers or vigil" + /> + toggle('announcement')} /> + } + label="Funeral announcement" + /> + + + ); + }; + + return ; + }, +}; diff --git a/src/components/atoms/Checkbox/Checkbox.tsx b/src/components/atoms/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..53033b9 --- /dev/null +++ b/src/components/atoms/Checkbox/Checkbox.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import MuiCheckbox from '@mui/material/Checkbox'; +import type { CheckboxProps as MuiCheckboxProps } from '@mui/material/Checkbox'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Props for the FA Checkbox component */ +export type CheckboxProps = MuiCheckboxProps; + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Checkbox for the FA design system. + * + * Multi-select control for independent boolean options. Wraps MUI Checkbox + * with FA brand tokens — warm gold fill when checked, rounded square shape. + * + * Usage: + * - For independent booleans ("I agree to terms", "Include catering") + * - For mutually exclusive options, use Radio instead + * - For binary toggles, use Switch instead + * + * **Accessibility**: Always wrap in `FormControlLabel` with a `label` prop. + * A standalone Checkbox without a visible label is inaccessible — screen + * readers cannot announce what the checkbox controls. + * ```tsx + * } label="I agree to the terms" /> + * ``` + */ +export const Checkbox = React.forwardRef((props, ref) => { + return ; +}); + +Checkbox.displayName = 'Checkbox'; +export default Checkbox; diff --git a/src/components/atoms/Checkbox/index.ts b/src/components/atoms/Checkbox/index.ts new file mode 100644 index 0000000..b2bfbc4 --- /dev/null +++ b/src/components/atoms/Checkbox/index.ts @@ -0,0 +1,2 @@ +export { Checkbox, default } from './Checkbox'; +export type { CheckboxProps } from './Checkbox'; diff --git a/src/components/pages/PaymentStep/PaymentStep.tsx b/src/components/pages/PaymentStep/PaymentStep.tsx index 2abae27..36d0cf6 100644 --- a/src/components/pages/PaymentStep/PaymentStep.tsx +++ b/src/components/pages/PaymentStep/PaymentStep.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; -import Checkbox from '@mui/material/Checkbox'; +import { Checkbox } from '../../atoms/Checkbox'; import FormControlLabel from '@mui/material/FormControlLabel'; import type { SxProps, Theme } from '@mui/material/styles'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; diff --git a/src/theme/index.ts b/src/theme/index.ts index c9b6b3b..412146d 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -783,6 +783,32 @@ export const theme = createTheme({ }, }, }, + MuiCheckbox: { + styleOverrides: { + root: { + color: t.ColorNeutral400, + transition: 'color 150ms ease-in-out', + '&:hover': { + color: t.ColorNeutral600, + backgroundColor: 'transparent', + }, + '&.Mui-checked': { + color: t.ColorInteractiveDefault, + '&:hover': { + color: t.ColorInteractiveHover, + }, + }, + '&:focus-visible': { + outline: `2px solid ${t.ColorInteractiveFocus}`, + outlineOffset: '2px', + borderRadius: '4px', + }, + '&.Mui-disabled': { + color: t.ColorNeutral300, + }, + }, + }, + }, MuiRadio: { styleOverrides: { root: {