feat(hero): rotate three positioning lines on a 10s cycle
All three lines render in the DOM; CSS drives the fade via data-active. Reduced-motion users see the first line only (no interval, no fade). Rotation pauses on hover and keyboard focus. aria-live=off on the rotator so AT does not announce every swap; aria-hidden flips per-swap to avoid duplicate heading announcements. Also set vite.build.assetsInlineLimit=0 in astro.config.mjs so Astro emits the rotator script as a same-origin external file (dist/assets/) rather than inlining it — required for CSP script-src 'self' compliance.
This commit is contained in:
@@ -19,6 +19,11 @@ export default defineConfig({
|
|||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
cssMinify: 'lightningcss',
|
cssMinify: 'lightningcss',
|
||||||
|
// Prevent Astro from inlining small scripts into the HTML.
|
||||||
|
// Without this, the hero rotator script (< 4 KB) gets inlined as a
|
||||||
|
// <script type="module"> tag, which violates CSP script-src 'self'
|
||||||
|
// (no 'unsafe-inline'). Setting 0 forces all scripts to external files.
|
||||||
|
assetsInlineLimit: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ import TopographicBg from '../TopographicBg.astro';
|
|||||||
Your camels called. They want a GPS.
|
Your camels called. They want a GPS.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-display font-bold text-text mb-6">
|
<h1
|
||||||
Run Apache Camel without running Apache Camel.
|
class="text-display font-bold text-text mb-6 hero-rotator"
|
||||||
|
aria-live="off"
|
||||||
|
data-hero-rotator
|
||||||
|
>
|
||||||
|
<span class="hero-line" data-active aria-hidden="false">Run Apache Camel without running Apache Camel.</span>
|
||||||
|
<span class="hero-line" aria-hidden="true">Camel integrations, minus the baggage.</span>
|
||||||
|
<span class="hero-line" aria-hidden="true">Your camels, our caravan. You just ride.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-text-muted max-w-prose leading-relaxed mb-10">
|
<p class="text-lg md:text-xl text-text-muted max-w-prose leading-relaxed mb-10">
|
||||||
The hosted home for your Camel integrations — with deep tracing, replay, and live control built in. Because you chose Camel to stay free, not to stay up all night.
|
The hosted home for your Camel integrations — with deep tracing, replay, and live control built in. Because you chose Camel to stay free, not to stay up all night.
|
||||||
@@ -33,3 +39,57 @@ import TopographicBg from '../TopographicBg.astro';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero-rotator {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
/* Reserve height for the tallest line so no layout shift on swap.
|
||||||
|
Two lines at current H1 size handles all three on most viewports. */
|
||||||
|
min-height: 2.2em;
|
||||||
|
}
|
||||||
|
.hero-line {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 700ms ease-in-out;
|
||||||
|
/* Stack all lines on top of each other — only [data-active] is visible. */
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
.hero-line[data-active] {
|
||||||
|
opacity: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.hero-line {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Hero rotating headline. Bundled by Astro (CSP: script-src 'self').
|
||||||
|
const rotator = document.querySelector<HTMLElement>('[data-hero-rotator]');
|
||||||
|
if (rotator) {
|
||||||
|
const lines = Array.from(rotator.querySelectorAll<HTMLElement>('.hero-line'));
|
||||||
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
if (!reduced && lines.length > 1) {
|
||||||
|
let index = 0;
|
||||||
|
let paused = false;
|
||||||
|
const pause = () => { paused = true; };
|
||||||
|
const resume = () => { paused = false; };
|
||||||
|
rotator.addEventListener('mouseenter', pause);
|
||||||
|
rotator.addEventListener('mouseleave', resume);
|
||||||
|
rotator.addEventListener('focusin', pause);
|
||||||
|
rotator.addEventListener('focusout', resume);
|
||||||
|
setInterval(() => {
|
||||||
|
if (paused) return;
|
||||||
|
lines[index].removeAttribute('data-active');
|
||||||
|
lines[index].setAttribute('aria-hidden', 'true');
|
||||||
|
index = (index + 1) % lines.length;
|
||||||
|
lines[index].setAttribute('data-active', '');
|
||||||
|
lines[index].setAttribute('aria-hidden', 'false');
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user