feat(v5.5.0): Embeds & Media riche - universal embeds, bookmark cards, lightbox, inline previews, cover & icon
FlowDeck CI / lint (push) Successful in 44s
FlowDeck CI / test (push) Successful in 4m10s
FlowDeck CI / docker (push) Successful in 37s

- embeds.py: provider detection/rewrite (YouTube, Vimeo, Figma, Maps, Docs,
  Loom, CodePen, Miro, Spotify, SoundCloud, Twitch, X/Twitter, Pinterest,
  Office) + resolve_embed/inline_kind/provider; POST /board/api/embed/resolve
- editor resolves pasted URLs and caches embed_src (persisted); renderer,
  public pages and MD/HTML/PDF exports prefer embed_src
- og_fetcher.py: robust meta parsing (any attribute order), favicon,
  injectable transport, network-safe fallback
- image lightbox with keyboard nav (arrows/Esc) in editor and public pages
- inline PDF/video/audio previews
- cover (URL or upload) & page icon endpoints
- fix broken editor API paths (/api/pages -> /board/api/pages) for cover,
  icon, versions, backlinks, import, move and OG metadata
- 47 tests in tests/test_v55.py; full suite 444 green; ruff clean
- version 5.11.2
This commit is contained in:
2026-09-12 09:40:50 -04:00
parent c435e277f2
commit bdc15c7328
13 changed files with 961 additions and 167 deletions
+53 -8
View File
@@ -125,6 +125,18 @@
color: var(--accent);
text-decoration: none;
}
.pub-content img[data-full] { cursor: zoom-in; }
.fd-lightbox { user-select: none; }
.fd-lightbox-nav {
position: absolute; top: 50%; transform: translateY(-50%);
width: 48px; height: 48px; border-radius: 50%; border: none;
background: rgba(255,255,255,.12); color: #fff; font-size: 30px;
line-height: 1; cursor: pointer; display: flex; align-items: center;
justify-content: center; transition: background .15s ease;
}
.fd-lightbox-nav:hover { background: rgba(255,255,255,.25); }
.fd-lightbox-nav[data-nav="prev"] { left: 24px; }
.fd-lightbox-nav[data-nav="next"] { right: 24px; }
</style>
</head>
<body>
@@ -162,15 +174,48 @@
<script src="/static/js/katex.min.js?v=0.16.11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (!window.katex) return;
document.querySelectorAll('div[data-katex]').forEach(function (el) {
var tex = el.getAttribute('data-katex') || '';
try {
el.innerHTML = window.katex.renderToString(tex, { displayMode: true, throwOnError: false });
} catch (e) {
el.textContent = tex;
if (window.katex) {
document.querySelectorAll('div[data-katex]').forEach(function (el) {
var tex = el.getAttribute('data-katex') || '';
try {
el.innerHTML = window.katex.renderToString(tex, { displayMode: true, throwOnError: false });
} catch (e) {
el.textContent = tex;
}
});
}
// v5.5.0 — Image lightbox (fullscreen + keyboard navigation)
var imgs = Array.prototype.slice.call(document.querySelectorAll('.pub-content img[data-full]'));
imgs.forEach(function (im) { im.addEventListener('click', function () { openLb(im); }); });
function openLb(target) {
var idx = imgs.indexOf(target); if (idx < 0) idx = 0;
var ov = document.createElement('div');
ov.className = 'fd-lightbox';
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:5000;display:flex;align-items:center;justify-content:center;flex-direction:column;';
ov.innerHTML = '<button class="fd-lightbox-nav" data-nav="prev" aria-label="Previous">&#8249;</button>'
+ '<img style="max-width:95vw;max-height:88vh;border-radius:4px;box-shadow:0 0 40px rgba(0,0,0,.8);">'
+ '<button class="fd-lightbox-nav" data-nav="next" aria-label="Next">&#8250;</button>'
+ '<div class="fd-lb-hint" style="margin-top:14px;color:#999;font-size:13px;"></div>';
document.body.appendChild(ov);
var img = ov.querySelector('img');
var hint = ov.querySelector('.fd-lb-hint');
function show() {
var cur = imgs[idx];
img.src = cur.getAttribute('data-full') || cur.src;
hint.textContent = (imgs.length > 1 ? (idx + 1) + ' / ' + imgs.length + ' — ' : '') + '← → to navigate · Esc to close';
}
});
function step(d) { if (imgs.length < 2) return; idx = (idx + d + imgs.length) % imgs.length; show(); }
function close() { ov.remove(); document.removeEventListener('keydown', kd); }
function kd(e) { if (e.key === 'Escape') close(); else if (e.key === 'ArrowRight') step(1); else if (e.key === 'ArrowLeft') step(-1); }
document.addEventListener('keydown', kd);
ov.querySelectorAll('[data-nav]').forEach(function (b) {
b.addEventListener('click', function (e) { e.stopPropagation(); step(b.getAttribute('data-nav') === 'next' ? 1 : -1); });
});
ov.addEventListener('click', function (e) { if (e.target === ov) close(); });
show();
}
});
</script>