From 804e6313b3f4ec4bda9b202e75ad7e8fb0358fb1 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 28 Jul 2026 10:08:11 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20finalisation=20split=20view=20#75=20?= =?UTF-8?q?=E2=80=94=20boutons,=20nav=20clavier,=20palette,=20ROADMAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajouts: - C3: Boutons Split ⊞→ ⊞↓ dans la barre d'onglets (visibles au survol) - C2: Ctrl+Alt+←/→/↑/↓ navigation entre panneaux - C2: Ctrl+Shift+Alt+W fermer tous les autres panneaux - H1: 5 commandes palette (Split R/D, Close Pane, Focus Next/Prev) - ROADMAP.md: section 75 mise à jour avec statut 80% complété CSS: .tab-split-buttons, .tab-split-btn avec opacity hover --- docs/ROADMAP.md | 180 +++++++++++++----------------------- frontend/js/palette.js | 6 ++ frontend/js/pane-manager.js | 57 ++++++++++++ frontend/style.css | 35 +++++++ 4 files changed, 160 insertions(+), 118 deletions(-) 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; }