feat: Share/Publish dialog — Notion-style with tabs, permissions, publish
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Has been skipped

Complete redesign per Bruno's spec:
- Floating dialog (440px, 18px radius) under Share button
- Header: Share | Publish tabs with active underline, close button
- Share tab: invite input, participants list with role dropdowns,
  General Access (Only invited / Anyone with link), footer with Copy link
- Publish tab: preview card, Publish button (before), URL bar + settings
  rows (link expiry, SEO, duplicate, edit, comments) + Unpublish (after)
- Toast notification for copy link (bottom centered, 2500ms)
- Colors: #1F1F1F surface, #2A2A2A secondary, #2383E2 accent
- Toggle: OFF=#555, ON=#2383E2, thumb=white 16px
This commit is contained in:
2026-07-10 11:36:28 -04:00
parent ed1b5d4193
commit 124bb3cd9f
2 changed files with 376 additions and 155 deletions
+171 -95
View File
@@ -3,7 +3,7 @@
{% block page_title %}{{ page.title }}{% endblock %}
{% block topbar %}
<!-- Page editor uses its own topbar — base.html topbar hidden -->
<!-- Page editor uses its own topbar -->
{% endblock %}
{% block content %}
@@ -22,75 +22,170 @@
</button>
<button class="page-action-btn" @click="moreOpen = !moreOpen">⋮</button>
<!-- Share dropdown -->
<div class="share-dropdown" x-show="shareOpen" @click.away="shareOpen = false" x-transition>
<!-- ═══════════ Share / Publish Dialog ═══════════ -->
<div class="share-dialog" x-show="shareOpen" @click.away="shareOpen = false" x-transition.opacity.scale.origin.top.right>
<!-- Header with tabs -->
<div class="sd-header">
<button class="sd-tab" :class="{ active: shareTab === 'share' }" @click="shareTab = 'share'">Share</button>
<button class="sd-tab" :class="{ active: shareTab === 'publish' }" @click="shareTab = 'publish'">
Publish
<span class="sd-badge" x-show="pagePublished"></span>
</button>
<button class="sd-close" @click="shareOpen = false">✕</button>
</div>
<!-- General Access -->
<div class="share-dd-section">
<div class="share-dd-label">General Access</div>
<div style="position:relative;">
<button class="share-access-btn" @click="accessMenuOpen = !accessMenuOpen">
<span x-text="generalAccess === 'invited' ? '🔒 Only people invited' : '🌐 Anyone with the link'"></span>
<span class="chevron-down">▾</span>
</button>
<div class="share-access-menu" x-show="accessMenuOpen" @click.away="accessMenuOpen = false" x-transition>
<div class="share-access-option" @click="generalAccess='invited'; accessMenuOpen=false">
<span>🔒</span> Only people invited
<!-- ═══════ SHARE TAB ═══════ -->
<div class="sd-body" x-show="shareTab === 'share'" x-transition>
<!-- Invite input -->
<div class="sd-invite-row">
<input type="email" class="sd-input" placeholder="Add people, groups or emails..." x-model="inviteEmail" @keydown.enter="invitePerson">
<button class="sd-btn-primary" @click="invitePerson">Invite</button>
</div>
<!-- Context card (shown when independent permissions) -->
<div class="sd-context-card" x-show="false">
Share settings on this page are unlinked from parent page
</div>
<!-- Participants list -->
<div class="sd-participants" x-show="accessList.length">
<template x-for="a in accessList" :key="a.email">
<div class="sd-participant">
<div class="sd-participant-info">
<div class="sd-avatar">{{ a.email[0].toUpperCase() }}</div>
<div>
<div class="sd-participant-name" x-text="a.email"></div>
<div class="sd-participant-email" x-text="a.email"></div>
</div>
</div>
<button class="sd-perm-btn" @click="a.permOpen = !a.permOpen">
<span x-text="a.permission === 'editor' ? 'Full access' : a.permission === 'commenter' ? 'Can comment' : 'Can view'"></span> ▾
</button>
<div class="sd-perm-menu" x-show="a.permOpen" @click.away="a.permOpen = false">
<div class="sd-perm-option" :class="{ active: a.permission === 'editor' }" @click="a.permission='editor'; a.permOpen=false">
<span>Can edit</span>
<span class="sd-perm-desc">Edit, suggest and comment</span>
</div>
<div class="sd-perm-option" :class="{ active: a.permission === 'commenter' }" @click="a.permission='commenter'; a.permOpen=false">
<span>Can comment</span>
<span class="sd-perm-desc">Suggest and comment only</span>
</div>
<div class="sd-perm-option" :class="{ active: a.permission === 'viewer' }" @click="a.permission='viewer'; a.permOpen=false">
<span>Can view</span>
<span class="sd-perm-desc">Read only</span>
</div>
<div class="sd-perm-sep"></div>
<div class="sd-perm-option danger" @click="removeAccess(a.email); a.permOpen=false">Remove</div>
</div>
</div>
<div class="share-access-option" @click="generalAccess='anyone'; accessMenuOpen=false">
<span>🌐</span> Anyone with the link
</template>
</div>
<div class="sd-sep"></div>
<!-- General Access -->
<div class="sd-section">
<div class="sd-section-title">General access</div>
<div style="position:relative;">
<button class="sd-access-btn" @click="accessMenuOpen = !accessMenuOpen">
<span x-text="generalAccess === 'invited' ? '🔒 Only people invited' : '🌐 Anyone with the link'"></span>
<span class="chevron-down">▾</span>
</button>
<div class="sd-access-menu" x-show="accessMenuOpen" @click.away="accessMenuOpen = false" x-transition>
<div class="sd-perm-option" :class="{ active: generalAccess === 'invited' }" @click="generalAccess='invited'; accessMenuOpen=false">
<span>🔒 Only people invited</span>
<span class="sd-perm-desc">People need to be invited</span>
</div>
<div class="sd-perm-option" :class="{ active: generalAccess === 'anyone' }" @click="generalAccess='anyone'; accessMenuOpen=false">
<span>🌐 Anyone with the link</span>
<span class="sd-perm-desc">Can view the page</span>
</div>
</div>
</div>
</div>
</div>
<div class="share-dd-sep"></div>
<!-- Invite -->
<div class="share-dd-section">
<div class="share-dd-label">Invite</div>
<div class="share-dd-invite-row">
<input type="email" class="share-dd-input" placeholder="Add people, groups or emails..." x-model="inviteEmail" @keydown.enter="invitePerson">
<select class="share-dd-select" x-model="invitePermission">
<option value="editor">Can edit</option>
<option value="commenter">Can comment</option>
<option value="viewer">Can view</option>
</select>
<button class="share-dd-btn" @click="invitePerson">Invite</button>
</div>
</div>
<div class="share-dd-sep"></div>
<!-- Publish -->
<div class="share-dd-section">
<div class="share-dd-row" style="justify-content:space-between;">
<div>
<span style="font-size:14px;">🌐 Publish to web</span>
<p class="share-dd-desc">Make this page visible to anyone on the internet.</p>
</div>
<label class="toggle-switch sm">
<input type="checkbox" x-model="pagePublished" @change="togglePublish">
<span class="toggle-slider"></span>
</label>
</div>
<div x-show="pagePublished" style="margin-top:8px;">
<div class="share-dd-row">
<input type="text" class="share-dd-input" :value="publishedUrl || pageUrl" readonly @click="$event.target.select()" style="flex:1;">
<button class="share-dd-btn" @click="copyPageLink">Copy link</button>
<!-- Public permission when link is open -->
<div x-show="generalAccess === 'anyone'" style="margin-top:6px;display:flex;align-items:center;gap:8px;">
<span style="font-size:13px;color:var(--text-primary);">Anyone with the link</span>
<select class="sd-select-sm" x-model="publicPerm">
<option value="viewer">Can view</option>
<option value="commenter">Can comment</option>
<option value="editor">Can edit</option>
</select>
</div>
</div>
</div>
<div class="share-dd-sep"></div>
<div class="sd-sep"></div>
<!-- Copy link -->
<div class="share-dd-section">
<div class="share-dd-row clickable" @click="copyPageLink">
<span>🔗 Copy page link</span>
<!-- Footer -->
<div class="sd-footer">
<a href="#" class="sd-footer-link" @click.prevent>? Learn about sharing</a>
<button class="sd-footer-action" @click="copyPageLink">🔗 Copy link</button>
</div>
</div>
<!-- ═══════ PUBLISH TAB ═══════ -->
<div class="sd-body" x-show="shareTab === 'publish'" x-transition>
<!-- Before publishing -->
<div x-show="!pagePublished" class="sd-publish-hero">
<div class="sd-publish-preview">
<div class="sd-preview-bar"></div>
<div class="sd-preview-title">🌐 {{ page.title }}</div>
</div>
<h3 class="sd-publish-heading">Publish to web</h3>
<p class="sd-publish-desc">Make this page visible to anyone on the internet. You can share the link with anyone.</p>
<button class="sd-btn-primary sd-btn-full" @click="pagePublished = true">Publish</button>
<p class="sd-publish-note">Your page will be visible to anyone with the link.</p>
</div>
<!-- After publishing -->
<div x-show="pagePublished">
<div class="sd-url-bar">
<span style="font-size:13px;color:var(--text-dim);">{{ workspace_key or 'flowdeck' }}</span>
<input type="text" class="sd-url-input" :value="publishedUrl || pageUrl" readonly @click="$event.target.select()">
<button class="sd-btn-primary" style="height:32px;padding:0 12px;font-size:12px;" @click="copyPageLink">Copy link</button>
</div>
<div class="sd-sep"></div>
<!-- Settings list -->
<div class="sd-settings">
<div class="sd-setting-row">
<span>🔗 Link expires</span>
<span style="color:var(--text-dim);">Never ▾</span>
</div>
<div class="sd-setting-row">
<span>🔍 Search engine indexing</span>
<span style="color:var(--text-dim);">Off ▾</span>
</div>
<div class="sd-setting-row">
<span>📄 Allow duplicate as template</span>
<label class="toggle-switch sm">
<input type="checkbox" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="sd-setting-row">
<span>✏️ Allow editing</span>
<label class="toggle-switch sm">
<input type="checkbox" x-model="pubAllowEdit">
<span class="toggle-slider"></span>
</label>
</div>
<div class="sd-setting-row">
<span>💬 Allow comments</span>
<label class="toggle-switch sm">
<input type="checkbox" x-model="pubAllowComments" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="sd-sep"></div>
<button class="sd-danger-btn" @click="pagePublished = false">Unpublish</button>
</div>
</div>
</div>
<!-- More menu dropdown -->
@@ -127,28 +222,19 @@
<button @click="fmtLink()">🔗</button>
</div>
<!-- ═══════════ Copy Link Toast ═══════════ -->
<div class="sd-toast" x-show="toastVisible" x-transition x-text="toastMsg"></div>
<!-- ═══════════ Get Started Toolbar (bottom) ═══════════ -->
<div class="get-started-toolbar" x-show="blocks.length === 1 && blocks[0].type === 'paragraph' && !blocks[0].content.trim()">
<span class="gs-label">Get started with</span>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Ask AI to write...';render()">
<span class="gs-icon">✨</span> Ask AI
</button>
<button class="gs-pill" @click="replaceBlock(0, 'heading_1');blocks[0].content='AI Meeting Note';render()">
<span class="gs-icon">📝</span> AI meeting note
</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Database';render()">
<span class="gs-icon">🗄️</span> Database
</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Form';render()">
<span class="gs-icon">📋</span> Form
</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Templates';render()">
<span class="gs-icon">📑</span> Templates
</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Ask AI to write...';render()"><span class="gs-icon">✨</span> Ask AI</button>
<button class="gs-pill" @click="replaceBlock(0, 'heading_1');blocks[0].content='AI Meeting Note';render()"><span class="gs-icon">📝</span> AI meeting note</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Database';render()"><span class="gs-icon">🗄️</span> Database</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Form';render()"><span class="gs-icon">📋</span> Form</button>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Templates';render()"><span class="gs-icon">📑</span> Templates</button>
<div style="position:relative;">
<button class="gs-pill" @click="gsMoreOpen = !gsMoreOpen">
<span>⋯</span>
</button>
<button class="gs-pill" @click="gsMoreOpen = !gsMoreOpen"><span>⋯</span></button>
<div class="gs-more-dropdown" x-show="gsMoreOpen" @click.away="gsMoreOpen = false" x-transition>
<div class="gs-more-item" @click="gsMoreOpen=false">📊 Table</div>
<div class="gs-more-item" @click="gsMoreOpen=false">📋 Board</div>
@@ -246,10 +332,12 @@ function editor(pid,title,fmt,raw,favorited){
pid,pageTitle:title,blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
fmt:{open:false,top:0,left:0,idx:-1},
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,
favorited:!!favorited,pagePublished:false,
generalAccess:'invited',
shareTab:'share',favorited:!!favorited,pagePublished:false,
generalAccess:'invited',publicPerm:'viewer',
pubAllowEdit:false,pubAllowComments:true,
pageUrl:window.location.href,publishedUrl:'',
inviteEmail:'',invitePermission:'editor',accessList:[],
toastVisible:false,toastMsg:'',
init(){
_bid=Math.floor(Math.random()*10000);
@@ -263,26 +351,20 @@ function editor(pid,title,fmt,raw,favorited){
if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{if(evt.oldIndex===evt.newIndex)return;const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);this.dirty=true;this.autoSave();this.render();}});
},100);
},
mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;return b;},
getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;},
getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);},
getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};},
focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}},
onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}},
sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='divider')continue;const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el)b.content=el.textContent||'';}},
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
ct.querySelectorAll('.block-wrapper').forEach(w=>{w.onmouseenter=()=>{w.querySelector('.block-handle')?.classList.add('visible');w.querySelector('.block-actions')?.classList.add('visible');};w.onmouseleave=()=>{w.querySelector('.block-handle')?.classList.remove('visible');w.querySelector('.block-actions')?.classList.remove('visible');};});
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
},
replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();b.type=type;b.content='';if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
addAt(idx,type,content){this.sync();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;},
addAfter(idx,type){return this.addAt(idx+1,type);},
removeBlock(idx){this.sync();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
toggleFavorite(){
@@ -293,12 +375,13 @@ function editor(pid,title,fmt,raw,favorited){
},
copyPageLink(){
navigator.clipboard.writeText(this.pageUrl).then(()=>{
const btn=document.querySelector('.page-action-btn[title="Copy link"]');
if(btn){btn.textContent='✓ Copied';setTimeout(()=>{btn.textContent='🔗';},1500);}
this.toastMsg='Link copied to clipboard';
this.toastVisible=true;
setTimeout(()=>{this.toastVisible=false;},2500);
}).catch(()=>{});
},
togglePublish(){this.pagePublished=!this.pagePublished;},
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission});this.inviteEmail='';},
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
exportPage(){this.moreOpen=false;},
duplicatePage(){this.moreOpen=false;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages?title=${encodeURIComponent(this.pageTitle+' copy')}&section=Private&project=${encodeURIComponent('{{ workspace_key }}')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{alert('Duplicate failed');});},
@@ -339,7 +422,6 @@ function editor(pid,title,fmt,raw,favorited){
if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');}
this.dirty=true;this.autoSave();
},
checkMd(idx){
if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||'';
const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'};
@@ -348,7 +430,6 @@ function editor(pid,title,fmt,raw,favorited){
if(text==='---'){block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
if(text==='```'){block.type='code';block.content='';block.language='Plain Text';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
},
applySlash(id){
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
const block=this.blocks[idx];this.sync();block.content='';
@@ -357,15 +438,12 @@ function editor(pid,title,fmt,raw,favorited){
this.render();this.dirty=true;this.autoSave();
setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);
},
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].checked=v;this.dirty=true;this.autoSave();}},
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].expanded=!this.blocks[idx].expanded;this.dirty=true;this.autoSave();}},
showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;return;}const r=s.getRangeAt(0).getBoundingClientRect();this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=Math.max(r.top-44,0);this.fmt.left=Math.min(r.left+r.width/2-120,window.innerWidth-260);this._sel={range:s.getRangeAt(0).cloneRange(),idx};},
fmtApply(f){this.fmt.open=false;if(!this._sel)return;const s=window.getSelection();s.removeAllRanges();s.addRange(this._sel.range);document.execCommand(f==='strikeThrough'?'strikeThrough':f);this._sel=null;this.dirty=true;this.autoSave();},
fmtLink(){this.fmt.open=false;const u=prompt('URL:');if(u){document.execCommand('createLink',false,u);this.dirty=true;this.autoSave();}},
pastePlain(e){e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();},
autoSave(){clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
save(){
if(this.saving)return;
@@ -378,12 +456,10 @@ function editor(pid,title,fmt,raw,favorited){
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
}).catch(()=>{this.saving=false;});
},
md2b(md){const bl=[];let cb=null;for(const line of md.split('\n')){if(cb){if(line.trim()==='```'){bl.push(cb);cb=null;}else cb.content+=(cb.content?'\n':'')+line;continue;}const t=line.trim();if(!t)continue;if(t.startsWith('```')){cb=this.mkB('code','',{language:t.substring(3).trim()||'Plain Text'});continue;}if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));else bl.push(this.mkB('paragraph',t));}if(cb)bl.push(cb);return bl;},
};
return self;
}
document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}},80);});
</script>
{% endblock %}