---
title: 'Fix Muuri Masonry Grid - Drag & Drop et Layout Responsive'
slug: 'fix-muuri-masonry-grid'
created: '2026-01-18'
status: 'ready-for-dev'
stepsCompleted: [1, 2, 3, 4]
tech_stack: ['muuri@0.9.5', 'react@19.2.3', 'typescript@5.x', 'next.js@16.1.1', 'web-animations-js']
files_to_modify:
- 'components/masonry-grid.tsx'
- 'components/note-card.tsx'
- 'components/masonry-grid.css'
- 'config/masonry-layout.ts'
- 'tests/drag-drop.spec.ts'
code_patterns:
- 'Dynamic Muuri import (SSR-safe)'
- 'useResizeObserver hook with RAF debounce'
- 'NotebookDragContext for cross-component state'
- 'dragHandle: .muuri-drag-handle (mobile only)'
- 'NoteSize type: small | medium | large'
test_patterns:
- 'Playwright E2E with [data-draggable="true"] selectors'
- 'API cleanup in beforeAll/afterEach'
- 'dragTo() for reliable drag operations'
---
# Tech-Spec: Fix Muuri Masonry Grid - Drag & Drop et Layout Responsive
**Created:** 2026-01-18
**Status:** đ Review
## Overview
### Problem Statement
Le systÚme de grille masonry avec Muuri présente 4 problÚmes critiques:
1. **â Drag & Drop cassĂ©** - Les tests Playwright cherchent `data-draggable="true"` mais l'attribut est sur `NoteCard` (ligne 273), pas sur le `MasonryItem` wrapper que Muuri manipule.
2. **â Tailles de notes non gĂ©rĂ©es** - Les notes ont `data-size` mais Muuri ne recalcule pas le layout aprĂšs le rendu du contenu. La fonction `getItemDimensions` est dĂ©finie mais jamais rĂ©utilisĂ©e lors des syncs.
3. **â Layout non responsive** - Les colonnes sont calculĂ©es via `calculateColumns()` mais les largeurs ne sont appliquĂ©es qu'une seule fois. Le `useEffect` de sync (lignes 295-322) ne gĂšre pas l'ajout/suppression d'items.
4. **â Synchronisation items cassĂ©e** - Quand React ajoute/supprime des notes, Muuri n'est pas notifiĂ©. Les nouveaux items ne sont pas ajoutĂ©s Ă la grille Muuri.
### Solution
Refactoriser l'intégration Muuri en 5 tùches:
1. Propager `data-draggable="true"` au `MasonryItem` wrapper
2. Centraliser le calcul des dimensions dans une fonction réutilisable
3. Utiliser `ResizeObserver` sur le conteneur principal
4. Synchroniser les items DOM avec Muuri aprĂšs chaque rendu React
5. Vérifier les tests Playwright
### Scope
**In Scope:**
- â
Correction du drag & drop Muuri
- â
Layout responsive avec colonnes dynamiques (1â5 selon largeur)
- â
Gestion correcte des tailles (small/medium/large)
- â
Compatibilité tests Playwright existants
**Out of Scope:**
- â Nouvelles tailles de notes
- â Migration vers autre librairie
- â Modification persistance ordre
---
## Context for Development
### Codebase Patterns
**Import dynamique Muuri (SSR-safe):**
```typescript
const MuuriClass = (await import('muuri')).default;
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions);
```
**Hook useResizeObserver existant:**
```typescript
// hooks/use-resize-observer.ts
const observer = new ResizeObserver((entries) => {
if (frameId.current) cancelAnimationFrame(frameId.current);
frameId.current = requestAnimationFrame(() => {
for (const entry of entries) callback(entry);
});
});
```
**NotebookDragContext (état cross-component):**
```typescript
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
```
**Drag handle mobile:**
```typescript
dragHandle: isMobile ? '.muuri-drag-handle' : undefined,
```
### Files to Reference
| File | Purpose | Lines clés |
| ---- | ------- | ---------- |
| [masonry-grid.tsx](file:///d:/dev_new_pc/Keep/keep-notes/components/masonry-grid.tsx) | Composant grille Muuri | 116-292 (init), 295-322 (sync) |
| [note-card.tsx](file:///d:/dev_new_pc/Keep/keep-notes/components/note-card.tsx) | Carte note avec data-draggable | 271-301 (Card props) |
| [masonry-grid.css](file:///d:/dev_new_pc/Keep/keep-notes/components/masonry-grid.css) | Styles tailles et drag | 54-67, 70-97 |
| [masonry-layout.ts](file:///d:/dev_new_pc/Keep/keep-notes/config/masonry-layout.ts) | Config breakpoints | 81-90 (calculateColumns) |
| [drag-drop.spec.ts](file:///d:/dev_new_pc/Keep/keep-notes/tests/drag-drop.spec.ts) | Tests E2E | 45, 75-78 (data-draggable) |
### Technical Decisions
1. **Garder Muuri** - Fonctionne pour masonry, on corrige l'intégration
2. **Réutiliser useResizeObserver** - Hook existant avec RAF debounce
3. **Hauteur auto** - Comme Google Keep, contenu détermine hauteur
4. **Largeur fixe** - Toutes notes mĂȘme largeur par colonne
---
## Implementation Plan
### Tasks
#### Task 1: Ajouter `data-draggable` au MasonryItem wrapper
- [ ] **File:** `components/masonry-grid.tsx`
- [ ] **Action:** Ajouter `data-draggable="true"` au div wrapper `.masonry-item`
- [ ] **Lignes:** 32-37
```typescript
// AVANT (ligne 32-37)
// APRĂS
```
---
#### Task 2: Créer fonction `applyItemDimensions` réutilisable
- [ ] **File:** `components/masonry-grid.tsx`
- [ ] **Action:** Extraire la logique de calcul des dimensions dans une fonction callback
- [ ] **Position:** AprĂšs la ligne 109 (refreshLayout)
```typescript
// Nouvelle fonction Ă ajouter aprĂšs refreshLayout
const applyItemDimensions = useCallback((grid: any, containerWidth: number) => {
if (!grid) return;
const columns = calculateColumns(containerWidth);
const itemWidth = calculateItemWidth(containerWidth, columns);
const items = grid.getItems();
items.forEach((item: any) => {
const el = item.getElement();
if (el) {
el.style.width = `${itemWidth}px`;
// Height auto - determined by content (Google Keep style)
}
});
}, []);
```
---
#### Task 3: Améliorer la gestion du resize avec ResizeObserver sur conteneur
- [ ] **File:** `components/masonry-grid.tsx`
- [ ] **Action:** Remplacer `window.addEventListener('resize')` par ResizeObserver sur `.masonry-container`
- [ ] **Lignes:** 325-378 (useEffect resize)
```typescript
// REMPLACER le useEffect de resize (lignes 325-378)
const containerRef = useRef
(null);
useEffect(() => {
if (!containerRef.current || (!pinnedMuuri.current && !othersMuuri.current)) return;
let resizeTimeout: NodeJS.Timeout;
const handleResize = (entries: ResizeObserverEntry[]) => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const containerWidth = entries[0]?.contentRect.width || window.innerWidth - 32;
const columns = calculateColumns(containerWidth);
const itemWidth = calculateItemWidth(containerWidth, columns);
console.log(`[Masonry Resize] Width: ${containerWidth}px, Columns: ${columns}`);
// Apply dimensions to both grids
applyItemDimensions(pinnedMuuri.current, containerWidth);
applyItemDimensions(othersMuuri.current, containerWidth);
// Refresh layouts
requestAnimationFrame(() => {
pinnedMuuri.current?.refreshItems().layout();
othersMuuri.current?.refreshItems().layout();
});
}, 150);
};
const observer = new ResizeObserver(handleResize);
observer.observe(containerRef.current);
// Initial layout
handleResize([{ contentRect: containerRef.current.getBoundingClientRect() } as ResizeObserverEntry]);
return () => {
clearTimeout(resizeTimeout);
observer.disconnect();
};
}, [applyItemDimensions]);
```
- [ ] **Action:** Ajouter `ref={containerRef}` au div `.masonry-container` (ligne 381)
```typescript
// AVANT
// APRĂS
```
---
#### Task 4: Synchroniser items DOM â Muuri aprĂšs rendu React
- [ ] **File:** `components/masonry-grid.tsx`
- [ ] **Action:** Améliorer le useEffect de sync pour gérer ajout/suppression d'items
- [ ] **Lignes:** 295-322
```typescript
// REMPLACER le useEffect de sync (lignes 295-322)
useEffect(() => {
const syncGridItems = (grid: any, gridRef: React.RefObject, notesArray: Note[]) => {
if (!grid || !gridRef.current) return;
const containerWidth = containerRef.current?.getBoundingClientRect().width || window.innerWidth - 32;
const columns = calculateColumns(containerWidth);
const itemWidth = calculateItemWidth(containerWidth, columns);
// Get current DOM elements and Muuri items
const domElements = Array.from(gridRef.current.children) as HTMLElement[];
const muuriItems = grid.getItems();
const muuriElements = muuriItems.map((item: any) => item.getElement());
// Find new elements to add
const newElements = domElements.filter(el => !muuriElements.includes(el));
// Find removed elements
const removedItems = muuriItems.filter((item: any) =>
!domElements.includes(item.getElement())
);
// Remove old items
if (removedItems.length > 0) {
grid.remove(removedItems, { layout: false });
}
// Add new items with correct width
if (newElements.length > 0) {
newElements.forEach(el => {
el.style.width = `${itemWidth}px`;
});
grid.add(newElements, { layout: false });
}
// Update all item widths
domElements.forEach(el => {
el.style.width = `${itemWidth}px`;
});
// Refresh and layout
grid.refreshItems().layout();
};
requestAnimationFrame(() => {
syncGridItems(pinnedMuuri.current, pinnedGridRef, pinnedNotes);
syncGridItems(othersMuuri.current, othersGridRef, othersNotes);
});
}, [pinnedNotes, othersNotes]);
```
---
#### Task 5: Vérifier les tests Playwright
- [ ] **File:** `tests/drag-drop.spec.ts`
- [ ] **Action:** Exécuter les tests et vérifier que les sélecteurs `[data-draggable="true"]` matchent le wrapper
- [ ] **Commande:** `npx playwright test drag-drop.spec.ts`
**Points de vérification:**
- Ligne 45: `page.locator('[data-draggable="true"]')` doit trouver les `.masonry-item` wrappers
- Ligne 149: `firstNote.dragTo(secondNote)` doit fonctionner avec Muuri
---
## Acceptance Criteria
### AC1: Drag & Drop fonctionnel
- [ ] **Given** une grille de notes affichée
- [ ] **When** je drag une note vers une autre position
- [ ] **Then** la note se déplace visuellement avec placeholder
- [ ] **And** l'ordre est persisté aprÚs le drop
### AC2: Layout responsive
- [ ] **Given** une grille de notes avec différentes tailles
- [ ] **When** je redimensionne la fenĂȘtre du navigateur
- [ ] **Then** le nombre de colonnes s'adapte:
- < 480px: 1 colonne
- 480-768px: 2 colonnes
- 768-1024px: 2 colonnes
- 1024-1280px: 3 colonnes
- 1280-1600px: 4 colonnes
- > 1600px: 5 colonnes
### AC3: Tailles de notes respectées
- [ ] **Given** une note avec `data-size="large"`
- [ ] **When** la note est affichée dans la grille
- [ ] **Then** elle a une `min-height` de 300px
- [ ] **And** sa hauteur finale est déterminée par son contenu
### AC4: Synchronisation React-Muuri
- [ ] **Given** une grille avec des notes
- [ ] **When** j'ajoute une nouvelle note via l'input
- [ ] **Then** la note apparaĂźt dans la grille avec les bonnes dimensions
- [ ] **And** elle est draggable immédiatement
### AC5: Tests Playwright passants
- [ ] **Given** les tests Playwright existants
- [ ] **When** j'exécute `npx playwright test drag-drop.spec.ts`
- [ ] **Then** tous les tests passent avec les sélecteurs `[data-draggable="true"]`
---
## Additional Context
### Dependencies
| Dépendance | Version | Usage |
|------------|---------|-------|
| muuri | ^0.9.5 | Grille masonry avec drag & drop |
| web-animations-js | (bundled) | Polyfill animations |
| ResizeObserver | Native | Détection resize conteneur |
### Testing Strategy
**Tests automatisés:**
```bash
# Exécuter tests drag-drop
npx playwright test drag-drop.spec.ts
# Exécuter tests responsive (à ajouter)
npx playwright test --grep "responsive"
```
**Tests manuels:**
1. Ouvrir l'app sur différentes tailles d'écran
2. Vérifier le nombre de colonnes selon breakpoints
3. Drag une note et vérifier le placeholder
4. Ajouter une note et vérifier qu'elle est draggable
5. Redimensionner la fenĂȘtre et vĂ©rifier le re-layout
### Notes & Risques
> [!WARNING]
> **Risque: Synchronisation timing**
> Le `requestAnimationFrame` dans `syncGridItems` doit s'exĂ©cuter APRĂS que React ait rendu les nouveaux Ă©lĂ©ments DOM. Si des problĂšmes de timing apparaissent, utiliser `setTimeout(..., 0)` ou `MutationObserver`.
> [!NOTE]
> **Comportement Google Keep**
> Google Keep utilise des hauteurs automatiques basées sur le contenu. On ne fixe pas de hauteur, seulement la largeur. Muuri gÚre le positionnement vertical automatiquement.
> [!TIP]
> **Debug Muuri**
> Ajouter `console.log` dans `handleDragEnd` pour vérifier que l'ordre est bien capturé aprÚs un drag.
---
## Ordre d'exécution recommandé
```mermaid
flowchart TD
T1[Task 1: data-draggable] --> T4[Task 4: Sync React-Muuri]
T2[Task 2: applyItemDimensions] --> T3[Task 3: ResizeObserver]
T3 --> T4
T4 --> T5[Task 5: Tests Playwright]
```
1. **Task 1** (5 min) - Modification simple, débloque les tests
2. **Task 2** (10 min) - Refactoring fonction, prépare Task 3
3. **Task 3** (15 min) - ResizeObserver, dépend de Task 2
4. **Task 4** (20 min) - Sync React-Muuri, le plus critique
5. **Task 5** (5 min) - Validation finale
**Temps estimé total:** ~55 minutes