Aller au contenu principal

ADR-033 : Composants UI Partagés — Abstraction de Thème & Patterns Réutilisables

Metadata

ChampValeur
StatutAccepté
Date2026-02-06
LinearCAB-1100

Contexte

STOA dispose de deux frontends React :

  • Console (control-plane-ui) — Pour les fournisseurs d'API et les administrateurs de plateforme
  • Portal — Pour les consommateurs d'API et les développeurs

Les deux applications ont besoin de :

  • Cohérence visuelle et d'expérience
  • Support des thèmes sombre/clair
  • Patterns UI communs (toasts, dialogues, états vides)

Le Problème

« Nous dupliquons des composants UI entre la console et le portal. Les modifications nécessitent de mettre à jour les deux. »

Décision

Créer une bibliothèque de composants partagés dans shared/ que les deux frontends importent.

Structure

shared/
├── components/
│ ├── Breadcrumb/
│ │ └── Breadcrumb.tsx
│ ├── Celebration/
│ │ └── Celebration.tsx
│ ├── Collapsible/
│ │ └── Collapsible.tsx
│ ├── CommandPalette/
│ │ ├── CommandPalette.tsx
│ │ └── CommandPalette.css
│ ├── ConfirmDialog/
│ │ └── ConfirmDialog.tsx
│ ├── EmptyState/
│ │ └── EmptyState.tsx
│ ├── FormWizard/
│ │ └── FormWizard.tsx
│ ├── Skeleton/
│ │ └── Skeleton.tsx
│ ├── ThemeToggle/
│ │ └── ThemeToggle.tsx
│ └── Toast/
│ ├── Toast.tsx
│ └── Toast.css

└── contexts/
└── ThemeContext.tsx

Composants

ThemeContext

Gestion centralisée du thème :

interface ThemeContextValue {
theme: 'light' | 'dark' | 'system';
setTheme: (theme: 'light' | 'dark' | 'system') => void;
resolvedTheme: 'light' | 'dark';
}

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
const resolvedTheme = useResolvedTheme(theme);

useEffect(() => {
document.documentElement.classList.toggle('dark', resolvedTheme === 'dark');
}, [resolvedTheme]);

return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
};

CommandPalette

Recherche et navigation globales :

interface CommandPaletteProps {
commands: Command[];
onSelect: (command: Command) => void;
placeholder?: string;
}

// Usage
<CommandPalette
commands={[
{ id: 'apis', label: 'Go to APIs', action: () => navigate('/apis') },
{ id: 'tenants', label: 'Go to Tenants', action: () => navigate('/tenants') },
]}
onSelect={(cmd) => cmd.action()}
/>

Toast

Système de notifications :

interface ToastProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
duration?: number;
onClose: () => void;
}

// Usage via hook
const { toast } = useToast();
toast.success('API created successfully');
toast.error('Failed to delete tenant');

EmptyState

Affichages d'état vide cohérents :

<EmptyState
icon={<FolderOpen />}
title="No APIs found"
description="Create your first API to get started"
action={<Button onClick={createApi}>Create API</Button>}
/>

Pattern d'Import

Les frontends importent depuis un chemin relatif :

// control-plane-ui/src/App.tsx
import { ThemeProvider } from '../../shared/contexts/ThemeContext';
import { Toast } from '../../shared/components/Toast';
import { CommandPalette } from '../../shared/components/CommandPalette';

Principes du Design System

PrincipeImplémentation
CohérenceMêmes composants dans les deux applications
AccessibilitéLabels ARIA, navigation au clavier
ResponsiveMobile-first, breakpoints
Mode SombreVariables CSS, préférence système

Conséquences

Positives

  • DRY — Pas de code de composant dupliqué
  • Cohérence — UX identique entre les applications
  • Maintenabilité — Source unique de vérité
  • Synchronisation du thème — Le mode sombre fonctionne partout

Négatives

  • Couplage — Les modifications affectent les deux applications
  • Complexité de build — Code partagé dans le monorepo
  • Versionning — Pas de releases indépendantes

Atténuations

DéfiAtténuation
Changements majeursLa PR nécessite de tester les deux applications
Complexité de buildConfiguration d'alias Vite
VersionningLe monorepo assure la synchronisation

Références