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 %}
+205 -60
View File
@@ -2218,78 +2218,222 @@ input, select, textarea { font-family: inherit; color: inherit; }
.star-btn { font-size: 16px; padding: 4px 8px; }
.star-btn .starred { color: #f5c542; }
/* ── Share dropdown ── */
.share-dropdown {
position: absolute; top: calc(100% + 4px); right: 0;
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
min-width: 360px; z-index: 200; padding: 8px 0;
/* ═══════════ Share / Publish Dialog ═══════════ */
.share-dialog {
position: absolute; top: calc(100% + 8px); right: 0;
width: 440px; max-height: 70vh; overflow-y: auto;
background: #1F1F1F; border: 1px solid rgba(255,255,255,.08);
border-radius: 18px; box-shadow: 0 12px 48px rgba(0,0,0,0.6);
z-index: 200;
}
.share-dd-section { padding: 8px 16px; }
.share-dd-row {
display: flex; align-items: center; gap: 8px;
font-size: 14px; color: var(--text-primary);
/* Header with tabs */
.sd-header {
display: flex; align-items: center; height: 56px;
padding: 0 8px; border-bottom: 1px solid rgba(255,255,255,.06);
}
.share-dd-row.clickable { cursor: pointer; }
.share-dd-row.clickable:hover { opacity: 0.8; }
.share-dd-hint { font-size: 12px; color: var(--text-dim); }
.share-dd-desc { font-size: 12px; color: var(--text-dim); margin: 4px 0 0; line-height: 1.4; }
.share-dd-sep { height: 1px; background: var(--border); margin: 4px 0; }
.share-dd-label { font-size: 12px; color: var(--text-dim); margin-bottom: 6px; }
.share-dd-invite-row { display: flex; gap: 6px; }
.share-dd-input {
flex: 1; padding: 7px 10px; background: var(--bg-primary);
border: 1px solid var(--border); border-radius: 6px;
color: var(--text-primary); font-size: 13px; outline: none;
.sd-tab {
height: 56px; padding: 0 14px; display: flex; align-items: center; gap: 6px;
font-size: 14px; font-weight: 500; cursor: pointer;
background: none; border: none;
color: rgba(255,255,255,.5); position: relative;
transition: color 150ms ease;
}
.share-dd-input::placeholder { color: var(--text-dim); }
.share-dd-select {
padding: 7px 10px; background: var(--bg-primary);
border: 1px solid var(--border); border-radius: 6px;
color: var(--text-primary); font-size: 13px; outline: none;
.sd-tab.active { color: #fff; }
.sd-tab.active::after {
content: ''; position: absolute; bottom: 0; left: 14px; right: 14px;
height: 2px; background: #fff; transition: 200ms ease;
}
.share-dd-btn {
padding: 7px 14px; background: var(--blue); color: #fff;
border: none; border-radius: 6px; cursor: pointer; font-size: 13px;
.sd-tab:hover { color: rgba(255,255,255,.8); }
.sd-badge {
width: 6px; height: 6px; border-radius: 999px; background: #2383E2;
display: inline-block;
}
.share-dd-btn:hover { opacity: 0.9; }
.sd-close {
margin-left: auto; background: none; border: none;
color: rgba(255,255,255,.5); font-size: 16px; cursor: pointer;
padding: 8px; border-radius: 6px;
}
.sd-close:hover { background: rgba(255,255,255,.06); color: #fff; }
/* ── General Access dropdown ── */
.share-access-btn {
width: 100%; display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; background: var(--bg-primary);
border: 1px solid var(--border); border-radius: 6px;
color: var(--text-primary); font-size: 13px; cursor: pointer;
margin-top: 4px;
/* Body */
.sd-body { padding: 16px 20px; }
/* Invite row */
.sd-invite-row { display: flex; gap: 8px; margin-bottom: 12px; }
.sd-input {
flex: 1; height: 40px; padding: 0 14px;
background: #262626; border: 1px solid rgba(255,255,255,.06);
border-radius: 10px; color: #fff; font-size: 13px; outline: none;
transition: border-color 150ms, box-shadow 150ms;
}
.share-access-btn:hover { background: var(--bg-hover); }
.share-access-btn .chevron-down { font-size: 10px; opacity: 0.5; }
.share-access-menu {
position: absolute; top: 100%; left: 0; right: 0; margin-top: 2px;
background: var(--bg-primary); border: 1px solid var(--border);
border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
.sd-input::placeholder { color: rgba(255,255,255,.45); }
.sd-input:focus { border-color: #2383E2; box-shadow: 0 0 0 1px #2383E2; }
.sd-btn-primary {
height: 40px; padding: 0 16px; background: #2383E2; color: #fff;
border: none; border-radius: 10px; font-size: 13px; font-weight: 500;
cursor: pointer; transition: background 150ms ease; white-space: nowrap;
}
.sd-btn-primary:hover { background: #2C8CEB; }
.sd-btn-full { width: 100%; height: 46px; font-size: 14px; }
/* Context card */
.sd-context-card {
background: #2B2B2B; border-radius: 12px;
padding: 12px; font-size: 13px; color: rgba(255,255,255,.55);
margin-bottom: 12px;
}
/* Section */
.sd-section { margin: 8px 0; }
.sd-section-title {
font-size: 12px; color: rgba(255,255,255,.55); margin-bottom: 6px;
}
.sd-sep { height: 1px; background: rgba(255,255,255,.06); margin: 12px 0; }
/* Access button */
.sd-access-btn {
width: 100%; display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px; background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.06); border-radius: 10px;
color: #fff; font-size: 13px; cursor: pointer;
}
.sd-access-btn:hover { background: rgba(255,255,255,.08); }
.sd-access-menu {
position: absolute; top: calc(100% + 2px); left: 0; right: 0;
background: #2A2A2A; border: 1px solid rgba(255,255,255,.08);
border-radius: 14px; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 210; padding: 4px 0;
}
.share-access-option {
padding: 8px 14px; font-size: 13px; color: var(--text-primary);
cursor: pointer; display: flex; align-items: center; gap: 8px;
/* Permissions */
.sd-select-sm {
padding: 4px 8px; background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.06); border-radius: 6px;
color: #fff; font-size: 12px; outline: none;
}
/* Participants */
.sd-participants { margin-bottom: 8px; }
.sd-participant {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 0;
}
.sd-participant-info { display: flex; align-items: center; gap: 10px; }
.sd-avatar {
width: 32px; height: 32px; border-radius: 50%;
background: #2383E2; color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 600;
}
.sd-participant-name { font-size: 14px; font-weight: 500; color: #fff; }
.sd-participant-email { font-size: 13px; color: rgba(255,255,255,.6); }
.sd-perm-btn {
padding: 4px 10px; background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.06); border-radius: 6px;
color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer;
}
.sd-perm-btn:hover { background: rgba(255,255,255,.08); }
.sd-perm-menu {
position: absolute; top: 100%; right: 0; width: 280px;
background: #2A2A2A; border: 1px solid rgba(255,255,255,.08);
border-radius: 14px; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 211; padding: 4px 0;
}
.sd-perm-option {
padding: 10px 16px; cursor: pointer;
display: flex; flex-direction: column;
}
.sd-perm-option span:first-child { font-size: 14px; color: #fff; }
.sd-perm-option:hover { background: #343434; }
.sd-perm-option.active { background: rgba(35,131,226,.15); }
.sd-perm-option.danger:hover { background: rgba(224,62,62,.15); }
.sd-perm-option.danger span:first-child { color: #e03e3e; }
.sd-perm-desc { font-size: 12px !important; color: rgba(255,255,255,.45) !important; margin-top: 2px; }
.sd-perm-sep { height: 1px; background: rgba(255,255,255,.06); margin: 4px 0; }
/* Footer */
.sd-footer {
display: flex; align-items: center; justify-content: space-between;
}
.sd-footer-link { font-size: 13px; color: rgba(255,255,255,.55); text-decoration: none; }
.sd-footer-link:hover { color: #fff; }
.sd-footer-action {
padding: 6px 14px; background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.06); border-radius: 10px;
color: #fff; font-size: 13px; cursor: pointer;
display: flex; align-items: center; gap: 6px;
}
.sd-footer-action:hover { background: rgba(255,255,255,.1); }
/* Publish tab */
.sd-publish-hero {
display: flex; flex-direction: column; align-items: center;
text-align: center; padding: 16px 0;
}
.sd-publish-preview {
width: 100%; height: 200px; background: #262626;
border-radius: 12px; margin-bottom: 20px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.sd-preview-bar {
width: 100%; height: 32px; background: rgba(255,255,255,.04);
border-radius: 12px 12px 0 0; display: flex; align-items: center; padding: 0 12px;
gap: 6px;
}
.sd-preview-title { font-size: 16px; color: rgba(255,255,255,.6); margin-top: 20px; }
.sd-publish-heading { font-size: 18px; font-weight: 600; color: #fff; margin: 0 0 8px; }
.sd-publish-desc { font-size: 13px; color: rgba(255,255,255,.55); margin: 0 0 16px; line-height: 1.5; }
.sd-publish-note { font-size: 12px; color: rgba(255,255,255,.35); margin-top: 12px; }
/* URL bar (after publish) */
.sd-url-bar {
display: flex; align-items: center; gap: 8px;
padding: 8px; background: rgba(255,255,255,.04);
border-radius: 10px;
}
.sd-url-input {
flex: 1; height: 32px; padding: 0 10px;
background: transparent; border: none;
color: rgba(255,255,255,.6); font-size: 13px; outline: none;
}
/* Settings rows */
.sd-settings { margin: 4px 0; }
.sd-setting-row {
display: flex; align-items: center; justify-content: space-between;
height: 40px; padding: 0 4px;
font-size: 14px; color: #fff; cursor: pointer;
border-radius: 6px;
}
.sd-setting-row:hover { background: #2F2F2F; }
.sd-danger-btn {
width: 100%; padding: 10px; background: none;
border: 1px solid rgba(255,255,255,.08); border-radius: 10px;
color: rgba(255,255,255,.55); font-size: 13px; cursor: pointer;
}
.sd-danger-btn:hover { background: rgba(224,62,62,.1); color: #e03e3e; }
/* Toast */
.sd-toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
background: #111; color: #fff; padding: 10px 18px;
border-radius: 10px; font-size: 13px; z-index: 300;
}
.share-access-option:hover { background: var(--bg-hover); }
/* ── More menu dropdown ── */
.more-menu {
position: absolute; top: calc(100% + 4px); right: 0;
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
background: #2A2A2A; border: 1px solid rgba(255,255,255,.08);
border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
min-width: 200px; z-index: 100; padding: 4px 0;
}
.more-menu-item {
padding: 8px 14px; font-size: 13px; color: var(--text-primary);
padding: 8px 14px; font-size: 13px; color: #fff;
cursor: pointer; display: flex; align-items: center; gap: 8px;
}
.more-menu-item:hover { background: var(--bg-hover); }
.more-menu-item:hover { background: rgba(255,255,255,.06); }
.more-menu-item.danger { color: #e03e3e; }
.more-menu-sep { height: 1px; background: var(--border); margin: 4px 0; }
.more-menu-sep { height: 1px; background: rgba(255,255,255,.06); margin: 4px 0; }
/* ── Toggle switch ── */
.toggle-switch { position: relative; display: inline-block; width: 36px; height: 20px; }
@@ -2297,17 +2441,18 @@ input, select, textarea { font-family: inherit; color: inherit; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute; inset: 0; cursor: pointer;
background: var(--bg-hover); border-radius: 20px;
transition: 0.2s; border: 1px solid var(--border);
background: #555; border-radius: 20px;
transition: background 150ms ease;
}
.toggle-slider:before {
content: ''; position: absolute; height: 14px; width: 14px;
left: 2px; bottom: 2px; background: var(--text-dim);
border-radius: 50%; transition: 0.2s;
content: ''; position: absolute; height: 16px; width: 16px;
left: 2px; bottom: 2px; background: #fff;
border-radius: 50%; transition: transform 150ms ease;
}
.toggle-switch input:checked + .toggle-slider { background: var(--blue); }
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); background: #fff; }
.toggle-switch.sm .toggle-slider:before { height: 12px; width: 12px; bottom: 2px; }
.toggle-switch input:checked + .toggle-slider { background: #2383E2; }
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); }
.toggle-switch.sm { width: 32px; height: 18px; }
.toggle-switch.sm .toggle-slider:before { height: 14px; width: 14px; }
.toggle-switch.sm input:checked + .toggle-slider:before { transform: translateX(14px); }
/* ═══════════ Empty Page Toolbar ═══════════ */