Implement about page
This commit is contained in:
@@ -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";
|
||||
Reference in New Issue
Block a user