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:
@@ -17,6 +17,9 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import "./hero.css";
|
||||
import "./motion.css";
|
||||
|
||||
export function cx(...parts: Array<string | false | null | undefined>): string {
|
||||
return parts.filter(Boolean).join(" ");
|
||||
}
|
||||
@@ -50,9 +53,16 @@ export interface PageSectionProps {
|
||||
/** Flip this section (and only this section) to the other surface. */
|
||||
surface?: "dark" | "light";
|
||||
className?: string;
|
||||
/**
|
||||
* Paint this section as one of the drafts' lighter #1D1D1B bands. Cards
|
||||
* inside it then draw themselves a step darker, as the drafts do.
|
||||
*/
|
||||
band?: boolean;
|
||||
/** Content is placed directly in the section, without the centred column. */
|
||||
bleed?: boolean;
|
||||
"aria-labelledby"?: string;
|
||||
/** For a band whose heading is not on the page - a figures row, a logo wall. */
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,6 +73,7 @@ export function PageSection({
|
||||
children,
|
||||
id,
|
||||
surface,
|
||||
band = false,
|
||||
className,
|
||||
bleed = false,
|
||||
...rest
|
||||
@@ -71,7 +82,7 @@ export function PageSection({
|
||||
<section
|
||||
id={id}
|
||||
{...(surface ? { "data-surface": surface } : {})}
|
||||
className={cx("py-3xl md:py-4xl", className)}
|
||||
className={cx("py-3xl md:py-4xl", band && "sh-band", className)}
|
||||
{...rest}
|
||||
>
|
||||
{bleed ? children : <Container>{children}</Container>}
|
||||
@@ -169,8 +180,49 @@ export interface PageHeroWordmark {
|
||||
/** Intrinsic pixels of the asset, for the aspect ratio the browser reserves. */
|
||||
width: number;
|
||||
height: number;
|
||||
/** Drawn width in CSS pixels. Figma 253:4800 draws the hero lockup at 419. */
|
||||
/** Drawn width in CSS pixels. Figma 253:4800 draws the hero lockup at 406. */
|
||||
drawnWidth: number;
|
||||
/**
|
||||
* A second mark drawn flush after this one at the same scale, faded in once
|
||||
* the page has rendered, sliding out from behind it - the home hero's
|
||||
* "-ER". Always decorative.
|
||||
*/
|
||||
suffix?: PageHeroWordmarkSuffix;
|
||||
}
|
||||
|
||||
export interface PageHeroWordmarkSuffix {
|
||||
src: string;
|
||||
/** Intrinsic pixels of the asset, for the aspect ratio the browser reserves. */
|
||||
width: number;
|
||||
height: number;
|
||||
/** Drawn width in CSS pixels when the lockup is at full size. */
|
||||
drawnWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One soft radial wash drifting behind a hero. Every draft positions its own
|
||||
* differently - which edge it hangs from, how far in, how big and how slowly
|
||||
* it drifts - so the geometry travels with the page rather than being baked
|
||||
* into the hero.
|
||||
*
|
||||
* `offset` is measured from the INLINE start or end, so the wash mirrors in
|
||||
* Arabic along with the rest of the page.
|
||||
*/
|
||||
export interface HeroWash {
|
||||
/** Offset from the hero's top edge in px; negative pulls it above. */
|
||||
top: number;
|
||||
/** Which inline edge the wash hangs from. */
|
||||
from: "start" | "end";
|
||||
/** How far in from that edge - the draft's own value, e.g. "8%" or "-160px". */
|
||||
offset: string;
|
||||
width: number;
|
||||
height: number;
|
||||
/** Which brand wash. The drafts use green everywhere and red once, on home. */
|
||||
tone?: "accent" | "danger";
|
||||
/** Seconds one drift cycle takes; the drafts vary this from 18s to 24s. */
|
||||
durationSeconds?: number;
|
||||
/** Drift the other way round, as the home hero's red wash does. */
|
||||
reverse?: boolean;
|
||||
}
|
||||
|
||||
export interface PageHeroMedia {
|
||||
@@ -195,17 +247,125 @@ export interface PageHeroProps {
|
||||
lead?: string;
|
||||
/**
|
||||
* Brand lockup drawn above the headline. Only the home hero carries one
|
||||
* (figma 253:4800, a 419x58 wordmark 48px above the headline row).
|
||||
* (figma 253:4800, a 406x86 wordmark 48px above the headline row).
|
||||
*/
|
||||
wordmark?: PageHeroWordmark;
|
||||
/**
|
||||
* Funding attribution, drawn 24px directly above the wordmark (figma
|
||||
* 478:15951). Only the home hero carries one; every other page relies on the
|
||||
* footer for EU visibility.
|
||||
*/
|
||||
funderBadge?: ReactNode;
|
||||
/**
|
||||
* A short status line drawn as a pill above the headline - the redesign's
|
||||
* "Now live" chip. Only rendered when the caller passes one, so it can never
|
||||
* announce a state the CMS has not authored.
|
||||
*/
|
||||
statusPill?: ReactNode;
|
||||
/** Buttons, rendered inline-end of the headline on wide viewports. */
|
||||
actions?: ReactNode;
|
||||
/**
|
||||
* Put the lead on its own row, opposite the buttons, instead of under the
|
||||
* headline. The home draft lays the hero out this way - headline across the
|
||||
* top, then the lead and the CTA pair sharing a row, baseline aligned. Every
|
||||
* interior hero stacks eyebrow / headline / lead in one column.
|
||||
*/
|
||||
leadWithActions?: boolean;
|
||||
/**
|
||||
* The washes drifting behind this hero, in the page's own geometry.
|
||||
* Decorative and opt-in: omit it and the hero is flat.
|
||||
*/
|
||||
ambient?: readonly HeroWash[];
|
||||
/**
|
||||
* Tone of the lead. The drafts set it quiet almost everywhere, but What We
|
||||
* Do prints it in the primary tone, a step up from the body colour.
|
||||
*/
|
||||
leadTone?: "secondary" | "primary";
|
||||
/**
|
||||
* Stagger the hero's own content in on first paint. Opt-in for the same
|
||||
* reason: only the home hero is designed as an arrival moment.
|
||||
*/
|
||||
reveal?: boolean;
|
||||
/** Wide picture below the headline row, as in Figma 253:4800. */
|
||||
media?: PageHeroMedia;
|
||||
/**
|
||||
* Content closing the hero band, under the picture and its brand stripe -
|
||||
* the redesign puts the figures row here so it sits on the hero's own
|
||||
* surface rather than opening a band of its own.
|
||||
*/
|
||||
below?: ReactNode;
|
||||
mediaTreatment?: PageHeroMediaTreatment;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The lockup above the hero headline. Its size is a graphic dimension, not a
|
||||
* spacing value - set the way Brand.tsx and the footer size their lockups, with
|
||||
* the intrinsic box on each <img> so the browser reserves the right height.
|
||||
*
|
||||
* With a `suffix`, the two marks share one row. The row is pinned to
|
||||
* `dir="ltr"` because a Latin wordmark never mirrors (its position on the page
|
||||
* still follows the page's direction), and each mark takes the share of the
|
||||
* row its drawn width gives it, so the pair shrinks together on a narrow screen
|
||||
* and stays at one scale at every width.
|
||||
*/
|
||||
function HeroWordmark({ wordmark }: { wordmark: PageHeroWordmark }) {
|
||||
const { suffix } = wordmark;
|
||||
|
||||
if (!suffix) {
|
||||
return (
|
||||
<img
|
||||
src={wordmark.src}
|
||||
alt={wordmark.alt}
|
||||
width={wordmark.width}
|
||||
height={wordmark.height}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
style={{ maxWidth: `${wordmark.drawnWidth}px` }}
|
||||
className="block h-auto w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
dir="ltr"
|
||||
style={{ maxWidth: `${wordmark.drawnWidth + suffix.drawnWidth}px` }}
|
||||
className="flex w-full items-start"
|
||||
>
|
||||
<img
|
||||
src={wordmark.src}
|
||||
alt={wordmark.alt}
|
||||
width={wordmark.width}
|
||||
height={wordmark.height}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
style={{ flex: `${wordmark.drawnWidth} 1 0%` }}
|
||||
className="block h-auto min-w-0"
|
||||
/>
|
||||
{/* The clipping box starts at the P's right edge, so the mark inside it
|
||||
can slide out from behind the P (hero.css). */}
|
||||
<span
|
||||
style={{ flex: `${suffix.drawnWidth} 1 0%` }}
|
||||
className="sh-hero-suffix block min-w-0"
|
||||
>
|
||||
<img
|
||||
src={suffix.src}
|
||||
alt=""
|
||||
width={suffix.width}
|
||||
height={suffix.height}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
className="sh-hero-suffix__mark block h-auto w-full"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The page hero - Figma "Hero header" (253:4800): headline on the inline-start
|
||||
* side, the call-to-action group on the inline-end side, and a full-width
|
||||
@@ -217,54 +377,158 @@ export function PageHero({
|
||||
eyebrow,
|
||||
lead,
|
||||
wordmark,
|
||||
funderBadge,
|
||||
statusPill,
|
||||
actions,
|
||||
leadWithActions = false,
|
||||
ambient,
|
||||
leadTone = "secondary",
|
||||
reveal = false,
|
||||
media,
|
||||
mediaTreatment = "rounded",
|
||||
below,
|
||||
className,
|
||||
}: PageHeroProps) {
|
||||
/** Only paid for when the caller asks for the arrival treatment. */
|
||||
const rise = (step?: 2 | 3 | 4 | 5) =>
|
||||
reveal ? cx("sh-rise", step && `sh-rise--${step}`) : undefined;
|
||||
|
||||
const washes = ambient ?? [];
|
||||
|
||||
return (
|
||||
<section className={cx("pt-2xl md:pt-3xl", className)}>
|
||||
<section
|
||||
className={cx(
|
||||
"pt-[var(--layout-hero-top)] pb-[var(--layout-hero-bottom)]",
|
||||
washes.length > 0 && "relative isolate overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{washes.length > 0 ? (
|
||||
/* Flat washes of the brand colours, drifting. Purely decorative:
|
||||
behind the content, never catching the pointer, and hidden from
|
||||
assistive technology. The geometry is the page's own; only the
|
||||
drift itself is shared (motion.css). */
|
||||
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
|
||||
{washes.map((wash, index) => (
|
||||
<span
|
||||
key={`${wash.from}-${wash.offset}-${index}`}
|
||||
className={cx(
|
||||
"sh-glow absolute rounded-pill",
|
||||
wash.tone === "danger"
|
||||
? "bg-[radial-gradient(closest-side,var(--surface-danger-subtle),transparent)]"
|
||||
: "bg-[radial-gradient(closest-side,var(--surface-accent-subtle),transparent)]",
|
||||
)}
|
||||
style={{
|
||||
top: `${wash.top}px`,
|
||||
width: `${wash.width}px`,
|
||||
height: `${wash.height}px`,
|
||||
...(wash.from === "end"
|
||||
? { insetInlineEnd: wash.offset }
|
||||
: { insetInlineStart: wash.offset }),
|
||||
...(wash.durationSeconds
|
||||
? { animationDuration: `${wash.durationSeconds}s` }
|
||||
: {}),
|
||||
...(wash.reverse ? { animationDirection: "reverse" } : {}),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Container className="flex flex-col gap-2xl">
|
||||
{wordmark ? (
|
||||
/* A graphic dimension, not a spacing value - sized the way Brand.tsx
|
||||
and the footer size their lockups, with the intrinsic box on the
|
||||
element so the browser reserves the right height. */
|
||||
<img
|
||||
src={wordmark.src}
|
||||
alt={wordmark.alt}
|
||||
width={wordmark.width}
|
||||
height={wordmark.height}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
style={{ maxWidth: `${wordmark.drawnWidth}px` }}
|
||||
className="block h-auto w-full"
|
||||
/>
|
||||
{funderBadge || wordmark ? (
|
||||
/* Figma groups the two (478:16183): the badge, then 24px, then the
|
||||
lockup. The group as a whole keeps the 48px the Container's gap
|
||||
already provides above the headline row. */
|
||||
<div className={cx("flex flex-col gap-lg", rise())}>
|
||||
{funderBadge}
|
||||
|
||||
{wordmark ? <HeroWordmark wordmark={wordmark} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-lg lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="flex max-w-[46rem] flex-col gap-sm">
|
||||
{eyebrow ? (
|
||||
<p className="label-md uppercase text-[var(--text-accent)]">{eyebrow}</p>
|
||||
{statusPill ? <div className={rise(2)}>{statusPill}</div> : null}
|
||||
|
||||
{leadWithActions ? (
|
||||
<>
|
||||
{/* Headline across the top ... */}
|
||||
<div className={cx("flex max-w-[43.75rem] flex-col gap-sm", rise(3))}>
|
||||
{eyebrow ? (
|
||||
<p className="label-md uppercase text-[var(--text-accent)]">{eyebrow}</p>
|
||||
) : null}
|
||||
|
||||
<h1 className="heading-h1 text-[var(--text-primary)] md:display-sm">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* ... then the lead and the buttons sharing a row, sitting on the
|
||||
same baseline, as the home draft draws them. */}
|
||||
{lead || actions ? (
|
||||
<div
|
||||
className={cx(
|
||||
"flex flex-col gap-xl lg:flex-row lg:items-end lg:justify-between",
|
||||
rise(4),
|
||||
)}
|
||||
>
|
||||
{lead ? (
|
||||
<p
|
||||
className={cx(
|
||||
"body-lg max-w-[40rem]",
|
||||
leadTone === "primary"
|
||||
? "text-[var(--text-primary)]"
|
||||
: "text-[var(--text-secondary)]",
|
||||
)}
|
||||
>
|
||||
{lead}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{actions ? (
|
||||
<div className="flex flex-wrap items-center gap-lg lg:justify-end">
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-lg lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className={cx("flex max-w-[46rem] flex-col gap-sm", rise(3))}>
|
||||
{eyebrow ? (
|
||||
<p className="label-md uppercase text-[var(--text-accent)]">{eyebrow}</p>
|
||||
) : null}
|
||||
|
||||
<h1 className="heading-h1 text-[var(--text-primary)] md:display-sm">
|
||||
{title}
|
||||
</h1>
|
||||
<h1 className="heading-h1 text-[var(--text-primary)] md:display-sm">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{lead ? (
|
||||
<p className="body-lg max-w-[var(--layout-prose-max)] text-[var(--text-secondary)]">
|
||||
{lead}
|
||||
</p>
|
||||
{lead ? (
|
||||
<p
|
||||
className={cx(
|
||||
"body-lg max-w-[var(--layout-prose-max)]",
|
||||
leadTone === "primary"
|
||||
? "text-[var(--text-primary)]"
|
||||
: "text-[var(--text-secondary)]",
|
||||
)}
|
||||
>
|
||||
{lead}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{actions ? (
|
||||
<div
|
||||
className={cx(
|
||||
"flex flex-wrap items-center gap-lg lg:justify-end",
|
||||
rise(4),
|
||||
)}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{actions ? (
|
||||
<div className="flex flex-wrap items-center gap-lg lg:justify-end">
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{media ? (
|
||||
mediaTreatment === "banner" ? (
|
||||
@@ -273,23 +537,54 @@ export function PageHero({
|
||||
kept at 16:9 on a phone, where a 2.9:1 strip would be a sliver.
|
||||
The two washes are flat fills in the frame, not gradients: a
|
||||
mint tint over a shade, both painted on the photograph. */
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<img
|
||||
src={media.src}
|
||||
alt={media.alt}
|
||||
{...(media.width ? { width: media.width } : {})}
|
||||
{...(media.height ? { height: media.height } : {})}
|
||||
fetchPriority="high"
|
||||
className="aspect-[16/9] w-full object-cover md:aspect-[1244/425]"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 bg-[var(--overlay-picture-shade)]"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 bg-[var(--overlay-picture-tint)]"
|
||||
/>
|
||||
<div className={cx("w-full", rise(5))}>
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<img
|
||||
src={media.src}
|
||||
alt={media.alt}
|
||||
{...(media.width ? { width: media.width } : {})}
|
||||
{...(media.height ? { height: media.height } : {})}
|
||||
fetchPriority="high"
|
||||
className="aspect-[16/9] w-full object-cover md:aspect-[1244/415]"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 bg-[var(--overlay-picture-shade)]"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 bg-[var(--overlay-picture-tint)]"
|
||||
/>
|
||||
{/* A highlight travelling across the picture - the draft's
|
||||
`sheen`. Decorative, and it rides on top of both washes. */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="sh-sheen pointer-events-none absolute inset-0 bg-[linear-gradient(100deg,transparent_35%,var(--overlay-veil)_50%,transparent_65%)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* The colour band (478:16360): three blocks flush under the
|
||||
picture, 6px tall. Figma lays out 390 / 495 / 363 inside a
|
||||
1244-wide clipping frame, so the last block loses 4px and the
|
||||
split that is actually drawn is 390 : 495 : 359 - carried here
|
||||
as flex-grow ratios so it holds at every width instead of only
|
||||
at 1244. The three fills are `background/accent`,
|
||||
`background/inverse` and `background/danger`, which is why the
|
||||
middle block reads white on the dark hero and near-black on a
|
||||
light surface.
|
||||
|
||||
Purely decorative, so it is hidden from assistive technology;
|
||||
as an ordinary flex row it mirrors in Arabic along with the
|
||||
rest of the page. */}
|
||||
<div aria-hidden="true" className="sh-bar-grow flex h-[6px] w-full">
|
||||
<span className="basis-0 grow-[390] bg-[var(--surface-accent)]" />
|
||||
{/* The middle block is the raised dark, not the inverse:
|
||||
the redesign drops pure white from the page entirely, so
|
||||
the stripe reads green / dark / red on the near-black
|
||||
band instead of green / white / red. */}
|
||||
<span className="basis-0 grow-[495] bg-[var(--surface-raised)]" />
|
||||
<span className="basis-0 grow-[359] bg-[var(--surface-danger)]" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
@@ -302,6 +597,8 @@ export function PageHero({
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{below}
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
@@ -316,6 +613,11 @@ export interface InfoPanelProps {
|
||||
children?: ReactNode;
|
||||
/** Small line above the title - a number, a partner name, a role. */
|
||||
eyebrow?: string;
|
||||
/**
|
||||
* How the eyebrow is drawn. "chip" is the redesign's filled role tag on the
|
||||
* subtle accent surface; "plain" is the bare accent label inner pages use.
|
||||
*/
|
||||
eyebrowVariant?: "plain" | "chip";
|
||||
titleAs?: "h2" | "h3" | "h4";
|
||||
className?: string;
|
||||
}
|
||||
@@ -329,6 +631,7 @@ export function InfoPanel({
|
||||
title,
|
||||
children,
|
||||
eyebrow,
|
||||
eyebrowVariant = "plain",
|
||||
titleAs = "h3",
|
||||
className,
|
||||
}: InfoPanelProps) {
|
||||
@@ -337,14 +640,22 @@ export function InfoPanel({
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"flex h-full flex-col gap-xs rounded-card border border-[var(--border-subtle)] bg-[var(--surface-raised)] p-lg",
|
||||
"flex h-full flex-col gap-xs rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-lg",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{eyebrow ? (
|
||||
<p className="label-md uppercase text-[var(--text-accent)]">{eyebrow}</p>
|
||||
<p
|
||||
className={cx(
|
||||
"label-md uppercase text-[var(--text-accent)]",
|
||||
eyebrowVariant === "chip" &&
|
||||
"self-start rounded-control bg-[var(--surface-accent-subtle)] px-xs py-2xs",
|
||||
)}
|
||||
>
|
||||
{eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
<Title className="body-lg-bold text-[var(--text-primary)]">{title}</Title>
|
||||
<Title className="heading-h3 text-[var(--text-primary)]">{title}</Title>
|
||||
{children ? (
|
||||
<div className="body-md text-[var(--text-secondary)]">{children}</div>
|
||||
) : null}
|
||||
@@ -389,23 +700,48 @@ export function EmptyState({ message, title, className, children }: EmptyStatePr
|
||||
export interface ValueChipsProps {
|
||||
values: readonly string[];
|
||||
label?: string;
|
||||
/** The redesign centres the row on What We Do; inner pages read start. */
|
||||
align?: "start" | "center";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** The "What guides us" row: Dialogue · Community Leadership · Learning · … */
|
||||
export function ValueChips({ values, label, className }: ValueChipsProps) {
|
||||
export function ValueChips({
|
||||
values,
|
||||
label,
|
||||
align = "start",
|
||||
className,
|
||||
}: ValueChipsProps) {
|
||||
if (values.length === 0) return null;
|
||||
|
||||
const centred = align === "center";
|
||||
|
||||
return (
|
||||
<div className={cx("flex flex-col gap-sm", className)}>
|
||||
<div
|
||||
className={cx(
|
||||
"flex flex-col gap-sm",
|
||||
centred && "items-center text-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Accent, like every other eyebrow in the system (SectionHeader). */}
|
||||
{label ? (
|
||||
<p className="label-md uppercase text-[var(--text-tertiary)]">{label}</p>
|
||||
<p className="label-md uppercase text-[var(--text-accent)]">{label}</p>
|
||||
) : null}
|
||||
<ul className="flex flex-wrap gap-sm">
|
||||
<ul className={cx("flex flex-wrap gap-sm", centred && "justify-center")}>
|
||||
{values.map((value) => (
|
||||
<li
|
||||
key={value}
|
||||
className="rounded-pill border border-[var(--border-accent)] bg-[var(--surface-accent-subtle)] px-md py-2xs body-sm-bold text-[var(--text-primary)]"
|
||||
/* The drafts rest a chip as a plain outline on the band and only
|
||||
fill it under the pointer, where it also takes the 3px lift
|
||||
every other control has. */
|
||||
className={cx(
|
||||
"rounded-pill border border-[var(--border-default)] bg-transparent",
|
||||
"px-md py-2xs body-sm-bold text-[var(--text-primary)]",
|
||||
"transition duration-[var(--motion-duration-base)]",
|
||||
"hover:-translate-y-[3px] hover:border-[var(--border-accent)]",
|
||||
"hover:bg-[var(--surface-accent-subtle)]",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user