Match the marketing homepage and About page to the latest Figma: black hero CTA, spaces in word-split headings, redesigned feature grid, and cited About statistics.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-09 17:40:11 -06:00
co-authored by Cursor
parent 2d351987cf
commit 40225e2845
32 changed files with 362 additions and 219 deletions
+2 -9
View File
@@ -36,11 +36,6 @@ export default function AboutPage() {
const headerSegments = asArray<AboutHeaderSegment>(page.aboutHeader.segments); const headerSegments = asArray<AboutHeaderSegment>(page.aboutHeader.segments);
const statsItems = asArray<StatItem>(page.stats.items); const statsItems = asArray<StatItem>(page.stats.items);
const statsAsOf =
typeof page.stats.asOf === "string"
? page.stats.asOf
: String(page.stats.asOf ?? "");
const faqItems = asArray<FaqAccordionItem>(page.faq.items); const faqItems = asArray<FaqAccordionItem>(page.faq.items);
const tripleColumns = asArray<TripleTextBlockColumn>(page.tripleTextBlock.columns); const tripleColumns = asArray<TripleTextBlockColumn>(page.tripleTextBlock.columns);
@@ -57,10 +52,8 @@ export default function AboutPage() {
titlePrefix={page.stats.titlePrefix} titlePrefix={page.stats.titlePrefix}
titleEmphasis={page.stats.titleEmphasis} titleEmphasis={page.stats.titleEmphasis}
titleSuffix={page.stats.titleSuffix} titleSuffix={page.stats.titleSuffix}
items={statsItems.map((item) => ({ asOfPrefix={page.stats.asOfPrefix}
...item, items={statsItems}
asOf: statsAsOf,
}))}
/> />
<TripleTextBlock columns={tripleColumns} /> <TripleTextBlock columns={tripleColumns} />
<Book <Book
+3
View File
@@ -4,6 +4,9 @@ export interface StatProps {
value: string; value: string;
label: string; label: string;
asOf?: string; asOf?: string;
asOfPrefix?: string;
sourceHref?: string;
sourceName?: string;
shapeVariant?: StatShapeVariant; shapeVariant?: StatShapeVariant;
className?: string; className?: string;
} }
+21
View File
@@ -11,9 +11,14 @@ function StatView({
value, value,
label, label,
asOf, asOf,
asOfPrefix,
sourceHref,
sourceName,
shapeVariant, shapeVariant,
className = "", className = "",
}: StatViewProps) { }: StatViewProps) {
const sourcedYear = Boolean(asOfPrefix || sourceHref);
return ( return (
<article <article
className={`relative flex h-auto min-h-[182px] w-full flex-col items-start justify-between rounded-[var(--radius-measures-radius-xlarge,20px)] bg-[var(--color-surface-invert-primary,white)] px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)] sm:h-[170px] sm:min-h-0 sm:p-[var(--spacing-scale-024)] ${className}`.trim()} className={`relative flex h-auto min-h-[182px] w-full flex-col items-start justify-between rounded-[var(--radius-measures-radius-xlarge,20px)] bg-[var(--color-surface-invert-primary,white)] px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)] sm:h-[170px] sm:min-h-0 sm:p-[var(--spacing-scale-024)] ${className}`.trim()}
@@ -34,7 +39,23 @@ function StatView({
</div> </div>
{asOf ? ( {asOf ? (
<p className="w-full text-xx-small-paragraph text-[var(--color-content-invert-tertiary,#2d2d2d)]"> <p className="w-full text-xx-small-paragraph text-[var(--color-content-invert-tertiary,#2d2d2d)]">
{sourcedYear ? (
<>
{asOfPrefix ? <span>{`${asOfPrefix} `}</span> : null}
{sourceHref ? (
<a href={sourceHref} className="underline">
{asOf} {asOf}
{sourceName ? (
<span className="sr-only">{` (${sourceName})`}</span>
) : null}
</a>
) : (
<span className="underline">{asOf}</span>
)}
</>
) : (
asOf
)}
</p> </p>
) : null} ) : null}
</article> </article>
@@ -1,11 +1,11 @@
"use client"; "use client";
/** /**
* Figma: "Section / Feature-Grid" (18847:22410) * Figma: "Section / Feature-Grid" (18632:10668)
*/ */
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import { getAssetPath, featurePanelLayout, featurePanelPath } from "../../../../lib/assetUtils"; import { featureIconPath, getAssetPath } from "../../../../lib/assetUtils";
import { useTranslation } from "../../../contexts/MessagesContext"; import { useTranslation } from "../../../contexts/MessagesContext";
import FeatureGridView from "./FeatureGrid.view"; import FeatureGridView from "./FeatureGrid.view";
import type { FeatureGridProps, Feature } from "./FeatureGrid.types"; import type { FeatureGridProps, Feature } from "./FeatureGrid.types";
@@ -17,52 +17,40 @@ const FeatureGridContainer = memo<FeatureGridProps>(
const features: Feature[] = useMemo( const features: Feature[] = useMemo(
() => [ () => [
{ {
backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]", backgroundColor: "bg-[var(--color-surface-invert-brand-lavender)]",
labelLine1: t( title: t("pages.home.featureGrid.features.decisionMaking.title"),
"pages.home.featureGrid.features.decisionMaking.labelLine1", description: t(
"pages.home.featureGrid.features.decisionMaking.description",
), ),
labelLine2: t( descriptionClassName: "text-[#3a3273]",
"pages.home.featureGrid.features.decisionMaking.labelLine2", iconSrc: getAssetPath(featureIconPath("git-branch")),
),
panelContent: getAssetPath(featurePanelPath("support")),
...featurePanelLayout("support"),
ariaLabel: t("featureGrid.features.decisionMaking.ariaLabel"),
}, },
{ {
backgroundColor: "bg-[var(--color-surface-invert-brand-lime)]", backgroundColor: "bg-[var(--color-surface-invert-brand-lime)]",
labelLine1: t( title: t("pages.home.featureGrid.features.valuesAlignment.title"),
"pages.home.featureGrid.features.valuesAlignment.labelLine1", description: t(
"pages.home.featureGrid.features.valuesAlignment.description",
), ),
labelLine2: t( descriptionClassName: "text-[#424f1a]",
"pages.home.featureGrid.features.valuesAlignment.labelLine2", iconSrc: getAssetPath(featureIconPath("arrow-left-right")),
),
panelContent: getAssetPath(featurePanelPath("exercises")),
...featurePanelLayout("exercises"),
ariaLabel: t("featureGrid.features.valuesAlignment.ariaLabel"),
}, },
{ {
backgroundColor: "bg-[var(--color-surface-invert-brand-rust)]", backgroundColor: "bg-[var(--color-surface-invert-brand-rust)]",
labelLine1: t( title: t("pages.home.featureGrid.features.membershipGuidance.title"),
"pages.home.featureGrid.features.membershipGuidance.labelLine1", description: t(
"pages.home.featureGrid.features.membershipGuidance.description",
), ),
labelLine2: t( descriptionClassName: "text-[#5e2e23]",
"pages.home.featureGrid.features.membershipGuidance.labelLine2", iconSrc: getAssetPath(featureIconPath("door-open")),
),
panelContent: getAssetPath(featurePanelPath("guidance")),
...featurePanelLayout("guidance"),
ariaLabel: t("featureGrid.features.membershipGuidance.ariaLabel"),
}, },
{ {
backgroundColor: "bg-[var(--color-surface-invert-brand-teal)]", backgroundColor: "bg-[var(--color-surface-invert-brand-teal)]",
labelLine1: t( title: t("pages.home.featureGrid.features.conflictResolution.title"),
"pages.home.featureGrid.features.conflictResolution.labelLine1", description: t(
"pages.home.featureGrid.features.conflictResolution.description",
), ),
labelLine2: t( descriptionClassName: "text-[#1f4d47]",
"pages.home.featureGrid.features.conflictResolution.labelLine2", iconSrc: getAssetPath(featureIconPath("message-square-share")),
),
panelContent: getAssetPath(featurePanelPath("tools")),
...featurePanelLayout("tools"),
ariaLabel: t("featureGrid.features.conflictResolution.ariaLabel"),
}, },
], ],
[t], [t],
@@ -6,13 +6,10 @@ export interface FeatureGridProps {
export interface Feature { export interface Feature {
backgroundColor: string; backgroundColor: string;
labelLine1: string; title: string;
labelLine2: string; description: string;
panelContent: string; descriptionClassName: string;
panelWidth: number; iconSrc: string;
panelHeight: number;
panelImageClassName?: string;
ariaLabel: string;
} }
export interface FeatureGridViewProps extends FeatureGridProps { export interface FeatureGridViewProps extends FeatureGridProps {
@@ -2,10 +2,9 @@
import { useTranslation } from "../../../contexts/MessagesContext"; import { useTranslation } from "../../../contexts/MessagesContext";
import ContentLockup from "../../type/ContentLockup"; import ContentLockup from "../../type/ContentLockup";
import Mini from "../../cards/Mini";
import type { FeatureGridViewProps } from "./FeatureGrid.types"; import type { FeatureGridViewProps } from "./FeatureGrid.types";
/** Figma **Section / Feature-Grid** [18847:22410](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=18847-22410&m=dev). */ /** Figma **Section / Feature-Grid** [18632:10668](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=18632-10668&m=dev). */
function FeatureGridView({ function FeatureGridView({
title, title,
subtitle, subtitle,
@@ -20,16 +19,16 @@ function FeatureGridView({
return ( return (
<section <section
className={`p-0 lg:p-[var(--spacing-scale-064)] ${className}`} className={`p-0 md:px-[var(--spacing-scale-024)] lg:p-[var(--spacing-scale-064)] ${className}`}
aria-labelledby={labelledBy} aria-labelledby={labelledBy}
aria-label={labelledBy ? undefined : ariaLabel} aria-label={labelledBy ? undefined : ariaLabel}
> >
<div <div
data-figma-node="18847-22410" data-figma-node="18632-10668"
className="rounded-[var(--measures-radius-500,20px)] bg-[var(--color-surface-default-secondary)] px-[var(--spacing-scale-020)] py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-048)] md:pb-[var(--spacing-scale-048)] md:pt-[var(--spacing-scale-076)] lg:pb-[var(--spacing-scale-076)]" className="rounded-none bg-[var(--color-surface-default-secondary)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-032)] md:rounded-[var(--radius-measures-radius-large)] md:p-[var(--spacing-scale-048)] lg:p-[var(--spacing-scale-064)]"
> >
<div className="mx-auto w-full gap-[var(--spacing-scale-048)] [container-type:inline-size] lg:flex lg:items-start lg:gap-[var(--spacing-scale-048)]"> <div className="mx-auto flex w-full flex-col gap-[var(--spacing-scale-040)] md:gap-[var(--spacing-scale-048)] lg:flex-row lg:items-center lg:gap-[var(--spacing-scale-064)]">
<div className="lg:min-w-0 lg:shrink"> <div className="lg:min-w-0 lg:flex-1">
<ContentLockup <ContentLockup
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
@@ -40,20 +39,34 @@ function FeatureGridView({
/> />
</div> </div>
<div className="mt-[var(--spacing-scale-048)] grid grid-cols-2 grid-rows-[repeat(2,minmax(0,1fr))] gap-x-[12px] gap-y-[12px] max-md:min-h-[384px] md:grid-cols-4 md:grid-rows-1 md:min-h-0 lg:mt-0 lg:shrink-0 lg:flex-grow"> <div className="grid w-full grid-cols-1 gap-[var(--spacing-scale-008)] md:grid-cols-2 md:gap-[var(--spacing-scale-016)] lg:flex-1">
{features.map((feature, index) => ( {features.map((feature) => (
<Mini <div
key={index} key={feature.title}
backgroundColor={feature.backgroundColor} className={`flex flex-col gap-[var(--spacing-scale-016)] p-[var(--spacing-scale-024)] ${feature.backgroundColor} rounded-[var(--radius-200)] md:rounded-[var(--radius-400)] lg:rounded-[var(--radius-measures-radius-large)]`}
labelLine1={feature.labelLine1} >
labelLine2={feature.labelLine2} <div className="size-[40px] shrink-0 overflow-clip">
panelContent={feature.panelContent} {/* eslint-disable-next-line @next/next/no-img-element -- decorative Figma glyph */}
panelWidth={feature.panelWidth} <img
panelHeight={feature.panelHeight} src={feature.iconSrc}
panelImageClassName={feature.panelImageClassName} alt=""
ariaLabel={feature.ariaLabel} width={40}
featureGridShell height={40}
className="size-full object-contain"
role="presentation"
/> />
</div>
<div className="flex w-full flex-col gap-[var(--spacing-scale-008)]">
<p className="font-semibold text-[17px] leading-[22px] text-[var(--color-content-inverse-primary)]">
{feature.title}
</p>
<p
className={`text-small-paragraph ${feature.descriptionClassName}`}
>
{feature.description}
</p>
</div>
</div>
))} ))}
</div> </div>
</div> </div>
@@ -4,6 +4,9 @@ export interface StatItem {
value: string; value: string;
label: string; label: string;
asOf?: string; asOf?: string;
asOfPrefix?: string;
sourceHref?: string;
sourceName?: string;
shapeVariant?: StatShapeVariant; shapeVariant?: StatShapeVariant;
} }
@@ -11,6 +14,7 @@ export interface StatsProps {
titlePrefix?: string; titlePrefix?: string;
titleEmphasis?: string; titleEmphasis?: string;
titleSuffix?: string; titleSuffix?: string;
asOfPrefix?: string;
items: StatItem[]; items: StatItem[];
className?: string; className?: string;
} }
+6 -1
View File
@@ -21,6 +21,7 @@ function StatsView({
titlePrefix, titlePrefix,
titleEmphasis, titleEmphasis,
titleSuffix, titleSuffix,
asOfPrefix,
items, items,
headingId, headingId,
className = "", className = "",
@@ -88,7 +89,11 @@ function StatsView({
return ( return (
<li key={`${item.value}-${index}`} className={staggerClass}> <li key={`${item.value}-${index}`} className={staggerClass}>
<Stat {...item} className={heightClass} /> <Stat
{...item}
asOfPrefix={item.asOfPrefix ?? asOfPrefix}
className={heightClass}
/>
</li> </li>
); );
})} })}
@@ -25,16 +25,27 @@ function AboutHeaderView({
variant="about" variant="about"
alignment="left" alignment="left"
titleId={titleId} titleId={titleId}
titleContent={segments.map((segment, index) => { titleContent={segments.flatMap((segment, index) => {
if (segment.type === "word") { const spaceBefore =
return ( segment.type === "word" && index > 0 ? (
<span key={`${segment.text}-${index}`} className="whitespace-nowrap"> <span key={`space-${index}`} className="sr-only">
{segment.text} {" "}
</span> </span>
); ) : null;
if (segment.type === "word") {
return [
spaceBefore,
<span
key={`${segment.text}-${index}`}
className="whitespace-nowrap"
>
{segment.text}
</span>,
];
} }
return ( return [
<span <span
key={`${segment.icon}-${index}`} key={`${segment.icon}-${index}`}
className={ className={
@@ -50,8 +61,8 @@ function AboutHeaderView({
className="size-full object-contain" className="size-full object-contain"
role="presentation" role="presentation"
/> />
</span> </span>,
); ];
})} })}
/> />
</section> </section>
@@ -84,8 +84,13 @@ function ContentLockupView({
)} )}
</div> </div>
{/* Subtitle */} {subtitle ? (
{subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null} variant === "feature" ? (
<p className={styles.subtitle}>{subtitle}</p>
) : (
<h2 className={styles.subtitle}>{subtitle}</h2>
)
) : null}
</div> </div>
{/* Description */} {/* Description */}
@@ -110,7 +115,7 @@ function ContentLockupView({
<div className="block md:hidden"> <div className="block md:hidden">
<Button <Button
buttonType="filled" buttonType="filled"
palette={variant === "hero" ? "default" : "inverse"} palette="inverse"
size="small" size="small"
href={ctaHref} href={ctaHref}
> >
@@ -121,7 +126,7 @@ function ContentLockupView({
<div className="hidden md:block xl:hidden"> <div className="hidden md:block xl:hidden">
<Button <Button
buttonType="filled" buttonType="filled"
palette={variant === "hero" ? "default" : "inverse"} palette="inverse"
size="large" size="large"
className={buttonClassName} className={buttonClassName}
href={ctaHref} href={ctaHref}
@@ -133,7 +138,7 @@ function ContentLockupView({
<div className="hidden xl:block"> <div className="hidden xl:block">
<Button <Button
buttonType="filled" buttonType="filled"
palette={variant === "hero" ? "default" : "inverse"} palette="inverse"
size="xlarge" size="xlarge"
href={ctaHref} href={ctaHref}
> >
+8 -22
View File
@@ -114,29 +114,15 @@ export function featurePanelPath(key: FeaturePanelKey): string {
return `assets/marketing/feature-${key}.svg`; return `assets/marketing/feature-${key}.svg`;
} }
/** Intrinsic icon bounds from Figma Feature-Grid (18632:10911). */ /** Feature-grid card glyphs (Figma **18632:10668**). */
export const FEATURE_PANEL_LAYOUT: Record< export type FeatureIconKey =
FeaturePanelKey, | "git-branch"
{ width: number; height: number; panelImageClassName?: string } | "arrow-left-right"
> = { | "door-open"
support: { width: 48, height: 48 }, | "message-square-share";
exercises: { width: 55, height: 48 },
guidance: { width: 56, height: 39 },
tools: {
width: 50,
height: 47,
/** Figma 18632:10947 — raw asset is inverted; frame applies rotate + flip. */
panelImageClassName: "rotate-180 -scale-x-100",
},
};
export function featurePanelLayout(key: FeaturePanelKey): { export function featureIconPath(key: FeatureIconKey): string {
panelWidth: number; return `assets/marketing/feature-icon-${key}.svg`;
panelHeight: number;
panelImageClassName?: string;
} {
const { width, height, panelImageClassName } = FEATURE_PANEL_LAYOUT[key];
return { panelWidth: width, panelHeight: height, panelImageClassName };
} }
/** Case study card artwork in `public/assets/case-study/`. */ /** Case study card artwork in `public/assets/case-study/`. */
+6 -6
View File
@@ -1,20 +1,20 @@
{ {
"_comment": "FeatureGrid component defaults (shared across pages). Learn more goes to /learn; feature cards stay non-interactive until destinations are decided.", "_comment": "FeatureGrid component defaults (shared across pages). Explore the toolkit goes to /learn; feature cards stay non-interactive until destinations are decided.",
"linkText": "Learn more", "linkText": "Explore the toolkit",
"linkHref": "/learn", "linkHref": "/learn",
"ariaLabel": "Feature tools and services", "ariaLabel": "Feature tools and services",
"features": { "features": {
"decisionMaking": { "decisionMaking": {
"ariaLabel": "Decision-making support tools" "ariaLabel": "Choose how you decide"
}, },
"valuesAlignment": { "valuesAlignment": {
"ariaLabel": "Values alignment exercises" "ariaLabel": "Name your values"
}, },
"membershipGuidance": { "membershipGuidance": {
"ariaLabel": "Membership guidance resources" "ariaLabel": "Define membership"
}, },
"conflictResolution": { "conflictResolution": {
"ariaLabel": "Conflict resolution tools" "ariaLabel": "Plan for conflict"
} }
} }
} }
+19 -7
View File
@@ -23,27 +23,39 @@
"titlePrefix": "From", "titlePrefix": "From",
"titleEmphasis": "projects", "titleEmphasis": "projects",
"titleSuffix": "to communities", "titleSuffix": "to communities",
"asOf": "as of June 30, 2024", "asOfPrefix": "as of",
"items": [ "items": [
{ {
"value": "420M+", "value": "420M+",
"label": "open source projects", "label": "open source projects",
"shapeVariant": "yellow" "shapeVariant": "yellow",
"asOf": "2025",
"sourceHref": "https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/",
"sourceName": "GitHub Octoverse"
}, },
{ {
"value": "27%", "value": "24%",
"label": "year over year growth in open source", "label": "year over year growth in open source",
"shapeVariant": "purple" "shapeVariant": "purple",
"asOf": "2025",
"sourceHref": "https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/",
"sourceName": "GitHub Octoverse"
}, },
{ {
"value": "45%", "value": "45%",
"label": "adults in the US participate in community service", "label": "adults in the US participate in community service",
"shapeVariant": "green" "shapeVariant": "green",
"asOf": "2025",
"sourceHref": "https://www.americorps.gov/about/our-impact/volunteering-civic-life",
"sourceName": "AmeriCorps Volunteering and Civic Life in America"
}, },
{ {
"value": "8000+", "value": "800+",
"label": "mutual aid groups in the U.S.", "label": "mutual aid groups in the U.S.",
"shapeVariant": "orange" "shapeVariant": "orange",
"asOf": "2026",
"sourceHref": "https://www.nea.org/nea-today/all-news-articles/mutual-aid-act-resistance-and-community-support",
"sourceName": "Mutual Aid Hub via NEA"
} }
] ]
}, },
+10 -10
View File
@@ -29,24 +29,24 @@
} }
}, },
"featureGrid": { "featureGrid": {
"title": "We've got your back, every step of the way", "title": "Tools for the parts that get hard",
"subtitle": "Use our toolkit to improve, document, and evolve your organization.", "subtitle": "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
"features": { "features": {
"decisionMaking": { "decisionMaking": {
"labelLine1": "Decision-making", "title": "Choose how you decide",
"labelLine2": "support" "description": "Compare consensus, do-ocracy, and other patterns, then commit to one."
}, },
"valuesAlignment": { "valuesAlignment": {
"labelLine1": "Values alignment", "title": "Name your values",
"labelLine2": "exercises" "description": "Short exercises that surface what your group agrees on before it's tested."
}, },
"membershipGuidance": { "membershipGuidance": {
"labelLine1": "Membership", "title": "Define membership",
"labelLine2": "guidance" "description": "Spell out how people join, take on roles, and step away."
}, },
"conflictResolution": { "conflictResolution": {
"labelLine1": "Conflict resolution", "title": "Plan for conflict",
"labelLine2": "tools" "description": "Agree on how you'll handle disagreement while everyone is still calm."
} }
} }
}, },
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="arrow-left-right">
<path id="Vector" d="M13.334 18.3333L6.668 11.6667L13.334 5M6.668 11.6667H33.332M26.666 21.6667L33.332 28.3333L26.666 35M33.332 28.3333H6.668" stroke="#6AA90B" stroke-width="2" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 406 B

@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="door-open">
<path id="Vector" d="M18.3332 33.3351H3.332M31.6676 33.3354L20.4034 36.1505C20.1577 36.2117 19.9013 36.2162 19.6537 36.1636C19.406 36.111 19.1736 36.0027 18.974 35.8469C18.7745 35.691 18.6131 35.4918 18.5021 35.2643C18.391 35.0368 18.3333 34.7869 18.3332 34.5338V7.60489C18.3335 7.09852 18.4492 6.59721 18.6715 6.14222C18.8938 5.68724 19.2168 5.28887 19.6161 4.97737C20.0153 4.66586 20.4803 4.4494 20.9757 4.34442C21.4711 4.23944 21.9839 4.2487 22.4752 4.3715L29.1424 6.0382C29.8636 6.21846 30.5038 6.63458 30.9614 7.22044C31.4189 7.80629 31.6675 8.52826 31.6676 9.27159V33.3354ZM31.6676 33.3354L36.668 33.3351M18.3332 6.66788H13.3328C12.4487 6.66788 11.6008 7.01908 10.9756 7.64421C10.3504 8.26934 9.9992 9.11721 9.9992 10.0013V33.3351M23.3336 20.0015H23.3503" stroke="#D96043" stroke-width="2" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1018 B

@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="git-branch">
<path id="Vector" d="M25 10C21.0218 10 17.2064 11.5804 14.3934 14.3934C11.5804 17.2064 10 21.0218 10 25M25 10C25 12.7614 27.2386 15 30 15C32.7614 15 35 12.7614 35 10C35 7.23858 32.7614 5 30 5C27.2386 5 25 7.23858 25 10ZM10 25V5M10 25C7.23858 25 5 27.2386 5 30C5 32.7614 7.23858 35 10 35C12.7614 35 15 32.7614 15 30C15 27.2386 12.7614 25 10 25Z" stroke="#8D5AC4" stroke-width="2" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 602 B

@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="message-square-share">
<path id="Vector" d="M20 5H6.6656C5.78147 5 4.93356 5.35119 4.30839 5.97632C3.68322 6.60144 3.332 7.44929 3.332 8.33335V35.4768C3.33203 35.7108 3.40145 35.9396 3.53149 36.1342C3.66153 36.3287 3.84635 36.4804 4.06257 36.5699C4.27879 36.6595 4.51672 36.6829 4.74626 36.6373C4.9758 36.5916 5.18666 36.4789 5.35216 36.3135L9.02245 32.6435C9.64748 32.0183 10.4953 31.667 11.3793 31.6668H33.3344C34.2185 31.6668 35.0664 31.3156 35.6916 30.6905C36.3168 30.0654 36.668 29.2175 36.668 28.3334V21.6667M36.668 15V5H26.6672M36.668 5L26.6672 15" stroke="#16A9C4" stroke-width="2" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 800 B

+6 -6
View File
@@ -8,12 +8,12 @@ export default {
docs: { docs: {
description: { description: {
component: ` component: `
Feature grid for the home marketing section (Figma 18847:22410). Feature grid for the home marketing section (Figma 18632:10668).
- **Layout**: 2×2 on mobile, 1×4 on tablet, horizontal lockup + grid on desktop - **Layout**: stacked cards on mobile, 2×2 from tablet, horizontal lockup + grid on desktop
- **Shell**: \`surface/default/secondary\` content block with responsive spacing tokens - **Shell**: \`surface/default/secondary\` content block with responsive spacing tokens
- **ContentLockup**: Feature variant — 18/22 title & 14/20 subtitle (mobile), 24/32 title & 16/24 subtitle (640px+), 16/24 link - **ContentLockup**: Feature variant — 18/22 title & 14/20 subtitle (mobile), 24/32 title & 16/24 subtitle (640px+), 16/24 link
- **Mini tiles**: Invert-brand surfaces (royal, lime, rust, teal), 138px panel height, 48px icons - **Cards**: Invert-brand surfaces (lavender, lime, rust, teal) with title, description, and 40px icons
`, `,
}, },
}, },
@@ -36,15 +36,15 @@ Feature grid for the home marketing section (Figma 18847:22410).
export const Default = { export const Default = {
args: { args: {
title: "We've got your back, every step of the way", title: "Tools for the parts that get hard",
subtitle: subtitle:
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
}, },
parameters: { parameters: {
docs: { docs: {
description: { description: {
story: ` story: `
Default FeatureGrid — responsive breakpoint layout with Figma styling (invert-brand tiles, secondary surface, updated lockup typography). Default FeatureGrid — responsive breakpoint layout with Figma styling (invert-brand cards, secondary surface, updated lockup copy).
`, `,
}, },
}, },
+3 -3
View File
@@ -48,11 +48,11 @@ export const Feature = {
export const FeatureWithLink = { export const FeatureWithLink = {
args: { args: {
title: "We've got your back, every step of the way", title: "Tools for the parts that get hard",
subtitle: subtitle:
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
variant: "feature", variant: "feature",
linkText: "Learn more", linkText: "Explore the toolkit",
linkHref: "/learn", linkHref: "/learn",
}, },
}; };
+23 -39
View File
@@ -41,33 +41,33 @@ describe("FeatureGrid (behavioral tests)", () => {
expect( expect(
screen.getByRole("heading", { name: "Test Title" }), screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
expect( expect(
screen.getByRole("heading", { name: "Test Subtitle" }), screen.queryByRole("heading", { name: "Test Subtitle" }),
).toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("renders all four feature cards as static tiles", () => { it("renders all four feature cards as static tiles", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
expect(screen.getByText("Decision-making")).toBeInTheDocument(); expect(screen.getByText("Choose how you decide")).toBeInTheDocument();
expect(screen.getByText("Values alignment")).toBeInTheDocument(); expect(screen.getByText("Name your values")).toBeInTheDocument();
expect(screen.getByText("Membership")).toBeInTheDocument(); expect(screen.getByText("Define membership")).toBeInTheDocument();
expect(screen.getByText("Conflict resolution")).toBeInTheDocument(); expect(screen.getByText("Plan for conflict")).toBeInTheDocument();
expect( expect(
screen.queryByRole("link", { name: "Decision-making support tools" }), screen.queryByRole("link", { name: "Choose how you decide" }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("sends Learn more to the Learn page", () => { it("sends Explore the toolkit to the Learn page", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
expect(screen.getByRole("link", { name: "Learn more" })).toHaveAttribute( expect(
"href", screen.getByRole("link", { name: "Explore the toolkit" }),
"/learn", ).toHaveAttribute("href", "/learn");
);
}); });
it("does not apply a focus ring to the entire grid shell", () => { it("does not apply a focus ring to the entire grid shell", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
const shell = document.querySelector('[data-figma-node="18847-22410"]'); const shell = document.querySelector('[data-figma-node="18632-10668"]');
expect(shell?.className).not.toContain("focus-within:ring-2"); expect(shell?.className).not.toContain("focus-within:ring-2");
}); });
@@ -83,11 +83,11 @@ describe("FeatureGrid (behavioral tests)", () => {
expect(section).toBeInTheDocument(); expect(section).toBeInTheDocument();
}); });
it("uses Figma invert surface colors on mini tiles", () => { it("uses Figma invert surface colors on feature cards", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
expect( expect(
document.querySelector( document.querySelector(
'[class*="bg-[var(--color-surface-invert-brand-royal)]"]', '[class*="bg-[var(--color-surface-invert-brand-lavender)]"]',
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
@@ -110,7 +110,7 @@ describe("FeatureGrid (behavioral tests)", () => {
it("marks the content block with the Figma node id", () => { it("marks the content block with the Figma node id", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
expect( expect(
document.querySelector('[data-figma-node="18847-22410"]'), document.querySelector('[data-figma-node="18632-10668"]'),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -119,35 +119,19 @@ describe("FeatureGrid (behavioral tests)", () => {
const title = screen.getByRole("heading", { name: "Test Title" }); const title = screen.getByRole("heading", { name: "Test Title" });
expect(title.className).toMatch(/text-\[18px\]/); expect(title.className).toMatch(/text-\[18px\]/);
expect(title.className).toMatch(/md:text-\[length:var\(--sizing-600,24px\)\]/); expect(title.className).toMatch(/md:text-\[length:var\(--sizing-600,24px\)\]/);
const subtitle = screen.getByRole("heading", { name: "Test Subtitle" }); const subtitle = screen.getByText("Test Subtitle");
expect(subtitle.tagName).toBe("P");
expect(subtitle).toHaveClass("text-small-paragraph", "md:text-medium-paragraph"); expect(subtitle).toHaveClass("text-small-paragraph", "md:text-medium-paragraph");
}); });
it("applies grain texture to feature grid icons", () => { it("renders 40px decorative icons from Figma", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
const icons = document.querySelectorAll("section img"); const icons = document.querySelectorAll("section img");
expect(icons.length).toBeGreaterThanOrEqual(4); expect(icons.length).toBe(4);
icons.forEach((icon) => { icons.forEach((icon) => {
expect(icon.getAttribute("style")).toContain("#grain"); expect(icon).toHaveAttribute("width", "40");
expect(icon).toHaveAttribute("height", "40");
expect(icon).toHaveAttribute("alt", "");
}); });
}); });
it("preserves per-icon aspect ratios from Figma layout", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const icons = Array.from(document.querySelectorAll("section img"));
expect(icons.map((icon) => icon.getAttribute("width"))).toEqual([
"48",
"55",
"56",
"50",
]);
expect(icons.map((icon) => icon.getAttribute("height"))).toEqual([
"48",
"48",
"39",
"47",
]);
expect(icons[3]?.className).toContain("rotate-180");
expect(icons[3]?.className).toContain("-scale-x-100");
});
}); });
+5 -4
View File
@@ -57,10 +57,11 @@ describe("HeroBanner (behavioral tests)", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("renders CTA link when provided", () => { it("renders CTA with the inverse (black) filled palette", () => {
render(<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />); render(<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />);
expect( const cta = screen.getAllByRole("link", { name: "Get Started" })[0];
screen.getAllByRole("link", { name: "Get Started" }).length, expect(cta?.className).toContain(
).toBeGreaterThan(0); "bg-[var(--color-surface-default-primary)]",
);
}); });
}); });
+17
View File
@@ -78,4 +78,21 @@ describe("SectionHeader rule stack title max width", () => {
}), }),
).toHaveClass("md:max-w-[571px]"); ).toHaveClass("md:max-w-[571px]");
}); });
it("keeps spaces between stacked desktop title lines", () => {
render(
<SectionHeader
title="How CommunityRule helps"
subtitle="Start from a proven pattern."
variant="multi-line"
stackedDesktopLines={["How", "CommunityRule", "helps"]}
/>,
);
const heading = screen.getByRole("heading", {
name: /How CommunityRule helps/,
});
expect(heading.textContent).toMatch(/How CommunityRule helps/);
expect(heading.textContent).not.toMatch(/HowCommunityRulehelps/);
});
}); });
+20
View File
@@ -16,4 +16,24 @@ describe("Stat", () => {
expect(screen.getByText("open source projects")).toBeInTheDocument(); expect(screen.getByText("open source projects")).toBeInTheDocument();
expect(screen.getByText("as of June 30, 2024")).toBeInTheDocument(); expect(screen.getByText("as of June 30, 2024")).toBeInTheDocument();
}); });
it("links the as-of year when a source is provided", () => {
render(
<Stat
value="420M+"
label="open source projects"
asOfPrefix="as of"
asOf="2025"
sourceHref="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/"
sourceName="GitHub Octoverse"
/>,
);
expect(
screen.getByRole("link", { name: /2025/ }),
).toHaveAttribute(
"href",
"https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/",
);
});
}); });
@@ -16,6 +16,27 @@ describe("AboutHeader", () => {
expect( expect(
screen.getByRole("heading", { name: /CommunityRule helps/i }), screen.getByRole("heading", { name: /CommunityRule helps/i }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("heading").textContent).toMatch(
/CommunityRule helps/,
);
expect(screen.getByRole("heading").textContent).not.toMatch(
/CommunityRulehelps/,
);
});
it("keeps spaces when icons sit between words", () => {
render(
<AboutHeader
segments={[
{ type: "word", text: "CommunityRule" },
{ type: "icon", icon: "arrow" },
{ type: "word", text: "is" },
{ type: "word", text: "a" },
]}
/>,
);
expect(screen.getByRole("heading").textContent).toMatch(/CommunityRule is a/);
}); });
it("sizes inline marks to the 320 / 640 / 1440 About Header variants", () => { it("sizes inline marks to the 320 / 640 / 1440 About Header variants", () => {
@@ -25,6 +25,24 @@ describe("ContentLockup", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("uses the inverse filled palette on the hero CTA", () => {
renderWithProviders(
<ContentLockup
variant="hero"
title="Collaborate"
ctaText="Learn how CommunityRule works"
ctaHref="/how-it-works"
/>,
);
const cta = screen.getAllByRole("link", {
name: "Learn how CommunityRule works",
})[0];
expect(cta?.className).toContain(
"bg-[var(--color-surface-default-primary)]",
);
});
it("uses Display line-height and Small/Paragraph on the hero lockup", () => { it("uses Display line-height and Small/Paragraph on the hero lockup", () => {
renderWithProviders( renderWithProviders(
<ContentLockup <ContentLockup
+16 -17
View File
@@ -52,10 +52,10 @@ test.describe("Critical User Journeys", () => {
// 7. User checks out features // 7. User checks out features
const features = [ const features = [
"Decision-making support", "Choose how you decide",
"Values alignment exercises", "Name your values",
"Membership guidance", "Define membership",
"Conflict resolution tools", "Plan for conflict",
]; ];
for (const feature of features) { for (const feature of features) {
@@ -96,7 +96,7 @@ test.describe("Critical User Journeys", () => {
await expect(howItWorksHeading).toBeVisible(); await expect(howItWorksHeading).toBeVisible();
await expect( await expect(
page.locator("h1").filter({ hasText: "We've got your back" }), page.locator("h1").filter({ hasText: "Tools for the parts that get hard" }),
).toBeVisible(); ).toBeVisible();
// Check key components are rendered // Check key components are rendered
@@ -108,33 +108,32 @@ test.describe("Critical User Journeys", () => {
test("feature grid section functionality", async ({ page }) => { test("feature grid section functionality", async ({ page }) => {
// Check section header // Check section header
await expect( await expect(
page.locator('h1:has-text("We\'ve got your back")'), page.locator('h1:has-text("Tools for the parts that get hard")'),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.locator( page.locator(
"text=Use our toolkit to improve, document, and evolve your organization", "text=Every group runs into the same knots. CommunityRule gives you a way through",
), ),
).toBeVisible(); ).toBeVisible();
// Check all four feature cards - FeatureGrid uses section with grid layout // Check all four feature cards - FeatureGrid uses section with grid layout
const featureSection = page.locator( const featureSection = page.locator(
'section[aria-label="Feature tools and services"], section:has-text("We\'ve got your back")', 'section[aria-label="Feature tools and services"], section:has-text("Tools for the parts that get hard")',
); );
await expect(featureSection.locator("text=Decision-making")).toBeVisible(); await expect(featureSection.locator("text=Choose how you decide")).toBeVisible();
await expect(featureSection.locator("text=Values alignment")).toBeVisible(); await expect(featureSection.locator("text=Name your values")).toBeVisible();
await expect(featureSection.locator("text=Membership")).toBeVisible(); await expect(featureSection.locator("text=Define membership")).toBeVisible();
await expect( await expect(
featureSection.locator("text=Conflict resolution"), featureSection.locator("text=Plan for conflict"),
).toBeVisible(); ).toBeVisible();
// Feature tiles are presentational — only the lockup "Learn more" is a link // Feature tiles are presentational — only the lockup "Explore the toolkit" is a link
await expect( await expect(
featureSection.locator('a[href="#decision-making"]'), featureSection.locator('a[href="#decision-making"]'),
).toHaveCount(0); ).toHaveCount(0);
await expect(featureSection.getByRole("link", { name: "Learn more" })).toHaveAttribute( await expect(
"href", featureSection.getByRole("link", { name: "Explore the toolkit" }),
"/learn", ).toHaveAttribute("href", "/learn");
);
}); });
test("header navigation functionality", async ({ page }) => { test("header navigation functionality", async ({ page }) => {
+23
View File
@@ -63,4 +63,27 @@ describe("AboutPage", () => {
}), }),
).toHaveAttribute("href", getAssetPath(governanceBookletPath())); ).toHaveAttribute("href", getAssetPath(governanceBookletPath()));
}); });
test("shows sourced About statistics with as-of years", () => {
render(<AboutPage />);
const page = messages.pages.about;
expect(screen.getByText("420M+")).toBeInTheDocument();
expect(screen.getByText("24%")).toBeInTheDocument();
expect(screen.getByText("45%")).toBeInTheDocument();
expect(screen.getByText("800+")).toBeInTheDocument();
expect(screen.queryByText("8000+")).not.toBeInTheDocument();
expect(screen.queryByText("27%")).not.toBeInTheDocument();
for (const item of page.stats.items) {
expect(screen.getByText(item.label)).toBeInTheDocument();
if (item.sourceHref) {
expect(
screen.getAllByRole("link").some(
(link) => link.getAttribute("href") === item.sourceHref,
),
).toBe(true);
}
}
});
}); });
+11 -9
View File
@@ -35,13 +35,13 @@ describe("Page", () => {
// FeatureGrid is next/dynamic — wait like other code-split sections // FeatureGrid is next/dynamic — wait like other code-split sections
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way") screen.getAllByText("Tools for the parts that get hard")
.length, .length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
screen.getAllByText( screen.getAllByText(
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
).length, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
@@ -111,16 +111,18 @@ describe("Page", () => {
// Wait for dynamically imported FeatureGrid component to load // Wait for dynamically imported FeatureGrid component to load
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way") screen.getAllByText("Tools for the parts that get hard")
.length, .length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
screen.getAllByText( screen.getAllByText(
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
).length, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
const learnMore = screen.getAllByRole("link", { name: "Learn more" }); const learnMore = screen.getAllByRole("link", {
name: "Explore the toolkit",
});
expect(learnMore[0]).toHaveAttribute("href", "/learn"); expect(learnMore[0]).toHaveAttribute("href", "/learn");
}); });
@@ -157,7 +159,7 @@ describe("Page", () => {
// FeatureGrid // FeatureGrid
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way") screen.getAllByText("Tools for the parts that get hard")
.length, .length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -211,7 +213,7 @@ describe("Page", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText( screen.getAllByText(
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
).length, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -236,7 +238,7 @@ describe("Page", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way") screen.getAllByText("Tools for the parts that get hard")
.length, .length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -285,7 +287,7 @@ describe("Page", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText( screen.getAllByText(
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
).length, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
+3 -3
View File
@@ -90,13 +90,13 @@ describe("Page Flow Integration", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByRole("heading", { screen.getByRole("heading", {
name: "We've got your back, every step of the way", name: "Tools for the parts that get hard",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
expect( expect(
screen.getByText( screen.getByText(
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -233,7 +233,7 @@ describe("Page Flow Integration", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("We've got your back, every step of the way"), screen.getByText("Tools for the parts that get hard"),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
+3 -3
View File
@@ -151,12 +151,12 @@ describe("User Journey Integration", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("We've got your back, every step of the way"), screen.getByText("Tools for the parts that get hard"),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
expect( expect(
screen.getByText( screen.getByText(
"Use our toolkit to improve, document, and evolve your organization.", "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -206,7 +206,7 @@ describe("User Journey Integration", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("We've got your back, every step of the way"), screen.getByText("Tools for the parts that get hard"),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });