Compare commits
4 Commits
docs/RUNBO
...
sync/etape
| Author | SHA1 | Date | |
|---|---|---|---|
| e2468be522 | |||
| dc2826df08 | |||
| 3e4df18b88 | |||
| a2d1df427d |
102
astro.config.mjs
102
astro.config.mjs
@@ -10,41 +10,101 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|||||||
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
|
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
|
||||||
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
|
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
|
||||||
|
|
||||||
const must = (name, fn) => {
|
/**
|
||||||
if (typeof fn !== "function") {
|
* Cast minimal pour satisfaire @ts-check sans dépendre de types internes Astro/Unified.
|
||||||
throw new Error(`[astro.config] rehype plugin "${name}" is not a function (export default vs named?)`);
|
* @param {unknown} x
|
||||||
}
|
* @returns {any}
|
||||||
return fn;
|
*/
|
||||||
};
|
const asAny = (x) => /** @type {any} */ (x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} node
|
||||||
|
* @param {string} cls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasClass(node, cls) {
|
||||||
|
const cn = node?.properties?.className;
|
||||||
|
if (Array.isArray(cn)) return cn.includes(cls);
|
||||||
|
if (typeof cn === "string") return cn.split(/\s+/).includes(cls);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rehype plugin: retire les ids dupliqués en gardant en priorité:
|
||||||
|
* 1) span.details-anchor
|
||||||
|
* 2) h1..h6
|
||||||
|
* 3) sinon: premier rencontré
|
||||||
|
* @returns {(tree: any) => void}
|
||||||
|
*/
|
||||||
|
function rehypeDedupeIds() {
|
||||||
|
/** @param {any} tree */
|
||||||
|
return (tree) => {
|
||||||
|
/** @type {Map<string, Array<{node:any, pref:number, idx:number}>>} */
|
||||||
|
const occ = new Map();
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
/** @param {any} node */
|
||||||
|
const walk = (node) => {
|
||||||
|
if (!node || typeof node !== "object") return;
|
||||||
|
|
||||||
|
if (node.type === "element") {
|
||||||
|
const id = node.properties?.id;
|
||||||
|
if (typeof id === "string" && id) {
|
||||||
|
let pref = 2;
|
||||||
|
if (node.tagName === "span" && hasClass(node, "details-anchor")) pref = 0;
|
||||||
|
else if (/^h[1-6]$/.test(String(node.tagName || ""))) pref = 1;
|
||||||
|
|
||||||
|
const arr = occ.get(id) || [];
|
||||||
|
arr.push({ node, pref, idx: idx++ });
|
||||||
|
occ.set(id, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = node.children;
|
||||||
|
if (Array.isArray(children)) for (const c of children) walk(c);
|
||||||
|
} else if (Array.isArray(node.children)) {
|
||||||
|
for (const c of node.children) walk(c);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(tree);
|
||||||
|
|
||||||
|
for (const [id, items] of occ.entries()) {
|
||||||
|
if (items.length <= 1) continue;
|
||||||
|
|
||||||
|
items.sort((a, b) => (a.pref - b.pref) || (a.idx - b.idx));
|
||||||
|
const keep = items[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < items.length; i++) {
|
||||||
|
const n = items[i].node;
|
||||||
|
if (n?.properties?.id === id) delete n.properties.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety: on s'assure qu'un seul garde bien l'id
|
||||||
|
if (keep?.node?.properties) keep.node.properties.id = id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: "static",
|
output: "static",
|
||||||
trailingSlash: "always",
|
trailingSlash: "always",
|
||||||
|
|
||||||
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
|
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
|
||||||
|
|
||||||
integrations: [
|
integrations: [
|
||||||
mdx(),
|
// Important: MDX hérite du pipeline markdown (ids p-… + autres plugins)
|
||||||
|
mdx({ extendMarkdownConfig: true }),
|
||||||
sitemap({
|
sitemap({
|
||||||
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
|
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
// ✅ Plugins appliqués AU MDX
|
|
||||||
mdx: {
|
|
||||||
// ✅ MDX hérite déjà de markdown.rehypePlugins
|
|
||||||
// donc ici on ne met QUE le spécifique MDX
|
|
||||||
rehypePlugins: [
|
|
||||||
must("rehype-details-sections", rehypeDetailsSections),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✅ Plugins appliqués au Markdown non-MDX
|
|
||||||
markdown: {
|
markdown: {
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
must("rehype-slug", rehypeSlug),
|
asAny(rehypeSlug),
|
||||||
[must("rehype-autolink-headings", rehypeAutolinkHeadings), { behavior: "append" }],
|
[asAny(rehypeAutolinkHeadings), { behavior: "append" }],
|
||||||
must("rehype-paragraph-ids", rehypeParagraphIds),
|
asAny(rehypeDetailsSections),
|
||||||
|
asAny(rehypeParagraphIds),
|
||||||
|
asAny(rehypeDedupeIds),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"build:clean": "npm run clean && npm run build",
|
"build:clean": "npm run clean && npm run build",
|
||||||
|
|
||||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
|
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && npx pagefind --site dist",
|
||||||
|
|
||||||
"import": "node scripts/import-docx.mjs",
|
"import": "node scripts/import-docx.mjs",
|
||||||
"apply:ticket": "node scripts/apply-ticket.mjs",
|
"apply:ticket": "node scripts/apply-ticket.mjs",
|
||||||
|
|||||||
134
scripts/dedupe-ids-dist.mjs
Normal file
134
scripts/dedupe-ids-dist.mjs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const DIST_DIR = path.resolve("dist");
|
||||||
|
|
||||||
|
/** @param {string} dir */
|
||||||
|
async function walkHtml(dir) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
const out = [];
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
for (const e of entries) {
|
||||||
|
const p = path.join(dir, e.name);
|
||||||
|
if (e.isDirectory()) out.push(...(await walkHtml(p)));
|
||||||
|
else if (e.isFile() && p.endsWith(".html")) out.push(p);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} attrs */
|
||||||
|
function getClass(attrs) {
|
||||||
|
const m = attrs.match(/\bclass="([^"]*)"/i);
|
||||||
|
return m ? m[1] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {{tag:string,id:string,cls:string}} occ */
|
||||||
|
function score(occ) {
|
||||||
|
// plus petit = mieux (on garde)
|
||||||
|
if (occ.tag === "span" && /\bdetails-anchor\b/.test(occ.cls)) return 0;
|
||||||
|
if (/^h[1-6]$/.test(occ.tag)) return 1;
|
||||||
|
if (occ.tag === "p" && occ.id.startsWith("p-")) return 2;
|
||||||
|
return 10; // tout le reste (toc, nav, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let changedFiles = 0;
|
||||||
|
let removed = 0;
|
||||||
|
|
||||||
|
const files = await walkHtml(DIST_DIR);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
let html = await fs.readFile(file, "utf8");
|
||||||
|
|
||||||
|
// capture: <tag ... id="X" ...>
|
||||||
|
const re = /<([A-Za-z][\w:-]*)([^>]*?)\s+id="([^"]+)"([^>]*?)>/g;
|
||||||
|
|
||||||
|
/** @type {Array<{id:string,tag:string,pre:string,post:string,start:number,end:number,cls:string,idx:number}>} */
|
||||||
|
const occs = [];
|
||||||
|
let m;
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
const tag = m[1].toLowerCase();
|
||||||
|
const pre = m[2] || "";
|
||||||
|
const id = m[3] || "";
|
||||||
|
const post = m[4] || "";
|
||||||
|
const fullAttrs = `${pre}${post}`;
|
||||||
|
const cls = getClass(fullAttrs);
|
||||||
|
|
||||||
|
occs.push({
|
||||||
|
id,
|
||||||
|
tag,
|
||||||
|
pre,
|
||||||
|
post,
|
||||||
|
start: m.index,
|
||||||
|
end: m.index + m[0].length,
|
||||||
|
cls,
|
||||||
|
idx: idx++,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (occs.length === 0) continue;
|
||||||
|
|
||||||
|
/** @type {Map<string, Array<typeof occs[number]>>} */
|
||||||
|
const byId = new Map();
|
||||||
|
for (const o of occs) {
|
||||||
|
if (!o.id) continue;
|
||||||
|
const arr = byId.get(o.id) || [];
|
||||||
|
arr.push(o);
|
||||||
|
byId.set(o.id, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Array<{start:number,end:number,repl:string}>} */
|
||||||
|
const edits = [];
|
||||||
|
|
||||||
|
for (const [id, arr] of byId.entries()) {
|
||||||
|
if (arr.length <= 1) continue;
|
||||||
|
|
||||||
|
// choisir le “meilleur” porteur d’id : details-anchor > h2/h3... > p-... > reste
|
||||||
|
const sorted = [...arr].sort((a, b) => {
|
||||||
|
const sa = score(a);
|
||||||
|
const sb = score(b);
|
||||||
|
if (sa !== sb) return sa - sb;
|
||||||
|
return a.idx - b.idx; // stable: premier
|
||||||
|
});
|
||||||
|
|
||||||
|
const keep = sorted[0];
|
||||||
|
|
||||||
|
for (const o of sorted.slice(1)) {
|
||||||
|
// remplacer l’ouverture de tag en supprimant l’attribut id
|
||||||
|
// <tag{pre} id="X"{post}> ==> <tag{pre}{post}>
|
||||||
|
const repl = `<${o.tag}${o.pre}${o.post}>`;
|
||||||
|
edits.push({ start: o.start, end: o.end, repl });
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sécurité: on “force” l'id sur le keep (au cas où il aurait été modifié plus haut)
|
||||||
|
// (on ne touche pas au keep ici, juste on ne le retire pas)
|
||||||
|
void keep;
|
||||||
|
void id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edits.length === 0) continue;
|
||||||
|
|
||||||
|
// appliquer de la fin vers le début
|
||||||
|
edits.sort((a, b) => b.start - a.start);
|
||||||
|
for (const e of edits) {
|
||||||
|
html = html.slice(0, e.start) + e.repl + html.slice(e.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(file, html, "utf8");
|
||||||
|
changedFiles++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedFiles > 0) {
|
||||||
|
console.log(`✅ dedupe-ids-dist: files_changed=${changedFiles} ids_removed=${removed}`);
|
||||||
|
} else {
|
||||||
|
console.log("ℹ️ dedupe-ids-dist: no duplicates found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("❌ dedupe-ids-dist failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
59
src/annotations/archicratie/archicrat-ia/prologue.yml
Normal file
59
src/annotations/archicratie/archicrat-ia/prologue.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
schema: 1
|
||||||
|
|
||||||
|
# optionnel (si présent, doit matcher le chemin du fichier)
|
||||||
|
page: archicratie/archicrat-ia/prologue
|
||||||
|
|
||||||
|
paras:
|
||||||
|
p-0-d7974f88:
|
||||||
|
refs:
|
||||||
|
- label: "Happycratie — (Cabanas & Illouz) via Cairn"
|
||||||
|
url: "https://shs.cairn.info/revue-ethnologie-francaise-2019-4-page-813?lang=fr"
|
||||||
|
kind: "article"
|
||||||
|
- label: "Techno-féodalisme — Variations (OpenEdition)"
|
||||||
|
url: "https://journals.openedition.org/variations/2290"
|
||||||
|
kind: "article"
|
||||||
|
|
||||||
|
authors:
|
||||||
|
- "Eva Illouz"
|
||||||
|
- "Yanis Varoufakis"
|
||||||
|
|
||||||
|
quotes:
|
||||||
|
- text: "Dans Happycratie, Edgar Cabanas et Eva Illouz..."
|
||||||
|
source: "Happycratie, p.1"
|
||||||
|
- text: "En eux-mêmes, les actifs ne sont ni féodaux ni capitalistes..."
|
||||||
|
source: "Entretien Morozov/Varoufakis — techno-féodalisme"
|
||||||
|
|
||||||
|
media:
|
||||||
|
- type: "image"
|
||||||
|
src: "/public/media/archicratie/archicrat-ia/prologue/p-0-d7974f88/schema-1.svg"
|
||||||
|
caption: "Tableau explicatif"
|
||||||
|
credit: "ChatGPT"
|
||||||
|
- type: "image"
|
||||||
|
src: "/public/media/archicratie/archicrat-ia/prologue/p-0-d7974f88/schema-2.svg"
|
||||||
|
caption: "Diagramme d’évolution"
|
||||||
|
credit: "Yanis Varoufakis"
|
||||||
|
|
||||||
|
comments_editorial:
|
||||||
|
- text: "TODO: nuancer / préciser — commentaire éditorial versionné (pas public)."
|
||||||
|
status: "draft"
|
||||||
|
|
||||||
|
p-1-2ef25f29:
|
||||||
|
refs:
|
||||||
|
- label: "Kafka et le pouvoir — Bernard Lahire (Cairn)"
|
||||||
|
url: "https://shs.cairn.info/franz-kafka--9782707159410-page-475?lang=fr"
|
||||||
|
kind: "book"
|
||||||
|
|
||||||
|
authors:
|
||||||
|
- "Bernard Lahire"
|
||||||
|
|
||||||
|
quotes:
|
||||||
|
- text: "Si l’on voulait chercher quelque chose comme une vision du monde chez Kafka..."
|
||||||
|
source: "Bernard Lahire, Franz Kafka, p.475+"
|
||||||
|
|
||||||
|
media:
|
||||||
|
- type: "video"
|
||||||
|
src: "/media/prologue/p-1-2ef25f29/bien_commun.mp4"
|
||||||
|
caption: "Entretien avec Bernard Lahire"
|
||||||
|
credit: "Cairn.info"
|
||||||
|
|
||||||
|
comments_editorial: []
|
||||||
@@ -1,35 +1,128 @@
|
|||||||
---
|
---
|
||||||
|
// src/components/LevelToggle.astro
|
||||||
const { initialLevel = 1 } = Astro.props;
|
const { initialLevel = 1 } = Astro.props;
|
||||||
---
|
---
|
||||||
<div class="level-toggle" role="group" aria-label="Niveau de lecture">
|
|
||||||
<button type="button" class="lvl-btn" data-level="1" aria-pressed="true">Niveau 1</button>
|
<div class="level-toggle" role="group" aria-label="Mode d’édition">
|
||||||
<button type="button" class="lvl-btn" data-level="2" aria-pressed="false">Niveau 2</button>
|
<button type="button" class="level-btn" data-level="1">Propos</button>
|
||||||
<button type="button" class="lvl-btn" data-level="3" aria-pressed="false">Niveau 3</button>
|
<button type="button" class="level-btn" data-level="2">Références</button>
|
||||||
|
<button type="button" class="level-btn" data-level="3">Illustrations</button>
|
||||||
|
<button type="button" class="level-btn" data-level="4">Commentaires</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline define:vars={{ initialLevel }}>
|
||||||
(() => {
|
(() => {
|
||||||
const KEY = "archicratie.readingLevel";
|
const BODY = document.body;
|
||||||
const buttons = Array.from(document.querySelectorAll(".lvl-btn"));
|
|
||||||
|
|
||||||
function apply(level) {
|
const wrap = document.querySelector(".level-toggle");
|
||||||
document.body.setAttribute("data-reading-level", String(level));
|
if (!wrap) return;
|
||||||
buttons.forEach((b) => b.setAttribute("aria-pressed", b.dataset.level === String(level) ? "true" : "false"));
|
|
||||||
|
const buttons = Array.from(wrap.querySelectorAll("button[data-level]"));
|
||||||
|
if (!buttons.length) return;
|
||||||
|
|
||||||
|
const KEY = "archicratie:readingLevel";
|
||||||
|
|
||||||
|
function clampLevel(n) {
|
||||||
|
const x = Number.parseInt(String(n), 10);
|
||||||
|
if (!Number.isFinite(x)) return 1;
|
||||||
|
return Math.min(4, Math.max(1, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valeur par défaut : si rien n'est stocké, on met 1 (citoyen).
|
function setActiveUI(lvl) {
|
||||||
// Si JS est absent/casse, le site reste lisible (tout s'affiche).
|
for (const b of buttons) {
|
||||||
const stored = Number(localStorage.getItem(KEY));
|
const on = String(b.dataset.level) === String(lvl);
|
||||||
const level = (stored === 1 || stored === 2 || stored === 3) ? stored : 1;
|
b.classList.toggle("is-active", on);
|
||||||
|
b.setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apply(level);
|
function captureBeforeLevelSwitch() {
|
||||||
|
const paraId =
|
||||||
|
window.__archiCurrentParaId ||
|
||||||
|
window.__archiLastParaId ||
|
||||||
|
String(location.hash || "").replace(/^#/, "") ||
|
||||||
|
"";
|
||||||
|
|
||||||
buttons.forEach((b) => {
|
window.__archiLevelSwitchCtx = {
|
||||||
b.addEventListener("click", () => {
|
paraId,
|
||||||
const lvl = Number(b.dataset.level);
|
hash: location.hash || "",
|
||||||
localStorage.setItem(KEY, String(lvl));
|
scrollY: window.scrollY || 0,
|
||||||
apply(lvl);
|
t: Date.now(),
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLevel(lvl, { persist = true } = {}) {
|
||||||
|
const v = clampLevel(lvl);
|
||||||
|
|
||||||
|
if (BODY) BODY.dataset.readingLevel = String(v);
|
||||||
|
setActiveUI(v);
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
try { localStorage.setItem(KEY, String(v)); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("archicratie:readingLevel", { detail: { level: v } })
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init : storage > initialLevel
|
||||||
|
let start = clampLevel(initialLevel);
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(KEY);
|
||||||
|
if (stored) start = clampLevel(stored);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
applyLevel(start, { persist: false });
|
||||||
|
|
||||||
|
// clicks
|
||||||
|
wrap.addEventListener("click", (ev) => {
|
||||||
|
const btn = ev.target?.closest?.("button[data-level]");
|
||||||
|
if (!btn) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// ✅ crucial : on capture la position AVANT le reflow lié au changement de niveau
|
||||||
|
captureBeforeLevelSwitch();
|
||||||
|
applyLevel(btn.dataset.level);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.level-toggle{
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn{
|
||||||
|
border: 1px solid rgba(127,127,127,0.40);
|
||||||
|
background: rgba(127,127,127,0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: filter .12s ease, transform .12s ease, background .12s ease, border-color .12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn:hover{
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn.is-active{
|
||||||
|
border-color: rgba(160,160,255,0.95);
|
||||||
|
background: rgba(140,140,255,0.18);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn.is-active:hover{
|
||||||
|
filter: brightness(1.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn:active{
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
1064
src/components/SidePanel.astro
Normal file
1064
src/components/SidePanel.astro
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,12 @@ main { padding: 0; }
|
|||||||
border-top: 1px dashed rgba(127,127,127,0.35);
|
border-top: 1px dashed rgba(127,127,127,0.35);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
/* Edition-bar: cacher des badges (non destructif) */
|
||||||
|
.edition-bar [data-badge="edition"],
|
||||||
|
.edition-bar [data-badge="status"],
|
||||||
|
.edition-bar [data-badge="version"]{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -95,7 +101,34 @@ main { padding: 0; }
|
|||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle niveaux */
|
/* Jump by paragraph id */
|
||||||
|
.jump-form{
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.jump-input{
|
||||||
|
border: 1px solid rgba(127,127,127,0.55);
|
||||||
|
background: transparent;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 13px;
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
.jump-input.is-error{
|
||||||
|
outline: 2px solid rgba(127,127,127,0.55);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.jump-btn{
|
||||||
|
border: 1px solid rgba(127,127,127,0.55);
|
||||||
|
background: transparent;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle niveaux (legacy, non bloquant) */
|
||||||
.level-toggle { display: inline-flex; gap: 6px; }
|
.level-toggle { display: inline-flex; gap: 6px; }
|
||||||
.lvl-btn {
|
.lvl-btn {
|
||||||
border: 1px solid rgba(127,127,127,0.55);
|
border: 1px solid rgba(127,127,127,0.55);
|
||||||
@@ -112,14 +145,22 @@ main { padding: 0; }
|
|||||||
|
|
||||||
/* Règles niveaux */
|
/* Règles niveaux */
|
||||||
body[data-reading-level="1"] .level-2,
|
body[data-reading-level="1"] .level-2,
|
||||||
body[data-reading-level="1"] .level-3 { display: none; }
|
body[data-reading-level="1"] .level-3,
|
||||||
body[data-reading-level="2"] .level-3 { display: none; }
|
body[data-reading-level="1"] .level-4 { display: none; }
|
||||||
|
|
||||||
|
body[data-reading-level="2"] .level-3,
|
||||||
|
body[data-reading-level="2"] .level-4 { display: none; }
|
||||||
|
|
||||||
|
body[data-reading-level="3"] .level-2,
|
||||||
|
body[data-reading-level="3"] .level-4 { display: none; }
|
||||||
|
|
||||||
|
body[data-reading-level="4"] .level-2,
|
||||||
|
body[data-reading-level="4"] .level-3 { display: none; }
|
||||||
|
|
||||||
/* ==========================
|
/* ==========================
|
||||||
Scroll offset (anchors / headings / paras)
|
Scroll offset (anchors / headings / paras)
|
||||||
========================== */
|
========================== */
|
||||||
|
|
||||||
/* Paragraph tools + bookmark */
|
|
||||||
.reading p[id]{
|
.reading p[id]{
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-right: 14rem;
|
padding-right: 14rem;
|
||||||
@@ -183,6 +224,14 @@ body[data-reading-level="2"] .level-3 { display: none; }
|
|||||||
}
|
}
|
||||||
.para-bookmark:hover{ text-decoration: underline; }
|
.para-bookmark:hover{ text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Highlight (jump / resume / arrivée hash) */
|
||||||
|
.para-highlight{
|
||||||
|
background: rgba(127,127,127,0.10);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 0 2px rgba(127,127,127,0.35);
|
||||||
|
transition: box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.build-stamp {
|
.build-stamp {
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
@@ -196,15 +245,51 @@ body[data-reading-level="2"] .level-3 { display: none; }
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
margin: 14px 0;
|
margin: 14px 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.details-summary {
|
|
||||||
cursor: pointer;
|
/* ✅ Handle minimal pour sections fermées : pas de titre visible, mais ouvrable */
|
||||||
font-weight: 650;
|
.details-summary{
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
border: 1px dashed rgba(127,127,127,.25);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
background: rgba(127,127,127,0.06);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
/* cache le texte réel (souvent le titre), sans casser l’accessibilité */
|
||||||
|
color: transparent;
|
||||||
}
|
}
|
||||||
.details-summary::-webkit-details-marker { display: none; }
|
.details-summary::-webkit-details-marker { display: none; }
|
||||||
.details-summary a { text-decoration: none; }
|
|
||||||
.details-summary a:hover { text-decoration: underline; }
|
.details-summary::before{
|
||||||
|
content: "▸ Ouvrir la section";
|
||||||
|
color: rgba(127,127,127,0.85);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
.details-summary::before{ color: rgba(220,220,220,0.82); }
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > .details-summary{
|
||||||
|
/* une fois ouvert, on le rend “SR-only” pour éviter le doublon visuel */
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.details-body { margin-top: 10px; }
|
.details-body { margin-top: 10px; }
|
||||||
|
|
||||||
/* Smooth scroll */
|
/* Smooth scroll */
|
||||||
@@ -224,7 +309,6 @@ html{ scroll-behavior: smooth; }
|
|||||||
width: var(--reading-width);
|
width: var(--reading-width);
|
||||||
right: auto;
|
right: auto;
|
||||||
|
|
||||||
/* colle au header */
|
|
||||||
top: var(--sticky-header-h);
|
top: var(--sticky-header-h);
|
||||||
|
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
@@ -247,7 +331,7 @@ html{ scroll-behavior: smooth; }
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
padding-right: 84px; /* réserve pour les boutons */
|
padding-right: 84px;
|
||||||
|
|
||||||
border: 1px solid rgba(127,127,127,.20);
|
border: 1px solid rgba(127,127,127,.20);
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
@@ -259,7 +343,7 @@ html{ scroll-behavior: smooth; }
|
|||||||
|
|
||||||
box-shadow: 0 10px 22px rgba(0,0,0,.06);
|
box-shadow: 0 10px 22px rgba(0,0,0,.06);
|
||||||
|
|
||||||
position: relative; /* pour rf-actions */
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark){
|
@media (prefers-color-scheme: dark){
|
||||||
@@ -278,7 +362,6 @@ html{ scroll-behavior: smooth; }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rf-line[hidden]{ display: none !important; }
|
.rf-line[hidden]{ display: none !important; }
|
||||||
|
|
||||||
.rf-h1{
|
.rf-h1{
|
||||||
@@ -298,7 +381,6 @@ html{ scroll-behavior: smooth; }
|
|||||||
font-weight: var(--rf-h3-fw);
|
font-weight: var(--rf-h3-fw);
|
||||||
opacity: .92;
|
opacity: .92;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rf-line:hover{ text-decoration: underline; }
|
.rf-line:hover{ text-decoration: underline; }
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
@@ -327,3 +409,14 @@ html{ scroll-behavior: smooth; }
|
|||||||
background: rgba(127,127,127,0.16);
|
background: rgba(127,127,127,0.16);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================
|
||||||
|
PATCH CRUCIAL : éviter les “rectangles vides”
|
||||||
|
(details fermés + summary handle minimal)
|
||||||
|
========================== */
|
||||||
|
|
||||||
|
.reading details.details-section:not([open]){
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user