Files
archicratie-edition/src/pages/glossaire/index-complet.astro
Archicratia a9f2a5bbd4
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 54s
CI / build-and-anchors (pull_request) Successful in 43s
feat(glossaire): harmonize portal pages and sticky reading ux
2026-03-26 12:58:17 +01:00

827 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import GlossaryLayout from "../../layouts/GlossaryLayout.astro";
import GlossaryPortalAside from "../../components/GlossaryPortalAside.astro";
import GlossaryPortalHero from "../../components/GlossaryPortalHero.astro";
import GlossaryPortalSection from "../../components/GlossaryPortalSection.astro";
import GlossaryPortalStickySync from "../../components/GlossaryPortalStickySync.astro";
import { getCollection } from "astro:content";
import { hrefOfGlossaryEntry } from "../../lib/glossary";
const entries = await getCollection("glossaire");
const collator = new Intl.Collator("fr", { sensitivity: "base", numeric: true });
const kindLabels = {
concept: "Concept",
diagnostic: "Diagnostic",
topologie: "Topologie",
verbe: "Verbe",
paradigme: "Paradigme",
doctrine: "Doctrine",
dispositif: "Dispositif",
figure: "Figure",
qualification: "Qualification",
epistemologie: "Épistémologie",
};
const domainLabels = {
transversal: "Transversal",
theorie: "Théorie",
"cas-ia": "Cas IA",
};
const levelLabels = {
fondamental: "Fondamental",
intermediaire: "Intermédiaire",
avance: "Avancé",
};
const sorted = [...entries].sort((a, b) => collator.compare(a.data.term, b.data.term));
function groupByInitial(list) {
const map = new Map();
for (const entry of list) {
const letter = (entry.data.term || "").trim().charAt(0).toUpperCase() || "#";
if (!map.has(letter)) map.set(letter, []);
map.get(letter).push(entry);
}
return [...map.entries()].sort((a, b) => collator.compare(a[0], b[0]));
}
const groupedAlpha = groupByInitial(sorted);
const totalEntries = sorted.length;
const totalLetters = groupedAlpha.length;
const paradigmesCount = entries.filter((entry) => entry.data.kind === "paradigme").length;
const doctrinesCount = entries.filter((entry) => entry.data.kind === "doctrine").length;
const verbesCount = entries.filter((entry) => entry.data.kind === "verbe").length;
const casIaCount = entries.filter((entry) => entry.data.domain === "cas-ia").length;
const pageItems = [
{ href: "#index-alphabetique", label: "Index alphabétique" },
{ href: "#prolonger-la-lecture", label: "Prolonger la lecture" },
];
const usefulLinks = [
{ href: "/glossaire/", label: "Accueil du glossaire" },
{ href: "/glossaire/concepts-fondamentaux/", label: "Concepts fondamentaux" },
{ href: "/glossaire/paradigme-archicratique/", label: "Paradigme archicratique" },
{ href: "/glossaire/scenes-archicratiques/", label: "Scènes archicratiques" },
{ href: "/glossaire/dynamiques-archicratiques/", label: "Dynamiques archicratiques" },
{ href: "/glossaire/tensions-irreductibles/", label: "Tensions irréductibles" },
{ href: "/glossaire/paradigmes/", label: "Paradigmes et doctrines" },
{ href: "/glossaire/archicrations/", label: "Méta-régimes archicratiques" },
];
const prolongerLinks = [
{
href: "/glossaire/",
title: "Accueil du glossaire",
text: "Revenir à la cartographie générale du système archicratique.",
},
{
href: "/glossaire/concepts-fondamentaux/",
title: "Concepts fondamentaux",
text:
"Repartir du noyau minimal : arcalité, cratialité, tension, archicration, co-viabilité, archicratie.",
},
{
href: "/glossaire/paradigmes/",
title: "Paradigmes et doctrines",
text:
"Retrouver le paysage théorique au sein duquel larchicratie se situe et se distingue.",
},
{
href: "/glossaire/archicrations/",
title: "Méta-régimes archicratiques",
text:
"Parcourir les grandes formes de co-viabilité et leurs modulations historiques.",
},
];
---
<GlossaryLayout
title="Index complet du glossaire"
version="1.0"
stickyMode="glossary-portal"
>
<Fragment slot="aside">
<div class="gic-aside-stack">
<GlossaryPortalAside
ariaLabel="Navigation de lindex complet du glossaire"
title="Index complet"
meta={`${totalEntries} entrée${totalEntries > 1 ? "s" : ""} · ${totalLetters} lettre${totalLetters > 1 ? "s" : ""}`}
pageItems={pageItems}
usefulLinks={usefulLinks}
/>
<div class="gic-aside__block">
<h2 class="gic-aside__heading">Repères de lecture</h2>
<div class="gic-aside__pills" aria-label="Repères de lindex">
<span class="gic-aside__pill">
{paradigmesCount} paradigme{paradigmesCount > 1 ? "s" : ""}
</span>
<span class="gic-aside__pill">
{doctrinesCount} doctrine{doctrinesCount > 1 ? "s" : ""}
</span>
<span class="gic-aside__pill">
{verbesCount} verbe{verbesCount > 1 ? "s" : ""}
</span>
<span class="gic-aside__pill">
{casIaCount} entrée{casIaCount > 1 ? "s" : ""} cas IA
</span>
</div>
<p class="gic-aside__note">
Cet index complète les portails thématiques : il permet de retrouver
rapidement une fiche, puis de repartir vers les grandes cartographies du glossaire.
</p>
</div>
</div>
</Fragment>
<section class="gic-page">
<GlossaryPortalHero
prefix="gic"
kicker="Référentiel terminologique"
title="Index complet du glossaire"
intro="Cette page rassemble lensemble des entrées du glossaire dans un ordre alphabétique intégral."
moreParagraphs={[
"Elle complète laccueil conceptuel du glossaire par une navigation plus encyclopédique, plus directe et plus exhaustive.",
]}
introMaxWidth="72ch"
followIntroMaxWidth="68ch"
moreMaxHeight="12rem"
/>
<GlossaryPortalSection
id="index-alphabetique"
title="Index alphabétique"
count={`${totalEntries} entrée${totalEntries > 1 ? "s" : ""}`}
intro="Naviguer par lettre permet de retrouver rapidement chaque fiche du glossaire, tout en conservant ses principaux marqueurs de lecture."
>
<nav class="gic-letters" id="gic-letters-source" aria-label="Lettres de lindex">
{groupedAlpha.map(([letter]) => (
<a href={`#letter-${letter}`}>{letter}</a>
))}
</nav>
<nav
class="gic-follow-letters"
id="gic-follow-letters"
aria-label="Lettres de lindex flottant"
aria-hidden="true"
hidden
>
{groupedAlpha.map(([letter]) => (
<a href={`#letter-${letter}`}>{letter}</a>
))}
</nav>
</GlossaryPortalSection>
<div class="gic-groups">
{groupedAlpha.map(([letter, items]) => (
<section class="gic-section gic-group" id={`letter-${letter}`}>
<div class="gic-section__head">
<h2>{letter}</h2>
<span class="gic-section__count">
{items.length} entrée{items.length > 1 ? "s" : ""}
</span>
</div>
<div class="gic-list">
{items.map((entry) => (
<article class="gic-item">
<a class="gic-term" href={hrefOfGlossaryEntry(entry)}>
{entry.data.term}
</a>
<p class="gic-def">{entry.data.definitionShort}</p>
<p class="gic-meta">
<span>{kindLabels[entry.data.kind] ?? entry.data.kind}</span>
<span>{domainLabels[entry.data.domain] ?? entry.data.domain}</span>
<span>{levelLabels[entry.data.level] ?? entry.data.level}</span>
</p>
</article>
))}
</div>
</section>
))}
</div>
<GlossaryPortalSection
id="prolonger-la-lecture"
title="Prolonger la lecture"
intro="Cet index intégral complète les portails thématiques sans sy substituer. Il permet de repartir ensuite vers les grandes cartographies déjà stabilisées du glossaire."
final={true}
>
<div class="gic-cards">
{prolongerLinks.map((item) => (
<a class="gic-card" href={item.href}>
<strong>{item.title}</strong>
<span>{item.text}</span>
</a>
))}
</div>
</GlossaryPortalSection>
</section>
<GlossaryPortalStickySync
heroMoreId="gic-hero-more"
heroToggleId="gic-hero-toggle"
/>
<script is:inline>
(() => {
const boot = () => {
const body = document.body;
const root = document.documentElement;
const follow = document.getElementById("reading-follow");
const lettersSource = document.getElementById("gic-letters-source");
const lettersFollow = document.getElementById("gic-follow-letters");
const groupSections = Array.from(document.querySelectorAll(".gic-group"));
if (!body || !root || !follow || !lettersSource || !lettersFollow || groupSections.length === 0) {
return;
}
const BODY_CLASS = "is-index-complet-page";
const LETTERS_DOCKED_CLASS = "gic-letters-docked";
const ACTIVE_CLASS = "is-active";
const mqMobile = window.matchMedia("(max-width: 860px)");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const EXTRA_GAP = 12;
const MAX_CORRECTION_PASSES = 8;
const sourceLinks = Array.from(lettersSource.querySelectorAll("a"));
const followLinks = Array.from(lettersFollow.querySelectorAll("a"));
const allLetterLinks = [...sourceLinks, ...followLinks];
body.classList.add(BODY_CLASS);
const computeFollowOn = () =>
!mqMobile.matches &&
follow.classList.contains("is-on") &&
follow.style.display !== "none" &&
follow.getAttribute("aria-hidden") !== "true";
const setAnchorOffset = (value) => {
root.style.setProperty(
"--gic-follow-letters-offset",
`${Math.max(0, Math.round(value))}px`
);
};
const syncFollowLettersTop = () => {
const inner = follow.querySelector(".reading-follow__inner");
const followIsOn =
follow.classList.contains("is-on") &&
follow.style.display !== "none" &&
follow.getAttribute("aria-hidden") !== "true";
if (!followIsOn || !inner) {
lettersFollow.style.top = "";
return;
}
const followRect = follow.getBoundingClientRect();
const innerRect = inner.getBoundingClientRect();
const top = followRect.top + innerRect.height;
lettersFollow.style.top = `${Math.round(top)}px`;
};
const syncLettersDockState = () => {
if (mqMobile.matches || !computeFollowOn()) {
body.classList.remove(LETTERS_DOCKED_CLASS);
lettersFollow.hidden = true;
lettersFollow.setAttribute("aria-hidden", "true");
setAnchorOffset(0);
return;
}
const inner = follow.querySelector(".reading-follow__inner");
if (!inner) {
body.classList.remove(LETTERS_DOCKED_CLASS);
lettersFollow.hidden = true;
lettersFollow.setAttribute("aria-hidden", "true");
setAnchorOffset(0);
return;
}
const sourceRect = lettersSource.getBoundingClientRect();
const innerRect = inner.getBoundingClientRect();
const shouldDock = sourceRect.top <= (innerRect.bottom + 6);
body.classList.toggle(LETTERS_DOCKED_CLASS, shouldDock);
lettersFollow.hidden = !shouldDock;
lettersFollow.setAttribute("aria-hidden", shouldDock ? "false" : "true");
if (shouldDock) {
const dockedHeight = lettersFollow.getBoundingClientRect().height || 0;
setAnchorOffset(dockedHeight + EXTRA_GAP);
} else {
setAnchorOffset(0);
}
};
const getAnchorViewportTop = () => {
const inner = follow.querySelector(".reading-follow__inner");
const followOn = computeFollowOn();
if (!followOn || !inner) {
return 12;
}
const followLettersVisible =
!lettersFollow.hidden &&
lettersFollow.getAttribute("aria-hidden") === "false";
if (followLettersVisible) {
return Math.round(lettersFollow.getBoundingClientRect().bottom + EXTRA_GAP);
}
return Math.round(inner.getBoundingClientRect().bottom + EXTRA_GAP);
};
const getTargetHead = (section) =>
section?.querySelector(".gic-section__head") || section;
const correctAnchorAlignment = (section, pass = 0) => {
if (!section || pass >= MAX_CORRECTION_PASSES) return;
requestAnimationFrame(() => {
syncFollowLettersTop();
syncLettersDockState();
const head = getTargetHead(section);
if (!head) return;
const wantedTop = getAnchorViewportTop();
const headTop = head.getBoundingClientRect().top;
const delta = headTop - wantedTop;
if (Math.abs(delta) <= 1) return;
window.scrollBy({
top: delta,
behavior: "auto",
});
correctAnchorAlignment(section, pass + 1);
});
};
const scrollToLetterTarget = (targetId) => {
const section = document.getElementById(targetId);
if (!section) return;
const head = getTargetHead(section);
if (!head) return;
syncFollowLettersTop();
syncLettersDockState();
const targetTop = getAnchorViewportTop();
const rect = head.getBoundingClientRect();
const absoluteTop = window.scrollY + rect.top - targetTop;
if (window.location.hash !== `#${targetId}`) {
history.pushState(null, "", `#${targetId}`);
}
window.scrollTo({
top: Math.max(0, Math.round(absoluteTop)),
behavior: reducedMotion.matches ? "auto" : "smooth",
});
requestAnimationFrame(() => {
requestAnimationFrame(() => {
correctAnchorAlignment(section, 0);
});
});
window.setTimeout(() => correctAnchorAlignment(section, 0), 120);
window.setTimeout(() => correctAnchorAlignment(section, 0), 260);
};
const syncActiveLetter = () => {
let activeId = null;
const inner = follow.querySelector(".reading-follow__inner");
const followOn = computeFollowOn();
const followBottom = followOn && inner
? inner.getBoundingClientRect().bottom
: 0;
const dockedHeight =
!lettersFollow.hidden && lettersFollow.getAttribute("aria-hidden") === "false"
? lettersFollow.getBoundingClientRect().height
: 0;
const threshold = followBottom > 0
? followBottom + dockedHeight + 12
: 140;
for (const section of groupSections) {
const rect = section.getBoundingClientRect();
if (rect.top <= threshold) {
activeId = section.id;
} else {
break;
}
}
if (!activeId && groupSections[0]) {
activeId = groupSections[0].id;
}
const applyState = (links) => {
links.forEach((link) => {
const isActive = link.getAttribute("href") === `#${activeId}`;
link.classList.toggle(ACTIVE_CLASS, isActive);
if (isActive) {
link.setAttribute("aria-current", "true");
} else {
link.removeAttribute("aria-current");
}
});
};
applyState(sourceLinks);
applyState(followLinks);
};
const handleLetterClick = (event) => {
const link = event.currentTarget;
const href = link?.getAttribute("href") || "";
if (!href.startsWith("#letter-")) return;
event.preventDefault();
scrollToLetterTarget(href.slice(1));
};
allLetterLinks.forEach((link) => {
link.addEventListener("click", handleLetterClick);
});
let raf = 0;
const schedule = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
syncFollowLettersTop();
syncLettersDockState();
syncActiveLetter();
});
};
const onScroll = () => {
schedule();
};
const onHashChange = () => {
const id = window.location.hash.replace(/^#/, "");
if (!id.startsWith("letter-")) return;
scrollToLetterTarget(id);
};
const followObserver = new MutationObserver(schedule);
followObserver.observe(follow, {
attributes: true,
attributeFilter: ["class", "style", "aria-hidden"],
subtree: false,
});
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", schedule);
window.addEventListener("pageshow", schedule);
window.addEventListener("hashchange", onHashChange);
if (document.fonts?.ready) {
document.fonts.ready.then(schedule).catch(() => {});
}
if (mqMobile.addEventListener) {
mqMobile.addEventListener("change", schedule);
} else if (mqMobile.addListener) {
mqMobile.addListener(schedule);
}
if (reducedMotion.addEventListener) {
reducedMotion.addEventListener("change", schedule);
} else if (reducedMotion.addListener) {
reducedMotion.addListener(schedule);
}
setAnchorOffset(0);
schedule();
if (window.location.hash.startsWith("#letter-")) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollToLetterTarget(window.location.hash.slice(1));
});
});
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
})();
</script>
</GlossaryLayout>
<style>
.gic-page{
--gic-follow-letters-offset: 0px;
padding: 8px 0 24px;
}
.gic-aside-stack{
display: flex;
flex-direction: column;
gap: 14px;
}
.gic-aside__block{
border: 1px solid rgba(127,127,127,0.22);
border-radius: 16px;
padding: 12px;
background: rgba(127,127,127,0.05);
}
.gic-aside__heading{
margin: 0 0 10px;
font-size: 13px;
font-weight: 800;
opacity: .9;
}
.gic-aside__pills{
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.gic-aside__pill{
display: inline-flex;
align-items: center;
padding: 4px 9px;
border: 1px solid rgba(127,127,127,0.24);
border-radius: 999px;
background: rgba(127,127,127,0.04);
font-size: 12px;
line-height: 1.3;
opacity: .9;
}
.gic-aside__note{
margin: 10px 0 0;
font-size: 12px;
line-height: 1.45;
opacity: .78;
}
.gic-letters{
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.gic-letters a,
.gic-follow-letters a{
min-width: 34px;
text-align: center;
border: 1px solid rgba(127,127,127,0.24);
border-radius: 10px;
padding: 5px 8px;
text-decoration: none;
transition:
transform 120ms ease,
background 120ms ease,
border-color 120ms ease,
box-shadow 120ms ease,
color 120ms ease;
}
.gic-letters a:hover,
.gic-follow-letters a:hover{
transform: translateY(-1px);
background: rgba(127,127,127,0.08);
border-color: rgba(0,217,255,0.16);
text-decoration: none;
}
.gic-letters a.is-active,
.gic-follow-letters a.is-active,
.gic-letters a[aria-current="true"],
.gic-follow-letters a[aria-current="true"]{
border-color: rgba(0,217,255,0.34);
background:
linear-gradient(180deg, rgba(0,217,255,0.14), rgba(0,217,255,0.06)),
rgba(127,127,127,0.10);
color: var(--glossary-accent, #00d9ff);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.05),
0 0 0 1px rgba(0,217,255,0.06);
}
.gic-follow-letters{
position: fixed;
left: var(--reading-left);
width: var(--reading-width);
box-sizing: border-box;
z-index: 59;
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 12px 10px;
border: 1px solid rgba(127,127,127,.20);
border-top: 0;
border-radius: 0 0 14px 14px;
background: rgba(255,255,255,.86);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 10px 22px rgba(0,0,0,.06);
opacity: 0;
transform: translateY(-6px);
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease;
}
:global(body.is-index-complet-page.gic-letters-docked .gic-follow-letters){
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
:global(body.is-index-complet-page.gic-letters-docked #reading-follow .reading-follow__inner){
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.gic-groups{
display: flex;
flex-direction: column;
gap: 28px;
}
.gic-group{
scroll-margin-top: calc(
var(--sticky-offset-px, 96px) + 28px + var(--gic-follow-letters-offset, 0px)
);
}
.gic-section{
margin-top: 34px;
}
.gic-section__head{
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
scroll-margin-top: calc(
var(--sticky-offset-px, 96px) + 28px + var(--gic-follow-letters-offset, 0px)
);
}
.gic-section__head h2{
margin: 0;
scroll-margin-top: calc(
var(--sticky-offset-px, 96px) + 28px + var(--gic-follow-letters-offset, 0px)
);
}
.gic-section__count{
font-size: 13px;
opacity: .72;
white-space: nowrap;
}
.gic-list{
display: grid;
gap: 12px;
}
.gic-item{
border: 1px solid rgba(127,127,127,0.20);
border-radius: 16px;
padding: 14px 16px;
background: rgba(127,127,127,0.04);
}
.gic-term{
display: inline-block;
font-weight: 800;
font-size: 1.04rem;
line-height: 1.3;
text-decoration: none;
margin-bottom: 6px;
}
.gic-def{
margin: 0 0 8px;
line-height: 1.5;
opacity: .94;
}
.gic-meta{
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0;
font-size: 12px;
opacity: .78;
}
.gic-meta span{
border: 1px solid rgba(127,127,127,0.20);
border-radius: 999px;
padding: 2px 8px;
}
.gic-cards{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
margin-top: 14px;
}
.gic-card{
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px 16px;
border: 1px solid rgba(127,127,127,0.22);
border-radius: 16px;
background: rgba(127,127,127,0.05);
text-decoration: none;
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
}
.gic-card:hover{
transform: translateY(-1px);
background: rgba(127,127,127,0.08);
border-color: rgba(0,217,255,0.16);
text-decoration: none;
}
.gic-card strong{
font-size: 15px;
line-height: 1.3;
}
.gic-card span{
font-size: 14px;
line-height: 1.45;
opacity: .92;
}
@media (max-width: 860px){
.gic-follow-letters{
display: none !important;
}
}
@media (prefers-color-scheme: dark){
.gic-aside__block,
.gic-aside__pill,
.gic-item,
.gic-card{
background: rgba(255,255,255,0.04);
}
.gic-follow-letters{
background: rgba(0,0,0,.58);
box-shadow: 0 10px 22px rgba(0,0,0,.18);
}
.gic-letters a:hover,
.gic-follow-letters a:hover,
.gic-card:hover{
background: rgba(255,255,255,0.07);
}
.gic-letters a.is-active,
.gic-follow-letters a.is-active,
.gic-letters a[aria-current="true"],
.gic-follow-letters a[aria-current="true"]{
background:
linear-gradient(180deg, rgba(0,217,255,0.18), rgba(0,217,255,0.08)),
rgba(255,255,255,0.06);
}
}
</style>