Files
archicratie-edition/src/components/SidePanel.astro
Archicratia 68c3416594
Some checks failed
CI / build-and-anchors (push) Failing after 2m6s
SMOKE / smoke (push) Successful in 13s
fix: …
2026-02-23 12:07:01 +01:00

1077 lines
32 KiB
Plaintext
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.
---
// src/components/SidePanel.astro
---
<aside class="page-panel" aria-label="Panneau contextuel">
<div class="page-panel__inner">
<div class="panel-head">
<div class="panel-head__left">
<span class="panel-head__label" aria-hidden="true">¶</span>
<code class="panel-head__id" id="panel-para-id">—</code>
</div>
</div>
<div class="panel-msg panel-msg--head" id="panel-head-msg" hidden></div>
{/* ✅ actions medias déplacées en haut (niveau 3 uniquement) */}
<div class="panel-top-actions level-3" aria-label="Actions médias">
<div class="panel-actions">
<button class="panel-btn" id="panel-media-all" type="button">Voir tous les éléments</button>
<button class="panel-btn panel-btn--primary" id="panel-media-submit" type="button">
Soumettre un média (Gitea)
</button>
</div>
<div class="panel-msg" id="panel-media-msg" hidden></div>
</div>
{/* ✅ actions références en haut (niveau 2 uniquement) */}
<div class="panel-top-actions level-2" aria-label="Actions références">
<div class="panel-actions">
<button class="panel-btn panel-btn--primary" id="panel-ref-submit" type="button">
Soumettre une référence (Gitea)
</button>
</div>
<div class="panel-msg" id="panel-ref-msg" hidden></div>
</div>
<section class="panel-block level-2" aria-label="Références et auteurs">
<h2 class="panel-title">Références & auteurs</h2>
<div class="panel-body" id="panel-l2"></div>
</section>
<section class="panel-block level-3" aria-label="Illustrations et diagrammes">
<h2 class="panel-title">Illustrations & diagrammes</h2>
<div class="panel-body" id="panel-l3"></div>
</section>
<section class="panel-block level-4" aria-label="Commentaires">
<h2 class="panel-title">Commentaires</h2>
<div class="panel-body" id="panel-l4"></div>
<div class="panel-compose">
<label class="panel-label" for="panel-comment-text">Nouveau commentaire</label>
<textarea id="panel-comment-text" class="panel-textarea" rows="5" placeholder="Écris ici…"></textarea>
<div class="panel-actions">
<button class="panel-btn panel-btn--primary" id="panel-comment-send" type="button">Envoyer</button>
</div>
<div class="panel-msg" id="panel-comment-msg" hidden></div>
</div>
</section>
</div>
{/* ✅ Lightbox media (pop-up au-dessus du panel) */}
<div class="panel-lightbox" id="panel-lightbox" hidden aria-hidden="true">
<div class="panel-lightbox__overlay" data-close="1"></div>
<div class="panel-lightbox__dialog" role="dialog" aria-modal="true" aria-label="Aperçu du média">
<button class="panel-lightbox__close" type="button" data-close="1" aria-label="Fermer">×</button>
<div class="panel-lightbox__content" id="panel-lightbox-content"></div>
<div class="panel-lightbox__caption" id="panel-lightbox-caption" hidden></div>
</div>
</div>
</aside>
<script is:inline>
(() => {
const root = document.querySelector(".page-panel");
if (!root) return;
// Anti double-init (HMR / script injecté 2x / re-mount)
if (root.dataset.archiSidePanelInit === "1") return;
root.dataset.archiSidePanelInit = "1";
const pageKey = String(location.pathname || "").replace(/^\/+/, "").replace(/\/+$/, "");
const elId = root.querySelector("#panel-para-id");
const elL2 = root.querySelector("#panel-l2");
const elL3 = root.querySelector("#panel-l3");
const elL4 = root.querySelector("#panel-l4");
const msgHead = root.querySelector("#panel-head-msg");
const btnMediaAll = root.querySelector("#panel-media-all");
const btnMediaSubmit = root.querySelector("#panel-media-submit");
const msgMedia = root.querySelector("#panel-media-msg");
const taComment = root.querySelector("#panel-comment-text");
const btnSend = root.querySelector("#panel-comment-send");
const msgComment = root.querySelector("#panel-comment-msg");
const lb = root.querySelector("#panel-lightbox");
const lbContent = root.querySelector("#panel-lightbox-content");
const lbCaption = root.querySelector("#panel-lightbox-caption");
const btnRefSubmit = root.querySelector("#panel-ref-submit");
const msgRef = root.querySelector("#panel-ref-msg");
const docTitle = document.body?.dataset?.docTitle || document.title || "Archicratie";
const docVersion = document.body?.dataset?.docVersion || "";
const FULL_TEXT_SOFT_LIMIT = 1200;
const URL_HARD_LIMIT = 6500;
let _idxP = null;
let currentParaId = "";
let mediaShowAll = (localStorage.getItem("archicratie:panel:mediaAll") === "1");
// ===== globals =====
function getG() {
return window.__archiGitea || { ready: false, base: "", owner: "", repo: "" };
}
function getAuthInfoP() {
return window.__archiAuthInfoP || Promise.resolve({ ok: false, groups: [] });
}
function isDev() {
return Boolean((window.__archiFlags && window.__archiFlags.dev) || /^(localhost|127\.0\.0\.1|\[::1\])$/i.test(location.hostname));
}
const access = { ready: false, canUsers: false };
// On verrouille les boutons tant que laccès nest pas résolu
if (btnMediaSubmit) btnMediaSubmit.disabled = true;
if (btnSend) btnSend.disabled = true;
if (btnRefSubmit) btnRefSubmit.disabled = true;
function inGroup(groups, g) {
const gg = String(g || "").toLowerCase();
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
}
// ✅ règle mission : readers + editors peuvent soumettre médias + commentaires
// ✅ dev fallback : si /_auth/whoami nexiste pas, on autorise pour tester
getAuthInfoP().then((info) => {
const groups = Array.isArray(info?.groups) ? info.groups : [];
const canReaders = inGroup(groups, "readers");
const canEditors = inGroup(groups, "editors");
const whoamiSkipped = Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped);
access.canUsers = Boolean((info?.ok && (canReaders || canEditors)) || whoamiSkipped);
access.ready = true;
if (btnMediaSubmit) btnMediaSubmit.disabled = !access.canUsers;
if (btnSend) btnSend.disabled = !access.canUsers;
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
// si pas d'accès, on informe (soft)
if (!access.canUsers) {
if (msgHead) {
msgHead.hidden = false;
msgHead.textContent = "Connexion requise (readers ou editors) pour soumettre des médias/commentaires.";
msgHead.dataset.kind = "warn";
}
}
}).catch(() => {
// fallback dev (cohérent: media + ref + comment)
access.ready = true;
if (Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped)) {
access.canUsers = true;
if (btnMediaSubmit) btnMediaSubmit.disabled = false;
if (btnSend) btnSend.disabled = false;
if (btnRefSubmit) btnRefSubmit.disabled = false;
}
});
function esc(s) { return String(s ?? ""); }
function clear(el) {
if (!el) return;
while (el.firstChild) el.removeChild(el.firstChild);
}
function pText(txt) {
const p = document.createElement("p");
p.textContent = txt;
return p;
}
function link(url, label) {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.textContent = label || url;
return a;
}
function showMsg(el, text, kind = "info") {
if (!el) return;
el.hidden = false;
el.textContent = text;
el.dataset.kind = kind;
}
function hideMsg(el) {
if (!el) return;
el.hidden = true;
el.textContent = "";
el.dataset.kind = "";
}
async function loadIndex() {
if (_idxP) return _idxP;
_idxP = (async () => {
try {
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" });
if (res && res.ok) return await res.json();
} catch {}
// ✅ antifragile: ne pas “cacher” un échec pour toujours (dev/HMR/boot race)
_idxP = null;
return null;
})();
return _idxP;
}
function getParaText(paraId) {
try {
const el = document.getElementById(paraId);
if (!el) return "";
const clone = el.cloneNode(true);
clone.querySelectorAll(".para-tools,[data-noquote]").forEach((n) => n.remove());
return String(clone.innerText || clone.textContent || "").replace(/\s+/g, " ").trim();
} catch { return ""; }
}
function quoteBlock(s) {
return String(s || "")
.split(/\r?\n/)
.map((l) => (`> ${l}`).trimEnd())
.join("\n");
}
function buildIssueURL({ title, body }) {
const g = getG();
if (!g.ready) return "";
const base = String(g.base).replace(/\/+$/, "");
const issue = new URL(`${base}/${g.owner}/${g.repo}/issues/new`);
issue.searchParams.set("title", title);
let b = String(body || "");
if (b.length > 9000) b = b.slice(0, 9000) + "\n\n(…troncature automatique…)";
issue.searchParams.set("body", b);
if (issue.toString().length > URL_HARD_LIMIT) {
const shortBody = b.slice(0, 1800) + "\n\n(…troncature pour limite URL…)";
issue.searchParams.set("body", shortBody);
}
return issue.toString();
}
// Ouvre un nouvel onglet UNE SEULE FOIS (évite le double-open Safari/Firefox + noopener).
function openNewTab(url) {
try {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
return true; // on ne peut pas détecter proprement un blocage sans retomber dans le double-open
} catch {
return false;
}
}
// ====== GARDES ANTI-DOUBLONS ======
const _openStamp = new Map();
function openOnce(key, fn) {
const now = Date.now();
const prev = _openStamp.get(key) || 0;
if (now - prev < 700) return false;
_openStamp.set(key, now);
return Boolean(fn());
}
function guardEventOnce(ev, key) {
try {
const k = "__archiOnce_" + key;
if (ev && ev[k]) return true;
if (ev) ev[k] = true;
try { ev.stopImmediatePropagation(); } catch {}
try { ev.stopPropagation(); } catch {}
} catch {}
return false;
}
function bindClickOnce(el, handler) {
if (!el) return;
if (el.dataset.bound === "1") return;
el.dataset.bound = "1";
el.addEventListener("click", handler, { passive: false });
}
// ===== Lightbox =====
function closeLightbox() {
if (!lb) return;
lb.hidden = true;
lb.setAttribute("aria-hidden", "true");
if (lbContent) clear(lbContent);
if (lbCaption) { lbCaption.hidden = true; lbCaption.textContent = ""; }
}
function openLightbox({ type, src, caption }) {
if (!lb || !lbContent) return;
clear(lbContent);
const t = String(type || "link");
const s = String(src || "");
const cap = String(caption || "").trim();
if (!s) return;
if (t === "image") {
const img = document.createElement("img");
img.loading = "eager";
img.alt = cap || "Illustration";
img.src = s;
lbContent.appendChild(img);
} else if (t === "video") {
const v = document.createElement("video");
v.controls = true;
v.autoplay = false;
v.src = s;
lbContent.appendChild(v);
} else if (t === "audio") {
const a = document.createElement("audio");
a.controls = true;
a.src = s;
lbContent.appendChild(a);
} else {
const a = link(s, cap || s);
a.className = "panel-lightbox__link";
lbContent.appendChild(a);
}
if (lbCaption) {
if (cap) { lbCaption.hidden = false; lbCaption.textContent = cap; }
else { lbCaption.hidden = true; lbCaption.textContent = ""; }
}
lb.hidden = false;
lb.setAttribute("aria-hidden", "false");
}
if (lb) {
bindClickOnce(lb, (ev) => {
const t = ev.target;
if (t && t.getAttribute && t.getAttribute("data-close") === "1") {
ev.preventDefault();
closeLightbox();
}
});
window.addEventListener("keydown", (ev) => {
if (ev.key === "Escape" && !lb.hidden) closeLightbox();
});
}
// ===== Renders =====
function renderLevel2(data) {
clear(elL2);
if (!elL2) return;
if (!data) {
elL2.appendChild(pText("Aucune annotation pour ce paragraphe."));
return;
}
if (Array.isArray(data.authors) && data.authors.length) {
const h = document.createElement("h3");
h.className = "panel-subtitle";
h.textContent = "Auteurs";
elL2.appendChild(h);
const ul = document.createElement("ul");
ul.className = "panel-list";
for (const a of data.authors) {
const li = document.createElement("li");
li.textContent = esc(a);
ul.appendChild(li);
}
elL2.appendChild(ul);
}
if (Array.isArray(data.refs) && data.refs.length) {
const h = document.createElement("h3");
h.className = "panel-subtitle";
h.textContent = "Références";
elL2.appendChild(h);
const ul = document.createElement("ul");
ul.className = "panel-list";
for (const r of data.refs) {
const li = document.createElement("li");
const label = esc(r?.label || r?.url || "Référence");
const url = esc(r?.url || "");
const kind = esc(r?.kind || "");
if (url) li.appendChild(link(url, label));
else li.textContent = label;
if (kind) {
const k = document.createElement("span");
k.className = "panel-chip";
k.textContent = kind;
li.appendChild(document.createTextNode(" "));
li.appendChild(k);
}
ul.appendChild(li);
}
elL2.appendChild(ul);
}
if (Array.isArray(data.quotes) && data.quotes.length) {
const h = document.createElement("h3");
h.className = "panel-subtitle";
h.textContent = "Citations";
elL2.appendChild(h);
for (const q of data.quotes) {
const block = document.createElement("blockquote");
block.className = "panel-quote";
const t = document.createElement("div");
t.textContent = esc(q?.text || "");
block.appendChild(t);
const src = esc(q?.source || "");
if (src) {
const s = document.createElement("div");
s.className = "panel-quote__src";
s.textContent = src;
block.appendChild(s);
}
elL2.appendChild(block);
}
}
if (!elL2.firstChild) {
elL2.appendChild(pText("Aucune annotation pour ce paragraphe."));
}
}
function renderLevel3(data) {
clear(elL3);
hideMsg(msgMedia);
if (!elL3) return;
const arr = Array.isArray(data?.media) ? data.media : [];
if (!arr.length) {
elL3.appendChild(pText("Aucun média associé à ce paragraphe."));
if (btnMediaAll) btnMediaAll.disabled = true;
return;
}
if (btnMediaAll) btnMediaAll.disabled = arr.length <= 6;
const grid = document.createElement("div");
grid.className = "panel-media-grid";
const items = mediaShowAll ? arr : arr.slice(0, 6);
for (const m of items) {
const type = esc(m?.type || "link");
const src = esc(m?.src || "");
const caption = esc(m?.caption || "");
const credit = esc(m?.credit || "");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "panel-media-tile";
btn.setAttribute("aria-label", caption ? `Ouvrir média: ${caption}` : "Ouvrir média");
btn.dataset.src = src;
btn.dataset.type = type;
btn.dataset.caption = caption;
if (type === "image" && src) {
const img = document.createElement("img");
img.loading = "lazy";
img.alt = caption || "Illustration";
img.src = src;
btn.appendChild(img);
} else {
const ph = document.createElement("div");
ph.className = "panel-media-ph";
ph.textContent = type.toUpperCase();
btn.appendChild(ph);
}
if (caption) {
const c = document.createElement("div");
c.className = "panel-media-cap";
c.textContent = caption;
btn.appendChild(c);
} else if (src) {
const c = document.createElement("div");
c.className = "panel-media-cap";
c.textContent = src.split("/").pop() || src;
btn.appendChild(c);
}
if (credit) {
const cr = document.createElement("div");
cr.className = "panel-media-credit";
cr.textContent = credit;
btn.appendChild(cr);
}
bindClickOnce(btn, (ev) => {
ev.preventDefault();
if (!src) return;
openLightbox({ type, src, caption });
});
grid.appendChild(btn);
}
elL3.appendChild(grid);
}
function renderLevel4(data) {
clear(elL4);
hideMsg(msgComment);
if (!elL4) return;
const arr = Array.isArray(data?.comments_editorial) ? data.comments_editorial : [];
if (!arr.length) {
elL4.appendChild(pText("Aucun commentaire éditorial pour ce paragraphe."));
return;
}
const ul = document.createElement("ul");
ul.className = "panel-list";
for (const c of arr) {
const li = document.createElement("li");
li.className = "panel-comment";
const txt = document.createElement("div");
txt.textContent = esc(c?.text || "");
li.appendChild(txt);
const st = esc(c?.status || "");
if (st) {
const s = document.createElement("span");
s.className = "panel-chip";
s.textContent = st;
li.appendChild(s);
}
ul.appendChild(li);
}
elL4.appendChild(ul);
}
async function updatePanel(paraId) {
currentParaId = paraId || currentParaId || "";
if (elId) elId.textContent = currentParaId || "—";
hideMsg(msgHead);
hideMsg(msgMedia);
hideMsg(msgComment);
const idx = await loadIndex();
// ✅ message soft si lindex est indisponible (sans écraser le message dauth)
if (!idx && msgHead && msgHead.hidden) {
msgHead.hidden = false;
msgHead.textContent = "Index annotations indisponible (annotations-index.json).";
msgHead.dataset.kind = "info";
}
const data = idx?.pages?.[pageKey]?.paras?.[currentParaId] || null;
renderLevel2(data);
renderLevel3(data);
renderLevel4(data);
}
// ===== media "voir tous" =====
if (btnMediaAll) {
bindClickOnce(btnMediaAll, (ev) => {
ev.preventDefault();
mediaShowAll = !mediaShowAll;
localStorage.setItem("archicratie:panel:mediaAll", mediaShowAll ? "1" : "0");
btnMediaAll.textContent = mediaShowAll ? "Réduire la liste" : "Voir tous les éléments";
updatePanel(currentParaId);
});
btnMediaAll.textContent = mediaShowAll ? "Réduire la liste" : "Voir tous les éléments";
}
// ===== media submit (readers + editors) =====
if (btnMediaSubmit) {
bindClickOnce(btnMediaSubmit, (ev) => {
ev.preventDefault();
hideMsg(msgMedia);
if (guardEventOnce(ev, "gitea_open_media")) return;
if (!currentParaId) return showMsg(msgMedia, "Choisis dabord un paragraphe (scroll / survol).", "warn");
if (!getG().ready) return showMsg(msgMedia, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
if (btnMediaSubmit.disabled) return showMsg(msgMedia, "Connexion requise (readers/editors).", "error");
const pageUrl = new URL(location.href);
pageUrl.search = "";
pageUrl.hash = currentParaId;
const paraTxt = getParaText(currentParaId);
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
const title = `[Media] ${currentParaId} — ${docTitle}`;
const body = [
`Chemin: ${location.pathname}`,
`URL: ${pageUrl.toString()}`,
`Ancre: #${currentParaId}`,
`Version: ${docVersion || "(non renseignée)"}`,
`Type: type/media`,
``,
`Contexte (extrait):`,
quoteBlock(excerpt || ""),
``,
`---`,
`Action: dans Gitea, ajoute le fichier via la zone “Joindre un fichier” (drag & drop / upload).`,
`But: associer le média au paragraphe #${currentParaId}.`,
].join("\n");
const url = buildIssueURL({ title, body });
if (!url) return showMsg(msgMedia, "Impossible de générer lissue (config).", "error");
const ok = openOnce(`media:${currentParaId}`, () => openNewTab(url));
if (!ok) showMsg(msgMedia, "Popup bloqué : autorise les popups pour ouvrir Gitea.", "error");
});
}
// ===== référence submit (readers + editors) =====
if (btnRefSubmit) {
bindClickOnce(btnRefSubmit, (ev) => {
ev.preventDefault();
hideMsg(msgRef);
if (guardEventOnce(ev, "gitea_open_ref")) return;
if (!currentParaId) return showMsg(msgRef, "Choisis dabord un paragraphe (scroll / survol).", "warn");
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
const pageUrl = new URL(location.href);
pageUrl.search = "";
pageUrl.hash = currentParaId;
const paraTxt = getParaText(currentParaId);
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
const title = `[Reference] ${currentParaId} — ${docTitle}`;
const body = [
`Chemin: ${location.pathname}`,
`URL: ${pageUrl.toString()}`,
`Ancre: #${currentParaId}`,
`Version: ${docVersion || "(non renseignée)"}`,
`Type: type/reference`,
``,
`Contexte (extrait):`,
quoteBlock(excerpt || ""),
``,
`Référence (à compléter):`,
`- URL:`,
`- Label:`,
`- Kind: (livre / article / vidéo / site / autre)`,
`- Citation / passage (optionnel):`,
``,
`---`,
`Note: issue générée depuis le site (pré-remplissage).`,
].join("\n");
const url = buildIssueURL({ title, body });
if (!url) return showMsg(msgRef, "Impossible de générer lissue.", "error");
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
if (!ok) showMsg(msgRef, "Si rien ne souvre : autorise les popups pour ce site.", "error");
});
}
// ===== commentaire (readers + editors) =====
if (btnSend) {
bindClickOnce(btnSend, (ev) => {
ev.preventDefault();
hideMsg(msgComment);
if (guardEventOnce(ev, "gitea_open_comment")) return;
if (!currentParaId) return showMsg(msgComment, "Choisis dabord un paragraphe (scroll / survol).", "warn");
if (!getG().ready) return showMsg(msgComment, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
if (btnSend.disabled) return showMsg(msgComment, "Connexion requise (readers/editors).", "error");
const txt = String(taComment?.value || "").trim();
if (txt.length < 3) return showMsg(msgComment, "Commentaire trop court.", "warn");
const pageUrl = new URL(location.href);
pageUrl.search = "";
pageUrl.hash = currentParaId;
const paraTxt = getParaText(currentParaId);
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
const title = `[Comment] ${currentParaId} — ${docTitle}`;
const body = [
`Chemin: ${location.pathname}`,
`URL: ${pageUrl.toString()}`,
`Ancre: #${currentParaId}`,
`Version: ${docVersion || "(non renseignée)"}`,
`Type: type/comment`,
``,
`Contexte (extrait):`,
quoteBlock(excerpt || ""),
``,
`Commentaire:`,
txt,
``,
`---`,
`Note: issue générée depuis le site (pré-remplissage).`,
].join("\n");
const url = buildIssueURL({ title, body });
if (!url) return showMsg(msgComment, "Impossible de générer lissue.", "error");
const ok = openOnce(`comment:${currentParaId}`, () => openNewTab(url));
if (!ok) return showMsg(msgComment, "Popup bloqué : autorise les popups pour ouvrir Gitea.", "error");
taComment.value = "";
showMsg(msgComment, "Issue Gitea ouverte dans un nouvel onglet.", "ok");
setTimeout(() => hideMsg(msgComment), 900);
});
}
// ===== wiring: para courant (aligné sur le paragraphe sous le reading-follow) =====
function isPara(el) {
return Boolean(el && el.nodeType === 1 && el.matches && el.matches('.reading p[id^="p-"]'));
}
function pickParaAtY(y) {
const x = Math.max(0, Math.round(window.innerWidth * 0.5));
const candidates = [
document.elementFromPoint(x, y),
document.elementFromPoint(Math.min(window.innerWidth - 1, x + 60), y),
document.elementFromPoint(Math.max(0, x - 60), y),
].filter(Boolean);
for (const c of candidates) {
if (isPara(c)) return c;
const p = c.closest ? c.closest('.reading p[id^="p-"]') : null;
if (isPara(p)) return p;
}
return null;
}
let _lastPicked = "";
function syncFromFollowLine() {
const off = Number(document.documentElement.style.getPropertyValue("--sticky-offset-px")) || 0;
const y = Math.round(off + 8);
const p = pickParaAtY(y);
if (!p || !p.id) return;
if (p.id === _lastPicked) return;
_lastPicked = p.id;
// met à jour l'app global (EditionLayout écoute déjà currentPara)
try { window.dispatchEvent(new CustomEvent("archicratie:currentPara", { detail: { id: p.id } })); } catch {}
// et met à jour le panel immédiatement (sans attendre)
updatePanel(p.id);
}
let ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
syncFromFollowLine();
});
}
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
// Initial: hash > sinon calc
const initial = String(location.hash || "").replace(/^#/, "");
if (/^p-\d+-/i.test(initial)) updatePanel(initial);
else setTimeout(() => { try { syncFromFollowLine(); } catch {} }, 0);
})();
</script>
<style>
/* Fallback : niveau 1 → jamais de panel */
:global(body[data-reading-level="1"]) .page-panel{ display: none !important; }
.page-panel{
grid-column: 3;
position: sticky;
top: calc(var(--sticky-header-h) + var(--page-gap));
align-self: start;
}
:global(body[data-reading-level="3"]) .page-panel{
grid-column: 2;
}
.page-panel__inner{
max-height: calc(100vh - (var(--sticky-header-h) + var(--page-gap) + 12px));
overflow: auto;
scrollbar-gutter: stable;
border: 1px solid rgba(127,127,127,.22);
border-radius: 16px;
padding: 12px;
background: rgba(127,127,127,0.04);
}
.panel-head{
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed rgba(127,127,127,0.25);
}
.panel-head__left{
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.panel-head__label{ font-weight: 900; opacity: .8; }
.panel-head__id{
font-weight: 850;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 14rem;
}
.panel-msg{
margin-top: 8px;
font-size: 12px;
opacity: .95;
}
.panel-msg--head{
margin-top: 0;
margin-bottom: 10px;
}
.panel-title{
font-size: 13px;
font-weight: 900;
margin: 10px 0 8px;
opacity: .9;
}
.panel-subtitle{
font-size: 12px;
font-weight: 850;
margin: 10px 0 6px;
opacity: .88;
}
.panel-body p{ margin: 8px 0; opacity: .9; }
.panel-list{ margin: 0; padding-left: 18px; }
.panel-list li{ margin: 6px 0; }
.panel-chip{
display: inline-block;
margin-left: 8px;
font-size: 11px;
font-weight: 800;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid rgba(127,127,127,0.30);
background: rgba(127,127,127,0.10);
opacity: .92;
}
.panel-quote{
margin: 8px 0;
padding: 8px 10px;
border-left: 3px solid rgba(127,127,127,0.35);
background: rgba(127,127,127,0.06);
border-radius: 10px;
}
.panel-quote__src{ margin-top: 6px; font-size: 12px; opacity: .85; }
.panel-actions{
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.panel-btn{
border: 1px solid rgba(127,127,127,0.40);
background: rgba(127,127,127,0.10);
border-radius: 999px;
padding: 6px 10px;
font-size: 13px;
cursor: pointer;
}
.panel-btn:disabled{ opacity: .55; cursor: default; }
.panel-btn--primary{
border-color: rgba(127,127,127,0.65);
font-weight: 800;
}
/* actions médias en haut */
.panel-top-actions{ margin-top: 8px; }
/* ===== media thumbnails (150x150) ===== */
.panel-media-grid{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.panel-media-tile{
width: 150px;
max-width: 100%;
border: 1px solid rgba(127,127,127,.20);
border-radius: 14px;
padding: 8px;
background: rgba(127,127,127,0.04);
cursor: pointer;
text-align: left;
}
.panel-media-tile img{
width: 150px;
height: 150px;
max-width: 100%;
object-fit: cover;
display: block;
border-radius: 10px;
margin-bottom: 8px;
}
.panel-media-ph{
width: 150px;
height: 150px;
border-radius: 10px;
display: grid;
place-items: center;
background: rgba(127,127,127,0.10);
margin-bottom: 8px;
font-weight: 900;
font-size: 12px;
opacity: .9;
}
.panel-media-cap{
font-size: 12px;
font-weight: 800;
opacity: .92;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.panel-media-credit{
margin-top: 4px;
font-size: 11px;
opacity: .8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ===== compose ===== */
.panel-compose{ margin-top: 12px; padding-top: 10px; border-top: 1px dashed rgba(127,127,127,0.25); }
.panel-label{ display: block; font-size: 12px; font-weight: 900; opacity: .9; margin-bottom: 6px; }
.panel-textarea{
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(127,127,127,0.35);
border-radius: 12px;
padding: 8px 10px;
background: transparent;
font-size: 13px;
resize: vertical;
}
/* ===== Lightbox ===== */
.panel-lightbox{
position: fixed;
inset: 0;
z-index: 120;
}
.panel-lightbox__overlay{
position: absolute;
inset: 0;
background: rgba(0,0,0,0.80);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.panel-lightbox__dialog{
position: absolute;
right: 24px;
top: calc(var(--sticky-header-h) + 16px);
width: min(520px, calc(100vw - 48px));
max-height: calc(100vh - (var(--sticky-header-h) + 32px));
overflow: auto;
border: 1px solid rgba(127,127,127,0.22);
border-radius: 16px;
background: rgba(255,255,255,0.10);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 12px;
}
@media (prefers-color-scheme: dark){
.panel-lightbox__dialog{
background: rgba(0,0,0,0.28);
}
}
.panel-lightbox__close{
position: sticky;
top: 0;
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 30px;
border-radius: 10px;
border: 1px solid rgba(127,127,127,0.35);
background: rgba(127,127,127,0.10);
cursor: pointer;
font-size: 18px;
font-weight: 900;
}
.panel-lightbox__content img,
.panel-lightbox__content video{
display: block;
width: 100%;
height: auto;
max-width: 1400px;
margin: 0 auto;
border-radius: 12px;
}
.panel-lightbox__content audio{
width: 100%;
}
.panel-lightbox__caption{
margin-top: 10px;
font-size: 12px;
font-weight: 800;
opacity: .92;
}
@media (max-width: 1100px){
.page-panel{ display: none; }
}
</style>