website/layouts/partials/jumbo.html
Pierre-Olivier Mercier 9b15e8e3ea
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Update jumbo and screenshots
2026-06-11 19:05:18 +09:00

491 lines
16 KiB
HTML

<style>
.jumbo-hero {
padding: 8rem 0 5rem;
background: linear-gradient(
135deg,
var(--hd-accent-subtle, #f0fdf4) 0%,
var(--hd-bg-canvas, white) 60%
);
}
.hero-eyebrow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.5rem;
font-size: 0.875rem;
color: var(--hd-fg-3, #6b7280);
}
.hero-eyebrow .badge-os {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25em 0.75em;
background: var(--hd-accent-subtle, #f0fdf4);
border: 1px solid var(--hd-accent, #22c55e);
border-radius: 2em;
color: var(--hd-accent, #22c55e);
font-weight: 600;
font-size: 0.8rem;
}
.hero-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1.5rem;
font-size: 0.875rem;
color: var(--hd-fg-3, #6b7280);
}
.hero-meta .check {
color: var(--hd-accent, #22c55e);
margin-right: 0.25rem;
}
/* Browser mockup */
.hero-stack {
position: relative;
}
.hero-stack .browser:last-child {
position: absolute;
bottom: 0;
right: 0;
width: 86%;
z-index: 0;
opacity: 0.45;
pointer-events: none;
}
.hero-stack .browser:first-child {
position: relative;
z-index: 1;
}
.browser {
background: var(--hd-bg-canvas, #fff);
border-radius: 10px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12);
overflow: hidden;
border: 1px solid var(--hd-border, #e5e7eb);
}
.browser-bar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.55rem 1rem;
background: var(--hd-bg-subtle, #f9fafb);
border-bottom: 1px solid var(--hd-border, #e5e7eb);
}
.browser-dots {
display: flex;
gap: 5px;
}
.browser-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--hd-border, #e5e7eb);
}
.browser-url {
flex: 1;
display: flex;
align-items: center;
gap: 0.01rem;
background: var(--hd-bg-canvas, #fff);
border-radius: 4px;
padding: 0.22rem 0.6rem;
font-size: 0.75rem;
color: var(--hd-fg-3, #6b7280);
}
.browser-url .lock {
color: var(--hd-accent, #22c55e);
font-size: 0.7rem;
}
/* ── Screenshot carousel ── */
.browser {
position: relative;
}
/* window dots double as carousel indicators */
.browser-dots .bdot {
width: 10px;
height: 10px;
padding: 0;
border: none;
border-radius: 50%;
background: var(--hd-border, #e5e7eb);
cursor: pointer;
transition:
background 0.35s ease,
transform 0.35s ease,
box-shadow 0.35s ease;
}
.browser-dots .bdot:hover {
transform: scale(1.18);
}
.browser-dots .bdot.active {
background: var(--hd-accent, #22c55e);
transform: scale(1.25);
box-shadow: 0 0 0 3px var(--hd-accent-subtle, #f0fdf4);
}
/* URL path swaps with the slide */
.browser-url .url-path {
transition: opacity 0.25s ease;
}
.browser.loading .url-path {
opacity: 0.35;
}
/* loading sweep, like a browser navigating */
.browser-load {
position: absolute;
left: 0;
top: 0;
height: 2px;
width: 0;
background: var(--hd-accent, #22c55e);
opacity: 0;
z-index: 6;
pointer-events: none;
border-radius: 0 2px 2px 0;
}
.browser.loading .browser-load {
animation: browserLoad 0.7s ease-out;
}
@keyframes browserLoad {
0% {
width: 0;
opacity: 1;
}
70% {
width: 85%;
opacity: 1;
}
100% {
width: 100%;
opacity: 0;
}
}
/* the carousel viewport */
.carousel-window {
position: relative;
overflow: hidden;
aspect-ratio: 1920 / 980;
background: var(--hd-bg-subtle, #f9fafb);
}
.carousel-track {
position: absolute;
inset: 0;
}
.carousel-track .slide {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
transition: opacity 0.8s ease;
pointer-events: none;
}
.carousel-track .slide.active {
opacity: 1;
pointer-events: auto;
z-index: 2;
}
.carousel-track .slide img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top left;
display: block;
}
/* 7s progress indicator */
.carousel-progress {
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background: rgba(0, 0, 0, 0.06);
z-index: 4;
}
.carousel-progress span {
display: block;
height: 100%;
width: 0;
background: var(--hd-accent, #22c55e);
}
.carousel-progress span.run {
animation: carProg 7s linear forwards;
}
@keyframes carProg {
from {
width: 0;
}
to {
width: 100%;
}
}
/* pause the progress timer on hover */
.browser.paused .carousel-progress span {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.carousel-track .slide {
transition: none;
}
.carousel-progress span.run {
animation: none;
}
.browser.loading .browser-load {
animation: none;
}
}
</style>
<section class="jumbo-hero">
<div class="container">
<div class="row align-items-center g-5">
<!-- Copy column -->
<div class="col-lg-5">
<a
class="hero-eyebrow"
href="https://git.happydomain.org/"
target="_blank"
>
<span class="badge-os"
><i class="bi bi-git"></i> Open source</span
>
</a>
<h1 class="display-4 fw-bold mb-5" style="text-wrap: balance">
{{ i18n "slogan" | safeHTML }}
</h1>
<p class="lead mb-5 pb-2">
happy<strong>Domain</strong> {{ i18n "lead" | markdownify }}
</p>
<div class="d-flex flex-wrap gap-3">
<a
class="btn btn-lg btn-primary px-4"
data-umami-event="jumbo-tryit"
href="{{ .Site.Params.tryit }}?lang={{ .Language }}"
>
{{ i18n "tryit" }} <i class="bi bi-arrow-right"></i>
</a>
<a
class="btn btn-lg btn-outline-dark px-4"
data-umami-event="jumbo-downloads"
href="#downloads"
>
<i class="bi bi-box-seam"></i> {{ i18n "downloadit" }}
</a>
</div>
<div class="hero-meta">
<span
><i class="bi bi-check2 check"></i
><strong>No account</strong> needed for the demo</span
>
<span
><i class="bi bi-check2 check"></i
><strong>~30s</strong> to first zone</span
>
<span
><i class="bi bi-check2 check"></i
><strong>55+</strong> providers</span
>
</div>
</div>
<!-- Visual column -->
<div class="col-lg-7 d-none d-lg-block">
<div class="hero-stack">
<!-- Front browser frame: screenshot carousel -->
<div class="browser" id="hero-carousel">
<span class="browser-load"></span>
<div class="browser-bar">
<div
class="browser-dots"
role="tablist"
aria-label="Screenshots"
>
{{ range $i, $s := sort .Site.Params.jumboscreen "weight" }}
<button
type="button"
class="bdot{{ if eq $i 0 }} active{{ end }}"
data-slide="{{ $i }}"
role="tab"
aria-selected="{{ if eq $i 0 }}true{{ else }}false{{ end }}"
aria-label="{{ $s.alt }}"
></button>
{{ end }}
</div>
<div class="browser-url">
<i class="bi bi-lock-fill lock me-1"></i>
<span>app.</span><strong>happydomain.org</strong
><span class="url-path"
>{{ with index (sort .Site.Params.jumboscreen "weight") 0 }}{{ .path }}{{ end }}</span
>
</div>
</div>
<div class="carousel-window">
<div class="carousel-track">
{{ range $i, $s := sort .Site.Params.jumboscreen "weight" }}
<figure
class="slide{{ if eq $i 0 }} active{{ end }}"
data-path="{{ $s.path }}"
>
<img
src="{{ $s.image }}"
alt="{{ $s.alt }}"
loading="lazy"
decoding="async"
/>
</figure>
{{ end }}
</div>
<div class="carousel-progress"><span></span></div>
</div>
</div>
<!-- Back browser frame, peeking -->
<div class="browser" aria-hidden="true">
<div class="browser-bar">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<div class="browser-url">
<i class="bi bi-lock-fill lock"></i
><span>try.</span
><strong>happydomain.org</strong>
</div>
</div>
<div
style="
padding: 14px 16px;
height: 120px;
background: var(--hd-bg-subtle, #f9fafb);
"
></div>
</div>
</div>
</div>
</div>
<div class="alert alert-warning mt-5 mb-0" role="alert">
<div class="row align-items-center g-3">
<div class="col-lg-7">
<h5 class="alert-heading mb-1">
<i class="bi bi-info-circle"></i>
{{ i18n "beta-alert-title" }}
</h5>
<p class="mb-0">{{ i18n "beta-alert-text" | safeHTML }}</p>
</div>
<form
class="col-lg-5 d-flex flex-column flex-sm-row gap-2"
method="post"
action="https://lists.happydomain.org/subscription/form"
>
<input type="hidden" name="nonce" />
<input type="hidden" name="l" value="ef8b61ad-fa7d-4f1a-a20f-bb34ac37a3bf" />
<input type="hidden" name="lang" value="{{ site.LanguageCode }}" />
<input
type="email"
name="email"
required
placeholder="j.postel@isi.edu"
class="form-control"
/>
<altcha-widget
floating
challengeurl="https://lists.happydomain.org/api/public/captcha/altcha"
></altcha-widget>
<button
type="submit"
class="btn btn-primary text-nowrap"
data-umami-event="beta-join"
>
{{ i18n "beta-alert-button" }}
</button>
</form>
</div>
</div>
</div>
</section>
<script>
(function () {
var root = document.getElementById("hero-carousel");
if (!root) return;
var slides = Array.prototype.slice.call(
root.querySelectorAll(".slide"),
);
var dots = Array.prototype.slice.call(root.querySelectorAll(".bdot"));
var pathEl = root.querySelector(".url-path");
var prog = root.querySelector(".carousel-progress span");
var paths = slides.map(function (slide) {
return slide.getAttribute("data-path");
});
var DURATION = 7000;
var i = 0,
timer = null;
var reduce =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function restartProgress() {
if (!prog || reduce) return;
prog.classList.remove("run");
void prog.offsetWidth; // force reflow so the animation restarts
prog.classList.add("run");
}
function go(n) {
n = ((n % slides.length) + slides.length) % slides.length;
if (n === i) return;
// mimic a real page navigation: brief loading sweep + dimmed URL
root.classList.add("loading");
window.setTimeout(function () {
root.classList.remove("loading");
}, 700);
slides[i].classList.remove("active");
dots[i].classList.remove("active");
dots[i].setAttribute("aria-selected", "false");
i = n;
slides[i].classList.add("active");
dots[i].classList.add("active");
dots[i].setAttribute("aria-selected", "true");
if (pathEl) pathEl.textContent = paths[i];
restartProgress();
}
function schedule() {
window.clearInterval(timer);
if (reduce) return;
timer = window.setInterval(function () {
go(i + 1);
}, DURATION);
}
dots.forEach(function (dot, idx) {
dot.addEventListener("click", function () {
go(idx);
schedule(); // reset the timer so the chosen slide gets its full 7s
});
});
// Pause while the visitor is inspecting a screenshot.
root.addEventListener("mouseenter", function () {
root.classList.add("paused");
window.clearInterval(timer);
});
root.addEventListener("mouseleave", function () {
root.classList.remove("paused");
schedule();
});
restartProgress();
schedule();
})();
</script>