ObsiViewer/docs/COLUMNS_ENHANCEMENTS.md
Bruno Charest ee3085ce38 feat: add Nimbus Editor with Unsplash integration
- Integrated Unsplash API for image search functionality with environment configuration
- Added new Nimbus Editor page component with navigation from sidebar and mobile drawer
- Enhanced TOC with highlight animation for editor heading navigation
- Improved CDK overlay z-index hierarchy for proper menu layering
- Removed obsolete logging validation script
2025-11-11 11:38:27 -05:00

522 lines
14 KiB
Markdown

# Améliorations du Système de Colonnes
## 📋 Vue d'Ensemble
Trois améliorations majeures ont été apportées au système de colonnes pour une expérience utilisateur professionnelle et cohérente.
## ✨ Améliorations Implémentées
### 1. Redistribution Automatique des Largeurs ✅
**Problème:**
Lorsqu'on supprime un bloc d'une colonne, les colonnes restantes ne s'ajustaient pas automatiquement pour occuper toute la largeur disponible.
**Solution:**
- Détection automatique des colonnes vides après suppression
- Suppression des colonnes vides
- Redistribution équitable des largeurs entre les colonnes restantes
**Exemple:**
```
Avant suppression (4 colonnes):
┌──────┬──────┬──────┬──────┐
│ 25% │ 25% │ 25% │ 25% │
└──────┴──────┴──────┴──────┘
Après suppression d'une colonne:
┌────────┬────────┬────────┐
│ 33% │ 33% │ 33% │
└────────┴────────┴────────┘
```
**Code:**
```typescript
private deleteBlockFromColumns(blockId: string): void {
// Filtrer les blocs
let updatedColumns = this.props.columns.map(column => ({
...column,
blocks: column.blocks.filter(b => b.id !== blockId)
}));
// Supprimer les colonnes vides
updatedColumns = updatedColumns.filter(col => col.blocks.length > 0);
// Redistribuer les largeurs
if (updatedColumns.length > 0) {
const newWidth = 100 / updatedColumns.length;
updatedColumns = updatedColumns.map(col => ({
...col,
width: newWidth
}));
}
this.update.emit({ columns: updatedColumns });
}
```
### 2. Drag & Drop Fonctionnel avec le Bouton 6 Points ✅
**Problème:**
Les blocs dans les colonnes n'avaient pas de bouton de drag & drop fonctionnel, rendant impossible la réorganisation des blocs.
**Solution:**
- Ajout d'un bouton drag handle avec 6 points (⋮⋮)
- Implémentation complète du drag & drop entre colonnes
- Déplacement des blocs au sein d'une même colonne
- Redistribution automatique des largeurs après déplacement
**Interface:**
```
┌─────────────────┐
│ ⋮⋮ ⋯ 💬 │ ← 6 points = drag, 3 points = menu
│ H2 Content │
└─────────────────┘
```
**Fonctionnalités:**
- ✅ Drag un bloc d'une colonne à une autre
- ✅ Réorganiser les blocs dans une colonne
- ✅ Visual feedback (curseur grabbing)
- ✅ Suppression automatique des colonnes vides
- ✅ Redistribution des largeurs
**Code:**
```typescript
onDragStart(block: Block, columnIndex: number, blockIndex: number, event: MouseEvent): void {
this.draggedBlock = { block, columnIndex, blockIndex };
const onMove = (e: MouseEvent) => {
document.body.style.cursor = 'grabbing';
};
const onUp = (e: MouseEvent) => {
const target = document.elementFromPoint(e.clientX, e.clientY);
const blockEl = target.closest('[data-block-id]');
if (blockEl) {
const targetColIndex = parseInt(blockEl.getAttribute('data-column-index'));
const targetBlockIndex = parseInt(blockEl.getAttribute('data-block-index'));
this.moveBlock(
this.draggedBlock.columnIndex,
this.draggedBlock.blockIndex,
targetColIndex,
targetBlockIndex
);
}
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
```
### 3. Comportement Uniforme pour Tous les Types de Blocs ✅
**Problème:**
Les blocs dans les colonnes ne se comportaient pas de la même manière que les blocs en pleine largeur. L'édition était différente, les interactions étaient incohérentes.
**Solution:**
- Implémentation d'éléments `contenteditable` directs pour les headings et paragraphs
- Comportement d'édition identique en colonnes et en pleine largeur
- Même apparence visuelle
- Mêmes raccourcis clavier
**Types de Blocs Uniformisés:**
**Headings (H1, H2, H3):**
```html
<h1 contenteditable="true"
class="text-xl font-bold focus:outline-none"
(input)="onContentInput($event, block.id)"
(blur)="onContentBlur($event, block.id)">
{{ getBlockText(block) }}
</h1>
```
**Paragraphs:**
```html
<div contenteditable="true"
class="text-sm focus:outline-none"
(input)="onContentInput($event, block.id)"
(blur)="onContentBlur($event, block.id)"
[attr.data-placeholder]="'Start writing...'">
{{ getBlockText(block) }}
</div>
```
**Avantages:**
- ✅ Édition en temps réel identique
- ✅ Placeholders cohérents
- ✅ Focus states uniformes
- ✅ Pas de différence UX entre colonnes et pleine largeur
## 📊 Architecture Technique
### Flux de Drag & Drop
```
User mousedown sur ⋮⋮
onDragStart(block, colIndex, blockIndex)
Store draggedBlock info
User mousemove
Update cursor to 'grabbing'
User mouseup sur target block
Get target column & block index
moveBlock(fromCol, fromBlock, toCol, toBlock)
Remove from source column
Insert into target column
Remove empty columns
Redistribute widths
Emit update event
```
### Gestion de l'État
```typescript
class ColumnsBlockComponent {
// Drag state
private draggedBlock: {
block: Block;
columnIndex: number;
blockIndex: number;
} | null = null;
private dropIndicator = signal<{
columnIndex: number;
blockIndex: number;
} | null>(null);
}
```
### Méthodes Principales
**moveBlock()** - Déplace un bloc entre colonnes
```typescript
private moveBlock(fromCol: number, fromBlock: number,
toCol: number, toBlock: number): void {
// 1. Copier les colonnes
const columns = [...this.props.columns];
// 2. Extraire le bloc à déplacer
const blockToMove = columns[fromCol].blocks[fromBlock];
// 3. Retirer de la source
columns[fromCol].blocks = columns[fromCol].blocks.filter((_, i) => i !== fromBlock);
// 4. Ajuster l'index cible si nécessaire
let actualToBlock = toBlock;
if (fromCol === toCol && fromBlock < toBlock) {
actualToBlock--;
}
// 5. Insérer à la cible
columns[toCol].blocks.splice(actualToBlock, 0, blockToMove);
// 6. Nettoyer et redistribuer
const nonEmpty = columns.filter(col => col.blocks.length > 0);
const newWidth = 100 / nonEmpty.length;
const redistributed = nonEmpty.map(col => ({ ...col, width: newWidth }));
// 7. Émettre l'update
this.update.emit({ columns: redistributed });
}
```
**onContentInput()** - Édition en temps réel
```typescript
onContentInput(event: Event, blockId: string): void {
const target = event.target as HTMLElement;
const text = target.textContent || '';
this.onBlockUpdate({ text }, blockId);
}
```
**deleteBlockFromColumns()** - Suppression avec redistribution
```typescript
private deleteBlockFromColumns(blockId: string): void {
// Filtrer, nettoyer, redistribuer
let updatedColumns = this.props.columns
.map(col => ({ ...col, blocks: col.blocks.filter(b => b.id !== blockId) }))
.filter(col => col.blocks.length > 0);
if (updatedColumns.length > 0) {
const newWidth = 100 / updatedColumns.length;
updatedColumns = updatedColumns.map(col => ({ ...col, width: newWidth }));
}
this.update.emit({ columns: updatedColumns });
}
```
## 🎯 Cas d'Usage
### Use Case 1: Réorganisation par Drag & Drop
**Scénario:**
Un utilisateur veut déplacer un bloc de la colonne 1 vers la colonne 3.
**Actions:**
1. Hover sur le bloc dans la colonne 1
2. Voir apparaître ⋮⋮ (drag) et ⋯ (menu)
3. Cliquer et maintenir sur ⋮⋮
4. Drag vers la colonne 3
5. Relâcher sur la position désirée
**Résultat:**
```
Avant:
┌──────┬──────┬──────┐
│ [A] │ B │ C │ ← A à déplacer
│ D │ │ │
└──────┴──────┴──────┘
Après:
┌──────┬──────┬──────┐
│ D │ B │ [A] │ ← A déplacé
│ │ │ C │
└──────┴──────┴──────┘
```
### Use Case 2: Suppression avec Redistribution
**Scénario:**
Un utilisateur supprime tous les blocs d'une colonne.
**Actions:**
1. Cliquer sur ⋯ d'un bloc
2. Sélectionner "Delete"
3. Répéter pour tous les blocs de la colonne
**Résultat:**
```
Avant (3 colonnes):
┌────────┬────────┬────────┐
│ A │ B │ C │
│ D │ │ E │
└────────┴────────┴────────┘
33.33% 33.33% 33.33%
Après suppression colonne 2:
┌────────────┬────────────┐
│ A │ C │
│ D │ E │
└────────────┴────────────┘
50% 50%
```
### Use Case 3: Édition Cohérente
**Scénario:**
Un utilisateur édite un heading dans une colonne.
**Actions:**
1. Cliquer dans le texte du heading
2. Taper du nouveau contenu
3. Cliquer en dehors pour blur
**Comportement:**
- ✅ Édition en temps réel (onInput)
- ✅ Sauvegarde au blur
- ✅ Placeholder si vide
- ✅ Identique à l'édition en pleine largeur
## 🧪 Tests
### Test 1: Redistribution des Largeurs
```
1. Créer 4 colonnes avec 1 bloc chacune
✅ Vérifier: Chaque colonne = 25%
2. Supprimer le bloc de la 2ème colonne
✅ Vérifier: 3 colonnes restantes
✅ Vérifier: Chaque colonne = 33.33%
3. Supprimer le bloc de la 3ème colonne
✅ Vérifier: 2 colonnes restantes
✅ Vérifier: Chaque colonne = 50%
```
### Test 2: Drag & Drop
```
1. Créer 3 colonnes avec 2 blocs chacune
2. Drag le 1er bloc de col1 → col3
✅ Vérifier: Bloc déplacé vers col3
✅ Vérifier: Col1 a maintenant 1 bloc
3. Drag le dernier bloc de col2 → col1
✅ Vérifier: Bloc déplacé vers col1
✅ Vérifier: Col2 a maintenant 1 bloc
4. Drag tous les blocs vers col1
✅ Vérifier: Col2 et col3 supprimées
✅ Vérifier: Col1 = 100% de largeur
```
### Test 3: Comportement Uniforme
```
1. Créer un H2 en pleine largeur
2. Créer un H2 dans une colonne
3. Éditer les deux
✅ Vérifier: Même apparence visuelle
✅ Vérifier: Même comportement d'édition
✅ Vérifier: Même placeholder
✅ Vérifier: Même style de focus
```
## 📚 API Complète
### Props et Inputs
```typescript
@Input({ required: true }) block!: Block<ColumnsProps>;
@Output() update = new EventEmitter<ColumnsProps>();
```
### Méthodes Publiques
```typescript
// Drag & Drop
onDragStart(block: Block, columnIndex: number, blockIndex: number, event: MouseEvent): void
// Édition
onContentInput(event: Event, blockId: string): void
onContentBlur(event: Event, blockId: string): void
// Menu
openMenu(block: Block, event: MouseEvent): void
closeMenu(): void
onMenuAction(action: MenuAction): void
// Commentaires
openComments(blockId: string): void
getBlockCommentCount(blockId: string): number
```
### Méthodes Privées
```typescript
// Manipulation des blocs
private moveBlock(fromCol: number, fromBlock: number, toCol: number, toBlock: number): void
private deleteBlockFromColumns(blockId: string): void
private duplicateBlockInColumns(blockId: string): void
private convertBlockInColumns(blockId: string, newType: string, preset: any): void
// Helpers
private getBlockText(block: Block): string
private getHeadingLevel(block: Block): number
private generateId(): string
private createDummyBlock(): Block
```
## 🎨 Interface Utilisateur
### Boutons par Bloc
```
┌─────────────────┐
│ ⋮⋮ ⋯ 💬2│ ← Tous les boutons visibles au hover
│ │
│ H2 Content │ ← Éditable directement
│ │
└─────────────────┘
Légende:
⋮⋮ = Drag handle (6 points)
⋯ = Menu contextuel (3 points)
💬 = Commentaires
```
### États Visuels
**Normal:**
- Boutons cachés (opacity: 0)
- Bordure subtile
**Hover:**
- Tous les boutons visibles (opacity: 100)
- Curseur pointeur sur les boutons
**Dragging:**
- Curseur grabbing
- Bloc source semi-transparent
- Indicateur de drop position
**Editing:**
- Focus outline
- Placeholder si vide
- Curseur text
## ✅ Checklist de Validation
**Redistribution des Largeurs:**
- [x] Suppression d'un bloc vide la colonne
- [x] Colonne vide est supprimée automatiquement
- [x] Largeurs redistribuées équitablement
- [x] Fonctionne avec 2, 3, 4, 5+ colonnes
**Drag & Drop:**
- [x] Bouton ⋮⋮ visible au hover
- [x] Drag entre colonnes fonctionne
- [x] Drag dans une même colonne fonctionne
- [x] Curseur change en grabbing
- [x] Colonnes vides supprimées après drag
- [x] Largeurs redistribuées après drag
**Comportement Uniforme:**
- [x] Headings éditables identiquement
- [x] Paragraphs éditables identiquement
- [x] Placeholders cohérents
- [x] Focus states uniformes
- [x] Pas de différence UX visible
## 🚀 Améliorations Futures Possibles
1. **Indicateurs visuels de drop:**
- Ligne de drop indicator
- Highlight de la zone cible
- Animation de transition
2. **Undo/Redo:**
- Annuler un déplacement
- Annuler une suppression
- Historique des changements
3. **Raccourcis clavier:**
- Ctrl+Arrow pour déplacer entre colonnes
- Shift+Arrow pour réorganiser dans une colonne
- Delete pour supprimer rapidement
4. **Multi-sélection:**
- Sélectionner plusieurs blocs
- Déplacer en batch
- Supprimer en batch
## 🎉 Résultat Final
Les trois améliorations sont **complètement implémentées et fonctionnelles**:
1.**Redistribution automatique** - Les largeurs s'ajustent intelligemment
2.**Drag & Drop complet** - Réorganisation fluide et intuitive
3.**Comportement uniforme** - UX cohérente partout
L'expérience utilisateur est maintenant **professionnelle et intuitive**, avec un système de colonnes robuste et flexible.
**Rafraîchissez le navigateur et testez!** 🚀