Implement about page

This commit is contained in:
adilallo
2026-05-13 23:08:36 -06:00
parent d2dfa099a2
commit b6b9b63608
69 changed files with 1834 additions and 28 deletions
@@ -0,0 +1,18 @@
"use client";
import { memo, useId } from "react";
import AboutHeaderView from "./AboutHeader.view";
import type { AboutHeaderProps } from "./AboutHeader.types";
/**
* Figma: "Type / AboutHeader" (22135-889654).
*/
const AboutHeaderContainer = memo<AboutHeaderProps>((props) => {
const titleId = useId();
return <AboutHeaderView {...props} titleId={titleId} />;
});
AboutHeaderContainer.displayName = "AboutHeader";
export default AboutHeaderContainer;
@@ -0,0 +1,12 @@
export type AboutHeaderSegment =
| { type: "word"; text: string }
| { type: "icon"; icon: "arrow" | "about" };
export interface AboutHeaderProps {
segments: AboutHeaderSegment[];
className?: string;
}
export interface AboutHeaderViewProps extends AboutHeaderProps {
titleId: string;
}
@@ -0,0 +1,67 @@
"use client";
import { memo } from "react";
import { ASSETS, getAssetPath, vectorMarkPath } from "../../../../lib/assetUtils";
import ContentLockup from "../ContentLockup";
import type { AboutHeaderViewProps } from "./AboutHeader.types";
function assetRelativeForInlineIcon(icon: "arrow" | "about"): string {
return icon === "arrow" ? ASSETS.LOGO : vectorMarkPath("about");
}
/**
* Figma: "Type / AboutHeader" (22135-889654).
*/
function AboutHeaderView({
segments,
titleId,
className = "",
}: AboutHeaderViewProps) {
return (
<section
className={`bg-black px-[var(--spacing-scale-020)] py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-064)] md:py-[var(--spacing-scale-032)] ${className}`.trim()}
>
<ContentLockup
variant="about"
alignment="left"
titleId={titleId}
titleContent={segments.map((segment, index) => {
if (segment.type === "word") {
return (
<span key={`${segment.text}-${index}`} className="whitespace-nowrap">
{segment.text}
</span>
);
}
return (
<span
key={`${segment.icon}-${index}`}
className={
segment.icon === "arrow"
? "inline-flex h-[0.92em] min-h-[24px] max-h-[52px] shrink-0 items-center self-center md:max-h-[64px]"
: "inline-flex size-[29px] shrink-0 items-center justify-center md:size-[80px]"
}
>
{/* eslint-disable-next-line @next/next/no-img-element -- decorative inline vector */}
<img
src={getAssetPath(assetRelativeForInlineIcon(segment.icon))}
alt=""
className={
segment.icon === "arrow"
? "h-full w-auto max-w-[4.75rem] object-contain md:max-w-[6rem]"
: "size-full object-contain"
}
role="presentation"
/>
</span>
);
})}
/>
</section>
);
}
AboutHeaderView.displayName = "AboutHeaderView";
export default memo(AboutHeaderView);
@@ -0,0 +1,2 @@
export { default } from "./AboutHeader.container";
export type { AboutHeaderProps, AboutHeaderSegment } from "./AboutHeader.types";
@@ -16,6 +16,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
linkHref,
alignment: alignmentProp = "center",
titleId,
titleContent,
}) => {
const variant = variantProp;
const alignment = alignmentProp;
@@ -48,10 +49,44 @@ const ContentLockupContainer = memo<ContentLockupProps>(
subtitle:
"font-space-grotesk font-normal text-[20px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]",
description:
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]",
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-default-secondary)]",
shape:
"w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]",
},
about: {
container:
"flex flex-col gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-016)] relative z-10",
textContainer:
"flex flex-col gap-[var(--spacing-scale-008)] lg:gap-[var(--spacing-scale-004)]",
titleGroup:
"flex flex-col gap-[var(--spacing-scale-008)] lg:gap-[var(--spacing-scale-004)]",
titleContainer:
"flex flex-wrap items-center gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-010)]",
title:
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] md:text-[52px] xl:text-[length:var(--spacing-scale-064)] xl:leading-[length:var(--spacing-scale-064)] text-[var(--color-content-default-primary,white)]",
subtitle:
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] lg:text-[18px] lg:leading-[22px] xl:text-[32px] xl:leading-[1.1] text-[var(--color-content-default-primary,white)]",
description:
"font-inter font-normal text-[length:var(--text-x-large-paragraph)] leading-[length:var(--text-x-large-paragraph--line-height)] tracking-[var(--text-x-large-paragraph--letter-spacing)] text-[var(--color-content-default-secondary)] whitespace-pre-line lg:text-[14px] lg:leading-[20px] lg:tracking-[0] xl:text-[length:var(--text-x-large-paragraph)] xl:leading-[length:var(--text-x-large-paragraph--line-height)] xl:tracking-[var(--text-x-large-paragraph--letter-spacing)]",
shape:
"w-[20px] h-[20px] md:w-[28px] md:h-[28px]",
},
book: {
container:
"flex flex-col gap-[var(--spacing-scale-016)] lg:gap-[var(--spacing-scale-020)] relative z-10",
textContainer:
"flex flex-col gap-[var(--spacing-scale-004)] lg:gap-[var(--spacing-scale-008)]",
titleGroup: "flex flex-col gap-0",
titleContainer: "flex flex-wrap items-center gap-[var(--spacing-scale-008)]",
title:
"font-bricolage-grotesque font-medium text-[18px] leading-[22px] lg:text-[length:var(--spacing-scale-020)] lg:leading-[24px] xl:text-[length:var(--spacing-scale-032)] xl:leading-[1.1] text-[var(--color-content-default-primary)]",
subtitle:
"font-bricolage-grotesque font-medium text-[18px] leading-[22px] lg:text-[length:var(--spacing-scale-020)] lg:leading-[24px] xl:text-[length:var(--spacing-scale-032)] xl:leading-[1.1] text-[var(--color-content-default-primary)]",
description:
"font-inter font-normal text-[length:var(--sizing-350)] leading-[20px] text-[var(--color-content-default-secondary)] whitespace-pre-line lg:text-[length:var(--sizing-400)] lg:leading-[24px] xl:text-[length:var(--spacing-scale-024)] xl:leading-[length:var(--text-x-large-paragraph--line-height)]",
shape:
"w-[20px] h-[20px] md:w-[28px] md:h-[28px]",
},
learn: {
container:
"flex flex-col gap-[var(--spacing-scale-012)] relative z-10 pt-[var(--spacing-scale-016)] pb-[var(--spacing-scale-016)] px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-040)] sm:pb-0 md:pt-[var(--spacing-scale-056)] md:px-[var(--spacing-scale-032)] lg:pt-[var(--spacing-scale-056)] lg:px-[var(--spacing-scale-064)]",
@@ -65,7 +100,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
subtitle:
"font-space-grotesk font-normal text-[16px] leading-[24px] tracking-[0] lg:text-[24px] lg:leading-[28px] text-[var(--color-content-default-primary)]",
description:
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]",
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-default-secondary)]",
shape:
"w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]",
},
@@ -75,7 +110,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
title:
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-default-brand-primary)]",
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[52px] md:leading-[110%] text-[var(--color-content-default-brand-primary)]",
subtitle:
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]",
shape:
@@ -87,7 +122,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
title:
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[52px] md:leading-[110%] text-[var(--color-content-inverse-primary)]",
subtitle:
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-inverse-primary)]",
shape:
@@ -137,6 +172,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
linkHref={linkHref}
alignment={alignment}
titleId={titleId}
titleContent={titleContent}
styles={styles}
/>
);
@@ -1,7 +1,11 @@
import type { ReactNode } from "react";
export type ContentLockupVariantValue =
| "hero"
| "feature"
| "learn"
| "about"
| "book"
| "ask"
| "ask-inverse"
| "modal"
@@ -31,6 +35,8 @@ export interface ContentLockupProps {
* Useful when a parent section uses aria-labelledby.
*/
titleId?: string;
/** Replaces the default title string when inline title markup is required. */
titleContent?: ReactNode;
}
export interface VariantStyle {
@@ -55,6 +61,8 @@ export interface ContentLockupViewProps {
| "hero"
| "feature"
| "learn"
| "about"
| "book"
| "ask"
| "ask-inverse"
| "modal"
@@ -63,5 +71,6 @@ export interface ContentLockupViewProps {
linkHref?: string;
alignment: "center" | "left";
titleId?: string;
titleContent?: ReactNode;
styles: VariantStyle;
}
@@ -16,6 +16,7 @@ function ContentLockupView({
linkHref,
alignment,
titleId,
titleContent,
styles,
}: ContentLockupViewProps) {
return (
@@ -57,7 +58,14 @@ function ContentLockupView({
<div className={styles.titleGroup}>
{/* Title container */}
<div className={styles.titleContainer}>
{title ? (
{titleContent ? (
<h1
id={titleId}
className={`${styles.title} flex flex-wrap items-center gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-010)]`}
>
{titleContent}
</h1>
) : title ? (
<h1 id={titleId} className={styles.title}>
{title}
</h1>
@@ -0,0 +1,18 @@
"use client";
import { memo, useId } from "react";
import TripleTextBlockView from "./TripleTextBlock.view";
import type { TripleTextBlockProps } from "./TripleTextBlock.types";
/**
* Figma: "Type / TripleTextBlock" stacked 22137:890676; lg 22128:888715; xl 22135:889705.
*/
const TripleTextBlockContainer = memo<TripleTextBlockProps>((props) => {
const headingId = useId();
return <TripleTextBlockView {...props} headingId={headingId} />;
});
TripleTextBlockContainer.displayName = "TripleTextBlock";
export default TripleTextBlockContainer;
@@ -0,0 +1,23 @@
export interface TripleTextBlockColumn {
title: string;
description: string;
/**
* lg+ three-column layout (Figma 22128:888715). When either `lgTitle` or `lgDescription`
* is set, stacked breakpoints use `title`/`description` and lg uses these (missing side falls back).
*/
lgTitle?: string;
lgDescription?: string;
}
export interface TripleTextBlockProps {
/** Section heading above the column stack (e.g. About page). Omit when matching a headerless Figma frame. */
title?: string;
columns: TripleTextBlockColumn[];
ctaText?: string;
ctaHref?: string;
className?: string;
}
export interface TripleTextBlockViewProps extends TripleTextBlockProps {
headingId: string;
}
@@ -0,0 +1,114 @@
"use client";
import { memo } from "react";
import Button from "../../buttons/Button";
import ContentLockup from "../ContentLockup";
import type {
TripleTextBlockColumn,
TripleTextBlockViewProps,
} from "./TripleTextBlock.types";
function columnUsesLargeBreakpointCopy(column: TripleTextBlockColumn): boolean {
return column.lgTitle !== undefined || column.lgDescription !== undefined;
}
function TripleTextBlockColumnLockup({
column,
}: {
column: TripleTextBlockColumn;
}) {
const dual = columnUsesLargeBreakpointCopy(column);
const lgSubtitle = column.lgTitle ?? column.title;
const lgBody = column.lgDescription ?? column.description;
if (!dual) {
return (
<ContentLockup
variant="about"
alignment="left"
subtitle={column.title}
description={column.description}
/>
);
}
return (
<>
<div className="lg:hidden">
<ContentLockup
variant="about"
alignment="left"
subtitle={column.title}
description={column.description}
/>
</div>
<div className="hidden lg:block">
<ContentLockup
variant="about"
alignment="left"
subtitle={lgSubtitle}
description={lgBody}
/>
</div>
</>
);
}
/**
* Figma: "Type / TripleTextBlock" stacked **22137:890676**; lg 3-col **22128-888715**; xl typography + horizontal inset scale/160 **22135:889705** (Subtitle 32 Small/Display, Body X Large/Paragraph 24 / lh 32; section px scale/160, py scale/064).
*/
function TripleTextBlockView({
title = "",
columns,
ctaText,
ctaHref,
headingId,
className = "",
}: TripleTextBlockViewProps) {
const sectionTitle = title.trim();
const hasSectionTitle = sectionTitle.length > 0;
return (
<section
aria-labelledby={hasSectionTitle ? headingId : undefined}
className={`bg-black px-[var(--spacing-scale-032)] py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-096)] md:py-[var(--spacing-scale-064)] xl:px-[var(--spacing-scale-160)] ${className}`.trim()}
>
<div className="mx-auto flex w-full max-w-[1440px] flex-col items-start gap-[var(--spacing-scale-032)]">
{hasSectionTitle ? (
<h2
id={headingId}
className="w-full text-left font-bricolage-grotesque text-[32px] font-medium leading-[1.1] text-[var(--color-content-default-primary,white)]"
>
{sectionTitle}
</h2>
) : null}
<div className="flex w-full flex-col gap-[var(--spacing-scale-032)] lg:flex-row lg:items-start lg:gap-[var(--spacing-scale-032)]">
{columns.map((column, index) => (
<div
key={`${column.title}-${column.lgTitle ?? ""}-${index}`}
className="w-full min-w-0 lg:flex-1"
>
<TripleTextBlockColumnLockup column={column} />
</div>
))}
</div>
{ctaText ? (
<div className="flex w-full justify-start">
<Button
buttonType="filled"
palette="inverse"
size="large"
href={ctaHref}
>
{ctaText}
</Button>
</div>
) : null}
</div>
</section>
);
}
TripleTextBlockView.displayName = "TripleTextBlockView";
export default memo(TripleTextBlockView);
@@ -0,0 +1,5 @@
export { default } from "./TripleTextBlock.container";
export type {
TripleTextBlockProps,
TripleTextBlockColumn,
} from "./TripleTextBlock.types";