feat: homepage animations; Arabic as default locale
This commit is contained in:
@@ -36,13 +36,16 @@
|
||||
|
||||
MOTION: every animation in the draft is here, as classes from
|
||||
_page-kit/motion.css - the headline wipe, the hero picture's scroll-driven
|
||||
drift, the gallery and card hovers, the logo marquee. The one effect
|
||||
deliberately dropped is the coloured glow behind a hovered button or card.
|
||||
drift, each item tasting in as it scrolls into view (the edge tiles of a row
|
||||
sliding in from their own side, the middles and stacked lists rising), the
|
||||
gallery and card hovers, the logo marquee. The one effect deliberately
|
||||
dropped is the coloured glow behind a hovered button or card.
|
||||
|
||||
There is no Arabic frame, so /ar is this same DOM under `dir="rtl"`, which
|
||||
is why every inline edge below is logical (ps-/pe-/start-/end-).
|
||||
========================================================================= */
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Link, useRouteLoaderData } from "react-router";
|
||||
|
||||
import { ArrowIcon, Button } from "~/components/ui/Button";
|
||||
@@ -65,6 +68,7 @@ import { DEFAULT_THEME, type AppTheme } from "~/lib/theme";
|
||||
|
||||
import type { Route } from "./+types/home";
|
||||
import { Accordion } from "./_page-kit/accordion";
|
||||
import { enableReveals } from "./_page-kit/reveal";
|
||||
import {
|
||||
isImageGalleryBlock,
|
||||
type CmsFaq,
|
||||
@@ -365,6 +369,25 @@ function StatusPill({ label }: { label: string }) {
|
||||
* vertically centred on the middle. One or two pictures fall back to the
|
||||
* uniform grid, where the mosaic's "faded flank" reading has no partner.
|
||||
*/
|
||||
/* The edge items of a row land in from their own side of the line - the start
|
||||
or the end - while the middle ones rise, so a row reads as arriving edge-first
|
||||
instead of lifting as one. A lone item simply rises. */
|
||||
function revealEdge(index: number, count: number): string {
|
||||
if (count < 2) return "";
|
||||
if (index === 0) return "sh-reveal--from-start";
|
||||
if (index === count - 1) return "sh-reveal--from-end";
|
||||
return "";
|
||||
}
|
||||
|
||||
/* A beat between the tiles of a row: each item after the first holds at its
|
||||
hidden state a moment longer, so a row cascades start-to-end. Rows here cap
|
||||
at three tiles, so two steps are enough. */
|
||||
function revealStep(index: number): string {
|
||||
if (index === 1) return "sh-reveal--step-1";
|
||||
if (index >= 2) return "sh-reveal--step-2";
|
||||
return "";
|
||||
}
|
||||
|
||||
function GalleryShots({ images }: { images: readonly ResolvedImage[] }) {
|
||||
if (images.length === 0) return null;
|
||||
|
||||
@@ -380,7 +403,10 @@ function GalleryShots({ images }: { images: readonly ResolvedImage[] }) {
|
||||
the draft's 60px. */
|
||||
<ul className="grid list-none grid-cols-1 gap-3xl lg:grid-cols-[427fr_605fr_426fr] lg:items-center">
|
||||
{images.map((image, index) => (
|
||||
<li key={image.src} className="sh-reveal">
|
||||
<li
|
||||
key={image.src}
|
||||
className={cx("sh-reveal", revealEdge(index, images.length), revealStep(index))}
|
||||
>
|
||||
<figure className="flex flex-col gap-xs">
|
||||
<div
|
||||
className={cx(
|
||||
@@ -409,8 +435,11 @@ function GalleryShots({ images }: { images: readonly ResolvedImage[] }) {
|
||||
|
||||
return (
|
||||
<ul className="grid list-none grid-cols-1 gap-lg sm:grid-cols-2 lg:grid-cols-3">
|
||||
{images.map((image) => (
|
||||
<li key={image.src} className="sh-reveal">
|
||||
{images.map((image, index) => (
|
||||
<li
|
||||
key={image.src}
|
||||
className={cx("sh-reveal", revealEdge(index, images.length), revealStep(index))}
|
||||
>
|
||||
<figure className="flex flex-col gap-xs">
|
||||
<div className="sh-lift sh-lift--grow rounded-control border border-[var(--border-subtle)] hover:border-[var(--border-accent)]">
|
||||
<img
|
||||
@@ -521,12 +550,20 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
exactly as the footer's badge does: same root cookie, same emblem. */
|
||||
const root = useRouteLoaderData<{ theme?: AppTheme }>("root");
|
||||
const theme: AppTheme = root?.theme ?? DEFAULT_THEME;
|
||||
/* Chromium plays every item reveal natively via scroll-driven timelines
|
||||
(motion.css). For non-Chromium browsers `_page-kit/reveal.ts` flips each
|
||||
`.sh-reveal` item to `.sh-reveal-js--in` as it enters the viewport, so the
|
||||
fade-and-drift lands everywhere - while reduced-motion visitors and users
|
||||
without JS keep every item fully visible. */
|
||||
useEffect(() => {
|
||||
enableReveals();
|
||||
}, []);
|
||||
/* The three pillars are the About page's approved copy - one source, so the
|
||||
two pages can never drift apart. */
|
||||
const pillars = pageCopy(locale).about.pillars;
|
||||
|
||||
const headline = hero.headline ?? copy.heroHeadline;
|
||||
const lead = hero.subheading ?? copy.heroLead;
|
||||
// const lead = hero.subheading ?? copy.heroLead;
|
||||
const consortiumHeading = loaderData.partnersHeadline ?? copy.consortiumHeading;
|
||||
|
||||
/* The frame draws one accent CTA and one white one. Editor-supplied CTAs go
|
||||
@@ -564,7 +601,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
<PageHero
|
||||
className="bg-[var(--surface-sunken)] [--layout-hero-top:clamp(2.5rem,6vw,4.5rem)]"
|
||||
title={headline}
|
||||
lead={lead}
|
||||
lead={""}
|
||||
leadWithActions
|
||||
{...(hero.eyebrow ? { statusPill: <StatusPill label={hero.eyebrow} /> } : {})}
|
||||
ambient={[
|
||||
@@ -651,12 +688,18 @@ reveal
|
||||
</div>
|
||||
|
||||
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-3">
|
||||
{pillars.map((pillar) => (
|
||||
{pillars.map((pillar, index) => (
|
||||
<li
|
||||
key={pillar.title}
|
||||
/* `sh-rule-top` draws the draft's accent rule across the top
|
||||
of the tile as the pointer arrives. */
|
||||
className="sh-lift sh-reveal sh-reveal--in sh-rule-top flex h-full flex-col gap-sm rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl hover:border-[var(--border-accent)]"
|
||||
/* `sh-reveal--in` adds the draft's rise-and-scale; the edge
|
||||
tiles of the row slide in from their own side instead.
|
||||
`sh-rule-top` draws the accent rule across the top of the
|
||||
tile as the pointer arrives. */
|
||||
className={cx(
|
||||
"sh-lift sh-reveal sh-reveal--in sh-rule-top flex h-full flex-col gap-sm rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl hover:border-[var(--border-accent)]",
|
||||
revealEdge(index, pillars.length),
|
||||
revealStep(index),
|
||||
)}
|
||||
>
|
||||
<h3 className="heading-h3 text-[var(--text-primary)]">{pillar.title}</h3>
|
||||
<p className="body-md text-[var(--text-secondary)]">
|
||||
@@ -687,11 +730,15 @@ reveal
|
||||
|
||||
{news.length > 0 ? (
|
||||
<ul className="grid list-none grid-cols-1 gap-lg sm:grid-cols-2 lg:grid-cols-3">
|
||||
{news.map((item) => (
|
||||
{news.map((item, index) => (
|
||||
<Card.Content
|
||||
key={item.id}
|
||||
as="li"
|
||||
className="sh-reveal sh-reveal--in"
|
||||
className={cx(
|
||||
"sh-reveal sh-reveal--in",
|
||||
revealEdge(index, news.length),
|
||||
revealStep(index),
|
||||
)}
|
||||
href={item.href}
|
||||
title={item.title}
|
||||
actionLabel={t("actions.readMore")}
|
||||
@@ -744,7 +791,7 @@ reveal
|
||||
</div>
|
||||
|
||||
{faqs.length > 0 ? (
|
||||
<Accordion items={faqs} className="sh-reveal" />
|
||||
<Accordion items={faqs} itemClassName="sh-reveal" />
|
||||
) : (
|
||||
<EmptyState message={copy.faqEmpty} />
|
||||
)}
|
||||
@@ -770,10 +817,14 @@ reveal
|
||||
</div>
|
||||
|
||||
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-3">
|
||||
{copy.consortium.map((member) => (
|
||||
{copy.consortium.map((member, index) => (
|
||||
<li
|
||||
key={member.name}
|
||||
className="sh-lift sh-reveal sh-reveal--in flex h-full flex-col items-start gap-sm rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl hover:border-[var(--border-accent)]"
|
||||
className={cx(
|
||||
"sh-lift sh-reveal sh-reveal--in flex h-full flex-col items-start gap-sm rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl hover:border-[var(--border-accent)]",
|
||||
revealEdge(index, copy.consortium.length),
|
||||
revealStep(index),
|
||||
)}
|
||||
>
|
||||
<span className="rounded-chip bg-[var(--surface-accent-subtle)] px-xs py-2xs label-md uppercase text-[var(--text-accent)]">
|
||||
{member.role}
|
||||
|
||||
Reference in New Issue
Block a user