Rebuild the site to the Claude Design drafts
Carries the 11 page drafts plus the shared SiteNav and SiteFooter onto the project's own design system: tier-2 tokens, the published type roles, logical properties throughout, and both locales. Surfaces - The page stands on #0B0B0A with #1D1D1B bands lifted off it, as the drafts do; the body previously painted the band colour, so every band was a step too light. - A card is drawn one step off whatever it stands on (#1D1D1B on the ground, #141413 inside a band). Components paint --surface-card and a band re-points it for its subtree, so a card can never come out the same colour as its band. Shell - Nav: translucent ground with a blur, chevrons on group triggers, an accent rule under the current section, the drafts' compact dropdown card, and an EN | AR toggle in place of the globe menu. - Footer: the drafts' four-column grid, one-line brand text, Facebook as a bordered button, the EU lockup, the implemented-by row and the funding disclaimer. Pages - Home: hero with counting figures, launch band, three areas of change, newsroom, FAQ accordion and the consortium band. - The nine interior pages rebuilt and then audited section by section against their drafts. - Applications: the three-step application form now posts real submissions to the Strapi `application` type, validated server-side and working without JavaScript. Motion - Every animation in the drafts, as classes in _page-kit/motion.css: staggered entrances, scroll-driven reveals, the drifting washes (each page carries its own geometry), the sheen, the counting figures and the accordion. The drafts' coloured hover glow is deliberately not shipped. CMS - New `faq` collection type behind the home page's accordion. - `role` added to the consortium member component so an editor can supply the chip and the deliverable separately. Content - No invented figure, date, name or commitment ships: the five-working-day reply, the placeholder email and Facebook handle, the sample news, events, publications and FAQs, the invented application rules and the TBD counters are all withheld pending real content. Figures that survived (3 consortium partners, 4 areas of work, 16 human security sub-projects, Dec 2025 - Dec 2027) are verified against docs/ and the bundled draft copy. Verified: pnpm typecheck (web + CMS) and pnpm --filter web build pass; all 20 page/locale URLs render 200 through the built SSR handler, including with the CMS unreachable; no raw hex or physical-direction utility in components or routes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+230
-90
@@ -1,18 +1,43 @@
|
||||
/* ============================================================================
|
||||
ABOUT - /en/about and /ar/about
|
||||
----------------------------------------------------------------------------
|
||||
Section 1 of docs/SHARPER_Website_Draft_Section_EN_AR.docx: who SHARP-ER is,
|
||||
the three consortium members and their roles, the three focus areas, and the
|
||||
SHARP I / II legacy the project builds on.
|
||||
The redesigned About page ("About", Claude Design draft), carried onto the
|
||||
project's own design system. Five bands, alternating the two dark surfaces
|
||||
the way the draft alternates #0B0B0A and #1D1D1B:
|
||||
|
||||
The draft copy is the fallback; every block here is overridden field by
|
||||
field by api::about-page.about-page as soon as an editor fills it in.
|
||||
Hero eyebrow, headline and lead on the sunken surface, with the
|
||||
two drifting washes behind it.
|
||||
Facts a four-cell seam grid: the run, the coverage, the lead
|
||||
partner and the funder.
|
||||
Nationwide the approved body prose beside the three focus areas.
|
||||
Consortium the three members as role / name / deliverable panels, under
|
||||
a header row that carries the EU funding line.
|
||||
Legacy the SHARP I and II panel with the accent start-edge, and the
|
||||
call to action into What We Do.
|
||||
|
||||
WHAT THE DRAFT SHIPS THAT THIS PAGE DOES NOT
|
||||
--------------------------------------------
|
||||
* Nothing was cut for sourcing: every fact, role and sentence the draft
|
||||
prints is in section 1 of docs/SHARPER_Website_Draft_Section_EN_AR.docx or
|
||||
in the project brief. What the draft does NOT print - a figure of any kind
|
||||
- stays absent, because the draft marks every statistic "TBD".
|
||||
* The draft's coloured hover glow (a green box-shadow under a hovered card)
|
||||
is deliberately dropped; the tiles only rise.
|
||||
|
||||
Every block is the FALLBACK: api::about-page.about-page overrides it field
|
||||
by field as soon as an editor fills it in.
|
||||
|
||||
MOTION: classes from _page-kit/motion.css - the staggered hero entrance, the
|
||||
drifting washes, the scroll-driven band reveals and the hover lift.
|
||||
|
||||
There is no Arabic frame in the draft, so /ar is this same DOM under
|
||||
`dir="rtl"`: every inline edge below is logical (ps-/pe-/start-/end-, or an
|
||||
explicit `border-inline-start-*`), never left/right.
|
||||
========================================================================= */
|
||||
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { ArrowIcon, Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { createTranslator, sectionPath } from "~/i18n";
|
||||
import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo";
|
||||
|
||||
@@ -29,10 +54,11 @@ import {
|
||||
InfoPanel,
|
||||
PageHero,
|
||||
PageSection,
|
||||
Prose,
|
||||
SectionHeader,
|
||||
} from "./_page-kit/sections";
|
||||
|
||||
import "./_page-kit/motion.css";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const locale = routeLocale(params);
|
||||
|
||||
@@ -69,7 +95,10 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
.map((member) => ({
|
||||
id: member.partner?.documentId ?? String(member.id ?? member.partner?.id),
|
||||
name: member.partner?.name ?? "",
|
||||
role: member.roleDescription ?? member.partner?.description ?? "",
|
||||
/* The chip and the line under the name are separate fields, so a
|
||||
CMS-authored member reads exactly like the bundled copy. */
|
||||
role: member.role ?? null,
|
||||
description: member.roleDescription ?? member.partner?.description ?? "",
|
||||
website: member.partner?.website ?? null,
|
||||
})),
|
||||
legacy: {
|
||||
@@ -115,76 +144,179 @@ export function meta({ loaderData, matches }: Route.MetaArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The accent start-edge the draft paints down the left of its closing panel.
|
||||
* Written as arbitrary properties rather than `border-s-4`: the edge needs its
|
||||
* own colour as well as its own width, and `border-inline-start-*` is the only
|
||||
* spelling that mirrors in Arabic without an `rtl:` override.
|
||||
*/
|
||||
const ACCENT_EDGE =
|
||||
"[border-inline-start-color:var(--border-accent)] [border-inline-start-width:4px]";
|
||||
|
||||
/** One member of the consortium, however it reached the page. */
|
||||
interface ConsortiumMember {
|
||||
id: string;
|
||||
name: string;
|
||||
/** The role chip - `consortium.role` in the CMS, or the bundled copy's. */
|
||||
role: string | null;
|
||||
description: string;
|
||||
website: string | null;
|
||||
}
|
||||
|
||||
export default function About({ loaderData }: Route.ComponentProps) {
|
||||
const { locale, hero, body, richTextBlocks, legacy } = loaderData;
|
||||
const t = createTranslator(locale);
|
||||
const copy = pageCopy(locale).about;
|
||||
const whatWeDoCopy = pageCopy(locale).whatWeDo;
|
||||
|
||||
const pillars = loaderData.pillars.length > 0 ? loaderData.pillars : copy.pillars;
|
||||
const consortium =
|
||||
|
||||
const consortium: readonly ConsortiumMember[] =
|
||||
loaderData.consortium.length > 0
|
||||
? loaderData.consortium
|
||||
? loaderData.consortium.map((member) => ({
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
role: member.role,
|
||||
description: member.description,
|
||||
website: member.website,
|
||||
}))
|
||||
: copy.consortium.map((member) => ({
|
||||
id: member.name,
|
||||
name: member.name,
|
||||
role: member.role,
|
||||
description: member.description,
|
||||
website: null,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
className="border-b border-[var(--border-subtle)] bg-[var(--surface-sunken)] [--layout-hero-bottom:clamp(3rem,6vw,4.5rem)]"
|
||||
title={hero.headline ?? copy.heroHeadline}
|
||||
eyebrow={hero.eyebrow ?? t("nav.sections.aboutAndStrategy")}
|
||||
eyebrow={hero.eyebrow ?? copy.heroEyebrow}
|
||||
lead={hero.subheading ?? copy.lead}
|
||||
ambient={[{ top: -260, from: "start", offset: "6%", width: 760, height: 600, durationSeconds: 20 }]}
|
||||
reveal
|
||||
{...(hero.media ? { media: hero.media } : {})}
|
||||
/>
|
||||
|
||||
<PageSection className="pt-0 md:pt-0">
|
||||
<Prose>
|
||||
{body ? (
|
||||
<RichText markdown={body} />
|
||||
) : (
|
||||
<p className="body-md text-[var(--text-secondary)]">{copy.body}</p>
|
||||
)}
|
||||
|
||||
{richTextBlocks.map((block) => (
|
||||
<RichText key={block.id} markdown={block.body} />
|
||||
{/* THE FACTS BAND - four cells divided by a one-pixel seam, which is the
|
||||
band's own background showing between them. Facts, not figures: the
|
||||
run, the coverage, the lead partner and the funder, each written out
|
||||
in the approved draft. */}
|
||||
<PageSection className="border-b border-[var(--border-subtle)] bg-[var(--surface-sunken)] pt-0 pb-0 md:pt-0 md:pb-0">
|
||||
<dl className="grid grid-cols-1 gap-[1px] bg-[var(--border-subtle)] sm:grid-cols-2 lg:grid-cols-4">
|
||||
{copy.facts.map((fact) => (
|
||||
<div
|
||||
key={fact.label}
|
||||
className="sh-reveal flex flex-col gap-2xs bg-[var(--surface-sunken)] p-lg transition-colors duration-[var(--motion-duration-base)] hover:bg-[var(--surface-raised)]"
|
||||
>
|
||||
<dt className="label-md uppercase text-[var(--text-tertiary)]">
|
||||
{fact.label}
|
||||
</dt>
|
||||
<dd className="body-lg-bold text-[var(--text-primary)]">
|
||||
{fact.value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<p className="body-sm-bold text-[var(--text-accent)]">{copy.fundingNote}</p>
|
||||
</Prose>
|
||||
</dl>
|
||||
</PageSection>
|
||||
|
||||
{/* WHERE WE FOCUS */}
|
||||
<PageSection aria-labelledby="about-pillars-heading">
|
||||
<div className="flex flex-col gap-2xl">
|
||||
<SectionHeader id="about-pillars-heading" title={copy.pillarsHeading} />
|
||||
{/* A NATIONWIDE PROJECT, DELIVERED LOCALLY - the approved body prose on
|
||||
the inline-start side, the three focus areas stacked beside it. The
|
||||
band is the raised dark the draft paints (#1d1d1b) and has to say so:
|
||||
the document ground is now the sunken near-black. */}
|
||||
<PageSection
|
||||
aria-labelledby="about-body-heading"
|
||||
band
|
||||
>
|
||||
<div className="grid grid-cols-1 items-start gap-2xl lg:grid-cols-2 lg:gap-4xl">
|
||||
<div className="sh-reveal flex flex-col gap-md">
|
||||
<SectionHeader id="about-body-heading" title={copy.bodyHeading} />
|
||||
|
||||
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-3">
|
||||
{pillars.map((pillar) => (
|
||||
<li key={pillar.title}>
|
||||
<InfoPanel title={pillar.title}>{pillar.description}</InfoPanel>
|
||||
</li>
|
||||
{body ? (
|
||||
<RichText markdown={body} />
|
||||
) : (
|
||||
/* Two paragraphs, as the draft sets them - the run and the
|
||||
consortium first, then what the project does. */
|
||||
copy.body.map((paragraph) => (
|
||||
<p
|
||||
key={paragraph}
|
||||
className="body-md text-[var(--text-secondary)]"
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
))
|
||||
)}
|
||||
|
||||
{richTextBlocks.map((block) => (
|
||||
<RichText key={block.id} markdown={block.body} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-md">
|
||||
{/* The draft prints no heading over this column; the heading exists
|
||||
for the document outline and for screen readers. */}
|
||||
<h3 id="about-pillars-heading" className="sr-only">
|
||||
{copy.pillarsHeading}
|
||||
</h3>
|
||||
|
||||
<ul
|
||||
aria-labelledby="about-pillars-heading"
|
||||
className="flex list-none flex-col gap-md"
|
||||
>
|
||||
{pillars.map((pillar) => (
|
||||
<li key={pillar.title}>
|
||||
<InfoPanel
|
||||
className="sh-lift sh-reveal sh-reveal--in hover:border-[var(--border-accent)]"
|
||||
title={pillar.title}
|
||||
titleAs="h4"
|
||||
>
|
||||
{pillar.description}
|
||||
</InfoPanel>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
{/* THE CONSORTIUM */}
|
||||
<PageSection aria-labelledby="about-consortium-heading">
|
||||
{/* The draft draws no rule between the raised band above and this one -
|
||||
the change of surface is the seam - so this band carries none. */}
|
||||
<PageSection
|
||||
aria-labelledby="about-consortium-heading"
|
||||
className="bg-[var(--surface-sunken)]"
|
||||
>
|
||||
<div className="flex flex-col gap-2xl">
|
||||
<SectionHeader
|
||||
id="about-consortium-heading"
|
||||
title={copy.consortiumHeading}
|
||||
/>
|
||||
<div className="sh-reveal flex flex-wrap items-end justify-between gap-lg border-b border-[var(--border-default)] pb-lg">
|
||||
<SectionHeader
|
||||
id="about-consortium-heading"
|
||||
title={copy.consortiumHeading}
|
||||
/>
|
||||
|
||||
{/* EU visibility, as the draft draws it beside this heading. The
|
||||
emblem itself is carried by the navigation and the footer. */}
|
||||
<p className="label-md uppercase text-[var(--text-tertiary)]">
|
||||
{t("site.euStatement")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid list-none grid-cols-1 gap-lg md:grid-cols-3">
|
||||
{consortium.map((member) => (
|
||||
<li key={member.id}>
|
||||
<InfoPanel title={member.name}>
|
||||
<span className="block">{member.role}</span>
|
||||
<InfoPanel
|
||||
/* On the sunken band the draft lifts these cards to the
|
||||
raised dark it uses for a page band (#1d1d1b), one step
|
||||
lighter than the panel shade InfoPanel defaults to. The
|
||||
`!` is what makes the override beat the component's own
|
||||
`bg-*` utility, which sorts after this one. */
|
||||
className="sh-lift sh-reveal sh-reveal--in hover:border-[var(--border-accent)]"
|
||||
title={member.name}
|
||||
{...(member.role
|
||||
? { eyebrow: member.role, eyebrowVariant: "chip" as const }
|
||||
: {})}
|
||||
>
|
||||
<span className="block">{member.description}</span>
|
||||
{member.website ? (
|
||||
<a
|
||||
href={member.website}
|
||||
@@ -202,15 +334,21 @@ export default function About({ loaderData }: Route.ComponentProps) {
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
{/* SHARP I AND II */}
|
||||
<PageSection aria-labelledby="about-legacy-heading">
|
||||
<div className="flex flex-col gap-lg">
|
||||
<SectionHeader
|
||||
id="about-legacy-heading"
|
||||
title={legacy.headline ?? copy.legacyHeading}
|
||||
/>
|
||||
{/* SHARP I AND II, AND THE WAY ON TO WHAT WE DO */}
|
||||
<PageSection
|
||||
aria-labelledby="about-legacy-heading"
|
||||
className="bg-[var(--surface-sunken)] pt-0 md:pt-0"
|
||||
>
|
||||
<div
|
||||
className={`sh-reveal flex flex-wrap items-center justify-between gap-xl rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-lg md:p-xl ${ACCENT_EDGE}`}
|
||||
>
|
||||
<div className="flex max-w-[44rem] flex-col gap-sm">
|
||||
<SectionHeader
|
||||
id="about-legacy-heading"
|
||||
eyebrow={copy.legacyEyebrow}
|
||||
title={legacy.headline ?? copy.legacyHeading}
|
||||
/>
|
||||
|
||||
<Prose>
|
||||
{legacy.body ? (
|
||||
<RichText markdown={legacy.body} />
|
||||
) : (
|
||||
@@ -218,49 +356,51 @@ export default function About({ loaderData }: Route.ComponentProps) {
|
||||
{copy.legacyBody}
|
||||
</p>
|
||||
)}
|
||||
</Prose>
|
||||
</div>
|
||||
|
||||
{/* The draft marks the legacy page as "[link: …]" with no target yet,
|
||||
so the button appears only once the CMS supplies a real one. */}
|
||||
{legacy.link ? (
|
||||
legacy.link.isExternal ? (
|
||||
<Button
|
||||
as="a"
|
||||
href={legacy.link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="secondary"
|
||||
iconEnd={<ArrowIcon />}
|
||||
className="self-start"
|
||||
>
|
||||
{legacy.link.label}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
as={Link}
|
||||
to={legacy.link.href}
|
||||
prefetch="intent"
|
||||
variant="secondary"
|
||||
iconEnd={<ArrowIcon />}
|
||||
className="self-start"
|
||||
>
|
||||
{legacy.link.label}
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
<div className="flex flex-wrap items-center gap-md">
|
||||
<Button
|
||||
as={Link}
|
||||
to={sectionPath("whatWeDo", locale)}
|
||||
prefetch="intent"
|
||||
size="lg"
|
||||
iconEnd={<ArrowIcon />}
|
||||
>
|
||||
{t("actions.learnWhatWeDo")}
|
||||
</Button>
|
||||
|
||||
{/* The draft marks the legacy page as "[link: …]" with no target
|
||||
yet, so this second button appears only once the CMS gives it
|
||||
a real one. */}
|
||||
{legacy.link ? (
|
||||
legacy.link.isExternal ? (
|
||||
<Button
|
||||
as="a"
|
||||
href={legacy.link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
iconEnd={<ArrowIcon />}
|
||||
>
|
||||
{legacy.link.label}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
as={Link}
|
||||
to={legacy.link.href}
|
||||
prefetch="intent"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
iconEnd={<ArrowIcon />}
|
||||
>
|
||||
{legacy.link.label}
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
|
||||
<PageSection>
|
||||
<Card.CTA
|
||||
headline={whatWeDoCopy.heroHeadline}
|
||||
body={whatWeDoCopy.lead}
|
||||
action={{
|
||||
href: sectionPath("whatWeDo", locale),
|
||||
label: t("actions.learnWhatWeDo"),
|
||||
}}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user