refactor(glossaire): polish contextual aside navigation
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 42s

This commit is contained in:
2026-03-12 17:42:16 +01:00
parent 0c57c4bc6d
commit 0fc0976f8a
2 changed files with 305 additions and 1 deletions

View File

@@ -0,0 +1,297 @@
---
const {
currentEntry,
allEntries = [],
} = Astro.props;
const slugOf = (entry) => String(entry.id).replace(/\.(md|mdx)$/i, "");
const hrefOf = (entry) => `/glossaire/${slugOf(entry)}/`;
const collator = new Intl.Collator("fr", { sensitivity: "base", numeric: true });
const bySlug = new Map(allEntries.map((entry) => [slugOf(entry), entry]));
const currentSlug = slugOf(currentEntry);
const fondamentauxWanted = [
"archicratie",
"tension",
"arcalite",
"cratialite",
"archicration",
"co-viabilite",
];
const fondamentaux = fondamentauxWanted
.map((slug) => bySlug.get(slug))
.filter(Boolean);
function resolveList(slugs = []) {
return slugs
.map((slug) => bySlug.get(slug))
.filter(Boolean);
}
function uniqueBySlug(entries) {
const seen = new Set();
const out = [];
for (const entry of entries) {
const slug = slugOf(entry);
if (seen.has(slug)) continue;
seen.add(slug);
out.push(entry);
}
return out;
}
const relatedEntries = uniqueBySlug(resolveList(currentEntry.data.related ?? []))
.sort((a, b) => collator.compare(a.data.term, b.data.term));
const opposedEntries = uniqueBySlug(resolveList(currentEntry.data.opposedTo ?? []))
.sort((a, b) => collator.compare(a.data.term, b.data.term));
const seeAlsoEntries = uniqueBySlug(resolveList(currentEntry.data.seeAlso ?? []))
.sort((a, b) => collator.compare(a.data.term, b.data.term));
const paradigmes = [...allEntries]
.filter((e) => e.data.kind === "paradigme" && slugOf(e) !== currentSlug)
.sort((a, b) => collator.compare(a.data.term, b.data.term));
function contextualParadigmsFor(entry) {
const relatedParadigms = (entry.data.related ?? [])
.map((slug) => bySlug.get(slug))
.filter((e) => e && e.data.kind === "paradigme");
const seeAlsoParadigms = (entry.data.seeAlso ?? [])
.map((slug) => bySlug.get(slug))
.filter((e) => e && e.data.kind === "paradigme");
const opposedParadigms = (entry.data.opposedTo ?? [])
.map((slug) => bySlug.get(slug))
.filter((e) => e && e.data.kind === "paradigme");
const merged = uniqueBySlug([
...relatedParadigms,
...seeAlsoParadigms,
...opposedParadigms,
]);
if (merged.length > 0) {
return merged.slice(0, 5);
}
if (entry.data.kind === "paradigme") {
const preferred = [
"gouvernementalite",
"gouvernementalite-algorithmique",
"cybernetique",
"biopolitique",
"bureaucratie",
"contractualisme-hobbesien",
"liberalisme-proprietaire",
"volonte-generale",
];
return uniqueBySlug(
preferred
.filter((slug) => slug !== currentSlug)
.map((slug) => bySlug.get(slug))
.filter(Boolean)
).slice(0, 5);
}
return paradigmes.slice(0, 4);
}
const contextualParadigms = contextualParadigmsFor(currentEntry);
const kindLabels = {
concept: "Concept",
diagnostic: "Diagnostic",
topologie: "Topologie",
verbe: "Verbe",
paradigme: "Paradigme",
};
const domainLabels = {
transversal: "Transversal",
theorie: "Théorie",
"cas-ia": "Cas IA",
};
const levelLabels = {
fondamental: "Fondamental",
intermediaire: "Intermédiaire",
avance: "Avancé",
};
const metaLabel = [
kindLabels[currentEntry.data.kind] ?? currentEntry.data.kind,
domainLabels[currentEntry.data.domain] ?? currentEntry.data.domain,
levelLabels[currentEntry.data.level] ?? currentEntry.data.level,
].join(" · ");
---
<nav class="glossary-aside" aria-label="Navigation du glossaire">
<div class="glossary-aside__block glossary-aside__block--intro">
<a class="glossary-aside__back" href="/glossaire/">← Retour au glossaire</a>
<div class="glossary-aside__title">Glossaire archicratique</div>
<div class="glossary-aside__meta">{metaLabel}</div>
</div>
{fondamentaux.length > 0 && (
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">Noyau archicratique</h2>
<ul class="glossary-aside__list">
{fondamentaux.map((entry) => {
const active = slugOf(entry) === currentSlug;
return (
<li>
<a
href={hrefOf(entry)}
aria-current={active ? "page" : undefined}
class={active ? "is-active" : undefined}
>
{entry.data.term}
</a>
</li>
);
})}
</ul>
</section>
)}
{(relatedEntries.length > 0 || opposedEntries.length > 0 || seeAlsoEntries.length > 0) && (
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">Autour de cette fiche</h2>
{relatedEntries.length > 0 && (
<>
<h3 class="glossary-aside__subheading">Liés</h3>
<ul class="glossary-aside__list">
{relatedEntries.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</>
)}
{opposedEntries.length > 0 && (
<>
<h3 class="glossary-aside__subheading">Opposés</h3>
<ul class="glossary-aside__list">
{opposedEntries.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</>
)}
{seeAlsoEntries.length > 0 && (
<>
<h3 class="glossary-aside__subheading">Voir aussi</h3>
<ul class="glossary-aside__list">
{seeAlsoEntries.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</>
)}
</section>
)}
{contextualParadigms.length > 0 && (
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">
{currentEntry.data.kind === "paradigme" ? "Paradigmes voisins" : "Paradigmes mobilisés"}
</h2>
<ul class="glossary-aside__list">
{contextualParadigms.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</section>
)}
</nav>
<style>
.glossary-aside{
display: flex;
flex-direction: column;
gap: 14px;
}
.glossary-aside__block{
border: 1px solid rgba(127,127,127,0.22);
border-radius: 16px;
padding: 12px;
background: rgba(127,127,127,0.05);
}
.glossary-aside__block--intro{
padding-top: 11px;
padding-bottom: 11px;
}
.glossary-aside__back{
display: inline-block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
}
.glossary-aside__title{
font-size: 14px;
font-weight: 800;
letter-spacing: .2px;
line-height: 1.25;
}
.glossary-aside__meta{
margin-top: 8px;
font-size: 12px;
line-height: 1.35;
opacity: .78;
}
.glossary-aside__heading{
margin: 0 0 10px;
font-size: 13px;
font-weight: 800;
opacity: .9;
}
.glossary-aside__subheading{
margin: 12px 0 8px;
font-size: 12px;
font-weight: 800;
opacity: .8;
text-transform: uppercase;
letter-spacing: .04em;
}
.glossary-aside__list{
list-style: none;
margin: 0;
padding: 0;
}
.glossary-aside__list li{
margin: 6px 0;
}
.glossary-aside__list a{
text-decoration: none;
font-size: 13px;
line-height: 1.3;
}
.glossary-aside__list a.is-active{
font-weight: 800;
}
@media (prefers-color-scheme: dark){
.glossary-aside__block{
background: rgba(255,255,255,0.04);
}
}
</style>

View File

@@ -1,9 +1,11 @@
---
import EditionLayout from "../../layouts/EditionLayout.astro";
import GlossaryAside from "../../components/GlossaryAside.astro";
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const entries = await getCollection("glossaire");
return entries.map((entry) => ({
params: { slug: String(entry.id).replace(/\.(md|mdx)$/i, "") },
props: { entry },
@@ -11,6 +13,7 @@ export async function getStaticPaths() {
}
const { entry } = Astro.props;
const allEntries = await getCollection("glossaire");
const { Content } = await render(entry);
---
@@ -18,11 +21,15 @@ const { Content } = await render(entry);
title={entry.data.title}
editionLabel="Glossaire"
editionKey="glossaire"
statusLabel="référentiel"
statusLabel="Référentiel"
statusKey="referentiel"
level={1}
version={entry.data.version}
>
<Fragment slot="aside">
<GlossaryAside currentEntry={entry} allEntries={allEntries} />
</Fragment>
<h1>{entry.data.term}</h1>
<p><em>{entry.data.definitionShort}</em></p>
<Content />