feat: finalisation split view #75 — boutons, nav clavier, palette, ROADMAP
CI / lint (push) Failing after 12s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 13s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Failing after 12s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 13s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
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
This commit is contained in:
+62
-118
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<button class="tab-split-btn" title="Diviser à droite (Ctrl+Alt+\\)" data-action="split-right">
|
||||
<i data-lucide="panel-right" style="width:13px;height:13px"></i>
|
||||
</button>
|
||||
<button class="tab-split-btn" title="Diviser en bas (Ctrl+Alt+Shift+\\)" data-action="split-down">
|
||||
<i data-lucide="panel-bottom" style="width:13px;height:13px"></i>
|
||||
</button>
|
||||
`;
|
||||
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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user