Fix featured grid

This commit is contained in:
adilallo
2026-05-22 13:50:55 -06:00
parent 3dbb6b61d2
commit 5863a256f6
23 changed files with 243 additions and 88 deletions
@@ -21,6 +21,10 @@ const MiniContainer = memo<MiniProps>(
onClick, onClick,
href, href,
ariaLabel, ariaLabel,
featureGridShell = false,
panelWidth,
panelHeight,
panelImageClassName,
}) => { }) => {
const t = useTranslation("controlsChrome"); const t = useTranslation("controlsChrome");
@@ -92,6 +96,10 @@ const MiniContainer = memo<MiniProps>(
computedAriaLabel={computedAriaLabel} computedAriaLabel={computedAriaLabel}
wrapperElement={wrapperElement} wrapperElement={wrapperElement}
wrapperProps={wrapperProps} wrapperProps={wrapperProps}
featureGridShell={featureGridShell}
panelWidth={panelWidth}
panelHeight={panelHeight}
panelImageClassName={panelImageClassName}
> >
{children} {children}
</MiniView> </MiniView>
+9
View File
@@ -9,6 +9,11 @@ export interface MiniProps {
onClick?: () => void; onClick?: () => void;
href?: string; href?: string;
ariaLabel?: string; ariaLabel?: string;
/** Figma Feature-Grid mini tile shell (18847:22410). */
featureGridShell?: boolean;
panelWidth?: number;
panelHeight?: number;
panelImageClassName?: string;
} }
export interface MiniViewProps { export interface MiniViewProps {
@@ -25,4 +30,8 @@ export interface MiniViewProps {
| React.AnchorHTMLAttributes<HTMLAnchorElement> | React.AnchorHTMLAttributes<HTMLAnchorElement>
| React.ButtonHTMLAttributes<HTMLButtonElement> | React.ButtonHTMLAttributes<HTMLButtonElement>
| React.HTMLAttributes<HTMLDivElement>; | React.HTMLAttributes<HTMLDivElement>;
featureGridShell?: boolean;
panelWidth?: number;
panelHeight?: number;
panelImageClassName?: string;
} }
+36 -15
View File
@@ -2,6 +2,7 @@
import { memo } from "react"; import { memo } from "react";
import Image from "next/image"; import Image from "next/image";
import { SVG_GRAIN_MULTIPLY_FILTER } from "../../../../lib/svgGrainFilter";
import type { MiniViewProps } from "./Mini.types"; import type { MiniViewProps } from "./Mini.types";
function MiniView({ function MiniView({
@@ -15,39 +16,59 @@ function MiniView({
computedAriaLabel, computedAriaLabel,
wrapperElement, wrapperElement,
wrapperProps, wrapperProps,
featureGridShell = false,
panelWidth,
panelHeight,
panelImageClassName,
}: MiniViewProps) { }: MiniViewProps) {
const defaultPanelSize = featureGridShell ? 48 : 58;
const imageWidth = panelWidth ?? defaultPanelSize;
const imageHeight = panelHeight ?? defaultPanelSize;
const outerClass = featureGridShell
? `flex min-h-[159px] flex-col gap-[7px] ${className}`
: `h-[186px] flex flex-col gap-[7px] ${className}`;
const panelClass = featureGridShell
? `h-[138px] shrink-0 rounded-[var(--measures-radius-400,16px)] px-[24px] py-[32px] ${backgroundColor} flex items-center justify-center`
: `flex-1 rounded-[var(--radius-measures-radius-xlarge)] border border-[1px] py-[var(--spacing-scale-032)] px-[var(--spacing-scale-024)] ${backgroundColor} flex items-center justify-center transition-all duration-200 hover:scale-[1.02] hover:shadow-lg`;
const imageClass = featureGridShell
? `max-h-[48px] max-w-[56px] w-auto h-auto object-contain${panelImageClassName ? ` ${panelImageClassName}` : ""}`
: "max-w-[58px] max-h-[58px] w-auto h-auto object-contain";
const cardContentElement = ( const cardContentElement = (
<div className={`h-[186px] flex flex-col gap-[7px] ${className}`}> <div className={outerClass}>
{/* Top part - Inner panel */} <div className={panelClass}>
<div
className={`flex-1 rounded-[var(--radius-measures-radius-xlarge)] border border-[1px] py-[var(--spacing-scale-032)] px-[var(--spacing-scale-024)] ${backgroundColor} flex items-center justify-center transition-all duration-200 hover:scale-[1.02] hover:shadow-lg`}
>
{/* Content for the inner panel */}
{panelContent && ( {panelContent && (
<div className="flex items-center justify-center w-full h-full"> <div className="flex h-full w-full items-center justify-center">
<Image <Image
src={panelContent} src={panelContent}
alt={computedAriaLabel} alt={computedAriaLabel}
className="max-w-[58px] max-h-[58px] w-auto h-auto object-contain" className={imageClass}
width={58} width={imageWidth}
height={58} height={imageHeight}
sizes="(max-width: 768px) 50vw, 25vw" sizes="(max-width: 768px) 50vw, 25vw"
loading="lazy" loading="lazy"
placeholder="blur" style={
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=" featureGridShell
? {
filter: SVG_GRAIN_MULTIPLY_FILTER,
WebkitFilter: SVG_GRAIN_MULTIPLY_FILTER,
}
: undefined
}
/> />
</div> </div>
)} )}
{children} {children}
</div> </div>
{/* Bottom part - Text container */} <div className="text-center font-inter text-[12px] font-medium leading-[14px] text-[var(--color-content-default-primary)]">
<div className="font-inter font-medium text-[12px] leading-[14px] text-center text-[var(--color-content-default-primary)]">
{labelLine1 && labelLine2 ? ( {labelLine1 && labelLine2 ? (
<> <>
<div>{labelLine1}</div> <div>{labelLine1}</div>
<div>{labelLine2}</div> <div>{labelLine2}</div>
<div>&nbsp;</div>
</> </>
) : ( ) : (
label label
@@ -1,11 +1,11 @@
"use client"; "use client";
/** /**
* Figma: "Sections / FeatureGrid" (see registry) * Figma: "Section / Feature-Grid" (18847:22410)
*/ */
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import { getAssetPath, featurePanelPath } from "../../../../lib/assetUtils"; import { getAssetPath, featurePanelLayout, featurePanelPath } 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,7 +17,7 @@ const FeatureGridContainer = memo<FeatureGridProps>(
const features: Feature[] = useMemo( const features: Feature[] = useMemo(
() => [ () => [
{ {
backgroundColor: "bg-[var(--color-surface-default-brand-royal)]", backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]",
labelLine1: t( labelLine1: t(
"pages.home.featureGrid.features.decisionMaking.labelLine1", "pages.home.featureGrid.features.decisionMaking.labelLine1",
), ),
@@ -25,11 +25,12 @@ const FeatureGridContainer = memo<FeatureGridProps>(
"pages.home.featureGrid.features.decisionMaking.labelLine2", "pages.home.featureGrid.features.decisionMaking.labelLine2",
), ),
panelContent: getAssetPath(featurePanelPath("support")), panelContent: getAssetPath(featurePanelPath("support")),
...featurePanelLayout("support"),
ariaLabel: t("featureGrid.features.decisionMaking.ariaLabel"), ariaLabel: t("featureGrid.features.decisionMaking.ariaLabel"),
href: "#decision-making", href: "#decision-making",
}, },
{ {
backgroundColor: "bg-[#D1FFE2]", backgroundColor: "bg-[var(--color-surface-invert-brand-lime)]",
labelLine1: t( labelLine1: t(
"pages.home.featureGrid.features.valuesAlignment.labelLine1", "pages.home.featureGrid.features.valuesAlignment.labelLine1",
), ),
@@ -37,11 +38,12 @@ const FeatureGridContainer = memo<FeatureGridProps>(
"pages.home.featureGrid.features.valuesAlignment.labelLine2", "pages.home.featureGrid.features.valuesAlignment.labelLine2",
), ),
panelContent: getAssetPath(featurePanelPath("exercises")), panelContent: getAssetPath(featurePanelPath("exercises")),
...featurePanelLayout("exercises"),
ariaLabel: t("featureGrid.features.valuesAlignment.ariaLabel"), ariaLabel: t("featureGrid.features.valuesAlignment.ariaLabel"),
href: "#values-alignment", href: "#values-alignment",
}, },
{ {
backgroundColor: "bg-[#F4CAFF]", backgroundColor: "bg-[var(--color-surface-invert-brand-rust)]",
labelLine1: t( labelLine1: t(
"pages.home.featureGrid.features.membershipGuidance.labelLine1", "pages.home.featureGrid.features.membershipGuidance.labelLine1",
), ),
@@ -49,11 +51,12 @@ const FeatureGridContainer = memo<FeatureGridProps>(
"pages.home.featureGrid.features.membershipGuidance.labelLine2", "pages.home.featureGrid.features.membershipGuidance.labelLine2",
), ),
panelContent: getAssetPath(featurePanelPath("guidance")), panelContent: getAssetPath(featurePanelPath("guidance")),
...featurePanelLayout("guidance"),
ariaLabel: t("featureGrid.features.membershipGuidance.ariaLabel"), ariaLabel: t("featureGrid.features.membershipGuidance.ariaLabel"),
href: "#membership-guidance", href: "#membership-guidance",
}, },
{ {
backgroundColor: "bg-[#CBDDFF]", backgroundColor: "bg-[var(--color-surface-invert-brand-teal)]",
labelLine1: t( labelLine1: t(
"pages.home.featureGrid.features.conflictResolution.labelLine1", "pages.home.featureGrid.features.conflictResolution.labelLine1",
), ),
@@ -61,6 +64,7 @@ const FeatureGridContainer = memo<FeatureGridProps>(
"pages.home.featureGrid.features.conflictResolution.labelLine2", "pages.home.featureGrid.features.conflictResolution.labelLine2",
), ),
panelContent: getAssetPath(featurePanelPath("tools")), panelContent: getAssetPath(featurePanelPath("tools")),
...featurePanelLayout("tools"),
ariaLabel: t("featureGrid.features.conflictResolution.ariaLabel"), ariaLabel: t("featureGrid.features.conflictResolution.ariaLabel"),
href: "#conflict-resolution", href: "#conflict-resolution",
}, },
@@ -9,6 +9,9 @@ export interface Feature {
labelLine1: string; labelLine1: string;
labelLine2: string; labelLine2: string;
panelContent: string; panelContent: string;
panelWidth: number;
panelHeight: number;
panelImageClassName?: string;
ariaLabel: string; ariaLabel: string;
href: string; href: string;
} }
@@ -5,6 +5,7 @@ import ContentLockup from "../../type/ContentLockup";
import Mini from "../../cards/Mini"; 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). */
function FeatureGridView({ function FeatureGridView({
title, title,
subtitle, subtitle,
@@ -23,10 +24,12 @@ function FeatureGridView({
aria-labelledby={labelledBy} aria-labelledby={labelledBy}
aria-label={labelledBy ? undefined : ariaLabel} aria-label={labelledBy ? undefined : ariaLabel}
> >
<div className="py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] md:pt-[var(--spacing-scale-076)] md:pb-[var(--spacing-scale-048)] lg:pb-[var(--spacing-scale-076)] md:px-[var(--spacing-scale-048)] bg-[#171717] rounded-[var(--radius-measures-radius-xlarge)] focus-within:ring-2 focus-within:ring-[var(--color-surface-default-brand-royal)] focus-within:ring-offset-2"> <div
<div className="w-full mx-auto gap-[var(--spacing-scale-048)] lg:flex lg:items-start lg:gap-[var(--spacing-scale-048)] [container-type:inline-size]"> data-figma-node="18847-22410"
{/* Feature Content Lockup */} className="rounded-[var(--measures-radius-500,20px)] bg-[var(--color-surface-default-secondary)] px-[var(--spacing-scale-020)] py-[var(--spacing-scale-032)] focus-within:ring-2 focus-within:ring-[var(--color-surface-default-brand-royal)] focus-within:ring-offset-2 md:px-[var(--spacing-scale-048)] md:pb-[var(--spacing-scale-048)] md:pt-[var(--spacing-scale-076)] lg:pb-[var(--spacing-scale-076)]"
<div className="lg:shrink lg:min-w-0"> >
<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">
<ContentLockup <ContentLockup
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
@@ -37,17 +40,20 @@ function FeatureGridView({
/> />
</div> </div>
{/* Mini grid */} <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 grid-cols-2 md:grid-cols-4 gap-[var(--spacing-scale-012)] mt-[var(--spacing-scale-048)] lg:mt-0 lg:flex-grow lg:shrink-0">
{features.map((feature, index) => ( {features.map((feature, index) => (
<Mini <Mini
key={index} key={index}
backgroundColor={feature.backgroundColor} backgroundColor={feature.backgroundColor}
labelLine1={feature.labelLine1} labelLine1={feature.labelLine1}
labelLine2={feature.labelLine2} labelLine2={feature.labelLine2}
panelContent={feature.panelContent} panelContent={feature.panelContent}
ariaLabel={feature.ariaLabel} panelWidth={feature.panelWidth}
panelHeight={feature.panelHeight}
panelImageClassName={feature.panelImageClassName}
ariaLabel={feature.ariaLabel}
href={feature.href} href={feature.href}
featureGridShell
/> />
))} ))}
</div> </div>
@@ -2,14 +2,12 @@
import { memo } from "react"; import { memo } from "react";
import { getAssetPath, quoteStatementShapePath } from "../../../../lib/assetUtils"; import { getAssetPath, quoteStatementShapePath } from "../../../../lib/assetUtils";
import { SVG_GRAIN_MULTIPLY_FILTER } from "../../../../lib/svgGrainFilter";
/** Figma: Section / Quote — **`shape-quote.svg`** (22137:890679). */ /** Figma: Section / Quote — **`shape-quote.svg`** (22137:890679). */
const EDGE_MASK = const EDGE_MASK =
"linear-gradient(to right, #fff 0%, #fff 14%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, #fff 86%, #fff 100%)"; "linear-gradient(to right, #fff 0%, #fff 14%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, #fff 86%, #fff 100%)";
const GRAIN_MULTIPLY_FILTER =
'url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><defs><filter id="grain" filterUnits="objectBoundingBox" x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.4" numOctaves="3" seed="7" stitchTiles="stitch" result="noise"/><feColorMatrix in="noise" result="softNoise" type="matrix" values="0.8 0 0 0 0.3 0 0.6 0 0 0.2 0 0 1.0 0 0.4 0 0 0 0.25 0"/><feComposite in="softNoise" in2="SourceAlpha" operator="in" result="maskedNoise"/><feBlend in="SourceGraphic" in2="maskedNoise" mode="multiply"/></filter></defs></svg>#grain\')';
const QuoteStatementDecor = memo<{ className?: string }>(({ className = "" }) => { const QuoteStatementDecor = memo<{ className?: string }>(({ className = "" }) => {
const src = getAssetPath(quoteStatementShapePath()); const src = getAssetPath(quoteStatementShapePath());
const bg = `url("${src}")`; const bg = `url("${src}")`;
@@ -29,8 +27,8 @@ const QuoteStatementDecor = memo<{ className?: string }>(({ className = "" }) =>
maskSize: "100% 100%", maskSize: "100% 100%",
WebkitMaskRepeat: "no-repeat", WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat", maskRepeat: "no-repeat",
filter: GRAIN_MULTIPLY_FILTER, filter: SVG_GRAIN_MULTIPLY_FILTER,
WebkitFilter: GRAIN_MULTIPLY_FILTER, WebkitFilter: SVG_GRAIN_MULTIPLY_FILTER,
}} }}
/> />
); );
@@ -45,14 +45,14 @@ const ContentLockupContainer = memo<ContentLockupProps>(
"w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]", "w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]",
}, },
feature: { feature: {
container: "flex flex-col gap-[var(--spacing-scale-012)] relative z-10", container: "flex flex-col gap-[var(--space-400,16px)] md:gap-[var(--space-500,20px)] relative z-10",
textContainer: "flex flex-col gap-[var(--spacing-scale-012)]", textContainer: "flex flex-col gap-[var(--space-100,4px)] md:gap-[var(--space-150,6px)]",
titleGroup: "flex flex-col gap-[var(--spacing-scale-012)]", titleGroup: "flex flex-col gap-[var(--space-100,4px)] md:gap-[var(--space-150,6px)]",
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", titleContainer: "flex items-center",
title: title:
"font-bricolage-grotesque font-medium text-[32px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]", "font-bricolage-grotesque font-medium text-[18px] leading-[22px] md:text-[length:var(--sizing-600,24px)] md:leading-[32px] text-[var(--color-content-default-primary)]",
subtitle: subtitle:
"font-space-grotesk font-normal text-[20px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]", "font-inter font-normal text-[length:var(--sizing-350,14px)] leading-[20px] md:text-[length:var(--sizing-400,16px)] md:leading-[24px] text-[var(--color-content-default-secondary)]",
description: description:
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-default-secondary)]", "font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-default-secondary)]",
shape: shape:
@@ -97,7 +97,7 @@ function ContentLockupView({
{variant === "feature" && linkText && ( {variant === "feature" && linkText && (
<a <a
href={linkHref || "#"} href={linkHref || "#"}
className="font-inter font-medium text-[16px] leading-[20px] underline text-[var(--color-content-default-primary)] hover:text-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 focus:ring-offset-[#171717] rounded-sm px-1 py-0.5" className="font-inter font-normal text-[length:var(--sizing-400,16px)] leading-[24px] 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"
> >
{linkText} {linkText}
</a> </a>
+1 -1
View File
@@ -57,7 +57,7 @@ stage. Raster → SVG conversion is tracked in
| Path | Used by | Disposition | | Path | Used by | Disposition |
| --- | --- | --- | | --- | --- | --- |
| `logos/partners/*.svg` (×6) | LogoWall | **Done** — SVG (kebab org slug, no `logo-` prefix) | | `logos/partners/*.svg` (×6) | LogoWall | **Done** — SVG (kebab org slug, no `logo-` prefix) |
| `marketing/feature-*.png` (×4) | FeatureGrid | **Design review** — convert if vector in Figma, else keep raster | | `marketing/feature-*.svg` (×4) | FeatureGrid | Exported from Figma Section/Feature-Grid (18847:22410) |
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG | | `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG | | `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster | | `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
+26 -1
View File
@@ -106,7 +106,32 @@ export function governanceBookletPath(): string {
export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools"; export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools";
export function featurePanelPath(key: FeaturePanelKey): string { export function featurePanelPath(key: FeaturePanelKey): string {
return `assets/marketing/feature-${key}.png`; 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",
},
};
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 };
} }
/** Case study card artwork in `public/assets/case-study/`. */ /** Case study card artwork in `public/assets/case-study/`. */
+3
View File
@@ -0,0 +1,3 @@
/** feTurbulence grain masked to alpha and multiply-blended — matches HeroDecor. */
export const SVG_GRAIN_MULTIPLY_FILTER =
'url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><defs><filter id="grain" filterUnits="objectBoundingBox" x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.4" numOctaves="3" seed="7" stitchTiles="stitch" result="noise"/><feColorMatrix in="noise" result="softNoise" type="matrix" values="0.8 0 0 0 0.3 0 0.6 0 0 0.2 0 0 1.0 0 0.4 0 0 0 0.25 0"/><feComposite in="softNoise" in2="SourceAlpha" operator="in" result="maskedNoise"/><feBlend in="SourceGraphic" in2="maskedNoise" mode="multiply"/></filter></defs></svg>#grain\')';
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,15 @@
<svg preserveAspectRatio="xMidYMid meet" overflow="visible" style="display: block;" viewBox="0 0 54.6002 48.4575" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 7060">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M27.9826 12.285C28.3742 12.285 28.722 12.5356 28.8459 12.9072L29.8395 15.8881L32.8203 16.8817C33.1919 17.0056 33.4426 17.3533 33.4426 17.745C33.4426 18.1367 33.1919 18.4845 32.8203 18.6083L29.8395 19.6019L28.8459 22.5828C28.722 22.9544 28.3742 23.205 27.9826 23.205C27.5909 23.205 27.2431 22.9544 27.1192 22.5828L26.1256 19.6019L23.1448 18.6083C22.7732 18.4845 22.5225 18.1367 22.5225 17.745C22.5225 17.3533 22.7732 17.0056 23.1448 16.8817L26.1256 15.8881L27.1192 12.9072C27.2431 12.5356 27.5909 12.285 27.9826 12.285Z" fill="var(--fill-0, #CCF66F)"/>
<g id="Group">
<path id="Vector_2" d="M27.3001 26.6175C30.3213 26.6175 32.7601 29.0563 32.7601 32.0775C32.7601 35.0987 30.3213 37.5375 27.3001 37.5375C24.2788 37.5375 21.84 35.0987 21.84 32.0775C21.84 29.0563 24.2788 26.6175 27.3001 26.6175Z" fill="var(--fill-0, #CCF66F)"/>
<path id="Vector_3" d="M8.19004 13.65C10.8336 13.65 12.9675 15.784 12.9675 18.4275C12.9675 21.0711 10.8336 23.205 8.19004 23.205C5.54648 23.205 3.41253 21.0711 3.41253 18.4275C3.41253 15.784 5.54648 13.65 8.19004 13.65Z" fill="var(--fill-0, #CCF66F)"/>
<path id="Vector_4" d="M46.4101 13.65C49.0536 13.65 51.1876 15.784 51.1876 18.4275C51.1876 21.0711 49.0536 23.205 46.4101 23.205C43.7665 23.205 41.6326 21.0711 41.6326 18.4275C41.6326 15.784 43.7665 13.65 46.4101 13.65Z" fill="var(--fill-0, #CCF66F)"/>
<path id="Vector_5" d="M35.4901 0C38.1336 0 40.2676 2.13395 40.2676 4.7775C40.2676 7.42105 38.1336 9.55501 35.4901 9.55501C32.8465 9.55501 30.7126 7.42105 30.7126 4.7775C30.7126 2.13395 32.8465 0 35.4901 0Z" fill="var(--fill-0, #CCF66F)"/>
<path id="Vector_6" d="M19.11 0C21.7536 0 23.8875 2.13395 23.8875 4.7775C23.8875 7.42105 21.7536 9.55501 19.11 9.55501C16.4665 9.55501 14.3325 7.42105 14.3325 4.7775C14.3325 2.13395 16.4665 0 19.11 0Z" fill="var(--fill-0, #CCF66F)"/>
<path id="Vector_7" d="M13.9776 28.587C12.3806 27.8577 10.415 27.3 8.19004 27.3C5.96508 27.3 3.99948 27.8577 2.40243 28.6013C0.928231 29.2734 3.05176e-05 30.8178 3.05176e-05 32.5052V34.8075H16.38V32.4909C16.38 30.8178 15.4518 29.2734 13.9776 28.587Z" fill="var(--fill-0, #CCF66F)"/>
<path id="Vector_8" d="M52.1977 28.587C50.6006 27.8577 48.635 27.3 46.4101 27.3C44.1851 27.3 42.2195 27.8577 40.6225 28.6013C39.1483 29.2734 38.2201 30.8178 38.2201 32.5052V34.8075H54.6001V32.4909C54.6001 30.8178 53.6719 29.2734 52.1977 28.587Z" fill="var(--fill-0, #CCF66F)"/>
</g>
<path id="Vector_9" d="M35.0169 41.6715C32.8875 40.8759 30.2667 40.2675 27.3 40.2675C24.3334 40.2675 21.7126 40.8759 19.5832 41.6871C17.6176 42.4203 16.38 44.1051 16.38 45.9459V48.4575H38.2201V45.9303C38.2201 44.1051 36.9825 42.4203 35.0169 41.6715Z" fill="var(--fill-0, #CCF66F)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

@@ -0,0 +1,8 @@
<svg preserveAspectRatio="xMidYMid meet" overflow="visible" style="display: block;" viewBox="0 0 56 38.7335" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 7059">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M16.3331 2.22193e-05C16.9357 2.22193e-05 17.4707 0.385624 17.6613 0.957303L19.1899 5.54322L23.7758 7.07186C24.3475 7.26242 24.7331 7.79742 24.7331 8.40002C24.7331 9.00262 24.3475 9.53762 23.7758 9.72818L19.1899 11.2568L17.6613 15.8427C17.4707 16.4144 16.9357 16.8 16.3331 16.8C15.7305 16.8 15.1955 16.4144 15.005 15.8427L13.4763 11.2568L8.89041 9.72818C8.31873 9.53762 7.93313 9.00262 7.93313 8.40002C7.93313 7.79742 8.31873 7.26242 8.89041 7.07186L13.4763 5.54322L15.005 0.957303C15.1955 0.385624 15.7305 2.22193e-05 16.3331 2.22193e-05Z" fill="var(--fill-0, #F9B0A6)"/>
<g id="Group">
<path id="Vector_2" d="M28 26.4835C31.8034 26.4835 35.1634 27.3935 37.8934 28.5835C40.4134 29.7035 42 32.2235 42 34.9535V38.7335H14V34.9768C14 32.2235 15.5867 29.7035 18.1067 28.6068C20.8367 27.3935 24.1967 26.4835 28 26.4835ZM9.33337 27.0668C11.9 27.0668 14 24.9668 14 22.4002C14 19.8335 11.9 17.7335 9.33337 17.7335C6.7667 17.7335 4.6667 19.8335 4.6667 22.4002C4.6667 24.9668 6.7667 27.0668 9.33337 27.0668ZM11.97 29.6335C11.1067 29.4935 10.2434 29.4002 9.33337 29.4002C7.02337 29.4002 4.83004 29.8902 2.8467 30.7535C1.12004 31.5002 3.75112e-05 33.1802 3.75112e-05 35.0702V38.7335H10.5V34.9768C10.5 33.0402 11.0367 31.2202 11.97 29.6335ZM46.6667 27.0668C49.2334 27.0668 51.3334 24.9668 51.3334 22.4002C51.3334 19.8335 49.2334 17.7335 46.6667 17.7335C44.1 17.7335 42 19.8335 42 22.4002C42 24.9668 44.1 27.0668 46.6667 27.0668ZM56 35.0702C56 33.1802 54.88 31.5002 53.1534 30.7535C51.17 29.8902 48.9767 29.4002 46.6667 29.4002C45.7567 29.4002 44.8934 29.4935 44.03 29.6335C44.9634 31.2202 45.5 33.0402 45.5 34.9768V38.7335H56V35.0702ZM28 10.7335C31.8734 10.7335 35 13.8602 35 17.7335C35 21.6068 31.8734 24.7335 28 24.7335C24.1267 24.7335 21 21.6068 21 17.7335C21 13.8602 24.1267 10.7335 28 10.7335Z" fill="var(--fill-0, #F9B0A6)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,9 @@
<svg preserveAspectRatio="xMidYMid meet" overflow="visible" style="display: block;" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 0C14.8609 0 15.6251 0.55086 15.8974 1.36754L18.5811 9.41886L26.6325 12.1026C27.4491 12.3749 28 13.1391 28 14C28 14.8609 27.4491 15.6251 26.6325 15.8974L18.5811 18.5811L15.8974 26.6325C15.6251 27.4491 14.8609 28 14 28C13.1391 28 12.3749 27.4491 12.1026 26.6325L9.41886 18.5811L1.36754 15.8974C0.55086 15.6251 0 14.8609 0 14C0 13.1391 0.55086 12.3749 1.36754 12.1026L9.41886 9.41886L12.1026 1.36754C12.3749 0.55086 13.1391 0 14 0ZM14 8.32456L12.8974 11.6325C12.6983 12.2297 12.2297 12.6983 11.6325 12.8974L8.32456 14L11.6325 15.1026C12.2297 15.3017 12.6983 15.7703 12.8974 16.3675L14 19.6754L15.1026 16.3675C15.3017 15.7703 15.7703 15.3017 16.3675 15.1026L19.6754 14L16.3675 12.8974C15.7703 12.6983 15.3017 12.2297 15.1026 11.6325L14 8.32456Z" fill="var(--fill-0, #A8ADFD)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.5858 13.5858C40.9191 12.2525 43.0809 12.2525 44.4142 13.5858C45.7475 14.9191 45.7475 17.0809 44.4142 18.4142L30.4142 32.4142C29.0809 33.7476 26.9191 33.7475 25.5858 32.4142C24.2525 31.0809 24.2525 28.9191 25.5858 27.5858L39.5858 13.5858Z" fill="var(--fill-0, #A8ADFD)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4401 41.5878C15.5064 42.0159 16.625 42.0924 17.7449 41.6362C19.1062 41.0817 19.5819 40.1783 19.7019 39.1811C19.7694 38.6201 19.7102 38.043 19.5653 37.5535C19.4193 37.06 19.2444 36.8418 19.2296 36.8234V36.8234C18.7988 36.3895 18.5168 36.2284 18.3371 36.1528C18.1617 36.079 17.8807 36 17.3332 36C17.1948 36 16.9413 36.0219 16.6227 36.0946C16.3321 36.1608 16.0622 36.2523 15.8457 36.351C15.832 36.4548 15.8184 36.5592 15.8044 36.6678C15.7751 36.8943 15.7434 37.1385 15.7026 37.4303C15.6136 38.0671 15.4874 38.862 15.2513 39.6665C15.0622 40.3107 14.8011 40.9617 14.4401 41.5878ZM11.4066 44.5152C13.4852 45.7779 16.2214 46.5759 19.2539 45.3406C25 43 24.3549 36.3084 22.0674 34.0041C20.6478 32.5741 19.2539 32.0001 17.3333 32C15.4127 31.9999 12.3109 33.0512 12 35C11.9324 35.4235 11.8787 35.8361 11.8267 36.2352C11.5573 38.3037 11.3348 40.0115 9.48038 41.0095C8.83315 41.3578 8.54891 42.2089 9.08028 42.7167C9.69512 43.3043 10.4605 43.9332 11.3477 44.4792V44.4792C11.3672 44.4912 11.387 44.5033 11.4066 44.5152V44.5152Z" fill="var(--fill-0, #A8ADFD)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

@@ -0,0 +1,6 @@
<svg preserveAspectRatio="xMidYMid meet" overflow="visible" style="display: block;" viewBox="0 0 49.9755 46.7474" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Vector" d="M40.7417 45.8942C40.5854 45.4255 40.6375 44.1755 40.95 43.0296C41.5229 40.8421 41.6792 40.9463 38.6583 41.8317C37.2521 42.1963 35.3771 41.3109 35.3771 40.2692C35.3771 38.9671 38.5542 37.3525 40.6896 37.5088L42.5646 37.665L43.1375 34.8525C43.9188 30.9463 43.9188 30.8942 42.1479 32.4046C40.325 33.915 37.7729 34.54 35.4813 34.0713C34.6479 33.863 32.0438 32.1963 29.7 30.2692C26.0021 27.3005 25.2208 26.9359 23.8667 27.1963C21.575 27.613 15.95 31.1025 11.0542 35.0088C8.71042 36.8838 6.47084 38.4463 6.05417 38.4463C4.38751 38.4463 3.97084 36.988 4.33542 32.4046C4.85626 25.3213 7.25209 19.4359 10.8458 16.4671L12.4083 15.113L11.0021 14.1234C9.90834 13.3942 8.60626 13.1859 5.22084 13.1859C-0.352076 13.1859 -0.664576 12.8213 0.637508 7.92546C1.62709 4.12337 3.91876 0.269207 5.32501 0.00879082C5.84584 -0.0953759 7.25209 0.737958 8.50209 1.88379C12.2 5.32129 13.5542 5.79004 19.1792 5.68587C25.95 5.58171 28.9708 6.51921 32.2521 9.69629C34.3875 11.7796 34.9083 12.7692 35.8979 16.0505C37.825 22.7692 38.5021 24.2796 40.3771 25.9463C43.1896 28.3942 43.9188 28.0817 43.3979 24.8005C43.1896 23.29 42.9813 21.1025 42.9813 19.9567C42.9292 17.9775 43.0333 17.8734 44.4917 17.8734C45.8458 17.8734 46.1583 18.1338 46.7313 19.9567C47.7729 23.0296 48.0854 30.113 47.4083 34.488L46.7833 38.29L48.3458 39.9046C51.2625 42.7692 49.9604 46.0505 46.8354 43.7588L45.3771 42.665L44.7521 44.2275C43.7625 46.5713 41.3667 47.613 40.7417 45.8942Z" fill="var(--fill-0, #A8FDF8)"/>
<path id="Vector_2" d="M17.2521 40.4254C16.8875 40.0609 16.6271 38.915 16.6271 37.8734C16.6271 35.7379 17.9292 34.3838 22.0958 32.3004C23.9188 31.415 24.3354 31.3629 25.1688 31.9359C26.6792 33.0296 25.8458 35.0609 22.5646 38.2379C19.7 41.0504 18.3979 41.5713 17.2521 40.4254Z" fill="var(--fill-0, #A8FDF8)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+10 -10
View File
@@ -11,10 +11,10 @@ export default {
backgroundColor: { backgroundColor: {
control: "select", control: "select",
options: [ options: [
"bg-[var(--color-surface-default-brand-royal)]", "bg-[var(--color-surface-invert-brand-royal)]",
"bg-[#D1FFE2]", "bg-[var(--color-surface-invert-brand-lime)]",
"bg-[#F4CAFF]", "bg-[var(--color-surface-invert-brand-rust)]",
"bg-[#CBDDFF]", "bg-[var(--color-surface-invert-brand-teal)]",
], ],
}, },
labelLine1: { control: "text" }, labelLine1: { control: "text" },
@@ -28,7 +28,7 @@ export default {
export const Default = { export const Default = {
args: { args: {
backgroundColor: "bg-[var(--color-surface-default-brand-royal)]", backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]",
labelLine1: "Decision-making", labelLine1: "Decision-making",
labelLine2: "support", labelLine2: "support",
panelContent: getAssetPath(featurePanelPath("support")), panelContent: getAssetPath(featurePanelPath("support")),
@@ -39,25 +39,25 @@ export const ColorVariants = {
render: () => ( render: () => (
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Mini <Mini
backgroundColor="bg-[var(--color-surface-default-brand-royal)]" backgroundColor="bg-[var(--color-surface-invert-brand-royal)]"
labelLine1="Decision-making" labelLine1="Decision-making"
labelLine2="support" labelLine2="support"
panelContent={getAssetPath(featurePanelPath("support"))} panelContent={getAssetPath(featurePanelPath("support"))}
/> />
<Mini <Mini
backgroundColor="bg-[#D1FFE2]" backgroundColor="bg-[var(--color-surface-invert-brand-lime)]"
labelLine1="Values alignment" labelLine1="Values alignment"
labelLine2="exercises" labelLine2="exercises"
panelContent={getAssetPath(featurePanelPath("exercises"))} panelContent={getAssetPath(featurePanelPath("exercises"))}
/> />
<Mini <Mini
backgroundColor="bg-[#F4CAFF]" backgroundColor="bg-[var(--color-surface-invert-brand-rust)]"
labelLine1="Membership" labelLine1="Membership"
labelLine2="guidance" labelLine2="guidance"
panelContent={getAssetPath(featurePanelPath("guidance"))} panelContent={getAssetPath(featurePanelPath("guidance"))}
/> />
<Mini <Mini
backgroundColor="bg-[#CBDDFF]" backgroundColor="bg-[var(--color-surface-invert-brand-teal)]"
labelLine1="Conflict resolution" labelLine1="Conflict resolution"
labelLine2="tools" labelLine2="tools"
panelContent={getAssetPath(featurePanelPath("tools"))} panelContent={getAssetPath(featurePanelPath("tools"))}
@@ -68,7 +68,7 @@ export const ColorVariants = {
export const AsLink = { export const AsLink = {
args: { args: {
backgroundColor: "bg-[var(--color-surface-default-brand-royal)]", backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]",
labelLine1: "Decision-making", labelLine1: "Decision-making",
labelLine2: "support", labelLine2: "support",
panelContent: getAssetPath(featurePanelPath("support")), panelContent: getAssetPath(featurePanelPath("support")),
+6 -35
View File
@@ -8,34 +8,12 @@ export default {
docs: { docs: {
description: { description: {
component: ` component: `
A responsive feature grid component that displays organizational tools and services in a clean card-based layout with supportive messaging and categorized feature highlights. Feature grid for the home marketing section (Figma 18847:22410).
## Features - **Layout**: 2×2 on mobile, 1×4 on tablet, horizontal lockup + grid on desktop
- **Shell**: \`surface/default/secondary\` content block with responsive spacing tokens
- **Responsive Layout**: Adapts from 2x2 grid on mobile to 1x4 grid on tablet to horizontal layout on desktop - **ContentLockup**: Feature variant — 18/22 title & 14/20 subtitle (mobile), 24/32 title & 16/24 subtitle (640px+), 16/24 link
- **ContentLockup Integration**: Uses the feature variant with "Learn more" link - **Mini tiles**: Invert-brand surfaces (royal, lime, rust, teal), 138px panel height, 48px icons
- **Mini grid**: Four feature tiles with color-coded backgrounds and icons
- **Accessibility**: Full keyboard navigation, focus indicators, and ARIA labels
- **Design System**: Uses design tokens for consistent spacing, colors, and typography
## Responsive Behavior
- **Mobile (< 768px)**: 2x2 grid layout with ContentLockup on top
- **Tablet (768px - 1024px)**: 1x4 grid layout with ContentLockup on top
- **Desktop (> 1024px)**: Horizontal layout with ContentLockup on left, 1x4 grid on right
## Interactive Elements
- **Mini tiles**: Hover effects, focus indicators, and keyboard navigation
- **Learn More Link**: Underlined link with focus states
- **Color-coded Features**: Royal, green, pink, and blue backgrounds for categorization
## Accessibility
- WCAG 2.1 AA compliant
- Keyboard navigation support
- Screen reader friendly with proper ARIA labels
- Focus management with visible indicators
`, `,
}, },
}, },
@@ -66,14 +44,7 @@ export const Default = {
docs: { docs: {
description: { description: {
story: ` story: `
Default FeatureGrid with standard content. This component demonstrates: Default FeatureGrid — responsive breakpoint layout with Figma styling (invert-brand tiles, secondary surface, updated lockup typography).
- **ContentLockup**: Feature variant with title, subtitle, and "Learn more" link
- **Mini grid**: Four feature tiles with different colors and icons
- **Responsive Design**: Layout adapts across mobile, tablet, and desktop breakpoints
- **Interactive States**: Hover effects and focus indicators on all interactive elements
The component uses a dark background (#171717) with rounded corners and proper spacing using design tokens.
`, `,
}, },
}, },
+69
View File
@@ -73,4 +73,73 @@ describe("FeatureGrid (behavioral tests)", () => {
const section = document.querySelector("section"); const section = document.querySelector("section");
expect(section).toBeInTheDocument(); expect(section).toBeInTheDocument();
}); });
it("uses Figma invert surface colors on mini tiles", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
expect(
document.querySelector(
'[class*="bg-[var(--color-surface-invert-brand-royal)]"]',
),
).toBeInTheDocument();
expect(
document.querySelector(
'[class*="bg-[var(--color-surface-invert-brand-lime)]"]',
),
).toBeInTheDocument();
expect(
document.querySelector(
'[class*="bg-[var(--color-surface-invert-brand-rust)]"]',
),
).toBeInTheDocument();
expect(
document.querySelector(
'[class*="bg-[var(--color-surface-invert-brand-teal)]"]',
),
).toBeInTheDocument();
});
it("marks the content block with the Figma node id", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
expect(
document.querySelector('[data-figma-node="18847-22410"]'),
).toBeInTheDocument();
});
it("uses Figma responsive typography on the feature lockup", () => {
render(<FeatureGrid title="Test Title" subtitle="Test Subtitle" />);
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" });
expect(subtitle.className).toMatch(/text-\[length:var\(--sizing-350,14px\)\]/);
expect(subtitle.className).toMatch(/md:text-\[length:var\(--sizing-400,16px\)\]/);
});
it("applies grain texture to feature grid icons", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const icons = document.querySelectorAll("section img");
expect(icons.length).toBeGreaterThanOrEqual(4);
icons.forEach((icon) => {
expect(icon.getAttribute("style")).toContain("#grain");
});
});
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");
});
}); });