Seed from NAS prod snapshot 20260130-190531
All checks were successful
CI / build-and-anchors (push) Successful in 1m25s
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (pull_request) Successful in 1m20s

This commit is contained in:
archicratia
2026-01-31 10:51:38 +00:00
commit 60d88939b0
142 changed files with 33443 additions and 0 deletions

View File

@@ -0,0 +1,831 @@
---
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 "../styles/global.css";
const {
title,
editionLabel,
editionKey,
statusLabel,
statusKey,
level,
version,
} = Astro.props;
const lvl = level ?? 1;
const canonical = Astro.site
? new URL(Astro.url.pathname, Astro.site).href
: Astro.url.href;
// Cible Gitea (injectée au build)
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 ?? "";
---
<!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 ?? "")}`} />
</head>
<body data-doc-title={title} data-doc-version={version}>
<header>
<SiteNav />
<div class="edition-bar">
<span class="badge"><strong>Édition</strong> : {editionLabel}</span>
<span class="badge"><strong>Statut</strong> : {statusLabel}</span>
<span class="badge"><strong>Niveau</strong> : {lvl}</span>
<span class="badge"><strong>Version</strong> : {version}</span>
<a class="resume-btn" id="resume-btn" href="#" hidden>Reprendre la lecture</a>
<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>
</div>
</main>
<!-- Bandeau lecture (H1/H2/H3 uniquement) -->
<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>
<!-- Actions : haut chapitre / haut section -->
<div class="rf-actions" aria-label="Navigation rapide">
<button
class="rf-btn"
id="rf-top-chapter"
type="button"
hidden
aria-label="Haut du chapitre"
title="Haut du chapitre"
>↑</button>
<button
class="rf-btn"
id="rf-top-section"
type="button"
hidden
aria-label="Haut de la section"
title="Haut de la section"
>↥</button>
</div>
</div>
</div>
<ProposeModal />
<style>
.page{
padding: var(--page-gap) 16px 48px;
}
.page-shell{
--aside-w: 320px;
--reading-w: 78ch;
--gap: 18px;
display: grid;
grid-template-columns: 1fr var(--aside-w) minmax(0, var(--reading-w)) 1fr;
column-gap: var(--gap);
align-items: start;
}
.page-aside{
grid-column: 2;
position: sticky;
/* colle sous header + padding haut */
top: calc(var(--sticky-header-h) + var(--page-gap));
/* ⛔️ plus de scroll ici */
overflow: visible;
max-height: none;
}
.page-aside__scroll{
/* ✅ le scroll est ici, donc englobe TOC global + local */
max-height: calc(100vh - (var(--sticky-header-h) + var(--page-gap) + 12px));
overflow: auto;
padding-right: 8px;
padding-top: 6px;
scrollbar-gutter: stable;
}
.reading{
grid-column: 3;
max-width: none;
margin: 0;
}
@media (max-width: 1100px){
.page-shell{
max-width: 1180px;
margin: 0 auto;
grid-template-columns: var(--aside-w) minmax(0, 1fr);
}
.page-aside{ grid-column: 1; }
.reading{
grid-column: 2;
max-width: 78ch;
margin: 0 auto;
}
}
@media (max-width: 860px){
.page-shell{
max-width: 78ch;
margin: 0 auto;
grid-template-columns: 1fr;
}
.page-aside{
grid-column: 1;
position: static;
overflow: visible;
margin-bottom: 12px;
}
.page-aside__scroll{
max-height: none;
overflow: visible;
padding-right: 0;
padding-top: 0;
}
.reading{
grid-column: 1;
max-width: 78ch;
margin: 0 auto;
}
}
</style>
<script is:inline define:vars={{ GITEA_BASE, GITEA_OWNER, GITEA_REPO }}>
(() => {
try {
const PAGE_GAP = 12; // doit matcher --page-gap (css)
const HYST = 4; // micro-hystérésis px
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");
function px(n){ return `${Math.max(0, Math.round(n))}px`; }
function setRootVar(name, value) {
document.documentElement.style.setProperty(name, value);
}
function headerH() {
return headerEl ? headerEl.getBoundingClientRect().height : 0;
}
function syncHeaderH() {
setRootVar("--sticky-header-h", px(headerH()));
setRootVar("--page-gap", px(PAGE_GAP));
}
function syncReadingRect() {
if (!reading) return;
const r = reading.getBoundingClientRect();
setRootVar("--reading-left", px(r.left));
setRootVar("--reading-width", px(r.width));
}
function syncHeadingMetrics() {
if (!reading) return;
const h1 = reading.querySelector("h1");
const h2 = reading.querySelector("h2");
const h3 = reading.querySelector("h3");
const sync = (el, prefix) => {
if (!el) return;
const cs = getComputedStyle(el);
if (cs.fontSize) setRootVar(`--rf-${prefix}-size`, cs.fontSize);
if (cs.lineHeight) setRootVar(`--rf-${prefix}-lh`, cs.lineHeight);
if (cs.fontWeight) setRootVar(`--rf-${prefix}-fw`, cs.fontWeight);
};
sync(h1, "h1");
sync(h2, "h2");
sync(h3, "h3");
}
syncHeaderH();
syncReadingRect();
syncHeadingMetrics();
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
syncHeaderH();
syncReadingRect();
syncHeadingMetrics();
}).catch(() => {});
}
window.addEventListener("resize", () => {
requestAnimationFrame(() => {
syncHeaderH();
syncReadingRect();
syncHeadingMetrics();
});
});
function getStickyOffsetPx() {
const raw = getComputedStyle(document.documentElement)
.getPropertyValue("--sticky-offset-px")
.trim();
const n = Number.parseFloat(raw);
return Number.isFinite(n) ? n : (headerH() + PAGE_GAP);
}
function absTop(el) {
const r = el.getBoundingClientRect();
return r.top + (window.scrollY || 0);
}
function scrollToElWithOffset(el, extra = 10, behavior = "auto") {
const y = absTop(el) - getStickyOffsetPx() - extra;
window.scrollTo({ top: Math.max(0, y), behavior });
}
function openDetailsIfNeeded(el) {
const d = el && el.closest && el.closest("details");
if (d && !d.open) d.open = true;
}
// ==========================
// Bookmarks (pinned + auto)
// ==========================
const PINNED_KEY = "archicratie:bookmark:pinned";
const LAST_KEY = `archicratie:bookmark:last:${window.location.pathname}`;
function readJSON(key) {
try { return JSON.parse(localStorage.getItem(key) || "null"); }
catch { return null; }
}
function writeJSON(key, obj) {
try { localStorage.setItem(key, JSON.stringify(obj)); } catch {}
}
function updateResumeButton() {
const btn = document.getElementById("resume-btn");
if (!btn) return;
const bm = readJSON(PINNED_KEY) || readJSON(LAST_KEY);
if (!bm || !bm.url) { btn.hidden = true; return; }
btn.hidden = false;
btn.href = bm.url;
btn.title = bm.title ? `Reprendre : ${bm.title}` : "Reprendre la lecture";
btn.onclick = (ev) => {
try {
const here = new URL(window.location.href);
here.search = "";
const target = new URL(bm.url, window.location.origin);
target.search = "";
if (here.pathname === target.pathname) {
ev.preventDefault();
const id = (target.hash || "").slice(1);
if (!id) return;
const el = document.getElementById(id);
if (!el) return;
openDetailsIfNeeded(el);
scrollToElWithOffset(el, 12, "auto");
history.replaceState(null, "", here.pathname + "#" + id);
}
} catch {}
};
}
// Nettoyage si un ancien bug a injecté ?body=... dans l'URL
if (window.location.search.includes("body=")) {
history.replaceState(null, "", window.location.pathname + window.location.hash);
}
// Hotfix compat citabilité : fallback #p-<idx>-<hash>
(function resolveLegacyParagraphHash() {
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(`[id^="${prefix}"]`);
if (!replacement) return;
console.warn("[anchors] legacy hash fallback used:", `#${oldId}`, "→", `#${replacement.id}`);
scrollToElWithOffset(replacement, 12, "auto");
})();
const docTitle = document.body.dataset.docTitle || document.title;
const docVersion = document.body.dataset.docVersion || "";
updateResumeButton();
// ==========================
// Gitea propose (optional)
// ==========================
const giteaReady = Boolean(GITEA_BASE && GITEA_OWNER && GITEA_REPO);
const FULL_TEXT_SOFT_LIMIT = 1600;
const URL_HARD_LIMIT = 6500;
const quoteBlock = (s) =>
String(s || "")
.split(/\r?\n/)
.map((l) => (`> ${l}`).trimEnd())
.join("\n");
function buildIssueURL(anchorId, fullText, excerpt) {
const base = String(GITEA_BASE).replace(/\/+$/, "");
const issue = new URL(`${base}/${GITEA_OWNER}/${GITEA_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 hasFull = Boolean(fullText && fullText.length);
const canTryFull = hasFull && fullText.length <= FULL_TEXT_SOFT_LIMIT;
const makeBody = (embedFull) => {
const header = [
`Chemin: ${path}`,
`URL locale: ${local.toString()}`,
`Ancre: #${anchorId}`,
`Version: ${docVersion || "(non renseignée)"}`,
`Type: type/correction`,
`State: state/recevable`,
``,
];
const texteActuel = embedFull
? [`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);
issue.searchParams.set("body", makeBody(Boolean(canTryFull)));
if (issue.toString().length > URL_HARD_LIMIT) {
issue.searchParams.set("body", makeBody(false));
}
return issue.toString();
}
// ==========================
// Paragraph tools (+ pinned bookmark)
// ==========================
const paras = Array.from(document.querySelectorAll('.reading p[id^="p-"]'));
for (const p of paras) {
if (p.querySelector(".para-tools")) continue;
const tools = document.createElement("span");
tools.className = "para-tools";
const row = document.createElement("span");
row.className = "para-tools-row";
const a = document.createElement("a");
a.className = "para-anchor";
a.href = `#${p.id}`;
a.setAttribute("aria-label", "Lien vers ce paragraphe");
a.textContent = "¶";
const citeBtn = document.createElement("button");
citeBtn.type = "button";
citeBtn.className = "para-cite";
citeBtn.textContent = "Citer";
citeBtn.addEventListener("click", async () => {
const pageUrl = new URL(window.location.href);
pageUrl.search = "";
pageUrl.hash = p.id;
const cite = `${docTitle}${docVersion ? ` (v${docVersion})` : ""} — ${pageUrl.toString()}`;
try {
await navigator.clipboard.writeText(cite);
const prev = citeBtn.textContent;
citeBtn.textContent = "Copié";
setTimeout(() => (citeBtn.textContent = prev), 900);
} catch {
window.prompt("Copiez la citation :", cite);
}
});
row.appendChild(a);
row.appendChild(citeBtn);
if (giteaReady) {
const propose = document.createElement("a");
propose.className = "para-propose";
propose.textContent = "Proposer";
propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
const raw = (p.textContent || "").trim().replace(/\s+/g, " ");
const excerpt = raw.length > 420 ? (raw.slice(0, 420) + "…") : raw;
const issueUrl = buildIssueURL(p.id, raw, excerpt);
propose.href = issueUrl;
// 🔥 indispensables pour réactiver ProposeModal (les 2 filtres)
propose.dataset.propose = "1";
propose.dataset.url = issueUrl;
// optionnel mais top : permet à ProposeModal de copier le texte complet
propose.dataset.full = raw;
// ✅ CRUCIAL : permet à ProposeModal dintercepter le clic (2 étapes)
propose.setAttribute("data-propose", "1");
propose.setAttribute("data-url", issueUrl);
// ✅ Bonus : ProposeModal peut copier/upgrade avec le texte complet si dispo
// (tu peux commenter si tu veux éviter de stocker le texte en attribut)
propose.setAttribute("data-full", raw);
// ✅ Fallback si JS casse : on ouvre quand même le lien (idéalement en nouvel onglet)
propose.href = issueUrl;
propose.target = "_blank";
propose.rel = "noopener noreferrer";
row.appendChild(propose);
}
tools.appendChild(row);
const bmBtn = document.createElement("button");
bmBtn.type = "button";
bmBtn.className = "para-bookmark";
bmBtn.textContent = "Marque-page";
bmBtn.addEventListener("click", () => {
const pageUrl = new URL(window.location.href);
pageUrl.search = "";
pageUrl.hash = p.id;
writeJSON(PINNED_KEY, {
url: pageUrl.toString(),
path: pageUrl.pathname,
anchor: p.id,
title: docTitle,
version: docVersion,
ts: Date.now(),
});
updateResumeButton();
const prev = bmBtn.textContent;
bmBtn.textContent = "Marqué ✓";
setTimeout(() => (bmBtn.textContent = prev), 900);
});
tools.appendChild(bmBtn);
p.appendChild(tools);
}
// Auto-checkpoint
let lastAuto = 0;
function writeLastSeen(id) {
const now = Date.now();
if (now - lastAuto < 800) return;
lastAuto = now;
const u = new URL(window.location.href);
u.search = "";
u.hash = id;
writeJSON(LAST_KEY, {
url: u.toString(),
path: u.pathname,
anchor: id,
title: docTitle,
version: docVersion,
ts: now,
});
}
const ioPara = new IntersectionObserver((entries) => {
const best = entries
.filter(e => e.isIntersecting)
.sort((a,b) => (b.intersectionRatio - a.intersectionRatio))[0];
if (!best) return;
const el = best.target;
if (el && el.id) {
writeLastSeen(el.id);
updateResumeButton();
}
}, { threshold: [0.6] });
for (const p of paras) ioPara.observe(p);
// ==========================
// Reading-follow (H1/H2/H3)
// ==========================
const h1 = reading ? reading.querySelector("h1") : null;
// H2 : soit via details-anchor (pour citabilité stable), soit via h2[id]
const h2Anchors = Array.from(document.querySelectorAll("span.details-anchor[id]"))
.map((s) => {
const d = s.closest("details");
const h2 = d ? d.querySelector(".details-body h2") : null;
const title = (h2?.textContent || "").trim() || "Section";
return h2 ? { id: s.id, anchor: s, marker: s, title, h2 } : null;
})
.filter(Boolean);
const h2Plain = Array.from(document.querySelectorAll(".reading h2[id]"))
.map((h2) => ({
id: h2.id,
anchor: h2,
marker: h2,
title: (h2.textContent || "").trim() || "Section",
h2
}));
// On préfère le mode "anchors" s'il existe
const H2 = (h2Anchors.length ? h2Anchors : h2Plain)
.slice()
.sort((a,b) => absTop(a.marker) - absTop(b.marker));
const H3 = Array.from(document.querySelectorAll(".reading h3[id]"))
.map((h3) => ({ id: h3.id, el: h3, title: (h3.textContent || "").trim() || "Sous-section" }));
let curH2 = null;
let curH3 = null;
let lastY = window.scrollY || 0;
function computeLineY(followH) {
// “ligne” = bas du bandeau (header + page-gap + bandeau)
return headerH() + PAGE_GAP + (followH || 0) + HYST;
}
function pickH2(lineY) {
let cand = null;
for (const t of H2) {
const top = t.marker.getBoundingClientRect().top;
if (top <= lineY) cand = t; else break;
}
if (!cand && H2.length) cand = H2[0];
const down = (window.scrollY || 0) >= lastY;
if (!down && cand && curH2 && cand.id !== curH2.id) {
const topNew = cand.marker.getBoundingClientRect().top;
if (topNew > lineY - (HYST * 2)) cand = curH2;
}
return cand;
}
function isAfter(start, el) {
if (!start || !el) return false;
return Boolean(start.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
}
function isBefore(end, el) {
if (!end || !el) return true; // pas de borne haute => OK
return Boolean(end.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING);
}
function pickH3(lineY, activeH2) {
if (!activeH2) return null;
// H3 visibles (pas dans un details fermé)
const visible = H3.filter((t) => {
const d = t.el.closest?.("details");
return !(d && !d.open);
});
// Scope : même <details> si applicable (plus robuste)
const d2 = activeH2.h2?.closest?.("details") || activeH2.anchor?.closest?.("details") || null;
let scoped = d2 ? visible.filter((t) => t.el.closest?.("details") === d2) : visible;
// Scope DOM : entre H2 courant et H2 suivant (gère aussi les pages "plates")
const i2 = H2.findIndex((t) => t.id === activeH2.id);
const nextH2 = (i2 >= 0 && i2 + 1 < H2.length) ? H2[i2 + 1] : null;
const startNode = activeH2.anchor || activeH2.h2;
const endNode = nextH2 ? (nextH2.anchor || nextH2.h2) : null;
scoped = scoped.filter((t) => isAfter(startNode, t.el) && isBefore(endNode, t.el));
// Choisir le dernier H3 du scope qui a passé la ligne
let cand = null;
for (const t of scoped) {
const top = t.el.getBoundingClientRect().top;
if (top <= lineY) cand = t;
else break;
}
return cand;
}
function updateFollow() {
if (!followEl) return;
// PASS 1 : H1 visible quand le H1 du reading passe sous la barre
const baseY = headerH() + PAGE_GAP;
const showH1 = !!(h1 && (h1.getBoundingClientRect().bottom <= baseY + HYST));
if (rfH1 && h1 && showH1) {
rfH1.hidden = false;
rfH1.textContent = (h1.textContent || "").trim();
} else if (rfH1) {
rfH1.hidden = true;
}
// PASS 2 : estimation H2/H3 sans bandeau (pour mesurer hauteur réelle)
const lineY0 = computeLineY(0);
curH2 = pickH2(lineY0);
curH3 = pickH3(lineY0, curH2);
if (rfH2 && curH2) { rfH2.hidden = false; rfH2.textContent = curH2.title; }
else if (rfH2) rfH2.hidden = true;
if (rfH3 && curH3) { rfH3.hidden = false; rfH3.textContent = curH3.title; }
else if (rfH3) rfH3.hidden = true;
const any0 = (rfH1 && !rfH1.hidden) || (rfH2 && !rfH2.hidden) || (rfH3 && !rfH3.hidden);
followEl.classList.toggle("is-on", Boolean(any0));
followEl.setAttribute("aria-hidden", any0 ? "false" : "true");
const inner = followEl.querySelector(".reading-follow__inner");
const followH = (any0 && inner) ? inner.getBoundingClientRect().height : 0;
// PASS 3 : recalc H2/H3 avec la vraie ligne (bas du bandeau)
const lineY = computeLineY(followH);
curH2 = pickH2(lineY);
curH3 = pickH3(lineY, curH2);
// H2 napparaît que sil est “actif” (titre passé sous la ligne)
if (rfH2 && curH2) {
const on = curH2.marker.getBoundingClientRect().top <= lineY;
rfH2.hidden = !on;
if (on) rfH2.textContent = curH2.title;
} else if (rfH2) rfH2.hidden = true;
// H3 : actif seulement si un H3 du H2 actif a passé la ligne
if (rfH3 && curH3) {
const on = curH3.el.getBoundingClientRect().top <= lineY;
rfH3.hidden = !on;
if (on) rfH3.textContent = curH3.title;
} else if (rfH3) rfH3.hidden = true;
const any = (rfH1 && !rfH1.hidden) || (rfH2 && !rfH2.hidden) || (rfH3 && !rfH3.hidden);
followEl.classList.toggle("is-on", Boolean(any));
followEl.setAttribute("aria-hidden", any ? "false" : "true");
const inner2 = followEl.querySelector(".reading-follow__inner");
const followH2 = (any && inner2) ? inner2.getBoundingClientRect().height : 0;
setRootVar("--followbar-h", px(followH2));
setRootVar("--sticky-offset-px", String(Math.round(headerH() + PAGE_GAP + followH2)));
// Boutons actions
if (btnTopChapter) btnTopChapter.hidden = !any;
if (btnTopSection) {
const ok = Boolean(curH2 && rfH2 && !rfH2.hidden);
btnTopSection.hidden = !ok;
}
lastY = window.scrollY || 0;
// recalage horizontal
syncReadingRect();
}
let ticking = false;
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
updateFollow();
});
};
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
// clics bandeau
if (rfH1) rfH1.addEventListener("click", () => {
if (!h1) return;
scrollToElWithOffset(h1, 12, "smooth");
});
if (rfH2) rfH2.addEventListener("click", () => {
if (!curH2) return;
openDetailsIfNeeded(curH2.anchor);
scrollToElWithOffset(curH2.anchor, 12, "smooth");
history.replaceState(null, "", `${window.location.pathname}#${curH2.id}`);
});
if (rfH3) rfH3.addEventListener("click", () => {
if (!curH3) return;
openDetailsIfNeeded(curH3.el);
scrollToElWithOffset(curH3.el, 12, "smooth");
history.replaceState(null, "", `${window.location.pathname}#${curH3.id}`);
});
// clics actions
if (btnTopChapter) btnTopChapter.addEventListener("click", () => {
if (!h1) return;
scrollToElWithOffset(h1, 12, "smooth");
});
if (btnTopSection) btnTopSection.addEventListener("click", () => {
if (!curH2) return;
openDetailsIfNeeded(curH2.anchor);
scrollToElWithOffset(curH2.anchor, 12, "smooth");
history.replaceState(null, "", `${window.location.pathname}#${curH2.id}`);
});
updateFollow();
// arrivée avec hash => ouvrir + offset
const initialHash = (location.hash || "").slice(1);
if (initialHash) {
const el = document.getElementById(initialHash);
if (el) {
openDetailsIfNeeded(el);
setTimeout(() => scrollToElWithOffset(el, 12, "auto"), 0);
}
}
} catch (err) {
console.error("[EditionLayout] init failed:", err);
}
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
---
import SiteNav from "../components/SiteNav.astro";
import BuildStamp from "../components/BuildStamp.astro";
import "../styles/global.css";
const { title } = Astro.props;
const canonical = Astro.site
? new URL(Astro.url.pathname, Astro.site).href
: Astro.url.href;
---
<!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" />
</head>
<body>
<header>
<SiteNav />
</header>
<main>
<article class="reading">
<slot />
<BuildStamp />
</article>
</main>
</body>
</html>