feat: homepage animations; Arabic as default locale
This commit is contained in:
@@ -40,6 +40,8 @@ export interface AccordionProps {
|
||||
*/
|
||||
defaultOpenIndex?: number | null;
|
||||
className?: string;
|
||||
/** Extra classes for each question row, e.g. a scroll-driven reveal. */
|
||||
itemClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +77,12 @@ function ToggleMark({ open }: { open: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Accordion({ items, defaultOpenIndex = 0, className }: AccordionProps) {
|
||||
export function Accordion({
|
||||
items,
|
||||
defaultOpenIndex = 0,
|
||||
className,
|
||||
itemClassName,
|
||||
}: AccordionProps) {
|
||||
const baseId = useId();
|
||||
const initial =
|
||||
defaultOpenIndex === null ? null : (items[defaultOpenIndex]?.id ?? null);
|
||||
@@ -97,6 +104,7 @@ export function Accordion({ items, defaultOpenIndex = 0, className }: AccordionP
|
||||
"border-b border-[var(--border-default)]",
|
||||
"transition-colors duration-[var(--motion-duration-base)]",
|
||||
"hover:bg-[var(--surface-muted)]",
|
||||
itemClassName,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -268,6 +268,39 @@
|
||||
animation-range: entry 0% entry 80%;
|
||||
}
|
||||
|
||||
/* The edge tiles of a row land in from their own side of the line - the
|
||||
reading start and the reading end - instead of rising, so a row drags
|
||||
into place edge-first while its middle rises. `translateX` has no
|
||||
logical keyword, so the RTL block swaps the two names: the drift always
|
||||
comes off the start edge, whichever script is on screen. */
|
||||
.sh-reveal--from-start {
|
||||
animation-name: sh-rise-from-start;
|
||||
}
|
||||
|
||||
.sh-reveal--from-end {
|
||||
animation-name: sh-rise-from-end;
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-reveal--from-start {
|
||||
animation-name: sh-rise-from-end;
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-reveal--from-end {
|
||||
animation-name: sh-rise-from-start;
|
||||
}
|
||||
|
||||
/* A beat between the tiles of a row: each item after the first holds at
|
||||
its hidden state until it is a little deeper in the viewport, so a row
|
||||
cascades start-to-end instead of landing as one. Only the start of the
|
||||
range shifts; `.sh-reveal--in` and the edge slides keep their ends. */
|
||||
.sh-reveal--step-1 {
|
||||
animation-range-start: entry 12%;
|
||||
}
|
||||
|
||||
.sh-reveal--step-2 {
|
||||
animation-range-start: entry 24%;
|
||||
}
|
||||
|
||||
/* The hero picture drifting as the page scrolls past it. It is scaled up
|
||||
before it travels, so the drift can never uncover an edge of the
|
||||
frame. Chromium-only, like every timeline here: elsewhere the picture
|
||||
@@ -306,6 +339,49 @@
|
||||
transform-origin: right center;
|
||||
}
|
||||
}
|
||||
|
||||
/* THE SAME REVEAL, WITHOUT A TIMELINE. `_page-kit/reveal.ts` tags a
|
||||
`.sh-reveal` element `.sh-reveal-js` (hidden) in browsers that lack
|
||||
scroll timelines, then `.sh-reveal-js--in` plays `sh-rise` once it
|
||||
scrolls into view. Both classes are inert without the helper, so
|
||||
content is always readable. */
|
||||
.sh-reveal-js {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sh-reveal-js--in {
|
||||
animation: sh-rise var(--motion-duration-reveal)
|
||||
var(--motion-ease-entrance) both;
|
||||
}
|
||||
|
||||
/* The edge tiles keep their drift here too: the reader who never sees a
|
||||
scroll timeline still gets the same from-start / from-end arrival. The
|
||||
RTL mirrors swap the names, as the Chromium block above does. */
|
||||
.sh-reveal--from-start.sh-reveal-js--in {
|
||||
animation-name: sh-rise-from-start;
|
||||
}
|
||||
|
||||
.sh-reveal--from-end.sh-reveal-js--in {
|
||||
animation-name: sh-rise-from-end;
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-reveal--from-start.sh-reveal-js--in {
|
||||
animation-name: sh-rise-from-end;
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-reveal--from-end.sh-reveal-js--in {
|
||||
animation-name: sh-rise-from-start;
|
||||
}
|
||||
|
||||
/* The same beat, expressed in time where there is no timeline. `both`
|
||||
already answers for the hidden hold during the delay. */
|
||||
.sh-reveal--step-1.sh-reveal-js--in {
|
||||
animation-delay: var(--motion-duration-fast);
|
||||
}
|
||||
|
||||
.sh-reveal--step-2.sh-reveal-js--in {
|
||||
animation-delay: calc(var(--motion-duration-fast) * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +409,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* The same reveal turned sideways. `--from-start` is picked up by the RTL
|
||||
block as the mirror of `--from-end`, so both names exist and the swap is
|
||||
legible there instead of shipping one keyframe and praying. */
|
||||
@keyframes sh-rise-from-start {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px) scale(0.985);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-rise-from-end {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px) scale(0.985);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-bar-grow {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Reveal-on-scroll for browsers without scroll-driven timelines.
|
||||
----------------------------------------------------------------------------
|
||||
motion.css handles `.sh-reveal` with `animation-timeline: view()`, which is
|
||||
Chromium-only; elsewhere every band sits at rest, fully visible. This helper
|
||||
gives those browsers the same `.sh-rise` reveal: it tags each `.sh-reveal`
|
||||
element `.sh-reveal-js` (hidden), then flips it to `.sh-reveal-js--in` (the
|
||||
animation runs) once it scrolls into view.
|
||||
|
||||
It is a progressive enhancement and is strictly opt-in per page:
|
||||
- Chromium already animates via CSS, so this returns early and never
|
||||
fights the scroll-driven version.
|
||||
- Reduced-motion visitors are left alone (the CSS never hides them).
|
||||
- With JS off, no classes are ever added, so content stays visible.
|
||||
|
||||
Renders nothing on the server; call from a `useEffect` in the page. */
|
||||
|
||||
const SELECTOR = ".sh-reveal";
|
||||
|
||||
export function enableReveals(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("IntersectionObserver" in window)) return;
|
||||
if ("CSS" in window && CSS.supports("animation-timeline: view()")) {
|
||||
/* Chromium already plays the reveal; nothing to add. */
|
||||
return;
|
||||
}
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
const element = entry.target;
|
||||
element.classList.add("sh-reveal-js--in");
|
||||
element.classList.remove("sh-reveal-js");
|
||||
observer.unobserve(element);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
|
||||
);
|
||||
|
||||
for (const element of document.querySelectorAll<HTMLElement>(SELECTOR)) {
|
||||
element.classList.add("sh-reveal-js");
|
||||
observer.observe(element);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user