diff --git a/p/assets/img/hero/gnome.png b/p/assets/img/hero/gnome.png new file mode 100644 index 0000000..e81bbf4 Binary files /dev/null and b/p/assets/img/hero/gnome.png differ diff --git a/p/assets/img/hero/install.png b/p/assets/img/hero/install.png new file mode 100644 index 0000000..b2ee28f Binary files /dev/null and b/p/assets/img/hero/install.png differ diff --git a/p/assets/img/hero/xfce.png b/p/assets/img/hero/xfce.png new file mode 100644 index 0000000..bd6bb09 Binary files /dev/null and b/p/assets/img/hero/xfce.png differ diff --git a/p/assets/js/hero-carousel.js b/p/assets/js/hero-carousel.js new file mode 100644 index 0000000..fd64fb5 --- /dev/null +++ b/p/assets/js/hero-carousel.js @@ -0,0 +1,155 @@ +(() => { + "use strict"; + + const existingCleanup = window.__heroCarouselCleanup; + if (typeof existingCleanup === "function") { + existingCleanup(); + } + + const hero = document.querySelector(".hero-carousel"); + if (!hero) { + return; + } + + const slides = Array.from(hero.querySelectorAll("[data-hero-slide]")); + const prevButton = hero.querySelector("[data-hero-prev]"); + const nextButton = hero.querySelector("[data-hero-next]"); + const dotsContainer = hero.querySelector("[data-hero-dots]"); + + if (!slides.length || !prevButton || !nextButton || !dotsContainer) { + return; + } + + let currentIndex = Math.max( + 0, + slides.findIndex((slide) => slide.classList.contains("is-active")) + ); + let autoRotateTimer = null; + const rotateDelay = 5500; + const abortController = new AbortController(); + const { signal } = abortController; + + const dots = slides.map((_, index) => { + const dot = document.createElement("button"); + dot.type = "button"; + dot.className = "hero-dot"; + dot.setAttribute("role", "tab"); + dot.setAttribute("aria-label", `Aller a l'image ${index + 1}`); + dot.dataset.index = index.toString(); + dotsContainer.appendChild(dot); + return dot; + }); + + function updateUI() { + slides.forEach((slide, index) => { + const isActive = index === currentIndex; + slide.classList.toggle("is-active", isActive); + slide.setAttribute("aria-hidden", String(!isActive)); + }); + + dots.forEach((dot, index) => { + const isActive = index === currentIndex; + dot.classList.toggle("is-active", isActive); + dot.setAttribute("aria-selected", String(isActive)); + dot.tabIndex = isActive ? 0 : -1; + }); + } + + function goTo(index) { + if (index < 0) { + currentIndex = slides.length - 1; + } else if (index >= slides.length) { + currentIndex = 0; + } else { + currentIndex = index; + } + updateUI(); + } + + function stopAutoRotate() { + if (autoRotateTimer !== null) { + window.clearInterval(autoRotateTimer); + autoRotateTimer = null; + } + } + + function startAutoRotate() { + stopAutoRotate(); + autoRotateTimer = window.setInterval(() => { + goTo(currentIndex + 1); + }, rotateDelay); + } + + prevButton.addEventListener( + "click", + () => { + goTo(currentIndex - 1); + startAutoRotate(); + }, + { signal } + ); + + nextButton.addEventListener( + "click", + () => { + goTo(currentIndex + 1); + startAutoRotate(); + }, + { signal } + ); + + dotsContainer.addEventListener( + "click", + (event) => { + const button = event.target.closest(".hero-dot"); + if (!button) { + return; + } + + const targetIndex = Number.parseInt(button.dataset.index || "0", 10); + goTo(targetIndex); + startAutoRotate(); + }, + { signal } + ); + + hero.addEventListener( + "mouseenter", + () => { + stopAutoRotate(); + }, + { signal } + ); + + hero.addEventListener( + "mouseleave", + () => { + startAutoRotate(); + }, + { signal } + ); + + document.addEventListener( + "visibilitychange", + () => { + if (document.hidden) { + stopAutoRotate(); + } else { + startAutoRotate(); + } + }, + { signal } + ); + + updateUI(); + startAutoRotate(); + + window.__heroCarouselCleanup = () => { + stopAutoRotate(); + abortController.abort(); + }; + + document.addEventListener("page:before-route-change", window.__heroCarouselCleanup, { + once: true, + }); +})(); diff --git a/p/assets/main-nav/app.js b/p/assets/main-nav/app.js index 3173bb4..e94c0aa 100644 --- a/p/assets/main-nav/app.js +++ b/p/assets/main-nav/app.js @@ -2,6 +2,7 @@ const main = document.getElementById("app-main"); const footerYear = document.getElementById("footer-year"); const pageStyleSelectors = "[data-page-style='true']"; + const pageScriptSelectors = "script[data-page-script='true']"; const links = Array.from(document.querySelectorAll("[data-route]")); @@ -13,6 +14,11 @@ document.querySelectorAll(pageStyleSelectors).forEach((node) => node.remove()); } + function clearInjectedPageScripts() { + document.dispatchEvent(new CustomEvent("page:before-route-change")); + document.querySelectorAll(pageScriptSelectors).forEach((node) => node.remove()); + } + function injectPageStyles(parsedDocument, sourceUrl) { clearInjectedPageStyles(); @@ -38,6 +44,37 @@ }); } + async function injectPageScripts(parsedDocument, sourceUrl) { + clearInjectedPageScripts(); + + const scriptNodes = Array.from(parsedDocument.querySelectorAll("script")); + + for (const node of scriptNodes) { + const scriptElement = document.createElement("script"); + scriptElement.setAttribute("data-page-script", "true"); + + const src = node.getAttribute("src"); + if (src) { + scriptElement.src = new URL(src, sourceUrl).toString(); + scriptElement.async = false; + if (node.defer) { + scriptElement.defer = true; + } + + await new Promise((resolve, reject) => { + scriptElement.onload = () => resolve(); + scriptElement.onerror = () => reject(new Error(`Impossible de charger le script: ${src}`)); + document.body.appendChild(scriptElement); + }); + + continue; + } + + scriptElement.textContent = node.textContent; + document.body.appendChild(scriptElement); + } + } + function normalizeRoute(value) { const raw = (value ?? "").toString().trim().toLowerCase(); if (!raw || raw === "/" || raw === "home" || raw === "index" || raw === "accueil") { @@ -117,6 +154,8 @@ } async function loadRoute(route) { + clearInjectedPageScripts(); + const source = routeToSource(route); const response = await fetch(source, { cache: "no-store" }); if (!response.ok) { @@ -128,6 +167,7 @@ injectPageStyles(parsed, response.url); const injected = parsed.querySelector("[data-page-content]") ?? parsed.body; main.innerHTML = injected.innerHTML.trim(); + await injectPageScripts(parsed, response.url); } function render(route, { replaceHistory = false } = {}) { diff --git a/p/assets/styles/main.css b/p/assets/styles/main.css index cac8f71..8ec2813 100644 --- a/p/assets/styles/main.css +++ b/p/assets/styles/main.css @@ -16,14 +16,51 @@ main[data-page-content] { .hero { - background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); + position: relative; + overflow: hidden; + background: #0b1324; box-shadow: var(--shadow); border-radius: 1rem; padding: clamp(2rem, 4.5vw, 3.5rem); color: var(--text); + min-height: clamp(24rem, 56vw, 34rem); } -.hero-inner { width: 100%; } +.hero-carousel-track { + position: absolute; + inset: 0; +} + +.hero-slide { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + opacity: 0; + transform: scale(1.02); + transition: opacity 700ms ease, transform 1000ms ease; +} + +.hero-slide::before { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(110deg, rgba(2, 6, 23, 0.78) 18%, rgba(2, 6, 23, 0.28) 58%, rgba(2, 6, 23, 0.66) 100%), + radial-gradient(circle at 72% 20%, rgba(56, 189, 248, 0.2), transparent 48%); +} + +.hero-slide.is-active { + opacity: 1; + transform: scale(1); +} + +.hero-inner { + width: 100%; + max-width: 52rem; + position: relative; + z-index: 2; +} .eyebrow { color: var(--accent); @@ -36,22 +73,26 @@ main[data-page-content] { .hero-title { font-family: "Georgia", "Palatino Linotype", serif; font-size: clamp(3rem, 8vw, 5rem); - line-height: 0.95; + line-height: 1; margin: 0 0 0.25rem; - color: var(--text); + color: #f8fbff; + text-shadow: 0 10px 28px rgba(2, 6, 23, 0.38); } .hero-subtitle { font-size: clamp(1rem, 2.2vw, 1.3rem); - color: var(--muted); + color: #e7edf9; margin: 0 0 1rem; font-weight: 600; + text-shadow: 0 4px 18px rgba(2, 6, 23, 0.35); } .lead { - color: var(--muted); + color: #d8e1f3; max-width: 68ch; margin: 0 0 1.25rem; + text-wrap: pretty; + text-shadow: 0 2px 14px rgba(2, 6, 23, 0.34); } .hero-actions { @@ -60,6 +101,62 @@ main[data-page-content] { flex-wrap: wrap; } +.hero-controls { + position: absolute; + right: clamp(1rem, 2.5vw, 2rem); + bottom: clamp(1rem, 2.8vw, 1.75rem); + z-index: 3; + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.4rem 0.55rem; + border-radius: 999px; + backdrop-filter: blur(10px); + background: rgba(2, 6, 23, 0.46); + border: 1px solid rgba(148, 163, 184, 0.28); +} + +.hero-control { + width: 2.2rem; + height: 2.2rem; + border: 0; + border-radius: 999px; + display: grid; + place-items: center; + font-size: 1.32rem; + color: #e2e8f0; + background: rgba(148, 163, 184, 0.22); + cursor: pointer; + transition: transform 180ms ease, background 180ms ease; +} + +.hero-control:hover { + transform: translateY(-1px); + background: rgba(125, 211, 252, 0.36); +} + +.hero-dots { + display: inline-flex; + align-items: center; + gap: 0.4rem; + margin: 0 0.15rem; +} + +.hero-dot { + width: 0.6rem; + height: 0.6rem; + border-radius: 999px; + border: 0; + background: rgba(226, 232, 240, 0.55); + cursor: pointer; + transition: transform 180ms ease, background 180ms ease; +} + +.hero-dot.is-active { + background: #7dd3fc; + transform: scale(1.18); +} + .button { display: inline-flex; @@ -178,6 +275,15 @@ main[data-page-content] { @media (max-width: 980px) { .hero-title { font-size: clamp(2.2rem, 7vw, 4rem); } + .hero { + min-height: 27rem; + } + + .hero-controls { + left: 1rem; + right: auto; + } + .features { flex-direction: column; } @@ -202,4 +308,21 @@ main[data-page-content] { box-shadow: none; pointer-events: auto; } +} + +@media (max-width: 640px) { + .hero { + min-height: 29rem; + padding: 1.5rem; + } + + .hero-inner { + max-width: 100%; + } + + .hero-controls { + left: 0.8rem; + right: 0.8rem; + justify-content: space-between; + } } \ No newline at end of file diff --git a/p/index/index.html b/p/index/index.html index c8ff671..d75f572 100644 --- a/p/index/index.html +++ b/p/index/index.html @@ -8,18 +8,28 @@
-
+
@@ -44,5 +54,7 @@
+ + \ No newline at end of file