Files
flowdeck/app/templates/_page_editor_content.html
T
bruno 400a827e3f
FlowDeck CI / lint (push) Successful in 2m5s
FlowDeck CI / test (push) Failing after 3h5m38s
FlowDeck CI / docker (push) Skipped
feat: commentaires façon Notion — ancres jaunes, tiroir refondu, réactions emoji sur texte (v7.64.0)
- Réaction sur texte : bouton 🙂+ (« React to selected text ») dans la toolbar
  de sélection → sélecteur d'émoji EXISTANT en mode réaction (en-tête = texte
  sélectionné + ✋, onglets masqués, ligne Recent au-dessus de la grille
  People, Filter… et barre de catégories avec + custom inchangés)
- Backend : migration 37 text_reactions (UNIQUE plage × emoji × utilisateur →
  POST idempotent) + GET/POST /api/pages/{id}/reactions (GET groupé par plage
  emoji/count/mine, POST toggle, 400 sur plage vide)
- Affichage : plage commentée/réagie peinte en jaune (_paintAnchors après
  render/loadComments/loadReactions), pastille 😀+compte après la plage, clic
  pastille = rouvrir le sélecteur, clic ancre = ouvrir et centrer le fil
- Pastilles = annotations : gtTok/gtMd les ignorent (y compris le chemin
  rapide innerText) et les offsets _srcOffset/_srcNodeLen les excluent → le
  texte sauvé est identique (vérifié dans le e2e)
- Tiroir refondu : carte de fil (avatar, nom, horodatage, ✓ toggle
  resolve/reopen, ⋯ → Copy link #cmt-<id>/Delete), saisie « Add a comment... »
  avec @ et envoi ↑/Entrée ; #cmt-<id> ouvre et centre le fil au chargement ;
  loadComments + loadReactions à l'ouverture (commentCount enfin renseigné)
- Fixes : resolveComment ne pouvait pas rouvrir (toujours resolved:true) ;
  showCommentBtn ignorait les sélections à ancre élément (bouton 💬 jamais
  affiché)
- Tests : tests/test_v764_comments_reactions.py (8, dont toggle API réel),
  e2e/v764_comments_reactions.spec.js (8 étapes) — 3/3 specs e2e verts,
  suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.0 (526 chemins)
2026-10-08 10:46:14 -04:00

927 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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="closeInviteSuggest()"
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="setSharePerm(a, 'edit')"
>
<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="setSharePerm(a, 'comment')"
>
<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="setSharePerm(a, 'view')"
>
<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="removeShareAndClose(a)"
>
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="pickGeneralAccess('invited')"
>
<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="pickGeneralAccess('anyone')"
>
<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="applyIcon()">
</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="moreToggleLock()">
<span x-text="isLocked ? '🔓 Unlock page' : '🔒 Lock page'"></span>
</div>
<div class="more-menu-item" @click="moreFullWidth()">
↔ Full width <span x-show="fullWidth" style="margin-left:auto;font-size:11px">✓</span>
</div>
<div class="more-menu-item" @click="moreFontSmall()">
Aa Small text <span x-show="fontSmall" style="margin-left:auto;font-size:11px">✓</span>
</div>
<div class="more-menu-item" @click="moreSaveTemplate()">
📑 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="markAndSave()"
@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 · refonte v7.64 façon Notion) ═══════════ -->
<div class="comments-drawer" x-show="commentsOpen" x-cloak x-transition
style="position:fixed;top:var(--topbar-height,44px);right:0;bottom:0;width:340px;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 style="color:var(--text-dim);font-weight:400;" x-text="'(' + commentCount + ')'"></span></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" :data-thread="c.id"
:class="{ 'is-resolved': c.resolved, 'has-anchor': !!c.anchor_block_id }">
<div class="ct-head">
<div class="ct-avatar"
: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 class="ct-name" x-text="c.author ? (c.author.full_name || c.author.login) : 'Unknown'"></span>
<span class="ct-time" x-text="fmtTime(c.created_at)"></span>
<div class="ct-actions">
<button type="button" class="ct-act" :title="c.resolved ? 'Reopen' : 'Resolve'"
@click="resolveComment(c)"
:class="{ 'is-on': c.resolved }">✓</button>
<button type="button" class="ct-act" title="More"
x-show="c.user_id === currentUserId"
@click.stop="commentMenu = commentMenu === c.id ? null : c.id">⋯</button>
</div>
<div class="ct-menu" x-cloak x-show="commentMenu === c.id" @click.outside="commentMenu = null">
<button type="button" class="ct-menu-item" @click="copyCommentLink(c.id)">🔗 Copy link</button>
<button type="button" class="ct-menu-item is-danger" @click="deleteComment(c.id)">🗑 Delete</button>
</div>
</div>
<div class="ct-body" x-text="c.body"></div>
<div class="ct-anchor-label" x-show="!!c.anchor_block_id && !c.resolved">💬 on selected text</div>
</div>
</template>
</div>
<div class="comments-drawer-input">
<div class="ct-input-row">
<textarea x-model="commentDraft" rows="1"
placeholder="Add a comment..."
@keydown.enter.exact.prevent="addPageComment()"></textarea>
<div class="ct-input-actions">
<button type="button" class="ct-act" title="Mention" @click="commentAt()">@</button>
<button type="button" class="ct-send" title="Send"
:disabled="!(commentDraft || '').trim()" @click="addPageComment()">↑</button>
</div>
</div>
</div>
</div>
<!-- ═══════════ Menu contextuel au highlight de texte (v7.62.0, façon Notion) ═══════════ -->
<div
class="format-toolbar"
x-show="fmt.open"
x-cloak
:style="{top:fmt.top+'px',left:fmt.left+'px'}"
@mousedown.prevent
>
<!-- Ligne 1 : style de texte -->
<div class="ft-row">
<button type="button" class="ft-item ft-style"
@click="fmtStyle($event)"
data-tooltip-html="<em>To be the foremost driver…</em><br>Just start writing with plain text">
<span class="ft-style-ico">T</span>
<span class="ft-style-name" x-text="fmtStyleName()"></span>
<span class="ft-caret">›</span>
</button>
</div>
<!-- Ligne 2 : formatage de base -->
<div class="ft-row">
<button type="button" class="ft-item ft-a" @click="fmtColor($event)" data-tooltip="Color — text and background colors"><span class="ft-a-letter">A</span></button>
<button type="button" class="ft-item" @click="fmtApply('bold')" data-tooltip="Bold — Ctrl+B"><strong>B</strong></button>
<button type="button" class="ft-item" @click="fmtApply('italic')" data-tooltip="Italic — Ctrl+I"><em>I</em></button>
<button type="button" class="ft-item" @click="fmtApply('underline')" data-tooltip="Underline"><u>U</u></button>
<button type="button" class="ft-item" @click="fmtApply('strikeThrough')" data-tooltip="Strikethrough"><s>T</s></button>
<button type="button" class="ft-item ft-tx" @click="fmtCode()" data-tooltip="Inline code">Tx</button>
</div>
<!-- Ligne 3 : outils avancés -->
<div class="ft-row">
<button type="button" class="ft-item" @click="fmtLink()" data-tooltip="Link">{{ fd_icon("link",15) }}</button>
<button type="button" class="ft-item" @click="fmtHighlight()" data-tooltip="Highlight"><span class="ft-hl">S</span></button>
<button type="button" class="ft-item ft-react" @click="fmtReact()" data-tooltip="React to selected text"><span class="ft-react-ico">🙂<span class="ft-react-plus">+</span></span></button>
<button type="button" class="ft-item ft-code-ico" @click="fmtCodeBlock()" data-tooltip="Code block">&lt;/&gt;</button>
<button type="button" class="ft-item" @click="fmtEquation()" data-tooltip="Equation">√x</button>
<button type="button" class="ft-item ft-more" @click="fmtMore($event)" data-tooltip="More actions">…</button>
</div>
<!-- Ligne 4 : actions -->
<div class="ft-row">
<button type="button" class="ft-item" @click="fmtComment()" data-tooltip="Comment — Ctrl+⇧+M">{{ fd_icon("message-square",15) }}</button>
<button type="button" class="ft-item" @click="fmtAskAI()" data-tooltip="Ask AI — Ctrl+J">{{ fd_icon("sparkles",15) }}</button>
</div>
<!-- Section « Skills » -->
<div class="ft-sep"></div>
<div class="ft-group">Skills</div>
<button type="button" class="ft-item ft-skill" @click="fmtSkill('Improve the writing of this text (wording, clarity, style). Keep the meaning, language and format. Reply ONLY with the improved text.')" data-tooltip="Run on this block">Improve writing</button>
<button type="button" class="ft-item ft-skill" @click="fmtSkill('Proofread this text: fix spelling and grammar mistakes only. Reply ONLY with the corrected text.')" data-tooltip="Run on this block">Proofread</button>
<button type="button" class="ft-item ft-skill" @click="fmtSkill('Explain this text in simpler terms, in the same language. Reply ONLY with the explanation.')" data-tooltip="Run on this block">Explain</button>
<button type="button" class="ft-item ft-skill" @click="fmtSkill('Reformat this text for readability (structure, spacing, lists where useful) without changing its meaning. Reply ONLY with the reformatted text.')" data-tooltip="Run on this block">Reformat</button>
<!-- Pied de page -->
<div class="ft-sep"></div>
<button type="button" class="ft-item ft-foot" @click="fmtEditAI()" data-tooltip="Edit with AI">
<span>Edit with AI</span>
<kbd>Alt+⇧+E</kbd>
</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>