Files
archicratie-edition/src/layouts/EditionLayout.astro
Archicratia 81baadd57f
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 36s
style(mobile): widen reading in landscape on small screens (css-only)
2026-03-04 17:36:56 +01:00

1635 lines
51 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.
---
import ProposeModal from "../components/ProposeModal.astro";
import SiteNav from "../components/SiteNav.astro";
import LevelToggle from "../components/LevelToggle.astro";
import BuildStamp from "../components/BuildStamp.astro";
import SidePanel from "../components/SidePanel.astro";
import "../styles/global.css";
import "../styles/panel.css";
const {
title,
editionLabel,
editionKey,
statusLabel,
statusKey,
level,
version,
} = Astro.props;
const lvl = level ?? 1;
const canonical = Astro.site
? new URL(Astro.url.pathname, Astro.site).href
: Astro.url.href;
const IS_DEV = import.meta.env.DEV;
const GITEA_BASE = import.meta.env.PUBLIC_GITEA_BASE ?? "";
const GITEA_OWNER = import.meta.env.PUBLIC_GITEA_OWNER ?? "";
const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
// ✅ OPTIONNEL : bridge serveur (proxy same-origin)
const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
// ✅ Auth whoami (same-origin) — configurable, antifragile en dev
const WHOAMI_PATH = import.meta.env.PUBLIC_WHOAMI_PATH ?? "/_auth/whoami";
// Par défaut: en DEV local on SKIP pour éviter le spam 404.
// Pour tester lauth en dev: export PUBLIC_WHOAMI_IN_DEV=1
const WHOAMI_IN_DEV = (import.meta.env.PUBLIC_WHOAMI_IN_DEV ?? "") === "1";
const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ?? "") === "1";
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title ? `${title} — Archicratie` : "Archicratie"}</title>
<link rel="canonical" href={canonical} />
<link rel="sitemap" href="/sitemap-index.xml" />
<meta data-pagefind-filter="edition[content]" content={String(editionKey ?? editionLabel)} />
<meta data-pagefind-filter="level[content]" content={String(lvl)} />
<meta data-pagefind-filter="status[content]" content={String(statusKey ?? statusLabel)} />
<meta data-pagefind-meta={`edition:${String(editionKey ?? editionLabel)}`} />
<meta data-pagefind-meta={`level:${String(lvl)}`} />
<meta data-pagefind-meta={`status:${String(statusKey ?? statusLabel)}`} />
<meta data-pagefind-meta={`version:${String(version ?? "")}`} />
{/* ✅ BOOT EARLY : SidePanel dépend de ces globals. */}
<script
is:inline
define:vars={{
IS_DEV,
GITEA_BASE,
GITEA_OWNER,
GITEA_REPO,
ISSUE_BRIDGE_PATH,
WHOAMI_PATH,
WHOAMI_IN_DEV,
WHOAMI_FORCE_LOCALHOST,
}}
>
(() => {
// ✅ anti double-init (HMR / inclusion accidentelle)
if (window.__archiBootOnce === 1) return;
window.__archiBootOnce = 1;
var __DEV__ = Boolean(IS_DEV);
// ===== Gitea globals =====
var base = String(GITEA_BASE || "").replace(/\/+$/, "");
var owner = String(GITEA_OWNER || "");
var repo = String(GITEA_REPO || "");
window.__archiGitea = {
ready: Boolean(base && owner && repo),
base, owner, repo
};
// ===== optional issue bridge (same-origin proxy) =====
var rawBridge = String(ISSUE_BRIDGE_PATH || "").trim();
var normBridge = rawBridge
? (rawBridge.startsWith("/") ? rawBridge : ("/" + rawBridge.replace(/^\/+/, ""))).replace(/\/+$/, "")
: "";
window.__archiIssueBridge = { ready: Boolean(normBridge), path: normBridge };
// ===== whoami config =====
var __WHOAMI_PATH__ = String(WHOAMI_PATH || "/_auth/whoami");
var __WHOAMI_IN_DEV__ = Boolean(WHOAMI_IN_DEV);
// En dev: par défaut on SKIP (=> pas de spam 404). Override via PUBLIC_WHOAMI_IN_DEV=1.
var SHOULD_FETCH_WHOAMI = (!__DEV__) || __WHOAMI_IN_DEV__;
window.__archiFlags = Object.assign({}, window.__archiFlags, {
dev: __DEV__,
whoamiPath: __WHOAMI_PATH__,
whoamiInDev: __WHOAMI_IN_DEV__,
whoamiFetch: SHOULD_FETCH_WHOAMI,
});
var REQUIRED_GROUP = "editors";
var READ_GROUP = "readers";
function parseWhoamiLine(text, key) {
var re = new RegExp("^" + key + ":\\s*(.*)$", "mi");
var m = String(text || "").match(re);
return (m && m[1] ? m[1] : "").trim();
}
function inGroup(groups, g) {
var gg = String(g || "").toLowerCase();
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
}
// ===== Auth info promise (single source of truth) =====
if (!window.__archiAuthInfoP) {
window.__archiAuthInfoP = (async () => {
// ✅ dev default: skip
if (!SHOULD_FETCH_WHOAMI) {
return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
}
var res = null;
try {
res = await fetch(__WHOAMI_PATH__ + "?_=" + Date.now(), {
credentials: "include",
cache: "no-store",
redirect: "manual",
headers: { Accept: "text/plain" },
});
} catch {
res = null;
}
if (!res) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
if (res.type === "opaqueredirect") return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
if (res.status >= 300 && res.status < 400) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
if (res.status === 404) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
var text = "";
try { text = await res.text(); } catch { text = ""; }
var looksLikeWhoami = /Remote-(User|Groups|Email|Name)\s*:/i.test(text);
if (!res.ok || !looksLikeWhoami) {
return { ok: false, user: "", name: "", email: "", groups: [], raw: text };
}
var groups = parseWhoamiLine(text, "Remote-Groups")
.split(/[;,]/)
.map((s) => s.trim())
.filter(Boolean)
.map((s) => s.toLowerCase());
return {
ok: true,
user: parseWhoamiLine(text, "Remote-User"),
name: parseWhoamiLine(text, "Remote-Name"),
email: parseWhoamiLine(text, "Remote-Email"),
groups,
raw: text,
};
})().catch(() => ({ ok: false, user: "", name: "", email: "", groups: [], raw: "" }));
}
// readers + editors (strict)
if (!window.__archiCanReadP) {
window.__archiCanReadP = window.__archiAuthInfoP.then((info) =>
Boolean(info && info.ok && (inGroup(info.groups, READ_GROUP) || inGroup(info.groups, REQUIRED_GROUP)))
);
}
// editors gate for "Proposer"
if (!window.__archiIsEditorP) {
window.__archiIsEditorP = window.__archiAuthInfoP
// ✅ DEV fallback: si whoami absent/KO => Proposer autorisé (comme ton intention initiale)
.then((info) => Boolean(inGroup(info.groups, REQUIRED_GROUP) || (__DEV__ && !(info && info.ok))))
.catch(() => Boolean(__DEV__));
}
})();
</script>
</head>
<body
data-doc-title={title}
data-doc-version={version}
data-reading-level={String(lvl)}
>
<header>
<SiteNav />
<div class="edition-bar">
<span class="badge" data-badge="edition"><strong>Édition</strong> : {editionLabel}</span>
<span class="badge" data-badge="status"><strong>Statut</strong> : {statusLabel}</span>
<span class="badge" data-badge="version"><strong>Version</strong> : {version}</span>
<a class="resume-btn" id="resume-btn" href="#" hidden>Reprendre la lecture</a>
<form class="jump-form" id="jump-form" role="search" aria-label="Aller à un paragraphe par identifiant">
<input
class="jump-input"
id="jump-input"
type="search"
inputmode="search"
autocomplete="off"
spellcheck="false"
placeholder="Aller à ¶ (p-… / URL#p-… / extrait)"
aria-label="Identifiant ou extrait de paragraphe"
/>
<button class="jump-btn" id="jump-go" type="submit">Aller</button>
</form>
<LevelToggle initialLevel={lvl} />
</div>
</header>
<main class="page">
<div class="page-shell">
<aside class="page-aside" aria-label="Navigation de lecture">
<div class="page-aside__scroll" aria-label="Sommaires">
<slot name="aside" />
</div>
</aside>
<article class="reading" data-pagefind-body>
<slot />
<BuildStamp />
</article>
<SidePanel />
</div>
</main>
<div class="reading-follow" id="reading-follow" aria-hidden="true">
<div class="reading-follow__inner">
<button class="rf-line rf-h1" id="rf-h1" type="button" hidden></button>
<button class="rf-line rf-h2" id="rf-h2" type="button" hidden></button>
<button class="rf-line rf-h3" id="rf-h3" type="button" hidden></button>
<div class="rf-actions" aria-label="Navigation rapide">
<button class="rf-btn" id="rf-top-chapter" type="button" hidden aria-label="Haut du chapitre" title="Haut du chapitre">↑</button>
<button class="rf-btn" id="rf-top-section" type="button" hidden aria-label="Haut de la section" title="Haut de la section">↥</button>
</div>
</div>
</div>
<ProposeModal />
<style>
.page{
padding: var(--page-gap) 16px 48px;
}
/* ✅ Grille robustifiée + panel plus confortable (base) */
.page-shell{
--aside-w: 320px;
--reading-w: 78ch;
--panel-w: 420px;
--gap: 18px;
max-width: min(
calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2)),
calc(100vw - 32px)
);
margin: 0 auto;
display: grid;
grid-template-columns:
var(--aside-w)
minmax(0, var(--reading-w))
minmax(0, var(--panel-w));
column-gap: var(--gap);
align-items: start;
}
.page-aside{
grid-column: 1;
position: sticky;
top: calc(var(--sticky-header-h) + var(--page-gap));
overflow: visible;
max-height: none;
}
.page-aside__scroll{
max-height: calc(100vh - (var(--sticky-header-h) + var(--page-gap) + 12px));
overflow: auto;
padding-right: 8px;
padding-top: 6px;
scrollbar-gutter: stable;
}
/* ✅ Mode “lecture continue” :
- on garde <details> pour les ancres / JS
- mais on supprime lUX accordéon (summary) et on montre le vrai H2
*/
:global(.reading details.details-section){
border: 0 !important;
padding: 0 !important;
margin: 22px 0 !important;
background: transparent !important;
}
/* summary invisible (plus de “Ouvrir la section…”) */
:global(.reading details.details-section > summary.details-summary){
position: absolute !important;
left: -9999px !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
white-space: nowrap !important;
border: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/* pas de “gap” artificiel ajouté après le summary */
:global(.reading details .details-body){
margin-top: 0 !important;
}
/* le vrai H2 redevient visible */
:global(.reading details .details-body > h2:first-child){
position: static !important;
left: auto !important;
width: auto !important;
height: auto !important;
overflow: visible !important;
white-space: normal !important;
}
.reading{
grid-column: 2;
max-width: none;
margin: 0;
}
/* Niveau 1 : panel OFF + lecture large */
:global(body[data-reading-level="1"]) .page-shell{
--panel-w: 0px;
max-width: min(1560px, calc(100vw - 32px));
grid-template-columns: var(--aside-w) minmax(0, 1fr);
}
:global(body[data-reading-level="1"]) .page-panel{ display: none !important; }
/* Niveau 3 : lecture plus étroite + panel LARGE ; TOC masqué */
:global(body[data-reading-level="3"]) .page-shell{
--reading-w: 60ch;
--panel-w: clamp(720px, 48vw, 940px);
max-width: min(
calc(var(--reading-w) + var(--panel-w) + var(--gap)),
calc(100vw - 32px)
);
grid-template-columns:
minmax(0, var(--reading-w))
minmax(0, var(--panel-w));
}
:global(body[data-reading-level="3"]) .page-aside{ display: none; }
:global(body[data-reading-level="3"]) .reading{ grid-column: 1; }
/* Niveau 4 : TOC à gauche + panel confortable */
:global(body[data-reading-level="4"]) .page-shell{
--aside-w: 320px;
--reading-w: 64ch;
--panel-w: clamp(520px, 36vw, 720px);
max-width: min(
calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2)),
calc(100vw - 32px)
);
grid-template-columns:
var(--aside-w)
minmax(0, var(--reading-w))
minmax(0, var(--panel-w));
}
:global(body[data-reading-level="4"]) .page-aside{ display: block; }
:global(body[data-reading-level="4"]) .reading{ grid-column: 2; }
@media (max-width: 1260px){
.page-shell{
max-width: 1180px;
grid-template-columns: var(--aside-w) minmax(0, 1fr);
}
.reading{ max-width: 78ch; margin: 0 auto; }
}
@media (max-width: 860px){
.page-shell{
max-width: 78ch;
grid-template-columns: 1fr;
}
.page-aside{
position: static;
margin-bottom: 12px;
}
.page-aside__scroll{
max-height: none;
overflow: visible;
padding-right: 0;
padding-top: 0;
}
.reading{ max-width: 78ch; margin: 0 auto; }
}
/* ===== para-tools layout (non destructif, surcouche) ===== */
:global(.reading p[id^="p-"]){
position: relative;
padding-right: 132px;
}
:global(.para-tools){
position: absolute;
right: 0;
top: -2px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
z-index: 2;
}
:global(.para-id){
border: 1px solid rgba(127,127,127,0.45);
background: rgba(127,127,127,0.08);
border-radius: 999px;
padding: 2px 8px;
font-size: 12px;
font-weight: 750;
cursor: pointer;
user-select: none;
}
:global(.para-tools-actions){
display: flex;
gap: 6px;
}
:global(.para-bookmark){
align-self: flex-end;
}
/* ✅ repère visuel du para "pilotant" le SidePanel */
:global(.reading p.is-panel-current){
background: rgba(140,140,255,0.07);
border-radius: 12px;
}
</style>
<script is:inline>
(() => {
const safe = (label, fn) => {
try { fn(); }
catch (e) { console.error(`[EditionLayout] ${label} failed:`, e); }
};
const PAGE_GAP = 12;
const HYST = 4;
const headerEl = document.querySelector("header");
const followEl = document.getElementById("reading-follow");
const reading = document.querySelector("article.reading");
const rfH1 = document.getElementById("rf-h1");
const rfH2 = document.getElementById("rf-h2");
const rfH3 = document.getElementById("rf-h3");
const btnTopChapter = document.getElementById("rf-top-chapter");
const btnTopSection = document.getElementById("rf-top-section");
const docTitle = document.body?.dataset?.docTitle || document.title || "Archicratie";
const docVersion = document.body?.dataset?.docVersion || "";
function px(n){ return `${Math.max(0, Math.round(n))}px`; }
function setRootVar(name, value) { document.documentElement.style.setProperty(name, value); }
function headerH() { return headerEl ? headerEl.getBoundingClientRect().height : 0; }
function syncHeaderH() {
setRootVar("--sticky-header-h", px(headerH()));
setRootVar("--page-gap", px(PAGE_GAP));
}
function syncReadingRect() {
if (!reading) return;
const r = reading.getBoundingClientRect();
setRootVar("--reading-left", px(r.left));
setRootVar("--reading-width", px(r.width));
}
function syncHeadingMetrics() {
if (!reading) return;
const h1 = reading.querySelector("h1");
const h2 = reading.querySelector("h2");
const h3 = reading.querySelector("h3");
const sync = (el, prefix) => {
if (!el) return;
const cs = getComputedStyle(el);
if (cs.fontSize) setRootVar(`--rf-${prefix}-size`, cs.fontSize);
if (cs.lineHeight) setRootVar(`--rf-${prefix}-lh`, cs.lineHeight);
if (cs.fontWeight) setRootVar(`--rf-${prefix}-fw`, cs.fontWeight);
};
sync(h1, "h1");
sync(h2, "h2");
sync(h3, "h3");
}
syncHeaderH(); syncReadingRect(); syncHeadingMetrics();
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
syncHeaderH(); syncReadingRect(); syncHeadingMetrics();
}).catch(() => {});
}
function getStickyOffsetPx() {
const raw = getComputedStyle(document.documentElement)
.getPropertyValue("--sticky-offset-px")
.trim();
const n = Number.parseFloat(raw);
return Number.isFinite(n) ? n : (headerH() + PAGE_GAP);
}
function absTop(el) {
const r = el.getBoundingClientRect();
return r.top + (window.scrollY || 0);
}
function scrollToElWithOffset(el, extra = 10, behavior = "auto") {
const y = absTop(el) - getStickyOffsetPx() - extra;
window.scrollTo({ top: Math.max(0, y), behavior });
}
// ===== details open (robuste) + export =====
function openDetailsIfNeeded(el) {
try {
if (!el) return;
let d = el.closest?.("details") || null;
if (!d && el.classList?.contains("details-anchor")) {
const n = el.nextElementSibling;
if (n && n.tagName === "DETAILS") d = n;
}
if (!d) {
const s = el.closest?.("summary");
if (s && s.parentElement && s.parentElement.tagName === "DETAILS") d = s.parentElement;
}
if (d && d.tagName === "DETAILS" && !d.open) d.open = true;
} catch {}
}
window.__archiOpenDetailsIfNeeded = openDetailsIfNeeded;
function highlightEl(el) {
try {
if (!el) return;
el.classList.add("para-highlight");
setTimeout(() => el.classList.remove("para-highlight"), 2400);
} catch {}
}
function highlightFromTarget(el) {
try {
if (!el) return;
if (el.matches?.('.reading p[id^="p-"]')) return highlightEl(el);
const p = el.closest?.('.reading p[id^="p-"]');
if (p) return highlightEl(p);
if (el.classList?.contains("details-anchor")) {
const d = (el.nextElementSibling && el.nextElementSibling.tagName === "DETAILS") ? el.nextElementSibling : null;
const p2 = d ? d.querySelector('.details-body p[id^="p-"]') : null;
if (p2) return highlightEl(p2);
}
highlightEl(el);
} catch {}
}
function cssEscape(s) {
try { return (window.CSS && CSS.escape) ? CSS.escape(String(s)) : String(s).replace(/["\\]/g, "\\$&"); }
catch { return String(s).replace(/["\\]/g, "\\$&"); }
}
// ===== Bookmarks (pinned + auto) =====
const PINNED_KEY = "archicratie:bookmark:pinned";
const LAST_KEY = `archicratie:bookmark:last:${window.location.pathname}`;
function readJSON(key) {
try { return JSON.parse(localStorage.getItem(key) || "null"); }
catch { return null; }
}
function writeJSON(key, obj) {
try { localStorage.setItem(key, JSON.stringify(obj)); } catch {}
}
function updateResumeButton() {
const btn = document.getElementById("resume-btn");
if (!btn) return;
const bm = readJSON(PINNED_KEY) || readJSON(LAST_KEY);
if (!bm || !bm.url) { btn.hidden = true; return; }
btn.hidden = false;
btn.href = bm.url;
btn.title = bm.title ? `Reprendre : ${bm.title}` : "Reprendre la lecture";
btn.onclick = (ev) => {
try {
const here = new URL(window.location.href);
here.search = "";
const target = new URL(bm.url, window.location.origin);
target.search = "";
if (here.pathname === target.pathname) {
ev.preventDefault();
const id = (target.hash || "").slice(1);
if (!id) return;
const el = document.getElementById(id);
if (!el) return;
openDetailsIfNeeded(el);
scrollToElWithOffset(el, 12, "auto");
highlightFromTarget(el);
history.replaceState(null, "", here.pathname + "#" + id);
}
} catch {}
};
}
window.__archiUpdateResumeButton = updateResumeButton;
if (window.location.search.includes("body=")) {
history.replaceState(null, "", window.location.pathname + window.location.hash);
}
// ===== legacy hash fallback (inchangé) =====
safe("legacy-hash", () => {
const h = window.location.hash || "";
const m = h.match(/^#p-(\d+)-[0-9a-f]{8}$/i);
if (!m) return;
const oldId = h.slice(1);
if (document.getElementById(oldId)) return;
const idx = m[1];
const prefix = `p-${idx}-`;
const replacement = document.querySelector(`.reading [id^="${prefix}"]`);
if (!replacement) return;
console.warn("[anchors] legacy hash fallback used:", `#${oldId}`, "→", `#${replacement.id}`);
scrollToElWithOffset(replacement, 12, "auto");
highlightFromTarget(replacement);
});
updateResumeButton();
// ===== Jump (id / URL#id / extrait) =====
const jumpForm = document.getElementById("jump-form");
const jumpInput = document.getElementById("jump-input");
function flashJumpError() {
if (!jumpInput) return;
jumpInput.classList.add("is-error");
setTimeout(() => jumpInput.classList.remove("is-error"), 650);
}
function normalizeParaId(input) {
const raw = String(input || "").trim();
if (!raw) return "";
const hashIdx = raw.lastIndexOf("#");
const h = (hashIdx >= 0) ? raw.slice(hashIdx + 1) : raw;
const id = h.replace(/^#/, "").trim();
if (!/^p-\d+-/i.test(id)) return "";
return id;
}
function findParaLocalByIdOrPrefix(id) {
const exact = document.getElementById(id);
if (exact) return exact;
return document.querySelector(`.reading [id^="${cssEscape(id)}"]`);
}
function normalizeForSearch(s) {
return String(s || "")
.toLowerCase()
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.replace(/\s+/g, " ")
.trim();
}
let _paraIndexP = null;
async function loadParaIndex() {
if (_paraIndexP) return _paraIndexP;
_paraIndexP = (async () => {
const res = await fetch("/para-index.json?_=" + Date.now(), { cache: "no-store" }).catch(() => null);
if (res && res.ok) return await res.json();
return null;
})();
return _paraIndexP;
}
function gotoPageAnchor(page, id) {
const url = new URL(page, window.location.origin);
url.hash = id;
window.location.assign(url.toString());
}
async function doJump(input) {
const raw = String(input || "").trim();
if (!raw) return false;
const id = normalizeParaId(raw);
if (id) {
const local = findParaLocalByIdOrPrefix(id);
if (local) {
openDetailsIfNeeded(local);
scrollToElWithOffset(local, 12, "auto");
highlightFromTarget(local);
history.replaceState(null, "", `${window.location.pathname}#${local.id}`);
return true;
}
const idx = await loadParaIndex();
if (idx?.byId && idx.items && idx.byId[id] != null) {
const item = idx.items[idx.byId[id]];
if (item?.page && item?.id) {
const targetPath = new URL(item.page, window.location.origin).pathname;
if (targetPath === window.location.pathname) {
window.location.hash = item.id;
return true;
}
gotoPageAnchor(item.page, item.id);
return true;
}
}
flashJumpError();
return false;
}
const q = normalizeForSearch(raw);
if (q.length < 10) { flashJumpError(); return false; }
const paras = Array.from(document.querySelectorAll('.reading p[id^="p-"]'));
for (const p of paras) {
const t = normalizeForSearch(p.innerText || p.textContent || "");
if (t.includes(q)) {
openDetailsIfNeeded(p);
scrollToElWithOffset(p, 12, "auto");
highlightFromTarget(p);
history.replaceState(null, "", `${window.location.pathname}#${p.id}`);
return true;
}
}
const idx = await loadParaIndex();
if (!idx?.items) { flashJumpError(); return false; }
let best = null;
let bestScore = -1;
for (const it of idx.items) {
if (!it?.text || !it?.page || !it?.id) continue;
const t = normalizeForSearch(it.text);
const pos = t.indexOf(q);
if (pos < 0) continue;
const score = (100000 - pos) + Math.min(4000, q.length * 8);
if (score > bestScore) { bestScore = score; best = it; }
}
if (!best) { flashJumpError(); return false; }
const targetPath = new URL(best.page, window.location.origin).pathname;
if (targetPath === window.location.pathname) {
window.location.hash = best.id;
return true;
}
gotoPageAnchor(best.page, best.id);
return true;
}
safe("jump-form", () => {
if (!jumpForm || !jumpInput) return;
jumpForm.addEventListener("submit", async (ev) => {
ev.preventDefault();
const ok = await doJump(jumpInput.value);
if (ok) jumpInput.value = "";
});
});
// ===== Gitea globals (déjà posés par le boot head) =====
const giteaReady = Boolean(window.__archiGitea && window.__archiGitea.ready);
const quoteBlock = (s) =>
String(s || "")
.split(/\r?\n/)
.map((l) => (`> ${l}`).trimEnd())
.join("\n");
function buildIssueURLCorrection(anchorId, fullText, excerpt) {
const g = window.__archiGitea || { base:"", owner:"", repo:"" };
const base = String(g.base).replace(/\/+$/, "");
const issue = new URL(`${base}/${g.owner}/${g.repo}/issues/new`);
const local = new URL(window.location.href);
local.search = "";
local.hash = anchorId;
const path = local.pathname;
const issueTitle = `[Correction] ${anchorId} — ${docTitle}`;
const makeBody = (useFull) => {
const header = [
`Chemin: ${path}`,
`URL: ${local.toString()}`,
`Ancre: #${anchorId}`,
`Version: ${docVersion || "(non renseignée)"}`,
`Type: type/correction`,
`State: state/recevable`,
``,
];
const texteActuel = useFull
? [`Texte actuel (copie exacte du paragraphe):`, quoteBlock(fullText || "")]
: [
`Texte actuel (extrait):`,
quoteBlock(excerpt || ""),
``,
`Note: paragraphe long → extrait (pour éviter une URL trop longue).`,
];
const footer = [
``,
`Proposition (remplacer par):`,
``,
`Justification:`,
``,
`---`,
`Note: issue générée depuis le site (pré-remplissage).`,
];
return header.concat(texteActuel, footer).join("\n");
};
issue.searchParams.set("title", issueTitle);
const fullOk = Boolean(fullText && fullText.length && fullText.length <= 1600);
issue.searchParams.set("body", makeBody(fullOk));
if (issue.toString().length > 6500) {
issue.searchParams.set("body", makeBody(false));
}
return issue.toString();
}
// ===== Paragraph tools + pinned bookmark =====
const parasAll = Array.from(document.querySelectorAll('.reading p[id^="p-"]'));
function normalizeSpaces(s) {
return String(s || "").replace(/\s+/g, " ").trim();
}
function getSelectionTextWithinPara(pEl) {
try {
const sel = window.getSelection ? window.getSelection() : null;
if (!sel || sel.isCollapsed) return "";
const t = String(sel.toString() || "").trim();
if (!t) return "";
const a = sel.anchorNode;
const f = sel.focusNode;
if (!a || !f) return "";
if (!pEl.contains(a) || !pEl.contains(f)) return "";
return normalizeSpaces(t);
} catch { return ""; }
}
function getCleanParagraphText(pEl) {
try {
const clone = pEl.cloneNode(true);
clone.querySelectorAll(".para-tools").forEach((n) => n.remove());
clone.querySelectorAll("[data-noquote]").forEach((n) => n.remove());
return normalizeSpaces(clone.innerText || "");
} catch {
return normalizeSpaces(pEl.innerText || pEl.textContent || "");
}
}
function hideEl(el) {
try {
el.hidden = true;
el.style.display = "none";
el.setAttribute("aria-hidden", "true");
el.setAttribute("tabindex", "-1");
} catch {}
}
function showEl(el) {
try {
el.hidden = false;
el.style.display = "";
el.removeAttribute("aria-hidden");
el.removeAttribute("tabindex");
} catch {}
}
safe("para-tools", () => {
for (const p of parasAll) {
if (p.querySelector(".para-tools")) continue;
const tools = document.createElement("span");
tools.className = "para-tools";
tools.setAttribute("data-noquote", "1");
const idBtn = document.createElement("button");
idBtn.type = "button";
idBtn.className = "para-id";
idBtn.textContent = p.id;
idBtn.title = "Copier le lien du paragraphe";
idBtn.addEventListener("click", async () => {
try {
const u = new URL(window.location.href);
u.search = "";
u.hash = p.id;
await navigator.clipboard.writeText(u.toString());
const prev = idBtn.textContent;
idBtn.textContent = "Lien copié";
setTimeout(() => (idBtn.textContent = prev), 900);
} catch {}
});
const actions = document.createElement("span");
actions.className = "para-tools-actions";
const citeBtn = document.createElement("button");
citeBtn.type = "button";
citeBtn.className = "para-cite";
citeBtn.textContent = "Citer";
citeBtn.addEventListener("click", async (ev) => {
const pageUrl = new URL(window.location.href);
pageUrl.search = "";
pageUrl.hash = p.id;
const signature = `${docTitle}${docVersion ? ` (v${docVersion})` : ""} — ${pageUrl.toString()}`;
let payload = signature;
if (!(ev && ev.shiftKey)) {
const selTxt = getSelectionTextWithinPara(p);
const paraTxt = selTxt || getCleanParagraphText(p);
payload = paraTxt ? `${paraTxt}\n\n${signature}` : signature;
}
try {
await navigator.clipboard.writeText(payload);
const prev = citeBtn.textContent;
citeBtn.textContent = (ev && ev.shiftKey) ? "Lien copié" : "Copié";
setTimeout(() => (citeBtn.textContent = prev), 900);
} catch {
window.prompt("Copiez la citation :", payload);
}
});
actions.appendChild(citeBtn);
let propose = null;
if (giteaReady) {
propose = document.createElement("a");
propose.className = "para-propose";
propose.textContent = "Proposer";
propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
const raw = getCleanParagraphText(p);
const excerpt = raw.length > 420 ? (raw.slice(0, 420) + "…") : raw;
const issueUrl = buildIssueURLCorrection(p.id, raw, excerpt);
propose.href = issueUrl;
propose.target = "_blank";
propose.rel = "noopener noreferrer";
propose.dataset.propose = "1";
propose.dataset.url = issueUrl;
propose.dataset.full = raw;
hideEl(propose);
actions.appendChild(propose);
}
const bmBtn = document.createElement("button");
bmBtn.type = "button";
bmBtn.className = "para-bookmark";
bmBtn.textContent = "Marque-page";
bmBtn.addEventListener("click", () => {
const pageUrl = new URL(window.location.href);
pageUrl.search = "";
pageUrl.hash = p.id;
writeJSON(PINNED_KEY, {
url: pageUrl.toString(),
path: pageUrl.pathname,
anchor: p.id,
title: docTitle,
version: docVersion,
ts: Date.now(),
});
updateResumeButton();
const prev = bmBtn.textContent;
bmBtn.textContent = "Marqué ✓";
setTimeout(() => (bmBtn.textContent = prev), 900);
});
tools.appendChild(idBtn);
tools.appendChild(actions);
tools.appendChild(bmBtn);
p.appendChild(tools);
}
});
safe("propose-gate", () => {
if (!giteaReady) return;
const p = window.__archiIsEditorP || Promise.resolve(false);
p.then((ok) => {
document.querySelectorAll(".para-propose").forEach((el) => {
if (ok) showEl(el);
else hideEl(el); // ✅ jamais remove => antifragile
});
}).catch((err) => {
console.warn("[proposer] gate failed; keeping Proposer hidden", err);
document.querySelectorAll(".para-propose").forEach((el) => hideEl(el));
});
});
// ===== Auto-checkpoint (sur paragraphe ACTIF, pas sur hover) =====
let lastAuto = 0;
function writeLastSeen(id) {
const now = Date.now();
if (now - lastAuto < 800) return;
lastAuto = now;
const u = new URL(window.location.href);
u.search = "";
u.hash = id;
writeJSON(LAST_KEY, {
url: u.toString(),
path: u.pathname,
anchor: id,
title: docTitle,
version: docVersion,
ts: now,
});
}
// ===== Détection “para actif” : juste sous reading-follow (✅ fixe le mismatch SidePanel) =====
let __activeParaId = "";
let __hoverParaId = "";
let __panelParaEl = null;
let __rafActive = 0;
function markPanelCurrent(id) {
try {
if (__panelParaEl) __panelParaEl.classList.remove("is-panel-current");
__panelParaEl = null;
const el = document.getElementById(id);
const p = el?.matches?.('.reading p[id^="p-"]') ? el : el?.closest?.('.reading p[id^="p-"]');
if (p) {
p.classList.add("is-panel-current");
__panelParaEl = p;
}
} catch {}
}
function dispatchCurrentPara(id, kind) {
try { window.dispatchEvent(new CustomEvent("archicratie:currentPara", { detail: { id, kind } })); } catch {}
}
function setCurrentPara(id, kind = "active") {
if (!id) return;
if (window.__archiCurrentParaId === id && kind !== "hover") return;
window.__archiCurrentParaId = id;
window.__archiLastParaId = id; // utile pour level switch
markPanelCurrent(id);
dispatchCurrentPara(id, kind);
}
function probeParaUnderFollow() {
if (!reading) return null;
const rr = reading.getBoundingClientRect();
if (!rr || rr.width < 10) return null;
const x = Math.round(rr.left + Math.min(140, rr.width * 0.45));
const baseY = Math.round(getStickyOffsetPx() + 14);
const ys = [baseY, baseY + 18, baseY + 36, baseY + 54, baseY + 72, baseY + 90, baseY + 108];
for (const y of ys) {
const stack = (document.elementsFromPoint ? document.elementsFromPoint(x, y) : [document.elementFromPoint(x, y)])
.filter(Boolean);
for (const el of stack) {
const p = el.closest?.('.reading p[id^="p-"]');
if (p && p.id) return p;
}
}
const y0 = baseY;
for (const p of parasAll) {
const r = p.getBoundingClientRect();
if (r.top <= y0 && r.bottom > y0) return p;
if (r.top > y0) return p;
}
return null;
}
function recomputeActivePara() {
const p = probeParaUnderFollow();
const id = p?.id || "";
if (!id) return;
if (id !== __activeParaId) {
__activeParaId = id;
writeLastSeen(id);
updateResumeButton();
}
if (!__hoverParaId) setCurrentPara(__activeParaId, "active");
}
function scheduleActive() {
if (__rafActive) return;
__rafActive = requestAnimationFrame(() => {
__rafActive = 0;
recomputeActivePara();
});
}
safe("active-para", () => {
scheduleActive();
window.addEventListener("scroll", scheduleActive, { passive: true });
});
safe("hover-para", () => {
if (!reading) return;
let t = 0;
reading.addEventListener("pointerover", (ev) => {
const p = ev.target?.closest?.('.reading p[id^="p-"]');
if (!p || !p.id) return;
__hoverParaId = p.id;
if (t) { clearTimeout(t); t = 0; }
setCurrentPara(__hoverParaId, "hover");
});
reading.addEventListener("pointerout", () => {
if (t) clearTimeout(t);
t = setTimeout(() => {
__hoverParaId = "";
if (__activeParaId) setCurrentPara(__activeParaId, "active");
}, 120);
});
});
/* ✅ NOUVEAU : clic paragraphe => snap sous reading-follow + SidePanel aligné */
safe("click-para-align", () => {
if (!reading) return;
reading.addEventListener("click", (ev) => {
try {
const t = ev.target;
// ne pas casser les boutons / liens / para-tools
if (t && t.closest && t.closest(".para-tools")) return;
const a = t && t.closest ? t.closest("a") : null;
if (a) return;
const p = t && t.closest ? t.closest('.reading p[id^="p-"]') : null;
if (!p || !p.id) return;
// si sélection texte en cours => ne pas snap
const sel = window.getSelection ? window.getSelection() : null;
if (sel && !sel.isCollapsed) return;
openDetailsIfNeeded(p);
// snap immédiat sous followbar
scrollToElWithOffset(p, 12, "auto");
// URL stable
history.replaceState(null, "", `${location.pathname}#${p.id}`);
// force la source de vérité (SidePanel suit via event archicratie:currentPara)
__hoverParaId = "";
__activeParaId = p.id;
setCurrentPara(p.id, "click");
writeLastSeen(p.id);
updateResumeButton();
} catch {}
}, { passive: true });
});
// ===== Hash handling (robuste) =====
function resolveParaIdFromEl(el) {
if (!el) return "";
if (el.matches?.('.reading p[id^="p-"]')) return el.id;
const p = el.closest?.('.reading p[id^="p-"]');
if (p?.id) return p.id;
if (el.classList?.contains("details-anchor")) {
const d = (el.nextElementSibling && el.nextElementSibling.tagName === "DETAILS") ? el.nextElementSibling : null;
const p2 = d ? d.querySelector('.details-body p[id^="p-"]') : null;
if (p2?.id) return p2.id;
}
return "";
}
function handleHashTarget(id, behavior = "auto") {
try {
const clean = String(id || "").replace(/^#/, "").trim();
if (!clean) return false;
const el = document.getElementById(clean);
if (!el) return false;
openDetailsIfNeeded(el);
scrollToElWithOffset(el, 12, behavior);
highlightFromTarget(el);
history.replaceState(null, "", `${window.location.pathname}#${clean}`);
const pid = resolveParaIdFromEl(el);
if (pid) {
__activeParaId = pid;
setCurrentPara(pid, "hash");
writeLastSeen(pid);
updateResumeButton();
}
return true;
} catch { return false; }
}
safe("hash-links", () => {
document.addEventListener("click", (ev) => {
const a = ev.target?.closest?.('a[href^="#"]');
if (!a) return;
const href = a.getAttribute("href") || "";
if (!href || href === "#") return;
const id = href.slice(1);
if (!id) return;
if (!document.getElementById(id)) return;
ev.preventDefault();
handleHashTarget(id, "smooth");
});
window.addEventListener("hashchange", () => {
handleHashTarget((window.location.hash || "").slice(1), "auto");
});
});
// ===== TOC local sync (inchangé) =====
function isScrollable(el) {
try {
if (!el || !(el instanceof HTMLElement)) return false;
const cs = getComputedStyle(el);
const oy = cs.overflowY;
if (!(oy === "auto" || oy === "scroll" || oy === "overlay")) return false;
return el.scrollHeight > el.clientHeight + 2;
} catch { return false; }
}
function findScrollParent(fromEl, stopEl) {
let n = fromEl;
while (n && n !== document.body) {
if (stopEl && n === stopEl) break;
if (isScrollable(n)) return n;
n = n.parentElement;
}
if (stopEl && isScrollable(stopEl)) return stopEl;
return null;
}
function syncTocLocal(activeId) {
if (!activeId) return;
const list =
document.querySelector(".toc-local__list") ||
document.querySelector("[data-toc-local]") ||
document.querySelector(".toc-local ul") ||
null;
if (!list) return;
const sel = `a[href="#${cssEscape(activeId)}"]`;
const link = list.querySelector(sel);
if (!link) return;
list.querySelectorAll("a[aria-current]").forEach((a) => a.removeAttribute("aria-current"));
link.setAttribute("aria-current", "location");
const stop = document.querySelector(".page-aside__scroll") || null;
const scrollBox = findScrollParent(link, stop) || stop || list;
if (!scrollBox) return;
const pad = 12;
const rLink = link.getBoundingClientRect();
const rBox = scrollBox.getBoundingClientRect();
const linkTopInBox = (rLink.top - rBox.top) + scrollBox.scrollTop;
const target = Math.max(0, Math.round(linkTopInBox - pad));
try { scrollBox.scrollTo({ top: target, behavior: "auto" }); }
catch { scrollBox.scrollTop = target; }
}
window.__archiSyncTocLocal = syncTocLocal;
// ===== Reading-follow (H1/H2/H3 only) =====
safe("reading-follow", () => {
if (!reading || !followEl) return;
const h1 = reading.querySelector("h1");
const h2Anchors = Array.from(reading.querySelectorAll(".details-anchor[id]"))
.map((s) => {
const d = (s.nextElementSibling && s.nextElementSibling.tagName === "DETAILS")
? s.nextElementSibling
: (s.closest?.("details") || null);
const h2 = d ? d.querySelector(".details-body h2") : null;
const sum = d ? d.querySelector("summary") : null;
const title =
((s.getAttribute("data-title") || "").trim()
|| (h2?.textContent || "").trim()
|| (sum?.textContent || "").trim()
|| "Section");
const marker = d || s;
return { id: s.id, anchor: s, marker, title, h2, details: d };
})
.filter(Boolean);
const h2Plain = Array.from(reading.querySelectorAll("h2[id]"))
.map((h2) => ({ id: h2.id, anchor: h2, marker: h2, title: (h2.textContent || "").trim() || "Section", h2 }));
const H2 = (h2Anchors.length ? h2Anchors : h2Plain)
.slice()
.sort((a,b) => absTop(a.marker) - absTop(b.marker));
const H3 = Array.from(reading.querySelectorAll("h3[id]"))
.map((h3) => ({ id: h3.id, el: h3, title: (h3.textContent || "").trim() || "Sous-section" }));
let curH2 = null;
let curH3 = null;
let lastY = window.scrollY || 0;
let lastOpenedH2 = "";
function computeLineY(followH) {
return headerH() + PAGE_GAP + (followH || 0) + HYST;
}
function pickH2(lineY) {
if (!H2.length) return null;
let cand = null;
for (const t of H2) {
const top = t.marker.getBoundingClientRect().top;
if (top <= lineY) cand = t;
else break;
}
if (!cand) cand = H2[0];
const down = (window.scrollY || 0) >= lastY;
if (!down && cand && curH2 && cand.id !== curH2.id) {
const topNew = cand.marker.getBoundingClientRect().top;
if (topNew > lineY - (HYST * 2)) cand = curH2;
}
return cand;
}
function isAfter(start, el) {
if (!start || !el) return false;
return Boolean(start.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
}
function isBefore(end, el) {
if (!end || !el) return true;
return Boolean(end.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING);
}
function pickH3(lineY, activeH2) {
if (!activeH2) return null;
const visible = H3.filter((t) => {
const d = t.el.closest?.("details");
return !(d && !d.open);
});
const d2 = activeH2.details || activeH2.h2?.closest?.("details") || null;
let scoped = d2 ? visible.filter((t) => t.el.closest?.("details") === d2) : visible;
const i2 = H2.findIndex((t) => t.id === activeH2.id);
const nextH2 = (i2 >= 0 && i2 + 1 < H2.length) ? H2[i2 + 1] : null;
const startNode = activeH2.anchor || activeH2.h2 || activeH2.marker;
const endNode = nextH2 ? (nextH2.anchor || nextH2.h2 || nextH2.marker) : null;
scoped = scoped.filter((t) => isAfter(startNode, t.el) && isBefore(endNode, t.el));
let cand = null;
for (const t of scoped) {
const top = t.el.getBoundingClientRect().top;
if (top <= lineY) cand = t;
else break;
}
return cand;
}
function maybeOpenActiveSection(activeH2, lineY) {
if (!activeH2 || !activeH2.id) return;
const pre = activeH2.marker.getBoundingClientRect().top <= (lineY + 140);
if (!pre) return;
if (activeH2.id !== lastOpenedH2) {
lastOpenedH2 = activeH2.id;
openDetailsIfNeeded(activeH2.anchor || activeH2.h2 || activeH2.marker);
}
}
function updateFollow() {
if (!followEl) return;
const baseY = headerH() + PAGE_GAP;
const showH1 = !!(h1 && (h1.getBoundingClientRect().bottom <= baseY + HYST));
if (rfH1 && h1 && showH1) { rfH1.hidden = false; rfH1.textContent = (h1.textContent || "").trim(); }
else if (rfH1) rfH1.hidden = true;
const lineY0 = computeLineY(0);
curH2 = pickH2(lineY0);
maybeOpenActiveSection(curH2, lineY0);
curH3 = pickH3(lineY0, curH2);
try { syncTocLocal((curH3 && curH3.id) ? curH3.id : (curH2 && curH2.id) ? curH2.id : ""); } catch {}
if (rfH2 && curH2) { rfH2.hidden = false; rfH2.textContent = curH2.title; }
else if (rfH2) rfH2.hidden = true;
if (rfH3 && curH3) { rfH3.hidden = false; rfH3.textContent = curH3.title; }
else if (rfH3) rfH3.hidden = true;
const any0 = (rfH1 && !rfH1.hidden) || (rfH2 && !rfH2.hidden) || (rfH3 && !rfH3.hidden);
followEl.classList.toggle("is-on", Boolean(any0));
followEl.setAttribute("aria-hidden", any0 ? "false" : "true");
const inner = followEl.querySelector(".reading-follow__inner");
const followH = (any0 && inner) ? inner.getBoundingClientRect().height : 0;
const lineY = computeLineY(followH);
curH2 = pickH2(lineY);
maybeOpenActiveSection(curH2, lineY);
curH3 = pickH3(lineY, curH2);
try { syncTocLocal((curH3 && curH3.id) ? curH3.id : (curH2 && curH2.id) ? curH2.id : ""); } catch {}
if (rfH2 && curH2) {
const on = curH2.marker.getBoundingClientRect().top <= lineY;
rfH2.hidden = !on;
if (on) rfH2.textContent = curH2.title;
} else if (rfH2) rfH2.hidden = true;
if (rfH3 && curH3) {
const on = curH3.el.getBoundingClientRect().top <= lineY;
rfH3.hidden = !on;
if (on) rfH3.textContent = curH3.title;
} else if (rfH3) rfH3.hidden = true;
const any = (rfH1 && !rfH1.hidden) || (rfH2 && !rfH2.hidden) || (rfH3 && !rfH3.hidden);
followEl.classList.toggle("is-on", Boolean(any));
followEl.setAttribute("aria-hidden", any ? "false" : "true");
const inner2 = followEl.querySelector(".reading-follow__inner");
const followH2 = (any && inner2) ? inner2.getBoundingClientRect().height : 0;
setRootVar("--followbar-h", px(followH2));
setRootVar("--sticky-offset-px", String(Math.round(headerH() + PAGE_GAP + followH2)));
if (btnTopChapter) btnTopChapter.hidden = !any;
if (btnTopSection) {
const ok = Boolean(curH2 && rfH2 && !rfH2.hidden);
btnTopSection.hidden = !ok;
}
lastY = window.scrollY || 0;
syncReadingRect();
scheduleActive();
}
window.__archiUpdateFollow = updateFollow;
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
updateFollow();
});
};
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
if (rfH1) rfH1.addEventListener("click", () => { if (h1) scrollToElWithOffset(h1, 12, "smooth"); });
if (rfH2) rfH2.addEventListener("click", () => {
if (!curH2) return;
openDetailsIfNeeded(curH2.anchor || curH2.h2 || curH2.marker);
scrollToElWithOffset(curH2.marker, 12, "smooth");
history.replaceState(null, "", `${window.location.pathname}#${curH2.id}`);
});
if (rfH3) rfH3.addEventListener("click", () => {
if (!curH3) return;
openDetailsIfNeeded(curH3.el);
scrollToElWithOffset(curH3.el, 12, "smooth");
history.replaceState(null, "", `${window.location.pathname}#${curH3.id}`);
});
if (btnTopChapter) btnTopChapter.addEventListener("click", () => { if (h1) scrollToElWithOffset(h1, 12, "smooth"); });
if (btnTopSection) btnTopSection.addEventListener("click", () => {
if (!curH2) return;
openDetailsIfNeeded(curH2.anchor || curH2.h2 || curH2.marker);
scrollToElWithOffset(curH2.marker, 12, "smooth");
history.replaceState(null, "", `${window.location.pathname}#${curH2.id}`);
});
updateFollow();
const initialHash = (location.hash || "").slice(1);
if (initialHash) {
const el = document.getElementById(initialHash);
if (el) {
openDetailsIfNeeded(el);
setTimeout(() => {
scrollToElWithOffset(el, 12, "auto");
highlightFromTarget(el);
scheduleActive();
}, 0);
}
}
});
// ✅ Reflow sync : resize + changement de niveau (restaure le même paragraphe)
function afterReflow(fn) {
requestAnimationFrame(() => requestAnimationFrame(fn));
}
safe("reflow-sync", () => {
window.addEventListener("resize", () => {
afterReflow(() => {
syncHeaderH(); syncReadingRect(); syncHeadingMetrics();
try { window.__archiUpdateFollow?.(); } catch {}
scheduleActive();
});
});
window.addEventListener("archicratie:readingLevel", () => {
afterReflow(() => {
syncHeaderH(); syncReadingRect(); syncHeadingMetrics();
try { window.__archiUpdateFollow?.(); } catch {}
const ctx = window.__archiLevelSwitchCtx || null;
const want =
(ctx && ctx.paraId) ||
window.__archiCurrentParaId ||
__activeParaId ||
window.__archiLastParaId ||
String(location.hash || "").replace(/^#/, "");
if (want) {
const el = document.getElementById(want);
if (el) {
openDetailsIfNeeded(el);
scrollToElWithOffset(el, 12, "auto");
highlightFromTarget(el);
history.replaceState(null, "", `${location.pathname}#${want}`);
__activeParaId = want;
setCurrentPara(want, "level");
} else if (ctx && Number.isFinite(ctx.scrollY)) {
window.scrollTo({ top: ctx.scrollY, behavior: "auto" });
}
}
scheduleActive();
});
});
});
// ===== Auto-unfold <details> (robuste) =====
safe("details-autounfold", () => {
if (!reading) return;
const sections = Array.from(reading.querySelectorAll("details.details-section"));
if (!sections.length) return;
// ✅ Lecture continue : tout ouvert, plus daccordéon
for (const d of sections) d.open = true;
try { window.__archiUpdateFollow?.(); } catch {}
scheduleActive();
});
})();
</script>
</body>
</html>