From 7f9a0e790bc9e3b00e98fc2685c0ee2c81f11794 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 18:31:32 +0000 Subject: [PATCH 1/4] =?UTF-8?q?Remplacer=20le=20bandeau=20par=20un=20panne?= =?UTF-8?q?au=20lat=C3=A9ral=20et=20=C3=A9largir=20les=20cours=20en=20pays?= =?UTF-8?q?age?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toutes les pages publiées s'organisent autour d'un panneau latéral : recherche, accueil, planning, matières dépliables et parcours ; sur un cours, son sommaire avec la section en cours de lecture, les cours voisins et le partage. Sur ordinateur, il se replie en rail d'icônes (état mémorisé) ; sur téléphone, le bouton « Menu » l'ouvre en tiroir. Sur écran paysage large, les cours s'élargissent et chaque manipulation à un seul schéma affiche le dessin, ses lectures et son commentaire à gauche, les réglages à droite. Manipulations plus hautes que l'écran : 98 → 13 sur 153 à 1366×768, 38 → 2 à 1440×900. Thème sombre pour les pages du wiki et le planning. Story S21, parcours RED puis GREEN. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01428nSMMKS8bv4K8Z2pAr6V --- README.md | 6 +- assets/planning.css | 49 +++--- assets/wiki.css | 225 ++++++++++++++++++++----- assets/wiki.js | 133 ++++++++++++++- docs/ARCHITECTURE.md | 4 +- docs/STORIES.md | 18 ++ tests/browser/test_applications.py | 6 +- tests/browser/test_documents.py | 1 + tests/browser/test_journeys.py | 3 + tests/browser/test_navigation_panel.py | 125 ++++++++++++++ wiki/build.py | 160 ++++++++++++++---- 11 files changed, 622 insertions(+), 108 deletions(-) create mode 100644 tests/browser/test_navigation_panel.py diff --git a/README.md b/README.md index 9047928..514916d 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Un jeu ou un simulateur complète une matière sans occuper l’accueil. Son cod - Seuls les `fichiers` listés sont publiés, dans le dossier `publication` : tests, documentation et scripts de l’application restent hors du site. Déclarer chaque nouveau fichier nécessaire au jeu. - L’application apparaît dans la section « S’entraîner » de sa matière, en pied des `cours_lies` et dans la recherche ; l’accueil ne mentionne que leur nombre dans la carte de la matière. -- La copie publiée reçoit la barre du wiki (retour à la matière, partage) ; la source reste jouable seule, par exemple avec `applications/mohr-forge/Jouer.cmd`. +- La copie publiée reçoit le panneau latéral du wiki, replié au départ pour laisser la place au jeu (retour à la matière, partage) ; la source reste jouable seule, par exemple avec `applications/mohr-forge/Jouer.cmd`. - Les états `disponible`, `a_venir` et `brouillon` suivent les règles des cours. Une application est du code : la routine **Publier** ne l’embarque pas, ses évolutions passent par les commits de développement. ## Planning de formation @@ -147,6 +147,8 @@ Les liens relatifs fonctionnent sous `/PolyWE/` et sur un domaine à la racine. Placer **https://pemcode.github.io/PolyWE/** dans la description du groupe et épingler le message d'annonce. Chacun peut naviguer par matière, rechercher une notion ou suivre un parcours reliant plusieurs cours. -Dans un cours, **Partager** ouvre le menu de partage de l'appareil ; **Copier le lien** permet de coller l'adresse dans WhatsApp. **Partager cette section** cible directement le passage concerné. Le partage natif et la copie proposent un repli si le navigateur ne fournit pas l'API attendue. Les adresses restent valables quand le contenu d'un cours est mis à jour, à condition de conserver son `url` et ses ancres. +Toutes les pages s'organisent autour d'un panneau latéral : recherche, accueil, planning, matières et cours, parcours ; sur un cours, son sommaire avec la section en cours de lecture. Sur ordinateur, il se replie en rail d'icônes (état mémorisé) ; sur téléphone, le bouton **Menu** l'ouvre en tiroir. Sur un écran paysage assez large, les cours s'élargissent et chaque manipulation affiche son schéma à gauche, ses réglages à droite. + +Dans le panneau d'un cours, **Partager** ouvre le menu de partage de l'appareil ; **Copier le lien** permet de coller l'adresse dans WhatsApp. **Partager cette section** cible directement le passage concerné. Le partage natif et la copie proposent un repli si le navigateur ne fournit pas l'API attendue. Les adresses restent valables quand le contenu d'un cours est mis à jour, à condition de conserver son `url` et ses ancres. Références de configuration : [uv](https://docs.astral.sh/uv/guides/projects/), [uv dans GitHub Actions](https://docs.astral.sh/uv/guides/integration/github/), [workflow GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages). diff --git a/assets/planning.css b/assets/planning.css index f56c76f..4911e20 100644 --- a/assets/planning.css +++ b/assets/planning.css @@ -1,65 +1,68 @@ +/* Couleurs de la frise : thème clair, puis sombre (préférence du système). */ +:root { --planning-card:#fff; --planning-soft:#eaf0ec; --planning-soft-line:#cbd8d0; --planning-green:#316d61; --planning-field:#9aada7; --planning-spine:#bbcfc5; --planning-dot:#7a9e92; --planning-current:#467d6e; --planning-current-glow:#dfece5; --planning-now:#a93a15; --planning-plus:#54766a; --planning-week-bg:#edf3ef; --planning-week-ink:#28584a; --planning-tag-bg:#f9e6d9; --planning-tag-ink:#87320e; --planning-divider:#e7ece8; --planning-event-line:#dce3df; --planning-exam-bg:#fff9f5; --planning-support-line:#e1e9e2; --planning-support-ink:#2b6351; --planning-pending-bg:#f0f1eb; --planning-cta:#173d32; --planning-cta-ink:#fff; --planning-focus:#d86632; } +@media (prefers-color-scheme:dark) { :root:not([data-theme="light"]) { --planning-card:#161e27; --planning-soft:#16212a; --planning-soft-line:#2c3a45; --planning-green:#6cc3a7; --planning-field:#4f6271; --planning-spine:#2f4a42; --planning-dot:#5f8f80; --planning-current:#6cc3a7; --planning-current-glow:#1c3a32; --planning-now:#ff9466; --planning-plus:#8fb8aa; --planning-week-bg:#1c2a26; --planning-week-ink:#9fd8c4; --planning-tag-bg:#3a2418; --planning-tag-ink:#ffb48f; --planning-divider:#25313b; --planning-event-line:#2c3946; --planning-exam-bg:#241a17; --planning-support-line:#2c3946; --planning-support-ink:#8fd0b9; --planning-pending-bg:#222c35; --planning-cta:#2f6f5f; --planning-cta-ink:#fff; --planning-focus:#ff9b6b; } } .planning-hero { display:grid; grid-template-columns:1.65fr 1fr; align-items:center; gap:48px; margin-bottom:30px; } .planning-hero h1 { font-size:clamp(2.6rem,5.5vw,4.8rem); } -.planning-position { padding:28px; background:#eaf0ec; border:1px solid #cbd8d0; border-radius:14px; } +.planning-position { padding:28px; background:var(--planning-soft); border:1px solid var(--planning-soft-line); border-radius:14px; } .planning-position strong { display:block; font-size:1.6rem; line-height:1.25; margin:14px 0; letter-spacing:-.035em; } .planning-position p { color:var(--wiki-muted); font-size:.9rem; } .planning-position a { font-size:.85rem; font-weight:650; } .planning-phase-cards { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:28px; } -.planning-phase-card { padding:20px 24px; border:1px solid var(--wiki-rule); border-top:3px solid #316d61; border-radius:9px; background:#fff; text-decoration:none; display:flex; flex-direction:column; gap:8px; } +.planning-phase-card { padding:20px 24px; border:1px solid var(--wiki-rule); border-top:3px solid var(--planning-green); border-radius:9px; background:var(--planning-card); text-decoration:none; display:flex; flex-direction:column; gap:8px; } .planning-phase-card:nth-child(2) { border-top-color:var(--wiki-accent); } .planning-phase-card strong { font-size:1.15rem; }.planning-phase-card>span:last-child { font-size:.85rem; color:var(--wiki-muted); } .planning-phase-card .wiki-eyebrow { font-size:.65rem; letter-spacing:.055em; } -.planning-filters { display:flex; flex-wrap:wrap; align-items:end; gap:12px 16px; padding:20px; background:#fff; border:1px solid var(--wiki-rule); border-radius:12px; } +.planning-filters { display:flex; flex-wrap:wrap; align-items:end; gap:12px 16px; padding:20px; background:var(--planning-card); border:1px solid var(--wiki-rule); border-radius:12px; } .planning-filters>div { flex:1; min-width:190px; } .planning-filters label { display:block; font-size:.8rem; font-weight:650; margin-bottom:6px; } -.planning-filters select { font:inherit; font-size:.9rem; width:100%; min-height:44px; padding:10px; border:1px solid #9aada7; border-radius:6px; background:#fff; color:var(--wiki-ink); } +.planning-filters select { font:inherit; font-size:.9rem; width:100%; min-height:44px; padding:10px; border:1px solid var(--planning-field); border-radius:6px; background:var(--planning-card); color:var(--wiki-ink); } .planning-filters .planning-check { display:flex; align-items:center; gap:8px; min-height:44px; margin:0; font-weight:500; } -.planning-check input { width:18px; height:18px; accent-color:#316d61; } +.planning-check input { width:18px; height:18px; accent-color:var(--planning-green); } .planning-filters button { font-size:.85rem; }.planning-filters button:disabled { opacity:.45; cursor:default; } -.planning-filters .planning-reset { background:#fff; color:var(--wiki-ink); } +.planning-filters .planning-reset { background:var(--planning-card); color:var(--wiki-ink); } #planning-status { font-size:.9rem; font-weight:650; margin:18px 0 6px; } .planning-explainer { font-size:.8rem; color:var(--wiki-muted); max-width:840px; margin:0 0 32px; } .planning-phase { margin:36px 0 44px; scroll-margin-top:20px; } .planning-phase-heading { display:flex; align-items:baseline; justify-content:space-between; gap:18px; margin-bottom:20px; } .planning-phase-heading a { font-size:.8rem; white-space:nowrap; } -.planning-spine { border-left:2px solid #bbcfc5; margin-left:18px; padding-left:26px; } -.planning-week { position:relative; background:#fff; border:1px solid var(--wiki-rule); border-radius:12px; margin-bottom:18px; scroll-margin-top:20px; } -.planning-week::before { content:""; position:absolute; width:12px; height:12px; border-radius:50%; background:#7a9e92; border:4px solid var(--wiki-paper); left:-37px; top:27px; } -.planning-week[data-current="true"] { border-color:#467d6e; box-shadow:0 0 0 2px #dfece5; } -.planning-week[data-current="true"]::before { background:#a93a15; } +.planning-spine { border-left:2px solid var(--planning-spine); margin-left:18px; padding-left:26px; } +.planning-week { position:relative; background:var(--planning-card); border:1px solid var(--wiki-rule); border-radius:12px; margin-bottom:18px; scroll-margin-top:20px; } +.planning-week::before { content:""; position:absolute; width:12px; height:12px; border-radius:50%; background:var(--planning-dot); border:4px solid var(--wiki-paper); left:-37px; top:27px; } +.planning-week[data-current="true"] { border-color:var(--planning-current); box-shadow:0 0 0 2px var(--planning-current-glow); } +.planning-week[data-current="true"]::before { background:var(--planning-now); } .planning-week>summary { cursor:pointer; display:flex; align-items:center; gap:16px; padding:22px; list-style:none; } .planning-week>summary::-webkit-details-marker { display:none; } -.planning-week>summary::after { content:"+"; margin-left:auto; font-size:1.5rem; line-height:1; color:#54766a; } +.planning-week>summary::after { content:"+"; margin-left:auto; font-size:1.5rem; line-height:1; color:var(--planning-plus); } .planning-week[open]>summary::after { content:"−"; } .planning-week>summary strong { display:block; font-size:1.02rem; line-height:1.4; } .planning-week>summary small { display:block; color:var(--wiki-muted); font-size:.75rem; margin-top:5px; max-width:620px; } -.planning-week-no { display:grid; place-content:center; min-width:45px; height:45px; font-size:.8rem; font-weight:750; border-radius:8px; background:#edf3ef; color:#28584a; } -.planning-week-current,.planning-today { display:inline-block; white-space:nowrap; font-size:.7rem; line-height:1.5; font-weight:650; background:#f9e6d9; color:#87320e; padding:4px 8px; border-radius:5px; } -.planning-week-content { padding:0 24px 24px; border-top:1px solid #e7ece8; } +.planning-week-no { display:grid; place-content:center; min-width:45px; height:45px; font-size:.8rem; font-weight:750; border-radius:8px; background:var(--planning-week-bg); color:var(--planning-week-ink); } +.planning-week-current,.planning-today { display:inline-block; white-space:nowrap; font-size:.7rem; line-height:1.5; font-weight:650; background:var(--planning-tag-bg); color:var(--planning-tag-ink); padding:4px 8px; border-radius:5px; } +.planning-week-content { padding:0 24px 24px; border-top:1px solid var(--planning-divider); } .planning-permalink { display:inline-block; font-size:.75rem; margin:14px 0 4px; } .planning-day { margin-top:22px; }.planning-day h3 { display:flex; align-items:center; gap:12px; flex-wrap:wrap; font-size:1rem; margin:0 0 12px; } .planning-events { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; align-items:start; } -.planning-event { --subject-color:#697c73; border:1px solid #dce3df; border-left:3px solid var(--subject-color); border-radius:6px; padding:18px; min-width:0; overflow-wrap:anywhere; } +.planning-event { --subject-color:#697c73; border:1px solid var(--planning-event-line); border-left:3px solid var(--subject-color); border-radius:6px; padding:18px; min-width:0; overflow-wrap:anywhere; } .planning-event[data-theme="metallurgie"] { --subject-color:#ab581d; } .planning-event[data-theme="rdm"] { --subject-color:#307795; } .planning-event[data-theme="procedes"] { --subject-color:#367a59; } .planning-event[data-theme="fabrication"] { --subject-color:#785587; } -.planning-event[data-theme="examens"] { --subject-color:#b34337; background:#fff9f5; } +.planning-event[data-theme="examens"] { --subject-color:#b34337; background:var(--planning-exam-bg); } .planning-event-meta { margin:0 0 8px; color:var(--wiki-muted); font-size:.7rem; } .planning-code { color:var(--subject-color); font-weight:750; margin-right:4px; } .planning-event h4 { font-size:1.05rem; line-height:1.4; margin:0 0 8px; letter-spacing:-.02em; } .planning-time { font-size:.78rem; color:var(--wiki-muted); margin:0 0 12px; } -.planning-supports { border-top:1px solid #e1e9e2; padding-top:10px; } -.planning-supports summary { cursor:pointer; font-weight:650; font-size:.8rem; color:#2b6351; padding:4px 0; } +.planning-supports { border-top:1px solid var(--planning-support-line); padding-top:10px; } +.planning-supports summary { cursor:pointer; font-weight:650; font-size:.8rem; color:var(--planning-support-ink); padding:4px 0; } .planning-supports ul { list-style:none; padding:0; margin:8px 0 0; } .planning-supports li { margin-top:10px; font-size:.85rem; line-height:1.5; }.planning-supports small { display:block; color:var(--wiki-muted); font-size:.7rem; margin-top:2px; } -.planning-pending { color:var(--wiki-muted); }.planning-pending span { display:inline-block; font-size:.65rem; background:#f0f1eb; border-radius:4px; padding:2px 5px; } -.planning-missing { font-size:.75rem; color:var(--wiki-muted); margin:12px 0 0; padding-top:10px; border-top:1px dashed #dce3df; } +.planning-pending { color:var(--wiki-muted); }.planning-pending span { display:inline-block; font-size:.65rem; background:var(--planning-pending-bg); border-radius:4px; padding:2px 5px; } +.planning-missing { font-size:.75rem; color:var(--wiki-muted); margin:12px 0 0; padding-top:10px; border-top:1px dashed var(--planning-event-line); } .planning-sources { font-size:.85rem; color:var(--wiki-muted); border-top:1px solid var(--wiki-rule); padding-top:20px; }.planning-sources summary { cursor:pointer; font-weight:650; } -#planning summary:focus-visible { outline:3px solid #d86632; outline-offset:4px; } +#planning summary:focus-visible { outline:3px solid var(--planning-focus); outline-offset:4px; } @media (max-width:800px) { .planning-hero { gap:20px; grid-template-columns:1fr; }.planning-position { padding:20px; }.planning-position strong { font-size:1.25rem; margin:8px 0; }.planning-events { grid-template-columns:1fr; }.planning-week-current { margin-left:0; } } @media (max-width:640px) { .planning-phase-cards { grid-template-columns:1fr; }.planning-phase-card { padding:16px; }.planning-phase-heading { flex-direction:column; gap:0; }.planning-filters { padding:16px; gap:10px; }.planning-filters>div { flex-basis:100%; }.planning-filters .planning-check { flex-basis:100%; }.planning-filters button { flex:1; }.planning-spine { margin-left:7px; padding-left:16px; }.planning-week::before { left:-27px; }.planning-week>summary { padding:16px 12px; gap:10px; flex-wrap:wrap; }.planning-week>summary>span:nth-child(2) { flex:1; min-width:150px; }.planning-week>summary strong { font-size:.88rem; }.planning-week-no { min-width:36px; height:36px; }.planning-week-content { padding:0 12px 18px; }.planning-event { padding:14px; }.planning-day h3 { font-size:.9rem; }.planning-week>summary small { font-size:.68rem; } } @media print { .planning-filters,.planning-phase-cards,.planning-position,.planning-permalink { display:none!important; }.planning-week { break-inside:avoid; }.planning-week::details-content { content-visibility:visible; height:auto; } } -.planning-position .planning-current-link { display:block; width:fit-content; background:#173d32; color:#fff; border-radius:7px; text-decoration:none; padding:10px 14px; margin:16px 0; font-weight:650; } +.planning-position .planning-current-link { display:block; width:fit-content; background:var(--planning-cta); color:var(--planning-cta-ink); border-radius:7px; text-decoration:none; padding:10px 14px; margin:16px 0; font-weight:650; } .planning-source-note { font-size:.75rem; color:var(--wiki-muted); max-width:380px; } diff --git a/assets/wiki.css b/assets/wiki.css index bfd67b5..c61b849 100644 --- a/assets/wiki.css +++ b/assets/wiki.css @@ -1,63 +1,206 @@ -:root { --wiki-ink: #172c37; --wiki-muted: #536774; --wiki-paper: #f7f7f2; --wiki-rule: #d8e0df; --wiki-accent: #a93a15; } -.wiki-shell { margin:0; background:var(--wiki-paper); color:var(--wiki-ink); font:16px/1.65 system-ui,sans-serif; } -.wiki-shell *, .wiki-nav *, .wiki-course-footer * { box-sizing:border-box; } +/* Révisions IWE : pages du wiki, panneau latéral et ajustements des copies publiées des cours. */ +:root { --wiki-ink:#172c37; --wiki-muted:#536774; --wiki-paper:#f7f7f2; --wiki-surface:#fff; --wiki-rule:#d8e0df; --wiki-accent:#a93a15; + --wiki-tint:#eaf0ec; --wiki-tint-line:#c9d6cf; --wiki-field:#8fa49c; --wiki-green:#316d61; --wiki-focus:#d86632; --wiki-planned:#e9ede9; + --wiki-panel-bg:#fbfbf8; --wiki-hover:rgba(23,44,55,.065); --wiki-active:rgba(169,58,21,.1); --wiki-shadow:0 12px 32px rgba(15,30,40,.2); + --wiki-panel-w:272px; --wiki-rail-w:60px; --wiki-font:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; } +@media (prefers-color-scheme:dark) { + :root:not([data-theme="light"]) { --wiki-ink:#e4ebf2; --wiki-muted:#9fb0bd; --wiki-paper:#10161d; --wiki-surface:#161e27; --wiki-rule:#2c3946; --wiki-accent:#ff9466; + --wiki-tint:#16212a; --wiki-tint-line:#2c3a45; --wiki-field:#4f6271; --wiki-green:#6cc3a7; --wiki-focus:#ff9b6b; --wiki-planned:#1d2832; + --wiki-panel-bg:#0c1217; --wiki-hover:rgba(228,235,242,.07); --wiki-active:rgba(255,148,102,.15); --wiki-shadow:0 12px 32px rgba(0,0,0,.55); } + :root:not([data-theme="light"]) .wiki-shell { color-scheme:dark; } +} +:root[data-theme="dark"] { --wiki-ink:#e4ebf2; --wiki-muted:#9fb0bd; --wiki-paper:#10161d; --wiki-surface:#161e27; --wiki-rule:#2c3946; --wiki-accent:#ff9466; + --wiki-tint:#16212a; --wiki-tint-line:#2c3a45; --wiki-field:#4f6271; --wiki-green:#6cc3a7; --wiki-focus:#ff9b6b; --wiki-planned:#1d2832; + --wiki-panel-bg:#0c1217; --wiki-hover:rgba(228,235,242,.07); --wiki-active:rgba(255,148,102,.15); --wiki-shadow:0 12px 32px rgba(0,0,0,.55); } + +/* ---------- Pages du wiki ---------- */ +.wiki-shell { margin:0; background:var(--wiki-paper); color:var(--wiki-ink); font:16px/1.65 var(--wiki-font); } +.wiki-shell *, .wiki-course-footer * { box-sizing:border-box; } .wiki-shell a { color:inherit; text-decoration-thickness:1px; text-underline-offset:4px; } .wiki-shell a:hover { color:var(--wiki-accent); } -.wiki-shell a:focus-visible, .wiki-shell button:focus-visible, .wiki-shell input:focus-visible, .wiki-shell select:focus-visible, .wiki-nav :focus-visible { outline:3px solid #d86632; outline-offset:4px; } -.wiki-top { max-width:1160px; margin:auto; padding:22px 32px; display:flex; align-items:center; justify-content:space-between; gap:20px; border-bottom:1px solid var(--wiki-rule); } -.wiki-brand { font-size:1.1rem; font-weight:750; text-decoration:none; display:flex; gap:10px; align-items:center; } -.wiki-mark { display:inline-grid; place-content:center; width:34px; height:34px; background:var(--wiki-ink); color:#fff; border-radius:8px; font-size:1rem; } -.wiki-top nav { display:flex; gap:24px; font-size:.9rem; } -.wiki-top nav a { text-decoration:none; } -.wiki-main { max-width:1096px; margin:auto; padding:44px 0 60px; } +.wiki-shell a:focus-visible, .wiki-shell button:focus-visible, .wiki-shell input:focus-visible, .wiki-shell select:focus-visible, .wiki-shell summary:focus-visible, +.wiki-panel :focus-visible, .wiki-menu-button:focus-visible, .wiki-course-footer :focus-visible, .wiki-skip:focus-visible { outline:3px solid var(--wiki-focus); outline-offset:3px; } +.wiki-main { max-width:1180px; margin:0 auto; padding:44px clamp(18px,4vw,48px) 60px; } .wiki-hero { padding:10px 0 34px; } .wiki-eyebrow { font-size:.75rem; text-transform:uppercase; letter-spacing:.13em; font-weight:750; color:var(--wiki-muted); } .wiki-shell h1 { font-size:clamp(2.6rem,6.5vw,5.7rem); line-height:1.05; letter-spacing:-.055em; max-width:900px; margin:22px 0; } .wiki-shell h1 em { font-style:normal; color:var(--wiki-accent); } .wiki-lead { color:var(--wiki-muted); max-width:710px; font-size:1.08rem; } .wiki-stats { display:flex; align-items:baseline; gap:10px; font-size:.86rem; color:var(--wiki-muted); flex-wrap:wrap; margin:30px 0 0; } -.wiki-stats strong { font-size:1.5rem; color:var(--wiki-ink); }.wiki-stats span { margin:0 12px; color:#a8b4b5; } +.wiki-stats strong { font-size:1.5rem; color:var(--wiki-ink); }.wiki-stats span { margin:0 12px; color:var(--wiki-field); } .wiki-shell h2 { font-size:1.45rem; line-height:1.25; letter-spacing:-.025em; margin:0 0 12px; } .wiki-shell h3 { font-size:1.25rem; line-height:1.35; letter-spacing:-.025em; margin:12px 0; } -.wiki-search { border:1px solid var(--wiki-rule); background:#eaf0ec; padding:26px; border-radius:14px; margin:10px 0 42px; } +.wiki-search { border:1px solid var(--wiki-rule); background:var(--wiki-tint); padding:26px; border-radius:14px; margin:10px 0 42px; scroll-margin-top:16px; } .wiki-search label { display:block; font-size:.85rem; margin-bottom:8px; } .wiki-search-row { display:flex; gap:10px; } -.wiki-search input, .wiki-search select, .wiki-shell button { font:inherit; border:1px solid #8fa49c; border-radius:7px; padding:12px; min-height:46px; } -.wiki-search input { width:100%; min-width:0; background:#fff; color:#172c37; } -.wiki-search select { max-width:240px; background:#fff; color:#172c37; } -.wiki-shell button { background:var(--wiki-ink); color:#fff; cursor:pointer; } +.wiki-search input, .wiki-search select, .wiki-main button { font:inherit; border:1px solid var(--wiki-field); border-radius:7px; padding:12px; min-height:46px; } +.wiki-search input { width:100%; min-width:0; background:var(--wiki-surface); color:var(--wiki-ink); } +.wiki-search select { max-width:240px; background:var(--wiki-surface); color:var(--wiki-ink); } +.wiki-main button { background:var(--wiki-ink); color:var(--wiki-paper); cursor:pointer; } #wiki-search-status { font-size:.85rem; margin:12px 0 0; color:var(--wiki-muted); } -.wiki-results { list-style:none; padding:0; margin:0; }.wiki-results li { padding:15px 0; border-top:1px solid #c9d6cf; }.wiki-results li:first-child { margin-top:16px; }.wiki-results a { font-weight:700; }.wiki-results small { display:block; color:var(--wiki-muted); margin-top:4px; } +.wiki-results { list-style:none; padding:0; margin:0; }.wiki-results li { padding:15px 0; border-top:1px solid var(--wiki-tint-line); }.wiki-results li:first-child { margin-top:16px; }.wiki-results a { font-weight:700; }.wiki-results small { display:block; color:var(--wiki-muted); margin-top:4px; } .wiki-section-title { display:flex; align-items:baseline; justify-content:space-between; gap:15px; margin:36px 0 8px; }.wiki-section-title span { color:var(--wiki-muted); font-size:.85rem; } -.wiki-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:18px; } -.wiki-subject, .wiki-card { display:block; border:1px solid var(--wiki-rule); border-radius:12px; padding:25px; background:#fff; overflow-wrap:anywhere; } -.wiki-subject { text-decoration:none; border-top:4px solid var(--wiki-ink); }.wiki-subject:nth-child(2n) { border-top-color:var(--wiki-accent); }.wiki-subject p,.wiki-card p { color:var(--wiki-muted); font-size:.9rem; margin:10px 0; } -.wiki-subject:hover, .wiki-card:focus-within { border-color:#a4b5b2; }.wiki-card h3 a { text-decoration:none; }.wiki-card h3 a:hover { text-decoration:underline; } +#matieres, #parcours { scroll-margin-top:16px; } +.wiki-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(min(100%,320px),1fr)); gap:18px; } +.wiki-subject, .wiki-card { display:block; border:1px solid var(--wiki-rule); border-radius:12px; padding:25px; background:var(--wiki-surface); overflow-wrap:anywhere; } +.wiki-subject { text-decoration:none; border-top:4px solid var(--wiki-ink); transition:border-color .15s, transform .15s; }.wiki-subject:nth-child(2n) { border-top-color:var(--wiki-accent); }.wiki-subject p,.wiki-card p { color:var(--wiki-muted); font-size:.9rem; margin:10px 0; } +.wiki-subject:hover, .wiki-card:focus-within { border-color:var(--wiki-field); }.wiki-card h3 a { text-decoration:none; }.wiki-card h3 a:hover { text-decoration:underline; } .wiki-card-end { display:flex; justify-content:space-between; margin-top:24px; font-size:.8rem; color:var(--wiki-muted); }.wiki-card-end span { color:var(--wiki-accent); font-size:1.2rem; } -.wiki-planned { background:transparent; border-style:dashed; }.wiki-tag { display:inline-block; padding:2px 9px; background:#e9ede9; border-radius:5px; } +.wiki-planned { background:transparent; border-style:dashed; }.wiki-tag { display:inline-block; padding:2px 9px; background:var(--wiki-planned); border-radius:5px; } .wiki-steps { max-width:800px; padding-left:28px; }.wiki-steps li { padding:20px 10px; border-bottom:1px solid var(--wiki-rule); }.wiki-steps a { font-size:1.2rem; font-weight:700; }.wiki-steps p { color:var(--wiki-muted); font-size:.85rem; } .wiki-bottom { padding:26px 32px; border-top:1px solid var(--wiki-rule); color:var(--wiki-muted); text-align:center; font-size:.8rem; } -.wiki-skip { position:absolute; left:12px; top:-100px; background:#fff; padding:10px; z-index:1000; }.wiki-skip:focus { top:8px; } -.wiki-sr { position:absolute!important; width:1px; height:1px; padding:0!important; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; } -.wiki-nav, .wiki-course-footer { background:#edf2ef; color:#172c37; font:14px/1.6 system-ui,sans-serif; border-bottom:1px solid #cad6cf; } -.wiki-nav-inner { max-width:1120px; margin:auto; padding:12px 20px; display:flex; align-items:center; gap:14px; flex-wrap:wrap; } -.wiki-crumbs { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }.wiki-nav a, .wiki-course-footer a { color:#173f49; text-decoration:underline; text-underline-offset:3px; }.wiki-crumbs [aria-current] { color:#55665e; } -.wiki-actions { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }.wiki-nav button { font:600 13px/1.3 system-ui,sans-serif; color:#173f49; background:#fff; border:1px solid #97aaa1; padding:10px 12px; border-radius:6px; min-height:40px; cursor:pointer; box-shadow:none; } -.wiki-status { font-size:12px; }.wiki-course-menu summary { cursor:pointer; padding:8px; }.wiki-course-menu[open] { flex-basis:100%; order:3; }.wiki-course-menu ol { padding-left:28px; }.wiki-course-menu li { margin:6px 0; } -.wiki-course-footer { margin-top:32px; padding:24px; border-top:1px solid #cad6cf; }.wiki-course-footer nav { display:flex; justify-content:space-between; gap:20px; flex-wrap:wrap; }.wiki-course-footer p { margin:0 0 15px; } -.wiki-copy-fallback { flex-basis:100%; }.wiki-copy-fallback input { width:100%; font:14px system-ui,sans-serif; padding:10px; } +.wiki-skip { position:absolute; left:12px; top:-100px; z-index:1000; background:var(--wiki-surface,#fff); color:var(--wiki-ink,#172c37); padding:10px 14px; border-radius:8px; font:600 14px/1.3 var(--wiki-font); }.wiki-skip:focus { top:8px; } +.wiki-sr { position:absolute!important; width:1px; height:1px; padding:0!important; margin:-1px!important; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0!important; } [hidden] { display:none!important; } -@media (max-width:1160px) { .wiki-main { padding:30px 24px 50px; } } -@media (max-width:640px) { .wiki-top { padding:16px 20px; flex-wrap:wrap; gap:12px; }.wiki-top nav { gap:18px; }.wiki-grid { grid-template-columns:1fr; }.wiki-section-title { display:block; }.wiki-section-title span { display:block; margin-bottom:14px; }.wiki-search { padding:20px; }.wiki-search-row { flex-wrap:wrap; }.wiki-search input { flex-basis:100%; }.wiki-search select { flex:1; min-width:0; max-width:none; }.wiki-subject,.wiki-card { padding:22px; }.wiki-stats { gap:8px; }.wiki-stats span { margin:0 3px; }.wiki-nav-inner { gap:6px; padding:10px 14px; } } +@media (max-width:640px) { .wiki-section-title { display:block; }.wiki-section-title span { display:block; margin-bottom:14px; }.wiki-search { padding:20px; }.wiki-search-row { flex-wrap:wrap; }.wiki-search input { flex-basis:100%; }.wiki-search select { flex:1; min-width:0; max-width:none; }.wiki-subject,.wiki-card { padding:22px; }.wiki-stats { gap:8px; }.wiki-stats span { margin:0 3px; }.wiki-main { padding-top:30px; } } @media (prefers-reduced-motion:no-preference) { .wiki-shell { scroll-behavior:smooth; } } -@media print { .wiki-nav, .wiki-course-footer, .wiki-search, .wiki-top nav { display:none; } } -.wiki-section-share { font:600 12px/1.4 system-ui,sans-serif; color:#254c57; background:#edf3f0; border:1px solid #b7cbc0; border-radius:5px; padding:7px 10px; cursor:pointer; margin:0 0 14px; } .wiki-section-share:focus-visible { outline:3px solid #d86632; outline-offset:3px; } @media print { .wiki-section-share { display:none; } } -.wiki-planning-teaser { display:flex; justify-content:space-between; align-items:center; gap:24px; border:1px solid #cad8ce; background:#fff; border-left:4px solid #316d61; border-radius:10px; padding:22px 24px; margin:0 0 30px; text-decoration:none; } +.wiki-planning-teaser { display:flex; justify-content:space-between; align-items:center; gap:24px; border:1px solid var(--wiki-tint-line); background:var(--wiki-surface); border-left:4px solid var(--wiki-green); border-radius:10px; padding:22px 24px; margin:0 0 30px; text-decoration:none; } .wiki-planning-teaser strong { display:block; font-size:1.1rem; }.wiki-planning-teaser small { display:block; color:var(--wiki-muted); margin-top:5px; }.wiki-planning-teaser>span:last-child { font-size:.85rem; font-weight:650; white-space:nowrap; } -.wiki-top nav { flex-wrap:wrap; } -@media (max-width:640px) { .wiki-planning-teaser { align-items:start; flex-direction:column; gap:12px; padding:20px; }.wiki-top nav { gap:14px; font-size:.85rem; } } - +@media (max-width:640px) { .wiki-planning-teaser { align-items:start; flex-direction:column; gap:12px; padding:20px; } } .wiki-training { margin-top:40px; } -.wiki-app-card { border-left:4px solid #316d61; } -.wiki-training-link span { color:#55665e; } +.wiki-app-card { border-left:4px solid var(--wiki-green); } + +/* ---------- Pied ajouté aux cours et partage d'une section ---------- */ +.wiki-course-footer { background:var(--wiki-tint); color:var(--wiki-ink); font:14px/1.6 var(--wiki-font); border-top:1px solid var(--wiki-rule); margin:32px 0 0; padding:24px clamp(18px,4vw,40px) 90px; max-width:none; } +.wiki-course-footer a { color:var(--wiki-ink); text-decoration:underline; text-underline-offset:3px; } +.wiki-course-footer p { margin:0 0 15px; }.wiki-course-footer nav { display:flex; justify-content:space-between; gap:20px; flex-wrap:wrap; } +.wiki-training-link span { color:var(--wiki-muted); } +.wiki-section-share { font:600 12px/1.4 var(--wiki-font); color:var(--wiki-ink); background:var(--wiki-tint); border:1px solid var(--wiki-tint-line); border-radius:6px; padding:7px 10px; cursor:pointer; margin:0 0 14px; } +.wiki-section-share:hover { border-color:var(--wiki-field); } +.wiki-section-share:focus-visible { outline:3px solid var(--wiki-focus); outline-offset:3px; } + +/* ---------- Panneau latéral ---------- */ +.wiki-panel { position:fixed; top:0; bottom:0; left:0; z-index:900; width:min(86vw,320px); display:flex; flex-direction:column; box-sizing:border-box; + background:var(--wiki-panel-bg); color:var(--wiki-ink); border-right:1px solid var(--wiki-rule); font:14px/1.45 var(--wiki-font); text-align:left; letter-spacing:normal; + -webkit-text-size-adjust:100%; transform:translateX(-102%); visibility:hidden; transition:transform .24s ease, visibility 0s linear .24s; } +html.wiki-tiroir .wiki-panel { transform:none; visibility:visible; box-shadow:var(--wiki-shadow); transition:transform .24s ease; } +.wiki-panel *, .wiki-panel *::before, .wiki-panel *::after, .wiki-menu-button, .wiki-menu-button * { box-sizing:border-box; } +.wiki-panel:focus { outline:none; } +:where(.wiki-panel) a { color:inherit; text-decoration:none; } +:where(.wiki-panel) :is(ul, ol) { list-style:none; margin:0; padding:0; } +:where(.wiki-panel) :is(li, p, details, form) { margin:0; } +:where(.wiki-panel) button { font:inherit; color:inherit; } +.wiki-ico { display:block; flex:none; width:20px; height:20px; max-width:none; overflow:visible; } +.wiki-panel-head { display:flex; align-items:center; gap:8px; padding:14px 12px 10px 16px; } +.wiki-brand { display:flex; flex:1; min-width:0; align-items:center; gap:10px; border-radius:9px; font-weight:750; font-size:15px; letter-spacing:-.01em; } +.wiki-mark { display:inline-grid; flex:none; place-content:center; width:32px; height:32px; border-radius:9px; background:var(--wiki-ink); color:var(--wiki-panel-bg); font:800 15px/1 var(--wiki-font); } +.wiki-panel-fold, .wiki-panel-close { display:grid; flex:none; place-content:center; width:36px; height:36px; margin:0; padding:0; border:1px solid transparent; border-radius:9px; background:transparent; color:var(--wiki-muted); cursor:pointer; } +.wiki-panel-fold:hover, .wiki-panel-close:hover { background:var(--wiki-hover); color:var(--wiki-ink); } +.wiki-panel-fold { display:none; } +.wiki-if-folded { display:none; } +.wiki-panel-scroll { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:2px 10px 18px; scrollbar-width:thin; scrollbar-color:var(--wiki-rule) transparent; } +.wiki-panel-search { position:relative; margin:4px 2px 10px; } +.wiki-panel-search .wiki-ico { position:absolute; left:11px; top:50%; width:17px; height:17px; transform:translateY(-50%); color:var(--wiki-muted); pointer-events:none; } +.wiki-panel-search input { display:block; width:100%; min-height:38px; margin:0; padding:8px 10px 8px 35px; border:1px solid var(--wiki-rule); border-radius:9px; background:var(--wiki-surface); color:var(--wiki-ink); font:inherit; -webkit-appearance:none; appearance:none; } +.wiki-panel-search input::placeholder { color:var(--wiki-muted); opacity:1; } +.wiki-panel-search input:focus { border-color:var(--wiki-field); } +.wiki-panel-nav a { display:flex; align-items:center; gap:10px; padding:7px 10px; border-radius:8px; color:var(--wiki-ink); } +.wiki-panel-nav a:hover { background:var(--wiki-hover); } +.wiki-panel-nav a[aria-current="page"] { background:var(--wiki-active); font-weight:650; } +.wiki-panel-quick { display:grid; gap:1px; } +.wiki-quick .wiki-ico { color:var(--wiki-muted); } +.wiki-quick a[aria-current="page"] .wiki-ico { color:var(--wiki-accent); } +.wiki-rail-only { display:none; } +.wiki-panel-label { margin:18px 10px 6px; font-size:11px; font-weight:750; letter-spacing:.12em; text-transform:uppercase; color:var(--wiki-muted); } +.wiki-panel-here { margin:10px 0 2px; padding:12px 0 10px; border-top:1px solid var(--wiki-rule); border-bottom:1px solid var(--wiki-rule); } +.wiki-panel-nav a.wiki-here-subject { display:inline-flex; padding:2px 10px; font-size:11.5px; font-weight:750; letter-spacing:.08em; text-transform:uppercase; color:var(--wiki-accent); } +.wiki-panel-nav a.wiki-here-subject:hover { text-decoration:underline; background:none; } +.wiki-here-title { margin:3px 10px 0; font-size:15px; font-weight:700; line-height:1.3; } +.wiki-here-title span { display:block; margin-bottom:2px; font-size:11.5px; font-weight:600; color:var(--wiki-muted); } +.wiki-panel-here .wiki-panel-label { margin-top:14px; } +.wiki-toc { display:grid; gap:1px; } +.wiki-toc a { position:relative; align-items:baseline; gap:6px; padding:5px 10px 5px 14px; font-size:13.5px; line-height:1.35; color:var(--wiki-muted); } +.wiki-toc a::before { content:""; position:absolute; left:2px; top:6px; bottom:6px; width:3px; border-radius:2px; background:transparent; } +.wiki-toc a:hover { color:var(--wiki-ink); } +.wiki-toc a[aria-current="location"] { color:var(--wiki-ink); font-weight:650; background:var(--wiki-active); } +.wiki-toc a[aria-current="location"]::before { background:var(--wiki-accent); } +.wiki-toc-num { flex:none; min-width:1.15em; font-weight:750; font-variant-numeric:tabular-nums; color:var(--wiki-accent); } +.wiki-here-pager { display:flex; gap:8px; margin:10px 6px 0; } +.wiki-panel-nav .wiki-here-pager a { padding:6px 10px; border:1px solid var(--wiki-rule); font-size:13px; font-weight:600; } +.wiki-panel-nav .wiki-here-pager a[rel="next"] { margin-left:auto; } +.wiki-panel-groups > li { margin:1px 0; } +.wiki-panel-groups summary { display:flex; align-items:center; gap:8px; padding:7px 10px; border-radius:8px; cursor:pointer; list-style:none; font-weight:600; font-size:14px; line-height:1.35; } +.wiki-panel-groups summary::-webkit-details-marker { display:none; } +.wiki-panel-groups summary::before { content:""; flex:none; width:6px; height:6px; margin:0 4px 0 2px; border-right:2px solid currentColor; border-bottom:2px solid currentColor; transform:rotate(-45deg); opacity:.55; transition:transform .15s; } +.wiki-panel-groups details[open] > summary::before { transform:rotate(45deg); } +.wiki-panel-groups summary:hover { background:var(--wiki-hover); } +.wiki-panel-groups summary > span:first-child { flex:1; min-width:0; } +.wiki-count { flex:none; padding:1px 7px; border-radius:999px; background:var(--wiki-hover); font-size:11px; font-weight:700; color:var(--wiki-muted); } +.wiki-panel-groups details ul { margin:2px 0 8px 15px; padding-left:7px; border-left:1px solid var(--wiki-rule); } +.wiki-panel-groups details a { align-items:baseline; gap:8px; padding:5px 8px; font-size:13.5px; line-height:1.35; color:var(--wiki-muted); } +.wiki-panel-groups details a:hover, .wiki-panel-groups details a[aria-current="page"] { color:var(--wiki-ink); } +.wiki-num { flex:none; font-size:11px; font-weight:700; font-variant-numeric:tabular-nums; color:var(--wiki-muted); opacity:.75; } +.wiki-panel-share { padding:10px 14px 12px; border-top:1px solid var(--wiki-rule); background:var(--wiki-panel-bg); } +.wiki-panel .wiki-actions { display:flex; flex-wrap:wrap; gap:6px; } +.wiki-panel .wiki-actions button { flex:1; min-height:36px; margin:0; padding:7px 10px; border:1px solid var(--wiki-rule); border-radius:8px; background:var(--wiki-surface); color:var(--wiki-ink); font:600 13px/1.2 var(--wiki-font); cursor:pointer; } +.wiki-panel .wiki-actions button:hover { border-color:var(--wiki-field); } +.wiki-status { display:block; font-size:12px; color:var(--wiki-muted); } +.wiki-status:not(:empty) { margin-top:6px; } +.wiki-copy-fallback { display:block; margin-top:8px; font-size:12px; } +.wiki-copy-fallback input { display:block; width:100%; margin-top:4px; padding:8px; border:1px solid var(--wiki-rule); border-radius:6px; background:var(--wiki-surface); color:var(--wiki-ink); font:13px var(--wiki-font); } +.wiki-menu-button { position:fixed; right:14px; bottom:calc(14px + env(safe-area-inset-bottom,0px)); z-index:880; display:inline-flex; align-items:center; gap:8px; min-height:46px; margin:0; padding:0 18px 0 14px; + border:0; border-radius:999px; background:var(--wiki-ink); color:var(--wiki-panel-bg); font:650 15px/1 var(--wiki-font); box-shadow:var(--wiki-shadow); cursor:pointer; } +.wiki-backdrop { position:fixed; inset:0; z-index:890; background:rgba(8,14,20,.45); } +/* Le jeu Mohr Forge affiche sa barre de quête en bas de l'écran étroit : le bouton remonte au-dessus. */ +@media (max-width:1050px) { body:has(.quest-tracker) .wiki-menu-button { bottom:calc(96px + env(safe-area-inset-bottom,0px)); } } + +@media (min-width:1000px) { + .wiki-panel { width:var(--wiki-panel-w); transform:none; visibility:visible; box-shadow:none; transition:none; } + .wiki-panel-fold { display:grid; } + .wiki-panel-close, .wiki-menu-button, .wiki-backdrop { display:none!important; } + html.wiki-avec-panneau body, body:has(> .wiki-panel) { padding-left:var(--wiki-panel-w); } + html.wiki-avec-panneau.wiki-replie body { padding-left:var(--wiki-rail-w); } + html.wiki-replie .wiki-panel { width:var(--wiki-rail-w); } + html.wiki-replie .wiki-panel-head { flex-direction:column; gap:6px; padding:12px 0 8px; } + html.wiki-replie .wiki-brand { flex:none; } + html.wiki-replie .wiki-panel-fold .wiki-ico { transform:scaleX(-1); } + html.wiki-replie .wiki-if-open { display:none; } + html.wiki-replie .wiki-if-folded { display:inline; } + html.wiki-replie .wiki-panel-scroll { padding:2px 8px 12px; } + html.wiki-replie .wiki-panel-search, html.wiki-replie .wiki-panel-here, html.wiki-replie .wiki-panel-label, + html.wiki-replie .wiki-panel-groups, html.wiki-replie .wiki-panel-share { display:none; } + html.wiki-replie .wiki-rail-only { display:block; } + html.wiki-replie .wiki-panel-quick a { justify-content:center; padding:10px 0; } + html.wiki-replie .wiki-panel-text { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; } + /* Le sommaire du panneau remplace le sommaire horizontal du cours tant que le panneau est ouvert. */ + html.wiki-avec-panneau:not(.wiki-replie) nav.toc { display:none; } +} + +/* ---------- Cours sur écran paysage : plus de largeur, manipulations côte à côte ---------- */ +@media (min-width:1000px) and (orientation:landscape) { + html.wiki-avec-panneau .wrap, html.wiki-avec-panneau .wrap.wide, html.wiki-avec-panneau .toc-in { max-width:1180px; } + html.wiki-avec-panneau .lede { max-width:980px; } + html.wiki-avec-panneau .lab svg { max-height:calc(100vh - 110px); } + /* La série Métallurgie garde sa colonne d'origine : ses figures suivent la largeur et s'allongeraient. */ +} +/* Manipulation à un seul schéma : schéma à gauche, au moins 600 px (seuil des dessins larges des cours), réglages à droite. + La largeur du schéma ne dépend que de la fenêtre : replier le panneau n'oblige pas les cours à redessiner. + Avant le script du wiki, la grille seule donne cette largeur ; ensuite, lectures et commentaire passent sous le schéma. */ +@media (min-width:1280px) and (orientation:landscape) { + html.wiki-avec-panneau .lab:has(> svg):not(:has(> svg ~ svg)) { display:grid; grid-template-columns:min(clamp(600px,60vw - 460px,760px),calc(100% - 324px)) minmax(300px,1fr); grid-template-rows:auto auto repeat(28,auto) 1fr; grid-auto-flow:row dense; column-gap:24px; align-items:start; } + html.wiki-avec-panneau .lab:has(> svg):not(:has(> svg ~ svg)) > * { grid-column:2; min-width:0; } + html.wiki-avec-panneau .lab:has(> svg):not(:has(> svg ~ svg)) > .lab-h, html.wiki-avec-panneau .lab:has(> svg):not(:has(> svg ~ svg)) > .lab-sub:nth-child(2) { grid-column:1 / -1; } + html.wiki-avec-panneau .lab:has(> svg):not(:has(> svg ~ svg)) > svg { grid-column:1; grid-row:3 / -1; position:sticky; top:14px; margin-top:.5rem; } + html.wiki-avec-panneau .lab.wiki-lab-split { display:grid; grid-template-columns:min(clamp(600px,60vw - 460px,760px),calc(100% - 324px)) minmax(300px,1fr); grid-template-rows:none; column-gap:24px; align-items:start; } + html.wiki-avec-panneau .lab.wiki-lab-split > .lab-h, html.wiki-avec-panneau .lab.wiki-lab-split > .lab-sub { grid-column:1 / -1; } + html.wiki-avec-panneau .lab.wiki-lab-split > .wiki-lab-fig { grid-column:1; grid-row:auto; position:sticky; top:14px; min-width:0; padding-top:.5rem; } + html.wiki-avec-panneau .lab.wiki-lab-split > .wiki-lab-side { grid-column:2; grid-row:auto; min-width:0; } + html.wiki-avec-panneau #quiz:has(> .qz) { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:16px; align-items:start; } + html.wiki-avec-panneau #checklist.check { columns:2; column-gap:32px; } + html.wiki-avec-panneau #checklist.check > label { break-inside:avoid; } +} +@media (min-width:1280px) and (orientation:landscape) { + html.wiki-avec-panneau.wiki-replie .lab:has(> svg):not(:has(> svg ~ svg)) > svg, html.wiki-avec-panneau.wiki-replie .lab.wiki-lab-split > .wiki-lab-fig { top:62px; } +} +/* Colonne de réglages étroite (panneau ouvert, fenêtre de 1 280 à 1 359 px) : curseur sous son libellé. */ +@media (min-width:1280px) and (max-width:1359.98px) and (orientation:landscape) { + html.wiki-avec-panneau:not(.wiki-replie) .lab:has(> svg):not(:has(> svg ~ svg)) .ctrl, html.wiki-avec-panneau:not(.wiki-replie) .wiki-lab-side .ctrl { grid-template-columns:minmax(0,1fr) auto; } + html.wiki-avec-panneau:not(.wiki-replie) .lab:has(> svg):not(:has(> svg ~ svg)) .ctrl input[type=range], html.wiki-avec-panneau:not(.wiki-replie) .wiki-lab-side .ctrl input[type=range] { grid-column:1 / -1; grid-row:2; } +} + +@media (prefers-reduced-motion:reduce) { .wiki-panel, html.wiki-tiroir .wiki-panel { transition:none; } } +@media print { + .wiki-panel, .wiki-menu-button, .wiki-backdrop, .wiki-skip, .wiki-course-footer, .wiki-search, .wiki-section-share { display:none!important; } + html.wiki-avec-panneau body, body:has(> .wiki-panel) { padding-left:0!important; } +} diff --git a/assets/wiki.js b/assets/wiki.js index 80dc7a6..8a22038 100644 --- a/assets/wiki.js +++ b/assets/wiki.js @@ -75,8 +75,9 @@ if (query.value || subject.value) search(); } - const navigation = document.querySelector(".wiki-nav"); - if (!navigation) return; + const panel = document.getElementById("wiki-panel"); + if (!panel) return; + const root = document.documentElement; const status = document.getElementById("wiki-share-status"); const title = document.querySelector('meta[name="wiki-course-title"]')?.content || document.title; const targetFor = hash => { @@ -85,17 +86,131 @@ }; const headingFor = target => target?.matches("h2,h3") ? target : target?.querySelector("h2,h3"); - // Ces boutons sont ajoutés au document affiché, jamais aux sources pédagogiques. - for (const anchor of navigation.querySelectorAll('.wiki-course-menu a[href^="#"]')) { - const target = targetFor(anchor.hash); - const heading = headingFor(target); + // Panneau : ouvert, replié en rail (écran large) ou tiroir (écran étroit). La largeur décide, comme dans la feuille de style. + const docked = matchMedia("(min-width: 1000px)"); + const opener = document.querySelector('[data-wiki-panel="open"]'); + const backdrop = document.querySelector(".wiki-backdrop"); + const fold = panel.querySelector('[data-wiki-panel="fold"]'); + const query = document.getElementById("wiki-panel-query"); + if (query && form) query.value = new URL(location.href).searchParams.get("q") || ""; + const syncFold = () => fold?.setAttribute("aria-expanded", String(!root.classList.contains("wiki-replie"))); + syncFold(); + + function openDrawer() { + root.classList.add("wiki-tiroir"); + opener?.setAttribute("aria-expanded", "true"); + if (backdrop) backdrop.hidden = false; + (panel.querySelector('[data-wiki-panel="close"]') || panel).focus(); + } + function closeDrawer(restoreFocus = true) { + if (!root.classList.contains("wiki-tiroir")) return; + root.classList.remove("wiki-tiroir"); + opener?.setAttribute("aria-expanded", "false"); + if (backdrop) backdrop.hidden = true; + if (restoreFocus) opener?.focus(); + } + const drawerOpen = () => !docked.matches && root.classList.contains("wiki-tiroir"); + + document.addEventListener("click", event => { + const control = event.target.closest("[data-wiki-panel]"); + if (control) { + const action = control.dataset.wikiPanel; + if (action === "open") openDrawer(); + else if (action === "close") closeDrawer(); + else if (action === "fold") { + root.classList.toggle("wiki-replie"); + syncFold(); + try { localStorage.setItem("wiki-panneau", root.classList.contains("wiki-replie") ? "replie" : "deplie"); } catch {} + } + return; + } + // Suivre un lien du tiroir le referme, y compris vers une section de la même page. + if (drawerOpen() && event.target.closest("#wiki-panel a[href]")) closeDrawer(false); + }); + document.addEventListener("keydown", event => { + if (!drawerOpen()) return; + if (event.key === "Escape") { event.preventDefault(); closeDrawer(); return; } + if (event.key !== "Tab") return; + const focusable = [...panel.querySelectorAll("a[href], button, input, summary")].filter(item => item.offsetParent !== null); + if (!focusable.length) return; + const first = focusable[0], last = focusable[focusable.length - 1]; + if (event.shiftKey && (document.activeElement === first || document.activeElement === panel)) { event.preventDefault(); last.focus(); } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } + }); + docked.addEventListener("change", () => closeDrawer(false)); + + // Sommaire du cours : surligner la section en cours de lecture et la garder visible dans le panneau. + const scroller = panel.querySelector(".wiki-panel-scroll"); + const sections = [...panel.querySelectorAll('.wiki-toc a[href^="#"]')] + .map(link => ({link, target: targetFor(link.hash)})).filter(item => item.target) + .sort((a, b) => a.target.compareDocumentPosition(b.target) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1); + let active = null, pending = false; + function spy() { + pending = false; + if (!sections.length) return; + const line = innerHeight * 0.3; + let current = sections[0]; + for (const item of sections) { + if (item.target.getBoundingClientRect().top <= line) current = item; else break; + } + if (innerHeight + scrollY >= document.documentElement.scrollHeight - 2) current = sections[sections.length - 1]; + if (current.link === active) return; + active?.removeAttribute("aria-current"); + active = current.link; + active.setAttribute("aria-current", "location"); + if (scroller && getComputedStyle(scroller).display !== "none") { + const box = active.getBoundingClientRect(), view = scroller.getBoundingClientRect(); + if (box.top < view.top + 24 || box.bottom > view.bottom - 24) scroller.scrollTop += box.top - view.top - view.height / 3; + } + } + const requestSpy = () => { if (!pending) { pending = true; requestAnimationFrame(spy); } }; + addEventListener("scroll", requestSpy, {passive: true}); + addEventListener("resize", requestSpy); + addEventListener("hashchange", requestSpy); + addEventListener("load", requestSpy); + spy(); + + // Manipulations sur écran paysage : lectures et commentaire sous le schéma, réglages à droite. + // La grille de la feuille de style a déjà donné au schéma sa largeur avant le premier dessin du cours ; + // le regroupement ne la change pas. En dessous du seuil, l'ordre d'origine des éléments est rétabli. + const landscape = matchMedia("(min-width: 1280px) and (orientation: landscape)"); + const labs = [...document.querySelectorAll(".lab")].filter(lab => lab.querySelectorAll(":scope > svg").length === 1); + function splitLab(lab) { + if (lab.classList.contains("wiki-lab-split")) return; + const children = [...lab.children]; + const figure = document.createElement("div"); + const side = document.createElement("div"); + figure.className = "wiki-lab-fig"; + side.className = "wiki-lab-side"; + for (const child of children) { + if (child.matches(".lab-h") || (child.matches(".lab-sub") && child === children[1])) continue; + (child.matches("svg, .readout, .status") ? figure : side).append(child); + } + lab.wikiChildren = children; + lab.append(figure, side); + lab.classList.add("wiki-lab-split"); + } + function joinLab(lab) { + if (!lab.classList.contains("wiki-lab-split")) return; + const wrappers = lab.querySelectorAll(":scope > .wiki-lab-fig, :scope > .wiki-lab-side"); + lab.append(...lab.wikiChildren); + wrappers.forEach(wrapper => wrapper.remove()); + lab.classList.remove("wiki-lab-split"); + } + const arrangeLabs = () => labs.forEach(landscape.matches ? splitLab : joinLab); + arrangeLabs(); + landscape.addEventListener("change", arrangeLabs); + + // Boutons « Partager cette section » : ajoutés au document affiché, jamais aux sources pédagogiques. + for (const {link} of sections) { + const heading = headingFor(targetFor(link.hash)); if (!heading) continue; const button = document.createElement("button"); button.type = "button"; button.className = "wiki-section-share"; button.textContent = "Partager cette section"; button.dataset.wikiShare = "native"; - button.dataset.wikiAnchor = anchor.hash; + button.dataset.wikiAnchor = link.hash; heading.insertAdjacentElement("afterend", button); } @@ -105,7 +220,9 @@ await navigator.clipboard.writeText(url); status.textContent = "Lien copié."; } catch { - const fallback = navigation.querySelector(".wiki-copy-fallback"); + if (!docked.matches && !root.classList.contains("wiki-tiroir")) openDrawer(); + if (docked.matches && root.classList.contains("wiki-replie")) { root.classList.remove("wiki-replie"); syncFold(); } + const fallback = panel.querySelector(".wiki-copy-fallback"); fallback.hidden = false; const input = fallback.querySelector("input"); input.value = url; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 283086b..3324e42 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -57,7 +57,7 @@ Le catalogue conserve les informations éditoriales stables. Les titres de secti ## Isolation des cours -Le générateur lit les sources et enrichit leurs copies avec une navigation commune. Il conserve les scripts et styles pédagogiques ; les styles communs utilisent les classes `wiki-*` et le portail `.wiki-shell`. Les fichiers `planning.css` et `planning.js` sont chargés uniquement sur la page de frise. Chaque cours reste dans son document, ce qui évite les collisions entre les nombreuses variables, identifiants et canvas des cours. +Le générateur lit les sources et enrichit leurs copies avec une navigation commune : un panneau latéral (marque, recherche, matières, parcours et, sur un cours, son sommaire), placé en fin de document et affiché à gauche par la feuille de style. Un court script en tête de `body` applique l'état replié mémorisé avant l'affichage. Sur écran paysage large, `wiki.css` élargit les cours et dispose les manipulations à un seul schéma en deux colonnes ; la largeur du schéma ne dépend que de la fenêtre, si bien que les cours, qui dessinent selon la largeur mesurée, n'ont pas à redessiner quand on replie le panneau. La série Métallurgie garde sa largeur d'origine. Il conserve les scripts et styles pédagogiques ; les styles communs utilisent les classes `wiki-*` et le portail `.wiki-shell`. Les fichiers `planning.css` et `planning.js` sont chargés uniquement sur la page de frise. Chaque cours reste dans son document, ce qui évite les collisions entre les nombreuses variables, identifiants et canvas des cours. L'accueil et les pages matière sont préconstruits. Aucun routeur JavaScript, chargement des cours en iframe ou application monopage n'est nécessaire. Les liens relatifs préservent le fonctionnement sous le nom du dépôt GitHub Pages. @@ -65,7 +65,7 @@ Les sorties sont reconstruites depuis les seules entrées disponibles et leurs f ## Applications interactives -Une application (jeu, simulateur) est du code, pas un support éditorial : ses sources vivent dans `applications//` avec leurs propres tests et leur documentation. Le catalogue la déclare dans la liste `applications` : matière, état, `fichiers` publiés un à un, dossier de `publication`, `cours_lies`. Le générateur copie seulement ces fichiers et enrichit la copie de la page d’entrée avec la barre du wiki. Les routes internes par ancre (`#laboratoire`, `#histoire/3`) sont reconnues grâce à la balise `wiki-application` et ne passent pas au contrôle des sections. +Une application (jeu, simulateur) est du code, pas un support éditorial : ses sources vivent dans `applications//` avec leurs propres tests et leur documentation. Le catalogue la déclare dans la liste `applications` : matière, état, `fichiers` publiés un à un, dossier de `publication`, `cours_lies`. Le générateur copie seulement ces fichiers et enrichit la copie de la page d’entrée avec le panneau du wiki, replié au départ. Les routes internes par ancre (`#laboratoire`, `#histoire/3`) sont reconnues grâce à la balise `wiki-application` et ne passent pas au contrôle des sections. Place volontairement secondaire : section « S’entraîner » de la matière, lien en pied des cours liés, entrée de recherche ; l’accueil ne montre que le nombre d’applications dans la carte de la matière. Aucune matière n’est codée en dur. La routine de maintenance construit ses sites candidats avec le code déclaré des applications, mais `publier` ne l’embarque jamais. Le premier cas est Mohr Forge, publié à `RDM/mohr-forge/` ; son ancien dépôt autonome est archivé localement dans `.sauvegardes/`. diff --git a/docs/STORIES.md b/docs/STORIES.md index 2c37d15..0d80c3c 100644 --- a/docs/STORIES.md +++ b/docs/STORIES.md @@ -23,6 +23,7 @@ Petites tranches livrables. Une story est terminée après ses critères d'accep | S17 | Intégrer la série Mécanique de la rupture | Intégré et vérifié | | S18 | Intégrer le fluage et le brasage, republier les corrections de rupture | Intégré et vérifié | | S19 | Compléter la série brasage avec les cours 3 et 4 | Intégré et vérifié | +| S21 | Naviguer avec un panneau latéral repliable et lire les cours en paysage | Intégré et vérifié | ## S01 — Catalogue extensible @@ -306,3 +307,20 @@ Validation locale : 87 tests Python et 23 parcours Chromium réussis ; catalogue Contrôle des quatre cours réels sous `/PolyWE/` : 24 manipulations et leurs animations, 48 questions de quiz et remise à zéro, cases mémorisées après rechargement, persistance du thème, partage de section via l’API native simulée, recherche sans accents vers le chapitre de soufflage magnétique, liens de la matière et du planning. Vérification des modes clair et sombre aux largeurs 360, 390, 768 et 1280 px. Les 2 880 états contrôlés (initiaux, extrêmes individuels et simultanés des curseurs, boutons de préréglage) ne présentent aucun débordement horizontal de page ni aucun texte SVG hors viewBox avec une tolérance de 2 unités. Aucune erreur JavaScript ou HTTP locale. Captures des 24 manipulations et des quatre en-têtes mobiles sombres inspectées. Empreintes SHA-256 des quatre sources inchangées depuis la réception. La mise à jour existante de `AGENTS.md` est versionnée dans un commit de documentation distinct, conformément à la demande de synchroniser toutes les modifications sur `main`. Le cinquième cours annoncé reste à livrer ; aucune nouvelle validation scientifique n’est revendiquée. + +## S21 — Panneau latéral et lecture paysage + +En tant qu’élève sur ordinateur portable, je navigue depuis un panneau latéral plutôt que depuis un bandeau en haut de page, et je vois une manipulation entière sur un seul écran ; sur téléphone, la navigation reste à portée de pouce sans gêner la lecture. + +- Toutes les pages publiées (accueil, matières, parcours, planning, cours, documents, applications) remplacent le bandeau supérieur par un panneau latéral gauche : marque, recherche, accueil, planning, matières dépliables avec leurs cours disponibles, parcours. Les brouillons, les cours à venir et les applications n’y figurent pas ; les applications restent hors de l’accueil. +- Sur un cours : matière, titre, sommaire du cours avec la section en cours de lecture surlignée, cours précédent et suivant, partage et copie du lien. Les boutons « Partager cette section » restent dans le contenu. +- Sur un écran d’au moins 1 000 px, le panneau est ouvert par défaut et se replie en rail d’icônes ; l’état est mémorisé dans le navigateur (`wiki-panneau`). Les applications démarrent avec le panneau replié. En dessous, il devient un tiroir fermé par défaut, ouvert par un bouton « Menu » et refermé par Échap, le fond, le bouton de fermeture ou un lien suivi. +- Sur un écran paysage assez large, les cours utilisent la largeur disponible : contenu élargi, manipulations à un seul schéma affichées côte à côte (schéma à gauche, réglages et lectures à droite), quiz et check-list sur deux colonnes. Le sommaire horizontal du cours laisse la place à celui du panneau quand le panneau est ouvert. En portrait et sur téléphone, la présentation des sources est conservée. +- Thème clair ou sombre suivi par le panneau et les pages du wiki ; aucun débordement horizontal à 360, 390, 768, 1280 et 1440 px ; liens valides à la racine et sous un sous-chemin ; sources des cours intactes, scripts et ancres préservés. +- Tests de parcours RED puis GREEN ; vérification au navigateur des cours réels. + +Preuve TDD du 7 octobre 2026 : RED de `uv run --group browser pytest tests/browser/test_navigation_panel.py` (10 échecs : ni panneau, ni bouton « Menu », bandeau encore présent, manipulation empilée). GREEN après implémentation : 10 réussis, à la racine et sous `/promo/`. Les parcours existants qui utilisaient le bandeau ouvrent d’abord le menu sur téléphone ; le parcours paysage a été ajusté au choix final (lectures et commentaire sous le schéma). Suites complètes : 86 tests Python, 33 parcours Chromium et 34 tests navigateur des applications. + +Mesure sur les 153 manipulations des cours réels, nombre plus hautes que la fenêtre avant → après : 86 → 18 à 1280×800, 98 → 13 à 1366×768, 38 → 2 à 1440×900, 1 → 1 à 1920×1080 (hauteur médiane : 1,09 → 0,83 écran à 1366×768). Les dessins mesurés par les cours restent à l’échelle 1 après regroupement. Balayage des 43 pages publiées à 360, 390, 768, 1280 et 1440 px, en clair et en sombre : aucun débordement horizontal ni erreur JavaScript. Captures du panneau ouvert, du rail, du tiroir sur téléphone, de l’accueil, d’une matière, du planning et du jeu inspectées. + +Limites : la série Métallurgie garde sa largeur d’origine, ses figures s’allongeant avec la largeur ; les manipulations à deux schémas ou à matrices restent empilées ; quelques manipulations très riches en réglages dépassent encore un écran de 768 px de haut. diff --git a/tests/browser/test_applications.py b/tests/browser/test_applications.py index 93f32e1..f2e3722 100644 --- a/tests/browser/test_applications.py +++ b/tests/browser/test_applications.py @@ -50,7 +50,8 @@ def test_application_opens_from_its_subject_and_returns_to_the_wiki(app_site): expect(page.locator("#ecran")).to_have_text("Prêt à jouer") page.get_by_role("link", name="Jouer", exact=True).click() expect(page.locator("#ecran")).to_have_text("Partie en cours") - page.get_by_role("navigation", name="Navigation du wiki").get_by_role("link", name="Thermique").click() + page.get_by_role("button", name="Menu", exact=True).click() + page.get_by_role("navigation", name="Navigation du wiki").get_by_role("link", name="Thermique", exact=True).click() expect(page.get_by_role("heading", level=1)).to_have_text("Thermique") page.goto(base + "index.html") page.get_by_label("Rechercher une notion, un cours ou une section").fill("quiz") @@ -76,7 +77,8 @@ def test_real_game_is_playable_under_the_published_subpath(tmp_path): expect(page.locator("#element-3d")).to_be_visible() assert page.locator("#element-3d").evaluate(PAINTED) > 300 assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") - page.get_by_role("navigation", name="Navigation du wiki").get_by_role("link", name="Résistance des matériaux").click() + page.get_by_role("button", name="Menu", exact=True).click() + page.get_by_role("navigation", name="Navigation du wiki").get_by_role("link", name="Résistance des matériaux", exact=True).click() expect(page.get_by_role("heading", level=1)).to_have_text("Résistance des matériaux") browser.close() finally: diff --git a/tests/browser/test_documents.py b/tests/browser/test_documents.py index 8bfd9cb..ea2c405 100644 --- a/tests/browser/test_documents.py +++ b/tests/browser/test_documents.py @@ -30,6 +30,7 @@ def test_document_from_planning_download_and_share(site, tmp_path): target = tmp_path / "download.pdf" download.save_as(target) assert target.read_bytes() == b"%PDF-1.4\nSupport fictif" + page.get_by_role("button", name="Menu", exact=True).click() page.get_by_role("button", name="Copier le lien", exact=True).click() expect(page.locator("#wiki-share-status")).to_have_text("Lien copié.") assert page.evaluate("window.copied") == base + "Supports/th-fiche.html" diff --git a/tests/browser/test_journeys.py b/tests/browser/test_journeys.py index 8756c19..13693ed 100644 --- a/tests/browser/test_journeys.py +++ b/tests/browser/test_journeys.py @@ -45,6 +45,7 @@ def test_find_unaccented_notion_and_open_exact_section(site): result.click() expect(page).to_have_url(base+"Thermique/introduction.html#intro") expect(page.get_by_text("Animation active", exact=True)).to_be_visible() + page.get_by_role("button", name="Menu", exact=True).click() page.get_by_role("link", name="Thermique", exact=True).click() expect(page).to_have_url(base+"matieres/thermique.html") assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") @@ -55,6 +56,7 @@ def test_copy_and_share_include_current_section(site): page.add_init_script("""Object.defineProperty(navigator, 'clipboard', {value: {writeText: async text => {window.copied = text;}}}); Object.defineProperty(navigator, 'share', {value: async data => {window.shared = data;}});""") page.goto(base+"Thermique/introduction.html#calcul") + page.get_by_role("button", name="Menu", exact=True).click() page.get_by_role("button", name="Copier le lien", exact=True).click() expect(page.locator("#wiki-share-status")).to_have_text("Lien copié.") assert page.evaluate("window.copied") == base+"Thermique/introduction.html#calcul" @@ -81,6 +83,7 @@ def test_copy_fallback_without_clipboard(site): page, base = site page.add_init_script("Object.defineProperty(navigator, 'clipboard', {value: undefined});") page.goto(base+"Thermique/introduction.html#intro") + page.get_by_role("button", name="Menu", exact=True).click() page.get_by_role("button", name="Copier le lien", exact=True).click() expect(page.get_by_role("textbox", name="Lien à copier", exact=True)).to_have_value(base+"Thermique/introduction.html#intro") diff --git a/tests/browser/test_navigation_panel.py b/tests/browser/test_navigation_panel.py new file mode 100644 index 0000000..b2655fb --- /dev/null +++ b/tests/browser/test_navigation_panel.py @@ -0,0 +1,125 @@ +"""Panneau latéral repliable et lecture paysage des cours (S21).""" +import pytest +from playwright.sync_api import expect + +from test_journeys import site + +LAB = """

Manipulation 1 : refroidir un cordon

+

Règle l’énergie de soudage et observe la courbe de refroidissement.

+
+
1,0 kJ/mm
+ +
t8/5 = 6 s

Refroidissement modéré.

""" + +DESKTOP = {"width": 1366, "height": 768} + + +@pytest.fixture +def project(project): + """Un cours assez long pour défiler, avec une manipulation typique des séries.""" + root, data = project + source = root / data["cours"][0]["fichier"] + html = source.read_text(encoding="utf-8") + html = html.replace("

Une explication.

", '

Une explication.

') + html = html.replace('

', '

' + LAB + '
') + source.write_text(html, encoding="utf-8") + return root, data + + +def panel(page): + return page.get_by_role("navigation", name="Navigation du wiki") + + +def test_desktop_panel_replaces_the_top_banner_and_stays_folded(site): + page, base = site + page.set_viewport_size(DESKTOP) + page.goto(base + "Thermique/introduction.html") + nav = panel(page) + expect(nav).to_be_visible() + expect(nav.get_by_role("link", name="Thermique", exact=True)).to_be_visible() + expect(nav.get_by_role("link", name="Le t8/5 en pratique", exact=True)).to_be_visible() + box = page.locator("#wiki-panel").bounding_box() + assert box["x"] == 0 and box["height"] >= DESKTOP["height"] - 1 + heading = page.get_by_role("heading", name="Préchauffage", level=1).bounding_box() + assert heading["x"] >= box["width"], "le contenu commence à droite du panneau" + assert heading["y"] < 120, "aucun bandeau ne repousse le contenu vers le bas" + page.get_by_role("button", name="Replier le menu").click() + expect(nav.get_by_role("link", name="Le t8/5 en pratique", exact=True)).to_be_hidden() + assert page.locator("#wiki-panel").bounding_box()["width"] <= 72 + page.reload() + expect(nav.get_by_role("link", name="Le t8/5 en pratique", exact=True)).to_be_hidden() + page.get_by_role("button", name="Déplier le menu").click() + expect(nav.get_by_role("link", name="Le t8/5 en pratique", exact=True)).to_be_visible() + page.reload() + expect(nav.get_by_role("link", name="Le t8/5 en pratique", exact=True)).to_be_visible() + assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") + + +def test_panel_highlights_the_section_being_read(site): + page, base = site + page.set_viewport_size(DESKTOP) + page.goto(base + "Thermique/introduction.html") + nav = panel(page) + expect(nav.get_by_role("link", name="Comprendre le préchauffage", exact=True)).to_have_attribute("aria-current", "location") + nav.get_by_role("link", name="Le t8/5 en pratique", exact=True).click() + expect(page).to_have_url(base + "Thermique/introduction.html#calcul") + expect(nav.get_by_role("link", name="Le t8/5 en pratique", exact=True)).to_have_attribute("aria-current", "location") + expect(nav.get_by_role("link", name="Comprendre le préchauffage", exact=True)).not_to_have_attribute("aria-current", "location") + + +def test_phone_drawer_opens_closes_and_follows_links(site): + page, base = site + page.goto(base + "Thermique/introduction.html") + nav = panel(page) + expect(nav).to_be_hidden() + menu = page.get_by_role("button", name="Menu", exact=True) + menu.click() + expect(nav).to_be_visible() + page.keyboard.press("Escape") + expect(nav).to_be_hidden() + expect(menu).to_be_focused() + menu.click() + page.get_by_role("button", name="Fermer le menu").click() + expect(nav).to_be_hidden() + menu.click() + nav.get_by_role("link", name="Le t8/5 en pratique", exact=True).click() + expect(page).to_have_url(base + "Thermique/introduction.html#calcul") + expect(nav).to_be_hidden() + assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") + + +def test_subject_pages_open_courses_from_the_panel(site): + page, base = site + page.set_viewport_size(DESKTOP) + page.goto(base) + nav = panel(page) + expect(nav.get_by_role("link", name="Accueil", exact=True)).to_have_attribute("aria-current", "page") + nav.get_by_text("Thermique", exact=True).click() + nav.get_by_role("link", name="Préchauffage", exact=True).click() + expect(page).to_have_url(base + "Thermique/introduction.html") + search = page.locator("#wiki-panel").get_by_role("searchbox") + search.fill("HAZ") + search.press("Enter") + expect(page.locator("#wiki-results li").first).to_be_visible() + expect(page.get_by_label("Rechercher une notion, un cours ou une section")).to_have_value("HAZ") + + +def test_landscape_course_shows_a_manipulation_side_by_side(site): + page, base = site + page.set_viewport_size(DESKTOP) + page.goto(base + "Thermique/introduction.html#m1") + drawing = page.locator("#m1svg").bounding_box() + controls = page.locator("#m1 .ctrl").bounding_box() + status = page.locator("#m1 .status").bounding_box() + assert drawing["x"] + drawing["width"] <= controls["x"] + 1, "schéma à gauche, réglages à droite" + assert controls["y"] < drawing["y"] + drawing["height"], "réglages à côté du schéma" + assert status["y"] >= drawing["y"] + drawing["height"] - 1 and status["x"] < controls["x"], "commentaire sous le schéma" + lab = page.locator("#m1").bounding_box() + assert lab["height"] <= DESKTOP["height"], "la manipulation tient sur un écran" + assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") + page.set_viewport_size({"width": 390, "height": 844}) + page.reload() + drawing = page.locator("#m1svg").bounding_box() + controls = page.locator("#m1 .ctrl").bounding_box() + assert drawing["y"] >= controls["y"] + controls["height"] - 1, "sur téléphone, la présentation d’origine est conservée" + assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") diff --git a/wiki/build.py b/wiki/build.py index 0d26720..7951b5b 100644 --- a/wiki/build.py +++ b/wiki/build.py @@ -25,22 +25,127 @@ def write(output, name, text): target.write_text(text, encoding="utf-8") -def shell(current, title, body, has_planning=False, extra_head=""): - planning_nav = f'Planning' if has_planning else "" +# Pictogrammes du panneau : traits simples qui suivent la couleur du texte. +ICONS = { + "home": '', + "calendar": '', + "search": '', + "subjects": '', + "routes": '', + "fold": '', + "close": '', + "menu": '', +} + + +def icon(name): + return (f'') + + +def short_title(course, subject): + """Sous la matière « Brasage… », le panneau affiche « Procédés… » plutôt que « Brasage : procédés… ».""" + head, sep, rest = course["titre"].partition(" : ") + if sep and rest and head.casefold() in subject["titre"].casefold(): + return rest[:1].upper() + rest[1:] + return course["titre"] + + +def panel_start(folded=False): + """Juste après : l’état du panneau est appliqué avant l’affichage et avant les scripts des cours.""" + state = 'd.classList.add("wiki-replie")' if folded else 'if(localStorage.getItem("wiki-panneau")==="replie")d.classList.add("wiki-replie")' + return ('Aller au menu du wiki') + + +def panel(current, site, here=None): + """Panneau latéral commun à toutes les pages : marque, recherche, page courante, matières et parcours. + + Il est placé en fin de document : le contenu vient d’abord au clavier et pour les lecteurs d’écran, + la feuille de style l’affiche à gauche, ouvert, replié en rail ou en tiroir selon la largeur.""" + here = here or {} + + def mark(target): + return ' aria-current="page"' if target == current else "" + + home = link(current, "index.html") + quick = [(home, "home", "Accueil", mark("index.html"), "")] + if site["planning"]: + quick.append((link(current, "planning.html"), "calendar", "Planning", mark("planning.html"), "")) + quick += [(link(current, "index.html", "rechercher"), "search", "Rechercher", "", " wiki-rail-only"), + (link(current, "index.html", "matieres"), "subjects", "Matières", "", " wiki-rail-only")] + if site["routes"]: + quick.append((link(current, "index.html", "parcours"), "routes", "Parcours", "", " wiki-rail-only")) + shortcuts = "".join(f'
  • {icon(name)}{label}
  • ' + for href, name, label, attr, extra in quick) + + context = "" + if here.get("subject"): + subject = here["subject"] + context = f'
    {h(subject["titre"])}' + context += f'

    {h(here["repere"])}{h(here["titre"])}

    ' + if here.get("sections"): + items = [] + for section in here["sections"]: + number = re.fullmatch(r"c(\d+)", section["ancre"]) + num = f'' if number else "" + items.append(f'
  • {num}{h(section["titre"])}
  • ') + context += f'

    Sommaire

      {"".join(items)}
    ' + pager = [] + for key, rel, label, arrow in (("previous", "prev", "Précédent", "←"), ("next", "next", "Suivant", "→")): + other = here.get(key) + if other: + text = f' {label}' if rel == "prev" else f'{label} ' + pager.append(f'{text} : {h(other["titre"])}') + if pager: + context += f'

    {"".join(pager)}

    ' + context += "
    " + + groups = [] + for subject, courses in site["subjects"]: + overview = "matieres/" + subject["id"] + ".html" + is_here = here.get("subject", {}).get("id") == subject["id"] or current == overview + entries = [f'
  • Vue d’ensemble : {h(subject["titre"])}
  • '] + for course in courses: + entries.append(f'
  • {h(short_title(course, subject))}
  • ') + groups.append(f'
  • {h(subject["titre"])}
      {"".join(entries)}
  • ') + routes = "" + if site["routes"]: + on_route = current.startswith("parcours/") + items = "".join(f'
  • {h(route["titre"])}
  • ' for route in site["routes"]) + routes = (f'

    Parcours

    • Faire le lien avec le soudage' + f'
        {items}
    ') + + return f""" + +""" + + +def shell(current, title, body, site, extra_head=""): css = link(current, "assets/wiki.css") js = link(current, "assets/wiki.js") - home = link(current, "index.html") return f""" {h(title)} · Révisions IWE {extra_head} -Aller au contenu -
    Révisions IWE -
    +{panel_start()}
    {body}
    -
    Supports de révision de la promo · DU Ingénierie du soudage · Polytech Nantes
    Un complément aux cours et aux TD de la formation.
    """ +
    Supports de révision de la promo · DU Ingénierie du soudage · Polytech Nantes
    Un complément aux cours et aux TD de la formation.
    +{panel(current, site)}""" def course_card(course, current): @@ -63,17 +168,15 @@ def app_card(app, current): return f'
    {genre}

    {title}

    {description}{h(tags)}
    ' -def app_page(app, subject): - """Copie publiée d'une application : barre du wiki ajoutée, source et routes internes intactes.""" +def app_page(app, subject, site): + """Copie publiée d'une application : panneau du wiki ajouté (replié au départ), source et routes internes intactes.""" current = app["url"] head = (f'' f'') - nav = f"""""" + here = {"subject": subject, "repere": app.get("genre", "Application interactive"), "titre": app["titre"]} html = re.sub(r"", lambda _: head + '\n', app["_source"], count=1, flags=re.I) - return re.sub(r"(]*>)", lambda m: m.group(1) + '\n' + nav, html, count=1, flags=re.I) + html = re.sub(r"(]*>)", lambda m: m.group(1) + '\n' + panel_start(folded=True), html, count=1, flags=re.I) + return re.sub(r"", lambda _: panel(current, site, here) + '\n', html, count=1, flags=re.I) def validate_site(output): @@ -137,6 +240,9 @@ def build(root): live_apps = [a for a in apps if a["statut"] == "disponible"] subjects = [s for s in data["matieres"] if any(item["matiere"] == s["id"] for item in [*courses, *apps])] subject_by_id = {s["id"]: s for s in subjects} + # Le panneau latéral liste les cours disponibles ; brouillons, cours à venir et applications n’y figurent pas. + site = {"planning": bool(plan), "routes": data["parcours"], + "subjects": [(s, [c for c in available if c["matiere"] == s["id"]]) for s in subjects]} subject_cards = [] for subject in subjects: grouped = [c for c in courses if c["matiere"] == subject["id"]] @@ -146,7 +252,7 @@ def build(root): subject_apps = [a for a in apps if a["matiere"] == subject["id"]] training = f'

    S’entraîner

    Jeux et applications interactives
    {"".join(app_card(a, current) for a in subject_apps)}
    ' if subject_apps else "" body = f'

    Matière · {count} cours disponibles

    {h(subject["titre"])}

    {h(subject.get("description", ""))}

    {cards}
    {training}' - write(output, current, shell(current, subject["titre"], body, has_planning=bool(plan))) + write(output, current, shell(current, subject["titre"], body, site)) playable = sum(a["statut"] == "disponible" for a in subject_apps) mention = f" · {playable} application{'s' if playable > 1 else ''}" if playable else "" subject_cards.append(f'{count} cours disponibles{mention}

    {h(subject["titre"])}

    {h(subject.get("description", ""))}

    ') @@ -160,7 +266,7 @@ def build(root): href = link(current, course["url"], step["ancre"]) steps.append(f'
  • {h(label)}

    {h(subject_by_id[course["matiere"]]["titre"])} · {h(course.get("repere", ""))}

  • ') body = f'

    Applications au soudage · {len(steps)} étapes

    {h(route["titre"])}

    {h(route.get("description", ""))}

      {"".join(steps)}
    ' - write(output, current, shell(current, route["titre"], body, has_planning=bool(plan))) + write(output, current, shell(current, route["titre"], body, site)) route_cards.append(f'') options = ''.join(f'' for s in subjects) planning_teaser = 'Suivre le fil de la formationPré-rentrée, semaines de cours et examens : les supports au bon moment.' if plan else "" @@ -175,7 +281,7 @@ def build(root):

    Explorez les matières ci-dessous ou recherchez un sujet précis.

      Avancer par matière

      Les bases, dans l’ordre
      {''.join(subject_cards)}

      Faire le lien avec le soudage

      Des parcours entre les cours
      {''.join(route_cards)}
      """ - write(output, "index.html", shell("index.html", "Accueil", body, has_planning=bool(plan))) + write(output, "index.html", shell("index.html", "Accueil", body, site)) if plan: for source in plan['sources']: if not source.get('publier', False): @@ -184,7 +290,7 @@ def build(root): target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(root / source['fichier'], target) extra = '' - write(output, 'planning.html', shell('planning.html', 'Planning de formation', render_planning(plan, data, link), has_planning=True, extra_head=extra)) + write(output, 'planning.html', shell('planning.html', 'Planning de formation', render_planning(plan, data, link), site, extra_head=extra)) search_rows = [] for course in available: current = course["url"] @@ -201,15 +307,9 @@ def build(root): prerequisite = by_id.get(ident) if prerequisite and prerequisite["statut"] == "disponible": prerequisites.append(f'{h(prerequisite["titre"])}') - home = link(current, "index.html") - subject_url = link(current, f"matieres/{subject['id']}.html") - section_links = ''.join(f'
    • {h(s["titre"])}
    • ' for s in course["sections"]) - planning_link = f'Planning' if plan else "" - nav = f"""""" + here = {"subject": subject, "repere": course.get("repere", "Cours"), "titre": course["titre"], "sections": course["sections"], + "previous": siblings[position-1] if position > 0 else None, + "next": siblings[position+1] if position+1 < len(siblings) else None} footer = '