1077 lines
32 KiB
Plaintext
1077 lines
32 KiB
Plaintext
---
|
||
// 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 l’accès n’est 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 n’existe 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 l’index est indisponible (sans écraser le message d’auth)
|
||
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 d’abord 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 l’issue (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 d’abord 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 l’issue.", "error");
|
||
|
||
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
|
||
if (!ok) showMsg(msgRef, "Si rien ne s’ouvre : 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 d’abord 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 l’issue.", "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>
|