style(mobile): widen reading in landscape on small screens (css-only)
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 36s

This commit is contained in:
2026-03-04 17:36:56 +01:00
parent 63d0ffc5fc
commit 81baadd57f
7 changed files with 1029 additions and 283 deletions

View File

@@ -25,12 +25,12 @@
{/* ✅ actions références en haut (niveau 2 uniquement) */}
<div class="panel-top-actions level-2" aria-label="Actions références">
<div class="panel-actions">
<div class="panel-actions">
<button class="panel-btn panel-btn--primary" id="panel-ref-submit" type="button">
Soumettre une référence (Gitea)
Soumettre une référence (Gitea)
</button>
</div>
<div class="panel-msg" id="panel-ref-msg" hidden></div>
</div>
<div class="panel-msg" id="panel-ref-msg" hidden></div>
</div>
<section class="panel-block level-2" aria-label="Références et auteurs">
@@ -60,7 +60,7 @@
</section>
</div>
{/* ✅ Lightbox media (pop-up au-dessus du panel) */}
{/* ✅ Lightbox media (plein écran) */}
<div class="panel-lightbox" id="panel-lightbox" hidden aria-hidden="true">
<div class="panel-lightbox__overlay" data-close="1"></div>
<div class="panel-lightbox__dialog" role="dialog" aria-modal="true" aria-label="Aperçu du média">
@@ -93,6 +93,9 @@
const btnMediaSubmit = root.querySelector("#panel-media-submit");
const msgMedia = root.querySelector("#panel-media-msg");
const btnRefSubmit = root.querySelector("#panel-ref-submit");
const msgRef = root.querySelector("#panel-ref-msg");
const taComment = root.querySelector("#panel-comment-text");
const btnSend = root.querySelector("#panel-comment-send");
const msgComment = root.querySelector("#panel-comment-msg");
@@ -101,9 +104,6 @@
const lbContent = root.querySelector("#panel-lightbox-content");
const lbCaption = root.querySelector("#panel-lightbox-caption");
const btnRefSubmit = root.querySelector("#panel-ref-submit");
const msgRef = root.querySelector("#panel-ref-msg");
const docTitle = document.body?.dataset?.docTitle || document.title || "Archicratie";
const docVersion = document.body?.dataset?.docVersion || "";
@@ -114,6 +114,16 @@
let currentParaId = "";
let mediaShowAll = (localStorage.getItem("archicratie:panel:mediaAll") === "1");
// ===== cosmetics: micro flash “update” =====
let _flashT = 0;
function flashUpdate(){
try {
root.classList.add("is-updating");
if (_flashT) clearTimeout(_flashT);
_flashT = setTimeout(() => root.classList.remove("is-updating"), 180);
} catch {}
}
// ===== globals =====
function getG() {
return window.__archiGitea || { ready: false, base: "", owner: "", repo: "" };
@@ -121,9 +131,6 @@
function getAuthInfoP() {
return window.__archiAuthInfoP || Promise.resolve({ ok: false, groups: [] });
}
function isDev() {
return Boolean((window.__archiFlags && window.__archiFlags.dev) || /^(localhost|127\.0\.0\.1|\[::1\])$/i.test(location.hostname));
}
const access = { ready: false, canUsers: false };
@@ -137,8 +144,7 @@
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
}
// ✅ règle mission : readers + editors peuvent soumettre médias + commentaires
// ✅ dev fallback : si /_auth/whoami nexiste pas, on autorise pour tester
// ✅ readers + editors peuvent soumettre médias + commentaires + refs
getAuthInfoP().then((info) => {
const groups = Array.isArray(info?.groups) ? info.groups : [];
const canReaders = inGroup(groups, "readers");
@@ -152,7 +158,6 @@
if (btnSend) btnSend.disabled = !access.canUsers;
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
// si pas d'accès, on informe (soft)
if (!access.canUsers) {
if (msgHead) {
msgHead.hidden = false;
@@ -161,7 +166,6 @@
}
}
}).catch(() => {
// fallback dev (cohérent: media + ref + comment)
access.ready = true;
if (Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped)) {
access.canUsers = true;
@@ -213,7 +217,6 @@
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" });
if (res && res.ok) return await res.json();
} catch {}
// ✅ antifragile: ne pas “cacher” un échec pour toujours (dev/HMR/boot race)
_idxP = null;
return null;
})();
@@ -255,24 +258,22 @@
return issue.toString();
}
// Ouvre un nouvel onglet UNE SEULE FOIS (évite le double-open Safari/Firefox + noopener).
function openNewTab(url) {
try {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
return true; // on ne peut pas détecter proprement un blocage sans retomber dans le double-open
} catch {
return false;
}
function openNewTab(url) {
try {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
return true;
} catch {
return false;
}
}
// ====== GARDES ANTI-DOUBLONS ======
const _openStamp = new Map();
function openOnce(key, fn) {
const now = Date.now();
@@ -301,13 +302,21 @@
}
// ===== Lightbox =====
function lockScroll(on) {
try {
document.documentElement.classList.toggle("archi-lb-open", !!on);
} catch {}
}
function closeLightbox() {
if (!lb) return;
lb.hidden = true;
lb.setAttribute("aria-hidden", "true");
if (lbContent) clear(lbContent);
if (lbCaption) { lbCaption.hidden = true; lbCaption.textContent = ""; }
lockScroll(false);
}
function openLightbox({ type, src, caption }) {
if (!lb || !lbContent) return;
clear(lbContent);
@@ -346,6 +355,7 @@
else { lbCaption.hidden = true; lbCaption.textContent = ""; }
}
lockScroll(true);
lb.hidden = false;
lb.setAttribute("aria-hidden", "false");
}
@@ -363,7 +373,6 @@
});
}
// ===== Renders =====
function renderLevel2(data) {
clear(elL2);
if (!elL2) return;
@@ -563,13 +572,16 @@
async function updatePanel(paraId) {
currentParaId = paraId || currentParaId || "";
if (elId) elId.textContent = currentParaId || "—";
flashUpdate();
hideMsg(msgHead);
hideMsg(msgMedia);
hideMsg(msgComment);
hideMsg(msgRef);
const idx = await loadIndex();
// ✅ message soft si lindex est indisponible (sans écraser le message dauth)
if (!idx && msgHead && msgHead.hidden) {
msgHead.hidden = false;
msgHead.textContent = "Index annotations indisponible (annotations-index.json).";
@@ -583,7 +595,6 @@
renderLevel4(data);
}
// ===== media "voir tous" =====
if (btnMediaAll) {
bindClickOnce(btnMediaAll, (ev) => {
ev.preventDefault();
@@ -595,7 +606,6 @@
btnMediaAll.textContent = mediaShowAll ? "Réduire la liste" : "Voir tous les éléments";
}
// ===== media submit (readers + editors) =====
if (btnMediaSubmit) {
bindClickOnce(btnMediaSubmit, (ev) => {
ev.preventDefault();
@@ -638,27 +648,26 @@
});
}
// ===== référence submit (readers + editors) =====
if (btnRefSubmit) {
if (btnRefSubmit) {
bindClickOnce(btnRefSubmit, (ev) => {
ev.preventDefault();
hideMsg(msgRef);
ev.preventDefault();
hideMsg(msgRef);
if (guardEventOnce(ev, "gitea_open_ref")) return;
if (guardEventOnce(ev, "gitea_open_ref")) return;
if (!currentParaId) return showMsg(msgRef, "Choisis dabord un paragraphe (scroll / survol).", "warn");
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
if (!currentParaId) return showMsg(msgRef, "Choisis dabord un paragraphe (scroll / survol).", "warn");
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
const pageUrl = new URL(location.href);
pageUrl.search = "";
pageUrl.hash = currentParaId;
const pageUrl = new URL(location.href);
pageUrl.search = "";
pageUrl.hash = currentParaId;
const paraTxt = getParaText(currentParaId);
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
const paraTxt = getParaText(currentParaId);
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
const title = `[Reference] ${currentParaId} — ${docTitle}`;
const body = [
const title = `[Reference] ${currentParaId} — ${docTitle}`;
const body = [
`Chemin: ${location.pathname}`,
`URL: ${pageUrl.toString()}`,
`Ancre: #${currentParaId}`,
@@ -676,18 +685,16 @@
``,
`---`,
`Note: issue générée depuis le site (pré-remplissage).`,
].join("\n");
].join("\n");
const url = buildIssueURL({ title, body });
if (!url) return showMsg(msgRef, "Impossible de générer lissue.", "error");
const url = buildIssueURL({ title, body });
if (!url) return showMsg(msgRef, "Impossible de générer lissue.", "error");
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
if (!ok) showMsg(msgRef, "Si rien ne souvre : autorise les popups pour ce site.", "error");
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
if (!ok) showMsg(msgRef, "Si rien ne souvre : autorise les popups pour ce site.", "error");
});
}
}
// ===== commentaire (readers + editors) =====
if (btnSend) {
bindClickOnce(btnSend, (ev) => {
ev.preventDefault();
@@ -739,60 +746,31 @@
});
}
// ===== wiring: para courant (aligné sur le paragraphe sous le reading-follow) =====
function isPara(el) {
return Boolean(el && el.nodeType === 1 && el.matches && el.matches('.reading p[id^="p-"]'));
// ===== wiring: para courant (SOURCE OF TRUTH = EditionLayout) =====
function onCurrentPara(ev) {
try {
const id = ev?.detail?.id ? String(ev.detail.id) : "";
if (!id || !/^p-\d+-/i.test(id)) return;
if (id === currentParaId) return;
updatePanel(id);
} catch {}
}
window.addEventListener("archicratie:currentPara", onCurrentPara);
function pickParaAtY(y) {
const x = Math.max(0, Math.round(window.innerWidth * 0.5));
const candidates = [
document.elementFromPoint(x, y),
document.elementFromPoint(Math.min(window.innerWidth - 1, x + 60), y),
document.elementFromPoint(Math.max(0, x - 60), y),
].filter(Boolean);
const initial = String(location.hash || "").replace(/^#/, "").trim();
for (const c of candidates) {
if (isPara(c)) return c;
const p = c.closest ? c.closest('.reading p[id^="p-"]') : null;
if (isPara(p)) return p;
}
return null;
if (/^p-\d+-/i.test(initial)) {
updatePanel(initial);
} else if (window.__archiCurrentParaId && /^p-\d+-/i.test(String(window.__archiCurrentParaId))) {
updatePanel(String(window.__archiCurrentParaId));
} else {
setTimeout(() => {
try {
const id = String(window.__archiCurrentParaId || "").trim();
if (/^p-\d+-/i.test(id)) updatePanel(id);
} catch {}
}, 0);
}
let _lastPicked = "";
function syncFromFollowLine() {
const off = Number(document.documentElement.style.getPropertyValue("--sticky-offset-px")) || 0;
const y = Math.round(off + 8);
const p = pickParaAtY(y);
if (!p || !p.id) return;
if (p.id === _lastPicked) return;
_lastPicked = p.id;
// met à jour l'app global (EditionLayout écoute déjà currentPara)
try { window.dispatchEvent(new CustomEvent("archicratie:currentPara", { detail: { id: p.id } })); } catch {}
// et met à jour le panel immédiatement (sans attendre)
updatePanel(p.id);
}
let ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
syncFromFollowLine();
});
}
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
// Initial: hash > sinon calc
const initial = String(location.hash || "").replace(/^#/, "");
if (/^p-\d+-/i.test(initial)) updatePanel(initial);
else setTimeout(() => { try { syncFromFollowLine(); } catch {} }, 0);
})();
</script>
@@ -805,6 +783,8 @@
position: sticky;
top: calc(var(--sticky-header-h) + var(--page-gap));
align-self: start;
--thumb: 92px; /* ✅ taille des vignettes (80110 selon goût) */
}
:global(body[data-reading-level="3"]) .page-panel{
@@ -922,28 +902,33 @@
/* actions médias en haut */
.panel-top-actions{ margin-top: 8px; }
/* ===== media thumbnails (150x150) ===== */
/* ===== media thumbnails (plus petits + plus denses) ===== */
.panel-media-grid{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(var(--thumb), 1fr));
gap: 10px;
}
.panel-media-tile{
width: 150px;
max-width: 100%;
width: 100%;
border: 1px solid rgba(127,127,127,.20);
border-radius: 14px;
padding: 8px;
background: rgba(127,127,127,0.04);
cursor: pointer;
text-align: left;
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
}
.panel-media-tile:hover{
transform: translateY(-1px);
background: rgba(127,127,127,0.07);
border-color: rgba(127,127,127,.32);
}
.panel-media-tile img{
width: 150px;
height: 150px;
max-width: 100%;
width: 100%;
height: var(--thumb);
object-fit: cover;
display: block;
border-radius: 10px;
@@ -951,8 +936,8 @@
}
.panel-media-ph{
width: 150px;
height: 150px;
width: 100%;
height: var(--thumb);
border-radius: 10px;
display: grid;
place-items: center;
@@ -995,7 +980,11 @@
resize: vertical;
}
/* ===== Lightbox ===== */
/* ===== Lightbox (plein écran “cinéma”) ===== */
:global(html.archi-lb-open){
overflow: hidden; /* ✅ empêche le scroll derrière */
}
.panel-lightbox{
position: fixed;
inset: 0;
@@ -1005,58 +994,66 @@
.panel-lightbox__overlay{
position: absolute;
inset: 0;
background: rgba(0,0,0,0.80);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
background: rgba(0,0,0,0.84);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.panel-lightbox__dialog{
position: absolute;
right: 24px;
top: calc(var(--sticky-header-h) + 16px);
width: min(520px, calc(100vw - 48px));
max-height: calc(100vh - (var(--sticky-header-h) + 32px));
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(1100px, 92vw);
max-height: 92vh;
overflow: auto;
border: 1px solid rgba(127,127,127,0.22);
border-radius: 16px;
background: rgba(255,255,255,0.10);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 12px;
}
border: 1px solid rgba(255,255,255,0.14);
border-radius: 18px;
@media (prefers-color-scheme: dark){
.panel-lightbox__dialog{
background: rgba(0,0,0,0.28);
}
background: rgba(20,20,20,0.55);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
padding: 16px;
box-shadow: 0 24px 70px rgba(0,0,0,0.55);
}
.panel-lightbox__close{
position: sticky;
top: 0;
margin-left: auto;
position: absolute;
top: 12px;
right: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 30px;
border-radius: 10px;
border: 1px solid rgba(127,127,127,0.35);
background: rgba(127,127,127,0.10);
width: 44px;
height: 40px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.22);
background: rgba(255,255,255,0.10);
cursor: pointer;
font-size: 18px;
font-size: 22px;
font-weight: 900;
}
.panel-lightbox__content{
margin-top: 36px;
}
.panel-lightbox__content img,
.panel-lightbox__content video{
display: block;
width: 100%;
height: auto;
max-width: 1400px;
margin: 0 auto;
border-radius: 12px;
max-height: calc(92vh - 160px);
object-fit: contain;
background: rgba(0,0,0,0.22);
border-radius: 14px;
}
.panel-lightbox__content audio{
@@ -1064,13 +1061,14 @@
}
.panel-lightbox__caption{
margin-top: 10px;
margin-top: 12px;
font-size: 12px;
font-weight: 800;
opacity: .92;
color: rgba(255,255,255,0.92);
}
@media (max-width: 1100px){
.page-panel{ display: none; }
}
</style>
</style>

View File

@@ -5,6 +5,7 @@ import LevelToggle from "../components/LevelToggle.astro";
import BuildStamp from "../components/BuildStamp.astro";
import SidePanel from "../components/SidePanel.astro";
import "../styles/global.css";
import "../styles/panel.css";
const {
title,
@@ -261,17 +262,24 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
padding: var(--page-gap) 16px 48px;
}
/* ✅ Grille robustifiée + panel plus confortable (base) */
.page-shell{
--aside-w: 320px;
--reading-w: 78ch;
--panel-w: 380px;
--panel-w: 420px;
--gap: 18px;
max-width: calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2));
max-width: min(
calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2)),
calc(100vw - 32px)
);
margin: 0 auto;
display: grid;
grid-template-columns: var(--aside-w) minmax(0, var(--reading-w)) var(--panel-w);
grid-template-columns:
var(--aside-w)
minmax(0, var(--reading-w))
minmax(0, var(--panel-w));
column-gap: var(--gap);
align-items: start;
}
@@ -346,23 +354,38 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
}
:global(body[data-reading-level="1"]) .page-panel{ display: none !important; }
/* Niveau 3 : lecture plus étroite + panel large ; TOC masqué */
/* Niveau 3 : lecture plus étroite + panel LARGE ; TOC masqué */
:global(body[data-reading-level="3"]) .page-shell{
--reading-w: 62ch;
--panel-w: 560px;
max-width: calc(var(--reading-w) + var(--panel-w) + (var(--gap) * 1));
grid-template-columns: minmax(0, var(--reading-w)) var(--panel-w);
--reading-w: 60ch;
--panel-w: clamp(720px, 48vw, 940px);
max-width: min(
calc(var(--reading-w) + var(--panel-w) + var(--gap)),
calc(100vw - 32px)
);
grid-template-columns:
minmax(0, var(--reading-w))
minmax(0, var(--panel-w));
}
:global(body[data-reading-level="3"]) .page-aside{ display: none; }
:global(body[data-reading-level="3"]) .reading{ grid-column: 1; }
/* Niveau 4 : TOC à gauche + panel */
/* Niveau 4 : TOC à gauche + panel confortable */
:global(body[data-reading-level="4"]) .page-shell{
--aside-w: 320px;
--reading-w: 64ch;
--panel-w: 520px;
max-width: calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2));
grid-template-columns: var(--aside-w) minmax(0, var(--reading-w)) var(--panel-w);
--panel-w: clamp(520px, 36vw, 720px);
max-width: min(
calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2)),
calc(100vw - 32px)
);
grid-template-columns:
var(--aside-w)
minmax(0, var(--reading-w))
minmax(0, var(--panel-w));
}
:global(body[data-reading-level="4"]) .page-aside{ display: block; }
:global(body[data-reading-level="4"]) .reading{ grid-column: 2; }
@@ -1158,6 +1181,45 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
});
});
/* ✅ NOUVEAU : clic paragraphe => snap sous reading-follow + SidePanel aligné */
safe("click-para-align", () => {
if (!reading) return;
reading.addEventListener("click", (ev) => {
try {
const t = ev.target;
// ne pas casser les boutons / liens / para-tools
if (t && t.closest && t.closest(".para-tools")) return;
const a = t && t.closest ? t.closest("a") : null;
if (a) return;
const p = t && t.closest ? t.closest('.reading p[id^="p-"]') : null;
if (!p || !p.id) return;
// si sélection texte en cours => ne pas snap
const sel = window.getSelection ? window.getSelection() : null;
if (sel && !sel.isCollapsed) return;
openDetailsIfNeeded(p);
// snap immédiat sous followbar
scrollToElWithOffset(p, 12, "auto");
// URL stable
history.replaceState(null, "", `${location.pathname}#${p.id}`);
// force la source de vérité (SidePanel suit via event archicratie:currentPara)
__hoverParaId = "";
__activeParaId = p.id;
setCurrentPara(p.id, "click");
writeLastSeen(p.id);
updateResumeButton();
} catch {}
}, { passive: true });
});
// ===== Hash handling (robuste) =====
function resolveParaIdFromEl(el) {
if (!el) return "";
@@ -1281,8 +1343,6 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
const h1 = reading.querySelector("h1");
// ✅ FIX: ne pas dépendre du tag <span>; accepter .details-anchor quel que soit le tag,
// et utiliser <details> comme marker (top fiable) pour que rf-h2 s'affiche correctement.
const h2Anchors = Array.from(reading.querySelectorAll(".details-anchor[id]"))
.map((s) => {
const d = (s.nextElementSibling && s.nextElementSibling.tagName === "DETAILS")
@@ -1572,4 +1632,4 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
})();
</script>
</body>
</html>
</html>

View File

@@ -27,17 +27,40 @@
--rf-h3-size: 1.25rem;
--rf-h3-lh: 1.25;
--rf-h3-fw: 650;
/* ===== Polish (non destructif) ===== */
--ease-out: cubic-bezier(.2,.9,.2,1);
--ease-soft: cubic-bezier(.2,.8,.2,1);
--focus-ring: 2px solid rgba(140,140,255,0.60);
--focus-ring-offset: 2px;
}
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
line-height: 1.6;
/* polish */
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { text-decoration: none; }
a:hover { text-decoration: underline; }
/* ===== Focus (accessibilité + feel premium) ===== */
:focus { outline: none; }
:focus-visible{
outline: var(--focus-ring);
outline-offset: var(--focus-ring-offset);
}
@media (prefers-reduced-motion: reduce){
*{ transition: none !important; animation: none !important; }
}
/* Header sticky */
header{
position: sticky;
@@ -61,15 +84,26 @@ main { padding: 0; }
.reading {
max-width: 78ch;
margin: 0 auto;
/* polish */
font-size: 16px;
letter-spacing: 0.005em;
}
.reading h1 {
line-height: 1.2;
margin: 0 0 10px;
/* polish */
letter-spacing: -0.01em;
}
.reading p { margin: 0 0 12px; }
/* petits ajustements de respiration */
.reading h2 { margin-top: 18px; }
.reading h3 { margin-top: 14px; }
.edition-bar {
display: flex;
flex-wrap: wrap;
@@ -99,6 +133,11 @@ main { padding: 0; }
border: 1px solid rgba(127,127,127,0.55);
border-radius: 999px;
padding: 5px 12px;
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
}
.resume-btn:hover{
background: rgba(127,127,127,0.10);
transform: translateY(-1px);
}
/* Jump by paragraph id */
@@ -114,6 +153,11 @@ main { padding: 0; }
border-radius: 999px;
font-size: 13px;
width: 320px;
transition: border-color 120ms var(--ease-out), box-shadow 120ms var(--ease-out);
}
.jump-input:focus-visible{
border-color: rgba(140,140,255,0.65);
box-shadow: 0 0 0 3px rgba(140,140,255,0.18);
}
.jump-input.is-error{
outline: 2px solid rgba(127,127,127,0.55);
@@ -126,6 +170,11 @@ main { padding: 0; }
border-radius: 999px;
cursor: pointer;
font-size: 13px;
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
}
.jump-btn:hover{
background: rgba(127,127,127,0.10);
transform: translateY(-1px);
}
/* Toggle niveaux (legacy, non bloquant) */
@@ -137,6 +186,11 @@ main { padding: 0; }
border-radius: 999px;
cursor: pointer;
font-size: 13px;
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.lvl-btn:hover{
background: rgba(127,127,127,0.10);
transform: translateY(-1px);
}
.lvl-btn[aria-pressed="true"] {
border-color: rgba(127,127,127,0.9);
@@ -211,6 +265,13 @@ body[data-reading-level="4"] .level-3 { display: none; }
border-radius: 999px;
padding: 2px 8px;
background: transparent;
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
}
.para-anchor:hover,
.para-propose:hover,
.para-cite:hover{
background: rgba(127,127,127,0.10);
transform: translateY(-1px);
}
.para-cite{ cursor: pointer; }
@@ -221,8 +282,13 @@ body[data-reading-level="4"] .level-3 { display: none; }
padding: 4px 12px;
background: transparent;
cursor: pointer;
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
}
.para-bookmark:hover{
background: rgba(127,127,127,0.10);
transform: translateY(-1px);
text-decoration: underline;
}
.para-bookmark:hover{ text-decoration: underline; }
/* Highlight (jump / resume / arrivée hash) */
.para-highlight{
@@ -420,3 +486,57 @@ html{ scroll-behavior: smooth; }
padding: 0;
background: transparent;
}
/* ==========================
Landscape boost (mobile/tablet)
CSS-only, non destructif
- iOS + Android
- élargit la lecture en paysage sur petits écrans
- ne touche pas au desktop
========================== */
/* 1) Quand on est en paysage sur “petits” devices,
on évite une colonne unique trop “étroite” et on retire le max-width 78ch. */
@media (orientation: landscape) and (max-width: 1024px){
/* La grille (EditionLayout) utilise .page-shell ; on la rend plus “fluide” */
.page-shell{
max-width: calc(100vw - 16px) !important; /* respire mais exploite la largeur */
}
/* La lecture ne doit pas rester “bridée” à 78ch en paysage */
.reading{
max-width: none !important;
width: 100% !important;
}
}
/* 2) Cas extrême : petits téléphones en paysage (hauteur faible).
On réduit légèrement les paddings pour gagner de la place utile. */
@media (orientation: landscape) and (max-width: 820px) and (max-height: 520px){
header{
padding-top: 10px;
padding-bottom: 10px;
}
/* allège un peu la barre dédition (sans la casser) */
.edition-bar{
margin-top: 8px;
padding-top: 8px;
gap: 8px 12px;
}
/* champ jump un peu moins large, pour éviter de “manger” la nav */
.jump-input{
width: min(260px, 48vw);
}
}
/* 3) iOS Safari : le viewport peut être “bizarre” en paysage (barres).
Cette règle aide à éviter des layouts coincés quand lUI Safari bouge. */
@supports (height: 100dvh){
@media (orientation: landscape) and (max-width: 1024px){
.page-shell{
min-height: 100dvh;
}
}
}

568
src/styles/panel.css Normal file
View File

@@ -0,0 +1,568 @@
/* src/styles/panel.css — v3.1 premium FIX
Objectifs :
- Lightbox AU-DESSUS du header (pas masquée)
- bouton close toujours visible
- le haut du média jamais coupé
- CSS-only, aucune logique JS modifiée
*/
/* ===== Tokens ===== */
:root{
--panel-radius: 18px;
--panel-radius-sm: 14px;
--panel-border: rgba(127,127,127,0.16);
--panel-border-strong: rgba(127,127,127,0.26);
--panel-glass: rgba(255,255,255,0.055);
--panel-glass-2: rgba(255,255,255,0.075);
--panel-shadow: 0 14px 34px rgba(0,0,0,0.07);
--panel-shadow-2: 0 22px 60px rgba(0,0,0,0.10);
--panel-title: rgba(20,20,20,0.90);
--panel-text: rgba(20,20,20,0.84);
--panel-muted: rgba(20,20,20,0.62);
--chip-bg: rgba(127,127,127,0.10);
--chip-bd: rgba(127,127,127,0.22);
--btn-bg: rgba(127,127,127,0.10);
--btn-bd: rgba(127,127,127,0.30);
/* thumbs */
--thumb: 84px;
/* Motion */
--ease: cubic-bezier(.2,.85,.2,1);
--ease2: cubic-bezier(.18,.9,.18,1);
/* Lightbox sizing */
--lb-maxw: 1480px;
/* Inset (viewport safe) : header + notch + marges */
--lb-inset-x: 14px;
--lb-inset-top: max(calc(env(safe-area-inset-top, 0px) + 12px), 12px);
--lb-inset-bot: max(calc(env(safe-area-inset-bottom, 0px) + 12px), 12px);
/* Space reserved for close button strip inside dialog */
--lb-topbar-h: 56px;
/* padding viewer */
--lb-pad: 16px;
}
@media (min-width: 900px){
:root{ --lb-inset-x: 24px; --lb-pad: 18px; }
}
@media (prefers-color-scheme: dark){
:root{
--panel-border: rgba(255,255,255,0.12);
--panel-border-strong: rgba(255,255,255,0.18);
--panel-glass: rgba(255,255,255,0.045);
--panel-glass-2: rgba(255,255,255,0.070);
--panel-shadow: 0 18px 48px rgba(0,0,0,0.26);
--panel-shadow-2: 0 30px 80px rgba(0,0,0,0.38);
--panel-title: rgba(255,255,255,0.90);
--panel-text: rgba(255,255,255,0.86);
--panel-muted: rgba(255,255,255,0.62);
--chip-bg: rgba(255,255,255,0.08);
--chip-bd: rgba(255,255,255,0.14);
--btn-bg: rgba(255,255,255,0.07);
--btn-bd: rgba(255,255,255,0.16);
}
}
/* ===== Panel container ===== */
body[data-reading-level="1"] .page-panel{ display: none !important; }
/* IMPORTANT :
Pour que la Lightbox (enfant du panel) puisse passer AU-DESSUS du header,
on met le panel dans une couche au-dessus du header (z=50).
Comme le panel nempiète pas sur le header (top calculé), cest safe.
*/
.page-panel{
position: sticky;
top: calc(var(--sticky-header-h) + var(--page-gap));
align-self: start;
color: var(--panel-text);
z-index: 80; /* ✅ au-dessus du header (50) => Lightbox non masquée */
}
.page-panel__inner{
max-height: calc(100vh - (var(--sticky-header-h) + var(--page-gap) + 12px));
overflow: auto;
scrollbar-gutter: stable;
border: 1px solid var(--panel-border);
border-radius: var(--panel-radius);
padding: 14px;
background:
radial-gradient(1200px 600px at 20% 0%, rgba(140,140,255,0.10), transparent 55%),
radial-gradient(900px 520px at 80% 20%, rgba(127,255,210,0.06), transparent 55%),
linear-gradient(180deg, var(--panel-glass), transparent 68%);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: var(--panel-shadow);
transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease), transform 140ms var(--ease);
}
.page-panel.is-updating .page-panel__inner{
border-color: var(--panel-border-strong);
box-shadow: var(--panel-shadow-2);
transform: translateY(-1px);
}
/* ===== Head ===== */
.panel-head{
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(127,127,127,0.18);
}
.panel-head__left{
display: inline-flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.panel-head__label{ font-weight: 900; opacity: .88; }
.panel-head__id{
font-weight: 850;
letter-spacing: .01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 16rem;
padding: 2px 10px;
border-radius: 999px;
border: 1px solid var(--chip-bd);
background: var(--chip-bg);
}
/* ===== Messages ===== */
.panel-msg{ margin-top: 8px; font-size: 12px; opacity: .92; }
.panel-msg--head{ margin-top: 0; margin-bottom: 10px; }
/* ===== Toolbar actions : compacte + sticky ===== */
.panel-top-actions{
position: sticky;
top: 0;
z-index: 6;
margin: 10px 0 10px;
padding: 10px;
border: 1px solid rgba(127,127,127,0.14);
border-radius: var(--panel-radius-sm);
background: rgba(255,255,255,0.55);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 10px 24px rgba(0,0,0,0.06);
}
@media (prefers-color-scheme: dark){
.panel-top-actions{
background: rgba(0,0,0,0.28);
box-shadow: 0 12px 28px rgba(0,0,0,0.22);
}
}
.panel-top-actions::after{
content: "";
display: block;
margin-top: 10px;
height: 1px;
background: rgba(127,127,127,0.16);
}
/* ===== Buttons ===== */
.panel-actions{
display: flex;
gap: 8px;
flex-wrap: wrap;
margin: 0;
}
.panel-btn{
border: 1px solid var(--btn-bd);
background: var(--btn-bg);
border-radius: 999px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
color: inherit;
transition: transform 120ms var(--ease2), background 120ms var(--ease2), border-color 120ms var(--ease2);
}
.panel-btn:hover{
transform: translateY(-1px);
border-color: var(--panel-border-strong);
background: rgba(127,127,127,0.14);
}
@media (prefers-color-scheme: dark){
.panel-btn:hover{ background: rgba(255,255,255,0.10); }
}
.panel-btn:disabled{ opacity: .55; cursor: default; transform: none; }
.panel-btn--primary{ font-weight: 900; border-color: var(--panel-border-strong); }
/* ===== Sections ===== */
.panel-block{
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(127,127,127,0.16);
}
.panel-block:first-of-type{ margin-top: 10px; border-top: 0; padding-top: 0; }
.panel-title{
margin: 0 0 10px;
font-size: 13px;
font-weight: 950;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--panel-title);
opacity: .96;
}
.panel-subtitle{ font-size: 12px; font-weight: 900; margin: 12px 0 6px; opacity: .90; }
.panel-body p{ margin: 8px 0; opacity: .92; }
.panel-list{ margin: 0; padding-left: 18px; }
.panel-list li{ margin: 7px 0; }
.panel-chip{
display: inline-block;
margin-left: 8px;
font-size: 11px;
font-weight: 900;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--chip-bd);
background: var(--chip-bg);
opacity: .95;
}
.panel-quote{
margin: 8px 0;
padding: 10px 10px;
border-left: 3px solid rgba(140,140,255,0.35);
background: rgba(127,127,127,0.06);
border-radius: 12px;
}
@media (prefers-color-scheme: dark){
.panel-quote{ background: rgba(255,255,255,0.05); }
}
.panel-quote__src{ margin-top: 6px; font-size: 12px; opacity: .72; }
/* ===== Media grid ===== */
.panel-media-grid{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--thumb), 1fr));
gap: 10px;
align-items: start;
}
.panel-media-tile{
width: 100%;
border: 1px solid rgba(127,127,127,0.16);
border-radius: 14px;
padding: 8px;
background: rgba(127,127,127,0.04);
cursor: pointer;
text-align: left;
transition: transform 120ms var(--ease2), background 120ms var(--ease2), border-color 120ms var(--ease2);
}
@media (prefers-color-scheme: dark){
.panel-media-tile{ background: rgba(255,255,255,0.035); }
}
.panel-media-tile:hover{
transform: translateY(-1px);
border-color: var(--panel-border-strong);
background: rgba(127,127,127,0.08);
}
@media (prefers-color-scheme: dark){
.panel-media-tile:hover{ background: rgba(255,255,255,0.055); }
}
.panel-media-tile img{
width: 100%;
height: var(--thumb);
object-fit: cover;
display: block;
border-radius: 10px;
margin-bottom: 8px;
cursor: zoom-in;
}
.panel-media-ph{
width: 100%;
height: var(--thumb);
border-radius: 10px;
display: grid;
place-items: center;
background: var(--chip-bg);
margin-bottom: 8px;
font-weight: 950;
font-size: 12px;
opacity: .9;
}
.panel-media-cap{
font-size: 12px;
font-weight: 900;
opacity: .92;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.panel-media-credit{
margin-top: 4px;
font-size: 11px;
opacity: .74;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Featured (1 média) */
.panel-media-grid > .panel-media-tile:only-child{
grid-column: 1 / -1;
padding: 10px;
}
.panel-media-grid > .panel-media-tile:only-child img,
.panel-media-grid > .panel-media-tile:only-child .panel-media-ph{
height: min(52vh, 460px);
object-fit: contain;
background: rgba(0,0,0,0.08);
}
@media (prefers-color-scheme: dark){
.panel-media-grid > .panel-media-tile:only-child img,
.panel-media-grid > .panel-media-tile:only-child .panel-media-ph{
background: rgba(0,0,0,0.22);
}
}
.panel-media-grid > .panel-media-tile:only-child .panel-media-cap{
white-space: normal;
overflow: visible;
text-overflow: clip;
line-height: 1.35;
}
/* ===== Compose ===== */
.panel-compose{
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed rgba(127,127,127,0.22);
}
.panel-label{
display: block;
font-size: 12px;
font-weight: 950;
opacity: .90;
margin-bottom: 6px;
}
.panel-textarea{
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(127,127,127,0.24);
border-radius: 12px;
padding: 9px 10px;
background: transparent;
font-size: 13px;
resize: vertical;
}
/* =======================================================================
LIGHTBOX FIX (CRITIQUE)
- overlay couvre le header
- close toujours visible
- le haut du média nest jamais “mangé”
- overrides forts (important) car styles inline du composant existent
======================================================================= */
/* Quand hidden => pas dinteraction */
.panel-lightbox[hidden]{ display: none !important; }
/* Conteneur full-viewport AU DESSUS de tout */
.panel-lightbox{
position: fixed !important;
inset: 0 !important;
z-index: 2147483647 !important; /* max practical */
pointer-events: auto !important;
}
/* Overlay : couvre TOUT y compris header */
.panel-lightbox__overlay{
position: absolute !important;
inset: 0 !important;
background: rgba(0,0,0,0.86) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
}
/* Dialog : on bannit transform/centering qui peut “couper” le haut.
On utilise un inset fixe => le haut est toujours visible. */
.panel-lightbox__dialog{
position: fixed !important;
left: auto !important;
top: auto !important;
right: auto !important;
bottom: auto !important;
transform: none !important;
inset: var(--lb-inset-top) var(--lb-inset-x) var(--lb-inset-bot) var(--lb-inset-x) !important;
width: auto !important;
height: auto !important;
max-height: none !important;
border-radius: 18px !important;
border: 1px solid rgba(255,255,255,0.14) !important;
background: rgba(18,18,18,0.58) !important;
backdrop-filter: blur(14px) !important;
-webkit-backdrop-filter: blur(14px) !important;
box-shadow: 0 28px 90px rgba(0,0,0,0.60) !important;
overflow: hidden !important;
/* layout interne */
display: flex !important;
flex-direction: column !important;
/* petite anim (discrète) */
animation: archiLbIn 160ms var(--ease) !important;
}
@keyframes archiLbIn{
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Close : FIXED dans le viewport => ne peut plus être masqué par le header */
.panel-lightbox__close{
position: fixed !important;
top: calc(var(--lb-inset-top) + 8px) !important;
right: calc(var(--lb-inset-x) + 8px) !important;
z-index: 2147483647 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 46px !important;
height: 42px !important;
border-radius: 14px !important;
border: 1px solid rgba(255,255,255,0.30) !important;
background: rgba(0,0,0,0.50) !important;
color: rgba(255,255,255,0.92) !important;
cursor: pointer !important;
font-size: 22px !important;
font-weight: 950 !important;
transition: transform 120ms var(--ease2), background 120ms var(--ease2) !important;
}
.panel-lightbox__close:hover{
transform: translateY(-1px) !important;
background: rgba(255,255,255,0.14) !important;
}
/* Contenu : prend lespace, jamais sous le close (close est fixed viewport) */
.panel-lightbox__content{
flex: 1 1 auto !important;
min-height: 0 !important;
padding: var(--lb-pad) !important;
padding-top: 12px !important;
display: grid !important;
place-items: center !important;
overflow: auto !important;
}
/* Média : contain, et jamais coupé en haut (dialog inset fixe + scroll possible) */
.panel-lightbox__content img,
.panel-lightbox__content video{
display: block !important;
max-width: min(var(--lb-maxw), calc(100vw - (var(--lb-inset-x) * 2) - (var(--lb-pad) * 2))) !important;
max-height: calc(100vh - var(--lb-inset-top) - var(--lb-inset-bot) - 64px) !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
background: rgba(0,0,0,0.22) !important;
border-radius: 14px !important;
}
/* audio */
.panel-lightbox__content audio{
width: min(860px, calc(100vw - (var(--lb-inset-x) * 2) - 28px)) !important;
}
/* Caption : en bas, sans masquer le média (on reste simple) */
.panel-lightbox__caption{
position: relative !important;
z-index: 2 !important;
padding: 10px 14px !important;
font-size: 12px !important;
font-weight: 950 !important;
color: rgba(255,255,255,0.92) !important;
background: linear-gradient(to top, rgba(0,0,0,0.72), rgba(0,0,0,0.00)) !important;
}
/* Bonus : si le navigateur supporte :has(), on fige le scroll arrière-plan (premium) */
@supports selector(html:has(*)){
html:has(.panel-lightbox:not([hidden])){
overflow: hidden;
}
}
/* ===== Responsive : comme avant, panel caché sous 1100px ===== */
@media (max-width: 1100px){
.page-panel{ display: none; }
}
/* ===== Reduce motion ===== */
@media (prefers-reduced-motion: reduce){
.panel-lightbox__dialog{ animation: none !important; }
.panel-btn,
.panel-media-tile,
.page-panel__inner{ transition: none !important; }
}