diff --git a/CHANGELOG.md b/CHANGELOG.md
index ffba989..5f0aade 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# Changelog - FlowDeck
+## v7.64.1 (2026-10-08) — Fix : Delete sur un bloc vide supprime le bloc
+
+### Fixed
+
+- **La touche Delete ne supprimait pas le bloc vide** : seul `Backspace` avait la
+ garde « bloc sans texte → supprimer le bloc ». La branche couvre désormais les
+ **deux touches** (`onKd`, une seule garde partagée) — Delete vide le bloc puis
+ le supprime au coup suivant, comme Backspace ; le dernier bloc de la page n'est
+ jamais supprimé (l'éditeur garde un paragraphe).
+- Le vide est jugé avec **`gtTok(el)`** (le texte lu à la sauvegarde) et non
+ `textContent` : une pastille de réaction seule dans le bloc ne l'empêche plus
+ d'être supprimé.
+
+### Tests
+
+- **e2e** `e2e/v7641_delete_key.spec.js` : texte → Delete (bloc vidé, pas
+ supprimé) → Delete (bloc supprimé, le précédent reste) → dernier bloc vide
+ jamais supprimé.
+
+
## v7.64.0 (2026-10-08) — Commentaires façon Notion : ancres jaunes, tiroir refondu, réactions sur texte
### Added
diff --git a/ROADMAP.md b/ROADMAP.md
index 88373eb..8b934e7 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1459,6 +1459,10 @@ Détails livrés :
rouvrir
- [x] **Tests** — `tests/test_v764_comments_reactions.py` (8, dont toggle API réel),
e2e `e2e/v764_comments_reactions.spec.js` ; `VERSION` + `app/main.py` = 7.64.0
+- [x] **v7.64.1 — Fix Delete sur bloc vide** : la garde « texte vide → supprimer
+ le bloc » couvre `Backspace` **et** `Delete` (une seule branche dans `onKd`),
+ vide jugé via `gtTok()` (ignore les pastilles) ; e2e
+ `e2e/v7641_delete_key.spec.js`
---
diff --git a/VERSION b/VERSION
index 41f66b4..762011f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-7.64.0
+7.64.1
diff --git a/WORKLOAD.md b/WORKLOAD.md
index 432e92a..292d058 100644
--- a/WORKLOAD.md
+++ b/WORKLOAD.md
@@ -1,6 +1,6 @@
# WORKLOAD — FlowDeck Notion Clone
-> **Début**: 2026-07-08 | **Version**: v7.64.0 (commentaires façon Notion : ancres jaunes, tiroir refondu, réactions emoji sur texte) | **Statut**: EN COURS 🔄
+> **Début**: 2026-07-08 | **Version**: v7.64.1 (fix Delete sur bloc vide → suppression du bloc) | **Statut**: EN COURS 🔄
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
## Avancement Global
diff --git a/app/main.py b/app/main.py
index 67abd28..b1dd7ad 100644
--- a/app/main.py
+++ b/app/main.py
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
- version="7.64.0",
+ version="7.64.1",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json
index 7f0825d..d6213d5 100644
--- a/docs/openapi-v2.json
+++ b/docs/openapi-v2.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "FlowDeck",
- "version": "7.64.0"
+ "version": "7.64.1"
},
"paths": {
"/auth/register": {
diff --git a/e2e/v7641_delete_key.spec.js b/e2e/v7641_delete_key.spec.js
new file mode 100644
index 0000000..fadeaa4
--- /dev/null
+++ b/e2e/v7641_delete_key.spec.js
@@ -0,0 +1,80 @@
+const { test, expect } = require('@playwright/test');
+
+/**
+ * v7.64.1 — Delete sur un bloc vide supprime le bloc (comme Backspace).
+ * Instance de test attendue sur FD_BASE_URL (défaut 8081).
+ */
+const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8081';
+const USER = process.env.FD_USER || 'e2e@flowdeck.local';
+const PASS = process.env.FD_PASS || 'e2e-secret-123';
+
+test.use({ serviceWorkers: 'block' });
+
+async function login(page) {
+ await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
+ await page.fill('#email', USER);
+ await page.fill('#password', PASS);
+ await page.click('.btn-primary');
+ const ok = await page
+ .waitForURL('**/workspaces', { timeout: 8000 })
+ .then(() => true)
+ .catch(() => false);
+ if (!ok) {
+ const resp = await page.request.post(`${FD_BASE}/auth/register`, {
+ data: { email: USER, password: PASS, name: 'E2E' },
+ });
+ if (resp.status() !== 409) expect(resp.ok()).toBeTruthy();
+ await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' });
+ }
+}
+
+test('Delete : vide le bloc puis supprime le bloc vide', async ({ page }) => {
+ await login(page);
+ const cookies = await page.context().cookies();
+ const csrf = (cookies.find((c) => c.name === 'csrf_token') || {}).value || '';
+ const created = await page.request.post(
+ `${FD_BASE}/board/api/pages?title=Delete key ${Date.now()}`,
+ { headers: { 'X-CSRF-Token': csrf } }
+ );
+ expect(created.status(), await created.text()).toBeLessThan(400);
+ const { id } = await created.json();
+ const bs = await page.request.post(`${FD_BASE}/board/api/pages/${id}/blocks`, {
+ headers: { 'X-CSRF-Token': csrf },
+ data: { title: 'Delete key', blocks: [
+ { id: 'b1', type: 'paragraph', content: 'hello' },
+ { id: 'b2', type: 'paragraph', content: 'world' },
+ ] },
+ });
+ expect(bs.status(), await bs.text()).toBeLessThan(400);
+ await page.goto(`${FD_BASE}/pages/${id}`, { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 });
+
+ // ── 1. Delete avec du texte : suppression avant le curseur (comportement natif) ──
+ await page.locator('#_blocksCt [data-bid]').nth(1).click();
+ await page.keyboard.press('Control+a');
+ await page.keyboard.press('Delete');
+ await page.waitForTimeout(200);
+ const contents = await page.evaluate(() => {
+ window.E.sync();
+ return window.E.blocks.map((b) => b.content);
+ });
+ expect(contents[0]).toBe('hello');
+ expect(contents[1].trim()).toBe(''); // bloc vidé (un
résiduel), mais toujours là
+
+ // ── 2. Delete sur le bloc vide : le bloc est supprimé ──
+ await page.keyboard.press('Delete');
+ await page.waitForTimeout(300);
+ expect(await page.evaluate(() => window.E.blocks.length)).toBe(1);
+ expect(await page.evaluate(() => window.E.blocks[0].content)).toBe('hello');
+ await page.screenshot({ path: 'shots/v7641-delete.png' });
+
+ // ── 3. dernier bloc vide : jamais supprimé (l'éditeur garde un bloc) ──
+ await page.locator('#_blocksCt [data-bid]').first().click();
+ await page.keyboard.press('Control+a');
+ await page.keyboard.press('Delete');
+ await page.waitForTimeout(150);
+ await page.keyboard.press('Delete');
+ await page.waitForTimeout(300);
+ expect(await page.evaluate(() => window.E.blocks.length)).toBe(1);
+ expect(await page.evaluate(() => window.E.blocks[0].type)).toBe('paragraph');
+});
diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js
index 85eaee4..b11696e 100644
--- a/static/js/page_editor_scripts.js
+++ b/static/js/page_editor_scripts.js
@@ -2417,7 +2417,10 @@ applyAIBlocks(text){
}
if(e.key==='ArrowUp'){const _sp=splitCaret(el);if(_sp&&!_sp.before.trim()&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
if(e.key==='ArrowDown'){const _sp=splitCaret(el);if(_sp&&!_sp.after.trim()&&idx{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
+ // Backspace / Delete sur un bloc vide → supprimer le bloc (même garde
+ // pour les deux touches ; le texte restant est lu comme à la sauvegarde
+ // pour ignorer les annotations .fd-react-chip)
+ if(e.key==='Backspace'||e.key==='Delete'){if(!(gtTok(el)||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();this.pushHistory();block.type='paragraph';block.content='';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
if(e.key===' '){const i3=idx;setTimeout(()=>this.checkMd(i3),15);}
if((e.ctrlKey||e.metaKey)&&!e.altKey){
if(e.key.toLowerCase()==='z'){e.preventDefault();if(e.shiftKey)this.redo();else this.undo();return;}