diff --git a/scripts/convert_docx_to_mdx.py b/scripts/convert_docx_to_mdx.py new file mode 100755 index 0000000..6df25a2 --- /dev/null +++ b/scripts/convert_docx_to_mdx.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("Erreur : PyYAML n'est pas installé. Lance : pip3 install pyyaml") + sys.exit(1) + + +EDITION = "archicrat-ia" +STATUS = "essai_these" +VERSION = "0.1.0" + + +ORDER_MAP = { + "prologue": 10, + "chapitre-1": 20, + "chapitre-2": 30, + "chapitre-3": 40, + "chapitre-4": 50, + "chapitre-5": 60, + "conclusion": 70, +} + + +TITLE_MAP = { + "prologue": "Prologue — Fondation, finalité sociopolitique et historique", + "chapitre-1": "Chapitre 1 — Fondements épistémologiques et modélisation", + "chapitre-2": "Chapitre 2 — Archéogenèse des régimes de co-viabilité", + "chapitre-3": "Chapitre 3 — Philosophies du pouvoir et archicration", + "chapitre-4": "Chapitre 4 — Histoire archicratique des révolutions industrielles", + "chapitre-5": "Chapitre 5 — Tensions, co-viabilités et régulations", + "conclusion": "Conclusion — ArchiCraT-IA", +} + + +def slugify_name(path: Path) -> str: + stem = path.stem.lower().strip() + + replacements = { + " ": "-", + "_": "-", + "—": "-", + "–": "-", + "é": "e", + "è": "e", + "ê": "e", + "ë": "e", + "à": "a", + "â": "a", + "ä": "a", + "î": "i", + "ï": "i", + "ô": "o", + "ö": "o", + "ù": "u", + "û": "u", + "ü": "u", + "ç": "c", + "'": "", + "’": "", + } + + for old, new in replacements.items(): + stem = stem.replace(old, new) + + stem = re.sub(r"-+", "-", stem).strip("-") + + # normalisations spécifiques + stem = stem.replace("chapitre-1-fondements-epistemologiques-et-modelisation-archicratie-version-officielle-revise", "chapitre-1") + stem = stem.replace("chapitre-2", "chapitre-2") + stem = stem.replace("chapitre-3", "chapitre-3") + stem = stem.replace("chapitre-4", "chapitre-4") + stem = stem.replace("chapitre-5", "chapitre-5") + + if "prologue" in stem: + return "prologue" + if "chapitre-1" in stem: + return "chapitre-1" + if "chapitre-2" in stem: + return "chapitre-2" + if "chapitre-3" in stem: + return "chapitre-3" + if "chapitre-4" in stem: + return "chapitre-4" + if "chapitre-5" in stem: + return "chapitre-5" + if "conclusion" in stem: + return "conclusion" + + return stem + + +def extract_title_from_markdown(md_text: str) -> str | None: + for line in md_text.splitlines(): + line = line.strip() + if not line: + continue + if line.startswith("# "): + return line[2:].strip() + return None + + +def remove_first_h1(md_text: str) -> str: + lines = md_text.splitlines() + out = [] + removed = False + + for line in lines: + if not removed and line.strip().startswith("# "): + removed = True + continue + out.append(line) + + text = "\n".join(out).lstrip() + return text + + +def clean_markdown(md_text: str) -> str: + text = md_text.replace("\r\n", "\n").replace("\r", "\n") + + # nettoyer espaces multiples + text = re.sub(r"\n{3,}", "\n\n", text) + + # supprimer éventuels signets/artefacts de liens internes Pandoc + text = re.sub(r"\[\]\(#.*?\)", "", text) + + # convertir astérismes parasites + text = re.sub(r"[ \t]+$", "", text, flags=re.MULTILINE) + + return text.strip() + "\n" + + +def compute_level(slug: str) -> int: + if slug == "prologue": + return 1 + if slug.startswith("chapitre-"): + return 1 + if slug == "conclusion": + return 1 + return 1 + + +def convert_one_file(input_docx: Path, output_dir: Path, source_root: Path): + slug = slugify_name(input_docx) + output_mdx = output_dir / f"{slug}.mdx" + + cmd = [ + "pandoc", + str(input_docx), + "-f", + "docx", + "-t", + "gfm+smart", + ] + + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + md_text = result.stdout + + detected_title = extract_title_from_markdown(md_text) + md_body = remove_first_h1(md_text) + md_body = clean_markdown(md_body) + + title = TITLE_MAP.get(slug) or detected_title or input_docx.stem + order = ORDER_MAP.get(slug, 999) + level = compute_level(slug) + + relative_source = input_docx + try: + relative_source = input_docx.relative_to(source_root) + except ValueError: + relative_source = input_docx.name + + frontmatter = { + "title": title, + "edition": EDITION, + "status": STATUS, + "level": level, + "version": VERSION, + "concepts": [], + "links": [], + "order": order, + "summary": "", + "source": { + "kind": "docx", + "path": str(relative_source), + }, + } + + yaml_block = yaml.safe_dump( + frontmatter, + allow_unicode=True, + sort_keys=False, + default_flow_style=False, + ).strip() + + final_text = f"---\n{yaml_block}\n---\n{md_body if md_body.startswith(chr(10)) else chr(10) + md_body}" + output_mdx.write_text(final_text, encoding="utf-8") + print(f"✅ {input_docx.name} -> {output_mdx.name}") + + +def main(): + parser = argparse.ArgumentParser(description="Convertit un dossier DOCX en MDX avec frontmatter.") + parser.add_argument("input_dir", help="Dossier source contenant les DOCX") + parser.add_argument("output_dir", help="Dossier de sortie pour les MDX") + args = parser.parse_args() + + input_dir = Path(args.input_dir).expanduser().resolve() + output_dir = Path(args.output_dir).expanduser().resolve() + + if not shutil.which("pandoc"): + print("Erreur : pandoc n'est pas installé. Lance : brew install pandoc") + sys.exit(1) + + if not input_dir.exists() or not input_dir.is_dir(): + print(f"Erreur : dossier source introuvable : {input_dir}") + sys.exit(1) + + output_dir.mkdir(parents=True, exist_ok=True) + + docx_files = sorted(input_dir.glob("*.docx")) + if not docx_files: + print(f"Aucun DOCX trouvé dans : {input_dir}") + sys.exit(1) + + for docx_file in docx_files: + convert_one_file(docx_file, output_dir, input_dir) + + print() + print("Conversion DOCX -> MDX terminée.") + + +if __name__ == "__main__": + main() diff --git a/scripts/convert_mdx_to_docx.py b/scripts/convert_mdx_to_docx.py new file mode 100644 index 0000000..846d5e4 --- /dev/null +++ b/scripts/convert_mdx_to_docx.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +import argparse +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +import zipfile + +try: + import yaml +except ImportError: + print("Erreur : PyYAML n'est pas installé. Lance : pip3 install pyyaml") + sys.exit(1) + +try: + from docx import Document +except ImportError: + print("Erreur : python-docx n'est pas installé. Lance : pip3 install python-docx") + sys.exit(1) + + +def split_frontmatter(text: str): + if not text.startswith("---\n"): + return {}, text + + match = re.match(r"^---\n(.*?)\n---\n(.*)$", text, flags=re.DOTALL) + if not match: + return {}, text + + yaml_block = match.group(1) + body = match.group(2) + + try: + metadata = yaml.safe_load(yaml_block) or {} + except Exception as e: + print(f"Avertissement : frontmatter YAML illisible : {e}") + metadata = {} + + return metadata, body + + +def strip_mdx_artifacts(text: str): + # imports / exports MDX + text = re.sub(r"^\s*(import|export)\s+.+?$", "", text, flags=re.MULTILINE) + + # composants autofermants : + text = re.sub(r"<[A-Z][A-Za-z0-9._-]*\b[^>]*\/>", "", text) + + # composants bloc : ... + text = re.sub( + r"<([A-Z][A-Za-z0-9._-]*)\b[^>]*>.*?", + "", + text, + flags=re.DOTALL, + ) + + # accolades seules résiduelles sur ligne + text = re.sub(r"^\s*{\s*}\s*$", "", text, flags=re.MULTILINE) + + # lignes vides multiples + text = re.sub(r"\n{3,}", "\n\n", text) + + return text.strip() + "\n" + + +def inject_h1_from_title(metadata: dict, body: str): + title = metadata.get("title", "") + if not title: + return body + + if re.match(r"^\s*#\s+", body): + return body + + return f"# {title}\n\n{body.lstrip()}" + + +def find_style_by_candidates(doc, candidates): + # Cherche d'abord par nom visible + for style in doc.styles: + for candidate in candidates: + if style.name == candidate: + return style + + # Puis par style_id Word interne + for style in doc.styles: + style_id = getattr(style, "style_id", "") + if style_id in {"BodyText", "Heading1", "Heading2", "Heading3", "Heading4"}: + for candidate in candidates: + if candidate in {"Body Text", "Corps de texte"} and style_id == "BodyText": + return style + if candidate in {"Heading 1", "Titre 1"} and style_id == "Heading1": + return style + if candidate in {"Heading 2", "Titre 2"} and style_id == "Heading2": + return style + if candidate in {"Heading 3", "Titre 3"} and style_id == "Heading3": + return style + if candidate in {"Heading 4", "Titre 4"} and style_id == "Heading4": + return style + return None + +def strip_leading_paragraph_numbers(text: str): + """ + Supprime les numéros de paragraphe du type : + 2. Texte... + 11. Texte... + 101. Texte... + sans toucher aux titres Markdown (#, ##, ###). + """ + fixed_lines = [] + + for line in text.splitlines(): + stripped = line.lstrip() + + # Ne jamais toucher aux titres Markdown + if stripped.startswith("#"): + fixed_lines.append(line) + continue + + # Supprime un numéro de paragraphe en début de ligne + line = re.sub(r"^\s*\d+\.\s+", "", line) + fixed_lines.append(line) + + return "\n".join(fixed_lines) + "\n" + +def normalize_non_heading_paragraphs(docx_path: Path): + """ + Force tous les paragraphes non-titres en Body Text / Corps de texte. + On laisse intacts les Heading 1-4. + """ + doc = Document(str(docx_path)) + + body_style = find_style_by_candidates(doc, ["Body Text", "Corps de texte"]) + if body_style is None: + print(f"Avertissement : style 'Body Text / Corps de texte' introuvable dans {docx_path.name}") + return + + heading_names = { + "Heading 1", "Heading 2", "Heading 3", "Heading 4", + "Titre 1", "Titre 2", "Titre 3", "Titre 4", + } + heading_ids = {"Heading1", "Heading2", "Heading3", "Heading4"} + + changed = 0 + + for para in doc.paragraphs: + text = para.text.strip() + if not text: + continue + + current_style = para.style + current_name = current_style.name if current_style else "" + current_id = getattr(current_style, "style_id", "") if current_style else "" + + if current_name in heading_names or current_id in heading_ids: + continue + + # Tout le reste passe en Body Text + para.style = body_style + changed += 1 + + doc.save(str(docx_path)) + print(f" ↳ normalisation styles : {changed} paragraphe(s) mis en 'Body Text / Corps de texte'") + +def remove_word_bookmarks(docx_path: Path): + """ + Supprime les bookmarks Word (signets) du DOCX. + Ce sont eux qui apparaissent comme crochets gris dans LibreOffice/Word + quand l'affichage des signets est activé. + """ + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + + # Dézipper le docx + with zipfile.ZipFile(docx_path, "r") as zin: + zin.extractall(tmpdir) + + xml_targets = [ + tmpdir / "word" / "document.xml", + tmpdir / "word" / "footnotes.xml", + tmpdir / "word" / "endnotes.xml", + tmpdir / "word" / "comments.xml", + ] + + removed = 0 + + for xml_file in xml_targets: + if not xml_file.exists(): + continue + + text = xml_file.read_text(encoding="utf-8") + + # enlever et + text, c1 = re.subn(r"]*/>", "", text) + text, c2 = re.subn(r"]*/>", "", text) + + removed += c1 + c2 + xml_file.write_text(text, encoding="utf-8") + + # Rezipper + tmp_output = docx_path.with_suffix(".cleaned.docx") + with zipfile.ZipFile(tmp_output, "w", zipfile.ZIP_DEFLATED) as zout: + for file in tmpdir.rglob("*"): + if file.is_file(): + zout.write(file, file.relative_to(tmpdir)) + + tmp_output.replace(docx_path) + print(f" ↳ suppression signets : {removed} balise(s) supprimée(s)") + +def convert_one_file(input_path: Path, output_path: Path, reference_doc: Path | None): + raw = input_path.read_text(encoding="utf-8") + metadata, body = split_frontmatter(raw) + body = strip_mdx_artifacts(body) + body = strip_leading_paragraph_numbers(body) + body = inject_h1_from_title(metadata, body) + + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as tmp: + tmp.write(body) + tmp_md = Path(tmp.name) + + cmd = [ + "pandoc", + str(tmp_md), + "-f", + "markdown", + "-o", + str(output_path), + ] + + if reference_doc: + cmd.extend(["--reference-doc", str(reference_doc)]) + + try: + subprocess.run(cmd, check=True) + finally: + try: + tmp_md.unlink() + except FileNotFoundError: + pass + + normalize_non_heading_paragraphs(output_path) + remove_word_bookmarks(output_path) + +def main(): + parser = argparse.ArgumentParser( + description="Convertit des fichiers MDX en DOCX en conservant H1/H2/H3/H4 et en forçant le corps en Body Text." + ) + parser.add_argument("input_dir", help="Dossier contenant les .mdx") + parser.add_argument( + "--output-dir", + default=str(Path.home() / "Desktop" / "archicrat-ia-docx"), + help="Dossier de sortie DOCX" + ) + parser.add_argument( + "--reference-doc", + default=None, + help="DOCX modèle Word à utiliser comme reference-doc" + ) + + args = parser.parse_args() + + input_dir = Path(args.input_dir) + output_dir = Path(args.output_dir) + reference_doc = Path(args.reference_doc) if args.reference_doc else None + + if not shutil.which("pandoc"): + print("Erreur : pandoc n'est pas installé. Installe-le avec : brew install pandoc") + sys.exit(1) + + if not input_dir.exists() or not input_dir.is_dir(): + print(f"Erreur : dossier introuvable : {input_dir}") + sys.exit(1) + + if reference_doc and not reference_doc.exists(): + print(f"Erreur : reference-doc introuvable : {reference_doc}") + sys.exit(1) + + output_dir.mkdir(parents=True, exist_ok=True) + + mdx_files = sorted(input_dir.glob("*.mdx")) + if not mdx_files: + print(f"Aucun fichier .mdx trouvé dans : {input_dir}") + sys.exit(1) + + print(f"Conversion de {len(mdx_files)} fichier(s)...") + print(f"Entrée : {input_dir}") + print(f"Sortie : {output_dir}") + if reference_doc: + print(f"Modèle : {reference_doc}") + print() + + for mdx_file in mdx_files: + docx_name = mdx_file.with_suffix(".docx").name + out_file = output_dir / docx_name + print(f"→ {mdx_file.name} -> {docx_name}") + convert_one_file(mdx_file, out_file, reference_doc) + + print() + print("✅ Conversion terminée.") + + +if __name__ == "__main__": + main() diff --git a/src/components/EditionToc.astro b/src/components/EditionToc.astro index 975f4e3..33d84d0 100644 --- a/src/components/EditionToc.astro +++ b/src/components/EditionToc.astro @@ -22,43 +22,58 @@ const entries = [...await getCollection(collection)].sort((a, b) => { const bt = String(b.data.title ?? b.data.term ?? slugOf(b)); return collator.compare(at, bt); }); + +const tocId = `toc-global-${collection}-${String(basePath).replace(/[^\w-]+/g, "-")}`; --- - @@ -136,6 +165,7 @@ const portalLinks = getGlossaryPortalLinks(); display: flex; flex-direction: column; gap: 14px; + min-width: 0; } .glossary-aside__block{ @@ -143,6 +173,7 @@ const portalLinks = getGlossaryPortalLinks(); border-radius: 16px; padding: 14px; background: rgba(127,127,127,0.05); + min-width: 0; } .glossary-aside__block--intro{ @@ -160,10 +191,10 @@ const portalLinks = getGlossaryPortalLinks(); } .glossary-aside__title{ - font-size: 16px; - font-weight: 800; - letter-spacing: .2px; - line-height: 1.3; + font-size: 18px; + font-weight: 850; + letter-spacing: .1px; + line-height: 1.22; } .glossary-aside__pills{ @@ -183,6 +214,7 @@ const portalLinks = getGlossaryPortalLinks(); font-size: 13px; line-height: 1.35; opacity: .92; + min-width: 0; } .glossary-aside__pill--family{ @@ -190,12 +222,54 @@ const portalLinks = getGlossaryPortalLinks(); font-weight: 800; } + .glossary-aside__disclosure{ + padding: 0; + overflow: hidden; + } + + .glossary-aside__summary{ + list-style: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px; + cursor: pointer; + user-select: none; + } + + .glossary-aside__summary::-webkit-details-marker{ + display: none; + } + + .glossary-aside__summary:hover{ + background: rgba(127,127,127,0.035); + } + .glossary-aside__heading{ - margin: 0 0 11px; + margin: 0; + font-size: 16px; + font-weight: 850; + line-height: 1.28; + opacity: .97; + } + + .glossary-aside__chevron{ + flex: 0 0 auto; font-size: 14px; - font-weight: 800; - line-height: 1.35; - opacity: .94; + line-height: 1; + opacity: .72; + transform: rotate(0deg); + transition: transform 160ms ease, opacity 160ms ease; + } + + .glossary-aside__disclosure[open] .glossary-aside__chevron{ + transform: rotate(180deg); + opacity: .96; + } + + .glossary-aside__panel{ + padding: 0 14px 14px; } .glossary-aside__subheading{ @@ -222,16 +296,210 @@ const portalLinks = getGlossaryPortalLinks(); text-decoration: none; font-size: 14px; line-height: 1.4; + word-break: break-word; } .glossary-aside__list a.is-active{ font-weight: 800; } + @media (max-width: 860px){ + .glossary-aside{ + gap: 10px; + } + + .glossary-aside__block{ + border-radius: 14px; + } + + .glossary-aside__block--intro{ + padding: 12px; + } + + .glossary-aside__back{ + margin-bottom: 8px; + font-size: 13px; + line-height: 1.28; + } + + .glossary-aside__title{ + font-size: 19px; + line-height: 1.18; + } + + .glossary-aside__pills{ + gap: 6px; + margin-top: 8px; + } + + .glossary-aside__pill{ + padding: 4px 9px; + font-size: 12px; + line-height: 1.26; + } + + .glossary-aside__summary{ + padding: 12px; + } + + .glossary-aside__heading{ + font-size: 17px; + line-height: 1.2; + } + + .glossary-aside__panel{ + padding: 0 12px 12px; + } + + .glossary-aside__subheading{ + margin: 10px 0 6px; + font-size: 11.5px; + line-height: 1.26; + } + + .glossary-aside__list li{ + margin: 5px 0; + } + + .glossary-aside__list a{ + font-size: 14px; + line-height: 1.34; + } + + .glossary-aside__disclosure:not([open]) .glossary-aside__panel{ + display: none; + } + } + + @media (max-width: 860px){ + .glossary-aside__disclosure{ + background: rgba(127,127,127,0.045); + } + + .glossary-aside__disclosure[open] .glossary-aside__summary{ + border-bottom: 1px solid rgba(127,127,127,0.12); + } + } + + @media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){ + .glossary-aside{ + gap: 8px; + } + + .glossary-aside__block{ + border-radius: 12px; + } + + .glossary-aside__block--intro{ + padding: 10px 11px; + } + + .glossary-aside__back{ + margin-bottom: 6px; + font-size: 12px; + line-height: 1.2; + } + + .glossary-aside__title{ + font-size: 16px; + line-height: 1.14; + } + + .glossary-aside__pills{ + gap: 5px; + margin-top: 7px; + } + + .glossary-aside__pill{ + padding: 3px 8px; + font-size: 11px; + line-height: 1.2; + } + + .glossary-aside__summary{ + padding: 10px 11px; + } + + .glossary-aside__heading{ + font-size: 15px; + line-height: 1.16; + } + + .glossary-aside__panel{ + padding: 0 11px 10px; + } + + .glossary-aside__subheading{ + margin: 8px 0 5px; + font-size: 11px; + line-height: 1.18; + } + + .glossary-aside__list li{ + margin: 4px 0; + } + + .glossary-aside__list a{ + font-size: 13px; + line-height: 1.28; + } + } + + @media (min-width: 861px){ + .glossary-aside__summary{ + cursor: default; + } + + .glossary-aside__chevron{ + display: none; + } + } + @media (prefers-color-scheme: dark){ .glossary-aside__block, .glossary-aside__pill{ background: rgba(255,255,255,0.04); } + + .glossary-aside__summary:hover{ + background: rgba(255,255,255,0.03); + } } - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/components/GlossaryCardGrid.astro b/src/components/GlossaryCardGrid.astro index 1d406e8..568b57d 100644 --- a/src/components/GlossaryCardGrid.astro +++ b/src/components/GlossaryCardGrid.astro @@ -32,16 +32,16 @@ const { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; - margin-top: 14px; + margin-top: 12px; } .glossary-card{ display: flex; flex-direction: column; - gap: 8px; - padding: 14px 16px; + gap: 7px; + padding: 13px 14px; border: 1px solid var(--glossary-border); - border-radius: 18px; + border-radius: 16px; background: var(--glossary-bg-soft); text-decoration: none; transition: transform 120ms ease, background 120ms ease, border-color 120ms ease; @@ -60,17 +60,44 @@ const { .glossary-card strong{ color: var(--glossary-accent); - font-size: 1.04rem; - line-height: 1.28; + font-size: 1.02rem; + line-height: 1.24; } .glossary-card span{ color: inherit; - font-size: 1rem; - line-height: 1.5; + font-size: .98rem; + line-height: 1.46; opacity: .94; } + @media (max-width: 760px){ + .glossary-cards{ + grid-template-columns: 1fr; + gap: 10px; + margin-top: 10px; + } + + .glossary-card{ + gap: 6px; + padding: 12px 12px; + border-radius: 14px; + } + + .glossary-card strong{ + font-size: .98rem; + } + + .glossary-card span{ + font-size: .94rem; + line-height: 1.42; + } + + .glossary-card--wide{ + grid-column: auto; + } + } + @media (prefers-color-scheme: dark){ .glossary-card{ background: rgba(255,255,255,0.04); diff --git a/src/components/GlossaryEntryHero.astro b/src/components/GlossaryEntryHero.astro index bae628f..84971b0 100644 --- a/src/components/GlossaryEntryHero.astro +++ b/src/components/GlossaryEntryHero.astro @@ -75,9 +75,9 @@ const hasScholarlyMeta = position: sticky; top: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px)); z-index: 11; - margin: 0 0 24px; + margin: 0 0 22px; border: 1px solid rgba(127,127,127,0.18); - border-radius: 28px; + border-radius: 24px; background: linear-gradient(180deg, rgba(0,0,0,0.60), rgba(0,0,0,0.92)), radial-gradient(900px 240px at 20% 0%, rgba(0,217,255,0.08), transparent 60%); @@ -97,7 +97,7 @@ const hasScholarlyMeta = calc(var(--entry-hero-pad-top, 18px) - 2px); transition: padding 180ms ease; } - + .glossary-entry-head h1{ margin: 0; font-size: var(--entry-hero-h1-size, clamp(2.2rem, 4vw, 3.15rem)); @@ -129,12 +129,17 @@ const hasScholarlyMeta = max-width 180ms ease, font-size 180ms ease, line-height 180ms ease; + + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; } .glossary-entry-signals{ display: flex; flex-wrap: wrap; - gap: 8px; + gap: 7px; margin: 0; transition: gap 180ms ease; } @@ -142,13 +147,13 @@ const hasScholarlyMeta = .glossary-pill{ display: inline-flex; align-items: center; - gap: 6px; - padding: 5px 10px; + gap: 5px; + padding: 5px 9px; border: 1px solid rgba(127,127,127,0.24); border-radius: 999px; background: rgba(127,127,127,0.05); - font-size: 13px; - line-height: 1.35; + font-size: 12.5px; + line-height: 1.28; transition: padding 180ms ease, font-size 180ms ease, @@ -179,42 +184,67 @@ const hasScholarlyMeta = .glossary-entry-meta p{ margin: 0; - font-size: 14px; - line-height: 1.5; + font-size: 13.5px; + line-height: 1.45; } .glossary-entry-meta p + p{ margin-top: 6px; } - @media (max-width: 720px){ + @media (max-width: 860px){ + .glossary-entry-head{ + position: static; + border-radius: 18px; + margin-bottom: 16px; + } + + .glossary-entry-head__title{ + padding: 12px 12px 10px; + } + + .glossary-entry-summary{ + gap: 9px; + padding: 10px 12px 12px; + } + + .glossary-entry-dek{ + max-width: none; + -webkit-line-clamp: 3; + } + .glossary-entry-signals{ gap: 6px; } .glossary-pill{ font-size: 12px; + padding: 4px 8px; } } - @media (max-width: 860px){ + @media (max-width: 520px){ .glossary-entry-head{ - position: static; - border-radius: 22px; - margin-bottom: 20px; + border-radius: 16px; + margin-bottom: 14px; } .glossary-entry-head__title{ - padding: 14px 14px 12px; + padding: 10px 10px 9px; } .glossary-entry-summary{ - gap: 12px; - padding: 14px; + gap: 8px; + padding: 9px 10px 10px; } .glossary-entry-dek{ - max-width: none; + -webkit-line-clamp: 2; + } + + .glossary-pill{ + font-size: 11.5px; + padding: 3px 7px; } } diff --git a/src/components/GlossaryEntryStickySync.astro b/src/components/GlossaryEntryStickySync.astro index 8eb8e27..673634d 100644 --- a/src/components/GlossaryEntryStickySync.astro +++ b/src/components/GlossaryEntryStickySync.astro @@ -6,6 +6,9 @@ const hero = document.querySelector("[data-ge-hero]"); const follow = document.getElementById("reading-follow"); const mqMobile = window.matchMedia("(max-width: 860px)"); + const mqSmallLandscape = window.matchMedia( + "(orientation: landscape) and (max-width: 920px) and (max-height: 520px)" + ); if (!body || !root || !hero || !follow) return; @@ -18,11 +21,26 @@ body.classList.add(BODY_CLASS); + const isCompactViewport = () => + mqMobile.matches || mqSmallLandscape.matches; + const heroHeight = () => Math.max(0, Math.round(hero.getBoundingClientRect().height || 0)); + const neutralizeGlobalFollowIfCompact = () => { + if (!isCompactViewport()) { + follow.style.display = ""; + return; + } + + follow.classList.remove("is-on"); + follow.setAttribute("aria-hidden", "true"); + follow.style.display = "none"; + root.style.setProperty("--followbar-h", "0px"); + }; + const computeFollowOn = () => - !mqMobile.matches && + !isCompactViewport() && follow.classList.contains("is-on") && follow.style.display !== "none" && follow.getAttribute("aria-hidden") !== "true"; @@ -39,7 +57,7 @@ }; const applyLocalStickyHeight = () => { - const h = mqMobile.matches ? 0 : heroHeight(); + const h = isCompactViewport() ? 0 : heroHeight(); if (h === lastHeight) return; lastHeight = h; @@ -58,6 +76,7 @@ }; const syncAll = () => { + neutralizeGlobalFollowIfCompact(); stripLocalSticky(); syncFollowState(); applyLocalStickyHeight(); @@ -98,6 +117,12 @@ mqMobile.addListener(schedule); } + if (mqSmallLandscape.addEventListener) { + mqSmallLandscape.addEventListener("change", schedule); + } else if (mqSmallLandscape.addListener) { + mqSmallLandscape.addListener(schedule); + } + schedule(); }; @@ -114,37 +139,47 @@ z-index: 10; } - :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-signals){ - gap: 6px; - } - :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-head){ margin-bottom: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; + box-shadow: 0 8px 20px rgba(0,0,0,0.10); + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-head h1){ + letter-spacing: -.03em; } :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-summary){ - gap: 10px; - padding-top: 12px; - padding-bottom: 10px; + gap: 8px; + padding-top: 10px; + padding-bottom: 8px; + border-top-color: rgba(127,127,127,0.10); } :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-dek){ display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 2; + -webkit-line-clamp: 1; overflow: hidden; } + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-signals){ + gap: 5px; + } + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-pill){ - padding: 4px 8px; - font-size: 12px; + gap: 4px; + padding: 3px 7px; + font-size: 11px; } :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-meta){ padding: 0; border-color: transparent; + max-height: 0; + opacity: 0; + overflow: hidden; } :global(body.is-glossary-entry-page.glossary-entry-follow-on #reading-follow){ @@ -177,9 +212,78 @@ } @media (max-width: 860px){ + :global(body.is-glossary-entry-page #reading-follow), + :global(body.is-glossary-entry-page #reading-follow .reading-follow__inner){ + display: none !important; + opacity: 0 !important; + pointer-events: none !important; + visibility: hidden !important; + } + + :global(body.is-glossary-entry-page){ + --followbar-h: 0px !important; + --sticky-offset-px: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px)) !important; + } + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-head){ - margin-bottom: 20px; - border-radius: 22px; + margin-bottom: 18px; + border-radius: 20px; + box-shadow: none; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-summary){ + gap: 6px; + padding-top: 8px; + padding-bottom: 8px; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-dek){ + display: none; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-signals){ + gap: 5px; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-pill){ + padding: 3px 6px; + font-size: 10.5px; + } + } + + @media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){ + :global(body.is-glossary-entry-page #reading-follow), + :global(body.is-glossary-entry-page #reading-follow .reading-follow__inner){ + display: none !important; + opacity: 0 !important; + pointer-events: none !important; + visibility: hidden !important; + } + + :global(body.is-glossary-entry-page){ + --followbar-h: 0px !important; + --sticky-offset-px: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px)) !important; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-head){ + margin-bottom: 14px; + border-radius: 16px; + box-shadow: none; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-summary){ + gap: 5px; + padding-top: 6px; + padding-bottom: 6px; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-dek){ + display: none; + } + + :global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-pill){ + padding: 2px 6px; + font-size: 10px; } } \ No newline at end of file diff --git a/src/components/GlossaryHomeAside.astro b/src/components/GlossaryHomeAside.astro index d0ba2cb..3521b9c 100644 --- a/src/components/GlossaryHomeAside.astro +++ b/src/components/GlossaryHomeAside.astro @@ -37,24 +37,36 @@ const { -
-

Parcours du glossaire

- -
+
+ + Parcours du glossaire + + - {fondamentaux.length > 0 && ( -
-

Noyau archicratique

+
-
+ +
+ + {fondamentaux.length > 0 && ( +
+ + Noyau archicratique + + + +
+ +
+
)} @@ -63,6 +75,7 @@ const { display: flex; flex-direction: column; gap: 14px; + min-width: 0; } .glossary-home-aside__block{ @@ -70,6 +83,7 @@ const { border-radius: 16px; padding: 14px; background: rgba(127,127,127,0.05); + min-width: 0; } .glossary-home-aside__block--intro{ @@ -78,10 +92,10 @@ const { } .glossary-home-aside__title{ - font-size: 16px; - font-weight: 800; - letter-spacing: .2px; - line-height: 1.3; + font-size: 18px; + font-weight: 850; + letter-spacing: .1px; + line-height: 1.22; } .glossary-home-aside__meta{ @@ -108,14 +122,57 @@ const { font-size: 13px; line-height: 1.35; opacity: .92; + min-width: 0; + } + + .glossary-home-aside__disclosure{ + padding: 0; + overflow: hidden; + } + + .glossary-home-aside__summary{ + list-style: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px; + cursor: pointer; + user-select: none; + } + + .glossary-home-aside__summary::-webkit-details-marker{ + display: none; + } + + .glossary-home-aside__summary:hover{ + background: rgba(127,127,127,0.035); } .glossary-home-aside__heading{ - margin: 0 0 11px; + margin: 0; + font-size: 16px; + font-weight: 850; + line-height: 1.28; + opacity: .97; + } + + .glossary-home-aside__chevron{ + flex: 0 0 auto; font-size: 14px; - font-weight: 800; - line-height: 1.35; - opacity: .94; + line-height: 1; + opacity: .72; + transform: rotate(0deg); + transition: transform 160ms ease, opacity 160ms ease; + } + + .glossary-home-aside__disclosure[open] .glossary-home-aside__chevron{ + transform: rotate(180deg); + opacity: .96; + } + + .glossary-home-aside__panel{ + padding: 0 14px 14px; } .glossary-home-aside__list{ @@ -131,7 +188,148 @@ const { .glossary-home-aside__list a{ text-decoration: none; font-size: 14px; - line-height: 1.4; + line-height: 1.42; + word-break: break-word; + } + + @media (max-width: 860px){ + .glossary-home-aside{ + gap: 10px; + } + + .glossary-home-aside__block{ + border-radius: 14px; + } + + .glossary-home-aside__block--intro{ + padding: 12px; + } + + .glossary-home-aside__title{ + font-size: 19px; + line-height: 1.18; + } + + .glossary-home-aside__meta{ + margin-top: 6px; + font-size: 12px; + line-height: 1.32; + } + + .glossary-home-aside__pills{ + gap: 6px; + margin-top: 9px; + } + + .glossary-home-aside__pill{ + padding: 4px 9px; + font-size: 12px; + line-height: 1.28; + } + + .glossary-home-aside__summary{ + padding: 12px; + } + + .glossary-home-aside__heading{ + font-size: 17px; + line-height: 1.2; + } + + .glossary-home-aside__panel{ + padding: 0 12px 12px; + } + + .glossary-home-aside__list li{ + margin: 5px 0; + } + + .glossary-home-aside__list a{ + font-size: 14px; + line-height: 1.34; + } + + .glossary-home-aside__disclosure:not([open]) .glossary-home-aside__panel{ + display: none; + } + } + + @media (max-width: 860px){ + .glossary-home-aside__disclosure{ + background: rgba(127,127,127,0.045); + } + + .glossary-home-aside__disclosure[open] .glossary-home-aside__summary{ + border-bottom: 1px solid rgba(127,127,127,0.12); + } + } + + @media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){ + .glossary-home-aside{ + gap: 8px; + } + + .glossary-home-aside__block{ + border-radius: 12px; + } + + .glossary-home-aside__block--intro{ + padding: 10px 11px; + } + + .glossary-home-aside__title{ + font-size: 16px; + line-height: 1.14; + } + + .glossary-home-aside__meta{ + font-size: 11px; + line-height: 1.26; + margin-top: 5px; + } + + .glossary-home-aside__pills{ + gap: 5px; + margin-top: 8px; + } + + .glossary-home-aside__pill{ + padding: 3px 8px; + font-size: 11px; + line-height: 1.2; + } + + .glossary-home-aside__summary{ + padding: 10px 11px; + } + + .glossary-home-aside__heading{ + font-size: 15px; + line-height: 1.16; + } + + .glossary-home-aside__panel{ + padding: 0 11px 10px; + } + + .glossary-home-aside__list li{ + margin: 4px 0; + } + + .glossary-home-aside__list a{ + font-size: 13px; + line-height: 1.28; + } + } + + @media (min-width: 861px){ + .glossary-home-aside__summary{ + cursor: default; + } + + .glossary-home-aside__chevron{ + display: none; + } } @media (prefers-color-scheme: dark){ @@ -139,5 +337,46 @@ const { .glossary-home-aside__pill{ background: rgba(255,255,255,0.04); } + + .glossary-home-aside__summary:hover{ + background: rgba(255,255,255,0.03); + } } - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/components/GlossaryHomeHero.astro b/src/components/GlossaryHomeHero.astro index 7f570b5..ab55582 100644 --- a/src/components/GlossaryHomeHero.astro +++ b/src/components/GlossaryHomeHero.astro @@ -15,7 +15,28 @@ const {

{kicker}

{title}

-

{intro}

+ +
+

+ {intro} +

+ + +
+

\ No newline at end of file diff --git a/src/components/GlossaryHomeSection.astro b/src/components/GlossaryHomeSection.astro index e48c296..3a31739 100644 --- a/src/components/GlossaryHomeSection.astro +++ b/src/components/GlossaryHomeSection.astro @@ -43,22 +43,22 @@ const showCta = Boolean(ctaHref && ctaLabel); \ No newline at end of file diff --git a/src/components/GlossaryPortalAside.astro b/src/components/GlossaryPortalAside.astro index 1bb7693..f5897ab 100644 --- a/src/components/GlossaryPortalAside.astro +++ b/src/components/GlossaryPortalAside.astro @@ -65,37 +65,39 @@ const { .glossary-portal-aside__block{ border: 1px solid rgba(127,127,127,0.22); border-radius: 16px; - padding: 12px; + padding: 14px; background: rgba(127,127,127,0.05); } .glossary-portal-aside__back{ display: inline-block; - margin-bottom: 8px; - font-size: 13px; + margin-bottom: 10px; + font-size: 14px; font-weight: 700; + line-height: 1.35; text-decoration: none; } .glossary-portal-aside__title{ - font-size: 14px; + font-size: 16px; font-weight: 800; letter-spacing: .2px; - line-height: 1.25; + line-height: 1.3; } .glossary-portal-aside__meta{ margin-top: 8px; - font-size: 12px; - line-height: 1.35; - opacity: .78; + font-size: 13px; + line-height: 1.4; + opacity: .8; } .glossary-portal-aside__heading{ - margin: 0 0 10px; - font-size: 13px; + margin: 0 0 11px; + font-size: 14px; font-weight: 800; - opacity: .9; + line-height: 1.35; + opacity: .94; } .glossary-portal-aside__list{ @@ -105,13 +107,108 @@ const { } .glossary-portal-aside__list li{ - margin: 6px 0; + margin: 7px 0; } .glossary-portal-aside__list a{ text-decoration: none; - font-size: 13px; - line-height: 1.3; + font-size: 14px; + line-height: 1.4; + } + + @media (max-width: 980px){ + .glossary-portal-aside{ + gap: 12px; + } + + .glossary-portal-aside__block{ + padding: 12px; + border-radius: 14px; + } + } + + @media (max-width: 760px){ + .glossary-portal-aside{ + gap: 10px; + } + + .glossary-portal-aside__block{ + padding: 11px 12px; + border-radius: 14px; + } + + .glossary-portal-aside__back{ + margin-bottom: 8px; + font-size: 13px; + } + + .glossary-portal-aside__title{ + font-size: 15px; + line-height: 1.22; + } + + .glossary-portal-aside__meta{ + margin-top: 6px; + font-size: 12px; + line-height: 1.32; + } + + .glossary-portal-aside__heading{ + margin-bottom: 8px; + font-size: 13px; + line-height: 1.22; + } + + .glossary-portal-aside__list li{ + margin: 5px 0; + } + + .glossary-portal-aside__list a{ + font-size: 12.5px; + line-height: 1.3; + } + } + + @media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){ + .glossary-portal-aside{ + gap: 8px; + } + + .glossary-portal-aside__block{ + padding: 9px 10px; + border-radius: 12px; + } + + .glossary-portal-aside__back{ + margin-bottom: 6px; + font-size: 12px; + } + + .glossary-portal-aside__title{ + font-size: 14px; + line-height: 1.18; + } + + .glossary-portal-aside__meta{ + margin-top: 4px; + font-size: 11px; + line-height: 1.24; + } + + .glossary-portal-aside__heading{ + margin-bottom: 6px; + font-size: 12px; + line-height: 1.18; + } + + .glossary-portal-aside__list li{ + margin: 4px 0; + } + + .glossary-portal-aside__list a{ + font-size: 11.5px; + line-height: 1.22; + } } @media (prefers-color-scheme: dark){ diff --git a/src/components/GlossaryPortalCta.astro b/src/components/GlossaryPortalCta.astro index 98d04fa..fb04fa9 100644 --- a/src/components/GlossaryPortalCta.astro +++ b/src/components/GlossaryPortalCta.astro @@ -24,64 +24,44 @@ const { display: inline-flex; align-items: center; justify-content: center; - gap: 8px; min-height: 40px; - padding: 0 14px; - border: 1px solid rgba(0,217,255,0.24); + padding: 7px 14px; + border: 1px solid rgba(127,127,127,0.24); border-radius: 999px; - background: - linear-gradient(180deg, rgba(0,217,255,0.10), rgba(0,217,255,0.04)), - rgba(127,127,127,0.06); - box-shadow: - inset 0 1px 0 rgba(255,255,255,0.05), - 0 0 0 1px rgba(0,217,255,0.04); + background: rgba(127,127,127,0.05); text-decoration: none; - font-size: 12px; - font-weight: 800; - letter-spacing: .01em; - white-space: nowrap; + line-height: 1.2; transition: transform 120ms ease, background 120ms ease, - border-color 120ms ease, - box-shadow 120ms ease; + border-color 120ms ease; } .glossary-portal-cta:hover{ transform: translateY(-1px); - 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.08); - box-shadow: - inset 0 1px 0 rgba(255,255,255,0.06), - 0 0 0 1px rgba(0,217,255,0.08), - 0 10px 28px rgba(0,0,0,0.18); + background: rgba(127,127,127,0.08); + border-color: rgba(0,217,255,0.18); text-decoration: none; } .glossary-portal-cta:focus-visible{ outline: 2px solid rgba(0,217,255,0.28); - outline-offset: 4px; + outline-offset: 3px; } - @media (max-width: 720px){ + @media (max-width: 760px){ .glossary-portal-cta{ - width: 100%; + min-height: 36px; + padding: 6px 12px; + font-size: 12px; } } - @media (prefers-color-scheme: dark){ + @media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){ .glossary-portal-cta{ - background: - linear-gradient(180deg, rgba(0,217,255,0.12), rgba(0,217,255,0.05)), - rgba(255,255,255,0.04); - } - - .glossary-portal-cta:hover{ - background: - linear-gradient(180deg, rgba(0,217,255,0.16), rgba(0,217,255,0.07)), - rgba(255,255,255,0.06); + min-height: 32px; + padding: 5px 10px; + font-size: 11px; } } \ No newline at end of file diff --git a/src/components/GlossaryPortalGrid.astro b/src/components/GlossaryPortalGrid.astro index b78422a..0e079db 100644 --- a/src/components/GlossaryPortalGrid.astro +++ b/src/components/GlossaryPortalGrid.astro @@ -36,17 +36,17 @@ const { .glossary-portals{ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 14px; - margin-top: 14px; + gap: 12px; + margin-top: 12px; } .glossary-portal-card{ display: flex; flex-direction: column; - gap: 8px; - padding: 16px 18px; + gap: 7px; + padding: 14px 15px; border: 1px solid var(--glossary-border); - border-radius: 18px; + border-radius: 16px; background: var(--glossary-bg-soft); text-decoration: none; transition: transform 120ms ease, background 120ms ease, border-color 120ms ease; @@ -61,24 +61,51 @@ const { .glossary-portal-card strong{ color: var(--glossary-accent); - font-size: 1.08rem; - line-height: 1.28; + font-size: 1.04rem; + line-height: 1.24; } .glossary-portal-card span{ color: inherit; - font-size: 1rem; - line-height: 1.5; + font-size: .98rem; + line-height: 1.46; opacity: .94; } .glossary-portal-card small{ color: var(--glossary-accent); - font-size: .94rem; - line-height: 1.35; + font-size: .9rem; + line-height: 1.28; opacity: .9; } + @media (max-width: 760px){ + .glossary-portals{ + grid-template-columns: 1fr; + gap: 10px; + margin-top: 10px; + } + + .glossary-portal-card{ + padding: 12px 12px; + border-radius: 14px; + gap: 6px; + } + + .glossary-portal-card strong{ + font-size: .98rem; + } + + .glossary-portal-card span{ + font-size: .94rem; + line-height: 1.42; + } + + .glossary-portal-card small{ + font-size: .85rem; + } + } + @media (prefers-color-scheme: dark){ .glossary-portal-card{ background: rgba(255,255,255,0.04); diff --git a/src/components/GlossaryPortalHero.astro b/src/components/GlossaryPortalHero.astro index eec5282..433958b 100644 --- a/src/components/GlossaryPortalHero.astro +++ b/src/components/GlossaryPortalHero.astro @@ -16,8 +16,8 @@ const { title, intro, moreParagraphs = [], - introMaxWidth = "72ch", - followIntroMaxWidth = "68ch", + introMaxWidth = "70ch", + followIntroMaxWidth = "62ch", moreMaxHeight = "18rem", } = Astro.props; --- @@ -25,12 +25,13 @@ const {

{kicker}

+

{title}

-

+

{intro}

@@ -43,7 +44,9 @@ const { aria-hidden="false" > {moreParagraphs.map((paragraph) => ( -

{paragraph}

+

+ {paragraph} +

))}
@@ -65,93 +68,121 @@ const { \ No newline at end of file diff --git a/src/components/GlossaryPortalPanel.astro b/src/components/GlossaryPortalPanel.astro index 6a75f5d..569b035 100644 --- a/src/components/GlossaryPortalPanel.astro +++ b/src/components/GlossaryPortalPanel.astro @@ -38,59 +38,90 @@ const { \ No newline at end of file diff --git a/src/components/GlossaryPortalSection.astro b/src/components/GlossaryPortalSection.astro index 13f67f1..efc5096 100644 --- a/src/components/GlossaryPortalSection.astro +++ b/src/components/GlossaryPortalSection.astro @@ -31,37 +31,113 @@ const { \ No newline at end of file diff --git a/src/components/GlossaryPortalStickySync.astro b/src/components/GlossaryPortalStickySync.astro index b1effad..9274847 100644 --- a/src/components/GlossaryPortalStickySync.astro +++ b/src/components/GlossaryPortalStickySync.astro @@ -34,16 +34,24 @@ const { const BODY_CLASS = "is-glossary-portal-page"; const FOLLOW_ON_CLASS = "glossary-portal-follow-on"; const EXPANDED_CLASS = "glossary-portal-hero-expanded"; + const CONDENSED_CLASS = "glossary-portal-hero-condensed"; + const mqMobile = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`); + const mqSmallLandscape = window.matchMedia( + "(orientation: landscape) and (max-width: 920px) and (max-height: 520px)" + ); let expandedAtY = null; let lastScrollY = window.scrollY || 0; let raf = 0; + let lastFollowOn = null; + let lastCondensed = null; + let lastHeroHeight = -1; body.classList.add(BODY_CLASS); - const heroHeight = () => - Math.max(0, Math.round(hero.getBoundingClientRect().height || 0)); + const isCompactViewport = () => + mqMobile.matches || mqSmallLandscape.matches; const stripLocalSticky = () => { document.querySelectorAll(sectionHeadSelector).forEach((el) => { @@ -52,14 +60,53 @@ const { }); }; + const readStickyTop = () => { + const raw = getComputedStyle(document.documentElement) + .getPropertyValue("--glossary-sticky-top") + .trim(); + const n = Number.parseFloat(raw); + return Number.isFinite(n) ? n : 64; + }; + const computeFollowOn = () => - !mqMobile.matches && + !isCompactViewport() && follow.classList.contains("is-on") && follow.style.display !== "none" && follow.getAttribute("aria-hidden") !== "true"; + const computeCondensed = () => { + if (isCompactViewport()) return false; + + const heroRect = hero.getBoundingClientRect(); + const stickyTop = readStickyTop(); + + return heroRect.top <= stickyTop + 2; + }; + + const measureHeroHeight = () => + Math.max(0, Math.round(hero.getBoundingClientRect().height || 0)); + + const PIN_EPS = 3; + + const isHeroPinned = () => { + if (isCompactViewport()) return false; + + const rect = hero.getBoundingClientRect(); + const stickyTop = readStickyTop(); + const cs = getComputedStyle(hero); + + if (cs.position !== "sticky") return false; + + const pinnedOnRail = Math.abs(rect.top - stickyTop) <= PIN_EPS; + const stillVisible = rect.bottom > stickyTop + 24; + + return pinnedOnRail && stillVisible; + }; + const applyLocalStickyHeight = () => { - const h = mqMobile.matches ? 0 : heroHeight(); + const h = isHeroPinned() ? measureHeroHeight() : 0; + if (h === lastHeroHeight) return; + lastHeroHeight = h; if (typeof window.__archiSetLocalStickyHeight === "function") { window.__archiSetLocalStickyHeight(h); @@ -70,10 +117,26 @@ const { const syncFollowState = () => { const on = computeFollowOn(); - body.classList.toggle(FOLLOW_ON_CLASS, on); + + if (on !== lastFollowOn) { + lastFollowOn = on; + body.classList.toggle(FOLLOW_ON_CLASS, on); + } + return on; }; + const syncCondensedState = () => { + const condensed = computeCondensed(); + + if (condensed !== lastCondensed) { + lastCondensed = condensed; + body.classList.toggle(CONDENSED_CLASS, condensed); + } + + return condensed; + }; + const collapseHero = () => { if (!body.classList.contains(EXPANDED_CLASS)) return; @@ -116,12 +179,11 @@ const { schedule(); }; - const syncHeroState = () => { - const followOn = computeFollowOn(); + const syncHeroState = (condensed) => { const expanded = body.classList.contains(EXPANDED_CLASS); - const collapsed = followOn && !expanded; + const collapsed = condensed && !expanded; - if (!followOn || mqMobile.matches) { + if (isCompactViewport() || !condensed) { body.classList.remove(EXPANDED_CLASS); expandedAtY = null; @@ -148,12 +210,7 @@ const { }; const maybeAutoCollapseOnScroll = () => { - if (mqMobile.matches) { - lastScrollY = window.scrollY || 0; - return; - } - - if (!computeFollowOn()) { + if (isCompactViewport()) { lastScrollY = window.scrollY || 0; return; } @@ -181,16 +238,59 @@ const { const syncAll = () => { stripLocalSticky(); - syncFollowState(); - syncHeroState(); - applyLocalStickyHeight(); + + if (isCompactViewport()) { + body.classList.remove(FOLLOW_ON_CLASS); + body.classList.remove(CONDENSED_CLASS); + body.classList.remove(EXPANDED_CLASS); + + lastFollowOn = false; + lastCondensed = false; + expandedAtY = null; + + if (heroMore) { + heroMore.setAttribute("aria-hidden", "false"); + } + + if (heroToggle) { + heroToggle.hidden = true; + heroToggle.setAttribute("aria-expanded", "false"); + } + + requestAnimationFrame(() => { + applyLocalStickyHeight(); + try { + window.__archiUpdateFollow?.(); + } catch {} + }); + + return; + } + + const condensed = syncCondensedState(); + syncHeroState(condensed); + + requestAnimationFrame(() => { + applyLocalStickyHeight(); + syncFollowState(); + try { + window.__archiUpdateFollow?.(); + } catch {} + }); + + requestAnimationFrame(() => { + applyLocalStickyHeight(); + try { + window.__archiUpdateFollow?.(); + } catch {} + }); }; const schedule = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; - requestAnimationFrame(syncAll); + syncAll(); }); }; @@ -229,6 +329,12 @@ const { mqMobile.addListener(schedule); } + if (mqSmallLandscape.addEventListener) { + mqSmallLandscape.addEventListener("change", schedule); + } else if (mqSmallLandscape.addListener) { + mqSmallLandscape.addListener(schedule); + } + schedule(); }; @@ -245,40 +351,63 @@ const { z-index: 10; } - :global(body.is-glossary-portal-page.glossary-portal-follow-on .glossary-portal-hero){ - margin-bottom: 0; - padding: 12px 16px 14px; - row-gap: 10px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + /* Le hero se condense dès qu’il devient sticky */ + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed .glossary-portal-hero){ + padding: + var(--portal-hero-pad-top-condensed, 14px) + var(--portal-hero-pad-x-condensed, 16px) + var(--portal-hero-pad-bottom-condensed, 16px); + row-gap: var(--portal-hero-gap-condensed, 10px); + box-shadow: + inset 0 1px 0 rgba(255,255,255,0.02), + 0 8px 20px rgba(0,0,0,0.12); } - :global(body.is-glossary-portal-page.glossary-portal-follow-on .glossary-portal-hero h1){ - font-size: clamp(1.9rem, 3.2vw, 2.55rem); + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed .glossary-portal-hero h1){ + font-size: var(--portal-hero-h1-size-condensed, clamp(2.05rem, 3.15vw, 2.7rem)); + line-height: var(--portal-hero-h1-lh-condensed, 1); + letter-spacing: var(--portal-hero-h1-spacing-condensed, -.04em); } - :global(body.is-glossary-portal-page.glossary-portal-follow-on .glossary-portal-hero__intro){ - max-width: var(--portal-hero-follow-intro-max-w, 68ch); - font-size: .98rem; - line-height: 1.48; + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed .glossary-portal-hero__intro){ + max-width: var(--portal-hero-follow-intro-max-w, 62ch); + font-size: var(--portal-hero-intro-size-condensed, .98rem); + line-height: var(--portal-hero-intro-lh-condensed, 1.46); } - :global(body.is-glossary-portal-page.glossary-portal-follow-on:not(.glossary-portal-hero-expanded) .glossary-portal-hero__more){ + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed .glossary-portal-hero__kicker){ + opacity: .68; + } + + /* Le more se replie dès l’état condensé */ + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed:not(.glossary-portal-hero-expanded) .glossary-portal-hero__more){ max-height: 0; opacity: 0; overflow: hidden; pointer-events: none; } - :global(body.is-glossary-portal-page.glossary-portal-follow-on:not(.glossary-portal-hero-expanded) .glossary-portal-hero__toggle){ + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed:not(.glossary-portal-hero-expanded) .glossary-portal-hero__toggle){ display: inline-flex; } + /* L’accolage hero + follow n’arrive que quand le follow est actif */ + :global(body.is-glossary-portal-page.glossary-portal-hero-condensed.glossary-portal-follow-on .glossary-portal-hero){ + margin-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + :global(body.is-glossary-portal-page.glossary-portal-follow-on #reading-follow .reading-follow__inner){ + margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; } + :global(body.is-glossary-portal-page.glossary-portal-follow-on #reading-follow .rf-h2){ + letter-spacing: -.02em; + } + :global(body.is-glossary-portal-page .glossary-portal-section__head.is-sticky), :global(body.is-glossary-portal-page .glossary-portal-section__head[data-sticky-active="true"]){ position: static !important; @@ -291,4 +420,68 @@ const { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; } + + @media (max-width: 860px){ + :global(body.is-glossary-portal-page #reading-follow), + :global(body.is-glossary-portal-page #reading-follow .reading-follow__inner){ + display: none !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + } + + :global(body.is-glossary-portal-page){ + --followbar-h: 0px !important; + --sticky-offset-px: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px)) !important; + } + + :global(body.is-glossary-portal-page .glossary-portal-hero){ + margin-bottom: var(--portal-hero-margin-bottom, 18px); + border-radius: 20px !important; + box-shadow: none !important; + } + + :global(body.is-glossary-portal-page .glossary-portal-hero__more){ + max-height: none !important; + opacity: 1 !important; + overflow: visible !important; + pointer-events: auto !important; + } + + :global(body.is-glossary-portal-page .glossary-portal-hero__toggle){ + display: none !important; + } + } + + @media (orientation: landscape) and (max-width: 920px) and (max-height: 520px){ + :global(body.is-glossary-portal-page #reading-follow), + :global(body.is-glossary-portal-page #reading-follow .reading-follow__inner){ + display: none !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + } + + :global(body.is-glossary-portal-page){ + --followbar-h: 0px !important; + --sticky-offset-px: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px)) !important; + } + + :global(body.is-glossary-portal-page .glossary-portal-hero){ + margin-bottom: var(--portal-hero-margin-bottom, 12px); + border-radius: 16px !important; + box-shadow: none !important; + } + + :global(body.is-glossary-portal-page .glossary-portal-hero__more){ + max-height: none !important; + opacity: 1 !important; + overflow: visible !important; + pointer-events: auto !important; + } + + :global(body.is-glossary-portal-page .glossary-portal-hero__toggle){ + display: none !important; + } + } \ No newline at end of file diff --git a/src/components/GlossaryRelationCards.astro b/src/components/GlossaryRelationCards.astro index 5dc7a98..9c70166 100644 --- a/src/components/GlossaryRelationCards.astro +++ b/src/components/GlossaryRelationCards.astro @@ -36,44 +36,45 @@ const relationsHeadingId = "relations-conceptuelles"; + + @media (max-width: 980px){ + .level-toggle{ + gap: 6px; + } + + .level-btn{ + padding: 5px 9px; + font-size: 12px; + } + } + + @media (max-width: 760px){ + .level-toggle{ + display: none; + } + } + \ No newline at end of file diff --git a/src/components/LocalToc.astro b/src/components/LocalToc.astro index a9f8565..80701c3 100644 --- a/src/components/LocalToc.astro +++ b/src/components/LocalToc.astro @@ -3,49 +3,122 @@ const { headings } = Astro.props; // H2/H3 seulement const items = (headings || []).filter((h) => h.depth >= 2 && h.depth <= 3); +const tocId = `toc-local-${Math.random().toString(36).slice(2, 9)}`; --- {items.length > 0 && ( -