Align design system with ADS 3.0 and add new components

Token foundation: fix 16 palette colours to match official ADS_COLORS,
add 5 new palettes (teal, brown, purple, fuchsia, yellow), realign
semantic tokens (primary=navy, info=bright blue), fix border radii to
8px base, add responsive heading typography.

Component migration: swap primary/info references across all existing
components, update Button (44px/semibold), Switch (green/compact),
Chip (30px/8px radius + colour variants), SideNav (80px rail), Tag
(11 colours).

New components: SideNav, TopBar, Avatar, Tabs, PageHeader, Slider,
RangeSlider, FileInput, DataTable, List, Autocomplete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:24:23 +10:00
parent f4fd1fc04b
commit d915443b8c
45 changed files with 3029 additions and 54 deletions

View File

@@ -0,0 +1,82 @@
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import { Tabs, TabList, Tab, TabPanel } from './Tabs'
const meta: Meta<typeof Tabs> = {
title: 'Atoms/Tabs',
component: Tabs,
tags: ['autodocs'],
parameters: { layout: 'padded' },
}
export default meta
type Story = StoryObj<typeof Tabs>
const BasicTemplate = () => {
const [value, setValue] = useState('tab1')
return (
<Tabs value={value} onChange={setValue}>
<TabList>
<Tab value="tab1">Overview</Tab>
<Tab value="tab2">Details</Tab>
<Tab value="tab3">History</Tab>
</TabList>
<TabPanel value="tab1">Overview content goes here.</TabPanel>
<TabPanel value="tab2">Details content goes here.</TabPanel>
<TabPanel value="tab3">History content goes here.</TabPanel>
</Tabs>
)
}
export const Default: Story = {
render: () => <BasicTemplate />,
}
const WithIconsTemplate = () => {
const [value, setValue] = useState('status')
const StatusIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" /></svg>
)
const DetailsIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" /></svg>
)
return (
<Tabs value={value} onChange={setValue}>
<TabList>
<Tab value="status" icon={<StatusIcon />}>Status</Tab>
<Tab value="details" icon={<DetailsIcon />}>Details</Tab>
<Tab value="disabled" disabled>Disabled</Tab>
</TabList>
<TabPanel value="status">Status panel content.</TabPanel>
<TabPanel value="details">Details panel content.</TabPanel>
</Tabs>
)
}
export const WithIcons: Story = {
name: 'With icons',
render: () => <WithIconsTemplate />,
}
const ManyTabsTemplate = () => {
const [value, setValue] = useState('tab1')
return (
<Tabs value={value} onChange={setValue}>
<TabList>
{Array.from({ length: 8 }, (_, i) => (
<Tab key={i} value={`tab${i + 1}`}>Tab {i + 1}</Tab>
))}
</TabList>
{Array.from({ length: 8 }, (_, i) => (
<TabPanel key={i} value={`tab${i + 1}`}>Content for tab {i + 1}</TabPanel>
))}
</Tabs>
)
}
export const ManyTabs: Story = {
name: 'Many tabs',
render: () => <ManyTabsTemplate />,
}

View File

@@ -0,0 +1,141 @@
import {
createContext,
forwardRef,
useContext,
useId,
useMemo,
type HTMLAttributes,
type ReactNode,
} from 'react'
import { cn } from '@/lib/utils'
// --- Context ---
interface TabsContextValue {
value: string
onChange: (value: string) => void
baseId: string
}
const TabsContext = createContext<TabsContextValue | null>(null)
function useTabsContext() {
const ctx = useContext(TabsContext)
if (!ctx) throw new Error('Tab components must be used within Tabs')
return ctx
}
// --- Tabs ---
export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
value: string
onChange: (value: string) => void
}
export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
({ value, onChange, className, children, ...props }, ref) => {
const baseId = useId()
const ctx = useMemo(() => ({ value, onChange, baseId }), [value, onChange, baseId])
return (
<TabsContext.Provider value={ctx}>
<div ref={ref} className={className} {...props}>
{children}
</div>
</TabsContext.Provider>
)
},
)
Tabs.displayName = 'Tabs'
// --- TabList ---
export interface TabListProps extends HTMLAttributes<HTMLDivElement> {}
export const TabList = forwardRef<HTMLDivElement, TabListProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
role="tablist"
className={cn('flex border-b border-border', className)}
{...props}
>
{children}
</div>
),
)
TabList.displayName = 'TabList'
// --- Tab ---
export interface TabProps extends HTMLAttributes<HTMLButtonElement> {
value: string
icon?: ReactNode
disabled?: boolean
}
export const Tab = forwardRef<HTMLButtonElement, TabProps>(
({ value, icon, disabled = false, className, children, ...props }, ref) => {
const { value: selected, onChange, baseId } = useTabsContext()
const isSelected = value === selected
return (
<button
ref={ref}
role="tab"
type="button"
id={`${baseId}-tab-${value}`}
aria-selected={isSelected}
aria-controls={`${baseId}-panel-${value}`}
tabIndex={isSelected ? 0 : -1}
disabled={disabled}
onClick={() => onChange(value)}
className={cn(
'relative flex items-center gap-2 px-4 py-3 text-body font-semibold transition-colors',
'focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-info',
isSelected
? 'text-primary'
: 'text-text/80 hover:text-text',
isSelected && 'after:absolute after:bottom-0 after:left-0 after:right-0 after:h-1 after:bg-error',
disabled && 'pointer-events-none opacity-55',
className,
)}
{...props}
>
{icon && <span className="size-5 shrink-0 [&>svg]:size-full">{icon}</span>}
{children}
</button>
)
},
)
Tab.displayName = 'Tab'
// --- TabPanel ---
export interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
value: string
}
export const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(
({ value, className, children, ...props }, ref) => {
const { value: selected, baseId } = useTabsContext()
const isSelected = value === selected
if (!isSelected) return null
return (
<div
ref={ref}
role="tabpanel"
id={`${baseId}-panel-${value}`}
aria-labelledby={`${baseId}-tab-${value}`}
tabIndex={0}
className={cn('pt-4', className)}
{...props}
>
{children}
</div>
)
},
)
TabPanel.displayName = 'TabPanel'

View File

@@ -0,0 +1,2 @@
export { Tabs, TabList, Tab, TabPanel } from './Tabs'
export type { TabsProps, TabListProps, TabProps, TabPanelProps } from './Tabs'