diff --git a/CHANGELOG.md b/CHANGELOG.md
index a85e7e7..0d7d063 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,56 @@
# Changelog - FlowDeck
+## v7.61.0 (2026-10-08) — Menu contextuel de bloc refondu façon Notion
+
+### Added
+
+- **Panneau de gauche** : barre de recherche « Search actions... » (filtre en
+ direct), titre de section « Text », puis les 10 actions dans l'ordre Notion :
+ Turn into ›, Color ›, Copy link to block (`Alt+⇧+L`), Duplicate (`Ctrl+D`),
+ Move to (`Ctrl+⇧+P`), Delete (`Del`), Comment (`Ctrl+⇧+M`), Suggest edits
+ (`Ctrl+⇧+Alt+X`), Ask AI (`Ctrl+J`), Skills › — avec les raccourcis
+ affichés, et le pied de page « Last edited by … / Today at H:MM / N words,
+ M characters » (auteur lu dans le compte, heure = `updatedAt` du bloc page,
+ taille calculée sur le contenu du bloc).
+- **Sous-menu « Turn into »** (survol du panneau de gauche → panneau de
+ droite, fermeture différée) : Text ✓ (type actuel coché), Heading 1-4,
+ Bulleted list, Numbered list, To-do list, Toggle list, Code, Quote,
+ Callout, Block equation, Synced block, et **2/3/4/5 columns** avec
+ info-bulle « Create N columns of blocks » ; `turnInto(idx, type, cols)`
+ crée désormais N colonnes et ouvre le sélecteur de source pour les blocs
+ synchronisés.
+- **Sous-menu « Color »** : sections **Last used** (raccourci `Ctrl+⇧+H`),
+ **Text color** (Default, Gray, Brown, Orange, Yellow ✓, Green, Blue,
+ Purple, Pink, Red — glyphe « A » à la couleur) et **Background color**
+ (même palette avec carré de couleur) ; la couleur utilisée est cochée, la
+ dernière est mémorisée (`localStorage fdLastBlockColor`).
+- **Sous-menu « Skills »** : liste réelle des compétences de l'espace
+ (`GET /api/agent/skills`) → applique le `prompt_template` au contenu du
+ bloc via l'IA et insère le résultat dessous.
+- **Raccourcis** — handler clavier dédié : `Ctrl+J` (Ask AI), `Ctrl+⇧+H`
+ (dernière couleur), `Ctrl+⇧+M` (commenter le bloc : sélection du contenu +
+ tiroir de commentaires), `Ctrl+⇧+Alt+X` (suggérer une édition → résultat
+ inséré dessous), `Alt+⇧+L` (copier le lien), `Ctrl+⇧+P` (déplacer vers…),
+ `Ctrl+D` (dupliquer — déjà géré par la saisie, pas de double), `Del`
+ (**seulement avec le menu ouvert**, jamais pendant la saisie), `Échap`
+ (fermer).
+- **Actions Suggest edits / Ask AI / Comment / Skills** réelles côté éditeur
+ (`suggestEdit`, `askAI`, `commentBlock`, `runSkill`) — Ask AI ouvre le
+ composeur inline sans effacer le contenu du bloc.
+- **Styles** : `.bm-group`, `.bm-foot`, `.bm-check`, `.bm-current`,
+ `.bm-dot`, sous-menu `60vh` défilable, recherche focalisée à l'ouverture.
+- **Tests** — `tests/test_v761_block_menu.py` : **9 tests** (structure et
+ ordre du panneau, dispatch des 10 actions, 3 sous-menus, survol/fermeture,
+ les 9 raccourcis, comportement node : `turnInto(…, 'columns', 3)` → 3
+ colonnes, styles) + `test_v7593` adapté à la nouvelle signature.
+
+### Removed
+
+- Entrées **Copy / Cut / Paste blocks** du menu (absentes de la spécification
+ Notion) — les raccourcis `Ctrl+C/X/V` sur blocs sélectionnés sont conservés.
+- Pastilles de couleurs inline : remplacées par le sous-menu Color nommé
+ (palette portée à 10 couleurs alignées Notion : +Brown, Pink réel).
+
## v7.60.0 (2026-10-07) — Éditeur : bouton + à gauche du handle de bloc
### Added
diff --git a/VERSION b/VERSION
index 22861a9..4a41042 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-7.60.0
+7.61.0
diff --git a/app/main.py b/app/main.py
index 6407807..94c75c6 100644
--- a/app/main.py
+++ b/app/main.py
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
- version="7.60.0",
+ version="7.61.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json
index 497cdc6..fc5cdcb 100644
--- a/docs/openapi-v2.json
+++ b/docs/openapi-v2.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "FlowDeck",
- "version": "7.60.0"
+ "version": "7.61.0"
},
"paths": {
"/auth/register": {
diff --git a/static/css/app.css b/static/css/app.css
index 07801cd..e27fe7d 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -2406,6 +2406,30 @@ button.breadcrumb-current { max-width: 260px; }
.block-menu .bm-item.bm-danger .bm-ico { color: #e5484d; }
.block-menu .bm-item.bm-danger:hover { background: rgba(229,72,77,.12); }
.block-menu .bm-sep { height: 1px; background: var(--border, #2a2a2a); margin: 5px 4px; }
+/* ── v7.61 : menu contextuel type Notion — recherche, sections, pied, sous-menus ── */
+.block-menu .bm-group {
+ padding: 9px 9px 3px;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: .5px;
+}
+.block-menu .bm-foot {
+ border-top: 1px solid var(--border, #2a2a2a);
+ margin-top: 6px;
+ padding: 8px 9px 6px;
+ font-size: 11.5px;
+ line-height: 1.55;
+ color: var(--text-tertiary);
+}
+.block-menu .bm-check { margin-left: auto; color: var(--accent, #2383e2); font-size: 12px; }
+.block-menu .bm-item.bm-current { background: var(--bg-secondary); }
+.block-menu .bm-dot {
+ display: inline-block; width: 12px; height: 12px; border-radius: 3px;
+ border: 1px solid var(--border, #333); vertical-align: -2px;
+}
+.block-menu .bm-search:focus { border-color: var(--accent, #2383e2); }
.block-menu .bm-colors { padding: 6px 9px; }
.block-menu .bm-colors-label { font-size: 10px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 6px; }
.block-menu .bm-swatches { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -2436,7 +2460,7 @@ button.breadcrumb-current { max-width: 260px; }
color: var(--text-secondary); cursor: pointer; font-size: 12px; border-radius: 6px;
}
.block-menu .bm-cancel:hover { background: var(--bg-hover); }
-.block-menu-sub { min-width: 180px; max-height: 320px; overflow-y: auto; }
+.block-menu-sub { min-width: 230px; max-height: 60vh; overflow-y: auto; }
.ftable-hdr-toggle { margin-left: 12px; font-size: 12px; color: var(--text-tertiary); display:inline-flex; align-items:center; gap:2px; }
.ftable-hdr-toggle label { display:inline-flex; align-items:center; gap:4px; cursor:pointer; }
diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js
index 878c343..a3aaf54 100644
--- a/static/js/page_editor_scripts.js
+++ b/static/js/page_editor_scripts.js
@@ -921,74 +921,214 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
{id:'numbered_list',name:'Numbered list'},{id:'to_do',name:'To-do'},{id:'toggle',name:'Toggle'},
{id:'quote',name:'Quote'},{id:'callout',name:'Callout'},{id:'table',name:'Table'},
{id:'math',name:'Math equation'},{id:'columns',name:'Column list'},{id:'button',name:'Button / Automation'},
- {id:'code',name:'Code block'}
+ {id:'code',name:'Code block'},{id:'synced',name:'Synced block'}
];
- const _BLOCK_COLORS_TEXT=['default','#E5484D','#F76B15','#FFC53D','#46A758','#3E63DD','#8E4EC6','#12A594','#94A3B8'];
- const _BLOCK_COLORS_BG=['default','rgba(229,72,77,.15)','rgba(247,107,21,.15)','rgba(255,197,61,.15)','rgba(70,154,88,.15)','rgba(62,99,221,.15)','rgba(142,78,198,.15)','rgba(18,165,148,.15)','rgba(148,163,184,.2)'];
- function _blockMenuClose(){var m=document.getElementById('blockMenu');if(m)m.remove();m=document.getElementById('blockMenuSub');if(m)m.remove();}
+ // Sous-menu « Turn into » — ordre Notion ; id ∈ _BLOCK_TURN_TYPES, cols = nb de colonnes
+ const _BLOCK_TURN_MENU=[
+ {id:'paragraph',name:'Text',icon:'T'},{id:'heading_1',name:'Heading 1',icon:'H1'},
+ {id:'heading_2',name:'Heading 2',icon:'H2'},{id:'heading_3',name:'Heading 3',icon:'H3'},
+ {id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},
+ {id:'numbered_list',name:'Numbered list',icon:'1.'},{id:'to_do',name:'To-do list',icon:'☑'},
+ {id:'toggle',name:'Toggle list',icon:'▸'},{id:'code',name:'Code',icon:'</>'},
+ {id:'quote',name:'Quote',icon:'”'},{id:'callout',name:'Callout',icon:'ⓘ'},
+ {id:'math',name:'Block equation',icon:'∑'},{id:'synced',name:'Synced block',icon:'⛓'},
+ {id:'columns',name:'2 columns',icon:'▦',cols:2},{id:'columns',name:'3 columns',icon:'▦',cols:3},
+ {id:'columns',name:'4 columns',icon:'▦',cols:4},{id:'columns',name:'5 columns',icon:'▦',cols:5}
+ ];
+ // Couleurs texte/fond alignées par index — noms Notion (Dernière utilisée ailleurs)
+ const _BLOCK_COLOR_NAMES=['Default','Gray','Brown','Orange','Yellow','Green','Blue','Purple','Pink','Red'];
+ const _BLOCK_COLORS_TEXT=['default','#94A3B8','#8A6642','#F76B15','#FFC53D','#46A758','#3E63DD','#8E4EC6','#E5539B','#E5484D'];
+ const _BLOCK_COLORS_BG=['default','rgba(148,163,184,.15)','rgba(138,102,66,.15)','rgba(247,107,21,.15)','rgba(255,197,61,.15)','rgba(70,154,88,.15)','rgba(62,99,221,.15)','rgba(142,78,198,.15)','rgba(229,83,155,.15)','rgba(229,72,77,.15)'];
+ // Raccourcis affichés — implémentés par le handler clavier en bas de fichier
+ const _BM_SHORTCUTS={link:'Alt+⇧+L',duplicate:'Ctrl+D',move:'Ctrl+⇧+P',delete:'Del',comment:'Ctrl+⇧+M',suggest:'Ctrl+⇧+Alt+X',ai:'Ctrl+J',color:'Ctrl+⇧+H'};
+ var _bmSubKind='',_bmSubTimer=null;
+ function _blockMenuClose(){var m=document.getElementById('blockMenu');if(m)m.remove();m=document.getElementById('blockMenuSub');if(m)m.remove();clearTimeout(_bmSubTimer);_bmSubKind='';}
+ function _blockAttr(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');}
+ function _blockMenuBid(){var m=document.getElementById('blockMenu');return m?(m.getAttribute('data-bid')||''):'';}
+ // Sous-menu ouvert au survol de l'entrée correspondante (panneau de droite)
+ function _blockSubOpen(kind,bid,anchor){
+ if(!kind||!bid)return;
+ var m=document.getElementById('blockMenu');if(!m)return;
+ if(_bmSubKind===kind&&document.getElementById('blockMenuSub'))return;
+ clearTimeout(_bmSubTimer);_bmSubKind=kind;
+ var r=m.getBoundingClientRect();
+ var a=anchor&&anchor.getBoundingClientRect?anchor.getBoundingClientRect():r;
+ var top=Math.max(8,Math.min(a.top,window.innerHeight-60));
+ if(kind==='turn')_blockTurnMenuOpen(r.right+4,top,bid);
+ else if(kind==='color')_blockColorMenuOpen(r.right+4,top,bid);
+ else if(kind==='skills')_blockSkillsMenuOpen(r.right+4,top,bid);
+ var s=document.getElementById('blockMenuSub');
+ if(s){s.addEventListener('mouseenter',function(){clearTimeout(_bmSubTimer);});
+ s.addEventListener('mouseleave',function(){_blockSubCloseLater();});}
+ }
+ function _blockSubCloseLater(){clearTimeout(_bmSubTimer);_bmSubTimer=setTimeout(function(){var s=document.getElementById('blockMenuSub');if(s&&!s.matches(':hover')){s.remove();_bmSubKind='';}},260);}
function _blockMenuClick(ev){
var t=ev.target.closest&&ev.target.closest('[data-bm-act]');if(!t)return;
- var act=t.getAttribute('data-bm-act');var bid=t.getAttribute('data-bid');
+ var act=t.getAttribute('data-bm-act');var bid=t.getAttribute('data-bid')||_blockMenuBid();
if(!act)return;
ev.preventDefault();ev.stopPropagation();
var E=window.E;if(!E)return;
var idx=bid?E.getIdx(bid):-1;
- if(act==='turn'){var m=document.getElementById('blockMenu');if(m){var r=m.getBoundingClientRect();_blockTurnMenuOpen(r.right+4,r.top,bid||'');}return;}
+ if(act==='submenu'){_blockSubOpen(t.getAttribute('data-sub'),bid,t);return;}
if(act==='duplicate'){E.duplicateBlock(idx);}
else if(act==='copy-link'){E.copyBlockLink(bid||'');}
- else if(act==='copy-blocks'){E.copySelectedBlocks();}
- else if(act==='cut-blocks'){E.cutSelectedBlocks();}
- else if(act==='paste-blocks'){E.pasteBlocks();}
else if(act==='move-to'){_blockMoveToOpen(idx);}
else if(act==='delete'){E.deleteSelected(idx);}
+ else if(act==='comment'){E.commentBlock(idx);}
+ else if(act==='suggest'){E.suggestEdit(idx);}
+ else if(act==='ai'){E.askAI(idx);}
+ else if(act==='skill'){E.runSkill(idx,t.getAttribute('data-prompt')||'');}
else if(act==='color'){var key=t.getAttribute('data-color-key'),val=t.getAttribute('data-color-val');E.setBlockColor(idx,key,val);}
- else if(act==='turn-type'){E.turnInto(idx,t.getAttribute('data-type'));_blockMenuClose();}
+ else if(act==='turn-type'){E.turnInto(idx,t.getAttribute('data-type'),parseInt(t.getAttribute('data-cols')||'0',10)||0);_blockMenuClose();return;}
_blockMenuClose();
}
+ function _blockMenuItem(act,icon,label,opts){
+ opts=opts||{};
+ var attr=' data-bm-act="'+act+'"';
+ if(opts.bid)attr+=' data-bid="'+_blockAttr(opts.bid)+'"';
+ if(opts.sub)attr+=' data-sub="'+opts.sub+'"';
+ if(opts.colorKey)attr+=' data-color-key="'+opts.colorKey+'" data-color-val="'+_blockAttr(opts.colorVal||'')+'"';
+ if(opts.prompt!=null)attr+=' data-prompt="'+_blockAttr(opts.prompt)+'"';
+ if(opts.type)attr+=' data-type="'+opts.type+'"';
+ if(opts.cols)attr+=' data-cols="'+opts.cols+'"';
+ if(opts.title)attr+=' title="'+_blockAttr(opts.title)+'"';
+ var right=opts.check?'✓'
+ :(opts.sub?'▸'
+ :(opts.shortcut?''+opts.shortcut+'':''));
+ return '
'
+ +(icon!=null?''+icon+'':'')+label+right+'
';
+ }
+ // Métadonnées du pied de page : auteur / heure de dernière édition / taille du bloc
+ function _blockMenuMeta(b){
+ var E=window.E||{};
+ var who='';
+ try{var n=document.querySelector('.um-name')||document.querySelector('.workspace-name');who=n?(n.textContent||'').trim():'';}catch(e){}
+ var when='';
+ if(E.updatedAt){var d=new Date(E.updatedAt);if(!isNaN(d)){
+ var same=d.toDateString()===new Date().toDateString();
+ var hm=d.toLocaleTimeString([],{hour:'numeric',minute:'2-digit'});
+ when=(same?'Today at ':(d.toLocaleDateString([])+' at '))+hm;}}
+ var content=(b&&b.content)||'';
+ var trimmed=content.trim();
+ var words=trimmed?trimmed.split(/\s+/).length:0;
+ return '
';
+ }
function blockMenuOpen(x,y,bid){
_blockMenuClose();
var E=window.E;if(!E)return;
var idx=E.getIdx(bid);if(idx<0)return;
var b=E.blocks[idx];if(!b)return;
- var d=document.createElement('div');d.id='blockMenu';d.className='block-menu';d.style.cssText='position:fixed;z-index:4000;';
- var colorsText='',colorsBg='';
- _BLOCK_COLORS_TEXT.forEach(function(c){colorsText+='';});
- _BLOCK_COLORS_BG.forEach(function(c){colorsBg+='';});
- d.innerHTML='
↩Turn into▸
'
- +''
- +'
▣Duplicate '+(_rtMac()?'⌘D':'Ctrl+D')+'
'
- +'
🔗Copy link to block
'
- +''
- +'
⎘Copy blocks '+(_rtMac()?'⌘C':'Ctrl+C')+'
'
- +'
✂Cut blocks '+(_rtMac()?'⌘X':'Ctrl+X')+'
'
- +'
⎘Paste blocks '+(_rtMac()?'⌘V':'Ctrl+V')+'
'
- +''
- +'
↪Move to…
'
- +''
- +'
Text
'+colorsText+'
'
- +'
Background
'+colorsBg+'
'
- +''
- +'
✕Delete ⌫
';
+ var d=document.createElement('div');d.id='blockMenu';d.className='block-menu';d.setAttribute('data-bid',b.id);d.style.cssText='position:fixed;z-index:4000;';
+ var S=_BM_SHORTCUTS;
+ d.innerHTML=''
+ +'
Text
'
+ +_blockMenuItem('submenu','↩','Turn into',{sub:'turn'})
+ +_blockMenuItem('submenu','🎨','Color',{sub:'color'})
+ +_blockMenuItem('link','🔗','Copy link to block',{shortcut:S.link})
+ +_blockMenuItem('duplicate','▣','Duplicate',{shortcut:S.duplicate})
+ +_blockMenuItem('move-to','↪','Move to…',{shortcut:S.move})
+ +_blockMenuItem('delete','✕','Delete',{shortcut:S.delete,danger:true})
+ +_blockMenuItem('comment','💬','Comment',{shortcut:S.comment})
+ +_blockMenuItem('suggest','✎','Suggest edits',{shortcut:S.suggest})
+ +_blockMenuItem('ai','✨','Ask AI',{shortcut:S.ai})
+ +_blockMenuItem('submenu','📚','Skills',{sub:'skills'})
+ +_blockMenuMeta(b);
d.addEventListener('click',_blockMenuClick);
+ d.addEventListener('input',function(ev){
+ if(!ev.target.classList||!ev.target.classList.contains('bm-search'))return;
+ var q=(ev.target.value||'').toLowerCase();
+ d.querySelectorAll('.bm-item').forEach(function(it){
+ it.style.display=(q&&it.textContent.toLowerCase().indexOf(q)<0)?'none':'';
+ });
+ var g=d.querySelector('.bm-group');if(g)g.style.display=q?'none':'';
+ });
+ d.addEventListener('mouseover',function(ev){
+ var it=ev.target&&ev.target.closest?ev.target.closest('[data-sub]'):null;
+ if(it)_blockSubOpen(it.getAttribute('data-sub'),b.id,it);
+ });
+ d.addEventListener('mouseleave',function(){_blockSubCloseLater();});
d.addEventListener('contextmenu',function(ev){ev.preventDefault();_blockMenuClose();});
document.body.appendChild(d);
var r=d.getBoundingClientRect();
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
d.style.left=x+'px';d.style.top=y+'px';
+ var si=d.querySelector('.bm-search');if(si)setTimeout(function(){si.focus();},10);
}
- function _blockTurnMenuOpen(x,y,bid){
+ // Panneau de droite : coquille commune des 3 sous-menus
+ function _blockSubShell(x,y){
var m=document.getElementById('blockMenuSub');if(m)m.remove();
var d=document.createElement('div');d.id='blockMenuSub';d.className='block-menu block-menu-sub';d.style.cssText='position:fixed;z-index:4001;';
- var html='';
- _BLOCK_TURN_TYPES.forEach(function(t){html+='
'+t.name+'
';});
- d.innerHTML=html;
d.addEventListener('click',_blockMenuClick);
document.body.appendChild(d);
var r=d.getBoundingClientRect();
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
d.style.left=x+'px';d.style.top=y+'px';
+ return d;
+ }
+ function _blockTurnMenuOpen(x,y,bid){
+ var d=_blockSubShell(x,y);
+ var E=window.E;var cur='';
+ if(E){var i=E.getIdx(bid);if(i>=0&&E.blocks[i])cur=E.blocks[i].type||'';}
+ var html='';
+ _BLOCK_TURN_MENU.forEach(function(t){
+ var on=(t.id===cur&&!t.cols);
+ html+=_blockMenuItem('turn-type',t.icon,t.name,{bid:bid,type:t.id,cols:t.cols,
+ title:t.cols?('Create '+t.cols+' columns of blocks'):'',
+ check:on,current:on});
+ });
+ d.innerHTML=html;
+ }
+ function _blockColorMenuOpen(x,y,bid){
+ var d=_blockSubShell(x,y);
+ var E=window.E;var idx=E?E.getIdx(bid):-1;var b=idx>=0?E.blocks[idx]:null;
+ var curT=(b&&b.style&&b.style.color)||'default';
+ var curB=(b&&b.style&&b.style.bgColor)||'default';
+ var last='';try{last=localStorage.getItem('fdLastBlockColor')||'';}catch(e){}
+ var html='';
+ if(last){
+ var i=last.indexOf('|');var lk=i<0?'color':last.slice(0,i);var lv=last.slice(i+1);
+ var isBg=(lk==='bgColor');
+ var arr=isBg?_BLOCK_COLORS_BG:_BLOCK_COLORS_TEXT;
+ var ni=arr.indexOf(lv);
+ var nm=(ni>=0?_BLOCK_COLOR_NAMES[ni]:'Custom')+(isBg?' background':' text');
+ html+='