Files
archicratie-edition/src/layouts/EditionLayout.astro
Archicratia 6b2fd25d23
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 37s
CI / build-and-anchors (pull_request) Successful in 38s
fix: stabilise reading follow on long editions
2026-04-26 18:22:11 +02:00

3243 lines
91 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,
stickyMode = "default",
} = Astro.props;
const lvl = level ?? 1;
const isGlossaryEdition = String(editionKey ?? "") === "glossaire";
const showLevelToggle = !isGlossaryEdition;
const stickyModeValue = String(stickyMode || "default");
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 ?? "")}`} />
<script
is:inline
define:vars={{
IS_DEV,
GITEA_BASE,
GITEA_OWNER,
GITEA_REPO,
ISSUE_BRIDGE_PATH,
WHOAMI_PATH,
WHOAMI_IN_DEV,
WHOAMI_FORCE_LOCALHOST,
}}
>
(() => {
if (window.__archiBootOnce === 1) return;
window.__archiBootOnce = 1;
var __DEV__ = Boolean(IS_DEV);
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
};
var rawBridge = String(ISSUE_BRIDGE_PATH || "").trim();
var normBridge = rawBridge
? (rawBridge.startsWith("/") ? rawBridge : ("/" + rawBridge.replace(/^\/+/, ""))).replace(/\/+$/, "")
: "";
window.__archiIssueBridge = { ready: Boolean(normBridge), path: normBridge };
var __WHOAMI_PATH__ = String(WHOAMI_PATH || "/_auth/whoami");
var __WHOAMI_IN_DEV__ = Boolean(WHOAMI_IN_DEV);
var __WHOAMI_FORCE_LOCALHOST__ = Boolean(WHOAMI_FORCE_LOCALHOST);
var IS_LOCAL_HOST =
location.hostname === "localhost" ||
location.hostname === "127.0.0.1" ||
location.hostname === "::1";
var SHOULD_FETCH_WHOAMI =
((!__DEV__) && !IS_LOCAL_HOST) ||
__WHOAMI_IN_DEV__ ||
__WHOAMI_FORCE_LOCALHOST__;
window.__archiFlags = Object.assign({}, window.__archiFlags, {
dev: __DEV__,
whoamiPath: __WHOAMI_PATH__,
whoamiInDev: __WHOAMI_IN_DEV__,
whoamiForceLocalhost: __WHOAMI_FORCE_LOCALHOST__,
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);
}
if (!window.__archiAuthInfoP) {
window.__archiAuthInfoP = (async () => {
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: "" }));
}
if (!window.__archiCanReadP) {
window.__archiCanReadP = window.__archiAuthInfoP.then((info) =>
Boolean(info && info.ok && (inGroup(info.groups, READ_GROUP) || inGroup(info.groups, REQUIRED_GROUP)))
);
}
if (!window.__archiIsEditorP) {
window.__archiIsEditorP = window.__archiAuthInfoP
.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)}
data-edition-key={String(editionKey ?? "")}
data-sticky-mode={stickyModeValue}
>
<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 is-disabled" id="resume-btn" href="#" aria-disabled="true">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>
{showLevelToggle && <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 />
<div class="mobile-para-menu" id="mobile-para-menu" hidden aria-hidden="true">
<div class="mobile-para-menu__inner" role="dialog" aria-label="Actions du paragraphe">
<div class="mobile-para-menu__meta" id="mobile-para-menu-meta"></div>
<div class="mobile-para-menu__actions">
<button type="button" class="mobile-para-menu__btn" id="mobile-para-copy-link">Copier le lien</button>
<button type="button" class="mobile-para-menu__btn" id="mobile-para-cite">Citer</button>
<a class="mobile-para-menu__btn mobile-para-menu__link" id="mobile-para-propose" href="#" target="_blank" rel="noopener noreferrer" hidden>Proposer</a>
<button type="button" class="mobile-para-menu__btn" id="mobile-para-bookmark">Marque-page</button>
</div>
</div>
</div>
<style>
:global(:root){
--glossary-local-sticky-h: 0px;
}
.page{
padding: var(--page-gap) 16px 48px;
}
.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;
}
:global(.reading details.details-section){
border: 0 !important;
padding: 0 !important;
margin: 22px 0 !important;
background: transparent !important;
}
: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;
}
:global(.reading details .details-body){
margin-top: 0 !important;
}
: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;
}
: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; }
: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; }
: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;
min-width: 0;
}
}
:global(.reading p[id^="p-"]){
position: relative;
padding-right: 132px;
}
:global(body[data-edition-key="glossaire"] .reading p[id^="p-"]){
padding-right: 0;
}
: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;
}
:global(.reading p.is-panel-current){
background: rgba(140,140,255,0.07);
border-radius: 12px;
}
:global(body[data-edition-key="glossaire"] .reading p.is-panel-current){
background: transparent;
border-radius: 0;
}
:global(body[data-edition-key="glossaire"]) .page-shell,
:global(body[data-edition-key="glossaire"]) .page-aside,
:global(body[data-edition-key="glossaire"]) .reading{
min-width: 0;
}
/* =========================================================
READING-FOLLOW — recalage premium
========================================================= */
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow){
z-index: 10;
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow .reading-follow__inner){
padding: 9px 14px 10px;
padding-right: 88px;
border: 1px solid rgba(127,127,127,.18);
border-top: 0;
border-radius: 0 0 16px 16px;
background: rgba(255,255,255,.88);
box-shadow: 0 10px 24px rgba(0,0,0,.06);
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow .rf-h2){
font-size: clamp(1.02rem, 1.4vw, 1.14rem);
line-height: 1.16;
font-weight: 760;
letter-spacing: -.015em;
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow .rf-h3){
font-size: .84rem;
line-height: 1.14;
font-weight: 650;
letter-spacing: -.01em;
opacity: .82;
margin-top: 2px;
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow .rf-actions){
right: 10px;
bottom: 8px;
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow .rf-btn){
width: 32px;
height: 28px;
border-radius: 9px;
}
@media (max-width: 980px){
.page{
padding: var(--page-gap) 10px 18px 18px;
}
.page-shell{
display: block !important;
width: 100% !important;
max-width: none !important;
min-width: 0 !important;
margin: 0 !important;
}
.page-aside,
.reading,
.page-panel{
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
margin: 0 !important;
}
.page-panel{
display: none !important;
}
.page-aside{
position: static !important;
top: auto !important;
grid-column: auto !important;
margin: 0 0 12px 0 !important;
}
.page-aside__scroll{
max-height: none !important;
overflow: visible !important;
padding-right: 0 !important;
padding-top: 0 !important;
}
.reading{
grid-column: auto !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
overflow-x: hidden !important;
}
article.reading > *{
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
:global(.reading),
:global(.reading *),
:global(.reading details),
:global(.reading details *),
:global(.reading .details-body),
:global(.reading .details-body *){
min-width: 0 !important;
box-sizing: border-box;
}
:global(.reading p[id^="p-"]){
padding-right: 0 !important;
}
:global(.para-tools){
display: none !important;
}
:global(.reading details),
:global(.reading .details-body),
:global(.reading .details-body > *){
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
:global(.reading h1),
:global(.reading h2),
:global(.reading h3),
:global(.reading h4),
:global(.reading p),
:global(.reading li),
:global(.reading blockquote),
:global(.reading figcaption),
:global(.reading .details-body h2),
:global(.reading .details-body h3),
:global(.reading .details-body h4),
:global(.reading .details-body p),
:global(.reading .details-body li),
:global(.reading .details-body blockquote),
:global(.reading .details-body figcaption){
max-width: 100% !important;
overflow-wrap: anywhere !important;
word-break: break-word !important;
hyphens: auto;
}
:global(.reading ul),
:global(.reading ol),
:global(.reading blockquote),
:global(.reading pre),
:global(.reading table),
:global(.reading figure),
:global(.reading img),
:global(.reading svg),
:global(.reading video),
:global(.reading iframe),
:global(.reading canvas),
:global(.reading .details-body ul),
:global(.reading .details-body ol),
:global(.reading .details-body blockquote),
:global(.reading .details-body pre),
:global(.reading .details-body table),
:global(.reading .details-body figure),
:global(.reading .details-body img),
:global(.reading .details-body svg),
:global(.reading .details-body video),
:global(.reading .details-body iframe),
:global(.reading .details-body canvas){
width: 100%;
max-width: 100% !important;
min-width: 0 !important;
}
:global(.reading img),
:global(.reading svg),
:global(.reading video),
:global(.reading iframe),
:global(.reading canvas),
:global(.reading .details-body img),
:global(.reading .details-body svg),
:global(.reading .details-body video),
:global(.reading .details-body iframe),
:global(.reading .details-body canvas){
height: auto;
}
:global(.reading pre),
:global(.reading .details-body pre){
overflow-x: auto !important;
white-space: pre-wrap !important;
word-break: break-word !important;
}
:global(.reading table),
:global(.reading .details-body table){
display: block;
overflow-x: auto;
}
:global(.reading [style*="width"]),
:global(.reading .details-body [style*="width"]){
max-width: 100% !important;
}
}
@media (orientation: landscape) and (min-width: 981px) and (max-width: 1220px) and (pointer: coarse){
.page{
padding-left: 10px;
padding-right: 10px;
}
.page-shell{
--aside-w: 300px;
--gap: 14px;
max-width: calc(100vw - 20px) !important;
grid-template-columns: minmax(260px, var(--aside-w)) minmax(0, 1fr) !important;
}
.reading{
max-width: none !important;
width: 100% !important;
min-width: 0 !important;
margin: 0 !important;
}
:global(.reading p[id^="p-"]){
padding-right: 108px;
}
:global(.para-tools){
right: 0;
}
:global(.para-tools-actions){
flex-wrap: wrap;
justify-content: flex-end;
}
}
@media (prefers-color-scheme: dark){
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"] #reading-follow .reading-follow__inner){
background: rgba(0,0,0,.6);
box-shadow: 0 12px 24px rgba(0,0,0,.2);
}
}
.resume-btn{
white-space: nowrap;
flex: 0 0 auto;
}
.resume-btn.is-disabled{
opacity: .55;
cursor: not-allowed;
pointer-events: auto;
}
.resume-btn.is-disabled:hover,
.resume-btn.is-disabled:focus{
transform: none;
}
@media (max-width: 760px){
.page{
padding: var(--page-gap) 12px 14px 18px;
}
.edition-bar{
gap: 8px 10px;
}
.jump-form{
min-width: 0;
flex: 1 1 100%;
max-width: 100%;
}
.jump-input{
width: 100%;
max-width: 100%;
}
:global(#reading-follow){
left: 0;
right: 0;
width: auto;
max-width: none;
}
:global(#reading-follow .reading-follow__inner){
padding: 9px 12px 10px;
padding-right: 70px;
border-radius: 0 0 12px 12px;
}
:global(#reading-follow .rf-line){
display: block;
width: 100%;
text-align: left;
margin: 0;
padding: 0;
}
:global(#reading-follow .rf-h1){
font-size: 1.76rem;
line-height: 1.03;
font-weight: 800;
letter-spacing: -.03em;
}
:global(#reading-follow .rf-h2){
font-size: 1.06rem;
line-height: 1.1;
font-weight: 760;
letter-spacing: -.018em;
margin-top: 2px;
}
:global(#reading-follow .rf-h3){
font-size: .82rem;
line-height: 1.1;
font-weight: 650;
opacity: .8;
margin-top: 2px;
}
:global(#reading-follow .rf-actions){
right: 8px;
bottom: 7px;
gap: 6px;
}
:global(#reading-follow .rf-btn){
width: 28px;
height: 26px;
border-radius: 8px;
font-size: 13px;
}
}
@media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){
.page{
padding: var(--page-gap) 8px 8px 14px;
}
header{
padding: 6px 10px;
}
.site-nav{
font-size: 11px;
}
.edition-bar{
margin-top: 5px;
padding-top: 5px;
gap: 5px 6px;
font-size: 11px;
}
.resume-btn{
font-size: 11px;
padding: 3px 8px;
}
.jump-form{
gap: 4px;
min-width: 0;
flex: 1 1 auto;
}
.jump-input{
width: min(180px, 38vw);
max-width: 100%;
padding: 2px 8px;
font-size: 11px;
}
.jump-btn{
padding: 2px 7px;
font-size: 11px;
}
:global(#reading-follow .reading-follow__inner){
padding: 3px 9px 3px;
padding-right: 54px;
}
:global(#reading-follow .rf-h1){
font-size: .96rem;
}
:global(#reading-follow .rf-h2){
font-size: .76rem;
}
:global(#reading-follow .rf-btn){
width: 22px;
height: 20px;
border-radius: 6px;
font-size: 10px;
}
}
@media (max-width: 980px){
:global(body[data-edition-key="glossaire"]){
--glossary-sticky-top: 0px !important;
--glossary-follow-height: 0px !important;
--glossary-local-sticky-h: 0px !important;
--followbar-h: 0px !important;
}
:global(body[data-edition-key="glossaire"]) .page{
padding: var(--page-gap) 10px 18px 18px !important;
}
:global(body[data-edition-key="glossaire"]) .page-shell{
display: block !important;
width: 100% !important;
max-width: none !important;
min-width: 0 !important;
margin: 0 !important;
}
:global(body[data-edition-key="glossaire"]) .page-aside,
:global(body[data-edition-key="glossaire"]) .reading,
:global(body[data-edition-key="glossaire"]) .page-panel{
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
margin: 0 !important;
}
:global(body[data-edition-key="glossaire"]) .page-panel{
display: none !important;
}
:global(body[data-edition-key="glossaire"]) .page-aside{
position: static !important;
top: auto !important;
grid-column: auto !important;
margin: 0 0 12px 0 !important;
}
:global(body[data-edition-key="glossaire"]) .page-aside__scroll{
max-height: none !important;
overflow: visible !important;
padding: 0 !important;
}
:global(body[data-edition-key="glossaire"]) .reading{
grid-column: auto !important;
overflow-x: clip !important;
}
:global(body[data-edition-key="glossaire"]) article.reading > *{
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
:global(body[data-edition-key="glossaire"]) .glossary-hero,
:global(body[data-edition-key="glossaire"]) .glossary-home,
:global(body[data-edition-key="glossaire"]) .glossary-map,
:global(body[data-edition-key="glossaire"]) .glossary-map-block,
:global(body[data-edition-key="glossaire"]) .glossary-map-section,
:global(body[data-edition-key="glossaire"]) .glossary-page-hero,
:global(body[data-edition-key="glossaire"]) .glossary-portal-hero,
:global(body[data-edition-key="glossaire"]) .scene-hero{
position: static !important;
top: auto !important;
left: auto !important;
right: auto !important;
inset: auto !important;
z-index: auto !important;
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
margin: 0 0 12px 0 !important;
transform: none !important;
}
:global(body[data-edition-key="glossaire"]) #reading-follow{
display: none !important;
}
}
@media (min-width: 761px) and (max-width: 980px){
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-entry"]) .page,
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"]) .page{
padding-top: 0 !important;
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-entry"]) .page-aside,
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"]) .page-aside{
margin-top: 0 !important;
}
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-entry"]) .reading,
:global(body[data-edition-key="glossaire"][data-sticky-mode="glossary-portal"]) .reading{
padding-top: 0 !important;
}
}
@media (max-width: 430px){
.page{
padding: var(--page-gap) 10px 12px 16px;
}
:global(#reading-follow .reading-follow__inner){
padding: 8px 10px 9px;
padding-right: 62px;
border-radius: 0 0 10px 10px;
}
:global(#reading-follow .rf-h1){
font-size: 1.5rem;
line-height: 1.02;
}
:global(#reading-follow .rf-h2){
font-size: .96rem;
line-height: 1.07;
margin-top: 1px;
}
:global(#reading-follow .rf-h3){
font-size: .74rem;
line-height: 1.05;
margin-top: 1px;
}
:global(#reading-follow .rf-actions){
right: 6px;
bottom: 6px;
gap: 4px;
}
:global(#reading-follow .rf-btn){
width: 25px;
height: 24px;
border-radius: 7px;
font-size: 12px;
}
}
.mobile-para-menu{
position: fixed;
inset: auto 10px 10px 10px;
z-index: 120;
}
.mobile-para-menu[hidden]{
display: none !important;
}
.mobile-para-menu__inner{
border: 1px solid rgba(127,127,127,.24);
border-radius: 14px;
background: rgba(20,20,24,.96);
box-shadow: 0 18px 42px rgba(0,0,0,.34);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 10px;
}
.mobile-para-menu__meta{
font-size: 12px;
font-weight: 700;
opacity: .82;
margin-bottom: 8px;
}
.mobile-para-menu__actions{
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.mobile-para-menu__btn,
.mobile-para-menu__link{
appearance: none;
border: 1px solid rgba(127,127,127,.28);
background: rgba(255,255,255,.04);
color: inherit;
text-decoration: none;
border-radius: 12px;
padding: 10px 12px;
font-size: 13px;
font-weight: 700;
text-align: center;
}
.mobile-para-menu__btn:active,
.mobile-para-menu__link:active{
transform: translateY(1px);
}
.mobile-para-menu{
touch-action: manipulation;
}
.mobile-para-menu__inner{
max-width: 100%;
}
.mobile-para-menu__meta{
overflow-wrap: anywhere;
word-break: break-word;
}
@media (max-width: 430px){
.mobile-para-menu{
inset: auto 8px 8px 8px;
}
.mobile-para-menu__actions{
grid-template-columns: 1fr;
}
.mobile-para-menu__btn,
.mobile-para-menu__link{
padding: 11px 12px;
font-size: 13px;
}
}
@media (min-width: 981px) and (hover: hover) and (pointer: fine){
.mobile-para-menu{
display: none !important;
}
}
</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;
function isCompactFollowViewport() {
return (
window.innerWidth <= 760 ||
window.matchMedia("(orientation: landscape) and (max-width: 920px) and (max-height: 520px)").matches
);
}
function stickyGap() {
return isCompactFollowViewport() ? 0 : PAGE_GAP;
}
const body = document.body;
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 || "";
const docEditionKey = document.body?.dataset?.editionKey || "";
const docStickyMode = String(document.body?.dataset?.stickyMode || "default");
const isGlossaryEdition = docEditionKey === "glossaire";
const isIntroEdition = docEditionKey === "commencer";
const hasLocalGlossaryFollow =
isGlossaryEdition && Boolean(document.getElementById("glossary-hero-follow"));
const isGlossaryHomeMode =
docStickyMode === "glossary-home" || hasLocalGlossaryFollow;
const isGlossaryEntryMode =
isGlossaryEdition && docStickyMode === "glossary-entry";
const isGlossaryPortalMode =
isGlossaryEdition && docStickyMode === "glossary-portal";
function syncGlossaryFollowState(isOn) {
if (!body || !isGlossaryEdition) return;
body.classList.toggle(
"glossary-follow-on",
Boolean(isOn && isGlossaryPortalMode)
);
}
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 currentPageGap() { return window.innerWidth <= 760 ? 0 : PAGE_GAP; }
function readPxVar(name) {
const raw = getComputedStyle(document.documentElement)
.getPropertyValue(name)
.trim();
const n = Number.parseFloat(raw);
return Number.isFinite(n) ? n : 0;
}
function autoMeasureGlossaryLocalStickyHeight() {
if (isGlossaryEntryMode) {
const head = document.querySelector(".glossary-entry-head");
return head ? Math.round(head.getBoundingClientRect().height) : 0;
}
return 0;
}
function getLocalStickyHeight() {
const explicit = readPxVar("--glossary-local-sticky-h");
if (explicit > 0) return explicit;
if (isGlossaryEntryMode) {
return autoMeasureGlossaryLocalStickyHeight();
}
return 0;
}
function glossaryEntryHeroBottom() {
const head = document.querySelector(".glossary-entry-head");
if (!head) return headerH();
const bottom = head.getBoundingClientRect().bottom;
return Math.max(headerH(), Math.round(bottom));
}
function syncFollowTop() {
if (!followEl) return;
if (isGlossaryEntryMode) {
followEl.style.top = px(glossaryEntryHeroBottom());
return;
}
const localH = isGlossaryPortalMode ? getLocalStickyHeight() : 0;
followEl.style.top = px(headerH() + stickyGap() + localH);
}
window.__archiSetLocalStickyHeight = (n) => {
const value = Math.max(0, Math.round(Number(n) || 0));
setRootVar("--glossary-local-sticky-h", px(value));
syncFollowTop();
try { window.__archiUpdateFollow?.(); } catch {}
};
window.__archiResetLocalStickyHeight = () => {
setRootVar("--glossary-local-sticky-h", "0px");
syncFollowTop();
try { window.__archiUpdateFollow?.(); } catch {}
};
function syncHeaderH() {
const h = headerH();
setRootVar("--sticky-header-h", px(h));
setRootVar("--page-gap", px(PAGE_GAP));
setRootVar("--glossary-sticky-top", px(h + stickyGap()));
syncFollowTop();
}
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);
if (Number.isFinite(n)) return n;
const localH =
(isGlossaryEntryMode || isGlossaryPortalMode)
? getLocalStickyHeight()
: 0;
const followH =
followEl && followEl.classList.contains("is-on")
? Math.round(
followEl.querySelector(".reading-follow__inner")?.getBoundingClientRect().height || 0
)
: 0;
return Math.max(0, Math.round(headerH() + stickyGap() + localH + followH));
}
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 });
}
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 {}
}
let __suppressReadingClickUntil = 0;
let __resumeClickBound = false;
let __suspendAutoTrackUntil = 0;
function suspendAutoTrack(ms = 1000) {
__suspendAutoTrackUntil = Date.now() + Math.max(0, Number(ms) || 0);
}
function autoTrackSuspended() {
return Date.now() < __suspendAutoTrackUntil;
}
function getParaFromTarget(el) {
try {
if (!el) return null;
if (el.matches?.('.reading p[id^="p-"]')) return el;
const p = el.closest?.('.reading p[id^="p-"]');
if (p) return 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 p2;
}
return null;
} catch {
return null;
}
}
function getAnchorIdFromTarget(el) {
try {
if (!el || !el.id) return "";
return String(el.id);
} catch {
return "";
}
}
function rememberParaFromTarget(el, kind = "active") {
try {
const p = getParaFromTarget(el);
if (!p || !p.id) return "";
__activeParaId = p.id;
setCurrentPara(p.id, kind);
writeLastSeen(p.id);
updateResumeButton();
return p.id;
} catch {
return "";
}
}
function focusReadingTarget(el, {
behavior = "auto",
extra = 12,
open = true,
highlight = true,
updateHash = true,
hashTarget = null,
remember = true,
kind = "active",
} = {}) {
try {
if (!el) return false;
if (open) openDetailsIfNeeded(el);
scrollToElWithOffset(el, extra, behavior);
if (highlight) highlightFromTarget(el);
const anchorId = getAnchorIdFromTarget(hashTarget || el);
if (updateHash && anchorId) {
history.replaceState(null, "", `${window.location.pathname}#${anchorId}`);
}
if (remember) {
rememberParaFromTarget(el, kind);
}
return true;
} catch {
return false;
}
}
function cssEscape(s) {
try { return (window.CSS && CSS.escape) ? CSS.escape(String(s)) : String(s).replace(/["\\]/g, "\\$&"); }
catch { return String(s).replace(/["\\]/g, "\\$&"); }
}
const LAST_KEY = `archicratie:bookmark:last:${window.location.pathname}`;
const RESUME_KEY = "archicratie:resume:explicit";
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 normalizeBookmark(v) {
if (!v || typeof v !== "object") return null;
if (!v.anchor) return null;
const path = String(v.path || window.location.pathname);
const url = String(v.url || `${window.location.origin}${path}#${v.anchor}`);
return {
url,
path,
anchor: String(v.anchor),
title: String(v.title || ""),
version: String(v.version || ""),
ts: Number(v.ts || Date.now()),
kind: String(v.kind || "explicit-bookmark"),
};
}
function setResumeBookmark(payload) {
const normalized = normalizeBookmark(payload);
if (!normalized) return null;
writeJSON(RESUME_KEY, normalized);
return normalized;
}
function getResumeBookmark() {
return normalizeBookmark(readJSON(RESUME_KEY));
}
function savePinnedBookmarkFromPara(p, kind = "explicit-bookmark") {
if (!p || !p.id) return false;
suspendAutoTrack(1400);
const pageUrl = new URL(window.location.href);
pageUrl.search = "";
pageUrl.hash = p.id;
const payload = {
url: pageUrl.toString(),
path: pageUrl.pathname,
anchor: p.id,
title: docTitle,
version: docVersion,
ts: Date.now(),
kind,
};
const saved = setResumeBookmark(payload);
if (!saved) return false;
__activeParaId = p.id;
try { setCurrentPara(p.id, "bookmark"); } catch {}
updateResumeButton();
return true;
}
function performResume() {
const current = getResumeBookmark();
if (!current || !current.anchor) return false;
const here = new URL(window.location.href);
here.search = "";
const targetUrl = new URL(current.url, window.location.origin);
targetUrl.search = "";
const id = String(current.anchor || "").replace(/^#/, "").trim();
if (!id) return false;
if (here.pathname !== targetUrl.pathname) {
window.location.assign(targetUrl.toString());
return true;
}
const el = resolveReadingTargetById(id);
if (!el) return false;
suspendAutoTrack(1800);
focusReadingTarget(el, {
behavior: "auto",
extra: 12,
open: true,
highlight: true,
updateHash: true,
remember: false,
kind: "resume",
});
requestAnimationFrame(() => {
requestAnimationFrame(() => {
try {
__activeParaId = id;
setCurrentPara(id, "resume");
updateResumeButton();
} catch {}
});
});
return true;
}
function updateResumeButton() {
const btn = document.getElementById("resume-btn");
if (!btn) return;
const current = getResumeBookmark();
btn.hidden = false;
btn.setAttribute("href", "#");
if (!current) {
delete btn.dataset.resumeAnchor;
delete btn.dataset.resumePath;
delete btn.dataset.resumeKind;
btn.setAttribute("aria-disabled", "true");
btn.classList.add("is-disabled");
btn.title = "Aucun marque-page explicite enregistré";
return;
}
btn.dataset.resumeAnchor = current.anchor;
btn.dataset.resumePath = current.path || "";
btn.dataset.resumeKind = current.kind || "explicit-bookmark";
btn.setAttribute("aria-disabled", "false");
btn.classList.remove("is-disabled");
btn.title = `Reprendre : ${current.anchor}`;
if (!__resumeClickBound) {
__resumeClickBound = true;
btn.addEventListener("click", (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (btn.getAttribute("aria-disabled") === "true") return;
performResume();
}, { passive: false });
}
}
window.__archiUpdateResumeButton = updateResumeButton;
if (window.location.search.includes("body=")) {
history.replaceState(null, "", window.location.pathname + window.location.hash);
}
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();
window.addEventListener("resize", () => {
suspendAutoTrack(1200);
updateResumeButton();
try { window.__archiUpdateFollow?.(); } catch {}
}, { passive: true });
window.addEventListener("orientationchange", () => {
suspendAutoTrack(2200);
updateResumeButton();
setTimeout(() => {
try { window.__archiUpdateFollow?.(); } catch {}
}, 250);
setTimeout(() => {
try { scheduleActive(); } catch {}
}, 1600);
});
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 resolveReadingTargetById(id) {
const clean = String(id || "").replace(/^#/, "").trim();
if (!clean) return null;
return (
document.getElementById(clean) ||
findParaLocalByIdOrPrefix(clean) ||
document.querySelector(`.reading [id="${cssEscape(clean)}"]`) ||
document.querySelector(`.reading [id^="${cssEscape(clean)}"]`) ||
null
);
}
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) {
focusReadingTarget(local, {
behavior: "auto",
extra: 12,
open: true,
highlight: true,
updateHash: true,
remember: true,
kind: "jump",
});
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)) {
focusReadingTarget(p, {
behavior: "auto",
extra: 12,
open: true,
highlight: true,
updateHash: true,
remember: true,
kind: "jump",
});
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 = "";
});
});
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();
}
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", () => {
if (isGlossaryEdition || isIntroEdition) return;
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", () => {
savePinnedBookmarkFromPara(p, "bookmark");
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("mobile-para-tools", () => {
if (isGlossaryEdition || isIntroEdition) return;
if (!reading) return;
const isMobileLike = () =>
window.innerWidth <= 980 ||
window.matchMedia("(hover: none) and (pointer: coarse)").matches;
const menu = document.getElementById("mobile-para-menu");
const meta = document.getElementById("mobile-para-menu-meta");
const btnCopyLink = document.getElementById("mobile-para-copy-link");
const btnCite = document.getElementById("mobile-para-cite");
const btnPropose = document.getElementById("mobile-para-propose");
const btnBookmark = document.getElementById("mobile-para-bookmark");
if (!menu || !meta || !btnCopyLink || !btnCite || !btnBookmark) return;
let activePara = null;
let pressTimer = 0;
let pressStartX = 0;
let pressStartY = 0;
let pressPointerId = null;
let menuOpen = false;
let touchLongPressActive = false;
const LONG_PRESS_DELAY = 520;
const isTabletLike = () =>
window.matchMedia("(pointer: coarse)").matches &&
Math.min(window.innerWidth, window.innerHeight) >= 700;
const getMoveTolerance = () => isTabletLike() ? 42 : 22;
function isInteractiveTarget(node) {
const el = node?.closest?.(
'a, button, input, textarea, select, summary, label, [role="button"], [data-no-longpress], .para-tools, .mobile-para-menu'
);
return Boolean(el);
}
function clearPressTimer() {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = 0;
}
}
function cancelLongPress() {
clearPressTimer();
pressPointerId = null;
touchLongPressActive = false;
}
function startLongPressForPara(p, x, y, pointerId = null) {
if (!p || !p.id) return;
activePara = p;
pressStartX = Number(x || 0);
pressStartY = Number(y || 0);
pressPointerId = pointerId;
touchLongPressActive = true;
clearPressTimer();
pressTimer = window.setTimeout(() => {
openMenuForPara(p);
clearPressTimer();
pressPointerId = null;
touchLongPressActive = false;
}, LONG_PRESS_DELAY);
}
function movedTooFar(x, y) {
const dx = Math.abs(Number(x || 0) - pressStartX);
const dy = Math.abs(Number(y || 0) - pressStartY);
const tol = getMoveTolerance();
// iPad/tablette : on tolère mieux la dérive verticale naturelle du doigt.
if (isTabletLike()) return dx > tol || dy > (tol * 1.65);
return dx > tol || dy > tol;
}
function closeMenu() {
cancelLongPress();
menu.hidden = true;
menu.setAttribute("aria-hidden", "true");
menuOpen = false;
activePara = null;
}
function openMenuForPara(p) {
if (!p || !p.id) return;
activePara = p;
meta.textContent = p.id;
const propose = p.querySelector(".para-propose");
if (btnPropose) {
if (propose && propose.href) {
btnPropose.href = propose.href;
btnPropose.hidden = false;
} else {
btnPropose.hidden = true;
btnPropose.removeAttribute("href");
}
}
menu.hidden = false;
menu.setAttribute("aria-hidden", "false");
menuOpen = true;
__suppressReadingClickUntil = Date.now() + 700;
try {
__activeParaId = p.id;
setCurrentPara(p.id, "longpress");
updateResumeButton();
} catch {}
}
function copyParaLink(p) {
if (!p || !p.id) return Promise.resolve();
const u = new URL(window.location.href);
u.search = "";
u.hash = p.id;
return navigator.clipboard.writeText(u.toString());
}
function copyParaCitation(p) {
if (!p || !p.id) return Promise.resolve();
const pageUrl = new URL(window.location.href);
pageUrl.search = "";
pageUrl.hash = p.id;
const signature = `${docTitle}${docVersion ? ` (v${docVersion})` : ""} — ${pageUrl.toString()}`;
const paraTxt = getCleanParagraphText(p);
const payload = paraTxt ? `${paraTxt}\n\n${signature}` : signature;
return navigator.clipboard.writeText(payload);
}
function bookmarkPara(p) {
savePinnedBookmarkFromPara(p, "mobile-bookmark");
}
function handlePressStart(ev) {
if (!isMobileLike()) return;
if (ev.pointerType === "mouse") return;
if (isInteractiveTarget(ev.target)) return;
const p = ev.target?.closest?.('.reading p[id^="p-"]');
if (!p || !p.id) return;
startLongPressForPara(
p,
ev.clientX,
ev.clientY,
ev.pointerId ?? null
);
}
function handlePressMove(ev) {
if (!pressTimer) return;
if (pressPointerId != null && ev.pointerId != null && ev.pointerId !== pressPointerId) return;
if (movedTooFar(ev.clientX, ev.clientY)) {
cancelLongPress();
}
}
function handlePressEnd() {
clearPressTimer();
pressPointerId = null;
touchLongPressActive = false;
}
reading.addEventListener("pointerdown", handlePressStart, { passive: true });
reading.addEventListener("pointermove", handlePressMove, { passive: true });
reading.addEventListener("pointerup", handlePressEnd, { passive: true });
reading.addEventListener("pointercancel", handlePressEnd, { passive: true });
reading.addEventListener("pointerleave", handlePressEnd, { passive: true });
reading.addEventListener("touchstart", (ev) => {
if (!isMobileLike()) return;
if (isInteractiveTarget(ev.target)) return;
const touch = ev.changedTouches?.[0];
const p = ev.target?.closest?.('.reading p[id^="p-"]');
if (!touch || !p || !p.id) return;
startLongPressForPara(p, touch.clientX, touch.clientY, null);
}, { passive: true });
reading.addEventListener("touchmove", (ev) => {
if (!pressTimer) return;
const touch = ev.changedTouches?.[0];
if (!touch) return;
if (movedTooFar(touch.clientX, touch.clientY)) {
cancelLongPress();
}
}, { passive: true });
reading.addEventListener("touchend", () => {
cancelLongPress();
}, { passive: true });
reading.addEventListener("touchcancel", () => {
cancelLongPress();
}, { passive: true });
function handleScrollDuringMobileMenu() {
// Tant que le timer long-press est en cours, ne pas l'annuler brutalement
// sur tablette : iPadOS peut produire un micro-scroll fantôme.
if (pressTimer && isTabletLike()) return;
clearPressTimer();
if (menuOpen) closeMenu();
}
window.addEventListener("scroll", handleScrollDuringMobileMenu, { passive: true });
document.addEventListener("scroll", handleScrollDuringMobileMenu, { passive: true, capture: true });
document.addEventListener("click", (ev) => {
const p = ev.target?.closest?.('.reading p[id^="p-"]');
if (Date.now() < __suppressReadingClickUntil && p) {
ev.preventDefault();
ev.stopPropagation();
return;
}
if (!menuOpen) return;
const insideMenu = ev.target?.closest?.("#mobile-para-menu");
if (insideMenu) return;
closeMenu();
}, { capture: true });
document.addEventListener("contextmenu", (ev) => {
if (!isMobileLike()) return;
const p = ev.target?.closest?.('.reading p[id^="p-"]');
if (!p) return;
ev.preventDefault();
});
document.addEventListener("keydown", (ev) => {
if (ev.key === "Escape" && menuOpen) {
closeMenu();
}
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) closeMenu();
});
window.addEventListener("pagehide", closeMenu);
window.addEventListener("blur", () => {
if (menuOpen) closeMenu();
});
btnCopyLink.addEventListener("click", async () => {
if (!activePara) return;
try { await copyParaLink(activePara); } catch {}
closeMenu();
});
btnCite.addEventListener("click", async () => {
if (!activePara) return;
try { await copyParaCitation(activePara); } catch {}
closeMenu();
});
btnBookmark.addEventListener("click", () => {
if (!activePara) return;
bookmarkPara(activePara);
closeMenu();
});
btnPropose?.addEventListener("click", () => {
closeMenu();
});
window.addEventListener("resize", () => {
if (!isMobileLike()) closeMenu();
});
});
safe("propose-gate", () => {
if (isGlossaryEdition) return;
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);
});
}).catch((err) => {
console.warn("[proposer] gate failed; keeping Proposer hidden", err);
document.querySelectorAll(".para-propose").forEach((el) => hideEl(el));
});
});
let lastAuto = 0;
function writeLastSeen(id) {
if (!id) return;
if (autoTrackSuspended()) return;
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,
});
}
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;
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() {
if (autoTrackSuspended()) return;
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", () => {
if (isGlossaryEdition) return;
scheduleActive();
window.addEventListener("scroll", scheduleActive, { passive: true });
});
safe("hover-para", () => {
if (isGlossaryEdition) return;
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);
});
});
safe("click-para-align", () => {
if (isGlossaryEdition) return;
if (!reading) return;
reading.addEventListener("click", (ev) => {
try {
if (Date.now() < __suppressReadingClickUntil) {
ev.preventDefault();
ev.stopPropagation();
return;
}
const t = ev.target;
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;
const sel = window.getSelection ? window.getSelection() : null;
if (sel && !sel.isCollapsed) return;
__hoverParaId = "";
focusReadingTarget(p, {
behavior: "auto",
extra: 12,
open: true,
highlight: false,
updateHash: true,
remember: true,
kind: "click",
});
} catch {}
}, { passive: false });
});
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 = resolveReadingTargetById(clean);
if (!el) return false;
return focusReadingTarget(el, {
behavior,
extra: 12,
open: true,
highlight: true,
updateHash: true,
remember: true,
kind: "hash",
});
} 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 (!resolveReadingTargetById(id)) return;
ev.preventDefault();
handleHashTarget(id, "smooth");
});
window.addEventListener("hashchange", () => {
handleHashTarget((window.location.hash || "").slice(1), "auto");
});
});
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;
safe("toc-auto-collapse-top-zone", () => {
const tocGlobal = document.querySelector("[data-toc-global]");
const tocLocal = document.querySelector("[data-toc-local]");
if (!tocGlobal && !tocLocal) return;
const mqCompact = window.matchMedia("(max-width: 980px)");
let raf = 0;
function setTocOpen(nav, open, emit = false) {
if (!nav) return;
const toggle = nav.querySelector('button[aria-controls]');
if (!toggle) return;
nav.classList.toggle("is-collapsed", !open);
toggle.setAttribute("aria-expanded", open ? "true" : "false");
const bodyClip =
nav.querySelector(".toc-global__body-clip") ||
nav.querySelector(".toc-local__body-clip");
if (bodyClip) {
bodyClip.hidden = !open;
}
const key = nav.dataset.tocKey
? `archicratie:${nav.dataset.tocKey || ""}`
: nav.classList.contains("toc-local")
? `archicratie:toc-local:${window.location.pathname}`
: null;
if (key) {
try { localStorage.setItem(key, open ? "open" : "closed"); } catch {}
}
if (emit && open && nav.classList.contains("toc-local")) {
window.dispatchEvent(new CustomEvent("archicratie:tocLocalOpen"));
}
}
function nearTopZone() {
const y = window.scrollY || 0;
return y <= 120;
}
function syncAutoCollapse() {
// Sur mobile/tablette, les TOC restent fermées par défaut.
// Elles ne souvrent que par action explicite utilisateur.
if (mqCompact.matches) {
setTocOpen(tocGlobal, false, false);
setTocOpen(tocLocal, false, false);
return;
}
// Desktop : comportement historique conservé.
const shouldOpen = nearTopZone();
setTocOpen(tocGlobal, shouldOpen, false);
setTocOpen(tocLocal, shouldOpen, false);
}
function schedule() {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
syncAutoCollapse();
});
}
window.addEventListener("scroll", schedule, { passive: true });
window.addEventListener("resize", schedule);
if (mqCompact.addEventListener) {
mqCompact.addEventListener("change", schedule);
} else if (mqCompact.addListener) {
mqCompact.addListener(schedule);
}
schedule();
});
/*
* TOC accordions are owned by EditionToc.astro / LocalToc.astro.
* Keep only the top-zone auto-collapse below; the previous legacy
* mobile-toc-collapse binder toggled an unused .is-open state and
* added duplicate click listeners on the same headers.
*/
safe("reading-follow", () => {
if (!reading || !followEl) return;
const mqGlossaryCompact = window.matchMedia("(max-width: 860px)");
const mqGlossaryCompactLandscape = window.matchMedia(
"(orientation: landscape) and (max-width: 920px) and (max-height: 520px)"
);
const isGlossaryCompactViewport = () =>
isGlossaryEdition &&
(mqGlossaryCompact.matches || mqGlossaryCompactLandscape.matches);
function forceOpenGlossaryEntryDetails() {
if (!isGlossaryEntryMode || !reading) return;
reading.querySelectorAll("details.details-section").forEach((details) => {
details.open = true;
});
}
forceOpenGlossaryEntryDetails();
function disableFollowForCompactGlossary() {
followEl.classList.remove("is-on");
followEl.setAttribute("aria-hidden", "true");
followEl.style.display = "none";
if (rfH1) rfH1.hidden = true;
if (rfH2) rfH2.hidden = true;
if (rfH3) rfH3.hidden = true;
if (btnTopChapter) btnTopChapter.hidden = true;
if (btnTopSection) btnTopSection.hidden = true;
setRootVar("--followbar-h", "0px");
setRootVar("--sticky-offset-px", String(Math.round(headerH() + stickyGap())));
syncGlossaryFollowState(false);
body.classList.remove("glossary-portal-follow-on");
body.classList.remove("glossary-portal-hero-condensed");
body.classList.remove("glossary-portal-hero-expanded");
document.documentElement.style.setProperty("--glossary-sticky-top", "0px");
document.documentElement.style.setProperty("--glossary-follow-height", "0px");
document.documentElement.style.setProperty("--glossary-local-sticky-h", "0px");
}
if (isGlossaryHomeMode || isGlossaryCompactViewport()) {
forceOpenGlossaryEntryDetails();
disableFollowForCompactGlossary();
return;
}
const h1 = reading.querySelector("h1");
const topChapterLabel =
isGlossaryEntryMode
? "Haut de la fiche"
: isGlossaryEdition
? "Haut de la page"
: "Haut du chapitre";
if (btnTopChapter) {
btnTopChapter.setAttribute("aria-label", topChapterLabel);
btnTopChapter.setAttribute("title", topChapterLabel);
}
function scrollToTopChapter(behavior = "smooth") {
if (isGlossaryEdition) {
window.scrollTo({ top: 0, behavior });
return;
}
if (!h1) return;
focusReadingTarget(h1, {
behavior,
extra: 12,
open: true,
highlight: false,
updateHash: !!h1.id,
remember: true,
kind: "follow",
});
}
function getH2ScrollTarget(item) {
return item?.openTarget || item?.triggerEl || item?.anchor || item?.h2 || null;
}
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");
return {
id: s.id,
title,
anchor: s,
triggerEl: s,
openTarget: d || s,
scopeRoot: d || null,
h2,
details: d || null,
};
})
.filter(Boolean);
const h2Plain = Array.from(reading.querySelectorAll("h2[id]"))
.filter((h2) => !h2.closest("details.details-section"))
.map((h2) => ({
id: h2.id,
title: (h2.textContent || "").trim() || "Section",
anchor: h2,
triggerEl: h2,
openTarget: h2,
scopeRoot: null,
h2,
details: null,
}));
const H2 = [...h2Anchors, ...h2Plain]
.slice()
.sort((a, b) => absTop(a.triggerEl) - absTop(b.triggerEl));
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 lastVisibleH2 = null;
let lastVisibleH3 = null;
let lastY = window.scrollY || 0;
let lastOpenedH2 = "";
function computeLineY(followH) {
if (isGlossaryEntryMode) {
return glossaryEntryHeroBottom() + (followH || 0);
}
const localH = isGlossaryPortalMode ? getLocalStickyHeight() : 0;
return headerH() + stickyGap() + localH + (followH || 0) + HYST;
}
function hasCrossedTop(el, lineY) {
return Boolean(el && el.getBoundingClientRect().top <= lineY);
}
function hasCrossedBottom(el, lineY) {
return Boolean(el && el.getBoundingClientRect().bottom <= lineY);
}
function getH2DisplayEl(item) {
return item?.triggerEl || item?.anchor || item?.h2 || null;
}
function getH3DisplayEl(item) {
return item?.el || null;
}
function pickH2(lineY) {
if (!H2.length) return null;
let cand = null;
for (const t of H2) {
const box = getH2DisplayEl(t);
if (!box) continue;
if (box.getBoundingClientRect().top <= lineY) {
cand = t;
} else {
break;
}
}
if (isGlossaryEntryMode) {
return cand;
}
// Éditions longues : ne jamais afficher une section
// avant qu'elle ait réellement franchi la ligne de capture.
if (!isGlossaryEdition) {
return cand;
}
return cand || H2[0] || null;
}
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 scopeRoot = activeH2.scopeRoot || null;
let scoped = scopeRoot
? visible.filter((t) => t.el.closest?.("details") === scopeRoot)
: 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.triggerEl || activeH2.anchor || activeH2.h2 || null;
const endNode = nextH2 ? (nextH2.triggerEl || nextH2.anchor || nextH2.h2 || null) : null;
scoped = scoped.filter((t) => isAfter(startNode, t.el) && isBefore(endNode, t.el));
if (!scoped.length) return null;
let cand = null;
for (const t of scoped) {
const box = getH3DisplayEl(t);
if (!box) continue;
if (box.getBoundingClientRect().top <= lineY + HYST) {
cand = t;
} else {
break;
}
}
if (!isGlossaryEdition) {
return cand;
}
return cand || scoped[0] || null;
}
function maybeOpenActiveSection(activeH2, lineY) {
if (isGlossaryEntryMode) return;
if (!activeH2 || !activeH2.id) return;
const triggerEl = activeH2.triggerEl || activeH2.anchor || null;
const openTarget = activeH2.openTarget || null;
if (!triggerEl || !openTarget) return;
const pre = hasCrossedTop(triggerEl, lineY + 140);
if (!pre) return;
if (activeH2.id !== lastOpenedH2) {
lastOpenedH2 = activeH2.id;
openDetailsIfNeeded(openTarget);
}
}
function maybePreopenNextSection(activeH2) {
if (isGlossaryEntryMode) return;
if (!activeH2) return;
const i = H2.findIndex((t) => t.id === activeH2.id);
if (i < 0 || i + 1 >= H2.length) return;
const nextH2 = H2[i + 1];
const triggerEl = nextH2?.triggerEl || null;
const openTarget = nextH2?.openTarget || null;
if (!triggerEl || !openTarget) return;
const preloadDistance = 220;
const viewportBottom = window.innerHeight || document.documentElement.clientHeight || 0;
const top = triggerEl.getBoundingClientRect().top;
if (top <= viewportBottom + preloadDistance) {
openDetailsIfNeeded(openTarget);
}
}
function renderPass(lineY) {
const showH1 =
!isGlossaryEdition &&
!!(h1 && h1.getBoundingClientRect().bottom <= (lineY - HYST));
if (rfH1) {
rfH1.hidden = !showH1;
if (showH1) rfH1.textContent = (h1.textContent || "").trim();
}
const nextH2 = pickH2(lineY);
maybeOpenActiveSection(nextH2, lineY);
maybePreopenNextSection(nextH2);
const nextH3 = pickH3(lineY, nextH2);
curH2 = nextH2;
curH3 = nextH3;
if (rfH2) {
rfH2.hidden = !curH2;
if (curH2) {
rfH2.textContent = curH2.title;
}
}
if (rfH3) {
rfH3.hidden = !curH3;
if (curH3) {
rfH3.textContent = curH3.title;
}
}
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");
try {
syncTocLocal(
(curH3 && rfH3 && !rfH3.hidden) ? curH3.id :
(curH2 && rfH2 && !rfH2.hidden) ? curH2.id :
""
);
} catch {}
return any;
}
function updateFollow() {
if (isGlossaryCompactViewport()) {
disableFollowForCompactGlossary();
return;
}
followEl.style.display = "";
syncFollowTop();
const lineY0 = computeLineY(0);
renderPass(lineY0);
const inner = followEl.querySelector(".reading-follow__inner");
const followH0 =
followEl.classList.contains("is-on") && inner
? inner.getBoundingClientRect().height
: 0;
const lineY1 = computeLineY(followH0);
renderPass(lineY1);
const inner2 = followEl.querySelector(".reading-follow__inner");
const followH =
followEl.classList.contains("is-on") && inner2
? inner2.getBoundingClientRect().height
: 0;
const occupiedTop = isGlossaryEntryMode
? glossaryEntryHeroBottom() + followH
: headerH() + stickyGap() + (isGlossaryPortalMode ? getLocalStickyHeight() : 0) + followH;
setRootVar("--followbar-h", px(followH));
setRootVar("--sticky-offset-px", String(Math.round(occupiedTop)));
syncGlossaryFollowState(followEl.classList.contains("is-on") && followH > 0);
if (btnTopChapter) {
const showTopChapter =
isGlossaryEdition
? Boolean(followEl.classList.contains("is-on"))
: Boolean(rfH1 && !rfH1.hidden);
btnTopChapter.hidden = !showTopChapter;
}
if (btnTopSection) {
btnTopSection.hidden = !(curH2 && rfH2 && !rfH2.hidden);
}
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);
const onCompactChange = () => {
if (isGlossaryCompactViewport()) {
disableFollowForCompactGlossary();
} else {
updateFollow();
}
};
if (mqGlossaryCompact.addEventListener) {
mqGlossaryCompact.addEventListener("change", onCompactChange);
} else if (mqGlossaryCompact.addListener) {
mqGlossaryCompact.addListener(onCompactChange);
}
if (mqGlossaryCompactLandscape.addEventListener) {
mqGlossaryCompactLandscape.addEventListener("change", onCompactChange);
} else if (mqGlossaryCompactLandscape.addListener) {
mqGlossaryCompactLandscape.addListener(onCompactChange);
}
if (rfH1) {
rfH1.addEventListener("click", () => {
if (!h1) return;
focusReadingTarget(h1, {
behavior: "smooth",
extra: 12,
open: true,
highlight: false,
updateHash: !!h1.id,
remember: true,
kind: "follow",
});
});
}
if (rfH2) {
rfH2.addEventListener("click", () => {
if (!curH2) return;
const target = getH2ScrollTarget(curH2);
const hashTarget = curH2.triggerEl || curH2.anchor || curH2.h2 || target;
if (!target) return;
focusReadingTarget(target, {
behavior: "smooth",
extra: 12,
open: true,
highlight: false,
updateHash: true,
hashTarget,
remember: true,
kind: "follow",
});
});
}
if (rfH3) {
rfH3.addEventListener("click", () => {
if (!curH3) return;
focusReadingTarget(curH3.el, {
behavior: "smooth",
extra: 12,
open: true,
highlight: false,
updateHash: true,
remember: true,
kind: "follow",
});
});
}
if (btnTopChapter) {
btnTopChapter.addEventListener("click", () => {
scrollToTopChapter("smooth");
});
}
if (btnTopSection) {
btnTopSection.addEventListener("click", () => {
if (!curH2) return;
const target = getH2ScrollTarget(curH2);
const hashTarget = curH2.triggerEl || curH2.anchor || curH2.h2 || target;
if (!target) return;
focusReadingTarget(target, {
behavior: "smooth",
extra: 12,
open: true,
highlight: false,
updateHash: true,
hashTarget,
remember: true,
kind: "follow",
});
});
}
updateFollow();
const initialHash = (location.hash || "").slice(1);
if (initialHash) {
const el = resolveReadingTargetById(initialHash);
if (el) {
setTimeout(() => {
focusReadingTarget(el, {
behavior: "auto",
extra: 12,
open: true,
highlight: true,
updateHash: true,
remember: true,
kind: "hash",
});
scheduleActive();
}, 0);
}
}
});
})();
</script>
</body>
</html>