feat: bouton + à gauche du handle de bloc — insère un bloc dessous (v7.60.0)
- nouveau bouton .block-insert-btn émis AVANT le handle (6 points) dans le wrapper de chaque bloc de premier niveau : clic → E.addAfter(idx) → addAt(idx+1) → bloc inséré sous le courant et focusé (mécanisme déjà en place, réutilisé tel quel) - position : left:-64px (gouttière .blocks-container = 64px), -56px dans le breakpoint où la gouttière passe à 32px ; apparaît au survol/focus comme le handle ; masqué avec lui sur écran étroit (2 breakpoints) - aria-label + title « Add block below » - tests : tests/test_v760_block_insert_btn.py (2 tests : ordre dans le wrapper, câblage E.addAfter, positions/masquages CSS) - livraison : VERSION + app/main = 7.60.0, OpenAPI 525 chemins, CHANGELOG
This commit is contained in:
@@ -1,5 +1,20 @@
|
|||||||
# Changelog - FlowDeck
|
# 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
|
## v7.59.3 (2026-10-07) — Correction : « Turn into » du menu contextuel
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.59.3",
|
version="7.60.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.59.3"
|
"version": "7.60.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
@@ -2341,6 +2341,36 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
cursor: grabbing;
|
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 ── */
|
/* ── v5.10.0: selection multiple + menu contexte bloc ── */
|
||||||
.block-wrapper.selected {
|
.block-wrapper.selected {
|
||||||
background: rgba(35,131,226,0.06);
|
background: rgba(35,131,226,0.06);
|
||||||
@@ -2934,6 +2964,7 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
.page-title-input { font-size: 28px; }
|
.page-title-input { font-size: 28px; }
|
||||||
.blocks-container { padding: 8px 16px 96px; }
|
.blocks-container { padding: 8px 16px 96px; }
|
||||||
.block-handle { display: none; }
|
.block-handle { display: none; }
|
||||||
|
.block-insert-btn { display: none; }
|
||||||
.block-actions { display: none; }
|
.block-actions { display: none; }
|
||||||
.editor-statusbar { left: 0; }
|
.editor-statusbar { left: 0; }
|
||||||
}
|
}
|
||||||
@@ -3409,6 +3440,7 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
.page-cover-area { padding: 32px 32px 8px; }
|
.page-cover-area { padding: 32px 32px 8px; }
|
||||||
.blocks-container { padding: 8px 32px 80px; }
|
.blocks-container { padding: 8px 32px 80px; }
|
||||||
.block-handle { left: -32px; }
|
.block-handle { left: -32px; }
|
||||||
|
.block-insert-btn { left: -56px; }
|
||||||
.page-title-block::before { left: -32px; }
|
.page-title-block::before { left: -32px; }
|
||||||
.page-title-input { font-size: 32px; }
|
.page-title-input { font-size: 32px; }
|
||||||
|
|
||||||
@@ -3499,6 +3531,7 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
.page-title-input { font-size: 24px; }
|
.page-title-input { font-size: 24px; }
|
||||||
.blocks-container { padding: 8px 16px 96px; }
|
.blocks-container { padding: 8px 16px 96px; }
|
||||||
.block-handle { display: none; }
|
.block-handle { display: none; }
|
||||||
|
.block-insert-btn { display: none; }
|
||||||
.block-actions { display: none; }
|
.block-actions { display: none; }
|
||||||
.page-title-block::before { display: none; }
|
.page-title-block::before { display: none; }
|
||||||
.page-title-block .page-icon-emoji { margin-left: 0; }
|
.page-title-block .page-icon-emoji { margin-left: 0; }
|
||||||
|
|||||||
@@ -609,7 +609,10 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
|||||||
numAttr=` data-num="${idx-s+1}"`;
|
numAttr=` data-num="${idx-s+1}"`;
|
||||||
}
|
}
|
||||||
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||||||
|
// + « ajouter un bloc dessous », juste avant le handle (gouttière 64px)
|
||||||
|
const insertBtn=idx>=0?`<button type="button" class="block-insert-btn" title="Add block below" aria-label="Add block below" onclick="E.addAfter(${idx})">+</button>`:'';
|
||||||
return `<div class="block-wrapper" data-id="${b.id}">
|
return `<div class="block-wrapper" data-id="${b.id}">
|
||||||
|
${insertBtn}
|
||||||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||||||
${actions}
|
${actions}
|
||||||
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
|
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
|
||||||
|
|||||||
@@ -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("<div class=\"block-handle\">", 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
|
||||||
Reference in New Issue
Block a user