docs(plans): atelier Workbench refonte — implementation plan (3 phases)
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user