diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index 8c30ccf..ad82ead 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -51,9 +51,6 @@ export default function Page() { title: t("pages.home.heroBanner.title"), subtitle: t("pages.home.heroBanner.subtitle"), description: t("pages.home.heroBanner.description"), - ctaText: t("pages.home.heroBanner.ctaText"), - ctaHref: t("pages.home.heroBanner.ctaHref"), - imageAlt: t("heroBanner.imageAlt"), }; const cardStepsData = { diff --git a/app/components/asset/Logo/Logo.tsx b/app/components/asset/Logo/Logo.tsx index 3b69457..996362d 100644 --- a/app/components/asset/Logo/Logo.tsx +++ b/app/components/asset/Logo/Logo.tsx @@ -95,11 +95,15 @@ const Logo = memo( const wordmarkVisibilityClass = size === "topNavHeader" ? wordmark - ? "hidden md:block" + ? "hidden sm:block" : "hidden" - : wordmark - ? "" - : "hidden"; + : size === "topNavFolderTop" + ? wordmark + ? "hidden md:block" + : "hidden" + : wordmark + ? "" + : "hidden"; return ( diff --git a/app/components/cards/Rule/Rule.container.tsx b/app/components/cards/Rule/Rule.container.tsx index 70768b9..88f4fe4 100644 --- a/app/components/cards/Rule/Rule.container.tsx +++ b/app/components/cards/Rule/Rule.container.tsx @@ -43,6 +43,7 @@ const RuleContainer = memo( logoAlt, communityInitials, hideCategoryAddButton = false, + interactive, hasBottomLinks = false, bottomStatusLabel, bottomLinks, @@ -53,12 +54,13 @@ const RuleContainer = memo( surfaceColor, }) => { const size = sizeProp ?? "L"; + const isInteractive = interactive ?? !hasBottomLinks; const t = useTranslation("ruleCard"); const cardAriaLabel = t("ariaLabel")?.replace("{title}", title) || title; const recommendedLabel = t("recommendedLabel"); const handleClick = () => { - if (hasBottomLinks) return; + if (!isInteractive || hasBottomLinks) return; // Basic analytics event tracking if (typeof window !== "undefined" && window.gtag) { window.gtag("event", "template_selected", { @@ -79,7 +81,7 @@ const RuleContainer = memo( }; const handleKeyDown = (event: React.KeyboardEvent) => { - if (hasBottomLinks) return; + if (!isInteractive || hasBottomLinks) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); handleClick(); @@ -98,9 +100,11 @@ const RuleContainer = memo( icon={icon} backgroundColor={backgroundColor} className={className} - href={hasBottomLinks ? undefined : href} - onClick={hasBottomLinks ? undefined : handleClick} - onKeyDown={hasBottomLinks || href ? undefined : handleKeyDown} + href={isInteractive && !hasBottomLinks ? href : undefined} + onClick={isInteractive && !hasBottomLinks ? handleClick : undefined} + onKeyDown={ + isInteractive && !hasBottomLinks && !href ? handleKeyDown : undefined + } expanded={expanded} size={size} categories={categories} @@ -108,6 +112,7 @@ const RuleContainer = memo( logoAlt={logoAlt} communityInitials={communityInitials} hideCategoryAddButton={hideCategoryAddButton} + interactive={isInteractive} hasBottomLinks={hasBottomLinks} bottomStatusLabel={bottomStatusLabel} bottomLinks={bottomLinks} diff --git a/app/components/cards/Rule/Rule.types.ts b/app/components/cards/Rule/Rule.types.ts index d4b8303..1bca614 100644 --- a/app/components/cards/Rule/Rule.types.ts +++ b/app/components/cards/Rule/Rule.types.ts @@ -63,6 +63,11 @@ export interface RuleProps { communityInitials?: string; /** Hide the per-category "+" add chip affordance (e.g. read-only template review). */ hideCategoryAddButton?: boolean; + /** + * When false, the card is a static article (Home hero). Default follows + * {@link hasBottomLinks}: interactive unless bottom links are shown. + */ + interactive?: boolean; /** * Figma `Card / Rule` variant: description + optional status chip + text links * (e.g. Duplicate / Delete, or Continue / Start new rule). When set, the card @@ -117,6 +122,7 @@ export interface RuleViewProps { logoAlt?: string; communityInitials?: string; hideCategoryAddButton?: boolean; + interactive?: boolean; hasBottomLinks?: boolean; bottomStatusLabel?: string; bottomLinks?: RuleBottomLink[]; diff --git a/app/components/cards/Rule/Rule.view.tsx b/app/components/cards/Rule/Rule.view.tsx index 4465e1c..6700353 100644 --- a/app/components/cards/Rule/Rule.view.tsx +++ b/app/components/cards/Rule/Rule.view.tsx @@ -29,6 +29,7 @@ export function RuleView({ logoAlt, communityInitials, hideCategoryAddButton = false, + interactive, hasBottomLinks = false, bottomStatusLabel, bottomLinks, @@ -41,7 +42,7 @@ export function RuleView({ recommendedLabel, }: RuleViewProps) { const ariaLabel = cardAriaLabel; - const interactiveCard = !hasBottomLinks; + const interactiveCard = interactive ?? !hasBottomLinks; // Size-based styling const isLarge = size === "L"; diff --git a/app/components/navigation/Top/Top.container.tsx b/app/components/navigation/Top/Top.container.tsx index ae9b420..64f3964 100644 --- a/app/components/navigation/Top/Top.container.tsx +++ b/app/components/navigation/Top/Top.container.tsx @@ -1,8 +1,8 @@ "use client"; /** - * Figma: "Navigation / Top" (22078-808559); Folder-top 430–639 (18582:22056), - * 640–1023 (18580:17113). + * Figma: "Navigation / Top" (5944:17779); Folder-top 320–429 (18583:22098), + * 430–639 (18582:22056); standard 320–429 (18411:57843). */ import { memo, useCallback } from "react"; @@ -82,7 +82,7 @@ const TopContainer = memo( const logoSize = folderTop ? "topNavFolderTop" : "topNavHeader"; const navigationItems = [ - { href: "/use-cases", text: t("navigation.useCases"), extraPadding: true }, + { href: "/workshops", text: t("navigation.workshops") }, { href: "/learn", text: t("navigation.learn") }, { href: "/about", text: t("navigation.about") }, ]; @@ -93,30 +93,25 @@ const TopContainer = memo( ? FOLDER_NAV_ITEM_RESPONSIVE_CLASS : NAV_ITEM_RESPONSIVE_CLASS; - return navigationItems.map((item) => { - const isUseCases = item.extraPadding === true; - - return ( - - {item.text} - - ); - }); + return navigationItems.map((item) => ( + + {item.text} + + )); }; - const renderLoginButton = () => { + const renderLoginButton = (className = "") => { const label = loggedIn ? t("buttons.profile") : t("buttons.logIn"); const ariaLabel = loggedIn ? t("ariaLabels.goToProfile") @@ -124,16 +119,23 @@ const TopContainer = memo( const navSelected = (loggedIn && pathname === "/profile") || (!loggedIn && pathname === "/login"); - const loginClassName = folderTop - ? FOLDER_NAV_ITEM_RESPONSIVE_CLASS - : NAV_ITEM_RESPONSIVE_CLASS; + const loginClassName = [ + folderTop + ? FOLDER_NAV_ITEM_RESPONSIVE_CLASS + : NAV_ITEM_RESPONSIVE_CLASS, + className, + ] + .filter(Boolean) + .join(" "); + const inFolderTab = folderTop && className.includes("md:hidden"); + const mode = inFolderTab ? "inverse" : "default"; if (loggedIn) { return ( ( } href="/login" size="X Small" - mode="default" + mode={mode} state={navSelected ? "selected" : "default"} className={loginClassName} ariaLabel={ariaLabel} @@ -182,8 +184,8 @@ const TopContainer = memo( size="small" className={ folderTop - ? "md:-space-x-[9px] lg:-space-x-[var(--space-250)] xl:-space-x-[13px]" - : "lg:-space-x-[var(--space-250)] xl:-space-x-[13px]" + ? "max-sm:hidden md:-space-x-[9px] lg:-space-x-[var(--space-250)] xl:-space-x-[13px]" + : "max-sm:hidden lg:-space-x-[var(--space-250)] xl:-space-x-[13px]" } > {avatarImageSources.map((src, index) => ( diff --git a/app/components/navigation/Top/Top.types.ts b/app/components/navigation/Top/Top.types.ts index f22a8a6..99f8028 100644 --- a/app/components/navigation/Top/Top.types.ts +++ b/app/components/navigation/Top/Top.types.ts @@ -28,6 +28,6 @@ export interface TopViewProps { }; logoSize: "topNavFolderTop" | "topNavHeader"; renderNavigationItems: () => React.ReactNode; - renderLoginButton: () => React.ReactNode; + renderLoginButton: (_className?: string) => React.ReactNode; renderCreateRuleButton: () => React.ReactNode; } diff --git a/app/components/navigation/Top/Top.view.tsx b/app/components/navigation/Top/Top.view.tsx index 968900b..eb845db 100644 --- a/app/components/navigation/Top/Top.view.tsx +++ b/app/components/navigation/Top/Top.view.tsx @@ -22,8 +22,9 @@ function TopView({ }: TopViewProps) { const t = useTranslation(folderTop ? "homeHeader" : "header"); - const loginControl = logIn ? ( - + const phoneAuthItem = logIn ? renderLoginButton("md:hidden") : null; + const desktopAuthItem = logIn ? ( + {renderLoginButton()} ) : null; @@ -40,13 +41,14 @@ function TopView({ className="w-full overflow-visible bg-transparent" role="banner" aria-label={t("ariaLabels.homePageNavigationHeader")} + data-figma-node="5944-17779" > @@ -105,11 +108,10 @@ function TopView({ /** * Standard marketing / app top nav. - * Figma: "Navigation / Top" (Community-Rule-System, node 22078-808559). - * Below md: auto | 1fr | auto so the logo cannot paint over the cluster. - * From md: 1fr | auto | 1fr so the cluster sits on the same page midline - * as Folder-top (Create rule is wider than the logo, so a leftover-column - * center sits off-center). + * Figma: "Navigation / Top" (Community-Rule-System, node 5944:17779). + * Below md: logo | links (with Log in / Profile) | Create rule, so the mark + * cannot paint over the cluster. From md: 1fr | auto | 1fr so the cluster + * sits on the page midline. */ return ( <> @@ -121,13 +123,17 @@ function TopView({ className="relative z-50 w-full border-b border-[var(--border-color-default-tertiary)] bg-[var(--color-surface-default-primary)]" role="banner" aria-label={t("ariaLabels.mainNavigationHeader")} + data-figma-node="5944-17779" > diff --git a/app/components/sections/AskOrganizer/AskOrganizer.view.tsx b/app/components/sections/AskOrganizer/AskOrganizer.view.tsx index b64f052..4538111 100644 --- a/app/components/sections/AskOrganizer/AskOrganizer.view.tsx +++ b/app/components/sections/AskOrganizer/AskOrganizer.view.tsx @@ -44,6 +44,7 @@ function AskOrganizerView({ variant={lockupVariant} alignment={lockupAlignment} titleId={labelledBy} + headingLevel="h2" />
+ title={title} + subtitle={subtitle} + variant="feature" + linkText={linkText} + linkHref={linkHref} + titleId={labelledBy} + headingLevel="h2" + />
diff --git a/app/components/sections/HeroBanner/HeroBanner.container.tsx b/app/components/sections/HeroBanner/HeroBanner.container.tsx new file mode 100644 index 0000000..6805c4e --- /dev/null +++ b/app/components/sections/HeroBanner/HeroBanner.container.tsx @@ -0,0 +1,85 @@ +"use client"; + +/** + * Figma: "Section / Hero Banner" (18477:15473) + */ + +import { memo, useCallback, useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useMessages, useTranslation } from "../../../contexts/MessagesContext"; +import { fetchAuthSession } from "../../../../lib/create/api"; +import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry"; +import type { Category } from "../../cards/Rule"; +import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types"; +import HeroBannerView from "./HeroBanner.view"; +import type { HeroBannerProps } from "./HeroBanner.types"; + +function chipsFromLabels(labels: string[]): ChipOption[] { + return labels.map((label) => ({ + id: label.toLowerCase().replace(/[^a-z0-9]+/g, "-"), + label, + state: "unselected" as const, + })); +} + +const HeroBannerContainer = memo( + ({ title, subtitle, description }) => { + const router = useRouter(); + const messages = useMessages(); + const tHome = useTranslation("pages.home.heroBanner"); + const tHero = useTranslation("heroBanner"); + const [signedIn, setSignedIn] = useState(false); + + useEffect(() => { + let cancelled = false; + void fetchAuthSession() + .then(({ user }) => { + if (!cancelled) setSignedIn(Boolean(user)); + }) + .catch(() => { + /* guest fallback */ + }); + return () => { + cancelled = true; + }; + }, []); + + const handleCreateRule = useCallback(() => { + prepareFreshCreateFlowEntrySync({ signedIn }); + router.push("/create/informational"); + }, [router, signedIn]); + + const ruleCategories = useMemo(() => { + const groups = messages.heroBanner.ruleCard.categories; + return [ + groups.values, + groups.communication, + groups.membership, + groups.decisionMaking, + groups.conflictManagement, + ].map((group) => ({ + name: group.name, + chipOptions: chipsFromLabels(group.chips), + })); + }, [messages]); + + return ( + + ); + }, +); + +HeroBannerContainer.displayName = "HeroBanner"; + +export default HeroBannerContainer; diff --git a/app/components/sections/HeroBanner/HeroBanner.tsx b/app/components/sections/HeroBanner/HeroBanner.tsx deleted file mode 100644 index f285ca6..0000000 --- a/app/components/sections/HeroBanner/HeroBanner.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Figma: "Sections / Hero" (see registry) - */ - -import { memo } from "react"; -import Image from "next/image"; -import ContentLockup from "../../type/ContentLockup"; -import HeroDecor from "./HeroDecor"; -import { ASSETS, getAssetPath } from "../../../../lib/assetUtils"; - -/** - * Intrinsic dimensions of `public/assets/marketing/hero-image.png` (2560×1600, - * 16:10). Passed to `next/image` to reserve aspect ratio + drive responsive - * srcset generation. Actual rendered size is governed by `sizes`. - */ -const HERO_IMAGE_WIDTH = 2560; -const HERO_IMAGE_HEIGHT = 1600; - -interface HeroBannerProps { - title?: string; - subtitle?: string; - description?: string; - ctaText?: string; - ctaHref?: string; - imageAlt?: string; -} - -const HeroBanner = memo( - ({ - title, - subtitle, - description, - ctaText, - ctaHref, - imageAlt = "Hero illustration", - }) => { - return ( -
-
- {/* Frame container for content */} -
- {/* DECORATIONS (behind content) */} - - - {/* Content lockup - Large variant */} -
- -
- - {/* Hero Image Container */} -
- {imageAlt} -
-
-
-
- ); - }, -); - -HeroBanner.displayName = "HeroBanner"; - -export default HeroBanner; diff --git a/app/components/sections/HeroBanner/HeroBanner.types.ts b/app/components/sections/HeroBanner/HeroBanner.types.ts new file mode 100644 index 0000000..d536cc8 --- /dev/null +++ b/app/components/sections/HeroBanner/HeroBanner.types.ts @@ -0,0 +1,20 @@ +import type { Category } from "../../cards/Rule"; + +export interface HeroBannerProps { + title?: string; + subtitle?: string; + description?: string; +} + +export interface HeroBannerViewProps { + title: string; + subtitle: string; + description: string; + facilitatorCta: string; + facilitatorHref: string; + createRuleCta: string; + onCreateRule: () => void; + ruleTitle: string; + ruleDescription: string; + ruleCategories: Category[]; +} diff --git a/app/components/sections/HeroBanner/HeroBanner.view.tsx b/app/components/sections/HeroBanner/HeroBanner.view.tsx new file mode 100644 index 0000000..797a6d3 --- /dev/null +++ b/app/components/sections/HeroBanner/HeroBanner.view.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { memo } from "react"; +import Button from "../../buttons/Button"; +import Rule from "../../cards/Rule"; +import ContentLockup from "../../type/ContentLockup"; +import { ASSETS, getAssetPath } from "../../../../lib/assetUtils"; +import HeroDecor from "./HeroDecor"; +import type { HeroBannerViewProps } from "./HeroBanner.types"; + +function HeroBannerView({ + title, + subtitle, + description, + facilitatorCta, + facilitatorHref, + createRuleCta, + onCreateRule, + ruleTitle, + ruleDescription, + ruleCategories, +}: HeroBannerViewProps) { + return ( +
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ + } + /> +
+
+
+
+ ); +} + +HeroBannerView.displayName = "HeroBannerView"; + +export default memo(HeroBannerView); diff --git a/app/components/sections/HeroBanner/index.tsx b/app/components/sections/HeroBanner/index.tsx index 406f808..3491c0b 100644 --- a/app/components/sections/HeroBanner/index.tsx +++ b/app/components/sections/HeroBanner/index.tsx @@ -1 +1,2 @@ -export { default } from "./HeroBanner"; +export { default } from "./HeroBanner.container"; +export type { HeroBannerProps } from "./HeroBanner.types"; diff --git a/app/components/sections/LogoWall/LogoWall.container.tsx b/app/components/sections/LogoWall/LogoWall.container.tsx index c031175..a2350a7 100644 --- a/app/components/sections/LogoWall/LogoWall.container.tsx +++ b/app/components/sections/LogoWall/LogoWall.container.tsx @@ -1,9 +1,8 @@ "use client"; /** - * Figma: "Section / Logo Wall" (18847:22366) - * Index frames: 320–429 2×3 grid, 430–639 3×2 grid, 640+ space-between row. - * Logo heights from those instances (1× / 1.25 at lg / 1.5625 at xl). + * Figma: "Section / Logo Wall" (18483:18425) + * Home uses the three pilot marks (street medics, Food Not Bombs, Mutual Aid Colorado). */ import { memo, useMemo } from "react"; @@ -17,41 +16,20 @@ const LogoWallContainer = memo(({ logos, className = "" }) => { const defaultLogos = useMemo( () => [ + { + src: getAssetPath(partnerLogoPath("boulder-street-medics")), + alt: t("partners.boulderStreetMedics"), + size: "h-[48px] lg:h-[60px] xl:h-[74.6px]", + }, { src: getAssetPath(partnerLogoPath("food-not-bombs")), alt: t("partners.foodNotBombs"), 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-[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-[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-[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-[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-[44.6px] lg:h-[55.8px] xl:h-[69.7px]", - order: "sm:order-3 md:order-6", }, ], [t], @@ -63,11 +41,7 @@ const LogoWallContainer = memo(({ logos, className = "" }) => { ); return ( - + ); }); diff --git a/app/components/sections/LogoWall/LogoWall.types.ts b/app/components/sections/LogoWall/LogoWall.types.ts index fb480e3..5687464 100644 --- a/app/components/sections/LogoWall/LogoWall.types.ts +++ b/app/components/sections/LogoWall/LogoWall.types.ts @@ -5,20 +5,17 @@ export interface LogoWallProps { width?: number; height?: number; size?: string; - order?: string; }>; className?: string; } export interface LogoWallViewProps { - heading: string; displayLogos: Array<{ src: string; alt: string; width?: number; height?: number; size?: string; - order?: string; }>; className: string; } diff --git a/app/components/sections/LogoWall/LogoWall.view.tsx b/app/components/sections/LogoWall/LogoWall.view.tsx index eb09009..2fee9a9 100644 --- a/app/components/sections/LogoWall/LogoWall.view.tsx +++ b/app/components/sections/LogoWall/LogoWall.view.tsx @@ -4,41 +4,31 @@ import { memo } from "react"; import Image from "next/image"; import type { LogoWallViewProps } from "./LogoWall.types"; -function LogoWallView({ - heading, - displayLogos, - className, -}: LogoWallViewProps) { +function LogoWallView({ displayLogos, className }: LogoWallViewProps) { return (
-
-

- {heading} -

-
-
- {displayLogos.map((logo, index) => ( -
- {logo.alt} -
- ))} -
+
+
+ {displayLogos.map((logo, index) => ( +
+ {logo.alt} +
+ ))}
diff --git a/app/components/type/ContentLockup/ContentLockup.container.tsx b/app/components/type/ContentLockup/ContentLockup.container.tsx index 6212390..9fc5092 100644 --- a/app/components/type/ContentLockup/ContentLockup.container.tsx +++ b/app/components/type/ContentLockup/ContentLockup.container.tsx @@ -22,6 +22,7 @@ const ContentLockupContainer = memo( alignment: alignmentProp = "center", titleId, titleContent, + headingLevel: headingLevelProp = "h1", }) => { const variant = variantProp; const alignment = alignmentProp; @@ -179,6 +180,7 @@ const ContentLockupContainer = memo( alignment={alignment} titleId={titleId} titleContent={titleContent} + headingLevel={headingLevelProp} styles={styles} /> ); diff --git a/app/components/type/ContentLockup/ContentLockup.types.ts b/app/components/type/ContentLockup/ContentLockup.types.ts index 6edee3f..953a4e3 100644 --- a/app/components/type/ContentLockup/ContentLockup.types.ts +++ b/app/components/type/ContentLockup/ContentLockup.types.ts @@ -13,6 +13,8 @@ export type ContentLockupVariantValue = export type ContentLockupAlignmentValue = "center" | "left"; +export type ContentLockupHeadingLevel = "h1" | "h2"; + export interface ContentLockupProps { title?: string; subtitle?: string; @@ -37,6 +39,12 @@ export interface ContentLockupProps { titleId?: string; /** Replaces the default title string when inline title markup is required. */ titleContent?: ReactNode; + /** + * Heading tag for the title. FeatureGrid and AskOrganizer use `h2` so each + * page keeps a single `h1`. + * @default "h1" + */ + headingLevel?: ContentLockupHeadingLevel; } export interface VariantStyle { @@ -72,5 +80,6 @@ export interface ContentLockupViewProps { alignment: "center" | "left"; titleId?: string; titleContent?: ReactNode; + headingLevel: ContentLockupHeadingLevel; styles: VariantStyle; } diff --git a/app/components/type/ContentLockup/ContentLockup.view.tsx b/app/components/type/ContentLockup/ContentLockup.view.tsx index c383faa..be9c6f4 100644 --- a/app/components/type/ContentLockup/ContentLockup.view.tsx +++ b/app/components/type/ContentLockup/ContentLockup.view.tsx @@ -19,8 +19,11 @@ function ContentLockupView({ alignment, titleId, titleContent, + headingLevel, styles, }: ContentLockupViewProps) { + const TitleTag = headingLevel; + return (
{variant === "ask" || @@ -43,9 +46,9 @@ function ContentLockupView({ }`} > {title ? ( -

+ {title} -

+ ) : null}
{subtitle ?

{subtitle}

: null} @@ -61,16 +64,16 @@ function ContentLockupView({ {/* Title container */}
{titleContent ? ( -

{titleContent} -

+ ) : title ? ( -

+ {title} -

+ ) : null} {variant === "hero" && ( <> diff --git a/docs/figma-component-registry.md b/docs/figma-component-registry.md index ce80cfc..46652c5 100644 --- a/docs/figma-component-registry.md +++ b/docs/figma-component-registry.md @@ -139,7 +139,7 @@ Inventory aligns with [**CR-104**](https://linear.app/community-rule/issue/CR-10 | Figma / product (typical) | Code (`app/components/sections/`) | Notes | | --- | --- | --- | -| Hero | **`HeroBanner/`** | **`HeroBanner.tsx`** + **`HeroDecor.tsx`** + **`index.tsx`** (presentational package). | +| Hero | **`HeroBanner/`** | **`container` / `view` / `types`** + **`HeroDecor.tsx`** + **`index.tsx`**. | | Content banner | **`ContentBanner.tsx`** | Single module at bucket root. | | Logo wall | **`LogoWall/`** | **`container` / `view` / `types`** + **`index.tsx`**. | | Card steps (SectionCardSteps) | **`CardSteps/`** | Composes **`cards/Step`** (Figma **Card / Step** — not **`progress/Stepper`**). | diff --git a/messages/en/components/featureGrid.json b/messages/en/components/featureGrid.json index d049d2d..84c470d 100644 --- a/messages/en/components/featureGrid.json +++ b/messages/en/components/featureGrid.json @@ -1,6 +1,6 @@ { - "_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", + "_comment": "FeatureGrid component defaults (shared across pages). Explore the patterns goes to /learn; feature cards stay non-interactive until destinations are decided.", + "linkText": "Explore the patterns", "linkHref": "/learn", "ariaLabel": "Feature tools and services", "features": { diff --git a/messages/en/components/header.json b/messages/en/components/header.json index 6f2c3cf..5555233 100644 --- a/messages/en/components/header.json +++ b/messages/en/components/header.json @@ -1,6 +1,6 @@ { "navigation": { - "useCases": "Use cases", + "workshops": "Workshops", "learn": "Learn", "about": "About" }, diff --git a/messages/en/components/heroBanner.json b/messages/en/components/heroBanner.json index db371ef..0d67503 100644 --- a/messages/en/components/heroBanner.json +++ b/messages/en/components/heroBanner.json @@ -1,4 +1,48 @@ { - "_comment": "HeroBanner component defaults (shared across pages)", - "imageAlt": "Hero illustration" + "_comment": "HeroBanner defaults and the Home Rule card. Chip names follow the current deck and may change when redesigned decks are approved.", + "facilitatorCta": "Talk to a facilitator", + "facilitatorHref": "/workshops", + "createRuleCta": "Create rule", + "ruleCard": { + "title": "CommunityRule Creative Team", + "description": "We build and maintain CommunityRule for the communities that use it. It is free for every community that wants to write down how it decides.", + "categories": { + "values": { + "name": "Values", + "chips": [ + "Transparency", + "Forkability", + "Documentation", + "Maintenance", + "Voluntarism" + ] + }, + "communication": { + "name": "Communication", + "chips": [ + "GitHub / GitLab", + "Matrix / Element", + "Email distribution list", + "How we run meetings" + ] + }, + "membership": { + "name": "Membership", + "chips": ["Join by contributing", "Companions"] + }, + "decisionMaking": { + "name": "Decision-making", + "chips": [ + "Consensus", + "Lazy consensus", + "Do-ocracy", + "Changing this Rule" + ] + }, + "conflictManagement": { + "name": "Conflict management", + "chips": ["Conflict ladder", "Hearing panel", "Restorative practices"] + } + } + } } diff --git a/messages/en/components/logoWall.json b/messages/en/components/logoWall.json index 01d634c..b834964 100644 --- a/messages/en/components/logoWall.json +++ b/messages/en/components/logoWall.json @@ -1,11 +1,8 @@ { - "heading": "Trusted by leading cooperators", + "_comment": "Home Logo Wall — three pilot lettered marks, no heading. Medics alt uses Boulder Street Medics until the group confirms its public name.", "partners": { + "boulderStreetMedics": "Boulder Street Medics", "foodNotBombs": "Food Not Bombs", - "metagov": "Metagov", - "mutualAidCo": "Mutual Aid CO", - "openCivics": "Open Civics", - "startCoop": "Start COOP", - "cuBoulder": "CU Boulder" + "mutualAidCo": "Mutual Aid Colorado" } } diff --git a/messages/en/navigation.json b/messages/en/navigation.json index 2cfcc12..523289e 100644 --- a/messages/en/navigation.json +++ b/messages/en/navigation.json @@ -1,6 +1,6 @@ { "_comment": "Navigation items", - "useCases": "Use cases", + "workshops": "Workshops", "learn": "Learn", "about": "About" } diff --git a/messages/en/pages/home.json b/messages/en/pages/home.json index 7e712a9..8d42f87 100644 --- a/messages/en/pages/home.json +++ b/messages/en/pages/home.json @@ -1,48 +1,45 @@ { - "_comment": "Home page specific content", + "_comment": "Home page specific content. Card Steps 1 and 2 follow the Prototype frame and are pending design approval of one set.", "heroBanner": { - "title": "Collaborate", - "subtitle": "with clarity", - "description": "Help your community make important decisions in a way that reflects its unique values.", - "ctaText": "Learn how CommunityRule works", - "ctaHref": "/how-it-works" + "title": "Build structure", + "subtitle": "before you need it", + "description": "Strong communities agree on four things early. They settle how they talk, who belongs, how they decide and what happens when they disagree. Write yours down together." }, "cardSteps": { - "title": "How CommunityRule works", + "title": "How CommunityRule helps", "headingDesktopLine1": "How", "headingDesktopLine2": "CommunityRule", "headingDesktopLine3": "helps", "subtitle": "Here's a quick overview of the process, from start to finish.", "cards": { "card1": { - "text": "Document how your community makes decisions", - "_comment": "First step card" + "text": "See what has worked for other communities", + "_comment": "Pending design approval versus Section / Card Steps" }, "card2": { - "text": "Build an operating manual for a successful community", - "_comment": "Second step card" + "text": "Decide how you will decide, while things are calm", + "_comment": "Pending design approval versus Section / Card Steps" }, "card3": { - "text": "Get a link to your manual for your group to review and evolve", - "_comment": "Third step card" + "text": "Invite your members and change it together as you grow" } } }, "featureGrid": { "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.", + "subtitle": "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", "features": { "decisionMaking": { "title": "Choose how you decide", - "description": "Compare consensus, do-ocracy, and other patterns, then commit to one." + "description": "Compare consensus, do-ocracy and other patterns, then commit to one." }, "valuesAlignment": { "title": "Name your values", - "description": "Short exercises that surface what your group agrees on before it's tested." + "description": "Short exercises that surface what your community agrees on before it's tested." }, "membershipGuidance": { "title": "Define membership", - "description": "Spell out how people join, take on roles, and step away." + "description": "Spell out how people join, take on roles and step away." }, "conflictResolution": { "title": "Plan for conflict", @@ -56,9 +53,8 @@ "buttonText": "Ask an organizer" }, "ruleStack": { - "title": "Popular templates", - "subtitle": "These are popular patterns for making decisions in mutual aid and open source communities. You can use them as they are or as a starting place for customizing your own CommunityRule.", - "subtitleLg": "These are popular patterns for making decisions in communities with egalitarian values. You can use them as they are or as a starting place for customizing your own CommunityRule.", + "title": "Get templates that help your community run smoothly", + "subtitle": "Ways other communities make decisions. Use one as it is or change it to fit yours.", "cards": { "consensusClusters": { "title": "Consensus clusters", diff --git a/public/assets/logos/partners/boulder-street-medics.svg b/public/assets/logos/partners/boulder-street-medics.svg new file mode 100644 index 0000000..85ad605 --- /dev/null +++ b/public/assets/logos/partners/boulder-street-medics.svg @@ -0,0 +1,56 @@ + + + diff --git a/stories/sections/CardSteps.stories.js b/stories/sections/CardSteps.stories.js index ce43129..d332a97 100644 --- a/stories/sections/CardSteps.stories.js +++ b/stories/sections/CardSteps.stories.js @@ -32,22 +32,22 @@ export default { export const Default = { args: { - title: "How CommunityRule works", + title: "How CommunityRule helps", subtitle: "Here's a quick overview of the process, from start to finish.", headingDesktopLines: ["How", "CommunityRule", "helps"], steps: [ { - text: "Document how your community makes decisions", + text: "See what has worked for other communities", iconShape: "blob", iconColor: "green", }, { - text: "Build an operating manual for a successful community", + text: "Decide how you will decide, while things are calm", iconShape: "gear", iconColor: "purple", }, { - text: "Get a link to your manual for your group to review and evolve", + text: "Invite your members and change it together as you grow", iconShape: "star", iconColor: "orange", }, @@ -66,7 +66,7 @@ export const CustomContent = { iconColor: "green", }, { - text: "Collaborate with our team to create the perfect solution", + text: "Build structure with our team to create the perfect solution", iconShape: "gear", iconColor: "purple", }, diff --git a/stories/sections/FeatureGrid.stories.js b/stories/sections/FeatureGrid.stories.js index 3effe47..7f9093d 100644 --- a/stories/sections/FeatureGrid.stories.js +++ b/stories/sections/FeatureGrid.stories.js @@ -38,7 +38,7 @@ export const Default = { args: { 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.", + "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", }, parameters: { docs: { diff --git a/stories/sections/HeroBanner.stories.js b/stories/sections/HeroBanner.stories.js index d6362e5..67a0618 100644 --- a/stories/sections/HeroBanner.stories.js +++ b/stories/sections/HeroBanner.stories.js @@ -8,7 +8,7 @@ export default { docs: { description: { component: - "A responsive hero banner component that showcases the Community Rule branding and messaging. Adapts across multiple breakpoints with proper spacing, typography, and interactive elements. Includes background decorations and product demo integration.", + "Home hero: ContentLockup plus the team's Rule card. Below lg the CTA is Talk to a facilitator; from lg it is Create rule.", }, }, }, @@ -25,32 +25,22 @@ export default { control: { type: "text" }, description: "The description text", }, - ctaText: { - control: { type: "text" }, - description: "The call-to-action button text", - }, - ctaHref: { - control: { type: "text" }, - description: "The call-to-action button link", - }, }, tags: ["autodocs"], }; export const Default = { args: { - title: "Collaborate", - subtitle: "with clarity", + title: "Build structure", + subtitle: "before you need it", description: - "Help your community make important decisions in a way that reflects its unique values.", - ctaText: "Learn how Community Rule works", - ctaHref: "#", + "Strong communities agree on four things early. They settle how they talk, who belongs, how they decide and what happens when they disagree. Write yours down together.", }, parameters: { docs: { description: { story: - "Default hero banner with standard Community Rule messaging and branding.", + "Default home hero with the team Rule card and breakpoint-specific CTAs.", }, }, }, diff --git a/stories/sections/LogoWall.stories.js b/stories/sections/LogoWall.stories.js index 1173f07..f8de374 100644 --- a/stories/sections/LogoWall.stories.js +++ b/stories/sections/LogoWall.stories.js @@ -7,49 +7,17 @@ export default { layout: "fullscreen", docs: { description: { - component: `A responsive logo wall component that displays partner/sponsor logos in a grid layout. Features responsive breakpoints with different layouts and sizing for mobile, tablet, and desktop views. + component: `Home Logo Wall: three pilot marks (Boulder Street Medics, Food Not Bombs, Mutual Aid Colorado) with no heading. ## Responsive Behavior -- **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 - -- **Opacity**: Partner logos render at 60% opacity to match Figma \`Section / Logo Wall\`. +- **Mobile**: stacked column +- **SM+**: wrapping row +- **MD+**: single space-between row ## Props -- **logos** (optional): Array of logo objects with src, alt, and size properties. If not provided, uses default partner logos. - -## Usage Examples - -### Custom Logos -\`\`\`jsx - -\`\`\` - -### Empty State -\`\`\`jsx - -\`\`\` -This will fall back to the default partner logos.`, +- **logos** (optional): Array of logo objects with src, alt, and size properties. If not provided, uses the three pilot marks.`, }, }, }, @@ -58,7 +26,7 @@ This will fall back to the default partner logos.`, logos: { control: "object", description: - "Array of logo objects with src, alt, and size properties. If not provided, uses default partner logos.", + "Array of logo objects with src, alt, and size properties. If not provided, uses the three pilot marks.", }, }, }; @@ -69,7 +37,7 @@ export const Default = { docs: { description: { story: - "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.", + "Default LogoWall with the three pilot marks at Figma 60% opacity.", }, }, }, diff --git a/tests/components/AskOrganizer.test.tsx b/tests/components/AskOrganizer.test.tsx index 797ab97..3312fb3 100644 --- a/tests/components/AskOrganizer.test.tsx +++ b/tests/components/AskOrganizer.test.tsx @@ -40,9 +40,9 @@ componentTestSuite(config); describe("AskOrganizer (behavioral tests)", () => { it("renders title", () => { render(); - expect( - screen.getByRole("heading", { name: "Test Title" }), - ).toBeInTheDocument(); + const title = screen.getByRole("heading", { name: "Test Title" }); + expect(title).toBeInTheDocument(); + expect(title.tagName).toBe("H2"); }); it("renders subtitle when provided", () => { diff --git a/tests/components/FeatureGrid.test.tsx b/tests/components/FeatureGrid.test.tsx index 8bd50e9..1d5f03c 100644 --- a/tests/components/FeatureGrid.test.tsx +++ b/tests/components/FeatureGrid.test.tsx @@ -38,9 +38,9 @@ componentTestSuite(config); describe("FeatureGrid (behavioral tests)", () => { it("renders title and subtitle", () => { render(); - expect( - screen.getByRole("heading", { name: "Test Title" }), - ).toBeInTheDocument(); + const title = screen.getByRole("heading", { name: "Test Title" }); + expect(title).toBeInTheDocument(); + expect(title.tagName).toBe("H2"); expect(screen.getByText("Test Subtitle")).toBeInTheDocument(); expect( screen.queryByRole("heading", { name: "Test Subtitle" }), @@ -58,10 +58,10 @@ describe("FeatureGrid (behavioral tests)", () => { ).not.toBeInTheDocument(); }); - it("sends Explore the toolkit to the Learn page", () => { + it("sends Explore the patterns to the Learn page", () => { render(); expect( - screen.getByRole("link", { name: "Explore the toolkit" }), + screen.getByRole("link", { name: "Explore the patterns" }), ).toHaveAttribute("href", "/learn"); }); diff --git a/tests/components/HeroBanner.test.tsx b/tests/components/HeroBanner.test.tsx index 9db02b5..08bef00 100644 --- a/tests/components/HeroBanner.test.tsx +++ b/tests/components/HeroBanner.test.tsx @@ -20,8 +20,6 @@ const config: ComponentTestSuiteConfig = { optionalProps: { subtitle: "Subtitle", description: "Description", - ctaText: "CTA", - ctaHref: "/link", }, primaryRole: "region", testCases: { @@ -50,18 +48,29 @@ describe("HeroBanner (behavioral tests)", () => { ).toBeInTheDocument(); }); - it("renders hero image", () => { + it("renders the team Rule card instead of a hero photograph", () => { render(); expect( - screen.getByRole("img", { name: "Hero illustration" }), + screen.getByRole("heading", { name: "CommunityRule Creative Team" }), ).toBeInTheDocument(); + expect( + screen.queryByRole("img", { name: "Hero illustration" }), + ).not.toBeInTheDocument(); }); - it("renders CTA with the inverse (black) filled palette", () => { - render(); - const cta = screen.getAllByRole("link", { name: "Get Started" })[0]; + it("sends Talk to a facilitator to the workshops page", () => { + render(); + const cta = screen.getAllByRole("link", { name: "Talk to a facilitator" })[0]; + expect(cta).toHaveAttribute("href", "/workshops"); expect(cta?.className).toContain( "bg-[var(--color-surface-default-primary)]", ); }); + + it("renders Create rule as a button for large screens", () => { + render(); + expect( + screen.getAllByRole("button", { name: "Create rule" }).length, + ).toBeGreaterThan(0); + }); }); diff --git a/tests/components/Logo.test.tsx b/tests/components/Logo.test.tsx index 8d7e9a8..fa32283 100644 --- a/tests/components/Logo.test.tsx +++ b/tests/components/Logo.test.tsx @@ -79,10 +79,18 @@ describe("Logo (behavioral tests)", () => { expect(row?.className).toContain("h-[var(--space-1000)]"); }); - it("shows the folder-top wordmark without a breakpoint hide", () => { + it("hides the folder-top wordmark below md", () => { render(); const wordmark = screen.getByText("CommunityRule"); - expect(wordmark.className).not.toMatch(/\bhidden\b/); + expect(wordmark.className).toMatch(/\bhidden\b/); + expect(wordmark.className).toContain("md:block"); + }); + + it("hides the standard header wordmark below sm", () => { + render(); + const wordmark = screen.getByText("CommunityRule"); + expect(wordmark.className).toContain("hidden"); + expect(wordmark.className).toContain("sm:block"); }); it("renders with different size variants", () => { diff --git a/tests/components/Top.test.tsx b/tests/components/Top.test.tsx index 2e5734a..4861618 100644 --- a/tests/components/Top.test.tsx +++ b/tests/components/Top.test.tsx @@ -131,7 +131,7 @@ describe("Top header chrome", () => { ); expect( screen.getAllByRole("menuitem", { - name: /navigate to use cases page/i, + name: /navigate to workshops page/i, hidden: true, }), ).toHaveLength(1); @@ -146,7 +146,7 @@ describe("Top header chrome", () => { name: /go to your profile/i, hidden: true, }), - ).toHaveLength(1); + ).toHaveLength(2); expect( screen.getAllByRole("button", { name: /create a new rule/i, @@ -157,11 +157,18 @@ describe("Top header chrome", () => { } }); + it("points Workshops at /workshops", () => { + renderWithProviders(); + expect( + screen.getByRole("menuitem", { name: /navigate to workshops page/i }), + ).toHaveAttribute("href", "/workshops"); + }); + it("shows Profile instead of Log in when signed in", () => { renderWithProviders(); expect( - screen.getByRole("menuitem", { name: /go to your profile/i }), - ).toBeInTheDocument(); + screen.getAllByRole("menuitem", { name: /go to your profile/i }).length, + ).toBeGreaterThan(0); expect( screen.queryByRole("button", { name: /log in to your account/i }), ).not.toBeInTheDocument(); @@ -174,6 +181,7 @@ describe("Top header chrome", () => { const headerNav = document.querySelector("header nav"); expect(logo?.className).not.toMatch(/\bz-20\b/); expect(nav?.className).not.toMatch(/\babsolute\b/); + expect(headerNav?.className).toContain("flex"); expect(headerNav?.className).toContain( "md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]", ); @@ -183,9 +191,8 @@ describe("Top header chrome", () => { renderWithProviders(); const tab = document.querySelector(".HeaderTab"); const nav = document.querySelector("[data-top='nav']"); - expect(tab?.className).toContain("w-fit"); - expect(tab?.className).toContain("sm:w-auto"); - expect(tab?.className).toContain("sm:flex-1"); + expect(tab?.className).toContain("flex-1"); + expect(tab?.className).toContain("justify-between"); expect(nav?.className).toContain("md:absolute"); expect(nav?.className).toContain( "md:left-[calc(50vw-var(--space-400))]", @@ -193,16 +200,17 @@ describe("Top header chrome", () => { expect(nav?.className).not.toContain("ml-auto"); }); - it("keeps a single home nav cluster and shows the folder wordmark", () => { + it("keeps a single home nav cluster and hides the folder wordmark below md", () => { renderWithProviders(); expect( screen.getAllByRole("menuitem", { - name: /navigate to use cases page/i, + name: /navigate to workshops page/i, hidden: true, }), ).toHaveLength(1); const wordmark = screen.getByText("CommunityRule"); - expect(wordmark.className).not.toMatch(/\bhidden\b/); + expect(wordmark.className).toMatch(/\bhidden\b/); + expect(wordmark.className).toContain("md:block"); expect( screen.getByRole("button", { name: /create a new rule/i }).className, ).toContain("md:text-x-small-label"); diff --git a/tests/components/TopWithPathname.test.tsx b/tests/components/TopWithPathname.test.tsx index bb88a2d..132d680 100644 --- a/tests/components/TopWithPathname.test.tsx +++ b/tests/components/TopWithPathname.test.tsx @@ -25,8 +25,8 @@ describe("TopWithPathname", () => { it("renders Profile from the server session before /api/auth/session resolves", () => { renderWithProviders(); expect( - screen.getByRole("menuitem", { name: /go to your profile/i }), - ).toBeInTheDocument(); + screen.getAllByRole("menuitem", { name: /go to your profile/i }).length, + ).toBeGreaterThan(0); expect( screen.queryByRole("button", { name: /log in to your account/i }), ).not.toBeInTheDocument(); diff --git a/tests/components/type/ContentLockup.test.tsx b/tests/components/type/ContentLockup.test.tsx index d99ecb9..d19919f 100644 --- a/tests/components/type/ContentLockup.test.tsx +++ b/tests/components/type/ContentLockup.test.tsx @@ -83,4 +83,27 @@ describe("ContentLockup", () => { expect(link.className).not.toMatch(/focus:ring-2/); expect(link.className).toMatch(/focus-visible:outline/); }); + + it("renders the title as h2 when headingLevel is h2", () => { + renderWithProviders( + , + ); + + expect( + screen.getByRole("heading", { + level: 2, + name: "Tools for the parts that get hard", + }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("heading", { + level: 1, + name: "Tools for the parts that get hard", + }), + ).not.toBeInTheDocument(); + }); }); diff --git a/tests/e2e/critical-journeys.spec.ts b/tests/e2e/critical-journeys.spec.ts index d41d8e4..534fe42 100644 --- a/tests/e2e/critical-journeys.spec.ts +++ b/tests/e2e/critical-journeys.spec.ts @@ -7,40 +7,40 @@ test.describe("Critical User Journeys", () => { test("complete user journey: learn about CommunityRule", async ({ page }) => { // 1. User lands on homepage - await expect(page.locator("text=Collaborate")).toBeVisible(); + await expect(page.locator("text=Build structure")).toBeVisible(); // 2. User reads hero section await expect( - page.locator("text=Help your community make important decisions"), + page.locator("text=Strong communities agree on four things early"), ).toBeVisible(); // 3. User clicks CTA to learn more const learnCta = page - .getByRole("link", { name: /Learn how CommunityRule works/i }) - .or(page.getByRole("button", { name: /Learn how CommunityRule works/i })) + .getByRole("link", { name: /Talk to a facilitator/i }) + .or(page.getByRole("button", { name: /Talk to a facilitator/i })) .first(); if ((await learnCta.count()) > 0 && (await learnCta.isVisible())) { await learnCta.click(); - await expect(page).toHaveURL(/\/how-it-works/); + await expect(page).toHaveURL(/\/workshops/); } // 4. User scrolls to CardSteps section (home) // Note: mobile title is one line; lg+ uses stacked "How / CommunityRule / helps" from home copy const howItWorksHeading = page.locator( - 'h2:has-text("How CommunityRule works"), h2:has-text("How"), h2:has-text("CommunityRule"), h2:has-text("helps")', + 'h2:has-text("How CommunityRule helps"), h2:has-text("How"), h2:has-text("CommunityRule"), h2:has-text("helps")', ); await expect(howItWorksHeading).toBeVisible(); // 5. User reads the process steps await expect( - page.locator("text=Document how your community makes decisions"), + page.locator("text=See what has worked for other communities"), ).toBeVisible(); await expect( - page.locator("text=Build an operating manual for a successful community"), + page.locator("text=Decide how you will decide, while things are calm"), ).toBeVisible(); await expect( page.locator( - "text=Get a link to your manual for your group to review and evolve", + "text=Invite your members and change it together as you grow", ), ).toBeVisible(); @@ -87,20 +87,22 @@ test.describe("Critical User Journeys", () => { // Check main sections are present await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), + page.locator("h1, h2").filter({ hasText: "Build structure" }), ).toBeVisible(); const howItWorksHeading = page.locator( - 'h2:has-text("How CommunityRule works"), h2:has-text("How CommunityRule helps")', + 'h2:has-text("How CommunityRule helps"), h2:has-text("How CommunityRule helps")', ); await expect(howItWorksHeading).toBeVisible(); await expect( - page.locator("h1").filter({ hasText: "Tools for the parts that get hard" }), + page.locator("h2").filter({ hasText: "Tools for the parts that get hard" }), ).toBeVisible(); // Check key components are rendered - await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible(); + await expect( + page.getByRole("heading", { name: "CommunityRule Creative Team" }), + ).toBeVisible(); await expect(page.locator('img[alt="Food Not Bombs"]')).toBeVisible(); await expect(page.locator("text=Jo Freeman")).toBeVisible(); }); @@ -108,11 +110,11 @@ test.describe("Critical User Journeys", () => { test("feature grid section functionality", async ({ page }) => { // Check section header await expect( - page.locator('h1:has-text("Tools for the parts that get hard")'), + page.locator('h2:has-text("Tools for the parts that get hard")'), ).toBeVisible(); await expect( page.locator( - "text=Every group runs into the same knots. CommunityRule gives you a way through", + "text=Every community runs into the same knots. CommunityRule gives you a way through", ), ).toBeVisible(); @@ -127,12 +129,12 @@ test.describe("Critical User Journeys", () => { featureSection.locator("text=Plan for conflict"), ).toBeVisible(); - // Feature tiles are presentational — only the lockup "Explore the toolkit" is a link + // Feature tiles are presentational — only the lockup "Explore the patterns" is a link await expect( featureSection.locator('a[href="#decision-making"]'), ).toHaveCount(0); await expect( - featureSection.getByRole("link", { name: "Explore the toolkit" }), + featureSection.getByRole("link", { name: "Explore the patterns" }), ).toHaveAttribute("href", "/learn"); }); @@ -186,19 +188,19 @@ test.describe("Critical User Journeys", () => { // Test mobile viewport await page.setViewportSize({ width: 375, height: 667 }); await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), + page.locator("h1, h2").filter({ hasText: "Build structure" }), ).toBeVisible(); // Test tablet viewport await page.setViewportSize({ width: 768, height: 1024 }); await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), + page.locator("h1, h2").filter({ hasText: "Build structure" }), ).toBeVisible(); // Test desktop viewport await page.setViewportSize({ width: 1440, height: 900 }); await expect( - page.locator("h1, h2").filter({ hasText: "Collaborate" }), + page.locator("h1, h2").filter({ hasText: "Build structure" }), ).toBeVisible(); }); }); diff --git a/tests/e2e/edge-cases.spec.ts b/tests/e2e/edge-cases.spec.ts index 2e30098..37b4b4e 100644 --- a/tests/e2e/edge-cases.spec.ts +++ b/tests/e2e/edge-cases.spec.ts @@ -18,7 +18,7 @@ test.describe("Edge Cases and Error Scenarios", () => { await page.goto("/", { waitUntil: "domcontentloaded", timeout: 10000 }); // Page should still load eventually - await expect(page.locator("text=Collaborate")).toBeVisible({ + await expect(page.locator("text=Build structure")).toBeVisible({ timeout: 10000, }); }); @@ -38,7 +38,7 @@ test.describe("Edge Cases and Error Scenarios", () => { await expect(body).toBeVisible(); // Verify key content is still accessible - await expect(page.locator("text=Collaborate")).toBeVisible(); + await expect(page.locator("text=Build structure")).toBeVisible(); }); test("handles JavaScript errors gracefully", async ({ page }) => { @@ -59,30 +59,19 @@ test.describe("Edge Cases and Error Scenarios", () => { }); // Page should continue to function - await expect(page.locator("text=Collaborate")).toBeVisible(); + await expect(page.locator("text=Build structure")).toBeVisible(); - const learnCta = page - .getByRole("link", { name: /Learn how CommunityRule works/i }) - .or(page.getByRole("button", { name: /Learn how CommunityRule works/i })); - const ctaCount = await learnCta.count(); - let visibleCta = null; - - for (let i = 0; i < ctaCount; i++) { - const cta = learnCta.nth(i); - if (await cta.isVisible()) { - visibleCta = cta; - break; - } + const createCta = page.getByRole("button", { name: "Create rule" }); + const facilitatorCta = page.getByRole("link", { + name: /Talk to a facilitator/i, + }); + const visibleCreate = createCta.filter({ has: page.locator(":visible") }); + if ((await visibleCreate.count()) > 0) { + await visibleCreate.first().click(); + await expect(page).toHaveURL(/\/create/); + } else { + await expect(facilitatorCta.first()).toBeVisible(); } - - if (!visibleCta) { - throw new Error( - 'No visible "Learn how CommunityRule works" CTA found', - ); - } - - await visibleCta.click(); - await expect(page).toHaveURL(/\/how-it-works/); }); test("handles missing images gracefully", async ({ page }) => { @@ -96,29 +85,9 @@ test.describe("Edge Cases and Error Scenarios", () => { await page.goto("/", { waitUntil: "domcontentloaded" }); // Page should still function without images - await expect(page.locator("text=Collaborate")).toBeVisible(); - - const learnCta = page - .getByRole("link", { name: /Learn how CommunityRule works/i }) - .or(page.getByRole("button", { name: /Learn how CommunityRule works/i })); - const ctaCount = await learnCta.count(); - let visibleCta = null; - - for (let i = 0; i < ctaCount; i++) { - const cta = learnCta.nth(i); - if (await cta.isVisible()) { - visibleCta = cta; - break; - } - } - - if (!visibleCta) { - throw new Error( - 'No visible "Learn how CommunityRule works" CTA found', - ); - } - - await visibleCta.click(); - await expect(page).toHaveURL(/\/how-it-works/); + await expect(page.locator("text=Build structure")).toBeVisible(); + await expect( + page.getByRole("heading", { name: "CommunityRule Creative Team" }), + ).toBeVisible(); }); }); diff --git a/tests/pages/home.test.jsx b/tests/pages/home.test.jsx index 5b1756e..86a31d2 100644 --- a/tests/pages/home.test.jsx +++ b/tests/pages/home.test.jsx @@ -11,11 +11,11 @@ describe("Page", () => { render(); // Check that all main sections are rendered (using getAllByText since there are multiple instances) - expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); - expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0); + expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0); + expect(screen.getAllByText("before you need it").length).toBeGreaterThan(0); expect( screen.getAllByText( - "Help your community make important decisions in a way that reflects its unique values.", + "Strong communities agree on four things early. They settle how they talk, who belongs, how they decide and what happens when they disagree. Write yours down together.", ).length, ).toBeGreaterThan(0); @@ -23,7 +23,7 @@ describe("Page", () => { // Check CardSteps section (using getAllByText since there are multiple instances) await waitFor(() => { expect( - screen.getAllByText("How CommunityRule works").length, + screen.getAllByText("How CommunityRule helps").length, ).toBeGreaterThan(0); }); expect( @@ -41,7 +41,7 @@ describe("Page", () => { }); expect( screen.getAllByText( - "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", + "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", ).length, ).toBeGreaterThan(0); @@ -58,20 +58,20 @@ describe("Page", () => { render(); // Check hero banner content (using getAllByText since there are multiple instances) - expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); - expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0); + expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0); + expect(screen.getAllByText("before you need it").length).toBeGreaterThan(0); expect( screen.getAllByText( - "Help your community make important decisions in a way that reflects its unique values.", + "Strong communities agree on four things early. They settle how they talk, who belongs, how they decide and what happens when they disagree. Write yours down together.", ).length, ).toBeGreaterThan(0); expect( - screen.getAllByText("Learn how CommunityRule works").length, + screen.getAllByText("Talk to a facilitator").length, ).toBeGreaterThan(0); const learnLinks = screen.getAllByRole("link", { - name: "Learn how CommunityRule works", + name: "Talk to a facilitator", }); - expect(learnLinks[0]).toHaveAttribute("href", "/how-it-works"); + expect(learnLinks[0]).toHaveAttribute("href", "/workshops"); }); test("renders CardSteps section with correct data", async () => { @@ -80,7 +80,7 @@ describe("Page", () => { // Wait for dynamically imported CardSteps component to load await waitFor(() => { expect( - screen.getAllByText("How CommunityRule works").length, + screen.getAllByText("How CommunityRule helps").length, ).toBeGreaterThan(0); }); expect( @@ -91,16 +91,16 @@ describe("Page", () => { // Check individual card content (using getAllByText since there are multiple instances) expect( - screen.getAllByText("Document how your community makes decisions").length, + screen.getAllByText("See what has worked for other communities").length, ).toBeGreaterThan(0); expect( screen.getAllByText( - "Build an operating manual for a successful community", + "Decide how you will decide, while things are calm", ).length, ).toBeGreaterThan(0); expect( screen.getAllByText( - "Get a link to your manual for your group to review and evolve", + "Invite your members and change it together as you grow", ).length, ).toBeGreaterThan(0); }); @@ -117,11 +117,11 @@ describe("Page", () => { }); expect( screen.getAllByText( - "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", + "Every community 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: "Explore the toolkit", + name: "Explore the patterns", }); expect(learnMore[0]).toHaveAttribute("href", "/learn"); }); @@ -144,14 +144,14 @@ describe("Page", () => { // Check that all major components are present by looking for their content // HeroBanner - expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); + expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0); // Wait for dynamically imported components to load // LogoWall - should be present (even if just the component structure) // CardSteps await waitFor(() => { expect( - screen.getAllByText("How CommunityRule works").length, + screen.getAllByText("How CommunityRule helps").length, ).toBeGreaterThan(0); }); @@ -174,7 +174,7 @@ describe("Page", () => { test("has correct page structure", () => { render(); - const mainContainer = screen.getAllByText("Collaborate")[0].closest("div"); + const mainContainer = screen.getAllByText("Build structure")[0].closest("div"); expect(mainContainer).toBeInTheDocument(); }); @@ -183,7 +183,7 @@ describe("Page", () => { // Check CTA button in hero banner expect( - screen.getAllByText("Learn how CommunityRule works").length, + screen.getAllByText("Talk to a facilitator").length, ).toBeGreaterThan(0); // Check CTA button in ask organizer section @@ -196,7 +196,7 @@ describe("Page", () => { // Check main description (using getAllByText since there are multiple instances) expect( screen.getAllByText( - "Help your community make important decisions in a way that reflects its unique values.", + "Strong communities agree on four things early. They settle how they talk, who belongs, how they decide and what happens when they disagree. Write yours down together.", ).length, ).toBeGreaterThan(0); @@ -213,7 +213,7 @@ describe("Page", () => { await waitFor(() => { expect( screen.getAllByText( - "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", + "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", ).length, ).toBeGreaterThan(0); }); @@ -228,12 +228,12 @@ describe("Page", () => { render(); // Check all section titles (using getAllByText since there are multiple instances) - expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); + expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0); // Wait for dynamically imported components await waitFor(() => { expect( - screen.getAllByText("How CommunityRule works").length, + screen.getAllByText("How CommunityRule helps").length, ).toBeGreaterThan(0); }); await waitFor(() => { @@ -254,18 +254,18 @@ describe("Page", () => { await waitFor(() => { // Three Step tiles inside CardSteps expect( - screen.getAllByText("Document how your community makes decisions") + screen.getAllByText("See what has worked for other communities") .length, ).toBeGreaterThan(0); }); expect( screen.getAllByText( - "Build an operating manual for a successful community", + "Decide how you will decide, while things are calm", ).length, ).toBeGreaterThan(0); expect( screen.getAllByText( - "Get a link to your manual for your group to review and evolve", + "Invite your members and change it together as you grow", ).length, ).toBeGreaterThan(0); }); @@ -274,7 +274,7 @@ describe("Page", () => { render(); // Check subtitles (using getAllByText since there are multiple instances) - expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0); + expect(screen.getAllByText("before you need it").length).toBeGreaterThan(0); // Wait for dynamically imported components await waitFor(() => { @@ -287,7 +287,7 @@ describe("Page", () => { await waitFor(() => { expect( screen.getAllByText( - "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", + "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", ).length, ).toBeGreaterThan(0); }); diff --git a/tests/pages/page-flow.test.jsx b/tests/pages/page-flow.test.jsx index 21c959b..7624696 100644 --- a/tests/pages/page-flow.test.jsx +++ b/tests/pages/page-flow.test.jsx @@ -28,33 +28,31 @@ describe("Page Flow Integration", () => { // Hero Banner section expect( - screen.getByRole("heading", { name: "Collaborate" }), + screen.getByRole("heading", { name: "Build structure" }), ).toBeInTheDocument(); expect( - screen.getByRole("heading", { name: "with clarity" }), + screen.getByRole("heading", { name: "before you need it" }), ).toBeInTheDocument(); expect( screen.getByText( - "Help your community make important decisions in a way that reflects its unique values.", + "Strong communities agree on four things early. They settle how they talk, who belongs, how they decide and what happens when they disagree. Write yours down together.", ), ).toBeInTheDocument(); const ctaButtons = screen.getAllByRole("link", { - name: "Learn how CommunityRule works", + name: "Talk to a facilitator", }); expect(ctaButtons.length).toBeGreaterThan(0); await waitFor(() => { expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); }); - expect(screen.getByAltText("Start COOP")).toBeInTheDocument(); - expect(screen.getByAltText("Metagov")).toBeInTheDocument(); - expect(screen.getByAltText("Open Civics")).toBeInTheDocument(); - expect(screen.getByAltText("Mutual Aid CO")).toBeInTheDocument(); - expect(screen.getByAltText("CU Boulder")).toBeInTheDocument(); + expect(screen.getByAltText("Boulder Street Medics")).toBeInTheDocument(); + expect(screen.getByAltText("Mutual Aid Colorado")).toBeInTheDocument(); + expect(screen.queryByAltText("Start COOP")).not.toBeInTheDocument(); await waitFor(() => { expect( - screen.getByRole("heading", { name: /How CommunityRule works/ }), + screen.getByRole("heading", { name: /How CommunityRule helps/ }), ).toBeInTheDocument(); }); expect( @@ -63,25 +61,27 @@ describe("Page Flow Integration", () => { ), ).toBeInTheDocument(); expect( - screen.getByText("Document how your community makes decisions"), + screen.getByText("See what has worked for other communities"), ).toBeInTheDocument(); expect( - screen.getByText("Build an operating manual for a successful community"), + screen.getByText("Decide how you will decide, while things are calm"), ).toBeInTheDocument(); expect( screen.getByText( - "Get a link to your manual for your group to review and evolve", + "Invite your members and change it together as you grow", ), ).toBeInTheDocument(); await waitFor(() => { - expect(screen.getByRole("heading", { name: "Consensus" })).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: "Devolution" }), + ).toBeInTheDocument(); }); expect( - screen.getByRole("heading", { name: "Do-ocracy" }), + screen.getByRole("heading", { name: "Consensus" }), ).toBeInTheDocument(); expect( - screen.getByRole("heading", { name: "Devolution" }), + screen.getByRole("heading", { name: "Do-ocracy" }), ).toBeInTheDocument(); expect( screen.getByRole("heading", { name: "Quadratic Governance" }), @@ -96,7 +96,7 @@ describe("Page Flow Integration", () => { }); expect( screen.getByText( - "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", + "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", ), ).toBeInTheDocument(); @@ -120,11 +120,11 @@ describe("Page Flow Integration", () => { renderPage(); const ctaLinks = screen.getAllByRole("link", { - name: "Learn how CommunityRule works", + name: "Talk to a facilitator", }); const ctaLink = ctaLinks[0]; expect(ctaLink).toBeInTheDocument(); - expect(ctaLink).toHaveAttribute("href", "/how-it-works"); + expect(ctaLink).toHaveAttribute("href", "/workshops"); await user.click(ctaLink); expect(ctaLink).toBeInTheDocument(); @@ -135,13 +135,13 @@ describe("Page Flow Integration", () => { await waitFor(() => { const cards = screen.getAllByText( - /Document how your community|Build an operating manual|Get a link to your manual/, + /See what has worked for other communities|Decide how you will decide|Invite your members and change it together/, ); expect(cards.length).toBeGreaterThan(0); }); const cards = screen.getAllByText( - /Document how your community|Build an operating manual|Get a link to your manual/, + /See what has worked for other communities|Decide how you will decide|Invite your members and change it together/, ); expect(cards.length).toBeGreaterThan(0); @@ -153,11 +153,11 @@ describe("Page Flow Integration", () => { renderPage(); await waitFor(() => { - expect(screen.getByText("Consensus")).toBeInTheDocument(); + expect(screen.getByText("Devolution")).toBeInTheDocument(); }); expect(screen.queryByText("Solidarity Network")).not.toBeInTheDocument(); - expect(screen.getByText("Do-ocracy")).toBeInTheDocument(); - expect(screen.getByText("Devolution")).toBeInTheDocument(); + expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0); + expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0); expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); const seeAll = screen.getByRole("link", { name: "See all templates" }); @@ -189,7 +189,7 @@ describe("Page Flow Integration", () => { expect(headings.length).toBeGreaterThan(4); const mainContent = screen.getByText( - /Help your community make important decisions/, + /Strong communities agree on four things early/, ); expect(mainContent).toBeInTheDocument(); }); @@ -220,15 +220,15 @@ describe("Page Flow Integration", () => { renderPage(); expect( - screen.getByText(/Help your community make important decisions/), + screen.getByText(/Strong communities agree on four things early/), ).toBeInTheDocument(); await waitFor(() => { - expect(screen.getByText("How CommunityRule works")).toBeInTheDocument(); + expect(screen.getByText("How CommunityRule helps")).toBeInTheDocument(); }); await waitFor(() => { - expect(screen.getByText("Consensus")).toBeInTheDocument(); + expect(screen.getByText("Devolution")).toBeInTheDocument(); }); await waitFor(() => { diff --git a/tests/pages/user-journey.test.jsx b/tests/pages/user-journey.test.jsx index 886b5ce..91f2e31 100644 --- a/tests/pages/user-journey.test.jsx +++ b/tests/pages/user-journey.test.jsx @@ -38,16 +38,16 @@ describe("User Journey Integration", () => { renderPageWithFooter(); expect( - screen.getByText(/Help your community make important decisions/), + screen.getByText(/Strong communities agree on four things early/), ).toBeInTheDocument(); const learnLinks = screen.getAllByRole("link", { - name: "Learn how CommunityRule works", + name: "Talk to a facilitator", }); await user.click(learnLinks[0]); await waitFor(() => { - expect(screen.getByText("How CommunityRule works")).toBeInTheDocument(); + expect(screen.getByText("How CommunityRule helps")).toBeInTheDocument(); }); }); @@ -56,10 +56,10 @@ describe("User Journey Integration", () => { renderPage(); await waitFor(() => { - expect(screen.getByText("Consensus")).toBeInTheDocument(); + expect(screen.getByText("Devolution")).toBeInTheDocument(); }); - expect(screen.getByText("Do-ocracy")).toBeInTheDocument(); - expect(screen.getByText("Devolution")).toBeInTheDocument(); + expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0); + expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0); expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); const seeHowLinks = screen.getAllByRole("link", { @@ -109,15 +109,15 @@ describe("User Journey Integration", () => { await waitFor(() => { expect( - screen.getByText("Document how your community makes decisions"), + screen.getByText("See what has worked for other communities"), ).toBeInTheDocument(); }); expect( - screen.getByText("Build an operating manual for a successful community"), + screen.getByText("Decide how you will decide, while things are calm"), ).toBeInTheDocument(); expect( screen.getByText( - "Get a link to your manual for your group to review and evolve", + "Invite your members and change it together as you grow", ), ).toBeInTheDocument(); @@ -156,7 +156,7 @@ describe("User Journey Integration", () => { }); expect( screen.getByText( - "Every group runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", + "Every community runs into the same knots. CommunityRule gives you a way through, and a written record of what you decided.", ), ).toBeInTheDocument(); @@ -173,11 +173,8 @@ describe("User Journey Integration", () => { const partnerLogos = logoImages.filter( (img) => img.alt?.includes("Food Not Bombs") || - img.alt?.includes("Start COOP") || - img.alt?.includes("Metagov") || - img.alt?.includes("Open Civics") || - img.alt?.includes("Mutual Aid CO") || - img.alt?.includes("CU Boulder"), + img.alt?.includes("Boulder Street Medics") || + img.alt?.includes("Mutual Aid Colorado"), ); expect(partnerLogos.length).toBeGreaterThan(0); }); @@ -193,11 +190,11 @@ describe("User Journey Integration", () => { test("user completes the full journey from discovery to action", async () => { renderPageWithFooter(); - expect(screen.getByText("Collaborate")).toBeInTheDocument(); - expect(screen.getByText("with clarity")).toBeInTheDocument(); + expect(screen.getByText("Build structure")).toBeInTheDocument(); + expect(screen.getByText("before you need it")).toBeInTheDocument(); await waitFor(() => { - expect(screen.getByText("How CommunityRule works")).toBeInTheDocument(); + expect(screen.getByText("How CommunityRule helps")).toBeInTheDocument(); }); await waitFor(() => { diff --git a/tests/unit/LogoWall.test.jsx b/tests/unit/LogoWall.test.jsx index 6099f87..c485e7b 100644 --- a/tests/unit/LogoWall.test.jsx +++ b/tests/unit/LogoWall.test.jsx @@ -7,16 +7,16 @@ afterEach(() => { cleanup(); }); describe("LogoWall Component", () => { - test("renders with default logos", () => { + test("renders the three pilot marks", () => { render(); - // Check for default logos + expect(screen.getByAltText("Boulder Street Medics")).toBeInTheDocument(); expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); - expect(screen.getByAltText("Start COOP")).toBeInTheDocument(); - expect(screen.getByAltText("Metagov")).toBeInTheDocument(); - expect(screen.getByAltText("Open Civics")).toBeInTheDocument(); - expect(screen.getByAltText("Mutual Aid CO")).toBeInTheDocument(); - expect(screen.getByAltText("CU Boulder")).toBeInTheDocument(); + expect(screen.getByAltText("Mutual Aid Colorado")).toBeInTheDocument(); + expect(screen.queryByAltText("Start COOP")).not.toBeInTheDocument(); + expect(screen.queryByAltText("Metagov")).not.toBeInTheDocument(); + expect(screen.queryByAltText("Open Civics")).not.toBeInTheDocument(); + expect(screen.queryByAltText("CU Boulder")).not.toBeInTheDocument(); }); test("renders with custom logos", () => { @@ -25,13 +25,11 @@ describe("LogoWall Component", () => { src: "assets/custom1.png", alt: "Custom Logo 1", size: "h-8", - order: "order-1", }, { src: "assets/custom2.png", alt: "Custom Logo 2", size: "h-10", - order: "order-2", }, ]; @@ -62,19 +60,15 @@ describe("LogoWall Component", () => { ); }); - test("applies responsive grid layout", () => { + test("lays out the three marks in a wrapping row", () => { render(); - const grid = document.querySelector( - '[class*="grid grid-cols-2 grid-rows-3"]', - ); - expect(grid).toBeInTheDocument(); - expect(grid).toHaveClass( - "sm:grid-cols-3", - "sm:grid-rows-2", - "md:flex", - "md:justify-between", + const row = document.querySelector('[class*="flex w-full flex-col"]'); + expect(row).toBeInTheDocument(); + expect(row).toHaveClass( + "sm:flex-row", "md:flex-nowrap", + "md:justify-between", "gap-[38.4px]", ); }); @@ -97,7 +91,6 @@ describe("LogoWall Component", () => { test("handles empty logos array", () => { render(); - // Should fall back to default logos expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); }); @@ -112,15 +105,14 @@ describe("LogoWall Component", () => { ).toBe(0); }); - test("renders with proper semantic structure", () => { + test("renders with proper semantic structure and no heading", () => { render(); const section = document.querySelector("section"); expect(section).toBeInTheDocument(); - expect( - screen.getByText("Trusted by leading cooperators"), - ).toBeInTheDocument(); + screen.queryByText("Trusted by leading cooperators"), + ).not.toBeInTheDocument(); }); test("renders partner logos at Figma 60% opacity", () => { @@ -136,22 +128,15 @@ describe("LogoWall Component", () => { const logos = screen.getAllByRole("img"); logos.forEach((logo) => { - // Next.js Image attributes are not rendered as HTML attributes in JSDOM - // Just verify the images are present expect(logo).toBeInTheDocument(); }); }); - test("prioritizes first two logos", () => { + test("marks the section with the Figma node id", () => { render(); - const logos = screen.getAllByRole("img"); - const foodNotBombsLogo = logos.find((img) => img.alt === "Food Not Bombs"); - const startCoopLogo = logos.find((img) => img.alt === "Start COOP"); - - // Next.js Image priority attribute is not rendered as HTML attribute in JSDOM - // Just verify the logos are present - expect(foodNotBombsLogo).toBeInTheDocument(); - expect(startCoopLogo).toBeInTheDocument(); + expect( + document.querySelector('[data-figma-node="18483-18425"]'), + ).toBeInTheDocument(); }); }); diff --git a/tests/unit/RuleStack.test.jsx b/tests/unit/RuleStack.test.jsx index f7604de..7034555 100644 --- a/tests/unit/RuleStack.test.jsx +++ b/tests/unit/RuleStack.test.jsx @@ -90,7 +90,9 @@ describe("RuleStack Component", () => { test("defaults to home rule stack heading copy when namespace omitted", () => { render(); const heading = screen.getByRole("heading", { level: 2 }); - expect(heading.textContent).toMatch(/Popular templates/); + expect(heading.textContent).toMatch( + /Get templates that help your community run smoothly/, + ); }); test("renders four featured governance template cards on the home row", async () => {