fix(csp): docs + 418 sans style/script inline — CSS/JS externalisés, header/footer injectés par components.js (#h/#f), playground API déplacé dans docs-playground.js et docs.css

This commit is contained in:
riricdev 2026-09-07 15:34:23 +02:00
commit b65db0be44
7 changed files with 310 additions and 112 deletions

160
public/css/docs.css Normal file
View file

@ -0,0 +1,160 @@
/* Page de documentation des API (docs/maxspeed etc.) */
.docs-wrap {
flex: 1;
width: 100%;
min-height: 0;
display: flex;
gap: 32px;
align-items: flex-start;
max-width: 1180px;
margin: 0 auto;
padding: 40px clamp(1.25rem, 5vw, 2.5rem);
}
.docs-side {
flex: 0 0 240px;
position: sticky;
top: 24px;
font-size: 14px;
}
.docs-side h3 {
margin: 0 0 10px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
opacity: 0.6;
}
.docs-side ul {
list-style: none;
margin: 0;
padding: 0;
}
.docs-side li {
margin: 4px 0;
}
.docs-side a {
color: inherit;
text-decoration: none;
opacity: 0.85;
}
.docs-side a:hover {
opacity: 1;
text-decoration: underline;
}
.docs-side a.active {
opacity: 1;
font-weight: 600;
}
.docs-body {
flex: 1 1 auto;
min-width: 0;
line-height: 1.7;
color: var(--text);
}
.docs-body h1 {
font-size: 1.9em;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
}
.docs-body h2 {
font-size: 1.35em;
margin-top: 2.2em;
border-bottom: 1px solid var(--border);
padding-bottom: 6px;
}
.docs-body h3 {
font-size: 1.1em;
margin-top: 1.6em;
}
.docs-body pre {
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
overflow: auto;
font-size: 13px;
color: var(--text);
}
.docs-body code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.docs-body :not(pre) > code {
background: var(--bg-soft);
padding: 2px 5px;
border-radius: 4px;
font-size: 0.9em;
}
.docs-body table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
font-size: 14px;
}
.docs-body th,
.docs-body td {
border: 1px solid var(--border);
padding: 8px 12px;
text-align: left;
}
.docs-body th {
background: var(--bg-soft);
}
.docs-body blockquote {
border-left: 3px solid var(--accent);
margin: 16px 0;
padding: 4px 16px;
opacity: 0.85;
}
@media (max-width: 760px) {
.docs-wrap {
flex-direction: column;
}
.docs-side {
position: static;
flex: auto;
}
}
/* ===== Playground d'interrogation en direct (docs API) ===== */
.ms-playground {
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
}
.ms-playground form {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: end;
}
.ms-playground label {
display: flex;
flex-direction: column;
font-size: 12px;
gap: 4px;
}
.ms-playground input {
background: var(--bg);
color: inherit;
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 8px;
width: 130px;
}
.ms-playground button {
background: var(--bg-soft);
color: inherit;
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
}
.ms-playground button:hover {
border-color: var(--accent);
}
.ms-out {
white-space: pre-wrap;
margin-top: 12px;
font-size: 13px;
}

104
public/css/teapot.css Normal file
View file

@ -0,0 +1,104 @@
/* Page easter egg 418 (I'm a Teapot — RFC 2324) */
.tea-hero {
text-align: center;
padding: clamp(5rem, 12vh, 8rem) 0 4rem;
position: relative;
}
.tea-hero::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(55% 45% at 50% 18%, var(--accent-soft) 0%, transparent 72%);
pointer-events: none;
}
.tea-hero > * {
position: relative;
}
.tea-hero h1 {
font-family: var(--font-head);
font-size: clamp(5rem, 18vw, 11rem);
font-weight: 900;
line-height: 1;
letter-spacing: -0.04em;
margin-bottom: 1.5rem;
}
.tea-hero .tea-sub {
color: var(--text-muted);
font-size: clamp(1rem, 2vw, 1.2rem);
max-width: 44ch;
margin: 0 auto 1.25rem;
line-height: 1.6;
}
.tea-stage {
position: relative;
width: min(340px, 80vw);
margin: 2rem auto;
}
.tea-pot {
display: block;
width: 100%;
color: var(--text);
}
.tea-steam {
position: absolute;
pointer-events: none;
}
.tea-steam span {
position: absolute;
bottom: 0;
display: block;
width: 14px;
height: 38px;
border-radius: 999px;
background: radial-gradient(circle at 50% 40%, var(--accent-soft) 0%, transparent 70%);
animation: tea-rise 3s ease-in infinite;
}
.tea-steam .ts1 { left: 44%; animation-delay: 0s; }
.tea-steam .ts2 { left: 52%; animation-delay: 1s; transform: scaleX(-1); }
.tea-steam .ts3 { left: 60%; animation-delay: 2s; }
@keyframes tea-rise {
0% { opacity: 0; transform: translateY(10px) scale(0.7); }
30% { opacity: 1; }
100% { opacity: 0; transform: translateY(-120px) scale(1.15); }
}
@media (prefers-reduced-motion: reduce) {
.tea-steam span { animation: none; opacity: 0.35; }
}
.tea-actions {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.tea-hint {
color: var(--text-muted);
opacity: 0.7;
font-size: 0.85rem;
margin-top: 2.25rem;
}
.tea-note {
color: var(--text-muted);
font-size: 0.9rem;
letter-spacing: 0.02em;
}
.tea-toast {
position: fixed;
left: 50%;
bottom: 2rem;
transform: translateX(-50%);
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text);
padding: 0.8rem 1.2rem;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
opacity: 0;
transition: opacity 0.25s ease;
z-index: 50;
pointer-events: none;
max-width: 90vw;
text-align: center;
}
.tea-toast.show {
opacity: 1;
}

View file

@ -0,0 +1,15 @@
(function () {
var form = document.getElementById("ms-form")
var out = document.getElementById("ms-out")
if (!form || !out) return
form.addEventListener("submit", function (e) {
e.preventDefault()
var lat = document.getElementById("ms-lat").value
var lon = document.getElementById("ms-lon").value
out.textContent = "Chargement…"
fetch("/api/maxspeed/point?lat=" + encodeURIComponent(lat) + "&lon=" + encodeURIComponent(lon))
.then(function (r) { return r.json() })
.then(function (d) { out.textContent = JSON.stringify(d, null, 2) })
.catch(function (err) { out.textContent = "Erreur: " + err })
})
})()

16
public/js/teapot.js Normal file
View file

@ -0,0 +1,16 @@
(function () {
var toast = document.querySelector(".tea-toast");
if (!toast) return;
var timer = null;
function spin(msg) {
toast.textContent = msg;
toast.classList.add("show");
if (timer) clearTimeout(timer);
timer = setTimeout(function () { toast.classList.remove("show"); }, 3200);
}
document.querySelectorAll("[data-tea]").forEach(function (b) {
b.addEventListener("click", function () {
spin(b.getAttribute("data-toast") || "");
});
});
})();

View file

@ -24,56 +24,18 @@ function layout(title: string, description: string, body: string, sidebar: strin
<meta property="og:image" content="${SITE_URL}/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="me" href="https://piaille.fr/@zektyc" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<style>
.docs-wrap { display: flex; gap: 32px; align-items: flex-start; max-width: 1180px; margin: 0 auto; padding: 40px 24px; }
.docs-side { flex: 0 0 240px; position: sticky; top: 24px; font-size: 14px; }
.docs-side h3 { margin: 0 0 10px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.6; }
.docs-side ul { list-style: none; margin: 0; padding: 0; }
.docs-side li { margin: 4px 0; }
.docs-side a { color: inherit; text-decoration: none; opacity: 0.85; }
.docs-side a:hover { opacity: 1; text-decoration: underline; }
.docs-side a.active { opacity: 1; font-weight: 600; }
.docs-body { flex: 1 1 auto; min-width: 0; line-height: 1.7; }
.docs-body h1 { font-size: 1.9em; border-bottom: 1px solid #222; padding-bottom: 12px; }
.docs-body h2 { font-size: 1.35em; margin-top: 2.2em; border-bottom: 1px solid #1e1e1e; padding-bottom: 6px; }
.docs-body h3 { font-size: 1.1em; margin-top: 1.6em; }
.docs-body pre { background: #0d1117; border: 1px solid #222; border-radius: 8px; padding: 14px; overflow: auto; font-size: 13px; }
.docs-body code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.docs-body :not(pre) > code { background: #161b22; padding: 2px 5px; border-radius: 4px; font-size: 0.9em; }
.docs-body table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 14px; }
.docs-body th, .docs-body td { border: 1px solid #24262b; padding: 8px 12px; text-align: left; }
.docs-body th { background: #0d1117; }
.docs-body blockquote { border-left: 3px solid #333; margin: 16px 0; padding: 4px 16px; opacity: 0.85; }
@media (max-width: 760px) { .docs-wrap { flex-direction: column; } .docs-side { position: static; flex: auto; } }
</style>
<link rel="stylesheet" href="/css/style.css?v=8" />
<link rel="stylesheet" href="/css/docs.css?v=2" />
<script src="/js/theme.js" defer></script>
<script src="/js/i18n.js" defer></script>
<script>document.addEventListener("click",function(e){var t=e.target.closest?e.target.closest(".site-nav"):null;if(t){var n=document.getElementById("nav-toggle");if(n)n.checked=false}})</script>
<script src="/js/global.js?v=1" defer></script>
<script src="/js/components.js?v=6" defer></script>
<script src="/js/docs-playground.js?v=1" defer></script>
</head>
<body>
<header class="site-header">
<a class="skip-link" href="#main">Aller au contenu</a>
<a class="brand" href="/"><span class="brand-mark" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16" role="presentation" focusable="false"><path d="M5 4h22L5 12h22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="brand-name">Zektyc</span></a>
<input type="checkbox" id="nav-toggle" class="nav-toggle-input" />
<label for="nav-toggle" class="nav-burger" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>
</label>
<nav class="site-nav" aria-label="Navigation">
<div class="lang-switch" role="group" aria-label="Langue / Language">
<button type="button" data-lang="fr" class="lang-btn active"><strong>FR</strong></button>
<button type="button" data-lang="en" class="lang-btn">EN</button>
</div>
<button type="button" class="theme-btn" aria-label="Theme">
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
</button>
</nav>
</header>
<header id="h"></header>
<main id="main" class="docs-wrap">
<nav class="docs-side" aria-label="Documentation">
@ -87,7 +49,7 @@ ${body}
</article>
</main>
<footer class="site-footer"></footer>
<footer id="f"></footer>
</body>
</html>`
}

View file

@ -228,33 +228,6 @@ l'API en direct.
<pre id="ms-out" class="ms-out">—</pre>
</div>
<style>
.ms-playground { background: #0d1117; border: 1px solid #222; border-radius: 8px; padding: 16px; }
.ms-playground form { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; }
.ms-playground label { display: flex; flex-direction: column; font-size: 12px; gap: 4px; }
.ms-playground input { background: #111; color: inherit; border: 1px solid #333; border-radius: 4px; padding: 6px 8px; width: 130px; }
.ms-playground button { background: #333; color: inherit; border: 0; border-radius: 6px; padding: 8px 14px; cursor: pointer; }
.ms-playground button:hover { background: #444; }
.ms-out { white-space: pre-wrap; margin-top: 12px; font-size: 13px; }
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
var form = document.getElementById("ms-form")
var out = document.getElementById("ms-out")
form.addEventListener("submit", function (e) {
e.preventDefault()
var lat = document.getElementById("ms-lat").value
var lon = document.getElementById("ms-lon").value
out.textContent = "Chargement…"
fetch("/api/maxspeed/point?lat=" + encodeURIComponent(lat) + "&lon=" + encodeURIComponent(lon))
.then(function (r) { return r.json() })
.then(function (d) { out.textContent = JSON.stringify(d, null, 2) })
.catch(function (err) { out.textContent = "Erreur: " + err })
})
})
</script>
---
_Documentation générée automatiquement. Dernière vérification de la section

View file

@ -612,30 +612,13 @@ function teapotPage(lang: "fr" | "en"): string {
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=8" />
<link rel="stylesheet" href="/css/teapot.css?v=1" />
<script src="/js/theme.js" defer></script>
<script src="/js/global.js?v=1" defer></script>
<style>
.tea-hero{text-align:center;padding:3rem 1rem 4rem}
.tea-hero h1{font-size:clamp(5rem,18vw,11rem);font-weight:900;line-height:1;letter-spacing:-0.04em;margin-bottom:.75rem}
.tea-hero .tea-sub{color:var(--text-muted);font-size:1.05rem;max-width:44ch;margin:0 auto 1.25rem;line-height:1.6}
.tea-stage{position:relative;width:min(340px,80vw);margin:1.5rem auto 2rem}
.tea-pot{display:block;width:100%;color:var(--text)}
.tea-steam{position:absolute;pointer-events:none}
.tea-steam span{position:absolute;bottom:0;display:block;width:14px;height:38px;border-radius:999px;background:radial-gradient(circle at 50% 40%,rgba(var(--accent-rgb,255,255,255),.35),transparent 70%);animation:tea-rise 3s ease-in infinite}
.tea-steam .ts1{left:44%;animation-delay:0s}
.tea-steam .ts2{left:52%;animation-delay:1s;transform:scaleX(-1)}
.tea-steam .ts3{left:60%;animation-delay:2s}
@keyframes tea-rise{0%{opacity:0;transform:translateY(10px) scale(.7)}30%{opacity:1}100%{opacity:0;transform:translateY(-120px) scale(1.15)}}
@media (prefers-reduced-motion: reduce){.tea-steam span{animation:none;opacity:.35}}
.tea-actions{display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}
.tea-hint{color:var(--text-muted);opacity:.7;font-size:.85rem;margin-top:2.25rem}
.tea-note{color:var(--text-muted);font-size:.9rem;letter-spacing:.02em}
.tea-toast{position:fixed;left:50%;bottom:2rem;transform:translateX(-50%);background:var(--bg-elevated,#111);border:1px solid var(--border,#333);color:var(--text);padding:.8rem 1.2rem;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.35);opacity:0;transition:opacity .25s ease;z-index:50;pointer-events:none;max-width:90vw;text-align:center}
.tea-toast.show{opacity:1}
</style>
<script src="/js/components.js?v=6" defer></script>
</head>
<body>
${siteHeader(lang)}
<header id="h"></header>
<main class="site-main" id="main">
<section class="tea-hero">
@ -654,8 +637,8 @@ function teapotPage(lang: "fr" | "en"): string {
</div>
<p>${text}</p>
<div class="tea-actions">
<button type="button" class="btn btn-primary" data-tea="coffee">${coffee}</button>
<button type="button" class="btn" data-tea="tea">${tea}</button>
<button type="button" class="btn btn-primary" data-tea="coffee" data-toast="${coffeeToast}">${coffee}</button>
<button type="button" class="btn" data-tea="tea" data-toast="${teaToast}">${tea}</button>
</div>
<p class="tea-hint">\u2191 \u2191 \u2193 \u2193 \u2190 \u2192 \u2190 \u2192 B A</p>
<p class="tea-note">${note}</p>
@ -664,24 +647,9 @@ function teapotPage(lang: "fr" | "en"): string {
<div class="tea-toast" role="status" aria-live="polite"></div>
${siteFooter(lang)}
<footer id="f"></footer>
<script>
(function () {
var toast = document.querySelector(".tea-toast");
var spin = function (m) {
toast.textContent = m;
toast.classList.add("show");
clearTimeout(toast._t);
toast._t = setTimeout(function () { toast.classList.remove("show"); }, 3200);
};
document.querySelectorAll("[data-tea]").forEach(function (b) {
b.addEventListener("click", function () {
spin(b.getAttribute("data-tea") === "coffee" ? "${coffeeToast}" : "${teaToast}");
});
});
})();
</script>
<script src="/js/teapot.js?v=1" defer></script>
</body>
</html>`
}