diff --git a/app/components/asset/logo/Logo.tsx b/app/components/asset/Logo/Logo.tsx similarity index 61% rename from app/components/asset/logo/Logo.tsx rename to app/components/asset/Logo/Logo.tsx index d639006..6e2d539 100644 --- a/app/components/asset/logo/Logo.tsx +++ b/app/components/asset/Logo/Logo.tsx @@ -36,54 +36,54 @@ const Logo = memo( ({ size = "default", palette = "default", wordmark = true }) => { const t = useTranslation("controlsChrome"); - // Size configurations + // Figma: "Asset / Logo" (4002:6735) — 40×6×27 at 1x; nav/footer/create-flow + // instances scale that lockup. const sizes: Record = { default: { - containerHeight: "h-[41px]", - gap: "gap-[8.28px]", + containerHeight: "h-[var(--spacing-scale-040)]", + gap: "gap-[var(--spacing-scale-006)]", textSize: "text-[21.97px]", lineHeight: "leading-[27.05px]", - iconSize: "w-[27.05px] h-[27.05px]", + iconSize: "w-[27px] h-[27px]", }, footer: { - containerHeight: - "h-[41px] md:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]", - gap: "gap-[8.28px] md:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]", + containerHeight: "h-[41px] md:h-[56px] lg:h-[82px]", + gap: "gap-[6.21px] md:gap-[8.49px] lg:gap-[12.43px]", textSize: - "text-[21.97px] md:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]", + "text-[22.75px] md:text-[31.08px] lg:text-[45.51px]", lineHeight: - "leading-[27.05px] md:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]", + "leading-[28px] md:leading-[38.25px] lg:leading-[56.01px]", iconSize: - "w-[27.05px] h-[27.05px] md:w-[calc(27.05px*1.37)] md:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]", + "w-[27.96px] h-[27.96px] md:w-[38.18px] md:h-[38.18px] lg:w-[55.91px] lg:h-[55.91px]", }, createFlow: { - containerHeight: "h-[30px] md:h-[41px]", - gap: "gap-[6px] md:gap-[8.28px]", + containerHeight: "h-[30px] md:h-[var(--spacing-scale-040)]", + gap: "gap-[4.5px] md:gap-[var(--spacing-scale-006)]", textSize: "text-[16.48px] md:text-[21.97px]", lineHeight: "leading-[20.28px] md:leading-[27.05px]", - iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]", + iconSize: "w-[20.25px] h-[20.25px] md:w-[27px] md:h-[27px]", }, topNavFolderTop: { containerHeight: - "h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]", - gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]", + "h-[14.1px] sm:h-[21.35px] md:h-[var(--spacing-scale-032)] lg:h-[var(--spacing-scale-040)] xl:h-[50.35px]", + gap: "gap-0 sm:gap-[3.2px] md:gap-[4.8px] lg:gap-[var(--spacing-scale-006)] xl:gap-[7.5px]", textSize: - "text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]", + "text-[11.57px] sm:text-[11.72px] md:text-[17.58px] lg:text-[21.97px] xl:text-[27.47px]", lineHeight: - "leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]", + "leading-[14.24px] sm:leading-[14.42px] md:leading-[21.64px] lg:leading-[27.05px] xl:leading-[33.81px]", iconSize: - "w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]", + "w-[14.1px] h-[14.1px] sm:w-[14.4px] sm:h-[14.4px] md:w-[21.6px] md:h-[21.6px] lg:w-[27px] lg:h-[27px] xl:w-[33.75px] xl:h-[33.75px]", }, topNavHeader: { containerHeight: - "h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]", - gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]", + "h-[14.2px] sm:h-[21.22px] md:h-[var(--spacing-scale-032)] lg:h-[40.69px] xl:h-[50.35px]", + gap: "gap-0 sm:gap-[3.18px] md:gap-[4.85px] lg:gap-[6.06px] xl:gap-[7.5px]", textSize: - "text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]", + "text-[11.57px] sm:text-[11.66px] md:text-[17.76px] lg:text-[22.2px] xl:text-[27.47px]", lineHeight: - "leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]", + "leading-[14.24px] sm:leading-[14.35px] md:leading-[21.86px] lg:leading-[27.32px] xl:leading-[33.81px]", iconSize: - "w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]", + "w-[14.2px] h-[14.2px] sm:w-[14.33px] sm:h-[14.33px] md:w-[21.82px] md:h-[21.82px] lg:w-[27.27px] lg:h-[27.27px] xl:w-[33.75px] xl:h-[33.75px]", }, }; @@ -121,8 +121,8 @@ const Logo = memo( {t("logoAlt")}(({ logos, className = "" }) => { const t = useTranslation("logoWall"); - const [isVisible, setIsVisible] = useState(false); const defaultLogos = useMemo( () => [ { src: getAssetPath(partnerLogoPath("food-not-bombs")), alt: t("partners.foodNotBombs"), - size: "h-11 lg:h-14 xl:h-[70px]", + size: "h-[48px] lg:h-[60px] xl:h-[74.6px]", + order: "sm:order-4 md:order-4", }, { src: getAssetPath(partnerLogoPath("start-coop")), alt: t("partners.startCoop"), - size: "h-[42px] lg:h-[53px] xl:h-[66px]", + size: "h-[51.4px] lg:h-[64.2px] xl:h-[80.3px]", + order: "sm:order-1 md:order-3", }, { src: getAssetPath(partnerLogoPath("metagov")), alt: t("partners.metagov"), - size: "h-6 lg:h-8 xl:h-[41px]", + size: "h-[26.2px] lg:h-[33px] xl:h-[41px]", + order: "sm:order-6 md:order-1", }, { src: getAssetPath(partnerLogoPath("open-civics")), alt: t("partners.openCivics"), - size: "h-8 lg:h-10 xl:h-[50px]", + size: "h-[50.1px] lg:h-[61.1px] xl:h-[72px]", + order: "sm:order-5 md:order-2", }, { src: getAssetPath(partnerLogoPath("mutual-aid-co")), alt: t("partners.mutualAidCo"), - size: "h-11 lg:h-14 xl:h-[70px]", + size: "h-[51.1px] lg:h-[63.6px] xl:h-[79.5px]", + order: "sm:order-2 md:order-5", }, { src: getAssetPath(partnerLogoPath("cu-boulder")), alt: t("partners.cuBoulder"), - size: "h-10 lg:h-12 xl:h-[60px]", + size: "h-[44.6px] lg:h-[55.8px] xl:h-[69.7px]", + order: "sm:order-3 md:order-6", }, ], [t], @@ -55,18 +62,9 @@ const LogoWallContainer = memo(({ logos, className = "" }) => { [logos, defaultLogos], ); - useEffect(() => { - // Trigger fade-in animation after component mounts - const timer = setTimeout(() => { - setIsVisible(true); - }, 100); - - return () => clearTimeout(timer); - }, []); - return ( diff --git a/app/components/sections/LogoWall/LogoWall.types.ts b/app/components/sections/LogoWall/LogoWall.types.ts index b186101..fb480e3 100644 --- a/app/components/sections/LogoWall/LogoWall.types.ts +++ b/app/components/sections/LogoWall/LogoWall.types.ts @@ -11,7 +11,7 @@ export interface LogoWallProps { } export interface LogoWallViewProps { - isVisible: boolean; + heading: string; displayLogos: Array<{ src: string; alt: string; diff --git a/app/components/sections/LogoWall/LogoWall.view.tsx b/app/components/sections/LogoWall/LogoWall.view.tsx index 02c7571..c1a82cd 100644 --- a/app/components/sections/LogoWall/LogoWall.view.tsx +++ b/app/components/sections/LogoWall/LogoWall.view.tsx @@ -5,25 +5,24 @@ import Image from "next/image"; import type { LogoWallViewProps } from "./LogoWall.types"; function LogoWallView({ - isVisible, + heading, displayLogos, className, }: LogoWallViewProps) { return (
-
-
-
+
+

+ {heading} +

+
+
{displayLogos.map((logo, index) => (
diff --git a/messages/en/components/logoWall.json b/messages/en/components/logoWall.json index 4863468..01d634c 100644 --- a/messages/en/components/logoWall.json +++ b/messages/en/components/logoWall.json @@ -1,4 +1,5 @@ { + "heading": "Trusted by leading cooperators", "partners": { "foodNotBombs": "Food Not Bombs", "metagov": "Metagov", diff --git a/stories/sections/LogoWall.stories.js b/stories/sections/LogoWall.stories.js index 5691f45..3cc55b7 100644 --- a/stories/sections/LogoWall.stories.js +++ b/stories/sections/LogoWall.stories.js @@ -11,16 +11,15 @@ export default { ## Responsive Behavior -- **Mobile**: 3 rows × 2 columns grid with 32px gaps -- **SM**: 2 rows × 3 columns grid with 48px row gap and 32px column gap -- **MD+**: Centered flex-wrap row of logos -- **LG**: Larger logo sizes and 64px horizontal padding -- **XL**: Largest logo sizes and 160px horizontal padding +- **Mobile**: 3 rows × 2 columns grid with 38.4px gaps +- **SM**: 2 rows × 3 columns grid with 38.4px gaps +- **MD+**: Single space-between row (no wrap) +- **LG**: 1.25× logo sizes, 48px gaps, 64px horizontal padding +- **XL**: 1.5625× logo sizes, 48px gaps, 160px horizontal padding ## Animations & Transitions -- **Fade-in Effect**: Logos fade in from opacity 0 to 60% after component mounts (500ms transition) -- **Loading States**: Progressive loading with fallback timer for reliable display +- **Opacity**: Partner logos render at 60% opacity to match Figma `Section / Logo Wall`. ## Props @@ -70,7 +69,7 @@ export const Default = { docs: { description: { story: - "Default LogoWall with all partner logos. Displays in a 3×2 grid on mobile, 2×3 grid on small screens, single row on medium screens, and larger sizes on large screens. Features a fade-in on mount.", + "Default LogoWall with all partner logos. Displays in a 2×3 grid on mobile, 3×2 grid from 430px, and a space-between row from 640px with Figma lockup sizes. Logos render at 60% opacity to match Figma.", }, }, }, diff --git a/tests/components/Logo.test.tsx b/tests/components/Logo.test.tsx index 29abcf4..1b9bedf 100644 --- a/tests/components/Logo.test.tsx +++ b/tests/components/Logo.test.tsx @@ -72,6 +72,13 @@ describe("Logo (behavioral tests)", () => { expect(img).toHaveClass("brightness-0"); }); + it("uses the Figma 6px wordmark-to-mark gap at the default size", () => { + const { container } = render(); + const row = container.querySelector("a > div"); + expect(row?.className).toContain("gap-[var(--spacing-scale-006)]"); + expect(row?.className).toContain("h-[var(--spacing-scale-040)]"); + }); + it("renders with different size variants", () => { const { rerender } = render(); expect(screen.getByRole("link")).toBeInTheDocument(); diff --git a/tests/unit/LogoWall.test.jsx b/tests/unit/LogoWall.test.jsx index 3c7c8da..c13d831 100644 --- a/tests/unit/LogoWall.test.jsx +++ b/tests/unit/LogoWall.test.jsx @@ -46,7 +46,10 @@ describe("LogoWall Component", () => { render(); const section = document.querySelector("section"); - expect(section).toHaveClass("p-[var(--spacing-scale-032)]"); + expect(section).toHaveClass( + "bg-[var(--color-surface-default-primary)]", + "p-[var(--spacing-scale-032)]", + ); }); test("renders with design tokens", () => { @@ -70,7 +73,9 @@ describe("LogoWall Component", () => { "sm:grid-cols-3", "sm:grid-rows-2", "md:flex", - "md:justify-center", + "md:justify-between", + "md:flex-nowrap", + "gap-[38.4px]", ); }); @@ -82,7 +87,11 @@ describe("LogoWall Component", () => { "src", "/assets/logos/partners/food-not-bombs.svg", ); - expect(foodNotBombsLogo).toHaveClass("h-11", "lg:h-14", "xl:h-[70px]"); + expect(foodNotBombsLogo).toHaveClass( + "h-[48px]", + "lg:h-[60px]", + "xl:h-[74.6px]", + ); }); test("handles empty logos array", () => { @@ -109,16 +118,17 @@ describe("LogoWall Component", () => { const section = document.querySelector("section"); expect(section).toBeInTheDocument(); - expect(screen.queryByText("Trusted by leading cooperators")).not.toBeInTheDocument(); + expect( + screen.getByText("Trusted by leading cooperators"), + ).toBeInTheDocument(); }); - test("applies transition effects", () => { + test("renders partner logos at Figma 60% opacity", () => { render(); - const logoContainers = document.querySelectorAll( - '[class*="transition-opacity duration-500"]', - ); - expect(logoContainers.length).toBeGreaterThan(0); + expect( + document.querySelectorAll('[class*="opacity-60"]').length, + ).toBeGreaterThan(0); }); test("renders with proper image optimization", () => {