3243 lines
91 KiB
Plaintext
3243 lines
91 KiB
Plaintext
---
|
||
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 l’auth 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 s’ouvrent 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> |