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:
@@ -0,0 +1,133 @@
|
||||
/* ============================================================================
|
||||
PAGE KIT - FAQ accordion
|
||||
----------------------------------------------------------------------------
|
||||
The disclosure list the home page's FAQ band draws. One question is open at
|
||||
a time, as the design draft has it, and the first one is open on arrival so
|
||||
the band never reads as a row of empty bars.
|
||||
|
||||
PATTERN: a plain disclosure - a <button aria-expanded> that controls the
|
||||
panel right after it. Deliberately NOT an ARIA accordion widget with arrow
|
||||
key roving: there is one tab stop per question, which is what a keyboard
|
||||
user expects from a list of links-like controls, and it matches the
|
||||
disclosure the navigation already ships (DesktopNav, LanguageMenu).
|
||||
|
||||
The answer is CMS richtext (markdown), so it renders through <RichText />
|
||||
rather than as a raw string.
|
||||
|
||||
RULES: tier-2 tokens only, logical properties only, and the open/close
|
||||
motion is a class from motion.css so this module carries no keyframes.
|
||||
========================================================================= */
|
||||
|
||||
import { useId, useState } from "react";
|
||||
|
||||
import { RichText } from "./rich-text";
|
||||
import { cx } from "./sections";
|
||||
|
||||
import "./motion.css";
|
||||
|
||||
export interface AccordionItem {
|
||||
readonly id: string;
|
||||
readonly question: string;
|
||||
/** Markdown, as authored in the CMS richtext field. */
|
||||
readonly answer: string;
|
||||
}
|
||||
|
||||
export interface AccordionProps {
|
||||
items: readonly AccordionItem[];
|
||||
/**
|
||||
* Index of the question open on first paint, or null for an all-closed
|
||||
* list. The server renders this same state, so nothing shifts on hydration.
|
||||
*/
|
||||
defaultOpenIndex?: number | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** The +/- mark. The upright bar collapses as the question opens. */
|
||||
function ToggleMark({ open }: { open: boolean }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cx(
|
||||
"flex size-8 shrink-0 items-center justify-center rounded-pill",
|
||||
"border border-[var(--border-default)] text-[var(--text-accent)]",
|
||||
"transition-colors duration-[var(--motion-duration-base)]",
|
||||
open && "border-[var(--border-accent)] bg-[var(--surface-accent-subtle)]",
|
||||
)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path d="M1 7h12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
<path
|
||||
d="M7 1v12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
className={cx(
|
||||
"origin-center transition-transform duration-[var(--motion-duration-base)]",
|
||||
open ? "scale-y-0" : "scale-y-100",
|
||||
)}
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function Accordion({ items, defaultOpenIndex = 0, className }: AccordionProps) {
|
||||
const baseId = useId();
|
||||
const initial =
|
||||
defaultOpenIndex === null ? null : (items[defaultOpenIndex]?.id ?? null);
|
||||
const [openId, setOpenId] = useState<string | null>(initial);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={cx("flex flex-col border-t border-[var(--border-default)]", className)}>
|
||||
{items.map((item) => {
|
||||
const open = item.id === openId;
|
||||
const buttonId = `${baseId}-${item.id}-question`;
|
||||
const panelId = `${baseId}-${item.id}-answer`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cx(
|
||||
"border-b border-[var(--border-default)]",
|
||||
"transition-colors duration-[var(--motion-duration-base)]",
|
||||
"hover:bg-[var(--surface-muted)]",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id={buttonId}
|
||||
aria-expanded={open}
|
||||
aria-controls={panelId}
|
||||
onClick={() => setOpenId(open ? null : item.id)}
|
||||
className={cx(
|
||||
"flex w-full cursor-pointer items-center justify-between gap-lg",
|
||||
"bg-transparent py-lg text-start",
|
||||
"body-lg-bold text-[var(--text-primary)]",
|
||||
"transition-colors duration-[var(--motion-duration-base)]",
|
||||
"hover:text-[var(--text-accent)]",
|
||||
)}
|
||||
>
|
||||
<span>{item.question}</span>
|
||||
<ToggleMark open={open} />
|
||||
</button>
|
||||
|
||||
{/* Kept out of the tree entirely while closed, so a screen reader
|
||||
never reaches an answer whose question reads as collapsed. */}
|
||||
{open ? (
|
||||
<div
|
||||
id={panelId}
|
||||
role="region"
|
||||
aria-labelledby={buttonId}
|
||||
className="sh-answer pb-lg pe-3xl body-md text-[var(--text-secondary)]"
|
||||
>
|
||||
<RichText markdown={item.answer} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,6 +103,8 @@ export interface CmsPartner extends StrapiEntity {
|
||||
export interface CmsConsortiumMember {
|
||||
readonly id?: number;
|
||||
readonly partner?: CmsPartner | null;
|
||||
/** Short role chip - "Lead implementing partner", "Consortium partner". */
|
||||
readonly role?: string | null;
|
||||
readonly roleDescription?: string | null;
|
||||
}
|
||||
|
||||
@@ -145,6 +147,27 @@ export function isRichTextBlock(block: CmsBlock): block is CmsRichTextBlock {
|
||||
return block.__component === "blocks.rich-text";
|
||||
}
|
||||
|
||||
/** api::news-article.news-article - the card shapes the home page needs. */
|
||||
export interface CmsNewsArticle extends StrapiEntity {
|
||||
readonly title: string;
|
||||
readonly slug: string;
|
||||
readonly excerpt?: string | null;
|
||||
readonly category: "sharper-news" | "from-the-network";
|
||||
/** Not localised in the CMS: one date for both languages. */
|
||||
readonly publishDate: string;
|
||||
readonly coverImage?: StrapiMedia | null;
|
||||
readonly authorOrganisation?: Pick<CmsPartner, "name" | "slug"> | null;
|
||||
readonly featured?: boolean | null;
|
||||
}
|
||||
|
||||
/** api::faq.faq - one question of the home page's FAQ accordion. */
|
||||
export interface CmsFaq extends StrapiEntity {
|
||||
readonly question: string;
|
||||
/** Markdown from the CMS richtext editor. */
|
||||
readonly answer: string;
|
||||
readonly order?: number | null;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
SINGLE TYPES
|
||||
------------------------------------------------------------------------ */
|
||||
@@ -154,6 +177,8 @@ export interface CmsHomePage extends StrapiEntity {
|
||||
readonly hero?: CmsHero | null;
|
||||
readonly stats?: readonly CmsStat[] | null;
|
||||
readonly blocks?: readonly CmsBlock[] | null;
|
||||
/** Curated newsroom picks; the page falls back to the latest articles. */
|
||||
readonly featuredNews?: readonly CmsNewsArticle[] | null;
|
||||
readonly partnersHeadline?: string | null;
|
||||
readonly partners?: readonly CmsPartner[] | null;
|
||||
readonly seo?: CmsSeo | null;
|
||||
|
||||
@@ -44,10 +44,16 @@ export interface TitledCopy {
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
/** One consortium member and the role the draft assigns it. */
|
||||
/**
|
||||
* One consortium member, split the way the redesign draws it: a role chip, the
|
||||
* member's name, then what it delivers. Both halves are the approved draft's
|
||||
* own wording - section 1 writes each member as
|
||||
* "<name> - <role> - <deliverable>".
|
||||
*/
|
||||
export interface ConsortiumCopy {
|
||||
readonly name: string;
|
||||
readonly role: string;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,47 +65,105 @@ export type ProseRun = string | { readonly em: string };
|
||||
|
||||
export interface HomeCopy {
|
||||
readonly heroHeadline: string;
|
||||
readonly statsHeading: string;
|
||||
readonly heroLead: string;
|
||||
readonly heroCtaPrimary: string;
|
||||
readonly heroCtaSecondary: string;
|
||||
/**
|
||||
* The figures band. Bundled so the band is never empty, and overridden by
|
||||
* `home-page.stats` the moment an editor writes one. Every figure here is
|
||||
* verifiable: three consortium members and the Dec 2025 - Dec 2027 run come
|
||||
* from the project brief, the four areas of work are the What We Do
|
||||
* workstreams, and the sixteen human security sub-projects are the approved
|
||||
* EN/AR draft's own wording.
|
||||
*/
|
||||
readonly stats: readonly { readonly value: string; readonly label: string }[];
|
||||
readonly launchEyebrow: string;
|
||||
readonly launchHeading: string;
|
||||
/** One paragraph, as the design draws it - not the full three-part story. */
|
||||
readonly launchLead: readonly ProseRun[];
|
||||
readonly mechanismsHeading: string;
|
||||
readonly mechanisms: readonly string[];
|
||||
readonly partnersHeading: string;
|
||||
readonly launchLead: string;
|
||||
readonly workHeading: string;
|
||||
readonly workLinkLabel: string;
|
||||
readonly newsEyebrow: string;
|
||||
readonly newsHeading: string;
|
||||
readonly newsEmpty: string;
|
||||
readonly faqEyebrow: string;
|
||||
readonly faqHeading: string;
|
||||
readonly faqLead: string;
|
||||
readonly faqEmpty: string;
|
||||
readonly faqCtaLabel: string;
|
||||
readonly consortiumHeading: string;
|
||||
/**
|
||||
* The same three members the About page lists, split the way the redesign
|
||||
* draws them: a role chip, the name, then what they deliver.
|
||||
*/
|
||||
readonly consortium: readonly {
|
||||
readonly role: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
}[];
|
||||
readonly networkLabel: string;
|
||||
readonly partnersEmpty: string;
|
||||
}
|
||||
|
||||
export interface AboutCopy {
|
||||
/** Draft eyebrow over the hero headline - "About the project". */
|
||||
readonly heroEyebrow: string;
|
||||
readonly heroHeadline: string;
|
||||
readonly lead: string;
|
||||
readonly body: string;
|
||||
/**
|
||||
* The four-cell band under the hero. These are project FACTS, not figures:
|
||||
* the run, the coverage, the lead partner and the funder, each one written
|
||||
* out in the approved draft's first section. Nothing here is counted or
|
||||
* estimated - the draft marks every statistic "TBD", so none is published.
|
||||
*/
|
||||
readonly facts: readonly { readonly label: string; readonly value: string }[];
|
||||
readonly bodyHeading: string;
|
||||
/**
|
||||
* The approved body prose, one entry per paragraph. The draft sets it as two
|
||||
* paragraphs beside the focus-area column, which a single string cannot
|
||||
* express.
|
||||
*/
|
||||
readonly body: readonly string[];
|
||||
readonly consortiumHeading: string;
|
||||
readonly consortium: readonly ConsortiumCopy[];
|
||||
readonly pillarsHeading: string;
|
||||
readonly pillars: readonly TitledCopy[];
|
||||
readonly legacyEyebrow: string;
|
||||
readonly legacyHeading: string;
|
||||
readonly legacyBody: string;
|
||||
readonly legacyLinkLabel: string;
|
||||
readonly fundingNote: string;
|
||||
}
|
||||
|
||||
export interface WhatWeDoCopy {
|
||||
readonly heroHeadline: string;
|
||||
readonly lead: string;
|
||||
/**
|
||||
* The draft's own heading over the areas grid. It restates the approved
|
||||
* "SHARP-ER is organised around four connected areas of work" line, which is
|
||||
* why the draft prints no separate intro paragraph under it and neither does
|
||||
* the page.
|
||||
*/
|
||||
readonly workstreamsHeading: string;
|
||||
readonly workstreamsIntro: string;
|
||||
readonly workstreams: readonly TitledCopy[];
|
||||
readonly valuesLabel: string;
|
||||
readonly values: readonly string[];
|
||||
/** Not drawn - it names the principles block for the document outline. */
|
||||
readonly principlesHeading: string;
|
||||
readonly principles: string;
|
||||
/**
|
||||
* The redesign draws the four principles as tiles. These are the four
|
||||
* clauses of the approved "Our approach rests on four principles…" sentence
|
||||
* and nothing else: the draft gives each tile an explanatory line, none of
|
||||
* which is in any approved document, so the tiles carry the clause alone.
|
||||
*/
|
||||
readonly principleTitles: readonly string[];
|
||||
readonly activitiesCtaHeading: string;
|
||||
}
|
||||
|
||||
export interface ContactCopy {
|
||||
readonly heroHeadline: string;
|
||||
readonly lead: string;
|
||||
/** Not drawn - it names the form for the document outline. */
|
||||
readonly formHeading: string;
|
||||
readonly formIntro: string;
|
||||
/** Not drawn - it names the channels column for the document outline. */
|
||||
readonly detailsHeading: string;
|
||||
readonly generalEnquiries: string;
|
||||
readonly websiteLabel: string;
|
||||
@@ -113,6 +177,8 @@ export interface ContactCopy {
|
||||
readonly errorTooLong: string;
|
||||
readonly honeypotLabel: string;
|
||||
readonly organisationHint: string;
|
||||
/** Link text for the project's Facebook page. */
|
||||
readonly facebookLabel: string;
|
||||
}
|
||||
|
||||
export interface PageCopy {
|
||||
@@ -128,50 +194,89 @@ export interface PageCopy {
|
||||
|
||||
const EN: PageCopy = {
|
||||
home: {
|
||||
/* The frame's wordmark above the headline carries the project name, so
|
||||
the headline itself does not repeat it (figma I253:4800;228:2609). */
|
||||
heroHeadline: "Project Officially Launches in Libya",
|
||||
statsHeading: "SHARP-ER in numbers",
|
||||
launchHeading: "The Journey of SHARP-ER Launch",
|
||||
launchLead: [
|
||||
"The SHARP-ER project officially launched on ",
|
||||
{ em: "Tuesday, June 30" },
|
||||
", joining representatives from the ",
|
||||
{ em: "European Union Delegation" },
|
||||
", key ",
|
||||
{ em: "Libyan Ministries" },
|
||||
", and ",
|
||||
{ em: "local civil society partners" },
|
||||
". The event marked the launch of the project’s official website and the Call for Expression of Interest (EOI).",
|
||||
heroHeadline: "SHARP-ER Project Officially Launches in Libya",
|
||||
heroLead:
|
||||
"Shared Action on Rule of Law Progress — Exit Route: a nationwide initiative supporting Libya’s civil society, local institutions, and communities in advancing justice, accountability, and good governance.",
|
||||
heroCtaPrimary: "Expression of Interest",
|
||||
heroCtaSecondary: "About the project",
|
||||
stats: [
|
||||
{ value: "3", label: "Consortium partners" },
|
||||
{ value: "4", label: "Areas of work" },
|
||||
{ value: "16", label: "Human security sub-projects" },
|
||||
{ value: "25 mo", label: "Dec 2025 – Dec 2027" },
|
||||
],
|
||||
mechanismsHeading: "What the launch set in motion",
|
||||
mechanisms: [
|
||||
"Building local capacity and skills",
|
||||
"Fostering cross-sector partnerships",
|
||||
"Supporting municipal and community-led initiatives",
|
||||
launchEyebrow: "30 June · Launch event",
|
||||
launchHeading: "An important start for the project",
|
||||
launchLead:
|
||||
"The launch brought together representatives from the European Union Delegation, the Ministry of Youth, the Ministry of Local Governance, the Ministry of Women’s Affairs, civil society organisations, partners, and stakeholders from across Libya. The event marked the opening of the Expression of Interest and the launch of this website.",
|
||||
workHeading: "Three areas of change",
|
||||
workLinkLabel: "What we do",
|
||||
newsEyebrow: "Newsroom",
|
||||
newsHeading: "News and Updates",
|
||||
newsEmpty: "News and updates will appear here as they are published.",
|
||||
faqEyebrow: "Frequently asked",
|
||||
faqHeading: "Questions about the project",
|
||||
/* The draft also promises a reply "within five working days". That
|
||||
commitment is in no approved document, so it is not published here. */
|
||||
faqLead: "Eligibility, timelines and how to take part.",
|
||||
faqEmpty: "Answers to common questions will be published here.",
|
||||
faqCtaLabel: "More in the Knowledge Hub",
|
||||
consortiumHeading: "The consortium",
|
||||
consortium: [
|
||||
{
|
||||
role: "Lead implementing partner",
|
||||
name: "Acted",
|
||||
description:
|
||||
"Overall project management and civil society sub-projects.",
|
||||
},
|
||||
{
|
||||
role: "Consortium partner",
|
||||
name: "United Efforts Consultancy (UEC)",
|
||||
description: "Network strengthening, dialogue, and peer-learning.",
|
||||
},
|
||||
{
|
||||
role: "Consortium partner",
|
||||
name: "Stichting Bureau Ensemble (SBE)",
|
||||
description: "Research, perception data, and knowledge publications.",
|
||||
},
|
||||
],
|
||||
partnersHeading: "Meet Our Partners",
|
||||
networkLabel: "Civil society network partners",
|
||||
partnersEmpty:
|
||||
"Partner organisations will be listed here as they join the SHARP-ER network.",
|
||||
},
|
||||
|
||||
about: {
|
||||
heroEyebrow: "About the project",
|
||||
heroHeadline: "Strengthening the Rule of Law, Together",
|
||||
lead: "SHARP-ER (Shared Action on Rule of Law Progress – Exit Route) is a nationwide initiative supporting Libya’s civil society, local institutions, and communities in advancing justice, accountability, and good governance.",
|
||||
body: "Running from December 2025 to December 2027, SHARP-ER is implemented by Acted, in consortium with United Efforts Consultancy (UEC) and Stichting Bureau Ensemble (SBE), and funded by the European Union. The project works nationwide across Libya, partnering with civil society organisations, municipalities, and community leaders to strengthen access to justice, build the capacity of civil society to hold institutions accountable, and improve local governance and service delivery.",
|
||||
facts: [
|
||||
{ label: "Duration", value: "December 2025 – December 2027" },
|
||||
{ label: "Coverage", value: "Nationwide, Libya" },
|
||||
{ label: "Lead partner", value: "Acted" },
|
||||
{ label: "Funded by", value: "The European Union" },
|
||||
],
|
||||
bodyHeading: "A nationwide project, delivered locally",
|
||||
body: [
|
||||
"Running from December 2025 to December 2027, SHARP-ER is implemented by Acted, in consortium with United Efforts Consultancy (UEC) and Stichting Bureau Ensemble (SBE), and funded by the European Union.",
|
||||
"The project works nationwide across Libya, partnering with civil society organisations, municipalities, and community leaders to strengthen access to justice, build the capacity of civil society to hold institutions accountable, and improve local governance and service delivery.",
|
||||
],
|
||||
consortiumHeading: "The consortium",
|
||||
consortium: [
|
||||
{
|
||||
name: "Acted",
|
||||
role: "Lead implementing partner — overall project management and civil society sub-projects",
|
||||
role: "Lead implementing partner",
|
||||
description:
|
||||
"Overall project management and civil society sub-projects.",
|
||||
},
|
||||
{
|
||||
name: "United Efforts Consultancy (UEC)",
|
||||
role: "Network strengthening, dialogue, and peer-learning",
|
||||
role: "Consortium partner",
|
||||
description: "Network strengthening, dialogue, and peer-learning.",
|
||||
},
|
||||
{
|
||||
name: "Stichting Bureau Ensemble (SBE)",
|
||||
role: "Research, perception data, and knowledge publications",
|
||||
role: "Consortium partner",
|
||||
description: "Research, perception data, and knowledge publications.",
|
||||
},
|
||||
],
|
||||
pillarsHeading: "Where we focus",
|
||||
@@ -192,18 +297,17 @@ const EN: PageCopy = {
|
||||
"Enhancing collaboration between municipalities and local actors to improve service delivery",
|
||||
},
|
||||
],
|
||||
legacyHeading: "Building on SHARP I and II",
|
||||
legacyEyebrow: "Building on SHARP",
|
||||
legacyHeading: "Carrying the SHARP I and II network into a new phase",
|
||||
legacyBody:
|
||||
"SHARP-ER builds on the foundation of SHARP I and II (2019–2024), carrying its network of civil society partners and its dialogue practice into a new phase focused on sustainability and exit.",
|
||||
legacyLinkLabel: "Read about the SHARP legacy",
|
||||
fundingNote: "Funded by the European Union.",
|
||||
},
|
||||
|
||||
whatWeDo: {
|
||||
heroHeadline: "A Participatory Approach to Justice and Governance",
|
||||
lead: "We work directly with civil society, municipalities, and communities — not around them.",
|
||||
workstreamsHeading: "Our areas of work",
|
||||
workstreamsIntro: "SHARP-ER is organised around four connected areas of work:",
|
||||
workstreamsHeading: "Four connected areas of work",
|
||||
workstreams: [
|
||||
{
|
||||
title: "Capacity Building",
|
||||
@@ -235,16 +339,21 @@ const EN: PageCopy = {
|
||||
"Responsibility & Accountability",
|
||||
],
|
||||
principlesHeading: "How we work",
|
||||
principles:
|
||||
"Our approach rests on four principles: working with local actors, promoting sustainable — not one-off — solutions, connecting civil society with institutions, and supporting gradual, realistic institutional reform.",
|
||||
principleTitles: [
|
||||
"Working with local actors",
|
||||
"Sustainable, not one-off, solutions",
|
||||
"Connecting civil society with institutions",
|
||||
"Gradual, realistic institutional reform",
|
||||
],
|
||||
activitiesCtaHeading:
|
||||
"See how this work is taking shape across the network",
|
||||
},
|
||||
|
||||
contact: {
|
||||
facebookLabel: "Follow us on Facebook",
|
||||
heroHeadline: "Get in Touch",
|
||||
lead: "Questions about SHARP-ER, a partnership, or a specific activity? Reach out.",
|
||||
formHeading: "Send us a message",
|
||||
formIntro:
|
||||
"Fill in the form and the SHARP-ER team will come back to you by email.",
|
||||
detailsHeading: "Contact details",
|
||||
generalEnquiries: "General enquiries",
|
||||
websiteLabel: "Website",
|
||||
@@ -274,49 +383,90 @@ const EN: PageCopy = {
|
||||
|
||||
const AR: PageCopy = {
|
||||
home: {
|
||||
/* (ar-draft) matches the English frame: the wordmark names the project. */
|
||||
heroHeadline: "انطلاق المشروع رسميًا في ليبيا",
|
||||
statsHeading: "SHARP-ER بالأرقام",
|
||||
launchHeading: "مسيرة إطلاق مشروع SHARP-ER",
|
||||
launchLead: [
|
||||
"انطلقت رسميًا فعاليات مشروع SHARP-ER يوم ",
|
||||
{ em: "الثلاثاء 30 يونيو" },
|
||||
"، بمشاركة ممثلين عن ",
|
||||
{ em: "بعثة الاتحاد الأوروبي" },
|
||||
"، وعدد من ",
|
||||
{ em: "الوزارات الليبية" },
|
||||
"، إلى جانب ",
|
||||
{ em: "شركاء المجتمع المدني المحليين" },
|
||||
". وشهد الحدث إطلاق الموقع الإلكتروني الرسمي للمشروع والدعوة للتعبير عن الاهتمام (EOI).",
|
||||
heroHeadline: "انطلاق مشروع SHARP-ER رسميًا في ليبيا",
|
||||
heroLead:
|
||||
"العمل المشترك لتعزيز سيادة القانون – مسار الخروج: مبادرة وطنية تدعم المجتمع المدني والمؤسسات المحلية والمجتمعات في ليبيا في تعزيز العدالة والمساءلة والحوكمة الرشيدة.",
|
||||
heroCtaPrimary: "الدعوة للتعبير عن الاهتمام",
|
||||
heroCtaSecondary: "عن المشروع",
|
||||
/* Western digits, as the rest of the Arabic draft writes its numbers
|
||||
("ديسمبر 2025", "SHARP I and II (2019–2024)"). */
|
||||
stats: [
|
||||
{ value: "3", label: "شركاء الائتلاف" },
|
||||
{ value: "4", label: "مجالات العمل" },
|
||||
{ value: "16", label: "مشروعًا فرعيًا للأمن الإنساني" },
|
||||
{ value: "25 شهرًا", label: "ديسمبر 2025 – ديسمبر 2027" },
|
||||
],
|
||||
mechanismsHeading: "ما الذي أطلقه هذا الحدث",
|
||||
mechanisms: [
|
||||
"بناء القدرات والمهارات المحلية",
|
||||
"تعزيز الشراكات بين القطاعات",
|
||||
"دعم المبادرات البلدية والمبادرات المجتمعية",
|
||||
launchEyebrow: "30 يونيو · فعالية الإطلاق",
|
||||
launchHeading: "بداية مهمة للمشروع",
|
||||
launchLead:
|
||||
"جمعت فعالية الإطلاق ممثلين عن بعثة الاتحاد الأوروبي، ووزارة الشباب، ووزارة الحكم المحلي، ووزارة شؤون المرأة، ومنظمات المجتمع المدني، والشركاء، وأصحاب المصلحة من مختلف أنحاء ليبيا. وشهد الحدث افتتاح الدعوة للتعبير عن الاهتمام وإطلاق هذا الموقع الإلكتروني.",
|
||||
workHeading: "ثلاثة مجالات للتغيير",
|
||||
workLinkLabel: "ماذا نفعل",
|
||||
newsEyebrow: "غرفة الأخبار",
|
||||
newsHeading: "الأخبار والمستجدات",
|
||||
newsEmpty: "ستظهر الأخبار والمستجدات هنا فور نشرها.",
|
||||
faqEyebrow: "الأسئلة الشائعة",
|
||||
faqHeading: "أسئلة حول المشروع",
|
||||
faqLead: "شروط الأهلية والجداول الزمنية وكيفية المشاركة.",
|
||||
faqEmpty: "ستُنشر إجابات الأسئلة الشائعة هنا قريبًا.",
|
||||
faqCtaLabel: "المزيد في مركز المعرفة",
|
||||
consortiumHeading: "الائتلاف المنفّذ",
|
||||
consortium: [
|
||||
{
|
||||
role: "الشريك المنفّذ الرئيسي",
|
||||
name: "Acted",
|
||||
description: "الإدارة العامة للمشروع والمشاريع الفرعية لمنظمات المجتمع المدني.",
|
||||
},
|
||||
{
|
||||
role: "شريك في الائتلاف",
|
||||
name: "United Efforts Consultancy (UEC)",
|
||||
description: "تعزيز الشبكة، والحوار، والتعلّم بين الأقران.",
|
||||
},
|
||||
{
|
||||
role: "شريك في الائتلاف",
|
||||
name: "Stichting Bureau Ensemble (SBE)",
|
||||
description: "البحث، وبيانات الاستطلاع، والمنشورات المعرفية.",
|
||||
},
|
||||
],
|
||||
partnersHeading: "تعرّف على شركائنا",
|
||||
networkLabel: "شركاء شبكة المجتمع المدني",
|
||||
partnersEmpty:
|
||||
"ستُدرج المنظمات الشريكة هنا فور انضمامها إلى شبكة SHARP-ER.",
|
||||
},
|
||||
|
||||
about: {
|
||||
heroEyebrow: "عن المشروع",
|
||||
heroHeadline: "نحو تعزيز سيادة القانون، معًا",
|
||||
lead: "SHARP-ER (العمل المشترك لتعزيز سيادة القانون – مسار الخروج) هو مبادرة وطنية تدعم المجتمع المدني والمؤسسات المحلية والمجتمعات الليبية في تعزيز العدالة والمساءلة والحوكمة الرشيدة.",
|
||||
body: "يمتد المشروع من ديسمبر 2025 إلى ديسمبر 2027، ويُنفَّذ من قِبل منظمة Acted بالشراكة مع United Efforts Consultancy (UEC) وStichting Bureau Ensemble (SBE)، وبتمويل من الاتحاد الأوروبي. يعمل المشروع على مستوى ليبيا بأكملها، بالتعاون مع منظمات المجتمع المدني والبلديات والقيادات المجتمعية، من أجل تحسين الوصول إلى العدالة، وبناء قدرات المجتمع المدني على مساءلة المؤسسات، وتحسين الحوكمة المحلية وتقديم الخدمات.",
|
||||
/* Western digits, as the Arabic column of the approved draft writes its
|
||||
own dates ("ديسمبر 2025", "(2019–2024)"). */
|
||||
facts: [
|
||||
{ label: "مدة المشروع", value: "ديسمبر 2025 – ديسمبر 2027" },
|
||||
{ label: "نطاق العمل", value: "على مستوى ليبيا بأكملها" },
|
||||
{ label: "الشريك المنفّذ الرئيسي", value: "Acted" },
|
||||
{ label: "بتمويل من", value: "الاتحاد الأوروبي" },
|
||||
],
|
||||
bodyHeading: "مشروع وطني يُنفَّذ محليًا",
|
||||
body: [
|
||||
"يمتد المشروع من ديسمبر 2025 إلى ديسمبر 2027، ويُنفَّذ من قِبل منظمة Acted بالشراكة مع United Efforts Consultancy (UEC) وStichting Bureau Ensemble (SBE)، وبتمويل من الاتحاد الأوروبي.",
|
||||
"يعمل المشروع على مستوى ليبيا بأكملها، بالتعاون مع منظمات المجتمع المدني والبلديات والقيادات المجتمعية، من أجل تحسين الوصول إلى العدالة، وبناء قدرات المجتمع المدني على مساءلة المؤسسات، وتحسين الحوكمة المحلية وتقديم الخدمات.",
|
||||
],
|
||||
consortiumHeading: "الائتلاف المنفّذ",
|
||||
consortium: [
|
||||
{
|
||||
name: "Acted",
|
||||
role: "الشريك المنفّذ الرئيسي — الإدارة العامة للمشروع والمشاريع الفرعية لمنظمات المجتمع المدني",
|
||||
role: "الشريك المنفّذ الرئيسي",
|
||||
description:
|
||||
"الإدارة العامة للمشروع والمشاريع الفرعية لمنظمات المجتمع المدني.",
|
||||
},
|
||||
{
|
||||
name: "United Efforts Consultancy (UEC)",
|
||||
role: "تعزيز الشبكة، والحوار، والتعلّم بين الأقران",
|
||||
role: "شريك في الائتلاف",
|
||||
description: "تعزيز الشبكة، والحوار، والتعلّم بين الأقران.",
|
||||
},
|
||||
{
|
||||
name: "Stichting Bureau Ensemble (SBE)",
|
||||
role: "البحث، وبيانات الاستطلاع، والمنشورات المعرفية",
|
||||
role: "شريك في الائتلاف",
|
||||
description: "البحث، وبيانات الاستطلاع، والمنشورات المعرفية.",
|
||||
},
|
||||
],
|
||||
pillarsHeading: "محاور تركيزنا",
|
||||
@@ -337,18 +487,21 @@ const AR: PageCopy = {
|
||||
"تعزيز التعاون بين البلديات والجهات الفاعلة المحلية لتحسين تقديم الخدمات",
|
||||
},
|
||||
],
|
||||
legacyHeading: "استكمالًا لمشروعي SHARP الأول والثاني",
|
||||
legacyEyebrow: "استكمالًا لمشروع SHARP",
|
||||
legacyHeading:
|
||||
"حمل شبكة SHARP الأول والثاني إلى مرحلة جديدة",
|
||||
legacyBody:
|
||||
"يبني SHARP-ER على الأساس الذي أرساه مشروعا SHARP الأول والثاني (2019–2024)، حاملًا معه شبكة الشركاء من المجتمع المدني وممارسة الحوار إلى مرحلة جديدة تركّز على الاستدامة ومسار الخروج.",
|
||||
legacyLinkLabel: "اطّلع على إرث مشروع SHARP",
|
||||
fundingNote: "بتمويل من الاتحاد الأوروبي.",
|
||||
},
|
||||
|
||||
whatWeDo: {
|
||||
heroHeadline: "نهج تشاركي في العدالة والحوكمة",
|
||||
lead: "نعمل مباشرة مع المجتمع المدني والبلديات والمجتمعات المحلية، لا من حولها.",
|
||||
workstreamsHeading: "محاور عملنا",
|
||||
workstreamsIntro: "يقوم عمل SHARP-ER على أربعة محاور مترابطة:",
|
||||
/* The approved Arabic column writes the same line as a sentence -
|
||||
"يقوم عمل SHARP-ER على أربعة محاور مترابطة" - which the draft's frame
|
||||
sets as the heading itself. */
|
||||
workstreamsHeading: "أربعة محاور عمل مترابطة",
|
||||
workstreams: [
|
||||
{
|
||||
title: "بناء القدرات",
|
||||
@@ -380,15 +533,20 @@ const AR: PageCopy = {
|
||||
"المساءلة والمسؤولية",
|
||||
],
|
||||
principlesHeading: "كيف نعمل",
|
||||
principles:
|
||||
"يسترشد عملنا بأربعة مبادئ: العمل مع الفاعلين المحليين، وتعزيز الحلول المستدامة لا الآنية، والربط بين المجتمع المدني والمؤسسات، ودعم إصلاح مؤسسي تدريجي وواقعي.",
|
||||
principleTitles: [
|
||||
"العمل مع الفاعلين المحليين",
|
||||
"حلول مستدامة لا آنية",
|
||||
"الربط بين المجتمع المدني والمؤسسات",
|
||||
"إصلاح مؤسسي تدريجي وواقعي",
|
||||
],
|
||||
activitiesCtaHeading: "اطّلع على كيفية تجسّد هذا العمل عبر الشبكة",
|
||||
},
|
||||
|
||||
contact: {
|
||||
facebookLabel: "تابعونا على فيسبوك",
|
||||
heroHeadline: "تواصل معنا",
|
||||
lead: "لديك استفسار حول SHARP-ER، أو شراكة محتملة، أو نشاط معيّن؟ راسلنا.",
|
||||
formHeading: "أرسل لنا رسالة",
|
||||
formIntro: "املأ النموذج وسيعاود فريق SHARP-ER التواصل معك عبر البريد الإلكتروني.",
|
||||
detailsHeading: "بيانات التواصل",
|
||||
generalEnquiries: "للاستفسارات العامة",
|
||||
websiteLabel: "الموقع الإلكتروني",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* ============================================================================
|
||||
HERO WORDMARK SUFFIX - the "-ER" after "SHARP" on the home hero
|
||||
----------------------------------------------------------------------------
|
||||
Not drawn in Figma: a project request, so the hero finishes on the full
|
||||
name. "SHARP" is on screen from the first paint; once the page has settled,
|
||||
"-ER" slides out from behind the P while it fades in.
|
||||
|
||||
HOW "BEHIND THE P" WORKS: the suffix sits in its own box whose inline-start
|
||||
edge is exactly the P's right edge, and that box clips. The mark starts a
|
||||
full box-width back - entirely inside the P's side of the clip, so nothing
|
||||
overlaps "SHARP" - and travels forward to rest. The opacity reaches 1 before
|
||||
the travel ends, so the glyphs read as solid for the last, slowest part of
|
||||
the decelerating slide.
|
||||
|
||||
The translate is physical (translateX) on purpose: the lockup row is pinned
|
||||
to dir="ltr" because a Latin wordmark never mirrors, so the P is always to
|
||||
the left of the suffix, in Arabic too.
|
||||
|
||||
Pure CSS, so it runs from the server-rendered HTML without waiting for
|
||||
hydration, and the `both` fill keeps the suffix hidden through the delay
|
||||
instead of flashing in first. It replays whenever the hero mounts again,
|
||||
e.g. on a client-side navigation back to the home page.
|
||||
|
||||
REDUCED MOTION: app.css shortens every animation to 0.01ms but leaves the
|
||||
delay alone, which would hold the suffix hidden for the delay and then pop
|
||||
it in. Here the animation is removed outright, so "SHARP-ER" is simply there
|
||||
from the start.
|
||||
========================================================================= */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.sh-hero-suffix {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sh-hero-suffix__mark {
|
||||
animation: sh-hero-suffix-in var(--motion-duration-reveal)
|
||||
var(--motion-ease-entrance) var(--motion-delay-reveal) both;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sh-hero-suffix__mark {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-hero-suffix-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/* ============================================================================
|
||||
HOME MOTION - the reveals the redesigned home page runs
|
||||
----------------------------------------------------------------------------
|
||||
Every animation in the Claude Design draft ("SHARP-ER Home v2 Dark"), carried
|
||||
over as classes so the page modules stay free of inline keyframes and raw
|
||||
values. Durations and easings are the motion tokens from semantic.css; the
|
||||
few long, decorative loops are expressed as multiples of them rather than as
|
||||
bare seconds, so retiming the system retimes the page.
|
||||
|
||||
TWO KINDS OF MOTION LIVE HERE
|
||||
* entrance - time based, runs once from the server-rendered HTML
|
||||
(.sh-rise, .sh-bar-grow), staggered with .sh-rise--2..5
|
||||
* scroll - scroll-driven (`animation-timeline`), so a band reveals as
|
||||
it enters the viewport and the top progress bar tracks the
|
||||
document
|
||||
|
||||
WHY THE @supports GUARD: scroll-driven timelines ship in Chromium only. The
|
||||
from-state (opacity 0) is applied ONLY where `animation-timeline: view()` is
|
||||
understood, so Safari and Firefox render every band at rest, fully visible,
|
||||
instead of holding an invisible element forever.
|
||||
|
||||
REDUCED MOTION: app.css already collapses every animation to 0.01ms. The
|
||||
scroll block is additionally gated on `prefers-reduced-motion: no-preference`
|
||||
so a reduced-motion visitor never gets a timeline-driven opacity ramp at all.
|
||||
|
||||
DIRECTION: `transform-origin` and `linear-gradient()` have no logical
|
||||
keywords, so the progress bar and the brand stripe carry an explicit RTL
|
||||
override. These are the only physical directions in the file, and they exist
|
||||
because the property itself has no logical form - every box edge elsewhere is
|
||||
still ps-/pe-/start-/end-.
|
||||
========================================================================= */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
/* -------------------------------------------------------------------------
|
||||
ENTRANCE - hero content, first paint
|
||||
---------------------------------------------------------------------- */
|
||||
.sh-rise {
|
||||
animation: sh-rise var(--motion-duration-reveal) var(--motion-ease-entrance)
|
||||
var(--sh-rise-delay, 0ms) both;
|
||||
}
|
||||
|
||||
/* The hero's cascade. The step is the system's fast duration, so the whole
|
||||
stagger retimes with the token instead of drifting on hard-coded ms. */
|
||||
.sh-rise--2 {
|
||||
--sh-rise-delay: calc(var(--motion-duration-fast) * 0.75);
|
||||
}
|
||||
|
||||
.sh-rise--3 {
|
||||
--sh-rise-delay: calc(var(--motion-duration-fast) * 1.5);
|
||||
}
|
||||
|
||||
.sh-rise--4 {
|
||||
--sh-rise-delay: calc(var(--motion-duration-fast) * 2.25);
|
||||
}
|
||||
|
||||
.sh-rise--5 {
|
||||
--sh-rise-delay: calc(var(--motion-duration-fast) * 3);
|
||||
}
|
||||
|
||||
/* The brand stripe under the hero picture, drawn on from the inline start. */
|
||||
.sh-bar-grow {
|
||||
transform-origin: left center;
|
||||
animation: sh-bar-grow var(--motion-duration-reveal)
|
||||
var(--motion-ease-entrance) calc(var(--motion-duration-fast) * 4) both;
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-bar-grow {
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
AMBIENT LOOPS - decorative, never carrying meaning
|
||||
---------------------------------------------------------------------- */
|
||||
|
||||
/* The two soft washes behind the hero. Long, slow drifts: 22.5x and 30x the
|
||||
reveal duration (18s / 24s at the shipped token). */
|
||||
.sh-glow {
|
||||
animation: sh-glow calc(var(--motion-duration-reveal) * 22.5) ease-in-out
|
||||
infinite;
|
||||
}
|
||||
|
||||
/* A highlight travelling across the hero picture. */
|
||||
.sh-sheen {
|
||||
background-size: 240% 100%;
|
||||
animation: sh-sheen calc(var(--motion-duration-reveal) * 8) linear infinite;
|
||||
}
|
||||
|
||||
/* The live dot in the hero status pill. */
|
||||
.sh-pulse {
|
||||
animation: sh-pulse calc(var(--motion-duration-reveal) * 2.75) ease-in-out
|
||||
infinite;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
INTERACTION
|
||||
---------------------------------------------------------------------- */
|
||||
|
||||
/* Tiles that rise a little under the pointer. No shadow: the draft's hover
|
||||
glow is deliberately left out. */
|
||||
.sh-lift {
|
||||
transition:
|
||||
transform var(--motion-duration-base) var(--motion-ease-standard),
|
||||
border-color var(--motion-duration-base) var(--motion-ease-standard),
|
||||
background-color var(--motion-duration-base) var(--motion-ease-standard);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.sh-lift:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* An FAQ answer opening under its question. */
|
||||
.sh-answer {
|
||||
animation: sh-answer var(--motion-duration-base) var(--motion-ease-entrance)
|
||||
both;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
SCROLL DRIVEN - Chromium today, inert and harmless elsewhere
|
||||
---------------------------------------------------------------------- */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@supports (animation-timeline: view()) {
|
||||
.sh-reveal {
|
||||
animation: sh-rise linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 0% entry 70%;
|
||||
}
|
||||
|
||||
/* Cards and pictures: the same reveal with a touch of scale. */
|
||||
.sh-reveal--in {
|
||||
animation-name: sh-reveal-in;
|
||||
animation-range: entry 0% entry 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
/* The reading-progress rule across the top of the page. It is painted
|
||||
ONLY inside this @supports block: without a scroll timeline the bar
|
||||
would sit there permanently at full width, which is a rule the design
|
||||
never asked for. */
|
||||
.sh-progress {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--surface-accent),
|
||||
var(--text-accent) 55%,
|
||||
var(--surface-danger)
|
||||
);
|
||||
transform-origin: left center;
|
||||
animation: sh-bar-grow linear both;
|
||||
animation-timeline: scroll();
|
||||
}
|
||||
|
||||
[dir="rtl"] .sh-progress {
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
var(--surface-accent),
|
||||
var(--text-accent) 55%,
|
||||
var(--surface-danger)
|
||||
);
|
||||
transform-origin: right center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(26px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-reveal-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.985);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-bar-grow {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-glow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
transform: translate3d(-4%, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: translate3d(6%, -3%, 0) scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-sheen {
|
||||
from {
|
||||
background-position: -160% 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 260% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.72);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sh-answer {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -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