Files
flowdeck/app/templates/_page_editor_content.html
T
bruno 12beab01c9
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / lint (push) Failing after 1m37s
fix(editor+sidebar): bouton ⋯ retiré, Form via modale app, doublon du dernier fichier du tree
- "Get started with" : suppression du bouton "⋯" et de son menu mort
  (Table/Board/List/Timeline/Calendar/Gallery/Import — aucun handler).
- Pilule Form : createForm() plantait sur page vide ("Cannot read
  properties of undefined (reading 'push')") car this.inlineDBs n'était
  jamais initialisé dans editorState() (seulement via loadInlineDBs si un
  bloc inline_dbs existait déjà). Corrigé : inlineDBs:[] initialisé.
  Le nom du form est demandé via une modale Alpine personnalisée
  (flowdeck-modal*) au lieu du prompt() natif.
- Sidebar tree après drag&drop : refreshSidebarTree() préservait le lien
  "Manage Workspaces" via ul.querySelector('li:last-child') — non scopé,
  il attrapait le dernier fichier d'une sous-liste de dossier imbriquée
  (premier li:last-child du document) et le ré-insérait → doublon du
  dernier fichier. Corrigé : ':scope > li:last-child' (enfant direct).
2026-10-10 20:49:45 -04:00

1003 lines
62 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-item" @click="openTplPicker()">
{{ fd_icon("file",14) }} Apply 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>
<!-- ═══════════ 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;">
<!-- v7.67 : les réactions emoji du texte vivent aussi ici (✕ = retirer) -->
<div class="ct-reactions" x-show="reactionList().length" x-cloak style="margin-bottom:14px;">
<div class="ct-section-title">Reactions</div>
<template x-for="r in reactionList()" :key="r.key">
<div class="ct-reaction">
<span class="ct-re-emoji" x-text="r.emoji"></span>
<span class="ct-re-count" x-show="r.count > 1" x-text="r.count"></span>
<span class="ct-re-text" x-text="r.text"></span>
<button type="button" class="ct-act" title="Remove reaction"
x-show="r.mine" @click="removeReaction(r)">✕</button>
</div>
</template>
</div>
<div x-show="comments.length === 0 && !reactionList().length" 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-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 : Comment à gauche, réaction à droite (le ✨ Ask AI sauté :
doublon exact du bouton du pied, même action) -->
<div class="ft-row ft-row-split">
<button type="button" class="ft-item ft-comment" @click="fmtComment()" data-tooltip="Comment — Ctrl+⇧+M">{{ fd_icon("message-square",15) }}<span>Comment</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>
</div>
<!-- Section « Skills » -->
<div class="ft-sep"></div>
<div class="ft-group ft-skills-head">Skills</div>
<div class="ft-skills">
<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>
</div>
<!-- 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="openFormModal()"
>
<span class="gs-icon">{{ fd_icon("copy",14) }}</span> Form
</button>
<button
class="gs-pill"
@click="openTplPicker()"
>
<span class="gs-icon">{{ fd_icon("file",14) }}</span> Templates
</button>
</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="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;display:flex;align-items:center;justify-content:center;padding:20px;"
x-transition.opacity
@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>
<!-- ═══════════ Unified template picker (registre unifié, §7.1) ═══════════
Toujours une issue visible (P5) : liste, état vide avec création,
ou erreur explicite avec Réessayer — jamais un no-op silencieux. -->
<div class="fd-tpl-overlay" id="fd-tpl-picker" data-testid="tpl-picker" x-show="tplOpen"
style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2200;"
x-transition.opacity
@click.self="closeTplPicker()">
<!-- Enveloppe flex SÉPARÉE du x-show : Alpine restaure display:block
sur l'overlay (pas le flex inline) — sans cette enveloppe, le
dialogue reste collé en haut à gauche au lieu d'être centré. -->
<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:20px;box-sizing:border-box;">
<div class="fd-tpl" style="width:760px;max-width:96vw;max-height:80vh;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:14px 20px;border-bottom:1px solid var(--border);">
<h3 style="margin:0;font-size:16px;color:var(--text,#fff);">Appliquer un template</h3>
<button class="flowdeck-modal-close" @click="closeTplPicker()">&times;</button>
</div>
<div style="padding:12px 20px 0;">
<input type="text" x-model="tplQuery" @input.debounce.300ms="loadTplList()" placeholder="🔍 Rechercher un template…"
style="width:100%;padding:9px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box;">
<div class="fd-gtpl-filters" style="margin-top:10px;">
<template x-for="k in tplKindOptionsList" :key="k">
<button type="button" class="fd-tpl-filter" :class="tplKind === k ? 'active' : ''"
@click="setTplKind(k)" x-text="tplKindLabel(k)"></button>
</template>
<a href="/templates" style="margin-left:auto;font-size:12px;color:var(--text-dim);align-self:center;text-decoration:none;padding-right:6px;">Gérer les templates…</a>
</div>
</div>
<div style="display:flex;gap:0;flex:1;min-height:0;padding:0 20px 12px;">
<div style="flex:1;overflow-y:auto;min-height:120px;max-height:44vh;padding-right:8px;">
<div x-show="tplLoading" style="color:var(--text-dim,#999);font-size:13px;padding:20px 0;text-align:center;">Chargement…</div>
<div x-show="tplError" style="color:#e5484d;font-size:13px;padding:12px;background:rgba(229,72,77,.1);border-radius:8px;margin-bottom:8px;">
<span x-text="tplError"></span>
<button @click="loadTplList()" style="margin-left:8px;">Réessayer</button>
</div>
<template x-for="t in tplItems" :key="t.id">
<div data-testid="tpl-item" @click="selectTpl(t)" :style="tplSelected && tplSelected.id === t.id ? 'border-color:var(--accent);' : ''"
style="display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--border);border-radius:8px;margin-bottom:8px;cursor:pointer;">
<span style="font-size:18px;" x-text="t.icon || '📄'"></span>
<div style="flex:1;min-width:0;">
<div style="font-weight:600;color:var(--text,#fff);font-size:13px;" x-text="t.name"></div>
<div style="font-size:11px;color:var(--text-dim,#999);" x-text="(t.kind + ' · ' + t.scope)"></div>
</div>
</div>
</template>
<div x-show="!tplLoading && !tplError && !tplItems.length" style="color:var(--text-dim,#999);font-size:13px;padding:16px 0;text-align:center;">
Aucun template pour ce filtre — <a href="/templates">créez-en un</a> ou choisissez un preset système.
</div>
</div>
<div style="flex:1;overflow-y:auto;min-height:120px;max-height:44vh;border-left:1px solid var(--border);padding-left:12px;" x-show="tplSelected">
<div style="font-weight:600;color:var(--text,#fff);font-size:14px;margin-bottom:4px;" x-text="tplPreviewTitle"></div>
<div x-show="tplPreviewWarnings.length" style="color:#f5a524;font-size:12px;margin-bottom:6px;" x-text="tplPreviewWarnings.join(' · ')"></div>
<template x-for="b in tplPreviewBlocks.slice(0,8)" :key="b.id">
<div style="font-size:12px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" x-text="('• ' + (b.content || ('[' + (b.type||'?') + ']')))"></div>
</template>
<div x-show="tplVarDefs.length" style="margin-top:8px;">
<div style="font-size:12px;font-weight:600;color:var(--text,#fff);margin-bottom:4px;">Variables</div>
<template x-for="v in tplVarDefs" :key="v.name">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="font-size:12px;color:var(--text-dim);min-width:80px;" x-text="v.label || v.name"></span>
<input type="text" :placeholder="v.default || ''" @input="setTplVar(v.name, $event.target.value)"
style="flex:1;padding:5px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:12px;">
</div>
</template>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--border);">
<button class="flowdeck-modal-close" @click="closeTplPicker()" style="padding:8px 16px;position:static;">Annuler</button>
<button data-testid="tpl-apply" @click="applyTpl()" :disabled="!tplSelected || tplApplying"
style="padding:8px 20px;border-radius:8px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:.95;"
x-text="tplApplying ? 'Application…' : 'Utiliser'"></button>
</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>
<!-- ═══════════ Get Started Form name modal ═══════════ -->
<div class="flowdeck-modal-overlay"
x-show="formOpen"
style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;display:flex;align-items:center;justify-content:center;padding:20px;"
@click.self="formOpen = false"
x-transition.opacity>
<div class="flowdeck-modal" @keydown.escape="formOpen = false">
<div class="flowdeck-modal-header">
<h3>{{ fd_icon("copy",16) }} Create form</h3>
<button class="flowdeck-modal-close" @click="formOpen = false">&times;</button>
</div>
<div class="flowdeck-modal-body">
<p>Enter a name for the new form.</p>
<input class="flowdeck-modal-input" type="text" x-model="formName" x-ref="formNameInput"
placeholder="Form name" @keydown.enter="confirmCreateForm()">
</div>
<div class="flowdeck-modal-footer">
<button class="flowdeck-modal-btn cancel" @click="formOpen = false">Cancel</button>
<button class="flowdeck-modal-btn confirm" @click="confirmCreateForm()">Create</button>
</div>
</div>
</div>
</div>