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:
@@ -26,6 +26,10 @@ duplicates) and MUST update it after completing one.
|
||||
| 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). |
|
||||
| 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 |
|
||||
|
||||
## Molecules
|
||||
|
||||
@@ -456,6 +456,46 @@ Each entry follows this structure:
|
||||
- **Planned (5 organisms):** ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer
|
||||
|
||||
**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)
|
||||
- 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
|
||||
|
||||
@@ -244,3 +244,23 @@ the correct token for any design property.
|
||||
| chip.deleteIconSize.md | 16px | Chip | Medium chip delete icon |
|
||||
| chip.iconGap.default | → spacing.1 (4px) | Chip | Icon-to-text gap |
|
||||
| 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 |
|
||||
|
||||
146
src/components/atoms/Radio/Radio.stories.tsx
Normal file
146
src/components/atoms/Radio/Radio.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
33
src/components/atoms/Radio/Radio.tsx
Normal file
33
src/components/atoms/Radio/Radio.tsx
Normal 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;
|
||||
2
src/components/atoms/Radio/index.ts
Normal file
2
src/components/atoms/Radio/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Radio, default } from './Radio';
|
||||
export type { RadioProps } from './Radio';
|
||||
134
src/components/atoms/Switch/Switch.stories.tsx
Normal file
134
src/components/atoms/Switch/Switch.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
33
src/components/atoms/Switch/Switch.tsx
Normal file
33
src/components/atoms/Switch/Switch.tsx
Normal 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;
|
||||
2
src/components/atoms/Switch/index.ts
Normal file
2
src/components/atoms/Switch/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Switch, default } from './Switch';
|
||||
export type { SwitchProps } from './Switch';
|
||||
@@ -26,6 +26,11 @@
|
||||
--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-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-100: #f7ecdf; /** Light warm — hover backgrounds, subtle fills */
|
||||
--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-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-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-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 */
|
||||
|
||||
@@ -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 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 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 ColorBrand100 = "#f7ecdf"; // Light warm — hover backgrounds, subtle fills
|
||||
export const ColorBrand200 = "#ebdac8"; // Warm light — secondary backgrounds, divider tones
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
15
tokens/component/radio.json
Normal file
15
tokens/component/radio.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
17
tokens/component/switch.json
Normal file
17
tokens/component/switch.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user