Skip to content

Commit 7751806

Browse files
Mazizovaclaude
andauthored
Homepage: hero carousel with the DuckDB × MySQL Day announcement first (#126)
The hero becomes a two-slide carousel: an event announcement (shown while a page flagged home_featured is still ahead) and the existing mission statement. Slides auto-advance every 8 seconds, pause on hover and focus, can be paused with a button, and don't auto-advance for prefers-reduced-motion. Includes arrows, dots, swipe, and keyboard-accessible controls. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 609aeb8 commit 7751806

3 files changed

Lines changed: 271 additions & 7 deletions

File tree

‎assets/scss/_hero.scss‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,169 @@
216216
align-items: center;
217217
}
218218
}
219+
220+
// ── Homepage hero carousel ──
221+
// Slides sit in one grid cell, so the hero is as tall as the tallest slide and
222+
// switching between them doesn't move the page. Without JavaScript only the first
223+
// (event) slide shows.
224+
.hero-carousel {
225+
.hero-slides {
226+
display: grid;
227+
width: 100%;
228+
max-width: 1200px;
229+
position: relative;
230+
z-index: 1;
231+
}
232+
233+
.hero-slide {
234+
grid-area: 1 / 1;
235+
display: flex;
236+
flex-direction: column;
237+
align-items: center;
238+
justify-content: center;
239+
text-align: center;
240+
visibility: hidden;
241+
opacity: 0;
242+
transition: opacity 0.5s ease, visibility 0s linear 0.5s;
243+
244+
&.is-active {
245+
visibility: visible;
246+
opacity: 1;
247+
transition: opacity 0.5s ease;
248+
}
249+
}
250+
251+
.hero-slide--event {
252+
flex-direction: row;
253+
align-items: center;
254+
justify-content: space-between;
255+
gap: 2rem;
256+
text-align: left;
257+
padding: 0 1rem;
258+
}
259+
260+
&[data-count="1"] .hero-slide { visibility: visible; opacity: 1; }
261+
}
262+
263+
.hero-event-copy {
264+
flex: 1 1 0;
265+
min-width: 0;
266+
max-width: 600px;
267+
}
268+
269+
.hero-event-kicker {
270+
display: inline-block;
271+
font-size: 0.75rem;
272+
font-weight: 700;
273+
letter-spacing: 0.1em;
274+
text-transform: uppercase;
275+
color: $accent;
276+
border: 1px solid rgba($accent, 0.25);
277+
background: $accent-glow;
278+
padding: 0.375rem 1rem;
279+
border-radius: 100px;
280+
margin-bottom: 1.5rem;
281+
}
282+
283+
.hero-event-title {
284+
font-size: clamp(2.5rem, 6vw, 4.75rem);
285+
font-weight: 900;
286+
letter-spacing: -0.03em;
287+
line-height: 1.05;
288+
margin: 0 0 1.25rem;
289+
color: $white;
290+
291+
.hero-event-x { font-weight: 400; color: #8791a6; }
292+
.hero-event-dot { color: #ffe655; }
293+
.gradient-text {
294+
background: linear-gradient(135deg, $accent 0%, #60a5fa 50%, #a78bfa 100%);
295+
-webkit-background-clip: text;
296+
-webkit-text-fill-color: transparent;
297+
background-clip: text;
298+
}
299+
}
300+
301+
.hero-event-desc {
302+
font-size: clamp(1rem, 1.8vw, 1.125rem);
303+
line-height: 1.7;
304+
color: $muted;
305+
margin: 0 0 2rem;
306+
max-width: 540px;
307+
}
308+
309+
.hero-actions--left { justify-content: flex-start; }
310+
311+
.hero-event-art {
312+
flex: 1 1 0;
313+
min-width: 0;
314+
max-width: 560px;
315+
316+
img { display: block; width: 100%; height: auto; }
317+
}
318+
319+
.hero-controls {
320+
position: relative;
321+
z-index: 2;
322+
display: flex;
323+
align-items: center;
324+
justify-content: center;
325+
gap: 0.75rem;
326+
margin-top: 1.5rem;
327+
}
328+
329+
.hero-dots { display: inline-flex; gap: 0.5rem; }
330+
331+
.hero-dot {
332+
width: 10px;
333+
height: 10px;
334+
padding: 0;
335+
border: 0;
336+
border-radius: 50%;
337+
background: rgba($white, 0.25);
338+
cursor: pointer;
339+
transition: background 0.2s ease, transform 0.2s ease;
340+
341+
&:hover { background: rgba($accent, 0.6); }
342+
&.is-active { background: $accent; transform: scale(1.2); }
343+
}
344+
345+
.hero-ctrl {
346+
width: 34px;
347+
height: 34px;
348+
border: 1px solid rgba($accent, 0.3);
349+
border-radius: 50%;
350+
font-size: 1.125rem;
351+
line-height: 1;
352+
color: $accent;
353+
background: $accent-glow;
354+
cursor: pointer;
355+
transition: background 0.2s ease;
356+
357+
&:hover, &:focus-visible { background: rgba($accent, 0.22); }
358+
&--pause { font-size: 0.625rem; letter-spacing: -1px; margin-left: 0.5rem; }
359+
}
360+
361+
@media (max-width: $break-lg) {
362+
.hero-carousel .hero-slide--event {
363+
flex-direction: column;
364+
text-align: center;
365+
padding: 0;
366+
}
367+
368+
.hero-event-copy { max-width: 640px; }
369+
.hero-event-desc { margin-left: auto; margin-right: auto; }
370+
.hero-actions--left { justify-content: center; }
371+
.hero-event-art { max-width: 380px; width: 100%; }
372+
}
373+
374+
@media (max-width: $break-md) {
375+
.hero-event-title { font-size: clamp(2.25rem, 11vw, 3rem); }
376+
.hero-event-art { max-width: 300px; }
377+
}
378+
379+
// On phones the illustration would push the buttons and the slide controls below the
380+
// first screen, so the event slide is text only there.
381+
@media (max-width: 640px) {
382+
.hero-event-art { display: none; }
383+
.hero-carousel .hero-controls { margin-top: 1.25rem; }
384+
}

‎assets/scss/_light-theme.scss‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@
8484

8585
.hero-title { color: #0f172a; }
8686
.hero-description { color: #64748b; }
87+
.hero-event-title { color: #0f172a; .hero-event-x { color: #64748b; } }
88+
.hero-event-desc { color: #64748b; }
89+
.hero-event-kicker { color: #0272a5; background: rgba(0, 200, 255, 0.08); border-color: rgba(0, 200, 255, 0.3); }
90+
.hero-dot { background: rgba(15, 23, 42, 0.32); &.is-active { background: #0272a5; } }
91+
.hero-ctrl { color: #0272a5; background: rgba(0, 200, 255, 0.08); border-color: rgba(0, 200, 255, 0.3); }
8792

8893
.page-layout {
8994
.page-intro { color: #64748b; border-bottom-color: #dde3ed; }

‎layouts/index.html‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,107 @@
4949

5050
{{ $slack := .Site.Params.social.slack }}
5151

52-
<!-- Hero -->
53-
<section class="hero hero--minimal">
54-
<h1 class="hero-title">The Future of the <span class="gradient-text">MySQL Ecosystem</span></h1>
55-
<p class="hero-description">A vendor-neutral nonprofit bringing together the MySQL ecosystem to share knowledge, collaborate openly, and build a strong, sustainable future.</p>
56-
<div class="hero-actions">
57-
<a href="/community/get-involved/" class="btn btn-primary">Join the Foundation</a>
58-
<a href="/resources/" class="btn btn-outline">Explore the ecosystem →</a>
52+
<!-- Hero carousel: the upcoming-event announcement first (when a page is flagged
53+
home_featured and still ahead), then the mission statement. With only one slide it
54+
renders as a plain hero, without controls. -->
55+
{{ $slideCount := 1 }}{{ if $featured }}{{ $slideCount = 2 }}{{ end }}
56+
<section class="hero hero--minimal hero-carousel" data-count="{{ $slideCount }}" aria-roledescription="carousel" aria-label="Featured">
57+
<div class="hero-slides">
58+
{{ with $featured }}
59+
{{ $art := .Resources.Get "hero-illustration.svg" }}
60+
<div class="hero-slide hero-slide--event is-active" role="group" aria-roledescription="slide" aria-label="1 of 2">
61+
<div class="hero-event-copy">
62+
<span class="hero-event-kicker">Upcoming event · {{ .Date.Format "January 2, 2006" }}{{ with .Params.home_meta }} · {{ . }}{{ end }}</span>
63+
<h2 class="hero-event-title">DuckDB <span class="hero-event-x">×</span> MySQL <span class="gradient-text">Day</span><span class="hero-event-dot">.</span></h2>
64+
<p class="hero-event-desc">{{ .Params.hero_description | default .Description }}</p>
65+
<div class="hero-actions hero-actions--left">
66+
<a href="{{ .RelPermalink }}#register" class="btn btn-primary">Register for free</a>
67+
<a href="{{ .RelPermalink }}#schedule" class="btn btn-outline">See the schedule →</a>
68+
</div>
69+
</div>
70+
{{ with $art }}
71+
<div class="hero-event-art"><img src="{{ .RelPermalink }}" alt="MySQL and DuckDB cards connected by data flows" width="800" height="790" decoding="async"></div>
72+
{{ end }}
73+
</div>
74+
{{ end }}
75+
<div class="hero-slide hero-slide--mission{{ if not $featured }} is-active{{ end }}"{{ if $featured }} role="group" aria-roledescription="slide" aria-label="2 of 2"{{ end }}>
76+
<h1 class="hero-title">The Future of the <span class="gradient-text">MySQL Ecosystem</span></h1>
77+
<p class="hero-description">A vendor-neutral nonprofit bringing together the MySQL ecosystem to share knowledge, collaborate openly, and build a strong, sustainable future.</p>
78+
<div class="hero-actions">
79+
<a href="/community/get-involved/" class="btn btn-primary">Join the Foundation</a>
80+
<a href="/resources/" class="btn btn-outline">Explore the ecosystem →</a>
81+
</div>
82+
</div>
5983
</div>
84+
{{ if $featured }}
85+
<div class="hero-controls">
86+
<button type="button" class="hero-ctrl hero-ctrl--prev" aria-label="Previous slide" hidden>‹</button>
87+
<div class="hero-dots" role="tablist" aria-label="Choose a slide"></div>
88+
<button type="button" class="hero-ctrl hero-ctrl--next" aria-label="Next slide" hidden>›</button>
89+
<button type="button" class="hero-ctrl hero-ctrl--pause" aria-label="Pause automatic slide change" hidden>❚❚</button>
90+
</div>
91+
<script>
92+
(function () {
93+
var root = document.querySelector('.hero-carousel');
94+
if (!root) return;
95+
var slides = [].slice.call(root.querySelectorAll('.hero-slide'));
96+
if (slides.length < 2) return;
97+
var dots = root.querySelector('.hero-dots');
98+
var prev = root.querySelector('.hero-ctrl--prev'), next = root.querySelector('.hero-ctrl--next'), pause = root.querySelector('.hero-ctrl--pause');
99+
var i = 0, timer = null, userPaused = false, hovering = false;
100+
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
101+
var buttons = slides.map(function (s, k) {
102+
var b = document.createElement('button');
103+
b.type = 'button'; b.className = 'hero-dot'; b.setAttribute('role', 'tab');
104+
b.setAttribute('aria-label', 'Slide ' + (k + 1) + ' of ' + slides.length);
105+
b.addEventListener('click', function () { show(k); restart(); });
106+
dots.appendChild(b); return b;
107+
});
108+
function show(k) {
109+
i = (k + slides.length) % slides.length;
110+
slides.forEach(function (s, n) {
111+
var on = n === i;
112+
s.classList.toggle('is-active', on);
113+
s.setAttribute('aria-hidden', on ? 'false' : 'true');
114+
[].forEach.call(s.querySelectorAll('a, button'), function (el) { el.tabIndex = on ? 0 : -1; });
115+
buttons[n].setAttribute('aria-selected', on ? 'true' : 'false');
116+
buttons[n].classList.toggle('is-active', on);
117+
});
118+
}
119+
function stop() { if (timer) { clearInterval(timer); timer = null; } }
120+
function start() { stop(); if (!reduce && !userPaused && !hovering && !document.hidden) timer = setInterval(function () { show(i + 1); }, 8000); }
121+
function restart() { start(); }
122+
prev.hidden = next.hidden = false;
123+
prev.addEventListener('click', function () { show(i - 1); restart(); });
124+
next.addEventListener('click', function () { show(i + 1); restart(); });
125+
if (!reduce) {
126+
pause.hidden = false;
127+
pause.addEventListener('click', function () {
128+
userPaused = !userPaused;
129+
pause.textContent = userPaused ? '▶' : '❚❚';
130+
pause.setAttribute('aria-label', userPaused ? 'Resume automatic slide change' : 'Pause automatic slide change');
131+
start();
132+
});
133+
}
134+
root.addEventListener('mouseenter', function () { hovering = true; stop(); });
135+
root.addEventListener('mouseleave', function () { hovering = false; start(); });
136+
root.addEventListener('focusin', function () { hovering = true; stop(); });
137+
root.addEventListener('focusout', function () { hovering = false; start(); });
138+
document.addEventListener('visibilitychange', start);
139+
var x0 = null;
140+
root.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true });
141+
root.addEventListener('touchend', function (e) {
142+
if (x0 === null) return;
143+
var dx = e.changedTouches[0].clientX - x0;
144+
if (Math.abs(dx) > 50) { show(i + (dx < 0 ? 1 : -1)); restart(); }
145+
x0 = null;
146+
});
147+
root.classList.add('is-ready');
148+
show(0);
149+
start();
150+
})();
151+
</script>
152+
{{ end }}
60153
</section>
61154

62155
<!-- What We Do -->

0 commit comments

Comments
 (0)