# Structura — Component contract

All components exported here are the stable API consumed by screens [7–11].
**Do not redefine these.** Import from `@/components/...`.

---

## Shell

### `AppShell`
```tsx
import { AppShell } from '@/components/shell/AppShell'
<AppShell topbarContent={<Breadcrumbs items={[...]} />}>
  {/* page content */}
</AppShell>
```
Props: `children: ReactNode`, `topbarContent?: ReactNode`

### `Sidebar`
Auto-rendered by `AppShell`. Active link via `usePathname`. Routes: `/`, `/projects`, `/analyses`, `/partners`, `/alerts`, `/settings`, `/help`.

### `Topbar`
Auto-rendered by `AppShell`. `children` slot = breadcrumbs. Contains: search input (simulated), 3 status dots, user menu.

### `PageHeader`
```tsx
import { PageHeader } from '@/components/shell/PageHeader'
<PageHeader title="Projets" description="..." actions={<Button>...</Button>} />
```
Props: `title: string`, `description?: string`, `actions?: ReactNode`

### `Breadcrumbs`
```tsx
import { Breadcrumbs } from '@/components/shell/Breadcrumbs'
<Breadcrumbs items={[{ label: 'Projets', href: '/projects' }, { label: 'Pont A13' }]} />
```
Props: `items: { label: string; href?: string }[]`, `className?: string`

---

## UI primitives

### `Button`
```tsx
import { Button } from '@/components/ui/Button'
<Button variant="primary" size="md" loading={false} iconLeft={<Icon />}>Lancer</Button>
```
Props: `variant?: 'primary'|'secondary'|'ghost'|'danger'`, `size?: 'sm'|'md'`, `loading?: boolean`, `iconLeft?: ReactNode`, `iconRight?: ReactNode` + all HTML button attrs.

### `Card`
```tsx
import { Card } from '@/components/ui/Card'
<Card padding="md" interactive header={<h2>Titre</h2>}>content</Card>
```
Props: `padding?: 'sm'|'md'|'lg'`, `interactive?: boolean`, `header?: ReactNode`, `className?`

### `Badge`
```tsx
import { Badge } from '@/components/ui/Badge'
<Badge variant="success" size="sm" icon={<Icon />}>Actif</Badge>
```
Props: `variant?: 'success'|'warning'|'danger'|'info'|'neutral'|'accent'`, `size?: 'xs'|'sm'`, `icon?: ReactNode`

### `Stat`
```tsx
import { Stat } from '@/components/ui/Stat'
<Stat label="Capteurs en ligne" value={42} unit="/" delta={5} trend="up" />
```
Props: `label: string`, `value: number`, `unit?: string`, `delta?: number`, `trend?: 'up'|'down'|'flat'`, `digits?: number`

### `Panel`
```tsx
import { Panel } from '@/components/ui/Panel'
<Panel title="Capteur A1" onClose={() => {}} width="320px" animate>content</Panel>
```
Props: `title: string`, `onClose?: () => void`, `width?: string`, `animate?: boolean`, `children`

### `Modal`
```tsx
import { Modal } from '@/components/ui/Modal'
<Modal open={open} onClose={() => setOpen(false)} title="Déploiement sécurisé" size="xl">
  content
</Modal>
```
Props: `open: boolean`, `onClose: () => void`, `title: string`, `size?: 'md'|'lg'|'xl'`, `children`

### `Tabs`
```tsx
import { Tabs } from '@/components/ui/Tabs'
const tabs = [{ id: 'amplitude', label: 'Amplitude' }, { id: 'fft', label: 'Spectre FFT' }]
<Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
```
Props: `tabs: { id: string; label: string; icon?: ReactNode }[]`, `active: string`, `onChange: (id: string) => void`

### `Input`
```tsx
import { Input } from '@/components/ui/Input'
<Input placeholder="Rechercher..." iconLeft={<Search size={14} />} value={q} onChange={...} />
```
Props: all HTML input attrs + `iconLeft?: ReactNode`

### `Select`
```tsx
import { Select } from '@/components/ui/Select'
<Select options={[{ value: 'bridge', label: 'Pont' }]} value={v} onChange={setV} placeholder="Type" />
```
Props: `options: { value: string; label: string }[]`, `value: string`, `onChange: (v: string) => void`, `placeholder?`

### `Tooltip`
```tsx
import { Tooltip } from '@/components/ui/Tooltip'
<Tooltip content="Valeur mesurée en mm/s²" side="top"><span>...</span></Tooltip>
```
Props: `content: ReactNode`, `side?: 'top'|'bottom'|'left'|'right'`, `children`

### `StatusDot`
```tsx
import { StatusDot } from '@/components/ui/StatusDot'
<StatusDot status="online" label="En ligne" size="md" />
```
Props: `status: 'online'|'offline'|'warning'|'critical'|'info'|'calibrating'`, `label?: string`, `size?: 'sm'|'md'`

### `CounterValue`
```tsx
import { CounterValue } from '@/components/ui/CounterValue'
<CounterValue value={42} digits={0} className="text-mono-large text-text-primary" />
```
Props: `value: number`, `digits?: number`, `duration?: number` (default 800ms), `className?`

### `EmptyState`
```tsx
import { EmptyState } from '@/components/ui/EmptyState'
<EmptyState icon={<FolderOpen />} title="Aucun projet" description="..." action={<Button>Créer</Button>} />
```
Props: `icon?: ReactNode`, `title: string`, `description?: string`, `action?: ReactNode`

### `Skeleton`
```tsx
import { Skeleton } from '@/components/ui/Skeleton'
<Skeleton width={200} height={16} rounded="md" />
```
Props: `width?: string|number`, `height?: string|number`, `rounded?: 'sm'|'md'|'lg'|'full'`

---

## Charts

### `LineChart`
```tsx
import { LineChart } from '@/components/charts/LineChart'
<LineChart series={[{ name: 'Amplitude', points: [{x:0,y:1},...], color: '#3DB8E8' }]} height={240} showGrid showAxis />
```
Props: `series: DataSeries[]` (from `@/types`), `height?: number`, `showGrid?: boolean`, `showAxis?: boolean`

### `BarChart`
```tsx
import { BarChart } from '@/components/charts/BarChart'
<BarChart data={[{ label: '10Hz', value: 0.5 }]} height={200} color="#3DB8E8" />
```
Props: `data: { label: string; value: number; color?: string }[]`, `height?: number`, `color?: string`, `showLabels?: boolean`

### `Heatmap`
```tsx
import { Heatmap } from '@/components/charts/Heatmap'
<Heatmap matrix={[[0.1, 0.9], [0.3, 0.7]]} rows={['C1','C2']} cols={['T1','T2']} cellSize={28} />
```
Props: `matrix: number[][]`, `rows: string[]`, `cols: string[]`, `colorScale?: (v,min,max) => string`, `cellSize?: number`

### `SparkLine`
```tsx
import { SparkLine } from '@/components/charts/SparkLine'
<SparkLine points={[1,2,1.5,3,2.8]} width={80} height={24} color="#3DB8E8" />
```
Props: `points: number[]`, `width?: number`, `height?: number`, `color?: string`

### `RingMeter`
```tsx
import { RingMeter } from '@/components/charts/RingMeter'
<RingMeter value={87} max={100} label="intégrité" color="#34D399" size={64} />
```
Props: `value: number`, `max?: number` (default 100), `label?: string`, `color?: string`, `size?: number`, `strokeWidth?: number`

---

## Three.js

### `SceneWrapper`
```tsx
import { SceneWrapper } from '@/components/three/SceneWrapper'
<SceneWrapper autoRotate autoRotateSpeed={0.5} enableInteraction={false} className="h-[320px]">
  {/* r3f objects: mesh, group, etc. */}
</SceneWrapper>
```
Props: `autoRotate?: boolean`, `autoRotateSpeed?: number`, `enableInteraction?: boolean`, `className?`, `style?`, `children?`

### `SceneLoader`
```tsx
import { SceneLoader } from '@/components/three/Loader'
<SceneLoader message="Chargement scène…" />
```
Used automatically by SceneWrapper's `loading` prop. Can be used manually as skeleton.

---

## Utility

### `cn`
```ts
import { cn } from '@/lib/utils'
cn('base-class', condition && 'conditional-class', className)
```

### `formatNumber`, `formatDate`, `formatDateShort`, `relativeTime`, `formatUnit`
```ts
import { formatNumber, formatDate, relativeTime, formatUnit } from '@/lib/utils'
formatUnit(1.234, 'mm/s²', 3) // → "1,234 mm/s²"
relativeTime('2026-04-15T10:00:00Z') // → "il y a 2h"
```
