diff --git a/app/components/cards/Mini/Mini.container.tsx b/app/components/cards/Mini/Mini.container.tsx index 7087b44..7397e63 100644 --- a/app/components/cards/Mini/Mini.container.tsx +++ b/app/components/cards/Mini/Mini.container.tsx @@ -21,6 +21,10 @@ const MiniContainer = memo( onClick, href, ariaLabel, + featureGridShell = false, + panelWidth, + panelHeight, + panelImageClassName, }) => { const t = useTranslation("controlsChrome"); @@ -92,6 +96,10 @@ const MiniContainer = memo( computedAriaLabel={computedAriaLabel} wrapperElement={wrapperElement} wrapperProps={wrapperProps} + featureGridShell={featureGridShell} + panelWidth={panelWidth} + panelHeight={panelHeight} + panelImageClassName={panelImageClassName} > {children} diff --git a/app/components/cards/Mini/Mini.types.ts b/app/components/cards/Mini/Mini.types.ts index ca0f41c..9f3f479 100644 --- a/app/components/cards/Mini/Mini.types.ts +++ b/app/components/cards/Mini/Mini.types.ts @@ -9,6 +9,11 @@ export interface MiniProps { onClick?: () => void; href?: string; ariaLabel?: string; + /** Figma Feature-Grid mini tile shell (18847:22410). */ + featureGridShell?: boolean; + panelWidth?: number; + panelHeight?: number; + panelImageClassName?: string; } export interface MiniViewProps { @@ -25,4 +30,8 @@ export interface MiniViewProps { | React.AnchorHTMLAttributes | React.ButtonHTMLAttributes | React.HTMLAttributes; + featureGridShell?: boolean; + panelWidth?: number; + panelHeight?: number; + panelImageClassName?: string; } diff --git a/app/components/cards/Mini/Mini.view.tsx b/app/components/cards/Mini/Mini.view.tsx index fe83111..fdd9434 100644 --- a/app/components/cards/Mini/Mini.view.tsx +++ b/app/components/cards/Mini/Mini.view.tsx @@ -2,6 +2,7 @@ import { memo } from "react"; import Image from "next/image"; +import { SVG_GRAIN_MULTIPLY_FILTER } from "../../../../lib/svgGrainFilter"; import type { MiniViewProps } from "./Mini.types"; function MiniView({ @@ -15,39 +16,59 @@ function MiniView({ computedAriaLabel, wrapperElement, wrapperProps, + featureGridShell = false, + panelWidth, + panelHeight, + panelImageClassName, }: 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 = ( -
- {/* Top part - Inner panel */} -
- {/* Content for the inner panel */} +
+
{panelContent && ( -
+
{computedAriaLabel}
)} {children}
- {/* Bottom part - Text container */} -
+
{labelLine1 && labelLine2 ? ( <>
{labelLine1}
{labelLine2}
-
 
) : ( label diff --git a/app/components/sections/FeatureGrid/FeatureGrid.container.tsx b/app/components/sections/FeatureGrid/FeatureGrid.container.tsx index ad0f1d2..a8b0ae6 100644 --- a/app/components/sections/FeatureGrid/FeatureGrid.container.tsx +++ b/app/components/sections/FeatureGrid/FeatureGrid.container.tsx @@ -1,11 +1,11 @@ "use client"; /** - * Figma: "Sections / FeatureGrid" (see registry) + * Figma: "Section / Feature-Grid" (18847:22410) */ import { memo, useMemo } from "react"; -import { getAssetPath, featurePanelPath } from "../../../../lib/assetUtils"; +import { getAssetPath, featurePanelLayout, featurePanelPath } from "../../../../lib/assetUtils"; import { useTranslation } from "../../../contexts/MessagesContext"; import FeatureGridView from "./FeatureGrid.view"; import type { FeatureGridProps, Feature } from "./FeatureGrid.types"; @@ -17,7 +17,7 @@ const FeatureGridContainer = memo( const features: Feature[] = useMemo( () => [ { - backgroundColor: "bg-[var(--color-surface-default-brand-royal)]", + backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]", labelLine1: t( "pages.home.featureGrid.features.decisionMaking.labelLine1", ), @@ -25,11 +25,12 @@ const FeatureGridContainer = memo( "pages.home.featureGrid.features.decisionMaking.labelLine2", ), panelContent: getAssetPath(featurePanelPath("support")), + ...featurePanelLayout("support"), ariaLabel: t("featureGrid.features.decisionMaking.ariaLabel"), href: "#decision-making", }, { - backgroundColor: "bg-[#D1FFE2]", + backgroundColor: "bg-[var(--color-surface-invert-brand-lime)]", labelLine1: t( "pages.home.featureGrid.features.valuesAlignment.labelLine1", ), @@ -37,11 +38,12 @@ const FeatureGridContainer = memo( "pages.home.featureGrid.features.valuesAlignment.labelLine2", ), panelContent: getAssetPath(featurePanelPath("exercises")), + ...featurePanelLayout("exercises"), ariaLabel: t("featureGrid.features.valuesAlignment.ariaLabel"), href: "#values-alignment", }, { - backgroundColor: "bg-[#F4CAFF]", + backgroundColor: "bg-[var(--color-surface-invert-brand-rust)]", labelLine1: t( "pages.home.featureGrid.features.membershipGuidance.labelLine1", ), @@ -49,11 +51,12 @@ const FeatureGridContainer = memo( "pages.home.featureGrid.features.membershipGuidance.labelLine2", ), panelContent: getAssetPath(featurePanelPath("guidance")), + ...featurePanelLayout("guidance"), ariaLabel: t("featureGrid.features.membershipGuidance.ariaLabel"), href: "#membership-guidance", }, { - backgroundColor: "bg-[#CBDDFF]", + backgroundColor: "bg-[var(--color-surface-invert-brand-teal)]", labelLine1: t( "pages.home.featureGrid.features.conflictResolution.labelLine1", ), @@ -61,6 +64,7 @@ const FeatureGridContainer = memo( "pages.home.featureGrid.features.conflictResolution.labelLine2", ), panelContent: getAssetPath(featurePanelPath("tools")), + ...featurePanelLayout("tools"), ariaLabel: t("featureGrid.features.conflictResolution.ariaLabel"), href: "#conflict-resolution", }, diff --git a/app/components/sections/FeatureGrid/FeatureGrid.types.ts b/app/components/sections/FeatureGrid/FeatureGrid.types.ts index 31e061a..e767831 100644 --- a/app/components/sections/FeatureGrid/FeatureGrid.types.ts +++ b/app/components/sections/FeatureGrid/FeatureGrid.types.ts @@ -9,6 +9,9 @@ export interface Feature { labelLine1: string; labelLine2: string; panelContent: string; + panelWidth: number; + panelHeight: number; + panelImageClassName?: string; ariaLabel: string; href: string; } diff --git a/app/components/sections/FeatureGrid/FeatureGrid.view.tsx b/app/components/sections/FeatureGrid/FeatureGrid.view.tsx index 48f8c14..2555aaf 100644 --- a/app/components/sections/FeatureGrid/FeatureGrid.view.tsx +++ b/app/components/sections/FeatureGrid/FeatureGrid.view.tsx @@ -5,6 +5,7 @@ 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). */ function FeatureGridView({ title, subtitle, @@ -23,10 +24,12 @@ function FeatureGridView({ aria-labelledby={labelledBy} aria-label={labelledBy ? undefined : ariaLabel} > -
-
- {/* Feature Content Lockup */} -
+
+
+
- {/* Mini grid */} -
+
{features.map((feature, index) => ( ))}
diff --git a/app/components/sections/QuoteBlock/QuoteStatementDecor.tsx b/app/components/sections/QuoteBlock/QuoteStatementDecor.tsx index b666783..1007ae9 100644 --- a/app/components/sections/QuoteBlock/QuoteStatementDecor.tsx +++ b/app/components/sections/QuoteBlock/QuoteStatementDecor.tsx @@ -2,14 +2,12 @@ import { memo } from "react"; import { getAssetPath, quoteStatementShapePath } from "../../../../lib/assetUtils"; +import { SVG_GRAIN_MULTIPLY_FILTER } from "../../../../lib/svgGrainFilter"; /** Figma: Section / Quote — **`shape-quote.svg`** (22137:890679). */ 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%)"; -const GRAIN_MULTIPLY_FILTER = - 'url(\'data:image/svg+xml;charset=utf-8,#grain\')'; - const QuoteStatementDecor = memo<{ className?: string }>(({ className = "" }) => { const src = getAssetPath(quoteStatementShapePath()); const bg = `url("${src}")`; @@ -29,8 +27,8 @@ const QuoteStatementDecor = memo<{ className?: string }>(({ className = "" }) => maskSize: "100% 100%", WebkitMaskRepeat: "no-repeat", maskRepeat: "no-repeat", - filter: GRAIN_MULTIPLY_FILTER, - WebkitFilter: GRAIN_MULTIPLY_FILTER, + filter: SVG_GRAIN_MULTIPLY_FILTER, + WebkitFilter: SVG_GRAIN_MULTIPLY_FILTER, }} /> ); diff --git a/app/components/type/ContentLockup/ContentLockup.container.tsx b/app/components/type/ContentLockup/ContentLockup.container.tsx index 49ad3ff..da97276 100644 --- a/app/components/type/ContentLockup/ContentLockup.container.tsx +++ b/app/components/type/ContentLockup/ContentLockup.container.tsx @@ -45,14 +45,14 @@ const ContentLockupContainer = memo( "w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]", }, feature: { - container: "flex flex-col gap-[var(--spacing-scale-012)] relative z-10", - textContainer: "flex flex-col gap-[var(--spacing-scale-012)]", - titleGroup: "flex flex-col gap-[var(--spacing-scale-012)]", - titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", + container: "flex flex-col gap-[var(--space-400,16px)] md:gap-[var(--space-500,20px)] relative z-10", + textContainer: "flex flex-col gap-[var(--space-100,4px)] md:gap-[var(--space-150,6px)]", + titleGroup: "flex flex-col gap-[var(--space-100,4px)] md:gap-[var(--space-150,6px)]", + titleContainer: "flex items-center", 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: - "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: "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: diff --git a/app/components/type/ContentLockup/ContentLockup.view.tsx b/app/components/type/ContentLockup/ContentLockup.view.tsx index c0a5505..b9a7999 100644 --- a/app/components/type/ContentLockup/ContentLockup.view.tsx +++ b/app/components/type/ContentLockup/ContentLockup.view.tsx @@ -97,7 +97,7 @@ function ContentLockupView({ {variant === "feature" && linkText && ( {linkText} diff --git a/docs/guides/static-assets.md b/docs/guides/static-assets.md index 5665bfb..a3d611c 100644 --- a/docs/guides/static-assets.md +++ b/docs/guides/static-assets.md @@ -57,7 +57,7 @@ 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-*.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/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG | | `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster | diff --git a/lib/assetUtils.ts b/lib/assetUtils.ts index f9f5c87..22f1a47 100644 --- a/lib/assetUtils.ts +++ b/lib/assetUtils.ts @@ -106,7 +106,32 @@ export function governanceBookletPath(): string { export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools"; 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/`. */ diff --git a/lib/svgGrainFilter.ts b/lib/svgGrainFilter.ts new file mode 100644 index 0000000..1b7924d --- /dev/null +++ b/lib/svgGrainFilter.ts @@ -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,#grain\')'; diff --git a/public/assets/marketing/feature-exercises.png b/public/assets/marketing/feature-exercises.png deleted file mode 100644 index 1af6a6c..0000000 Binary files a/public/assets/marketing/feature-exercises.png and /dev/null differ diff --git a/public/assets/marketing/feature-exercises.svg b/public/assets/marketing/feature-exercises.svg new file mode 100644 index 0000000..3ee984f --- /dev/null +++ b/public/assets/marketing/feature-exercises.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/marketing/feature-guidance.png b/public/assets/marketing/feature-guidance.png deleted file mode 100644 index 972e5e5..0000000 Binary files a/public/assets/marketing/feature-guidance.png and /dev/null differ diff --git a/public/assets/marketing/feature-guidance.svg b/public/assets/marketing/feature-guidance.svg new file mode 100644 index 0000000..0397007 --- /dev/null +++ b/public/assets/marketing/feature-guidance.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/assets/marketing/feature-support.png b/public/assets/marketing/feature-support.png deleted file mode 100644 index 9ddcdc2..0000000 Binary files a/public/assets/marketing/feature-support.png and /dev/null differ diff --git a/public/assets/marketing/feature-support.svg b/public/assets/marketing/feature-support.svg new file mode 100644 index 0000000..c92d412 --- /dev/null +++ b/public/assets/marketing/feature-support.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/assets/marketing/feature-tools.png b/public/assets/marketing/feature-tools.png deleted file mode 100644 index 9fa79b2..0000000 Binary files a/public/assets/marketing/feature-tools.png and /dev/null differ diff --git a/public/assets/marketing/feature-tools.svg b/public/assets/marketing/feature-tools.svg new file mode 100644 index 0000000..5ca1c5e --- /dev/null +++ b/public/assets/marketing/feature-tools.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/stories/cards/Mini.stories.js b/stories/cards/Mini.stories.js index 2a2f68f..aa3efac 100644 --- a/stories/cards/Mini.stories.js +++ b/stories/cards/Mini.stories.js @@ -11,10 +11,10 @@ export default { backgroundColor: { control: "select", options: [ - "bg-[var(--color-surface-default-brand-royal)]", - "bg-[#D1FFE2]", - "bg-[#F4CAFF]", - "bg-[#CBDDFF]", + "bg-[var(--color-surface-invert-brand-royal)]", + "bg-[var(--color-surface-invert-brand-lime)]", + "bg-[var(--color-surface-invert-brand-rust)]", + "bg-[var(--color-surface-invert-brand-teal)]", ], }, labelLine1: { control: "text" }, @@ -28,7 +28,7 @@ export default { export const Default = { args: { - backgroundColor: "bg-[var(--color-surface-default-brand-royal)]", + backgroundColor: "bg-[var(--color-surface-invert-brand-royal)]", labelLine1: "Decision-making", labelLine2: "support", panelContent: getAssetPath(featurePanelPath("support")), @@ -39,25 +39,25 @@ export const ColorVariants = { render: () => (
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 +- **Layout**: 2×2 on mobile, 1×4 on 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 `, }, }, @@ -66,14 +44,7 @@ export const Default = { docs: { description: { story: ` -Default FeatureGrid with standard content. This component demonstrates: - -- **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. +Default FeatureGrid — responsive breakpoint layout with Figma styling (invert-brand tiles, secondary surface, updated lockup typography). `, }, }, diff --git a/tests/components/FeatureGrid.test.tsx b/tests/components/FeatureGrid.test.tsx index e9ef002..d0e2f41 100644 --- a/tests/components/FeatureGrid.test.tsx +++ b/tests/components/FeatureGrid.test.tsx @@ -73,4 +73,73 @@ describe("FeatureGrid (behavioral tests)", () => { const section = document.querySelector("section"); expect(section).toBeInTheDocument(); }); + + it("uses Figma invert surface colors on mini tiles", () => { + render(); + 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(); + expect( + document.querySelector('[data-figma-node="18847-22410"]'), + ).toBeInTheDocument(); + }); + + it("uses Figma responsive typography on the feature lockup", () => { + render(); + 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(); + 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(); + 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"); + }); });