Files
flowdeck/app/templates/_page_editor_content.html
T
bruno 6914780f24
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
feat: A20 phase 3 LOT 3b — gitea + agent + éditeur verts en CSP (v7.41.0)
Changed :
- gitea_workspace : x-data="giteaWorkspace" → appel giteaWorkspace(),
  new Date(…) → fmtGwDate(pp), x-html icône arbre → bindGwIcon (x-init +
  Alpine.effect).
- agent_panel : x-html markdown → bindMarkdown($el, m) (effet reactif).
- page_editor : les 12 sites window.E du topbar right_actions →
  délégués appState (edCall('…') x6, edTimeAgo, edCommentCount, edShared,
  bindStar — les 2 branches du ternaire favorited étaient identiques) ;
  + 3 sites dans _page_editor_content (edCall commentOnSelection,
  openBacklink, fmtImportSize, bindIconHtml). Garde Jinja : quotes \' dans
  le set délimité par ' (quote nue = 500).
- Gate éditeur (csp_preview) : création collection → /pages/{id},
  délégués + editorState liés, filet 0-erreur.

Fixed :
- x-html iconHtml() du contenu éditeur = directive INTERDITE sous build
  CSP (attrapé par le filet) → x-init + Alpine.effect.

⚠️ BUG pre-existant identifie (pas introduit ici) : les right_actions du
topbar sont servis ÉCHAPPÉS sur TOUTES les pages (entities "/< —
boutons Share/Star/Settings en texte brut). _header:141 a bien |safe,
ENV standard, rendu local = PARSED ; cause serveur à cerner → suivi
ROADMAP dédié. Le gate éditeur n'asserte donc pas la présence boutons.

suite **1093/1093** · ruff OK · E2E **7/7** (5 csp_preview + 2 smoke)
· docs a jour
2026-10-02 15:15:26 -04:00

875 lines
51 KiB
HTML

<div
class="page-editor-wrapper"
x-data="editorState()"
x-init="loadCoverIcon()"
>
<!-- Share/More dialogs (in content area, triggered by header buttons) -->
<!-- ═══════════ Share / Publish Dialog ═══════════ -->
<div
class="share-dialog"
x-show="shareOpen"
@click.outside="!_justOpened && (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>
<!-- ═══════ SHARE TAB ═══════ -->
<div class="sd-body" x-show="shareTab === 'share'" x-transition>
<!-- Invite input -->
<div class="sd-invite-wrap">
<div class="sd-invite-row">
<input
type="text"
class="sd-input"
x-ref="inviteInput"
placeholder="Add people, emails or type a name..."
x-model="inviteEmail"
@input="inviteInput()"
@focus="inviteInput()"
@keydown.enter.prevent="inviteEnter()"
@keydown.down.prevent="inviteMove(1)"
@keydown.up.prevent="inviteMove(-1)"
@keydown.escape="inviteSuggestOpen = false; inviteUsers = []"
autocomplete="off"
/>
<button class="sd-btn-primary" @click="shareInvite()">
Invite
</button>
</div>
<!-- Existing users autocomplete -->
<div
class="sd-user-suggest"
x-ref="inviteSuggest"
x-show="inviteSuggestOpen"
@click.outside="inviteSuggestOpen = false"
x-transition.opacity
>
<template x-for="(u, i) in inviteUsers" :key="u.id">
<div
class="sd-user-item"
:class="{ active: inviteSel === i }"
@mouseenter="inviteSel = i"
@click="pickInviteUser(u)"
>
<div
class="sd-user-avatar"
:style="{ background: u.avatar_color || '#3A3A3A' }"
>
<img
x-show="u.avatar_url"
:src="u.avatar_url"
:alt="u.login"
style="width:100%;height:100%;border-radius:50%;object-fit:cover;"
/>
<span
x-show="!u.avatar_url"
x-text="((u.full_name || u.login || '?').charAt(0).toUpperCase())"
></span>
</div>
<div class="sd-user-meta">
<div class="sd-user-login" x-text="u.login"></div>
<div class="sd-user-name" x-text="u.full_name"></div>
</div>
<div class="sd-user-add">Invite</div>
</div>
</template>
<template x-if="inviteGroups && inviteGroups.length">
<div>
<div class="sd-user-empty" style="font-weight:600;">Groups</div>
<template x-for="g in inviteGroups" :key="'g'+g.id">
<div
class="sd-user-item"
@click="pickInviteGroup(g)"
>
<div class="sd-user-avatar" style="background:#5b5bd6;color:#fff;">
<span x-text="(g.name || '?').charAt(0).toUpperCase()"></span>
</div>
<div class="sd-user-meta">
<div class="sd-user-login" x-text="g.name"></div>
<div class="sd-user-name" x-text="'Group · ' + (g.member_count || 0) + ' members'"></div>
</div>
<div class="sd-user-add">Invite</div>
</div>
</template>
</div>
</template>
<div class="sd-user-empty" x-show="!inviteUsers.length && !(inviteGroups && inviteGroups.length)">No matching users</div>
</div>
</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.id">
{% raw %}
<div class="sd-participant">
<div class="sd-participant-info">
<div class="sd-avatar" x-show="a.user_avatar_url" style="overflow:hidden">
<img :src="a.user_avatar_url" style="width:100%;height:100%;border-radius:50%;object-fit:cover;" />
</div>
<!-- Group avatar -->
<div class="sd-avatar" x-show="(a.kind === 'group' || a.shared_with_group_id) && !a.user_avatar_url"
style="background:#5b5bd6;color:#fff;"
x-text="(a.group_name || '?')[0].toUpperCase()"
></div>
<div class="sd-avatar" x-show="!(a.kind === 'group' || a.shared_with_group_id) && !a.user_avatar_url"
x-text="(a.user_full_name || a.user_login || a.shared_with_email || '?')[0].toUpperCase()"
></div>
<div>
<div
class="sd-participant-name"
x-text="(a.kind === 'group' || a.shared_with_group_id) ? (a.group_name || 'Group') : (a.user_full_name || a.user_login || a.shared_with_email)"
></div>
<div
class="sd-participant-email"
x-text="(a.kind === 'group' || a.shared_with_group_id) ? 'Group' : (a.shared_with_email || a.user_login || '')"
></div>
</div>
</div>
<button
class="sd-perm-btn"
@click="a.permOpen = !a.permOpen"
>
<span
x-text="a.permission === 'edit' ? 'Full access' : a.permission === 'comment' ? 'Can comment' : 'Can view'"
></span>
▾
</button>
<div
class="sd-perm-menu"
x-show="a.permOpen"
@click.outside="a.permOpen = false"
>
<div
class="sd-perm-option"
:class="{ active: a.permission === 'edit' }"
@click="updateShare(a.id, 'edit'); 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 === 'comment' }"
@click="updateShare(a.id, 'comment'); 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 === 'view' }"
@click="updateShare(a.id, 'view'); 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="removeShare(a.id); a.permOpen=false"
>
Remove
</div>
</div>
</div>
{% endraw %}
</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.outside="accessMenuOpen = false"
x-transition
>
<div
class="sd-perm-option"
:class="{ active: generalAccess === 'invited' }"
@click="generalAccess='invited'; accessMenuOpen=false"
>
<span>{{ fd_icon('lock',14) }} 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>{{ fd_icon('globe',14) }} Anyone with the link</span>
<span class="sd-perm-desc"
>Can view the page</span
>
</div>
</div>
</div>
<!-- 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 class="sd-sep"></div>
<!-- v5.5.0: Cover & Icon -->
<div class="sd-section">
<div class="sd-section-title">{{ fd_icon('image',14) }} Cover & Icon</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<button class="sd-btn-secondary" @click="toggleCover()">
<span x-show="!coverOpen">{{ fd_icon('image',14) }} Cover</span>
<span x-show="coverOpen">{{ fd_icon('x',14) }} Hide</span>
</button>
<button class="sd-btn-secondary" @click="toggleIcon()">
<span x-show="!iconOpen">{{ fd_icon('sparkles',14) }} Icon</span>
<span x-show="iconOpen">{{ fd_icon('x',14) }} Hide</span>
</button>
</div>
<div x-show="coverOpen" style="margin-top:12px;" x-transition>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px;">
<input type="text" class="sd-input" x-model="coverUrl" placeholder="Cover image URL" style="flex:1;min-width:200px;">
<button class="sd-btn-primary" @click="setCoverUrl()" :disabled="coverLoading" x-show="!coverLoading">Set</button>
<button class="sd-btn-primary" :disabled="coverLoading" x-show="coverLoading">Saving…</button>
<label class="sd-btn-secondary" style="display:flex;align-items:center;gap:6px;">
{{ fd_icon('upload',14) }} Upload
<input type="file" accept="image/*" @change="uploadCover($event)" style="display:none;">
</label>
<button class="sd-btn-danger" @click="removeCover()" x-show="coverUrl">Remove</button>
</div>
<div x-show="coverUrl" style="max-width:320px;border-radius:8px;overflow:hidden;border:1px solid var(--border);">
<img :src="coverUrl" style="width:100%;height:auto;display:block;">
</div>
</div>
<div x-show="iconOpen" style="margin-top:12px;" x-transition>
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
<template x-for="(ic, ici) in ['📄','📝','📋','📊','🗂️','📁','📂','🗒️','🗓️','📌','🔖','⭐','🚀','💡','🎯','🔑','📚','🧪','🌍','💰','📝','🧩','🎨','🔧','⚙️','🗃️','📦','🏷️','📍','🏠','👤']" :key="ici">
<button type="button" class="sd-icon-btn" @click="setIcon(ic)" :class="{ active: iconValue === ic }" style="width:36px;height:36px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;" x-text="ic"></button>
</template>
<input type="text" class="sd-input" x-model="iconValue" placeholder="Custom emoji" style="max-width:80px;" @keydown.enter.prevent="setIcon(iconValue);iconOpen=false;">
</div>
</div>
</div>
<div class="sd-sep"></div>
<!-- Footer -->
<div class="sd-footer">
<a href="#" class="sd-footer-link" @click.prevent
>? Learn about sharing</a
>
<button class="sd-footer-action" @click="copyPageLink">
{{ fd_icon("link",14) }} 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">
{{ fd_icon('globe',14) }} {{ 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="publishPage()"
>
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>{{ fd_icon("link",14) }} Link expires</span>
<span style="color: var(--text-dim)"
>Never ▾</span
>
</div>
<div class="sd-setting-row">
<span>{{ fd_icon('search',14) }} Search engine indexing</span>
<span style="color: var(--text-dim)"
>Off ▾</span
>
</div>
<div class="sd-setting-row">
<span>{{ fd_icon("file",14) }} 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>{{ fd_icon("edit",14) }} 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>{{ fd_icon("message-square",14) }} 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="unpublishPage()"
>
Unpublish
</button>
</div>
</div>
</div>
<!-- ═══════════ Activity Popover ═══════════ -->
<div class="activity-popover"
x-show="activityOpen"
@click.outside="!_justOpened && (activityOpen = false)"
x-transition.opacity.scale.origin.top.right
style="position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-modal);z-index:1000;width:280px;padding:12px;">
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">Edited <span x-text="timeAgo"></span></div>
<template x-if="pageCreated">
<div style="font-size:12px;color:var(--text-tertiary);">Created <span x-text="formatDate(pageCreated)"></span></div>
</template>
</div>
<!-- More menu dropdown -->
<div
class="more-menu"
x-show="moreOpen"
@click.outside="!_justOpened && (moreOpen = false)"
x-transition
>
<div class="more-menu-item" @click="toggleBacklinks">
{{ fd_icon("link-2",14) }} Linked from
</div>
<div class="more-menu-item" @click="toggleVersions">
{{ fd_icon("history",14) }} Version history
</div>
<div class="more-menu-item" @click="toggleImport">
{{ fd_icon("download",14) }} Import
</div>
<div class="more-menu-item" @click="exportOpen = !exportOpen">↗ Export <span style="margin-left:auto;font-size:10px;opacity:.6">▾</span></div>
<template x-if="exportOpen">
<div class="more-menu-sub">
<div class="more-menu-item" @click="exportPage('markdown')"> Markdown (.md)</div>
<div class="more-menu-item" @click="exportPage('html')"> HTML (.html)</div>
<div class="more-menu-item" @click="exportPage('pdf')"> PDF (.pdf)</div>
<div class="more-menu-item" @click="exportPage('site')"> Site statique (.zip)</div>
</div>
</template>
<template x-if="contentFormat === 'file' && fileUrl">
<div>
<div class="more-menu-sep"></div>
<div class="more-menu-item" @click="downloadFile()">
{{ fd_icon("download",14) }} Download
</div>
<div class="more-menu-item" @click="copyFileContent()">
{{ fd_icon("copy",14) }} Copy content
</div>
</div>
</template>
<div class="more-menu-item" @click="duplicatePage">
{{ fd_icon("copy",14) }} Duplicate
</div>
<div class="more-menu-item" @click="movePage">↗ Move to</div>
<div class="more-menu-sep"></div>
<div class="more-menu-item" @click="moreOpen=false; toggleLock()">
<span x-text="isLocked ? '🔓 Unlock page' : '🔒 Lock page'"></span>
</div>
<div class="more-menu-item" @click="moreOpen=false; setPageOption('full_width', !fullWidth)">
↔ Full width <span x-show="fullWidth" style="margin-left:auto;font-size:11px">✓</span>
</div>
<div class="more-menu-item" @click="moreOpen=false; setPageOption('font_small', !fontSmall)">
Aa Small text <span x-show="fontSmall" style="margin-left:auto;font-size:11px">✓</span>
</div>
<div class="more-menu-item" @click="moreOpen=false; saveAsTemplate()">
📑 Save as template
</div>
<div class="more-menu-sep"></div>
<div class="more-menu-item danger" @click="deletePage">
{{ fd_icon("trash",14) }} Move to Trash
</div>
</div>
<!-- ═══════════ Move to Dialog ═══════════ -->
<div class="move-dialog"
x-show="moveOpen"
@click.outside="moveOpen = false"
x-transition.opacity.scale
style="position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-modal);z-index:1000;width:320px;padding:16px;">
<h3 style="font-size:14px;margin-bottom:12px;">Move to workspace</h3>
<div x-show="moveLoading" style="color:var(--text-tertiary);text-align:center;padding:20px;">Loading...</div>
<div x-show="!moveLoading" style="max-height:240px;overflow-y:auto;">
<template x-for="ws in moveWorkspaces" :key="ws.id">
<div class="move-ws-item"
@click="doMove(ws.id)"
style="display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;cursor:pointer;font-size:13px;">
<span>{{ fd_icon("folder",14) }}</span>
<span x-text="ws.name" style="flex:1;"></span>
<span style="font-size:11px;color:var(--text-tertiary);" x-text="ws.page_count + ' pages'"></span>
</div>
</template>
<div x-show="moveWorkspaces.length === 0" style="color:var(--text-tertiary);text-align:center;padding:12px;">
No workspaces available
</div>
</div>
</div>
<!-- ═══════════ Page Content ═══════════ -->
<div class="page-cover-area" x-show="coverUrl" style="position:relative;height:240px;overflow:hidden;">
<img :src="coverUrl" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;">
<div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 50%,rgba(0,0,0,.6) 100%);z-index:1;"></div>
</div>
<div class="page-title-block">
<span class="page-icon-emoji" @click="openIconPicker()" title="Change icon" x-init="bindIconHtml($el)"></span>
<div
class="page-title-input"
contenteditable="true"
id="_titleEl"
data-placeholder="New Page"
@input="dirty=true;save()"
@keydown.enter.prevent="focusBlock()"
@paste.prevent="pastePlain($event)"
spellcheck="false"
>
{{ page.title }}
</div>
</div>
<div
class="blocks-container"
id="_blocksCt"
@click="onCtClick($event)"
></div>
<div
id="_slashMenu"
class="slash-menu"
style="display: none; position: fixed; z-index: 999"
></div>
<!-- ═══════════ @mention autocomplete (v4.9.0) ═══════════ -->
<div id="_mentionMenu" class="mention-menu"
style="display:none;position:fixed;z-index:1200;min-width:240px;max-height:280px;overflow-y:auto;
background:var(--bg-modal,#1f1f1f);border:1px solid var(--border,#333);border-radius:10px;
box-shadow:var(--shadow-modal);padding:6px;"></div>
<!-- ═══════════ Inline comment trigger on selection (v4.9.0) ═══════════ -->
<button type="button" id="_commentSelBtn"
style="display:none;position:fixed;z-index:1100;padding:7px 12px;font-size:13px;font-weight:600;
color:#fff;background:var(--accent,#2383E2);border:none;border-radius:8px;cursor:pointer;
box-shadow:0 4px 16px rgba(0,0,0,.35);" title="Comment on selection"
@click="edCall('commentOnSelection')">
💬 Comment
</button>
<!-- ═══════════ Comments drawer (v4.9.0) ═══════════ -->
<div class="comments-drawer" x-show="commentsOpen" x-cloak x-transition
style="position:fixed;top:var(--topbar-height,44px);right:0;bottom:0;width:320px;max-width:92vw;
background:var(--bg-primary,#1c1c1c);border-left:1px solid var(--border,#333);
box-shadow:-8px 0 30px rgba(0,0,0,.25);z-index:900;display:flex;flex-direction:column;">
<div class="comments-drawer-header"
style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;
border-bottom:1px solid var(--border,#333);font-weight:600;font-size:14px;">
<span>Comments</span>
<button type="button" class="topbar-btn" @click="toggleComments()" title="Close">✕</button>
</div>
<div class="comments-drawer-list" style="flex:1;overflow-y:auto;padding:12px 16px;">
<div x-show="comments.length === 0" style="text-align:center;color:var(--text-dim,#999);padding:32px 0;">
No comments yet. Select some text and click 💬 Comment.
</div>
<template x-for="c in comments" :key="c.id">
<div class="comment-thread" style="margin-bottom:18px;"
:style="c.anchor_block_id ? 'border-left:3px solid var(--accent);padding-left:10px;' : ''">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
<div style="width:22px;height:22px;border-radius:50%;background:#3A3A3A;color:#fff;
display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;"
:style="{ background: c.author && c.author.avatar_color ? c.author.avatar_color : '#3A3A3A' }">
<span x-text="(c.author ? (c.author.full_name || c.author.login || '?') : '?').charAt(0).toUpperCase()"></span>
</div>
<span style="font-size:12px;font-weight:600;" x-text="c.author ? (c.author.full_name || c.author.login) : 'Unknown'"></span>
<span style="font-size:11px;color:var(--text-dim,#999);margin-left:auto;" x-text="fmtTime(c.created_at)"></span>
</div>
<div style="font-size:13px;white-space:pre-wrap;margin-bottom:6px;" x-text="c.body"></div>
<div style="display:flex;gap:8px;align-items:center;">
<button class="btn-sm" style="font-size:11px;" @click="resolveComment(c.id)"
x-text="c.resolved ? '↪ Reopen' : '✔ Resolve'"></button>
<button class="btn-sm" style="font-size:11px;" x-show="c.user_id === currentUserId"
@click="deleteComment(c.id)">🗑 Delete</button>
</div>
</div>
</template>
</div>
<div class="comments-drawer-input" style="border-top:1px solid var(--border,#333);padding:12px 16px;">
<textarea x-model="commentDraft" rows="2"
placeholder="Add a comment... Use @ to mention someone."
style="width:100%;background:var(--bg-secondary,#2a2a2a);border:1px solid var(--border,#333);
border-radius:8px;color:var(--text);font-size:13px;padding:8px 10px;resize:none;outline:none;"></textarea>
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
<button class="btn btn-primary" style="font-size:12px;padding:6px 14px;" @click="addPageComment()">Comment</button>
</div>
</div>
</div>
<div
class="format-toolbar"
x-show="fmt.open"
:style="{top:fmt.top+'px',left:fmt.left+'px'}"
@mousedown.prevent
>
<button @click="fmtApply('bold')"><strong>B</strong></button>
<button @click="fmtApply('italic')"><em>I</em></button>
<button @click="fmtApply('underline')"><u>U</u></button>
<button @click="fmtApply('strikeThrough')"><s>S</s></button>
<button @click="fmtLink()">{{ fd_icon("link",14) }}</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="askAI()"
>
<span class="gs-icon">{{ fd_icon("sparkles",14) }}</span> Ask AI
</button>
<button
class="gs-pill"
@click="meetingNote()"
>
<span class="gs-icon">{{ fd_icon('edit',14) }}</span> AI meeting note
</button>
<button
class="gs-pill"
@click="createDatabase()"
>
<span class="gs-icon">{{ fd_icon("folder",14) }}</span> Database
</button>
<button
class="gs-pill"
@click="createForm()"
>
<span class="gs-icon">{{ fd_icon("copy",14) }}</span> Form
</button>
<button
class="gs-pill"
@click="gsMoreOpen = !gsMoreOpen"
>
<span class="gs-icon">{{ fd_icon("file",14) }}</span> Templates
</button>
<div style="position: relative">
<button class="gs-pill" @click="gsMoreOpen = !gsMoreOpen">
<span>⋯</span>
</button>
<div
class="gs-more-dropdown"
x-show="gsMoreOpen"
@click.outside="gsMoreOpen = false"
x-transition
>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon("bar-chart",14) }} Table
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon("copy",14) }} Board
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon('list',14) }} List
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon("calendar",14) }} Timeline
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon("calendar",14) }} Calendar
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon("image",14) }} Gallery
</div>
<div class="gs-more-sep"></div>
<div class="gs-more-item" @click="gsMoreOpen=false">
{{ fd_icon("download",14) }} Import
</div>
</div>
</div>
</div>
<div class="editor-statusbar">
<span x-show="saving">Saving...</span>
<span x-show="!saving&&dirty">Unsaved</span>
<span x-show="!saving&&!dirty&&lastSaved"
>Saved <span x-text="lastSaved"></span
></span>
<span class="statusbar-right" x-text="blocks.length+' blocks'"></span>
</div>
<!-- ═══════════ Database template picker (v5.3.0) ═══════════ -->
<div class="fd-dbtpl-overlay" id="fd-dbtpl" x-show="dbTplOpen"
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;
align-items:flex-start;justify-content:center;padding-top:10vh;opacity:0;transition:opacity .15s ease;"
@click.self="closeDbTemplatePicker()">
<div class="fd-dbtpl" style="width:620px;max-width:94vw;max-height:76vh;display:flex;flex-direction:column;
background:var(--bg-secondary,#232323);border:1px solid var(--border,rgba(255,255,255,.08));border-radius:12px;
box-shadow:0 24px 70px rgba(0,0,0,.6);overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);">
<h3 style="margin:0;font-size:16px;color:var(--text,#fff);">Create a database</h3>
<button class="flowdeck-modal-close" @click="closeDbTemplatePicker()">&times;</button>
</div>
<div style="padding:12px 20px;overflow-y:auto;">
<div x-show="dbTplLoading" style="color:var(--text-dim,#999);font-size:13px;padding:20px 0;text-align:center;">Loading templates…</div>
<div x-show="!dbTplLoading">
<!-- Blank -->
<div class="fd-dbtpl-card" @click="createDbFromTemplate(null)"
style="display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;margin-bottom:10px;cursor:pointer;">
<span style="font-size:22px;">📋</span>
<div><div style="font-weight:600;color:var(--text,#fff);font-size:14px;">Blank database</div>
<div style="font-size:12px;color:var(--text-dim,#999);">Start from an empty database</div></div>
</div>
<template x-for="t in dbTemplates" :key="t.id">
<div class="fd-dbtpl-card" @click="createDbFromTemplate(t.name)"
style="display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;margin-bottom:10px;cursor:pointer;">
<span style="font-size:22px;" x-text="t.icon||'📋'"></span>
<div style="flex:1;"><div style="font-weight:600;color:var(--text,#fff);font-size:14px;" x-text="t.name"></div>
<div style="font-size:12px;color:var(--text-dim,#999);" x-text="t.description||''"></div></div>
</div>
</template>
<div x-show="!dbTplLoading && !dbTemplates.length" style="color:var(--text-dim,#999);font-size:13px;padding:16px 0;text-align:center;">
No templates available
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════ v5.4.0: Version History Popover ═══════════ -->
<div class="versions-popover"
x-show="versionsOpen"
@click.outside="versionsOpen = false"
x-transition.opacity.scale.origin.top.right
style="position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-modal);z-index:1000;width:380px;max-height:500px;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:14px;">
<span>{{ fd_icon('history',14) }} Version history</span>
<button class="topbar-btn" @click="versionsOpen = false">✕</button>
</div>
<div style="max-height:420px;overflow-y:auto;">
<div x-show="versionsLoading" style="padding:32px;text-align:center;color:var(--text-dim);">Loading…</div>
<template x-if="!versionsLoading && versionsList.length === 0">
<div style="padding:24px;text-align:center;color:var(--text-dim);">No versions yet</div>
</template>
<template x-for="v in versionsList" :key="v.id">
<div class="version-item"
style="padding:12px 16px;border-bottom:1px solid var(--border);cursor:pointer;"
@click="restoreVersion(v.id)">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="font-weight:500;color:var(--text-primary);" x-text="v.note || 'Edited'"></div>
<span style="font-size:12px;color:var(--text-dim);" x-text="v.author ? v.author + ' · ' : ''"></span>
</div>
<div style="font-size:12px;color:var(--text-dim);margin-top:4px;" x-text="formatDate(v.created_at)"></div>
</div>
</template>
</div>
</div>
<!-- ═══════════ v5.4.0: Backlinks Popover ═══════════ -->
<div class="backlinks-popover"
x-show="backlinksOpen"
@click.outside="backlinksOpen = false"
x-transition.opacity.scale.origin.top.right
style="position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-modal);z-index:1000;width:360px;max-height:500px;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:14px;">
<span>{{ fd_icon('link-2',14) }} Linked from</span>
<button class="topbar-btn" @click="backlinksOpen = false">✕</button>
</div>
<div style="max-height:420px;overflow-y:auto;">
<div x-show="backlinksLoading" style="padding:32px;text-align:center;color:var(--text-dim);">Loading…</div>
<template x-if="!backlinksLoading && backlinksList.length === 0">
<div style="padding:24px;text-align:center;color:var(--text-dim);">No pages link here</div>
</template>
<template x-for="b in backlinksList" :key="b.id">
<a :href="'/pages/' + b.id" style="display:block;padding:12px 16px;border-bottom:1px solid var(--border);text-decoration:none;color:inherit;" @click.prevent="openBacklink(b)">
<div style="font-weight:500;color:var(--text-primary);" x-text="b.title"></div>
<div style="font-size:12px;color:var(--text-dim);margin-top:2px;" x-text="b.workspace || ''"></div>
</a>
</template>
</div>
</div>
<!-- ═══════════ v5.4.0: Import Modal ═══════════ -->
<div class="import-modal"
x-show="importOpen"
@click.self="importOpen = false"
x-transition.opacity
style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;display:flex;align-items:center;justify-content:center;padding:20px;">
<div class="import-box" style="width:560px;max-width:94vw;max-height:76vh;background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;box-shadow:0 24px 70px rgba(0,0,0,.6);overflow:hidden;display:flex;flex-direction:column;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);">
<h3 style="margin:0;font-size:16px;color:var(--text);">{{ fd_icon('download',14) }} Import page</h3>
<button class="flowdeck-modal-close" @click="importOpen = false">&times;</button>
</div>
<div style="padding:20px;overflow-y:auto;flex:1;">
<div style="display:flex;flex-direction:column;gap:16px;">
<!-- Markdown text import -->
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:8px;color:var(--text);">Markdown text</label>
<textarea x-model="importText" rows="10" placeholder="Paste Markdown here…" style="width:100%;background:var(--bg-primary);border:1px solid var(--border);border-radius:8px;padding:12px;color:var(--text);font-family:var(--font-mono);font-size:13px;resize:vertical;outline:none;box-sizing:border-box;"></textarea>
<div style="display:flex;gap:8px;margin-top:10px;">
<button class="sd-btn-primary" @click="doImport()" :disabled="!importText.trim() && !importFile">{{ fd_icon('download',14) }} Import as new page</button>
</div>
</div>
<div style="border-top:1px solid var(--border);padding-top:16px;">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:8px;color:var(--text);">{{ fd_icon('file',14) }} File (.md, .txt, .zip)</label>
<input type="file" accept=".md,.markdown,.txt,.zip" @change="handleImportFile($event)" style="width:100%;padding:12px;background:var(--bg-primary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;box-sizing:border-box;">
<template x-if="importFile">
<div style="margin-top:8px;display:flex;align-items:center;gap:12px;color:var(--text-dim);font-size:13px;">
<span x-text="importFile.name"></span>
<span x-text="fmtImportSize(importFile)"></span>
</div>
</template>
<div style="display:flex;gap:8px;margin-top:10px;">
<button class="sd-btn-primary" @click="doImport()" :disabled="!importText.trim() && !importFile">{{ fd_icon('download',14) }} Import file</button>
</div>
</div>
</div>
</div>
<div style="padding:12px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;">
<a href="/import" style="font-size:13px;color:var(--accent);text-decoration:none;">Assistant d'import avancé (Obsidian, Notion, CSV, Excel…) →</a>
</div>
</div>
</div>
</div>