Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions themes/buildpacks/assets/js/how-it-works.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,65 @@ const round = (n) => Math.round(n * 100) / 100;
const root = document.querySelector('[data-hiw]');
if (root) init(root);

const topButton = document.querySelector('[data-hiw-top]');
if (topButton) initBackToTop(topButton);

// Floating button that returns to the top; its ring fills with page progress
function initBackToTop(button) {
const ring = button.querySelector('[data-hiw-top-progress]');
const hero = document.querySelector('.hiw-hero');
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
let target = 0;
let shown = 0;
let running = false;
let then = 0;

const paint = () => {
if (ring) ring.style.strokeDashoffset = (1 - shown).toFixed(4);
};

// The ring glides towards the scroll position instead of jumping with
// each wheel step
function frame(now) {
const dt = clamp((now - then) / 1000, 0, 0.1);
then = now;
shown += (target - shown) * (1 - Math.exp(-dt * 8));
if (reduced.matches || Math.abs(target - shown) < 0.0005) shown = target;
paint();
if (shown !== target) requestAnimationFrame(frame);
else running = false;
}

function update() {
const scrolled = window.scrollY;
const max = document.documentElement.scrollHeight - window.innerHeight;
const threshold = hero ? hero.offsetTop + hero.offsetHeight : window.innerHeight * 0.5;
button.classList.toggle('is-visible', scrolled > threshold);
target = clamp(max > 0 ? scrolled / max : 0);
if (!running) {
running = true;
then = performance.now();
requestAnimationFrame(frame);
}
}

window.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update, { passive: true });
update();
// Start from where the page opened, e.g. a reload part-way down
shown = target;
paint();

button.addEventListener('click', (event) => {
event.preventDefault();
window.scrollTo({ top: 0, behavior: reduced.matches ? 'auto' : 'smooth' });
if (location.hash) history.replaceState(null, '', location.pathname + location.search);
// Return keyboard focus to the start of the page
const target = document.querySelector('a[href], button') || document.body;
target.focus({ preventScroll: true });
});
}

function init(root) {
const steps = Array.from(root.querySelectorAll('[data-hiw-step]'));
const railItems = Array.from(root.querySelectorAll('[data-hiw-rail]'));
Expand Down
104 changes: 103 additions & 1 deletion themes/buildpacks/assets/scss/components/_how-it-works.scss
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,93 @@ $hiw-red: #D9363E;
}
}

// Back to top: a floating cube-blue button whose ring tracks page progress
&-top {
position: fixed;
right: 1.5rem;
bottom: 1.5rem;
z-index: 1030;
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: linear-gradient(145deg, $blue-light, $blue-dark);
box-shadow: 0 8px 22px rgba($blue-dark, 0.28);
opacity: 0;
visibility: hidden;
transform: translateY(20px) scale(0.8);
transition:
opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.55s cubic-bezier(0.22, 1, 0.36, 1),
visibility 0.45s,
box-shadow 0.35s ease;

&.is-visible {
opacity: 1;
visibility: visible;
transform: none;
}

&:hover,
&:focus-visible {
box-shadow: 0 12px 28px rgba($pink, 0.35);
transform: translateY(-3px);

.hiw-top__arrow {
transform: translateY(-3px) rotate(45deg);
}

.hiw-top__track {
stroke: rgba($white, 0.35);
}
}

&:focus-visible {
outline: 2px solid $pink;
outline-offset: 3px;
}

&__ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
}

&__track,
&__fill {
fill: none;
stroke-width: 3;
}

&__track {
stroke: rgba($white, 0.18);
transition: stroke 0.25s ease;
}

&__fill {
stroke: $pink;
stroke-linecap: round;
stroke-dasharray: 1;
stroke-dashoffset: 1;
}

// Chevron pointing up, nudged down so it sits optically centred
&__arrow {
width: 12px;
height: 12px;
margin-top: 5px;
border-top: 2.5px solid $white;
border-left: 2.5px solid $white;
border-radius: 2px 0 0 0;
transform: rotate(45deg);
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
}

// Phones and tablets: the scene fills the screen and the steps
// scroll over it as cards
@media (max-width: $hiw-compact) {
Expand Down Expand Up @@ -502,6 +589,19 @@ $hiw-red: #D9363E;
}
}

&-top {
right: 1rem;
bottom: 1rem;
width: 44px;
height: 44px;

&__arrow {
width: 10px;
height: 10px;
margin-top: 4px;
}
}

&-rail {
ol {
flex-wrap: nowrap;
Expand All @@ -521,7 +621,9 @@ $hiw-red: #D9363E;
animation: none !important;
}

&--ready .hiw-step__card {
&--ready .hiw-step__card,
&-top,
&-top__arrow {
transition: none;
}
}
Expand Down
10 changes: 9 additions & 1 deletion themes/buildpacks/layouts/_default/how-it-works.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{{ define "main" }}

<div class="hiw">
<section class="hiw-hero">
<section id="top" class="hiw-hero">
<div class="container">
<p class="hiw-hero__eyebrow">Under the hood</p>
<h1>How Buildpacks Work</h1>
Expand Down Expand Up @@ -84,6 +84,14 @@ <h3 class="mb-2">Rebase</h3>
</div>
</div>
</section>

<a class="hiw-top" href="#top" aria-label="Back to top" data-hiw-top>
<svg class="hiw-top__ring" viewBox="0 0 52 52" aria-hidden="true" focusable="false">
<circle class="hiw-top__track" cx="26" cy="26" r="24"></circle>
<circle class="hiw-top__fill" cx="26" cy="26" r="24" pathLength="1" data-hiw-top-progress></circle>
</svg>
<span class="hiw-top__arrow" aria-hidden="true"></span>
</a>
</div>

{{ $script := resources.Get "js/how-it-works.js" | minify | fingerprint }}
Expand Down
Loading