peek panels: load full page editor in iframe + resize handles
- Add ?embed=1 to /pages/{id} route, passes embed_mode to template
- base.html: embed-mode class hides sidebar/topbar, removes margins
- Replace peek-body content with iframe loading /pages/{id}?embed=1
in both library.html and local_workspace.html
- Remove async content loading (now editor handles rendering)
- Add resize handle on left edge of peek panels (both library + local-ws)
- Click without drag: closes panel
- Click + drag horizontal: resizes panel width (300px to 90vw)
- Width persisted in localStorage (fd_peek_width)
- 100% code reuse: editor runs same /pages/{id} with embed flag
This commit is contained in:
@@ -946,7 +946,9 @@ async def delete_page(request: Request, page_id: int):
|
||||
|
||||
@router.get("/pages/{page_id}", response_class=HTMLResponse)
|
||||
async def view_page(request: Request, page_id: int):
|
||||
"""Render a page as HTML, or a file viewer for uploaded files."""
|
||||
"""Render a page as HTML, or a file viewer for uploaded files.
|
||||
?embed=1 — minimal mode for side peek (no sidebar/topbar)."""
|
||||
embed = request.query_params.get("embed") == "1"
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
with get_conn() as conn:
|
||||
@@ -1003,7 +1005,8 @@ async def view_page(request: Request, page_id: int):
|
||||
"page_data": page_data,
|
||||
"breadcrumb_items": nav_crumbs,
|
||||
"nav_workspace_id": page.get("workspace_id") or 0,
|
||||
"nav_page_id": page_id}
|
||||
"nav_page_id": page_id,
|
||||
"embed_mode": embed}
|
||||
template = env.get_template("page_editor.html")
|
||||
response = template.render(**ctx)
|
||||
return HTMLResponse(content=response, headers={"Cache-Control": "no-store, max-age=0"})
|
||||
|
||||
@@ -88,12 +88,18 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
/* Embed mode — hide sidebar + topbar for peek panels */
|
||||
body.embed-mode .sidebar, body.embed-mode .topbar, body.embed-mode .app-layout { display: block !important; }
|
||||
body.embed-mode .sidebar { display: none !important; }
|
||||
body.embed-mode .topbar { display: none !important; }
|
||||
body.embed-mode .app-layout { margin-left: 0 !important; padding-top: 0 !important; }
|
||||
body.embed-mode .sidebar-collapsed .app-layout { margin-left: 0 !important; }
|
||||
</style>
|
||||
<script src="/static/js/htmx.min.js"></script>
|
||||
<script src="/static/js/alpine.min.js" defer></script>
|
||||
<script src="/static/js/sortable.min.js" defer></script>
|
||||
</head>
|
||||
<body hx-headers='{"X-CSRF-Token":"__CSRF_PLACEHOLDER__"}'>
|
||||
<body hx-headers='{"X-CSRF-Token":"__CSRF_PLACEHOLDER__"}'{% if embed_mode %} class="embed-mode"{% endif %}>
|
||||
<div class="app-layout" x-data="appState()">
|
||||
|
||||
<!-- ═══════════ MOBILE SIDEBAR OVERLAY ═══════════ -->
|
||||
|
||||
+45
-36
@@ -407,6 +407,7 @@
|
||||
|
||||
<!-- ── Side Peek Panel ── -->
|
||||
<div class="peek-overlay" :class="{ open: peekItem !== null }" @click.outside="closePeek()">
|
||||
<div id="lib-peek-resize-handle" style="position:absolute;left:-4px;top:0;width:8px;height:100%;cursor:col-resize;z-index:501;" title="Drag to resize — click to close"></div>
|
||||
<template x-if="peekItem">
|
||||
<div style="display:flex;flex-direction:column;height:100%;">
|
||||
<div class="peek-header">
|
||||
@@ -445,13 +446,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="peek-body">
|
||||
<div x-show="peekLoading" class="peek-loading">Loading…</div>
|
||||
<div x-show="!peekLoading && peekContent" x-html="peekContent"></div>
|
||||
<div x-show="!peekLoading && !peekContent" class="peek-empty">
|
||||
<div style="margin-bottom:8px;">{{ fd_icon("file",32) }}</div>
|
||||
<div style="font-size:14px;color:var(--text-secondary);">No preview available</div>
|
||||
</div>
|
||||
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;">
|
||||
<iframe id="lib-peek-iframe" src="" style="width:100%;height:100%;border:none;flex:1;" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1202,40 +1198,17 @@ function libraryPage() {
|
||||
// ── Side peek ──
|
||||
async openPeek(item) {
|
||||
this.peekItem = item;
|
||||
this.peekContent = '';
|
||||
this.peekLoading = true;
|
||||
this.peekFavorited = item.favorited || false;
|
||||
try {
|
||||
var r = await fetch('/board/api/pages/' + item.id);
|
||||
var d = await r.json();
|
||||
var content = d.content || '';
|
||||
// Simple preview
|
||||
if (d.content_format === 'blocks') {
|
||||
try {
|
||||
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
|
||||
var preview = '';
|
||||
var lines = Array.isArray(blocks) ? blocks.slice(0, 30) : ((blocks && blocks.blocks) ? blocks.blocks.slice(0, 30) : []);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var b = lines[i];
|
||||
var txt = b.content || b.text || (typeof b === 'string' ? b : '');
|
||||
if (txt && txt.trim()) preview += '<div style="margin-bottom:6px;font-size:14px;line-height:1.5;color:var(--text-primary);">' + this._escHtml(String(txt)) + '</div>';
|
||||
}
|
||||
this.peekContent = preview || '<div style="color:var(--text-dim);">(empty page)</div>';
|
||||
} catch(e) {
|
||||
this.peekContent = '<pre style="font-size:13px;color:var(--text-primary);white-space:pre-wrap;">' + this._escHtml(String(content).substring(0, 2000)) + '</pre>';
|
||||
}
|
||||
} else {
|
||||
this.peekContent = content ? '<div style="font-size:14px;line-height:1.6;color:var(--text-primary);white-space:pre-wrap;">' + this._escHtml(String(content).substring(0, 2000)) + '</div>' : '';
|
||||
}
|
||||
} catch(e) {
|
||||
this.peekContent = '';
|
||||
}
|
||||
this.peekLoading = false;
|
||||
// Load editor in iframe
|
||||
var iframe = document.getElementById('lib-peek-iframe');
|
||||
if (iframe) iframe.src = '/pages/' + item.id + '?embed=1';
|
||||
},
|
||||
|
||||
closePeek() {
|
||||
this.peekItem = null;
|
||||
this.peekContent = '';
|
||||
// Clear iframe
|
||||
var iframe = document.getElementById('lib-peek-iframe');
|
||||
if (iframe) iframe.src = '';
|
||||
},
|
||||
|
||||
async toggleFavoritePeek() {
|
||||
@@ -1303,5 +1276,41 @@ function libraryPage() {
|
||||
}
|
||||
};
|
||||
}
|
||||
// Init peek resize handle (vanilla JS)
|
||||
(function() {
|
||||
var panel = document.querySelector('.peek-overlay');
|
||||
var handle = document.getElementById('lib-peek-resize-handle');
|
||||
if (!panel || !handle) return;
|
||||
var startX, startWidth, dragging = false;
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
startX = e.clientX;
|
||||
startWidth = panel.offsetWidth;
|
||||
dragging = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (!dragging) return;
|
||||
var deltaX = startX - e.clientX;
|
||||
var newWidth = Math.max(300, Math.min(window.innerWidth * 0.9, startWidth + deltaX));
|
||||
panel.style.width = newWidth + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', function(e) {
|
||||
if (!dragging) return;
|
||||
document.body.style.userSelect = '';
|
||||
var moved = Math.abs(e.clientX - startX);
|
||||
if (moved < 3) {
|
||||
// Click — close via Alpine data
|
||||
var libData = document.querySelector('[x-data]')._x_dataStack[0];
|
||||
if (libData && libData.closePeek) libData.closePeek();
|
||||
}
|
||||
dragging = false;
|
||||
try { localStorage.setItem('fd_peek_width', panel.style.width); } catch(ex) {}
|
||||
});
|
||||
try {
|
||||
var saved = localStorage.getItem('fd_peek_width');
|
||||
if (saved) panel.style.width = saved;
|
||||
} catch(ex) {}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -631,6 +631,44 @@ window._wsInitData = (function() {
|
||||
this.loadWorkspaceTags();
|
||||
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
|
||||
this._recompute();
|
||||
this._initResizeHandle();
|
||||
},
|
||||
|
||||
_initResizeHandle() {
|
||||
var panel = document.getElementById('ws-preview-panel');
|
||||
var handle = document.getElementById('ws-preview-resize-handle');
|
||||
if (!panel || !handle) return;
|
||||
var startX, startWidth, dragging = false;
|
||||
handle.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
startX = e.clientX;
|
||||
startWidth = panel.offsetWidth;
|
||||
dragging = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (!dragging) return;
|
||||
var deltaX = startX - e.clientX;
|
||||
var newWidth = Math.max(300, Math.min(window.innerWidth * 0.9, startWidth + deltaX));
|
||||
panel.style.width = newWidth + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', function(e) {
|
||||
if (!dragging) return;
|
||||
document.body.style.userSelect = '';
|
||||
var moved = Math.abs(e.clientX - startX);
|
||||
if (moved < 3) {
|
||||
// Simple click — close panel
|
||||
window._wsData.closePreviewPanel();
|
||||
}
|
||||
dragging = false;
|
||||
// Save width preference
|
||||
try { localStorage.setItem('fd_peek_width', panel.style.width); } catch(ex) {}
|
||||
});
|
||||
// Restore saved width
|
||||
try {
|
||||
var saved = localStorage.getItem('fd_peek_width');
|
||||
if (saved) panel.style.width = saved;
|
||||
} catch(ex) {}
|
||||
},
|
||||
|
||||
_recompute() {
|
||||
@@ -796,7 +834,7 @@ window._wsInitData = (function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
// ── Preview panel (vanilla JS — matches Library peek-overlay) ──
|
||||
// ── Preview panel (vanilla JS — loads /pages/{id}?embed=1 in iframe) ──
|
||||
selectForPreview(node) {
|
||||
var panel = document.getElementById('ws-preview-panel');
|
||||
if (!panel) return;
|
||||
@@ -819,21 +857,9 @@ window._wsInitData = (function() {
|
||||
window.showToast('Failed to copy', 'error');
|
||||
});
|
||||
};
|
||||
// Body: meta info header + loading indicator
|
||||
var meta = '<div style="display:flex;gap:16px;flex-wrap:wrap;padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid var(--border);font-size:12px;color:var(--text-tertiary);">'
|
||||
+ '<span>' + (node.is_folder ? 'Folder' : this._fileTypeLabel(node.name, node.content_format)) + '</span>'
|
||||
+ '<span>' + (node.size_display || '-') + '</span>'
|
||||
+ '<span>' + this._formatDate(node.updated_at) + '</span>'
|
||||
+ '</div>';
|
||||
if (node.tags && node.tags.length > 0) {
|
||||
meta += '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:12px;">';
|
||||
for (var i = 0; i < node.tags.length; i++) {
|
||||
var t = node.tags[i];
|
||||
meta += '<span class="tag-chip" style="background:' + this._escHtml(t.color) + '">' + this._escHtml(t.name) + '</span>';
|
||||
}
|
||||
meta += '</div>';
|
||||
}
|
||||
document.getElementById('ws-preview-body').innerHTML = meta + '<div style="text-align:center;padding:40px;color:var(--text-tertiary)">Loading...</div>';
|
||||
// Load the full page editor in iframe (reuses /pages/{id} code)
|
||||
var iframe = document.getElementById('ws-preview-iframe');
|
||||
if (iframe) iframe.src = '/pages/' + node.id + '?embed=1';
|
||||
// Show panel with slide-in
|
||||
panel.style.display = 'flex';
|
||||
requestAnimationFrame(function() {
|
||||
@@ -841,8 +867,6 @@ window._wsInitData = (function() {
|
||||
panel.style.transform = 'translateX(0)';
|
||||
});
|
||||
});
|
||||
// Load content async
|
||||
this._loadPreviewContentVanilla(node);
|
||||
},
|
||||
|
||||
closePreviewPanel() {
|
||||
@@ -852,6 +876,9 @@ window._wsInitData = (function() {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
panel.style.display = 'none';
|
||||
// Clear iframe
|
||||
var iframe = document.getElementById('ws-preview-iframe');
|
||||
if (iframe) iframe.src = '';
|
||||
self.previewItem = null;
|
||||
}, 200);
|
||||
},
|
||||
@@ -2883,6 +2910,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
|
||||
<!-- Preview Panel — identical to Library peek-overlay -->
|
||||
<div id="ws-preview-panel" style="position:fixed;top:0;right:0;width:560px;max-width:90vw;height:100vh;background:var(--bg-modal);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.25);z-index:500;display:none;flex-direction:column;transform:translateX(100%);transition:transform 0.2s cubic-bezier(0.16,1,0.3,1);">
|
||||
<div id="ws-preview-resize-handle" style="position:absolute;left:-4px;top:0;width:8px;height:100%;cursor:col-resize;z-index:501;" title="Drag to resize — click to close"></div>
|
||||
<div class="peek-header" style="display:flex;align-items:center;gap:4px;padding:12px 16px;border-bottom:1px solid var(--border);">
|
||||
<button onclick="window._wsData.closePreviewPanel()" title="Close" style="background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;display:flex;align-items:center;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
@@ -2913,9 +2941,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ws-preview-body" style="flex:1;overflow-y:auto;padding:16px 20px;">
|
||||
<div style="text-align:center;padding:60px 20px;color:var(--text-dim);">{{ fd_icon("file",32) }}<div style="margin-top:8px;font-size:14px;">Select a file to preview</div></div>
|
||||
</div>
|
||||
<iframe id="ws-preview-iframe" src="" style="width:100%;height:100%;border:none;flex:1;" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
|
||||
</div>
|
||||
</div><!-- /ws-split -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user