diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9dd40..a85e7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog - FlowDeck +## v7.60.0 (2026-10-07) — Éditeur : bouton + à gauche du handle de bloc + +### Added + +- **Bouton `+` à gauche du handle** (les 6 points qui ouvrent le menu ⋮⋮ / + servent au drag) : clique → **nouveau bloc inséré sous le bloc courant**, + via le mécanisme déjà en place (`E.addAfter(idx)` → `addAt(idx+1)`). +- Positionné dans la gouttière (`left:-64px`, gouttière `.blocks-container` + de 64px, `-56px` quand elle passe à 32px), apparaît au survol/focus du bloc + comme le handle, masqué avec lui sur écran étroit (≤768px et le second + breakpoint — aucun débordement horizontal ajouté au conteneur. +- `aria-label` + `title` « Add block below » (accessibilité). +- **Tests** — `tests/test_v760_block_insert_btn.py` : **2 tests** (ordre + dans le wrapper, câblage `E.addAfter`, positions/masquages CSS). + ## v7.59.3 (2026-10-07) — Correction : « Turn into » du menu contextuel ### Fixed diff --git a/VERSION b/VERSION index 30e0d31..22861a9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.59.3 +7.60.0 diff --git a/app/main.py b/app/main.py index c1841fe..6407807 100644 --- a/app/main.py +++ b/app/main.py @@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.59.3", + version="7.60.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index fa08625..497cdc6 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.59.3" + "version": "7.60.0" }, "paths": { "/auth/register": { diff --git a/static/css/app.css b/static/css/app.css index 465ada7..07801cd 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -2341,6 +2341,36 @@ button.breadcrumb-current { max-width: 260px; } cursor: grabbing; } +/* + « ajouter un bloc dessous » — juste à gauche du handle (gouttière 64px) */ +.block-insert-btn { + position: absolute; + left: -64px; + top: 5px; + width: 22px; + height: 22px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + border-radius: 3px; + color: var(--text-dim); + font-size: 17px; + line-height: 1; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease; +} +.block-wrapper:hover .block-insert-btn, +.block-wrapper:focus-within .block-insert-btn { + opacity: 1; +} +.block-insert-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + /* ── v5.10.0: selection multiple + menu contexte bloc ── */ .block-wrapper.selected { background: rgba(35,131,226,0.06); @@ -2934,6 +2964,7 @@ button.breadcrumb-current { max-width: 260px; } .page-title-input { font-size: 28px; } .blocks-container { padding: 8px 16px 96px; } .block-handle { display: none; } + .block-insert-btn { display: none; } .block-actions { display: none; } .editor-statusbar { left: 0; } } @@ -3409,6 +3440,7 @@ button.breadcrumb-current { max-width: 260px; } .page-cover-area { padding: 32px 32px 8px; } .blocks-container { padding: 8px 32px 80px; } .block-handle { left: -32px; } + .block-insert-btn { left: -56px; } .page-title-block::before { left: -32px; } .page-title-input { font-size: 32px; } @@ -3499,6 +3531,7 @@ button.breadcrumb-current { max-width: 260px; } .page-title-input { font-size: 24px; } .blocks-container { padding: 8px 16px 96px; } .block-handle { display: none; } + .block-insert-btn { display: none; } .block-actions { display: none; } .page-title-block::before { display: none; } .page-title-block .page-icon-emoji { margin-left: 0; } diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js index 8eafe00..878c343 100644 --- a/static/js/page_editor_scripts.js +++ b/static/js/page_editor_scripts.js @@ -609,7 +609,10 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON. numAttr=` data-num="${idx-s+1}"`; } const actions=idx>=0?`
`:''; + // + « ajouter un bloc dessous », juste avant le handle (gouttière 64px) + const insertBtn=idx>=0?``:''; return `
+ ${insertBtn}
${actions} <${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner} diff --git a/tests/test_v760_block_insert_btn.py b/tests/test_v760_block_insert_btn.py new file mode 100644 index 0000000..7f0456a --- /dev/null +++ b/tests/test_v760_block_insert_btn.py @@ -0,0 +1,29 @@ +"""7.60.0 — bouton + à gauche du handle : ajoute un bloc sous le courant.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +EDITOR_JS = (ROOT / "static" / "js" / "page_editor_scripts.js").read_text(encoding="utf-8") +APP_CSS = (ROOT / "static" / "css" / "app.css").read_text(encoding="utf-8") + + +def test_button_rendered_left_of_the_handle(): + # le + est émis AVANT le handle dans le wrapper du bloc + i_btn = EDITOR_JS.index("class=\"block-insert-btn\"") + i_handle = EDITOR_JS.index("
", i_btn) + assert i_btn < i_handle + # clic → insertion sous le bloc courant (mécanisme déjà existant) + assert 'class="block-insert-btn" title="Add block below" aria-label="Add block below" onclick="E.addAfter(${idx})"' in EDITOR_JS + # réservé aux blocs de premier niveau (idx valide), comme .block-actions + assert "const insertBtn=idx>=0?" in EDITOR_JS + assert "addAfter(idx,type){return this.addAt(idx+1,type);}" in EDITOR_JS + + +def test_button_style_and_visibility(): + assert ".block-insert-btn {" in APP_CSS + assert "left: -64px;" in APP_CSS # gouttière .blocks-container (64px) + assert ".block-wrapper:hover .block-insert-btn," in APP_CSS + assert ".block-insert-btn:hover {" in APP_CSS + # suit le handle quand la gouttière passe à 32px, masqué partout où il l'est + assert ".block-insert-btn { left: -56px; }" in APP_CSS + assert APP_CSS.count(".block-insert-btn { display: none; }") == 2