Add Switch and Radio atom components

Switch:
- Wraps MUI Switch with FA brand tokens
- Bordered pill track (Figma Style One), brand.500 fill when active
- 4 component tokens: track width/height/borderRadius, thumb size
- Stories include interactive service add-ons demo

Radio:
- Wraps MUI Radio with FA brand tokens
- Brand.500 fill when selected, neutral.400 unchecked
- 2 component tokens: outer size, dot size
- Stories include card selection and payment method patterns

Also:
- Added ColourToggle and Slider to component registry (deferred)
- Updated token registry and session log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 17:04:37 +11:00
parent b2349d6c78
commit c10a5e4e1c
14 changed files with 529 additions and 2 deletions

View File

@@ -26,6 +26,10 @@ duplicates) and MUST update it after completing one.
| Divider | planned | horizontal, vertical | | Visual separator | | Divider | planned | horizontal, vertical | | Visual separator |
| Chip | review | filled, outlined × small, medium × clickable, deletable, selected × default, primary | chip.height/paddingX/fontSize/iconSize/deleteIconSize/iconGap/borderRadius, color.neutral.200-700, color.brand.200-700 | Interactive tag. Wraps MUI Chip with FA tokens. Selected state promotes to brand colour. Filled uses soft tonal bg (like Badge). | | Chip | review | filled, outlined × small, medium × clickable, deletable, selected × default, primary | chip.height/paddingX/fontSize/iconSize/deleteIconSize/iconGap/borderRadius, color.neutral.200-700, color.brand.200-700 | Interactive tag. Wraps MUI Chip with FA tokens. Selected state promotes to brand colour. Filled uses soft tonal bg (like Badge). |
| Card | done | elevated, outlined × default, compact, none padding × interactive × selected | card.borderRadius/padding/shadow/border/background, color.surface.raised/subtle/warm, color.border.default/brand, shadow.md/lg | Content container. Elevated (shadow) or outlined (border). Interactive adds hover bg fill + shadow lift. Selected adds brand border + warm bg. Three padding presets. | | Card | done | elevated, outlined × default, compact, none padding × interactive × selected | card.borderRadius/padding/shadow/border/background, color.surface.raised/subtle/warm, color.border.default/brand, shadow.md/lg | Content container. Elevated (shadow) or outlined (border). Interactive adds hover bg fill + shadow lift. Selected adds brand border + warm bg. Three padding presets. |
| Switch | review | bordered style × checked, unchecked, disabled | switch.track.width/height/borderRadius, switch.thumb.size, color.interactive.*, color.neutral.400 | Toggle for add-ons/options. Wraps MUI Switch. Bordered pill, brand.500 fill when active. From Parsons 1.0 Figma Style One. |
| Radio | review | checked, unchecked, disabled | radio.size/dotSize, color.interactive.*, color.neutral.400 | Single-select option. Wraps MUI Radio. Brand.500 fill when selected. From Parsons 1.0 Figma. |
| ColourToggle | planned | inactive, hover, active, locked × single, two-colour × desktop, mobile | | Circular colour swatch picker for products. Custom component. Deferred until product detail organisms. |
| Slider | planned | single, range × desktop, mobile | | Price range filter. Wraps MUI Slider. Deferred until search/filtering molecules. |
| Link | planned | default, subtle | | Navigation text | | Link | planned | default, subtle | | Navigation text |
## Molecules ## Molecules

View File

@@ -456,6 +456,46 @@ 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 Chip in Storybook - ~~User to review Chip in Storybook~~ ✓ Approved
- ~~Consider running /audit or /critique on completed atoms to establish baseline scores~~ ✓ Done
- Begin PriceCard molecule (depends on Card + Badge + Typography + Button + Chip) - Begin PriceCard molecule (depends on Card + Badge + Typography + Button + Chip)
- Consider running /audit or /critique on completed atoms to establish baseline scores
### Session 2026-03-25k — Audit, P1 fixes, Switch + Radio atoms
**Agent(s):** Claude Opus (via conversation)
**Work completed:**
- Ran /audit on all 5 completed atoms (Button, Typography, Input, Card, Badge)
- Average score: 18/20 (Excellent)
- 2 P1 issues identified and fixed, 7 P2s documented, 5 P3s noted
- Fixed P1: Button loading state now announces to screen readers (aria-busy + visually-hidden text)
- Fixed P1: Card interactive now has tabIndex={0} and role="button" for keyboard operability
- Fixed P2: Card Record<string, any> → Theme type for type safety
- Reviewed Parsons 1.0 Figma "Toggles" board (node 2322:42538) — identified 4 new atoms: Switch, Radio, ColourToggle, Slider
- Added all 4 to component registry (ColourToggle and Slider deferred)
- Created switch component tokens (`tokens/component/switch.json`): track width/height/borderRadius, thumb size — 4 tokens
- Created radio component tokens (`tokens/component/radio.json`): size, dotSize — 2 tokens
- Updated MUI theme with MuiSwitch overrides: bordered pill track, brand.500 active fill, focus-visible ring
- Updated MUI theme with MuiRadio overrides: neutral.400 unchecked, brand.500 checked, hover states
- Created Switch component — thin MUI wrapper with forwardRef
- Created Radio component — thin MUI wrapper with forwardRef
- Created Switch stories (4): Default, States, ServiceAddOns (interactive add-on toggle demo), WithLabels
- Created Radio stories (5): Default, States, RadioGroup, CardSelection (interactive card + radio demo), PaymentMethod
- Preflight passed all 5 checks
**Decisions made:**
- Switch implements Figma "Style One" (bordered pill) only — other styles deferred as variants if needed
- Switch/Radio are ultra-thin wrappers — all styling via MUI theme overrides, no component-level sx
- ColourToggle and Slider deferred until their consuming organisms are built
**Component status at end of session:**
- **Done (5):** Button, Typography, Input, Card, Badge
- **Review (3):** Chip, Switch, Radio
- **Planned (6 atoms):** IconButton, Icon, Avatar, Divider, ColourToggle, Slider, Link
- **Planned (5 molecules):** FormField, PriceCard, ServiceOption, SearchBar, StepIndicator
- **Planned (5 organisms):** ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer
**Next steps:**
- User to review Switch and Radio in Storybook
- Begin PriceCard molecule
- Address P2 audit issues in a future cleanup pass

View File

@@ -244,3 +244,23 @@ the correct token for any design property.
| chip.deleteIconSize.md | 16px | Chip | Medium chip delete icon | | chip.deleteIconSize.md | 16px | Chip | Medium chip delete icon |
| chip.iconGap.default | → spacing.1 (4px) | Chip | Icon-to-text gap | | chip.iconGap.default | → spacing.1 (4px) | Chip | Icon-to-text gap |
| chip.borderRadius.default | → borderRadius.full (9999px) | Chip | Pill shape | | chip.borderRadius.default | → borderRadius.full (9999px) | Chip | Pill shape |
### Switch
`tokens/component/switch.json`
| Token path | Value / Reference | Used by | Description |
|-----------|-----------|---------|-------------|
| switch.track.width | 44px | Switch | Track width |
| switch.track.height | 24px | Switch | Track height |
| switch.track.borderRadius | → borderRadius.full (9999px) | Switch | Pill shape |
| switch.thumb.size | 18px | Switch | Thumb diameter |
### Radio
`tokens/component/radio.json`
| Token path | Value / Reference | Used by | Description |
|-----------|-----------|---------|-------------|
| radio.size.default | 20px | Radio | Outer circle size |
| radio.dotSize.default | 10px | Radio | Inner selected dot size |

View File

@@ -0,0 +1,146 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Radio } from './Radio';
import { Typography } from '../Typography';
import { Card } from '../Card';
import Box from '@mui/material/Box';
import FormControlLabel from '@mui/material/FormControlLabel';
import RadioGroup from '@mui/material/RadioGroup';
import FormControl from '@mui/material/FormControl';
const meta: Meta<typeof Radio> = {
title: 'Atoms/Radio',
component: Radio,
tags: ['autodocs'],
parameters: {
layout: 'centered',
design: {
type: 'figma',
url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=2322-42538',
},
},
argTypes: {
checked: {
control: 'boolean',
description: 'Whether the radio is selected',
},
disabled: {
control: 'boolean',
description: 'Disable the radio',
table: { defaultValue: { summary: 'false' } },
},
},
};
export default meta;
type Story = StoryObj<typeof Radio>;
// ─── Default ────────────────────────────────────────────────────────────────
/** Default radio — unchecked */
export const Default: Story = {
args: {},
};
// ─── States ─────────────────────────────────────────────────────────────────
/** All visual states */
export const States: Story = {
render: () => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<FormControlLabel control={<Radio />} label="Unchecked" />
<FormControlLabel control={<Radio checked />} label="Checked" />
<FormControlLabel control={<Radio disabled />} label="Disabled unchecked" />
<FormControlLabel control={<Radio disabled checked />} label="Disabled checked" />
</Box>
),
};
// ─── Radio Group ────────────────────────────────────────────────────────────
/** Standard radio group with keyboard navigation */
export const Group: Story = {
name: 'Radio Group',
render: () => (
<FormControl>
<Typography variant="label" sx={{ mb: 1 }}>Service type</Typography>
<RadioGroup defaultValue="chapel">
<FormControlLabel value="chapel" control={<Radio />} label="Chapel ceremony" />
<FormControlLabel value="graveside" control={<Radio />} label="Graveside service" />
<FormControlLabel value="memorial" control={<Radio />} label="Memorial service" />
<FormControlLabel value="direct" control={<Radio />} label="Direct cremation" />
</RadioGroup>
</FormControl>
),
};
// ─── Interactive: Card Selection ────────────────────────────────────────────
/**
* Radio buttons inside interactive cards — the recommended pattern
* for option selection in FA. Combines Card's selected state with
* Radio for accessible single-select.
*/
export const CardSelection: Story = {
name: 'Interactive — Card Selection',
render: () => {
const CardSelectDemo = () => {
const [selected, setSelected] = useState('standard');
const options = [
{ value: 'direct', label: 'Direct cremation', desc: 'Simple, dignified cremation with no service.', price: '$1,800' },
{ value: 'standard', label: 'Standard service', desc: 'Traditional chapel ceremony with viewing.', price: '$4,200' },
{ value: 'premium', label: 'Premium service', desc: 'Full service with personalised memorial.', price: '$7,500' },
];
return (
<Box sx={{ maxWidth: 420 }}>
<Typography variant="h4" sx={{ mb: 2 }}>Choose a package</Typography>
<RadioGroup value={selected} onChange={(e) => setSelected(e.target.value)}>
{options.map((opt) => (
<Card
key={opt.value}
variant="outlined"
interactive
selected={selected === opt.value}
padding="compact"
sx={{ mb: 1 }}
onClick={() => setSelected(opt.value)}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<Radio value={opt.value} sx={{ mt: -0.5 }} />
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="label">{opt.label}</Typography>
<Typography variant="labelLg" color="primary">{opt.price}</Typography>
</Box>
<Typography variant="body2" color="text.secondary">{opt.desc}</Typography>
</Box>
</Box>
</Card>
))}
</RadioGroup>
</Box>
);
};
return <CardSelectDemo />;
},
};
// ─── Interactive: Payment Method ────────────────────────────────────────────
/** Horizontal radio group for payment method selection */
export const PaymentMethod: Story = {
name: 'Interactive — Payment Method',
render: () => (
<FormControl>
<Typography variant="label" sx={{ mb: 1 }}>Payment method</Typography>
<RadioGroup defaultValue="card" row>
<FormControlLabel value="card" control={<Radio />} label="Credit card" />
<FormControlLabel value="bank" control={<Radio />} label="Bank transfer" />
<FormControlLabel value="plan" control={<Radio />} label="Payment plan" />
</RadioGroup>
</FormControl>
),
};

View File

@@ -0,0 +1,33 @@
import React from 'react';
import MuiRadio from '@mui/material/Radio';
import type { RadioProps as MuiRadioProps } from '@mui/material/Radio';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Radio component */
export interface RadioProps extends MuiRadioProps {}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Radio button for the FA design system.
*
* Single-select control for mutually exclusive options. Wraps MUI Radio
* with FA brand tokens — warm gold fill when selected.
*
* From Parsons 1.0 Figma radio component — 16px circle with brand dot.
*
* Usage:
* - Always use inside a RadioGroup for proper keyboard navigation
* - Pair with FormControlLabel for accessible labels
* - For binary on/off, use Switch instead
* - For multi-select, use Checkbox (planned) or Chip
*/
export const Radio = React.forwardRef<HTMLButtonElement, RadioProps>(
(props, ref) => {
return <MuiRadio ref={ref} {...props} />;
},
);
Radio.displayName = 'Radio';
export default Radio;

View File

@@ -0,0 +1,2 @@
export { Radio, default } from './Radio';
export type { RadioProps } from './Radio';

View File

@@ -0,0 +1,134 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Switch } from './Switch';
import { Typography } from '../Typography';
import { Card } from '../Card';
import Box from '@mui/material/Box';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
const meta: Meta<typeof Switch> = {
title: 'Atoms/Switch',
component: Switch,
tags: ['autodocs'],
parameters: {
layout: 'centered',
design: {
type: 'figma',
url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=2322-42538',
},
},
argTypes: {
checked: {
control: 'boolean',
description: 'Whether the switch is on',
},
disabled: {
control: 'boolean',
description: 'Disable the switch',
table: { defaultValue: { summary: 'false' } },
},
},
};
export default meta;
type Story = StoryObj<typeof Switch>;
// ─── Default ────────────────────────────────────────────────────────────────
/** Default switch — unchecked */
export const Default: Story = {
args: {},
};
// ─── States ─────────────────────────────────────────────────────────────────
/** All visual states */
export const States: Story = {
render: () => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<FormControlLabel control={<Switch />} label="Unchecked" />
<FormControlLabel control={<Switch defaultChecked />} label="Checked" />
<FormControlLabel control={<Switch disabled />} label="Disabled unchecked" />
<FormControlLabel control={<Switch disabled defaultChecked />} label="Disabled checked" />
</Box>
),
};
// ─── Interactive: Service Add-ons ───────────────────────────────────────────
/**
* Realistic arrangement form pattern — toggle add-on services.
*/
export const ServiceAddOns: Story = {
name: 'Interactive — Service Add-ons',
render: () => {
const AddOnDemo = () => {
const [addOns, setAddOns] = useState({
catering: true,
flowers: true,
music: false,
memorial: false,
guestBook: false,
});
const toggle = (key: keyof typeof addOns) => {
setAddOns((prev) => ({ ...prev, [key]: !prev[key] }));
};
const items = [
{ key: 'catering' as const, label: 'Catering', desc: 'Light refreshments after the service', price: '$450' },
{ key: 'flowers' as const, label: 'Floral arrangements', desc: 'Seasonal flowers for the chapel', price: '$280' },
{ key: 'music' as const, label: 'Live music', desc: 'Organist or solo musician', price: '$350' },
{ key: 'memorial' as const, label: 'Memorial video', desc: 'Photo slideshow with music', price: '$200' },
{ key: 'guestBook' as const, label: 'Guest book', desc: 'Leather-bound memorial guest book', price: '$85' },
];
const total = items.reduce((sum, item) =>
addOns[item.key] ? sum + parseInt(item.price.replace('$', ''), 10) : sum, 0,
);
return (
<Box sx={{ maxWidth: 420 }}>
<Typography variant="h4" sx={{ mb: 2 }}>Service add-ons</Typography>
<FormGroup>
{items.map((item) => (
<Card key={item.key} variant="outlined" padding="compact" sx={{ mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ flex: 1 }}>
<Typography variant="label">{item.label}</Typography>
<Typography variant="body2" color="text.secondary">{item.desc}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="label" color="text.secondary">{item.price}</Typography>
<Switch checked={addOns[item.key]} onChange={() => toggle(item.key)} />
</Box>
</Box>
</Card>
))}
</FormGroup>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<Typography variant="labelLg">Total add-ons</Typography>
<Typography variant="labelLg" color="primary">${total}</Typography>
</Box>
</Box>
);
};
return <AddOnDemo />;
},
};
// ─── With Labels ────────────────────────────────────────────────────────────
/** FormControlLabel pairing — the recommended usage pattern */
export const WithLabels: Story = {
name: 'With Labels',
render: () => (
<FormGroup>
<FormControlLabel control={<Switch defaultChecked />} label="Email notifications" />
<FormControlLabel control={<Switch />} label="SMS notifications" />
<FormControlLabel control={<Switch defaultChecked />} label="Save arrangement progress" />
</FormGroup>
),
};

View File

@@ -0,0 +1,33 @@
import React from 'react';
import MuiSwitch from '@mui/material/Switch';
import type { SwitchProps as MuiSwitchProps } from '@mui/material/Switch';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Switch component */
export interface SwitchProps extends MuiSwitchProps {}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Toggle switch for the FA design system.
*
* Binary on/off control for enabling add-ons and options in arrangement
* forms. Wraps MUI Switch with FA brand tokens — warm gold track when
* active, bordered pill shape when inactive.
*
* From Parsons 1.0 Figma "Style One" — bordered variant with brand fill.
*
* Usage:
* - Use for boolean settings ("Include catering", "Add memorial video")
* - Pair with a label via FormControlLabel for accessibility
* - For mutually exclusive options, use Radio instead
*/
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
(props, ref) => {
return <MuiSwitch ref={ref} {...props} />;
},
);
Switch.displayName = 'Switch';
export default Switch;

View File

@@ -0,0 +1,2 @@
export { Switch, default } from './Switch';
export type { SwitchProps } from './Switch';

View File

@@ -26,6 +26,11 @@
--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-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-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-color-brand-50: #fef9f5; /** Lightest warm tint — warm section backgrounds */ --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-100: #f7ecdf; /** Light warm — hover backgrounds, subtle fills */
--fa-color-brand-200: #ebdac8; /** Warm light — secondary backgrounds, divider tones */ --fa-color-brand-200: #ebdac8; /** Warm light — secondary backgrounds, divider tones */
@@ -260,6 +265,7 @@
--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-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 */
--fa-color-text-tertiary: var(--fa-color-neutral-500); /** Tertiary text — placeholders, timestamps, attribution, meta information */ --fa-color-text-tertiary: var(--fa-color-neutral-500); /** Tertiary text — placeholders, timestamps, attribution, meta information */

View File

@@ -72,6 +72,12 @@ 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 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 SwitchTrackWidth = "44px"; // Track width — slightly narrower than Figma 52px for better proportion with 44px touch target
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 ColorBrand50 = "#fef9f5"; // Lightest warm tint — warm section backgrounds export const ColorBrand50 = "#fef9f5"; // Lightest warm tint — warm section backgrounds
export const ColorBrand100 = "#f7ecdf"; // Light warm — hover backgrounds, subtle fills export const ColorBrand100 = "#f7ecdf"; // Light warm — hover backgrounds, subtle fills
export const ColorBrand200 = "#ebdac8"; // Warm light — secondary backgrounds, divider tones export const ColorBrand200 = "#ebdac8"; // Warm light — secondary backgrounds, divider tones

View File

@@ -636,6 +636,75 @@ export const theme = createTheme({
}, },
}, },
}, },
MuiSwitch: {
styleOverrides: {
root: {
width: parseInt(t.SwitchTrackWidth, 10) + 12,
height: parseInt(t.SwitchTrackHeight, 10) + 12,
padding: 6,
},
switchBase: {
padding: 9,
'&.Mui-checked': {
transform: `translateX(${parseInt(t.SwitchTrackWidth, 10) - parseInt(t.SwitchTrackHeight, 10)}px)`,
color: t.ColorWhite,
'& + .MuiSwitch-track': {
backgroundColor: t.ColorInteractiveDefault,
borderColor: t.ColorInteractiveDefault,
opacity: 1,
},
'&:hover + .MuiSwitch-track': {
backgroundColor: t.ColorInteractiveHover,
},
},
'&.Mui-disabled + .MuiSwitch-track': {
opacity: 0.4,
},
'&:focus-visible + .MuiSwitch-track': {
outline: `2px solid ${t.ColorInteractiveFocus}`,
outlineOffset: '2px',
},
},
thumb: {
width: parseInt(t.SwitchThumbSize, 10),
height: parseInt(t.SwitchThumbSize, 10),
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
},
track: {
borderRadius: parseInt(t.SwitchTrackBorderRadius, 10),
backgroundColor: t.ColorWhite,
border: `1.5px solid ${t.ColorNeutral400}`,
opacity: 1,
transition: 'background-color 150ms ease-in-out, border-color 150ms ease-in-out',
},
},
},
MuiRadio: {
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: '50%',
},
'&.Mui-disabled': {
color: t.ColorNeutral300,
},
},
},
},
}, },
}); });

View File

@@ -0,0 +1,15 @@
{
"radio": {
"$description": "Radio component tokens — single-select control for mutually exclusive options. Used in service selection, payment method, and arrangement forms.",
"size": {
"$type": "dimension",
"$description": "Radio button outer circle size.",
"default": { "$value": "20px", "$description": "Default radio size — matches Figma 16px + padding for 44px touch target area" }
},
"dotSize": {
"$type": "dimension",
"$description": "Inner dot size when selected.",
"default": { "$value": "10px", "$description": "Selected indicator dot — 50% of outer size" }
}
}
}

View File

@@ -0,0 +1,17 @@
{
"switch": {
"$description": "Switch component tokens — toggle control for enabling/disabling options. Used in arrangement forms for add-on services.",
"track": {
"$type": "dimension",
"$description": "Switch track dimensions.",
"width": { "$value": "44px", "$description": "Track width — slightly narrower than Figma 52px for better proportion with 44px touch target" },
"height": { "$value": "24px", "$description": "Track height" },
"borderRadius": { "$value": "{borderRadius.full}", "$description": "Pill shape" }
},
"thumb": {
"$type": "dimension",
"$description": "Switch thumb (knob) dimensions.",
"size": { "$value": "18px", "$description": "Thumb diameter — sits inside the track with 3px inset" }
}
}
}