Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42716c262 | ||
|
|
40225e2845 |
@@ -36,11 +36,6 @@ export default function AboutPage() {
|
||||
const headerSegments = asArray<AboutHeaderSegment>(page.aboutHeader.segments);
|
||||
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 tripleColumns = asArray<TripleTextBlockColumn>(page.tripleTextBlock.columns);
|
||||
|
||||
@@ -57,10 +52,8 @@ export default function AboutPage() {
|
||||
titlePrefix={page.stats.titlePrefix}
|
||||
titleEmphasis={page.stats.titleEmphasis}
|
||||
titleSuffix={page.stats.titleSuffix}
|
||||
items={statsItems.map((item) => ({
|
||||
...item,
|
||||
asOf: statsAsOf,
|
||||
}))}
|
||||
asOfPrefix={page.stats.asOfPrefix}
|
||||
items={statsItems}
|
||||
/>
|
||||
<TripleTextBlock columns={tripleColumns} />
|
||||
<Book
|
||||
|
||||
@@ -4,6 +4,9 @@ export interface StatProps {
|
||||
value: string;
|
||||
label: string;
|
||||
asOf?: string;
|
||||
asOfPrefix?: string;
|
||||
sourceHref?: string;
|
||||
sourceName?: string;
|
||||
shapeVariant?: StatShapeVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,14 @@ function StatView({
|
||||
value,
|
||||
label,
|
||||
asOf,
|
||||
asOfPrefix,
|
||||
sourceHref,
|
||||
sourceName,
|
||||
shapeVariant,
|
||||
className = "",
|
||||
}: StatViewProps) {
|
||||
const sourcedYear = Boolean(asOfPrefix || sourceHref);
|
||||
|
||||
return (
|
||||
<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()}
|
||||
@@ -34,7 +39,23 @@ function StatView({
|
||||
</div>
|
||||
{asOf ? (
|
||||
<p className="w-full text-xx-small-paragraph text-[var(--color-content-invert-tertiary,#2d2d2d)]">
|
||||
{asOf}
|
||||
{sourcedYear ? (
|
||||
<>
|
||||
{asOfPrefix ? <span>{`${asOfPrefix} `}</span> : null}
|
||||
{sourceHref ? (
|
||||
<a href={sourceHref} className="underline">
|
||||
{asOf}
|
||||
{sourceName ? (
|
||||
<span className="sr-only">{` (${sourceName})`}</span>
|
||||
) : null}
|
||||
</a>
|
||||
) : (
|
||||
<span className="underline">{asOf}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
asOf
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Feature-Grid" (18847:22410)
|
||||
* Figma: "Section / Feature-Grid" (18632:10668)
|
||||
*/
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import { getAssetPath, featurePanelLayout, featurePanelPath } from "../../../../lib/assetUtils";
|
||||
import { featureIconPath, getAssetPath } from "../../../../lib/assetUtils";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import FeatureGridView from "./FeatureGrid.view";
|
||||
import type { FeatureGridProps, Feature } from "./FeatureGrid.types";
|
||||
@@ -17,52 +17,40 @@ const FeatureGridContainer = memo<FeatureGridProps>(
|
||||
const features: Feature[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]",
|
||||
labelLine1: t(
|
||||
"pages.home.featureGrid.features.decisionMaking.labelLine1",
|
||||
backgroundColor: "bg-[var(--color-surface-invert-brand-lavender)]",
|
||||
title: t("pages.home.featureGrid.features.decisionMaking.title"),
|
||||
description: t(
|
||||
"pages.home.featureGrid.features.decisionMaking.description",
|
||||
),
|
||||
labelLine2: t(
|
||||
"pages.home.featureGrid.features.decisionMaking.labelLine2",
|
||||
),
|
||||
panelContent: getAssetPath(featurePanelPath("support")),
|
||||
...featurePanelLayout("support"),
|
||||
ariaLabel: t("featureGrid.features.decisionMaking.ariaLabel"),
|
||||
descriptionClassName: "text-[#3a3273]",
|
||||
iconSrc: getAssetPath(featureIconPath("git-branch")),
|
||||
},
|
||||
{
|
||||
backgroundColor: "bg-[var(--color-surface-invert-brand-lime)]",
|
||||
labelLine1: t(
|
||||
"pages.home.featureGrid.features.valuesAlignment.labelLine1",
|
||||
title: t("pages.home.featureGrid.features.valuesAlignment.title"),
|
||||
description: t(
|
||||
"pages.home.featureGrid.features.valuesAlignment.description",
|
||||
),
|
||||
labelLine2: t(
|
||||
"pages.home.featureGrid.features.valuesAlignment.labelLine2",
|
||||
),
|
||||
panelContent: getAssetPath(featurePanelPath("exercises")),
|
||||
...featurePanelLayout("exercises"),
|
||||
ariaLabel: t("featureGrid.features.valuesAlignment.ariaLabel"),
|
||||
descriptionClassName: "text-[#424f1a]",
|
||||
iconSrc: getAssetPath(featureIconPath("arrow-left-right")),
|
||||
},
|
||||
{
|
||||
backgroundColor: "bg-[var(--color-surface-invert-brand-rust)]",
|
||||
labelLine1: t(
|
||||
"pages.home.featureGrid.features.membershipGuidance.labelLine1",
|
||||
title: t("pages.home.featureGrid.features.membershipGuidance.title"),
|
||||
description: t(
|
||||
"pages.home.featureGrid.features.membershipGuidance.description",
|
||||
),
|
||||
labelLine2: t(
|
||||
"pages.home.featureGrid.features.membershipGuidance.labelLine2",
|
||||
),
|
||||
panelContent: getAssetPath(featurePanelPath("guidance")),
|
||||
...featurePanelLayout("guidance"),
|
||||
ariaLabel: t("featureGrid.features.membershipGuidance.ariaLabel"),
|
||||
descriptionClassName: "text-[#5e2e23]",
|
||||
iconSrc: getAssetPath(featureIconPath("door-open")),
|
||||
},
|
||||
{
|
||||
backgroundColor: "bg-[var(--color-surface-invert-brand-teal)]",
|
||||
labelLine1: t(
|
||||
"pages.home.featureGrid.features.conflictResolution.labelLine1",
|
||||
title: t("pages.home.featureGrid.features.conflictResolution.title"),
|
||||
description: t(
|
||||
"pages.home.featureGrid.features.conflictResolution.description",
|
||||
),
|
||||
labelLine2: t(
|
||||
"pages.home.featureGrid.features.conflictResolution.labelLine2",
|
||||
),
|
||||
panelContent: getAssetPath(featurePanelPath("tools")),
|
||||
...featurePanelLayout("tools"),
|
||||
ariaLabel: t("featureGrid.features.conflictResolution.ariaLabel"),
|
||||
descriptionClassName: "text-[#1f4d47]",
|
||||
iconSrc: getAssetPath(featureIconPath("message-square-share")),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
|
||||
@@ -6,13 +6,10 @@ export interface FeatureGridProps {
|
||||
|
||||
export interface Feature {
|
||||
backgroundColor: string;
|
||||
labelLine1: string;
|
||||
labelLine2: string;
|
||||
panelContent: string;
|
||||
panelWidth: number;
|
||||
panelHeight: number;
|
||||
panelImageClassName?: string;
|
||||
ariaLabel: string;
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionClassName: string;
|
||||
iconSrc: string;
|
||||
}
|
||||
|
||||
export interface FeatureGridViewProps extends FeatureGridProps {
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import Mini from "../../cards/Mini";
|
||||
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({
|
||||
title,
|
||||
subtitle,
|
||||
@@ -20,16 +19,16 @@ function FeatureGridView({
|
||||
|
||||
return (
|
||||
<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-label={labelledBy ? undefined : ariaLabel}
|
||||
>
|
||||
<div
|
||||
data-figma-node="18847-22410"
|
||||
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)]"
|
||||
data-figma-node="18632-10668"
|
||||
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="lg:min-w-0 lg:shrink">
|
||||
<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:flex-1">
|
||||
<ContentLockup
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
@@ -40,20 +39,34 @@ function FeatureGridView({
|
||||
/>
|
||||
</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">
|
||||
{features.map((feature, index) => (
|
||||
<Mini
|
||||
key={index}
|
||||
backgroundColor={feature.backgroundColor}
|
||||
labelLine1={feature.labelLine1}
|
||||
labelLine2={feature.labelLine2}
|
||||
panelContent={feature.panelContent}
|
||||
panelWidth={feature.panelWidth}
|
||||
panelHeight={feature.panelHeight}
|
||||
panelImageClassName={feature.panelImageClassName}
|
||||
ariaLabel={feature.ariaLabel}
|
||||
featureGridShell
|
||||
/>
|
||||
<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) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
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)]`}
|
||||
>
|
||||
<div className="size-[40px] shrink-0 overflow-clip">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative Figma glyph */}
|
||||
<img
|
||||
src={feature.iconSrc}
|
||||
alt=""
|
||||
width={40}
|
||||
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>
|
||||
|
||||
@@ -4,6 +4,9 @@ export interface StatItem {
|
||||
value: string;
|
||||
label: string;
|
||||
asOf?: string;
|
||||
asOfPrefix?: string;
|
||||
sourceHref?: string;
|
||||
sourceName?: string;
|
||||
shapeVariant?: StatShapeVariant;
|
||||
}
|
||||
|
||||
@@ -11,6 +14,7 @@ export interface StatsProps {
|
||||
titlePrefix?: string;
|
||||
titleEmphasis?: string;
|
||||
titleSuffix?: string;
|
||||
asOfPrefix?: string;
|
||||
items: StatItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ function StatsView({
|
||||
titlePrefix,
|
||||
titleEmphasis,
|
||||
titleSuffix,
|
||||
asOfPrefix,
|
||||
items,
|
||||
headingId,
|
||||
className = "",
|
||||
@@ -88,7 +89,11 @@ function StatsView({
|
||||
|
||||
return (
|
||||
<li key={`${item.value}-${index}`} className={staggerClass}>
|
||||
<Stat {...item} className={heightClass} />
|
||||
<Stat
|
||||
{...item}
|
||||
asOfPrefix={item.asOfPrefix ?? asOfPrefix}
|
||||
className={heightClass}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -25,16 +25,27 @@ function AboutHeaderView({
|
||||
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}
|
||||
titleContent={segments.flatMap((segment, index) => {
|
||||
const spaceBefore =
|
||||
segment.type === "word" && index > 0 ? (
|
||||
<span key={`space-${index}`} className="sr-only">
|
||||
{" "}
|
||||
</span>
|
||||
);
|
||||
) : null;
|
||||
|
||||
if (segment.type === "word") {
|
||||
return [
|
||||
spaceBefore,
|
||||
<span
|
||||
key={`${segment.text}-${index}`}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{segment.text}
|
||||
</span>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
return [
|
||||
<span
|
||||
key={`${segment.icon}-${index}`}
|
||||
className={
|
||||
@@ -50,8 +61,8 @@ function AboutHeaderView({
|
||||
className="size-full object-contain"
|
||||
role="presentation"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
</span>,
|
||||
];
|
||||
})}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import NextLink from "next/link";
|
||||
import Button from "../../buttons/Button";
|
||||
import { contentLockupShapePath, getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type { ContentLockupViewProps } from "./ContentLockup.types";
|
||||
@@ -84,8 +85,13 @@ function ContentLockupView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
{subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null}
|
||||
{subtitle ? (
|
||||
variant === "feature" ? (
|
||||
<p className={styles.subtitle}>{subtitle}</p>
|
||||
) : (
|
||||
<h2 className={styles.subtitle}>{subtitle}</h2>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
@@ -93,14 +99,13 @@ function ContentLockupView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link for feature variant */}
|
||||
{variant === "feature" && linkText && linkHref && (
|
||||
<a
|
||||
<NextLink
|
||||
href={linkHref}
|
||||
className="text-medium-underline underline text-[var(--color-content-default-primary)] hover:text-[var(--color-content-default-secondary)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-secondary)] rounded-sm px-1 py-0.5"
|
||||
className="w-fit self-start cursor-pointer text-small-paragraph md:text-medium-paragraph text-[var(--color-content-default-primary)] underline decoration-solid [text-underline-position:from-font] hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]"
|
||||
>
|
||||
{linkText}
|
||||
</a>
|
||||
</NextLink>
|
||||
)}
|
||||
|
||||
{/* CTA Button */}
|
||||
@@ -110,7 +115,7 @@ function ContentLockupView({
|
||||
<div className="block md:hidden">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
palette="inverse"
|
||||
size="small"
|
||||
href={ctaHref}
|
||||
>
|
||||
@@ -121,7 +126,7 @@ function ContentLockupView({
|
||||
<div className="hidden md:block xl:hidden">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
palette="inverse"
|
||||
size="large"
|
||||
className={buttonClassName}
|
||||
href={ctaHref}
|
||||
@@ -133,7 +138,7 @@ function ContentLockupView({
|
||||
<div className="hidden xl:block">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
palette="inverse"
|
||||
size="xlarge"
|
||||
href={ctaHref}
|
||||
>
|
||||
|
||||
@@ -79,8 +79,8 @@ const SectionHeader = memo<SectionHeaderProps>(
|
||||
</span>
|
||||
{useStackedDesktop ? (
|
||||
<span className={splitFromMd ? "hidden md:block" : "hidden lg:block"}>
|
||||
<span className="block">{stackedDesktopLines[0]}</span>
|
||||
<span className="block">{stackedDesktopLines[1]}</span>
|
||||
<span className="block">{stackedDesktopLines[0]} </span>
|
||||
<span className="block">{stackedDesktopLines[1]} </span>
|
||||
<span className="block">{stackedDesktopLines[2]}</span>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -8,7 +8,7 @@ Convention for files served from `public/`. Path helpers live in
|
||||
```
|
||||
public/
|
||||
assets/
|
||||
icons/ # UI chrome (alert, close, help, pointer)
|
||||
icons/ # UI chrome (close, help) + FeatureGrid card glyphs
|
||||
logos/ # Brand + social lockups
|
||||
partners/ # Logo wall partner SVGs (kebab org slug)
|
||||
marketing/ # Hero, feature panels, section numbers, avatars, banners, book cover
|
||||
@@ -25,7 +25,7 @@ public/
|
||||
|
||||
| Location | Used for | Resolution |
|
||||
| --- | --- | --- |
|
||||
| `public/assets/icons/` | Static chrome served by URL (`icon-close.svg`, `icon-help.svg`) | `ASSETS.ICON_*` |
|
||||
| `public/assets/icons/` | Static chrome served by URL (`icon-close.svg`, `icon-help.svg`) and FeatureGrid card glyphs (`icon-git-branch.svg`, …) | `ASSETS.ICON_*`, `featureIconPath()` |
|
||||
| `app/components/asset/icon/` | Bundled create-flow / nav SVGs imported by `Icon.tsx` | Webpack import, not `public/` |
|
||||
|
||||
Do not duplicate the same glyph in both places unless migrating between systems.
|
||||
@@ -61,7 +61,8 @@ stage. Raster → SVG conversion is tracked in
|
||||
| Path | Used by | Disposition |
|
||||
| --- | --- | --- |
|
||||
| `logos/partners/*.svg` (×6) | LogoWall | **Done** — SVG (kebab org slug, no `logo-` prefix) |
|
||||
| `marketing/feature-*.svg` (×4) | FeatureGrid | Exported from Figma Section/Feature-Grid (18847:22410) |
|
||||
| `marketing/feature-*.svg` (×4) | Mini cards | Panel art (`support`, `exercises`, `guidance`, `tools`) |
|
||||
| `icons/icon-{git-branch,arrow-left-right,door-open,message-square-share}.svg` | FeatureGrid | Card glyphs from Figma Section/Feature-Grid (18632:10668) |
|
||||
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
|
||||
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
|
||||
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
|
||||
|
||||
+8
-22
@@ -114,29 +114,15 @@ export function featurePanelPath(key: FeaturePanelKey): string {
|
||||
return `assets/marketing/feature-${key}.svg`;
|
||||
}
|
||||
|
||||
/** Intrinsic icon bounds from Figma Feature-Grid (18632:10911). */
|
||||
export const FEATURE_PANEL_LAYOUT: Record<
|
||||
FeaturePanelKey,
|
||||
{ width: number; height: number; panelImageClassName?: string }
|
||||
> = {
|
||||
support: { width: 48, height: 48 },
|
||||
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",
|
||||
},
|
||||
};
|
||||
/** Feature-grid card glyphs in `public/assets/icons/` (Figma **18632:10668**). */
|
||||
export type FeatureIconKey =
|
||||
| "git-branch"
|
||||
| "arrow-left-right"
|
||||
| "door-open"
|
||||
| "message-square-share";
|
||||
|
||||
export function featurePanelLayout(key: FeaturePanelKey): {
|
||||
panelWidth: number;
|
||||
panelHeight: number;
|
||||
panelImageClassName?: string;
|
||||
} {
|
||||
const { width, height, panelImageClassName } = FEATURE_PANEL_LAYOUT[key];
|
||||
return { panelWidth: width, panelHeight: height, panelImageClassName };
|
||||
export function featureIconPath(key: FeatureIconKey): string {
|
||||
return `assets/icons/icon-${key}.svg`;
|
||||
}
|
||||
|
||||
/** Case study card artwork in `public/assets/case-study/`. */
|
||||
|
||||
@@ -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.",
|
||||
"linkText": "Learn more",
|
||||
"_comment": "FeatureGrid component defaults (shared across pages). Explore the toolkit goes to /learn; feature cards stay non-interactive until destinations are decided.",
|
||||
"linkText": "Explore the toolkit",
|
||||
"linkHref": "/learn",
|
||||
"ariaLabel": "Feature tools and services",
|
||||
"features": {
|
||||
"decisionMaking": {
|
||||
"ariaLabel": "Decision-making support tools"
|
||||
"ariaLabel": "Choose how you decide"
|
||||
},
|
||||
"valuesAlignment": {
|
||||
"ariaLabel": "Values alignment exercises"
|
||||
"ariaLabel": "Name your values"
|
||||
},
|
||||
"membershipGuidance": {
|
||||
"ariaLabel": "Membership guidance resources"
|
||||
"ariaLabel": "Define membership"
|
||||
},
|
||||
"conflictResolution": {
|
||||
"ariaLabel": "Conflict resolution tools"
|
||||
"ariaLabel": "Plan for conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,27 +23,39 @@
|
||||
"titlePrefix": "From",
|
||||
"titleEmphasis": "projects",
|
||||
"titleSuffix": "to communities",
|
||||
"asOf": "as of June 30, 2024",
|
||||
"asOfPrefix": "as of",
|
||||
"items": [
|
||||
{
|
||||
"value": "420M+",
|
||||
"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",
|
||||
"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%",
|
||||
"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.",
|
||||
"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
@@ -29,24 +29,24 @@
|
||||
}
|
||||
},
|
||||
"featureGrid": {
|
||||
"title": "We've got your back, every step of the way",
|
||||
"subtitle": "Use our toolkit to improve, document, and evolve your organization.",
|
||||
"title": "Tools for the parts that get hard",
|
||||
"subtitle": "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
|
||||
"features": {
|
||||
"decisionMaking": {
|
||||
"labelLine1": "Decision-making",
|
||||
"labelLine2": "support"
|
||||
"title": "Choose how you decide",
|
||||
"description": "Compare consensus, do-ocracy, and other patterns, then commit to one."
|
||||
},
|
||||
"valuesAlignment": {
|
||||
"labelLine1": "Values alignment",
|
||||
"labelLine2": "exercises"
|
||||
"title": "Name your values",
|
||||
"description": "Short exercises that surface what your group agrees on before it's tested."
|
||||
},
|
||||
"membershipGuidance": {
|
||||
"labelLine1": "Membership",
|
||||
"labelLine2": "guidance"
|
||||
"title": "Define membership",
|
||||
"description": "Spell out how people join, take on roles, and step away."
|
||||
},
|
||||
"conflictResolution": {
|
||||
"labelLine1": "Conflict resolution",
|
||||
"labelLine2": "tools"
|
||||
"title": "Plan for conflict",
|
||||
"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 |
@@ -8,12 +8,12 @@ export default {
|
||||
docs: {
|
||||
description: {
|
||||
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
|
||||
- **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 = {
|
||||
args: {
|
||||
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.",
|
||||
"Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
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).
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -48,11 +48,11 @@ export const Feature = {
|
||||
|
||||
export const FeatureWithLink = {
|
||||
args: {
|
||||
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.",
|
||||
"Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.",
|
||||
variant: "feature",
|
||||
linkText: "Learn more",
|
||||
linkText: "Explore the toolkit",
|
||||
linkHref: "/learn",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,33 +41,33 @@ describe("FeatureGrid (behavioral tests)", () => {
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Test Title" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Test Subtitle" }),
|
||||
).toBeInTheDocument();
|
||||
screen.queryByRole("heading", { name: "Test Subtitle" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all four feature cards as static tiles", () => {
|
||||
render(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
expect(screen.getByText("Decision-making")).toBeInTheDocument();
|
||||
expect(screen.getByText("Values alignment")).toBeInTheDocument();
|
||||
expect(screen.getByText("Membership")).toBeInTheDocument();
|
||||
expect(screen.getByText("Conflict resolution")).toBeInTheDocument();
|
||||
expect(screen.getByText("Choose how you decide")).toBeInTheDocument();
|
||||
expect(screen.getByText("Name your values")).toBeInTheDocument();
|
||||
expect(screen.getByText("Define membership")).toBeInTheDocument();
|
||||
expect(screen.getByText("Plan for conflict")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "Decision-making support tools" }),
|
||||
screen.queryByRole("link", { name: "Choose how you decide" }),
|
||||
).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" />);
|
||||
expect(screen.getByRole("link", { name: "Learn more" })).toHaveAttribute(
|
||||
"href",
|
||||
"/learn",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Explore the toolkit" }),
|
||||
).toHaveAttribute("href", "/learn");
|
||||
});
|
||||
|
||||
it("does not apply a focus ring to the entire grid shell", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -83,11 +83,11 @@ describe("FeatureGrid (behavioral tests)", () => {
|
||||
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" />);
|
||||
expect(
|
||||
document.querySelector(
|
||||
'[class*="bg-[var(--color-surface-invert-brand-royal)]"]',
|
||||
'[class*="bg-[var(--color-surface-invert-brand-lavender)]"]',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -110,7 +110,7 @@ describe("FeatureGrid (behavioral tests)", () => {
|
||||
it("marks the content block with the Figma node id", () => {
|
||||
render(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
expect(
|
||||
document.querySelector('[data-figma-node="18847-22410"]'),
|
||||
document.querySelector('[data-figma-node="18632-10668"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -119,35 +119,20 @@ describe("FeatureGrid (behavioral tests)", () => {
|
||||
const title = screen.getByRole("heading", { name: "Test Title" });
|
||||
expect(title.className).toMatch(/text-\[18px\]/);
|
||||
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");
|
||||
});
|
||||
|
||||
it("applies grain texture to feature grid icons", () => {
|
||||
it("renders 40px decorative icons from Figma", () => {
|
||||
render(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
const icons = document.querySelectorAll("section img");
|
||||
expect(icons.length).toBeGreaterThanOrEqual(4);
|
||||
expect(icons.length).toBe(4);
|
||||
icons.forEach((icon) => {
|
||||
expect(icon.getAttribute("style")).toContain("#grain");
|
||||
expect(icon).toHaveAttribute("width", "40");
|
||||
expect(icon).toHaveAttribute("height", "40");
|
||||
expect(icon).toHaveAttribute("alt", "");
|
||||
expect(icon.getAttribute("src")).toMatch(/^\/assets\/icons\/icon-/);
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,10 +57,11 @@ describe("HeroBanner (behavioral tests)", () => {
|
||||
).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" />);
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Get Started" }).length,
|
||||
).toBeGreaterThan(0);
|
||||
const cta = screen.getAllByRole("link", { name: "Get Started" })[0];
|
||||
expect(cta?.className).toContain(
|
||||
"bg-[var(--color-surface-default-primary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,4 +78,21 @@ describe("SectionHeader rule stack title max width", () => {
|
||||
}),
|
||||
).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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,4 +16,24 @@ describe("Stat", () => {
|
||||
expect(screen.getByText("open source projects")).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(
|
||||
screen.getByRole("heading", { name: /CommunityRule helps/i }),
|
||||
).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", () => {
|
||||
|
||||
@@ -25,6 +25,24 @@ describe("ContentLockup", () => {
|
||||
).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", () => {
|
||||
renderWithProviders(
|
||||
<ContentLockup
|
||||
@@ -45,4 +63,24 @@ describe("ContentLockup", () => {
|
||||
expect(description.className).toContain("text-small-paragraph");
|
||||
expect(description.className).toContain("sm:text-large-paragraph");
|
||||
});
|
||||
|
||||
it("renders the feature lockup CTA as an inline text link", () => {
|
||||
renderWithProviders(
|
||||
<ContentLockup
|
||||
variant="feature"
|
||||
title="Tools for the parts that get hard"
|
||||
subtitle="Every group runs into the same knots."
|
||||
linkText="Explore the toolkit"
|
||||
linkHref="/learn"
|
||||
/>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: "Explore the toolkit" });
|
||||
expect(link).toHaveAttribute("href", "/learn");
|
||||
expect(link.className).toMatch(/underline/);
|
||||
expect(link.className).toMatch(/w-fit/);
|
||||
expect(link.className).not.toMatch(/px-1/);
|
||||
expect(link.className).not.toMatch(/focus:ring-2/);
|
||||
expect(link.className).toMatch(/focus-visible:outline/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,10 +52,10 @@ test.describe("Critical User Journeys", () => {
|
||||
|
||||
// 7. User checks out features
|
||||
const features = [
|
||||
"Decision-making support",
|
||||
"Values alignment exercises",
|
||||
"Membership guidance",
|
||||
"Conflict resolution tools",
|
||||
"Choose how you decide",
|
||||
"Name your values",
|
||||
"Define membership",
|
||||
"Plan for conflict",
|
||||
];
|
||||
|
||||
for (const feature of features) {
|
||||
@@ -96,7 +96,7 @@ test.describe("Critical User Journeys", () => {
|
||||
await expect(howItWorksHeading).toBeVisible();
|
||||
|
||||
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();
|
||||
|
||||
// Check key components are rendered
|
||||
@@ -108,33 +108,32 @@ test.describe("Critical User Journeys", () => {
|
||||
test("feature grid section functionality", async ({ page }) => {
|
||||
// Check section header
|
||||
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();
|
||||
await expect(
|
||||
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();
|
||||
|
||||
// Check all four feature cards - FeatureGrid uses section with grid layout
|
||||
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=Values alignment")).toBeVisible();
|
||||
await expect(featureSection.locator("text=Membership")).toBeVisible();
|
||||
await expect(featureSection.locator("text=Choose how you decide")).toBeVisible();
|
||||
await expect(featureSection.locator("text=Name your values")).toBeVisible();
|
||||
await expect(featureSection.locator("text=Define membership")).toBeVisible();
|
||||
await expect(
|
||||
featureSection.locator("text=Conflict resolution"),
|
||||
featureSection.locator("text=Plan for conflict"),
|
||||
).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(
|
||||
featureSection.locator('a[href="#decision-making"]'),
|
||||
).toHaveCount(0);
|
||||
await expect(featureSection.getByRole("link", { name: "Learn more" })).toHaveAttribute(
|
||||
"href",
|
||||
"/learn",
|
||||
);
|
||||
await expect(
|
||||
featureSection.getByRole("link", { name: "Explore the toolkit" }),
|
||||
).toHaveAttribute("href", "/learn");
|
||||
});
|
||||
|
||||
test("header navigation functionality", async ({ page }) => {
|
||||
|
||||
@@ -63,4 +63,27 @@ describe("AboutPage", () => {
|
||||
}),
|
||||
).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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,13 +35,13 @@ describe("Page", () => {
|
||||
// FeatureGrid is next/dynamic — wait like other code-split sections
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
screen.getAllByText("Tools for the parts that get hard")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
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,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
@@ -111,16 +111,18 @@ describe("Page", () => {
|
||||
// Wait for dynamically imported FeatureGrid component to load
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
screen.getAllByText("Tools for the parts that get hard")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
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,
|
||||
).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");
|
||||
});
|
||||
|
||||
@@ -157,7 +159,7 @@ describe("Page", () => {
|
||||
// FeatureGrid
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
screen.getAllByText("Tools for the parts that get hard")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -211,7 +213,7 @@ describe("Page", () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
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,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -236,7 +238,7 @@ describe("Page", () => {
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
screen.getAllByText("Tools for the parts that get hard")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -285,7 +287,7 @@ describe("Page", () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
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,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -90,13 +90,13 @@ describe("Page Flow Integration", () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "We've got your back, every step of the way",
|
||||
name: "Tools for the parts that get hard",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
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();
|
||||
|
||||
@@ -233,7 +233,7 @@ describe("Page Flow Integration", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("We've got your back, every step of the way"),
|
||||
screen.getByText("Tools for the parts that get hard"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -151,12 +151,12 @@ describe("User Journey Integration", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("We've got your back, every step of the way"),
|
||||
screen.getByText("Tools for the parts that get hard"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
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();
|
||||
|
||||
@@ -206,7 +206,7 @@ describe("User Journey Integration", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("We've got your back, every step of the way"),
|
||||
screen.getByText("Tools for the parts that get hard"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user