docs(plans): atelier Workbench refonte — implementation plan (3 phases)

This commit is contained in:
2026-06-13 23:22:41 +02:00
parent 00f8770ec0
commit 808d8bca2a
@@ -0,0 +1,584 @@
# Atelier Refonte « Workbench Console » Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Refondre l'UI de `apps/atelier` : retirer la simulation 3D, appliquer un thème néo-rétro « Workbench » (palette firmware), et remplacer la colonne 3D par un Banc d'essai (aperçu écran device fidèle + vue flux du scénario, split vertical). Blockly est conservé.
**Architecture:** Trois phases séquentielles — A) soustraction 3D (build doit rester vert), B) thème Workbench (CSS + thème Blockly + re-skin), C) Banc d'essai (`WorkbenchPane` = `DeviceScreen` + `FlowGraph` pilotés par un `selectionStore`, lisant l'IR de `runtimeStore`).
**Tech Stack:** React 19, TypeScript (strict, `exactOptionalPropertyTypes`), Zustand, react-resizable-panels, Blockly, Vite. Verif par tâche : `pnpm exec tsc --noEmit` + `pnpm exec vite build` verts (pas de tests unitaires pour le rendu visuel ; les hooks e2e existants sont mis à jour en phase A).
**Réfs :** spec `docs/superpowers/specs/2026-06-13-atelier-refonte-workbench-design.md` ; types IR `frontend-v3/packages/scenario-engine/src/runtime-ir.ts` (`RuntimeIr.steps: RuntimeStep[]`, `RuntimeStep.id`, `RuntimeStep.scene?: StepScene{title,subtitle,symbol,effect}`, `RuntimeTransition.target_step_id`).
**Convention commit :** `git -c user.name=electron-rare -c user.email=c.saillant@gmail.com commit`. Toutes les commandes depuis `frontend-v3/apps/atelier` sauf indication.
---
## File Structure
| Fichier | Responsabilité | Phase |
|---|---|---|
| `src/components/StagePane.tsx` + `src/scene/*` + `src/puzzles/*` + `src/modes/*` + `src/stores/simStore.ts` | **supprimés** (3D) | A |
| `src/components/Layout.tsx` | layout : Blockly + `WorkbenchPane` + console ; toggle `⌘B` du banc | A, C |
| `src/components/ConsolePane.tsx` | console validation seule (retrait des onglets de modes) | A |
| `src/App.tsx` | hooks dev sans `sim` | A |
| `vite.config.ts` | retrait `manualChunks` three/r3f | A |
| `package.json` | retrait deps three/r3f | A |
| `src/theme/workbench.ts` | constantes palette/fonts (source unique) | B |
| `src/main.css` | shell + chrome Workbench | B |
| `src/components/BlocklyWorkspace.tsx` | thème Blockly Workbench | B |
| `src/components/MenuBar.tsx`, panneaux flottants | re-skin | B |
| `src/lib/screenSpec.ts` | déjà présent (P3a) — source des couleurs/limites écran | B (réf) |
| `src/components/DeviceScreenView.tsx` | rendu LCD fidèle réutilisable (extrait de `ScreenEditorPanel`) | C |
| `src/stores/selectionStore.ts` | `selectedStepId` | C |
| `src/lib/irSteps.ts` | parse `currentIr``{steps, edges}` | C |
| `src/components/FlowGraph.tsx` | graphe d'étapes cliquable | C |
| `src/components/DeviceScreen.tsx` | écran de l'étape sélectionnée | C |
| `src/components/WorkbenchPane.tsx` | split vertical Écran/Flux | C |
---
## PHASE A — Retrait de la 3D
### Task A1: Supprimer les fichiers 3D et le simStore
**Files:**
- Delete: `src/components/StagePane.tsx`, `src/scene/` (NpcBubble, PuzzleStation, PuzzleStations, Room, RoomScene, RtcPhone), `src/puzzles/` (P1Sound, P5Morse, P6Symbols, P7Coffre), `src/modes/` (DemoMode, SandboxMode, TestMode), `src/stores/simStore.ts`
- [ ] **Step 1: Supprimer les répertoires/fichiers 3D**
```bash
cd frontend-v3/apps/atelier
git rm -r src/scene src/puzzles src/modes src/components/StagePane.tsx src/stores/simStore.ts
```
- [ ] **Step 2: Lister les imports résiduels à corriger**
Run: `grep -rnE "simStore|StagePane|/scene/|/puzzles/|/modes/" src/ || echo "AUCUN"`
Expected: occurrences dans `Layout.tsx`, `App.tsx`, `ConsolePane.tsx`, `StagePane`-consumers — traitées aux tâches suivantes. (Ne pas committer encore : le build est cassé jusqu'à A4.)
### Task A2: Layout sans StagePane (placeholder banc en phase A)
**Files:**
- Modify: `src/components/Layout.tsx`
- [ ] **Step 1: Remplacer l'import + le panneau droit**
Dans `Layout.tsx`, retirer `import { StagePane } from './StagePane.js';` et remplacer le `<Panel ...><StagePane /></Panel>` droit par un placeholder neutre (le vrai `WorkbenchPane` arrive en phase C) :
```tsx
// en tête : remplacer l'import StagePane par :
// (phase C remplacera ce placeholder par WorkbenchPane)
// dans le PanelGroup horizontal, panneau droit :
<Panel
ref={stageRef}
defaultSize={45}
minSize={20}
collapsible
collapsedSize={0}
>
<div className="atelier-pane" style={{ display: 'grid', placeItems: 'center', color: '#9a9a9d' }}>
Banc d'essai (à venir)
</div>
</Panel>
```
Garder le toggle `⌘B` et `stageRef` inchangés (ils piloteront le banc).
- [ ] **Step 2: Vérifier qu'aucune réf StagePane ne subsiste dans Layout**
Run: `grep -n "StagePane" src/components/Layout.tsx || echo "OK"`
Expected: `OK`
### Task A3: ConsolePane sans onglets de modes
**Files:**
- Modify: `src/components/ConsolePane.tsx`
- [ ] **Step 1: Inspecter les dépendances sim de la console**
Run: `grep -nE "simStore|mode|Mode|SandboxMode|DemoMode|TestMode" src/components/ConsolePane.tsx`
Expected: identifie les imports/usages liés aux modes.
- [ ] **Step 2: Retirer l'état mode + l'onglet de modes, garder la validation**
Retirer de `ConsolePane.tsx` tout import de `simStore`/`modes/*` et le sélecteur d'onglets de modes. Conserver le rendu de la liste de validation (entries de `validationStore`). Le composant ne garde qu'un titre « Validation » + la liste des entrées. Remplacer toute lecture `simStore.getState().mode` par rien (supprimer la branche).
- [ ] **Step 3: Vérifier**
Run: `grep -nE "simStore|/modes/" src/components/ConsolePane.tsx || echo "OK"`
Expected: `OK`
### Task A4: App.tsx — hooks dev sans `sim`, retrait deps, build vert
**Files:**
- Modify: `src/App.tsx`, `vite.config.ts`, `package.json`
- [ ] **Step 1: Retirer `sim` des hooks dev de App.tsx**
Dans `src/App.tsx`, retirer `import { useSimStore } from './stores/simStore.js';` et la clé `sim: useSimStore,` de l'objet `window.__atelierStores`. Laisser `editor`, `runtime`, `validation`.
- [ ] **Step 2: Retirer le manualChunks three/r3f de vite.config.ts**
Run: `grep -nE "three|fiber|drei|r3f" vite.config.ts`
Puis supprimer les entrées `manualChunks` correspondantes (three / r3f) du `build.rollupOptions.output`. Garder le chunk blockly.
- [ ] **Step 3: Retirer les deps 3D de package.json**
```bash
cd frontend-v3/apps/atelier
pnpm remove three @types/three @react-three/fiber @react-three/drei
```
Expected: met à jour `package.json` + lockfile.
- [ ] **Step 4: Typecheck + build verts**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS, aucun import three/r3f restant. Si erreur « cannot find module ./stores/simStore » ou scene/puzzles/modes → corriger le consommateur résiduel signalé.
- [ ] **Step 5: Nettoyer les specs e2e pilotant la sim**
Run: `grep -rnlE "__atelierStores.*sim|simStore|SandboxMode|DemoMode|TestMode" ../../../e2e ../../e2e 2>/dev/null || echo "AUCUN e2e sim"`
Pour chaque spec trouvée : retirer/mettre à jour les assertions sur `sim`/modes (la sim n'existe plus). Si aucune, passer.
- [ ] **Step 6: Commit phase A**
```bash
cd frontend-v3/apps/atelier
git add -A && cd ../../.. && git add -A
git -c user.name=electron-rare -c user.email=c.saillant@gmail.com commit -m "refactor(atelier): remove 3D simulation (StagePane/scene/puzzles/modes/simStore, three deps)"
```
---
## PHASE B — Thème Workbench
### Task B1: Module de thème (source unique)
**Files:**
- Create: `src/theme/workbench.ts`
- [ ] **Step 1: Créer les constantes de thème**
```ts
// src/theme/workbench.ts
// Palette néo-rétro « Workbench » — alignée sur ESP32_ZACUS display_ui.cpp
// (COL_BG/COL_CODE/COL_VALUE/COL_LABEL) et lib/screenSpec.ts.
export const WB = {
bg: '#0a1722', // surface profonde (dérivée du bleu Workbench assombri)
panel: '#0f2233', // panneaux
panelBorder: '#1c3b52',
workbenchBlue: '#0055AA', // COL_BG firmware (accents/écran)
orange: '#FF8800', // COL_CODE — accent principal
value: '#FFFFFF', // COL_VALUE
label: '#AAAAAA', // COL_LABEL
led: { ok: '#34d399', warn: '#fbbf24', err: '#fca5a5', off: '#3b4a57' },
fontTitle: "'Orbitron','Arial Narrow',sans-serif",
fontMono: "'IBM Plex Mono',ui-monospace,monospace",
} as const;
```
- [ ] **Step 2: Typecheck**
Run: `pnpm exec tsc --noEmit`
Expected: PASS.
### Task B2: Polices locales + shell CSS Workbench
**Files:**
- Modify: `src/main.css`
- [ ] **Step 1: Charger Orbitron + IBM Plex Mono**
Les polices sont déjà bundlées par le firmware mais pas le front. Utiliser `@fontsource` (offline, pas de CDN) :
```bash
cd frontend-v3/apps/atelier
pnpm add @fontsource/orbitron @fontsource/ibm-plex-mono
```
Puis en tête de `src/main.css` :
```css
@import '@fontsource/orbitron/400.css';
@import '@fontsource/orbitron/700.css';
@import '@fontsource/ibm-plex-mono/400.css';
```
- [ ] **Step 2: Appliquer le thème au shell**
Dans `src/main.css`, mettre à jour `.atelier-shell`, `.atelier-pane`, `.atelier-resizer*` :
```css
.atelier-shell { background: #0a1722; color: #e8eef3; font-family: 'IBM Plex Mono',ui-monospace,monospace; }
.atelier-pane { background: #0f2233; border: 1px solid #1c3b52; }
.atelier-resizer { background: #1c3b52; }
.atelier-resizer:hover { background: #FF8800; }
/* légère grille de fond façon plan de travail */
.atelier-shell::before {
content: ''; position: fixed; inset: 0; pointer-events: none; opacity: 0.05;
background-image: linear-gradient(#FF8800 1px,transparent 1px),linear-gradient(90deg,#FF8800 1px,transparent 1px);
background-size: 32px 32px;
}
```
- [ ] **Step 3: Build vert**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS (les fonts apparaissent comme assets).
### Task B3: Barre-titre labo (MenuBar)
**Files:**
- Modify: `src/components/MenuBar.tsx`
- [ ] **Step 1: Re-skin de la barre**
Ajouter à gauche un titre `▦ ZACUS · ATELIER` en `WB.fontTitle`, couleur `WB.orange`, letter-spacing 2px ; fond `WB.panel`, bordure basse `WB.panelBorder`. Conserver toutes les actions existantes (ouvrir/charger/enregistrer/partager) — seulement restyler (boutons : fond `WB.panel`, bordure `WB.panelBorder`, texte `#e8eef3`, hover bordure `WB.orange`). Importer `WB` depuis `../theme/workbench.js`.
- [ ] **Step 2: Build vert**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS.
### Task B4: Thème Blockly Workbench
**Files:**
- Modify: `src/components/BlocklyWorkspace.tsx`
- [ ] **Step 1: Définir + appliquer un thème Blockly**
Avant `Blockly.inject`, créer un thème et le passer dans les options :
```ts
import * as Blockly from 'blockly';
const workbenchTheme = Blockly.Theme.defineTheme('zacus-workbench', {
name: 'zacus-workbench',
base: Blockly.Themes.Classic,
componentStyles: {
workspaceBackgroundColour: '#0a1722',
toolboxBackgroundColour: '#0f2233',
toolboxForegroundColour: '#e8eef3',
flyoutBackgroundColour: '#0f2233',
flyoutForegroundColour: '#AAAAAA',
scrollbarColour: '#1c3b52',
insertionMarkerColour: '#FF8800',
gridColour: '#1c3b52',
},
fontStyle: { family: "'IBM Plex Mono',monospace", size: 11 },
});
// dans les options d'inject : theme: workbenchTheme, grid: { spacing: 24, colour: '#13283a', snap: true }
```
- [ ] **Step 2: Build vert + sanity**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS. (Vérif visuelle ultérieure.)
- [ ] **Step 3: Commit phase B**
```bash
cd ../../.. && git add -A
git -c user.name=electron-rare -c user.email=c.saillant@gmail.com commit -m "feat(atelier): Workbench theme (palette firmware, Orbitron/IBM Plex Mono, Blockly theme)"
```
---
## PHASE C — Banc d'essai (Écran + Flux)
### Task C1: Extraire le rendu LCD fidèle réutilisable
**Files:**
- Create: `src/components/DeviceScreenView.tsx`
- Modify: `src/components/ScreenEditorPanel.tsx` (importer la vue partagée)
- [ ] **Step 1: Créer `DeviceScreenView` à partir du `ScreenPreview` de P3a**
Déplacer le composant `ScreenPreview` (et `symbolAnimation`) de `ScreenEditorPanel.tsx` vers un fichier partagé, paramétré par l'échelle :
```tsx
// src/components/DeviceScreenView.tsx
import { SCREEN, type SceneDraft, type ScreenEffect } from '../lib/screenSpec.js';
function symbolAnimation(effect: ScreenEffect): string | undefined {
if (effect === 'pulse') return 'zacus-pulse 2.4s ease-in-out infinite';
if (effect === 'glitch') return 'zacus-glitch 0.45s steps(2) infinite';
return undefined;
}
export function DeviceScreenView({ scene, scale = 0.66 }: { scene: SceneDraft; scale?: number }) {
return (
<div style={{ width: SCREEN.width * scale, height: SCREEN.height * scale, alignSelf: 'center' }}>
<div style={{
width: SCREEN.width, height: SCREEN.height, transform: `scale(${scale})`, transformOrigin: 'top left',
background: SCREEN.bg, position: 'relative', borderRadius: 6, overflow: 'hidden',
boxShadow: 'inset 0 0 0 2px rgba(0,0,0,0.4)',
}}>
<div style={{ position: 'absolute', top: 24, left: 12, right: 12, textAlign: 'center',
color: SCREEN.title.color, font: `700 40px ${SCREEN.title.font}`, letterSpacing: 2,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{scene.title}</div>
<div style={{ position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{scene.effect === 'gyro' && scene.symbol && (
<span style={{ position: 'absolute', width: 120, height: 120, borderRadius: '50%',
border: `3px solid ${SCREEN.symbol.color}`, borderTopColor: 'transparent',
animation: 'zacus-gyro-spin 3s linear infinite' }} />
)}
<span style={{ color: SCREEN.symbol.color, font: `700 40px ${SCREEN.symbol.font}`, letterSpacing: 3,
animation: symbolAnimation(scene.effect) }}>{scene.symbol}</span>
</div>
<div style={{ position: 'absolute', bottom: 10, left: 8, right: 8,
color: SCREEN.subtitle.color, font: `18px ${SCREEN.subtitle.font}`,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{scene.subtitle}</div>
</div>
</div>
);
}
```
- [ ] **Step 2: Réutiliser dans `ScreenEditorPanel`**
Dans `ScreenEditorPanel.tsx`, supprimer le `ScreenPreview`/`symbolAnimation` locaux et importer `import { DeviceScreenView } from './DeviceScreenView.js';` ; remplacer `<ScreenPreview scene={scene} />` par `<DeviceScreenView scene={scene} />`.
- [ ] **Step 3: Typecheck + build**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS.
### Task C2: selectionStore
**Files:**
- Create: `src/stores/selectionStore.ts`
- [ ] **Step 1: Créer le store**
```ts
// src/stores/selectionStore.ts
import { create } from 'zustand';
interface SelectionState {
selectedStepId: string | null;
selectStep: (id: string | null) => void;
}
export const useSelectionStore = create<SelectionState>((set) => ({
selectedStepId: null,
selectStep: (selectedStepId) => set({ selectedStepId }),
}));
```
- [ ] **Step 2: Typecheck**
Run: `pnpm exec tsc --noEmit`
Expected: PASS.
### Task C3: Parser IR → steps + edges
**Files:**
- Create: `src/lib/irSteps.ts`
- [ ] **Step 1: Écrire le parseur**
```ts
// src/lib/irSteps.ts
// Parse le currentIr (string JSON) du runtimeStore en nœuds/arêtes pour le FlowGraph.
import type { SceneDraft } from './screenSpec.js';
export interface FlowNode {
id: string;
scene: SceneDraft; // title/subtitle/symbol/effect (defaults vides + effect 'pulse')
}
export interface FlowEdge { from: string; to: string; }
export interface FlowGraphData { nodes: FlowNode[]; edges: FlowEdge[]; entry: string | null; }
interface RawStep {
id?: string;
scene?: { title?: string; subtitle?: string; symbol?: string; effect?: SceneDraft['effect'] };
transitions?: { target_step_id?: string }[];
}
export function parseIr(irJson: string | null): FlowGraphData {
if (!irJson) return { nodes: [], edges: [], entry: null };
let raw: { entry_step_id?: string; steps?: RawStep[] };
try { raw = JSON.parse(irJson); } catch { return { nodes: [], edges: [], entry: null }; }
const steps = raw.steps ?? [];
const nodes: FlowNode[] = steps.filter((s) => s.id).map((s) => ({
id: s.id as string,
scene: {
title: s.scene?.title ?? '',
subtitle: s.scene?.subtitle ?? '',
symbol: s.scene?.symbol ?? '',
effect: s.scene?.effect ?? 'pulse',
},
}));
const ids = new Set(nodes.map((n) => n.id));
const edges: FlowEdge[] = [];
for (const s of steps) {
if (!s.id) continue;
for (const t of s.transitions ?? []) {
if (t.target_step_id && ids.has(t.target_step_id)) edges.push({ from: s.id, to: t.target_step_id });
}
}
return { nodes, edges, entry: raw.entry_step_id ?? nodes[0]?.id ?? null };
}
```
- [ ] **Step 2: Typecheck**
Run: `pnpm exec tsc --noEmit`
Expected: PASS.
### Task C4: FlowGraph (liste ordonnée cliquable + arêtes)
**Files:**
- Create: `src/components/FlowGraph.tsx`
- [ ] **Step 1: Écrire le composant (V1 : colonne de nœuds + libellés de transition)**
```tsx
// src/components/FlowGraph.tsx
import { useMemo } from 'react';
import { useRuntimeStore } from '../stores/runtimeStore.js';
import { useSelectionStore } from '../stores/selectionStore.js';
import { parseIr } from '../lib/irSteps.js';
import { WB } from '../theme/workbench.js';
export function FlowGraph() {
const ir = useRuntimeStore((s) => s.currentIr ?? s.pendingIr);
const { selectedStepId, selectStep } = useSelectionStore();
const { nodes, edges, entry } = useMemo(() => parseIr(ir), [ir]);
const outgoing = (id: string) => edges.filter((e) => e.from === id).map((e) => e.to);
if (nodes.length === 0) {
return <div style={{ color: WB.label, padding: 8, fontFamily: WB.fontMono }}>Aucune étape édite le scénario puis lance (Run).</div>;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: 8, overflowY: 'auto', fontFamily: WB.fontMono, fontSize: 12 }}>
{nodes.map((n) => {
const sel = n.id === selectedStepId;
return (
<div key={n.id}>
<button type="button" onClick={() => selectStep(n.id)}
style={{ width: '100%', textAlign: 'left', padding: '6px 8px', cursor: 'pointer',
background: sel ? WB.orange : WB.panel, color: sel ? '#1a1a1d' : '#e8eef3',
border: `1px solid ${sel ? WB.orange : WB.panelBorder}`, borderRadius: 5 }}>
{n.id === entry ? '▶ ' : ''}{n.id}
{n.scene.symbol ? <span style={{ color: sel ? '#1a1a1d' : WB.orange }}> {n.scene.symbol}</span> : null}
</button>
{outgoing(n.id).map((to) => (
<div key={to} style={{ color: WB.label, paddingLeft: 16 }}> {to}</div>
))}
</div>
);
})}
</div>
);
}
```
- [ ] **Step 2: Typecheck + build**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS.
### Task C5: DeviceScreen (écran de l'étape sélectionnée)
**Files:**
- Create: `src/components/DeviceScreen.tsx`
- [ ] **Step 1: Écrire le composant**
```tsx
// src/components/DeviceScreen.tsx
import { useMemo } from 'react';
import { useRuntimeStore } from '../stores/runtimeStore.js';
import { useSelectionStore } from '../stores/selectionStore.js';
import { parseIr } from '../lib/irSteps.js';
import { EMPTY_SCENE } from '../lib/screenSpec.js';
import { DeviceScreenView } from './DeviceScreenView.js';
import { WB } from '../theme/workbench.js';
export function DeviceScreen() {
const ir = useRuntimeStore((s) => s.currentIr ?? s.pendingIr);
const selectedStepId = useSelectionStore((s) => s.selectedStepId);
const { nodes, entry } = useMemo(() => parseIr(ir), [ir]);
const node = nodes.find((n) => n.id === selectedStepId) ?? nodes.find((n) => n.id === entry) ?? null;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: 8, alignItems: 'center' }}>
<span style={{ color: WB.label, fontFamily: WB.fontMono, fontSize: 11, alignSelf: 'flex-start' }}>
ÉCRAN · {node ? node.id : '—'}
</span>
<DeviceScreenView scene={node ? node.scene : EMPTY_SCENE} scale={0.7} />
</div>
);
}
```
- [ ] **Step 2: Typecheck + build**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS.
### Task C6: WorkbenchPane (split vertical) + branchement Layout
**Files:**
- Create: `src/components/WorkbenchPane.tsx`
- Modify: `src/components/Layout.tsx`
- [ ] **Step 1: Écrire le split vertical**
```tsx
// src/components/WorkbenchPane.tsx
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { DeviceScreen } from './DeviceScreen.js';
import { FlowGraph } from './FlowGraph.js';
import { WB } from '../theme/workbench.js';
export function WorkbenchPane() {
return (
<div className="atelier-pane" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '4px 8px', color: WB.orange, fontFamily: WB.fontTitle, letterSpacing: 1,
borderBottom: `1px solid ${WB.panelBorder}`, fontSize: 12 }}>BANC D'ESSAI</div>
<PanelGroup direction="vertical" style={{ flex: 1 }}>
<Panel defaultSize={45} minSize={20}><DeviceScreen /></Panel>
<PanelResizeHandle className="atelier-resizer atelier-resizer--horizontal" />
<Panel defaultSize={55} minSize={20}><FlowGraph /></Panel>
</PanelGroup>
</div>
);
}
```
- [ ] **Step 2: Brancher dans Layout (remplacer le placeholder de A2)**
Dans `src/components/Layout.tsx` : `import { WorkbenchPane } from './WorkbenchPane.js';` et remplacer le `<div ...>Banc d'essai (à venir)</div>` par `<WorkbenchPane />`.
- [ ] **Step 3: Typecheck + build**
Run: `pnpm exec tsc --noEmit && pnpm exec vite build`
Expected: PASS.
- [ ] **Step 4: Commit phase C**
```bash
cd ../../.. && git add -A
git -c user.name=electron-rare -c user.email=c.saillant@gmail.com commit -m "feat(atelier): Workbench pane — faithful device screen + scenario flow graph"
```
---
## Self-review (couverture spec)
- Retrait 3D (spec §4 supprimés) → Phase A ✓
- Thème Workbench palette/fonts/chrome/Blockly (spec §3) → Phase B ✓
- DeviceScreen réutilisant P3a (spec §4 nouveaux) → C1/C5 ✓
- FlowGraph depuis IR (spec §5) → C3/C4 ✓
- selectionStore + wiring (spec §5) → C2, FlowGraph/DeviceScreen lisent `selectionStore`
- Split vertical (spec §2, décision) → C6 ✓
- Tests = tsc+build verts par tâche (spec §7) ✓
- Hors périmètre (Blockly conservé, pas de playtest, pas dashboard) respecté ✓
## Notes d'exécution
- Le `FlowGraph` V1 est une **liste ordonnée de nœuds + arêtes sortantes** (le plus sûr) ; un layout graphe spatial (positions/SVG) est une amélioration ultérieure hors de ce plan.
- Vérif visuelle finale recommandée via `pnpm --filter @zacus/atelier dev` (hors CI) après C6.
- Déploiement prod (`ops/deploy-frontend.sh atelier`) **hors de ce plan** — à faire après revue visuelle.