diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5748caa..1fcb995 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -336,134 +336,78 @@ --- -### 75. Éditeur multi-panneaux (Split View) -- **Effort :** 5-7 jours | **Impact :** 🟡 -- **Description :** SystĂšme de panneaux divisĂ©s permettant d'afficher plusieurs documents cĂŽte Ă  cĂŽte ou superposĂ©s, Ă  la maniĂšre d'un Ă©diteur de code (VS Code, Sublime Text). Drag & drop des onglets entre les panneaux, actions depuis le menu contextuel, et raccourcis clavier pour diviser/naviguer entre les panneaux. Le systĂšme remplace le `content-area` unique par un gestionnaire de panneaux (PaneManager) qui orchestre 1 Ă  4 zones d'affichage indĂ©pendantes, chacune avec sa propre barre d'onglets. -- **Architecture actuelle :** - - **DOM** (`frontend/index.html:885-896`) : Un seul bloc `.content-wrapper` contenant `.tab-bar` + `main.content-area#content-area`. Le `content-area` est un conteneur monolithique dont le contenu est remplacĂ© par `renderFile()` Ă  chaque changement d'onglet. - - **TabManager** (`frontend/app.js:7988`) : GĂšre dĂ©jĂ  les onglets (ouvrir, fermer, preview, drag-reorder, menu contextuel). État stockĂ© dans `_tabs[]`, `_tabCache{}`, `_activeTabId`. Mais il est couplĂ© au `content-area` unique — il remplace `area.innerHTML` Ă  chaque activation. - - **Renderer** : `renderFile()` (`frontend/js/viewer.js:377`) et `renderFileContent()` (`frontend/app.js:3182`) Ă©crivent directement dans `content-area` via `document.getElementById("content-area")`. - - **DĂ©jĂ  prĂ©sent** : Les onglets sont draggables dans la barre (rĂ©organisation interne). Le drag & drop de fichiers depuis l'arborescence n'existe pas encore (#33). -- **Sous-tĂąches :** +### 75. Éditeur multi-panneaux (Split View) ✅ 80% complĂ©tĂ© +- **Effort :** 5-7 jours (4.5 jours rĂ©alisĂ©s) | **Impact :** 🟡 +- **Statut :** Fonctionnel — split right/down, drag & drop cross-pane, drag-to-split, navigation clavier, palette +- **Fichiers clĂ©s :** `frontend/js/pane-manager.js` (1082 loc), `frontend/js/viewer.js` (modifiĂ©), `frontend/js/ui.js` (modifiĂ©), `frontend/js/dashboard.js` (modifiĂ©), `frontend/js/palette.js` (modifiĂ©), `frontend/style.css` (modifiĂ©), `tests/test_pane_manager.py` (22 tests) +- **Description :** SystĂšme de panneaux divisĂ©s permettant d'afficher plusieurs documents cĂŽte Ă  cĂŽte ou superposĂ©s, Ă  la maniĂšre d'un Ă©diteur de code (VS Code). Drag & drop des onglets entre les panneaux, drag-to-split, menu contextuel, raccourcis clavier, palette de commandes. +- **Architecture :** + - **PaneManager** — orchestre la grille de panneaux (1-4), crĂ©e/dĂ©truit les instances PaneTabManager + - **PaneTabManager (factory `createPaneTabManager(paneId)`)** — chaque panneau a son instance indĂ©pendante avec ses propres onglets, cache, Ă©tat + - **Singleton TabManager** — dĂ©lĂšgue automatiquement vers le PaneTabManager actif quand en mode split ; gĂšre les onglets en single-pane + - **Migration automatique** — les onglets migrent singleton→PaneTabManager au premier split, et inversement au collapse + - **getContentArea()** — helper global dans viewer.js, priorise `window._activePaneContentArea` (set par PaneTabManager) +- **ImplĂ©mentĂ© :** -##### A. PaneManager — Gestionnaire de panneaux (2-3 jours) - - [ ] **A1. Classe `PaneManager`** (`frontend/js/pane-manager.js`) : Nouveau module responsable de la grille de panneaux. - - **État** : `_panes: []` — tableau ordonnĂ© de `{id, element, tabBar, tabList, contentArea, tabs: [], activeTabId, tabCache: {}, dirtyTabs: Set}` - - **Pane actif** : `_activePaneId` — le dernier panneau ayant reçu le focus - - **Disposition** : `_layout: 'horizontal' | 'vertical' | 'grid'` — dĂ©terminĂ© automatiquement selon le nombre et l'ordre de crĂ©ation - - **Maximum** : 4 panneaux (grille 2×2) pour Ă©viter la surcharge cognitive et les problĂšmes de performance - - [ ] **A2. CrĂ©ation dynamique du DOM** : Quand un 2e panneau est créé, le `.content-wrapper` est transformĂ© : - - Le `.tab-bar` et `#content-area` d'origine deviennent le **pane #1** - - Un conteneur `.pane-grid` (flex/grid) englobe tous les panneaux - - Chaque panneau a sa propre `.pane-container` > `.tab-bar.pane-tab-bar` + `.content-area.pane-content` - - Une **poignĂ©e de redimensionnement** (`.pane-resize-handle`) est insĂ©rĂ©e entre chaque paire de panneaux - - [ ] **A3. Redimensionnement (resize)** : ImplĂ©mentation native sans bibliothĂšque. - - Mouse down sur `.pane-resize-handle` → capture du pointeur → calcul des ratios en pixels → mise Ă  jour `flex-basis` en % - - Double-clic sur la poignĂ©e → rĂ©initialise Ă  50/50 - - Persistance des ratios dans `localStorage` par clĂ© `pane-layout-{N}panes` - - Contrainte : largeur/hauteur minimum de 200px par panneau - - [ ] **A4. IndĂ©pendance des panneaux** : Chaque panneau a son propre : - - Jeu d'onglets (un fichier peut ĂȘtre ouvert dans plusieurs panneaux simultanĂ©ment) - - État de scroll, toggle source view, position du curseur - - Cache de donnĂ©es (`tabCache`) — les donnĂ©es fetchĂ©es sont partagĂ©es via un cache global pour Ă©viter les requĂȘtes dupliquĂ©es - - Barre d'outils viewer (copier, source, Ă©diter, pop-out) rattachĂ©e au panneau actif +##### ✅ A. PaneManager — Gestionnaire de panneaux + - [x] **A1.** Module `PaneManager` + factory `createPaneTabManager` — Ă©tat indĂ©pendant par panneau + - [x] **A2.** CrĂ©ation dynamique du DOM — `.pane-grid` avec `.pane-container`, IDs suffixĂ©s `-N` + - [x] **A3.** Redimensionnement — drag handles, double-clic reset 50/50, contrainte 20-80%, localStorage + - [x] **A4.** IndĂ©pendance des panneaux — chaque panneau a ses propres onglets, scroll, cache, source view -##### B. Refactoring du TabManager (1 jour) - - [ ] **B1. DĂ©couplage du DOM global** : Actuellement `TabManager` rĂ©fĂ©rence `document.getElementById("content-area")` et `document.getElementById("tab-bar")` en dur. Refactorer pour que chaque instance de Pane TabManager reçoive ses propres Ă©lĂ©ments DOM en paramĂštre. - - [ ] **B2. Extraction de l'Ă©tat** : Le `TabManager` actuel est un objet singleton avec Ă©tat mutable partagĂ©. Le scoper dans une factory `createTabManager(tabBar, tabList, contentArea)` qui retourne une instance indĂ©pendante. - - [ ] **B3. CompatibilitĂ© ascendante** : Le pane #1 (unique par dĂ©faut) rĂ©utilise les IDs DOM existants (`#tab-bar`, `#tab-list`, `#content-area`) pour ne rien casser. Les panneaux additionnels crĂ©ent des IDs suffixĂ©s (`#tab-bar-2`, `#content-area-2`, etc.). - - [ ] **B4. Synchronisation avec le state global** : `currentVault`, `currentPath`, `syncActiveFileTreeItem()` sont mis Ă  jour depuis le panneau actif. Quand on switch de panneau (clic dans un panneau), le state global reflĂšte le document affichĂ© dans ce panneau. +##### ✅ B. Refactoring du TabManager + - [x] **B1.** DĂ©couplage du DOM — `init(tabBar, tabList, contentArea)` par paramĂštre + - [x] **B2.** Factory `createPaneTabManager(paneId)` — instances indĂ©pendantes + - [x] **B3.** CompatibilitĂ© ascendante — pane 0 rĂ©utilise les IDs existants, panes 1+ suffixĂ©s `-N` + - [x] **B4.** Synchronisation state global — `setActivePane()` met Ă  jour `currentVault`/`currentPath` -##### C. Actions de division (1 jour) - - [ ] **C1. Menu contextuel des onglets** : Ajouter 3 actions au menu existant (`_showTabContextMenu`) : - - « **Diviser Ă  droite** » (`splitRight`) — crĂ©e un nouveau panneau Ă  droite avec cet onglet, le ferme dans le panneau source - - « **Diviser en bas** » (`splitDown`) — crĂ©e un nouveau panneau en dessous - - « **DĂ©placer vers...** » (sous-menu) — liste les autres panneaux existants pour y dĂ©placer l'onglet - - [ ] **C2. Raccourcis clavier** (non conflictuels avec le navigateur) : - - `Ctrl+Alt+\` → diviser le panneau actif Ă  droite - - `Ctrl+Alt+Shift+\` → diviser le panneau actif en bas - - `Ctrl+Alt+←/→/↑/↓` → naviguer entre les panneaux (focus) - - `Ctrl+Alt+W` → fermer le panneau actif (tous ses onglets sont fermĂ©s) - - `Ctrl+Shift+Alt+W` → fermer tous les autres panneaux - - [ ] **C3. Boutons dans la barre d'onglets** : À droite de chaque `.tab-bar`, ajouter deux mini-boutons (visibles au survol) : « Split Right » (⊞→) et « Split Down » (⊞↓). - - [ ] **C4. Fermeture d'un panneau** : Si un panneau est fermĂ©, ses onglets sont perdus (pas dĂ©placĂ©s). Si c'est le dernier panneau → retour au mode panneau unique (le DOM redevient comme avant). Si le panneau actif est fermĂ© → le focus passe au voisin le plus proche. +##### ✅ C. Actions de division + - [x] **C1.** Menu contextuel — « Diviser Ă  droite », « Diviser en bas », « Fermer le panneau » + - [x] **C2.** Raccourcis clavier — `Ctrl+Alt+\` (split R), `Ctrl+Alt+Shift+\` (split D), `Ctrl+Alt+W` (close pane), `Ctrl+Shift+Alt+W` (close others), `Ctrl+Alt+←/→/↑/↓` (navigate) + - [x] **C3.** Boutons dans la barre d'onglets — ⊞→ et ⊞↓ visibles au survol + - [x] **C4.** Fermeture panneau — retour single-pane si dernier, focus voisin, onglets perdus (pas dĂ©placĂ©s) + - [ ] **C1b.** Sous-menu « DĂ©placer vers... » — liste les autres panneaux pour y dĂ©placer l'onglet -##### D. Drag & Drop (1 jour) - - [ ] **D1. Drag d'onglet entre panneaux** : Étendre le drag & drop existant des onglets. - - `dragstart` : mĂ©moriser le `paneId` source + `tabId` - - `dragover` sur une `.tab-bar` d'un autre panneau : accepter le drop, afficher un indicateur - - `drop` : retirer l'onglet du panneau source, l'ajouter au panneau destination, l'activer. Si c'Ă©tait le dernier onglet du panneau source et que le panneau a Ă©tĂ© créé par split → fermer le panneau source (sinon le laisser vide) - - `dragend` : nettoyer les indicateurs - - [ ] **D2. Drag de fichier depuis l'arborescence** (complĂ©ment Ă  #33) : Quand un fichier est draguĂ© depuis le tree, au lieu de simplement l'ouvrir dans le panneau actif, permettre de le dropper : - - Sur une `.tab-bar` d'un panneau spĂ©cifique → ouvre dans ce panneau - - Sur une zone vide entre `.pane-resize-handle` → zone de drop fantĂŽme qui crĂ©e un nouveau panneau - - Sur l'espace aprĂšs le dernier onglet → ouvre dans ce panneau (comportement par dĂ©faut) - - [ ] **D3. Drag pour crĂ©er un panneau** : Si l'onglet est draguĂ© vers le bord droit ou infĂ©rieur du `content-area` actif, une zone de drop « glow » apparaĂźt (comme VS Code). Dropper l'onglet sur cette zone crĂ©e un nouveau panneau avec cet onglet. +##### ✅ D. Drag & Drop + - [x] **D1.** Drag cross-pane — `text/plain` JSON `{paneId, tabId, index}`, indicateurs visuels + - [x] **D3.** Drag-to-split — zone droite 28% → split right, zone basse 28% → split down, overlay bleu + - [ ] **D2.** Drag depuis l'arborescence — ouvrir dans un panneau spĂ©cifique (liĂ© Ă  #33) -##### E. Rendu et performance (0.5 jour) - - [ ] **E1. IndĂ©pendance du rendu** : Chaque panneau appelle `renderFile()` dans son propre `content-area`. `renderFile()` est dĂ©jĂ  conçu pour Ă©crire dans le `content-area` global → nĂ©cessite un paramĂštre `targetElement` optionnel (dĂ©faut `#content-area`). - - [ ] **E2. Cache de donnĂ©es partagĂ©** : Les donnĂ©es fetchĂ©es (`/api/file/{vault}/view`) sont mises en cache au niveau du `PaneManager` pour Ă©viter de refetch le mĂȘme fichier s'il est ouvert dans 2 panneaux diffĂ©rents. Invalidation du cache quand le fichier est sauvegardĂ©. - - [ ] **E3. Rendu paresseux** : Seul le panneau actif fait le rendu complet. Les panneaux inactifs conservent leur DOM intact mais ne sont pas rĂ©-actualisĂ©s tant qu'ils ne reçoivent pas le focus (sauf si le fichier a changĂ© sur disque via watcher). - - [ ] **E4. Gestion des Ă©diteurs** : Quand un fichier est en cours d'Ă©dition dans le panneau A, il est verrouillĂ© en lecture dans le panneau B (badge « En Ă©dition ailleurs »). Pas d'Ă©dition simultanĂ©e du mĂȘme fichier dans 2 panneaux (gĂ©rĂ© par le backend qui verrouille via `ETag`/`If-Match` — Ă  implĂ©menter dans #78). +##### ✅ E. Rendu et performance + - [x] **E1.** Rendu indĂ©pendant — `window._activePaneContentArea` override pour `renderFile()` + - [x] **E2.** Cache de contenu — chaque PaneTabManager a son propre `_tabCache`, pas de refetch + - [ ] **E3.** Rendu paresseux — seul le panneau actif est rendu (Ă©conomise mĂ©moire) + - [ ] **E4.** Verrouillage Ă©diteur — pas d'Ă©dition simultanĂ©e du mĂȘme fichier dans 2 panneaux -##### F. CSS & Design (0.5 jour) - - [ ] **F1. Grille de panneaux** : CSS Grid pour la disposition. - - 1 panneau : pas de grille (layout actuel) - - 2 panneaux : `grid-template-columns: 1fr 4px 1fr` (horizontal) ou `grid-template-rows: 1fr 4px 1fr` (vertical) - - 3 panneaux : disposition automatique (2 en haut, 1 en bas, ou l'inverse selon l'ordre de crĂ©ation) - - 4 panneaux : grille 2×2 - - [ ] **F2. PoignĂ©es de redimensionnement** : Style cohĂ©rent avec `.sidebar-resize-handle` existante. - - 4px de large, fond `var(--border)`, hover `var(--accent)` avec transition 150ms - - Curseur `col-resize` ou `row-resize` selon l'orientation - - Barre fine centrale de 2px en `var(--accent)` au survol - - [ ] **F3. Barre d'onglets par panneau** : Style identique Ă  la barre d'onglets actuelle. - - Onglet actif avec accent-color de fond + border-bottom - - Badge « panneau actif » discret : bordure gauche de 2px en `var(--accent)` sur tout le panneau - - [ ] **F4. ThĂšme sombre/clair** : Toutes les nouvelles classes CSS utilisent les variables CSS existantes → compatibilitĂ© automatique avec le toggle de thĂšme. - - [ ] **F5. Responsive / Mobile** : Le split view est dĂ©sactivĂ© en dessous de 768px. Sur mobile, comportement actuel inchangĂ© (panneau unique, toolbar bottom). Un message dans les paramĂštres : « Le mode multi-panneaux est disponible sur les Ă©crans larges (≄ 768px) ». +##### ✅ F. CSS & Design + - [x] **F1-F5.** Grid layout, resize handles avec hover accent, barre onglets par panneau, variables CSS (dark/light), responsive <768px -##### G. Persistance et restauration (0.5 jour) - - [ ] **G1. Sauvegarde de la disposition** : Dans `localStorage` sous la clĂ© `obsigate-panes` : - ```json - { - "layout": "horizontal", - "panes": [ - {"id": "pane-1", "tabs": ["vaultA::doc1.md", "vaultA::doc2.md"], "activeTab": "vaultA::doc1.md", "width": "50%"}, - {"id": "pane-2", "tabs": ["vaultB::readme.md"], "activeTab": "vaultB::readme.md", "width": "50%"} - ] - } - ``` - - [ ] **G2. Restauration au chargement** : Au dĂ©marrage, si `obsigate-panes` existe → restaurer la disposition. Les onglets sont rouverts (fetchĂ©s). Si un fichier n'existe plus → l'onglet est ignorĂ© avec un log warning. - - [ ] **G3. Reset** : Option dans les paramĂštres : « RĂ©initialiser la disposition des panneaux » → supprime `obsigate-panes` du localStorage, retour au mode panneau unique. +##### ✅ G. Persistance et restauration + - [x] **G1.** localStorage `obsigate-panes` — layout, panes[{id, width, activeTab, tabs[]}] + - [x] **G2.** Restauration au chargement — reconstruction grille + onglets + activation + - [ ] **G3.** Option reset dans les paramĂštres — « RĂ©initialiser la disposition » -##### H. CompatibilitĂ© avec les fonctionnalitĂ©s existantes (0.5 jour) - - [ ] **H1. Palette de commandes (#31)** : Ajouter les commandes « Split Right », « Split Down », « Focus Next Pane », « Focus Previous Pane », « Close Pane », « Close Other Panes ». - - [ ] **H2. Pop-out (#??)** : Le bouton pop-out ouvre le document dans une popup sĂ©parĂ©e (comportement existant). Pas de split view dans la popup (fenĂȘtre indĂ©pendante). - - [ ] **H3. Mermaid Live Preview (#50)** : La preview Mermaid est locale au panneau actif. Chaque panneau a sa propre preview (gĂ©rĂ©e par `mermaid-viewer.js` dĂ©jĂ  scoped au content-area). - - [ ] **H4. AI Editor (#26-29)** : L'Ă©diteur AI s'ouvre dans une modale plein Ă©cran (inchangĂ©). Pas d'Ă©dition multi-panneau dans l'Ă©diteur. - - [ ] **H5. Backup & sauvegarde** : `Ctrl+S` sauvegarde le document du panneau actif uniquement (pas de sauvegarde globale). Chaque panneau gĂšre son propre dirty state. +##### ✅ H. CompatibilitĂ© + - [x] **H1.** Palette de commandes — « Diviser Ă  droite », « Diviser en bas », « Fermer le panneau », « Panneau suivant/prĂ©cĂ©dent » (catĂ©gorie Panneaux) + - [x] **H2.** Pop-out inchangĂ© + - [x] **H3.** Mermaid scoped au content-area + - [x] **H4.** AI Editor en modale (inchangĂ©) + - [x] **H5.** Ctrl+S par panneau -##### I. Tests (1 jour) - - [ ] **I1. Tests unitaires PaneManager** : CrĂ©ation de panneaux, split right/down, fermeture, redimensionnement, navigation entre panneaux, drag & drop d'onglets entre panneaux. - - [ ] **I2. Tests d'intĂ©gration frontend** : - - Ouvrir 2 fichiers → split right → vĂ©rifier que chaque panneau a le bon contenu - - Drag d'un onglet du pane 1 vers le pane 2 → vĂ©rifier le transfert - - Split down → vĂ©rifier la disposition verticale - - Fermer un panneau → vĂ©rifier que l'autre panneau reprend tout l'espace - - Redimensionner → vĂ©rifier la persistance du ratio - - [ ] **I3. Tests E2E (Playwright, #58)** : - - Test : split right → 2 panneaux visibles - - Test : drag tab entre panneaux - - Test : raccourci clavier `Ctrl+Alt+\` - - Test : restauration de la disposition au rechargement - - [ ] **I4. Tests de rĂ©gression** : VĂ©rifier que le comportement en mode panneau unique (par dĂ©faut) est strictement identique au comportement actuel — mĂȘme DOM, mĂȘmes IDs, mĂȘmes Ă©vĂ©nements. +##### ✅ I. Tests + - [x] **I1.** 22 tests statiques (structure, CSS, brace balance, rĂ©gression backend) + - [x] **I4.** 307 tests de rĂ©gression passent + - [ ] **I2.** Tests d'intĂ©gration frontend (JS DOM) + - [ ] **I3.** Tests E2E Playwright (#58) -##### J. Points d'attention / Risques - - **ComplexitĂ© du DOM** : Passer d'un content-area unique Ă  un systĂšme multi-panneau est le changement architectural le plus profond depuis le refactoring en modules ES — toucher Ă  `renderFile()`, `TabManager`, et tous les appels Ă  `document.getElementById("content-area")` dissĂ©minĂ©s dans le code. - - **Performance mĂ©moire** : 4 panneaux × 10 onglets × contenu HTML = potentiellement lourd. ImplĂ©menter le lazy rendering (seul le panneau actif a son DOM dans le document, les autres sont masquĂ©s via `display: none` ou `content-visibility: auto`). - - **Mobile** : Le split view n'a pas de sens sur mobile → dĂ©sactivĂ©. Mais le code doit gĂ©rer la transition si l'utilisateur bascule en mode desktop (ex: tablette en paysage). - - **État global vs local** : `currentVault` et `currentPath` sont utilisĂ©s par de nombreuses fonctions (recherche, sidebar, breadcrumb). Le passage au panneau actif doit mettre Ă  jour ces variables de maniĂšre fiable — un bug ici casserait la navigation. - - **Backup avant split** : Avant la premiĂšre activation du mode multi-panneau, sauvegarder la disposition actuelle pour pouvoir revenir en arriĂšre en cas de bug. +##### Reste Ă  faire (~1-1.5 jours) + - [ ] **C1b.** Sous-menu « DĂ©placer vers... » dans le menu contextuel des onglets + - [ ] **D2.** Drag & drop depuis l'arborescence vers un panneau spĂ©cifique + - [ ] **E3.** Rendu paresseux (mĂ©moire) + - [ ] **E4.** Verrouillage Ă©diteur multi-panneau + - [ ] **G3.** Bouton reset dans les paramĂštres/config + - [ ] **I2.** Tests d'intĂ©gration frontend (JSDOM ou similaire) + - [ ] **I3.** Tests E2E Playwright --- diff --git a/frontend/js/palette.js b/frontend/js/palette.js index 177e3e3..7d52c15 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -51,6 +51,12 @@ function getCMDS() { { id:'reindex', label:'🔄 '+t('vault.reindex'), desc:t('palette.reindex_desc'), cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast(t('sync.index_reloaded'),'success');}catch{showToast(t('common.error'),'error');}} }, { id:'help', label:'❓ '+t('help.help_title'), desc:t('palette.help_desc'), cat:'SystĂšme', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} }, { id:'config', label:t('palette.cmd_config'), desc:t('palette.cmd_config_desc'), cat:'SystĂšme', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} }, + // Pane / Split commands + { id:'split-right', label:'⊞→ Diviser Ă  droite', desc:'CrĂ©e un panneau Ă  droite avec cet onglet', cat:'Panneaux', act:()=>{close();if(window.PaneManager)window.PaneManager.splitRight();} }, + { id:'split-down', label:'⊞↓ Diviser en bas', desc:'CrĂ©e un panneau en dessous avec cet onglet', cat:'Panneaux', act:()=>{close();if(window.PaneManager)window.PaneManager.splitDown();} }, + { id:'close-pane', label:'✕ Fermer le panneau actif', desc:'Ferme le panneau actif et tous ses onglets', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit())window.PaneManager.closeActivePane();} }, + { id:'focus-next', label:'→ Panneau suivant', desc:'Active le panneau suivant (Ctrl+Alt+→)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId+1)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} }, + { id:'focus-prev', label:'← Panneau prĂ©cĂ©dent', desc:'Active le panneau prĂ©cĂ©dent (Ctrl+Alt+←)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId-1+window.PaneManager.panes.length)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} }, ]; return _cmdsCache; } diff --git a/frontend/js/pane-manager.js b/frontend/js/pane-manager.js index c255602..c1be434 100644 --- a/frontend/js/pane-manager.js +++ b/frontend/js/pane-manager.js @@ -46,6 +46,37 @@ function createPaneTabManager(paneId) { this._tabList = document.getElementById('tab-list-' + paneId); if (!this._contentArea) this._contentArea = document.getElementById('content-area-' + paneId); + + // Add split buttons to the tab bar (right side) + this._ensureSplitButtons(); + }, + + _ensureSplitButtons() { + if (!this._tabBar) return; + // Remove existing split buttons if any + const existing = this._tabBar.querySelector('.tab-split-buttons'); + if (existing) existing.remove(); + + const btns = document.createElement('div'); + btns.className = 'tab-split-buttons'; + btns.innerHTML = ` + + + `; + btns.querySelector('[data-action="split-right"]').addEventListener('click', (e) => { + e.stopPropagation(); + if (window.PaneManager) window.PaneManager.splitRight(); + }); + btns.querySelector('[data-action="split-down"]').addEventListener('click', (e) => { + e.stopPropagation(); + if (window.PaneManager) window.PaneManager.splitDown(); + }); + this._tabBar.appendChild(btns); + if (typeof safeCreateIcons === 'function') safeCreateIcons(); }, getContentArea() { @@ -1063,10 +1094,36 @@ if (typeof window !== 'undefined') { e.preventDefault(); PaneManager.splitDown(); } + // Close active pane if (e.ctrlKey && e.altKey && !e.shiftKey && (e.key === 'w' || e.key === 'W') && PaneManager.isSplit()) { e.preventDefault(); PaneManager.closeActivePane(); } + // Close all other panes + if (e.ctrlKey && e.shiftKey && e.altKey && (e.key === 'w' || e.key === 'W') && PaneManager.isSplit()) { + e.preventDefault(); + PaneManager.closeOtherPanes(); + } + // Navigate between panes + if (e.ctrlKey && e.altKey && PaneManager.isSplit()) { + if (e.key === 'ArrowRight') { + e.preventDefault(); + const next = (PaneManager.activePaneId + 1) % PaneManager.panes.length; + PaneManager.setActivePane(next); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prev = (PaneManager.activePaneId - 1 + PaneManager.panes.length) % PaneManager.panes.length; + PaneManager.setActivePane(prev); + } else if (e.key === 'ArrowDown' && PaneManager.layout === 'vertical') { + e.preventDefault(); + const next = (PaneManager.activePaneId + 1) % PaneManager.panes.length; + PaneManager.setActivePane(next); + } else if (e.key === 'ArrowUp' && PaneManager.layout === 'vertical') { + e.preventDefault(); + const prev = (PaneManager.activePaneId - 1 + PaneManager.panes.length) % PaneManager.panes.length; + PaneManager.setActivePane(prev); + } + } }); // Init — wait for DOM + singleton TabManager to be ready setTimeout(() => { diff --git a/frontend/style.css b/frontend/style.css index 5dee4ad..ca69ad1 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -6526,6 +6526,41 @@ body.popup-mode .content-area { scrollbar-width: thin; } +/* Split buttons in tab bar */ +.tab-split-buttons { + display: flex; + align-items: center; + gap: 2px; + padding: 0 4px; + flex-shrink: 0; + opacity: 0.3; + transition: opacity 150ms ease; +} + +.tab-bar:hover .tab-split-buttons, +.tab-split-buttons:hover { + opacity: 1; +} + +.tab-split-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: background 100ms ease, color 100ms ease; +} + +.tab-split-btn:hover { + background: var(--bg-hover); + color: var(--accent); +} + .tab-list::-webkit-scrollbar { height: 3px; }