Architecture for zero code duplication:
- Extract page editor HTML into _page_editor_content.html (shared include)
- page_editor.html includes it (same behavior as before)
- page_editor_embed.html includes it with empty topbar (no header in peek)
- board.py: ?embed=1 renders page_editor_embed.html
Peek panels now load /pages/{id}?embed=1 in iframe:
- Editor renders without sidebar or header breadcrumb/actions
- Full editing capability (blocks, markdown, slash commands, save)
- Same code — one change to _page_editor_content.html updates everywhere
513 lines
24 KiB
HTML
513 lines
24 KiB
HTML
<div class="page-editor-wrapper" x-data="editorState()">
|
|
<!-- 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-row">
|
|
<input
|
|
type="email"
|
|
class="sd-input"
|
|
placeholder="Add people, groups or emails..."
|
|
x-model="inviteEmail"
|
|
@keydown.enter="shareInvite()"
|
|
/>
|
|
<button class="sd-btn-primary" @click="shareInvite()">
|
|
Invite
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Context card (shown when independent permissions) -->
|
|
<div class="sd-context-card" x-show="false">
|
|
Share settings on this page are unlinked from parent
|
|
page
|
|
</div>
|
|
|
|
<!-- Participants list -->
|
|
<div class="sd-participants" x-show="accessList.length">
|
|
<template x-for="a in accessList" :key="a.email">
|
|
{% raw %}
|
|
<div class="sd-participant">
|
|
<div class="sd-participant-info">
|
|
<div
|
|
class="sd-avatar"
|
|
x-text="a.email[0].toUpperCase()"
|
|
></div>
|
|
<div>
|
|
<div
|
|
class="sd-participant-name"
|
|
x-text="a.email"
|
|
></div>
|
|
<div
|
|
class="sd-participant-email"
|
|
x-text="a.email"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="sd-perm-btn"
|
|
@click="a.permOpen = !a.permOpen"
|
|
>
|
|
<span
|
|
x-text="a.permission === 'editor' ? 'Full access' : a.permission === 'commenter' ? 'Can comment' : 'Can view'"
|
|
></span>
|
|
▾
|
|
</button>
|
|
<div
|
|
class="sd-perm-menu"
|
|
x-show="a.permOpen"
|
|
@click.outside="a.permOpen = false"
|
|
>
|
|
<div
|
|
class="sd-perm-option"
|
|
:class="{ active: a.permission === 'editor' }"
|
|
@click="a.permission='editor'; a.permOpen=false"
|
|
>
|
|
<span>Can edit</span>
|
|
<span class="sd-perm-desc"
|
|
>Edit, suggest and comment</span
|
|
>
|
|
</div>
|
|
<div
|
|
class="sd-perm-option"
|
|
:class="{ active: a.permission === 'commenter' }"
|
|
@click="a.permission='commenter'; a.permOpen=false"
|
|
>
|
|
<span>Can comment</span>
|
|
<span class="sd-perm-desc"
|
|
>Suggest and comment only</span
|
|
>
|
|
</div>
|
|
<div
|
|
class="sd-perm-option"
|
|
:class="{ active: a.permission === 'viewer' }"
|
|
@click="a.permission='viewer'; a.permOpen=false"
|
|
>
|
|
<span>Can view</span>
|
|
<span class="sd-perm-desc"
|
|
>Read only</span
|
|
>
|
|
</div>
|
|
<div class="sd-perm-sep"></div>
|
|
<div
|
|
class="sd-perm-option danger"
|
|
@click="removeAccess(a.email); a.permOpen=false"
|
|
>
|
|
Remove
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% 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>
|
|
|
|
<!-- 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="exportPage">↗ Export</div>
|
|
<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 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">
|
|
<div class="page-title-block">
|
|
<span class="page-icon-emoji">{% if page.content_format == 'file' %}{{ fd_icon("paperclip",14) }}{% else %}{{ fd_icon("file",14) }}{% endif %}</span>
|
|
<div
|
|
class="page-title-input"
|
|
contenteditable="true"
|
|
id="_titleEl"
|
|
@input="dirty=true;save()"
|
|
@keydown.enter.prevent="focusBlock()"
|
|
@paste.prevent="pastePlain($event)"
|
|
spellcheck="false"
|
|
>
|
|
{{ page.title }}
|
|
</div>
|
|
</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>
|
|
|
|
<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="replaceBlock(0, 'paragraph');blocks[0].content='Ask AI to write...';render()"
|
|
>
|
|
<span class="gs-icon">{{ fd_icon("sparkles",14) }}</span> Ask AI
|
|
</button>
|
|
<button
|
|
class="gs-pill"
|
|
@click="replaceBlock(0, 'heading_1');blocks[0].content='AI Meeting Note';render()"
|
|
>
|
|
<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>
|
|
</div> |