Merge pull request 'Match Home, top nav, hero, and logo wall to the current marketing frames' (#88) from adilallo/feature/CR-226-home-and-nav into main

Reviewed-on: #88
This commit was merged in pull request #88.
This commit is contained in:
an.di committed 2026-11-03 23:29:03 +00:00
commit 3dee3a587b
60 files changed
+799 -601

No files matched your search

+3
View File
@@ -50,6 +50,9 @@ npm-cache/
# Marketing book cover (raster; see docs/guides/static-assets.md) # Marketing book cover (raster; see docs/guides/static-assets.md)
!public/assets/marketing/community-rules-cover.png !public/assets/marketing/community-rules-cover.png
# Header Create-rule portraits
!public/assets/marketing/avatar-*.png
# Open Graph 1200×630 share images (`npm run generate:og-images`) # Open Graph 1200×630 share images (`npm run generate:og-images`)
!public/assets/marketing/og-image.png !public/assets/marketing/og-image.png
!public/assets/marketing/og-image-templates.png !public/assets/marketing/og-image-templates.png
-3
View File
@@ -51,9 +51,6 @@ export default function Page() {
title: t("pages.home.heroBanner.title"), title: t("pages.home.heroBanner.title"),
subtitle: t("pages.home.heroBanner.subtitle"), subtitle: t("pages.home.heroBanner.subtitle"),
description: t("pages.home.heroBanner.description"), description: t("pages.home.heroBanner.description"),
ctaText: t("pages.home.heroBanner.ctaText"),
ctaHref: t("pages.home.heroBanner.ctaHref"),
imageAlt: t("heroBanner.imageAlt"),
}; };
const cardStepsData = { const cardStepsData = {
+8 -4
View File
@@ -95,11 +95,15 @@ const Logo = memo<LogoProps>(
const wordmarkVisibilityClass = const wordmarkVisibilityClass =
size === "topNavHeader" size === "topNavHeader"
? wordmark ? wordmark
? "hidden md:block" ? "hidden sm:block"
: "hidden" : "hidden"
: wordmark : size === "topNavFolderTop"
? "" ? wordmark
: "hidden"; ? "hidden md:block"
: "hidden"
: wordmark
? ""
: "hidden";
return ( return (
<Link href="/" className="group block" aria-label={t("logoAlt")}> <Link href="/" className="group block" aria-label={t("logoAlt")}>
+10 -5
View File
@@ -43,6 +43,7 @@ const RuleContainer = memo<RuleProps>(
logoAlt, logoAlt,
communityInitials, communityInitials,
hideCategoryAddButton = false, hideCategoryAddButton = false,
interactive,
hasBottomLinks = false, hasBottomLinks = false,
bottomStatusLabel, bottomStatusLabel,
bottomLinks, bottomLinks,
@@ -53,12 +54,13 @@ const RuleContainer = memo<RuleProps>(
surfaceColor, surfaceColor,
}) => { }) => {
const size = sizeProp ?? "L"; const size = sizeProp ?? "L";
const isInteractive = interactive ?? !hasBottomLinks;
const t = useTranslation("ruleCard"); const t = useTranslation("ruleCard");
const cardAriaLabel = t("ariaLabel")?.replace("{title}", title) || title; const cardAriaLabel = t("ariaLabel")?.replace("{title}", title) || title;
const recommendedLabel = t("recommendedLabel"); const recommendedLabel = t("recommendedLabel");
const handleClick = () => { const handleClick = () => {
if (hasBottomLinks) return; if (!isInteractive || hasBottomLinks) return;
// Basic analytics event tracking // Basic analytics event tracking
if (typeof window !== "undefined" && window.gtag) { if (typeof window !== "undefined" && window.gtag) {
window.gtag("event", "template_selected", { window.gtag("event", "template_selected", {
@@ -79,7 +81,7 @@ const RuleContainer = memo<RuleProps>(
}; };
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (hasBottomLinks) return; if (!isInteractive || hasBottomLinks) return;
if (event.key === "Enter" || event.key === " ") { if (event.key === "Enter" || event.key === " ") {
event.preventDefault(); event.preventDefault();
handleClick(); handleClick();
@@ -98,9 +100,11 @@ const RuleContainer = memo<RuleProps>(
icon={icon} icon={icon}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
className={className} className={className}
href={hasBottomLinks ? undefined : href} href={isInteractive && !hasBottomLinks ? href : undefined}
onClick={hasBottomLinks ? undefined : handleClick} onClick={isInteractive && !hasBottomLinks ? handleClick : undefined}
onKeyDown={hasBottomLinks || href ? undefined : handleKeyDown} onKeyDown={
isInteractive && !hasBottomLinks && !href ? handleKeyDown : undefined
}
expanded={expanded} expanded={expanded}
size={size} size={size}
categories={categories} categories={categories}
@@ -108,6 +112,7 @@ const RuleContainer = memo<RuleProps>(
logoAlt={logoAlt} logoAlt={logoAlt}
communityInitials={communityInitials} communityInitials={communityInitials}
hideCategoryAddButton={hideCategoryAddButton} hideCategoryAddButton={hideCategoryAddButton}
interactive={isInteractive}
hasBottomLinks={hasBottomLinks} hasBottomLinks={hasBottomLinks}
bottomStatusLabel={bottomStatusLabel} bottomStatusLabel={bottomStatusLabel}
bottomLinks={bottomLinks} bottomLinks={bottomLinks}
+6
View File
@@ -63,6 +63,11 @@ export interface RuleProps {
communityInitials?: string; communityInitials?: string;
/** Hide the per-category "+" add chip affordance (e.g. read-only template review). */ /** Hide the per-category "+" add chip affordance (e.g. read-only template review). */
hideCategoryAddButton?: boolean; 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 * Figma `Card / Rule` variant: description + optional status chip + text links
* (e.g. Duplicate / Delete, or Continue / Start new rule). When set, the card * (e.g. Duplicate / Delete, or Continue / Start new rule). When set, the card
@@ -117,6 +122,7 @@ export interface RuleViewProps {
logoAlt?: string; logoAlt?: string;
communityInitials?: string; communityInitials?: string;
hideCategoryAddButton?: boolean; hideCategoryAddButton?: boolean;
interactive?: boolean;
hasBottomLinks?: boolean; hasBottomLinks?: boolean;
bottomStatusLabel?: string; bottomStatusLabel?: string;
bottomLinks?: RuleBottomLink[]; bottomLinks?: RuleBottomLink[];
+2 -1
View File
@@ -29,6 +29,7 @@ export function RuleView({
logoAlt, logoAlt,
communityInitials, communityInitials,
hideCategoryAddButton = false, hideCategoryAddButton = false,
interactive,
hasBottomLinks = false, hasBottomLinks = false,
bottomStatusLabel, bottomStatusLabel,
bottomLinks, bottomLinks,
@@ -41,7 +42,7 @@ export function RuleView({
recommendedLabel, recommendedLabel,
}: RuleViewProps) { }: RuleViewProps) {
const ariaLabel = cardAriaLabel; const ariaLabel = cardAriaLabel;
const interactiveCard = !hasBottomLinks; const interactiveCard = interactive ?? !hasBottomLinks;
// Size-based styling // Size-based styling
const isLarge = size === "L"; const isLarge = size === "L";
+34 -32
View File
@@ -1,8 +1,8 @@
"use client"; "use client";
/** /**
* Figma: "Navigation / Top" (22078-808559); Folder-top 430–639 (18582:22056), * Figma: "Navigation / Top" (5944:17779); Folder-top 320–429 (18583:22098),
* 640–1023 (18580:17113). * 430–639 (18582:22056); standard 320–429 (18411:57843).
*/ */
import { memo, useCallback } from "react"; import { memo, useCallback } from "react";
@@ -82,7 +82,7 @@ const TopContainer = memo<TopProps>(
const logoSize = folderTop ? "topNavFolderTop" : "topNavHeader"; const logoSize = folderTop ? "topNavFolderTop" : "topNavHeader";
const navigationItems = [ const navigationItems = [
{ href: "/use-cases", text: t("navigation.useCases"), extraPadding: true }, { href: "/workshops", text: t("navigation.workshops") },
{ href: "/learn", text: t("navigation.learn") }, { href: "/learn", text: t("navigation.learn") },
{ href: "/about", text: t("navigation.about") }, { href: "/about", text: t("navigation.about") },
]; ];
@@ -93,30 +93,25 @@ const TopContainer = memo<TopProps>(
? FOLDER_NAV_ITEM_RESPONSIVE_CLASS ? FOLDER_NAV_ITEM_RESPONSIVE_CLASS
: NAV_ITEM_RESPONSIVE_CLASS; : NAV_ITEM_RESPONSIVE_CLASS;
return navigationItems.map((item) => { return navigationItems.map((item) => (
const isUseCases = item.extraPadding === true; <MenuItem
key={item.href}
return ( href={item.href}
<MenuItem size="X Small"
key={item.href} mode={mode}
href={item.href} state={pathname === item.href ? "selected" : "default"}
size="X Small" className={sizeClass}
mode={mode} ariaLabel={t("ariaLabels.navigateToPage").replace(
state={pathname === item.href ? "selected" : "default"} "{text}",
reducedPadding={isUseCases} item.text,
className={sizeClass} )}
ariaLabel={t("ariaLabels.navigateToPage").replace( >
"{text}", {item.text}
item.text, </MenuItem>
)} ));
>
{item.text}
</MenuItem>
);
});
}; };
const renderLoginButton = () => { const renderLoginButton = (className = "") => {
const label = loggedIn ? t("buttons.profile") : t("buttons.logIn"); const label = loggedIn ? t("buttons.profile") : t("buttons.logIn");
const ariaLabel = loggedIn const ariaLabel = loggedIn
? t("ariaLabels.goToProfile") ? t("ariaLabels.goToProfile")
@@ -124,16 +119,23 @@ const TopContainer = memo<TopProps>(
const navSelected = const navSelected =
(loggedIn && pathname === "/profile") || (loggedIn && pathname === "/profile") ||
(!loggedIn && pathname === "/login"); (!loggedIn && pathname === "/login");
const loginClassName = folderTop const loginClassName = [
? FOLDER_NAV_ITEM_RESPONSIVE_CLASS folderTop
: NAV_ITEM_RESPONSIVE_CLASS; ? 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) { if (loggedIn) {
return ( return (
<MenuItem <MenuItem
href="/profile" href="/profile"
size="X Small" size="X Small"
mode="default" mode={mode}
state={navSelected ? "selected" : "default"} state={navSelected ? "selected" : "default"}
className={loginClassName} className={loginClassName}
ariaLabel={ariaLabel} ariaLabel={ariaLabel}
@@ -154,7 +156,7 @@ const TopContainer = memo<TopProps>(
} }
href="/login" href="/login"
size="X Small" size="X Small"
mode="default" mode={mode}
state={navSelected ? "selected" : "default"} state={navSelected ? "selected" : "default"}
className={loginClassName} className={loginClassName}
ariaLabel={ariaLabel} ariaLabel={ariaLabel}
@@ -182,8 +184,8 @@ const TopContainer = memo<TopProps>(
size="small" size="small"
className={ className={
folderTop folderTop
? "md:-space-x-[9px] 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]"
: "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) => ( {avatarImageSources.map((src, index) => (
+1 -1
View File
@@ -28,6 +28,6 @@ export interface TopViewProps {
}; };
logoSize: "topNavFolderTop" | "topNavHeader"; logoSize: "topNavFolderTop" | "topNavHeader";
renderNavigationItems: () => React.ReactNode; renderNavigationItems: () => React.ReactNode;
renderLoginButton: () => React.ReactNode; renderLoginButton: (_className?: string) => React.ReactNode;
renderCreateRuleButton: () => React.ReactNode; renderCreateRuleButton: () => React.ReactNode;
} }
+21 -14
View File
@@ -22,8 +22,9 @@ function TopView({
}: TopViewProps) { }: TopViewProps) {
const t = useTranslation(folderTop ? "homeHeader" : "header"); const t = useTranslation(folderTop ? "homeHeader" : "header");
const loginControl = logIn ? ( const phoneAuthItem = logIn ? renderLoginButton("md:hidden") : null;
<Menu size="X Small" className="lg:gap-[var(--space-300)]"> const desktopAuthItem = logIn ? (
<Menu size="X Small" className="hidden md:flex lg:gap-[var(--space-300)]">
{renderLoginButton()} {renderLoginButton()}
</Menu> </Menu>
) : null; ) : null;
@@ -40,13 +41,14 @@ function TopView({
className="w-full overflow-visible bg-transparent" className="w-full overflow-visible bg-transparent"
role="banner" role="banner"
aria-label={t("ariaLabels.homePageNavigationHeader")} aria-label={t("ariaLabels.homePageNavigationHeader")}
data-figma-node="5944-17779"
> >
<nav <nav
className="relative mx-auto flex h-[50px] items-end justify-between pl-[var(--space-200)] pr-[var(--space-400)] pt-[var(--space-250)] sm:h-[62px] sm:px-[var(--space-250)] sm:pr-[var(--space-500)] sm:pt-[var(--space-250)] md:h-[68px] md:px-[var(--space-400)] md:pr-[var(--space-800)] md:pt-[var(--space-400)] lg:h-[68px] lg:pl-[var(--space-600)] lg:pr-[var(--space-1400)] lg:pt-[var(--space-400)] xl:h-[88px] xl:pl-[var(--space-1200)] xl:pr-[var(--space-1400)] xl:pt-[var(--space-600)]" className="relative mx-auto flex h-[50px] items-end justify-between pl-[var(--space-200)] pr-[var(--space-400)] pt-[var(--space-250)] sm:h-[62px] sm:px-[var(--space-250)] sm:pr-[var(--space-500)] sm:pt-[var(--space-250)] md:h-[68px] md:px-[var(--space-400)] md:pr-[var(--space-800)] md:pt-[var(--space-400)] lg:h-[68px] lg:pl-[var(--space-600)] lg:pr-[var(--space-1400)] lg:pt-[var(--space-400)] xl:h-[88px] xl:pl-[var(--space-1200)] xl:pr-[var(--space-1400)] xl:pt-[var(--space-600)]"
role="navigation" role="navigation"
aria-label={t("ariaLabels.mainNavigation")} aria-label={t("ariaLabels.mainNavigation")}
> >
<div className="HeaderTab header-breakpoint-transition relative flex h-[var(--space-1000)] w-fit min-w-0 items-center self-end overflow-visible rounded-tl-[var(--radius-400)] rounded-tr-[var(--radius-400)] bg-[var(--color-surface-invert-brand-primary)] pl-[var(--space-300)] pr-[var(--space-300)] sm:mr-[var(--space-200)] sm:h-[52px] sm:w-auto sm:flex-1 sm:rounded-t-[var(--radius-500)] sm:pr-[var(--space-150)] md:h-[52px] md:min-w-0 md:rounded-t-[var(--radius-500)] md:pl-[var(--space-600)] md:pr-[var(--space-300)] lg:mr-[var(--space-600)] lg:h-[52px] lg:rounded-t-[var(--radius-500)] lg:pl-[var(--space-600)] lg:pr-[var(--space-1200)] xl:mr-[var(--space-800)] xl:h-[64px] xl:rounded-t-[var(--radius-500)] xl:pl-[var(--space-800)] xl:pr-[var(--space-3000)]"> <div className="HeaderTab header-breakpoint-transition relative flex h-[var(--space-1000)] min-w-0 flex-1 items-center justify-between self-end overflow-visible rounded-tl-[var(--radius-400)] rounded-tr-[var(--radius-400)] bg-[var(--color-surface-invert-brand-primary)] pl-[var(--space-300)] pr-[var(--space-300)] sm:mr-[var(--space-200)] sm:h-[52px] sm:rounded-t-[var(--radius-500)] sm:pr-[var(--space-150)] md:h-[52px] md:rounded-t-[var(--radius-500)] md:pl-[var(--space-600)] md:pr-[var(--space-300)] lg:mr-[var(--space-600)] lg:h-[52px] lg:rounded-t-[var(--radius-500)] lg:pl-[var(--space-600)] lg:pr-[var(--space-1200)] xl:mr-[var(--space-800)] xl:h-[64px] xl:rounded-t-[var(--radius-500)] xl:pl-[var(--space-800)] xl:pr-[var(--space-3000)]">
<div className="relative z-[1] shrink-0"> <div className="relative z-[1] shrink-0">
<Logo <Logo
size={logoSize} size={logoSize}
@@ -56,7 +58,7 @@ function TopView({
</div> </div>
<div <div
className="relative z-[1] shrink-0 md:absolute md:top-1/2 md:left-[calc(50vw-var(--space-400))] md:-translate-x-1/2 md:-translate-y-1/2 lg:left-[calc(50vw-var(--space-600))] xl:left-[calc(50vw-var(--space-1200))]" className="relative z-[1] min-w-0 md:absolute md:top-1/2 md:left-[calc(50vw-var(--space-400))] md:-translate-x-1/2 md:-translate-y-1/2 lg:left-[calc(50vw-var(--space-600))] xl:left-[calc(50vw-var(--space-1200))]"
data-top="nav" data-top="nav"
> >
<Menu <Menu
@@ -64,6 +66,7 @@ function TopView({
className="md:gap-[var(--space-100)] lg:gap-[var(--space-300)]" className="md:gap-[var(--space-100)] lg:gap-[var(--space-300)]"
> >
{renderNavigationItems()} {renderNavigationItems()}
{phoneAuthItem}
</Menu> </Menu>
</div> </div>
@@ -94,7 +97,7 @@ function TopView({
className="flex shrink-0 items-center gap-[var(--space-100)] self-center md:gap-[var(--space-250)]" className="flex shrink-0 items-center gap-[var(--space-100)] self-center md:gap-[var(--space-250)]"
data-top="auth" data-top="auth"
> >
{loginControl} {desktopAuthItem}
{renderCreateRuleButton()} {renderCreateRuleButton()}
</div> </div>
</nav> </nav>
@@ -105,11 +108,10 @@ function TopView({
/** /**
* Standard marketing / app top nav. * Standard marketing / app top nav.
* Figma: "Navigation / Top" (Community-Rule-System, node 22078-808559). * Figma: "Navigation / Top" (Community-Rule-System, node 5944:17779).
* Below md: auto | 1fr | auto so the logo cannot paint over the cluster. * Below md: logo | links (with Log in / Profile) | Create rule, so the mark
* From md: 1fr | auto | 1fr so the cluster sits on the same page midline * cannot paint over the cluster. From md: 1fr | auto | 1fr so the cluster
* as Folder-top (Create rule is wider than the logo, so a leftover-column * sits on the page midline.
* center sits off-center).
*/ */
return ( 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)]" className="relative z-50 w-full border-b border-[var(--border-color-default-tertiary)] bg-[var(--color-surface-default-primary)]"
role="banner" role="banner"
aria-label={t("ariaLabels.mainNavigationHeader")} aria-label={t("ariaLabels.mainNavigationHeader")}
data-figma-node="5944-17779"
> >
<nav <nav
className="grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center px-[var(--space-400)] py-[var(--space-200)] sm:px-[var(--space-400)] md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] lg:px-[var(--space-1800)] lg:py-[var(--space-400)]" className="flex w-full items-center gap-[var(--space-200)] px-[var(--space-400)] py-[var(--space-200)] sm:px-[var(--space-400)] md:grid md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:gap-0 lg:px-[var(--space-1800)] lg:py-[var(--space-400)]"
role="navigation" role="navigation"
aria-label={t("ariaLabels.mainNavigation")} aria-label={t("ariaLabels.mainNavigation")}
> >
<div className="min-w-0 shrink-0 justify-self-start lg:w-[200px] lg:max-w-[200px]" data-top="logo"> <div
className="min-w-0 shrink-0 justify-self-start lg:w-[200px] lg:max-w-[200px]"
data-top="logo"
>
<Logo <Logo
size={logoSize} size={logoSize}
wordmark wordmark
@@ -136,7 +142,7 @@ function TopView({
</div> </div>
<div <div
className="flex min-w-0 items-center justify-end md:justify-center" className="flex min-w-0 flex-1 items-center justify-end md:flex-none md:justify-center"
data-top="nav" data-top="nav"
> >
<Menu <Menu
@@ -144,6 +150,7 @@ function TopView({
className="min-w-0 lg:gap-[var(--space-300)]" className="min-w-0 lg:gap-[var(--space-300)]"
> >
{renderNavigationItems()} {renderNavigationItems()}
{phoneAuthItem}
</Menu> </Menu>
</div> </div>
@@ -151,7 +158,7 @@ function TopView({
className="flex shrink-0 items-center justify-self-end gap-[var(--space-100)] md:gap-[var(--space-250)] lg:gap-[var(--space-100)]" className="flex shrink-0 items-center justify-self-end gap-[var(--space-100)] md:gap-[var(--space-250)] lg:gap-[var(--space-100)]"
data-top="auth" data-top="auth"
> >
{loginControl} {desktopAuthItem}
{renderCreateRuleButton()} {renderCreateRuleButton()}
</div> </div>
</nav> </nav>
@@ -44,6 +44,7 @@ function AskOrganizerView({
variant={lockupVariant} variant={lockupVariant}
alignment={lockupAlignment} alignment={lockupAlignment}
titleId={labelledBy} titleId={labelledBy}
headingLevel="h2"
/> />
<div <div
@@ -30,13 +30,14 @@ function FeatureGridView({
<div className="mx-auto flex w-full flex-col gap-[var(--space-1000)] md:gap-[var(--space-1200)] lg:flex-row lg:items-center lg:gap-[var(--space-1800)]"> <div className="mx-auto flex w-full flex-col gap-[var(--space-1000)] md:gap-[var(--space-1200)] lg:flex-row lg:items-center lg:gap-[var(--space-1800)]">
<div className="lg:min-w-0 lg:flex-1"> <div className="lg:min-w-0 lg:flex-1">
<ContentLockup <ContentLockup
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
variant="feature" variant="feature"
linkText={linkText} linkText={linkText}
linkHref={linkHref} linkHref={linkHref}
titleId={labelledBy} titleId={labelledBy}
/> headingLevel="h2"
/>
</div> </div>
<div className="grid w-full grid-cols-1 gap-[var(--space-200)] md:grid-cols-2 md:gap-[var(--space-400)] lg:flex-1"> <div className="grid w-full grid-cols-1 gap-[var(--space-200)] md:grid-cols-2 md:gap-[var(--space-400)] lg:flex-1">
@@ -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<HeroBannerProps>(
({ 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<Category[]>(() => {
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 (
<HeroBannerView
title={title ?? tHome("title")}
subtitle={subtitle ?? tHome("subtitle")}
description={description ?? tHome("description")}
facilitatorCta={tHero("facilitatorCta")}
facilitatorHref={tHero("facilitatorHref")}
createRuleCta={tHero("createRuleCta")}
onCreateRule={handleCreateRule}
ruleTitle={tHero("ruleCard.title")}
ruleDescription={tHero("ruleCard.description")}
ruleCategories={ruleCategories}
/>
);
},
);
HeroBannerContainer.displayName = "HeroBanner";
export default HeroBannerContainer;
@@ -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<HeroBannerProps>(
({
title,
subtitle,
description,
ctaText,
ctaHref,
imageAlt = "Hero illustration",
}) => {
return (
<section className="bg-transparent px-[var(--space-200)] sm:px-[var(--space-250)] md:px-[var(--space-400)] lg:px-[var(--space-600)] xl:px-[var(--space-1200)]">
<div className="flex flex-col gap-[var(--space-250)]">
{/* Frame container for content */}
<div className="bg-[var(--color-surface-invert-brand-primary)] p-[var(--space-300)] sm:p-[var(--space-400)] md:p-[var(--space-1800)] lg:py-[var(--space-2400)] lg:px-[var(--space-1800)] rounded-tl-none rounded-tr-[var(--radius-400)] rounded-br-[var(--radius-400)] rounded-bl-[var(--radius-400)] flex flex-col gap-[var(--space-600)] sm:gap-[var(--space-600)] md:flex-row md:gap-[var(--space-1200)] relative overflow-hidden">
{/* DECORATIONS (behind content) */}
<HeroDecor
className="pointer-events-none absolute z-0
left-0 top-0
translate-x-[-72px] translate-y-[26px] sm:translate-x-[-78px] sm:translate-y-[24px] md:translate-x-[-86px] md:translate-y-[16px] lg:translate-x-[-88px] lg:translate-y-[16px]
w-[1540px] h-[645px] scale-[1.04]"
/>
{/* Content lockup - Large variant */}
<div className="md:flex-1">
<ContentLockup
title={title}
subtitle={subtitle}
description={description}
ctaText={ctaText}
ctaHref={ctaHref}
buttonClassName="shrink-0 whitespace-nowrap min-w-[280px]"
/>
</div>
{/* Hero Image Container */}
<div className="relative z-10 flex w-full items-center justify-center overflow-hidden rounded-[8px] aspect-[16/10] md:flex-1">
<Image
src={getAssetPath(ASSETS.HERO_IMAGE)}
alt={imageAlt}
width={HERO_IMAGE_WIDTH}
height={HERO_IMAGE_HEIGHT}
priority
sizes="(min-width: 768px) 50vw, 100vw"
className="size-full object-contain"
/>
</div>
</div>
</div>
</section>
);
},
);
HeroBanner.displayName = "HeroBanner";
export default HeroBanner;
@@ -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[];
}
@@ -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 (
<section
className="bg-transparent px-[var(--space-200)] sm:px-[var(--space-250)] md:px-[var(--space-400)] lg:px-[var(--space-600)] xl:px-[var(--space-1200)]"
data-figma-node="18477-15473"
>
<div className="flex flex-col gap-[var(--space-250)]">
<div className="relative flex flex-col gap-[var(--space-600)] overflow-hidden rounded-tl-none rounded-tr-[var(--radius-400)] rounded-br-[var(--radius-400)] rounded-bl-[var(--radius-400)] bg-[var(--color-surface-invert-brand-primary)] p-[var(--space-300)] sm:p-[var(--space-400)] md:gap-[var(--space-1200)] md:p-[var(--space-1800)] lg:flex-row lg:items-center lg:gap-[var(--space-1200)] lg:px-[var(--space-1800)] lg:py-[var(--space-1200)] xl:py-[var(--space-1800)]">
<HeroDecor
className="pointer-events-none absolute z-0
left-0 top-0
translate-x-[-72px] translate-y-[26px] sm:translate-x-[-78px] sm:translate-y-[24px] md:translate-x-[-86px] md:translate-y-[16px] lg:translate-x-[-88px] lg:translate-y-[16px]
w-[1540px] h-[645px] scale-[1.04]"
/>
<div className="relative z-10 flex w-full flex-col gap-[var(--space-150)] sm:gap-[var(--space-300)] md:gap-[var(--space-500)] lg:flex-1">
<ContentLockup
title={title}
subtitle={subtitle}
description={description}
/>
<div className="relative z-10 flex justify-start">
<div className="lg:hidden">
<div className="md:hidden">
<Button
buttonType="filled"
palette="inverse"
size="small"
href={facilitatorHref}
>
{facilitatorCta}
</Button>
</div>
<div className="hidden md:block">
<Button
buttonType="filled"
palette="inverse"
size="large"
href={facilitatorHref}
>
{facilitatorCta}
</Button>
</div>
</div>
<div className="hidden lg:block">
<div className="xl:hidden">
<Button
buttonType="filled"
palette="inverse"
size="large"
onClick={onCreateRule}
>
{createRuleCta}
</Button>
</div>
<div className="hidden xl:block">
<Button
buttonType="filled"
palette="inverse"
size="xlarge"
onClick={onCreateRule}
>
{createRuleCta}
</Button>
</div>
</div>
</div>
</div>
<div className="relative z-10 flex w-full justify-center lg:w-[568px] lg:shrink-0">
<Rule
title={ruleTitle}
description={ruleDescription}
expanded
size="L"
fluidWidth
interactive={false}
hideCategoryAddButton
descriptionAtBottom
backgroundColor="bg-[var(--color-surface-invert-brand-primary)]"
className="w-full max-w-[568px] gap-[12px] rounded-[var(--radius-300)] lg:gap-[16px] lg:rounded-[var(--radius-500)]"
categories={ruleCategories}
icon={
// eslint-disable-next-line @next/next/no-img-element -- decorative brand mark beside the title
<img
src={getAssetPath(ASSETS.LOGO)}
alt=""
width={88}
height={88}
className="size-full object-contain brightness-0"
role="presentation"
/>
}
/>
</div>
</div>
</div>
</section>
);
}
HeroBannerView.displayName = "HeroBannerView";
export default memo(HeroBannerView);
+2 -1
View File
@@ -1 +1,2 @@
export { default } from "./HeroBanner"; export { default } from "./HeroBanner.container";
export type { HeroBannerProps } from "./HeroBanner.types";
@@ -1,9 +1,8 @@
"use client"; "use client";
/** /**
* Figma: "Section / Logo Wall" (18847:22366) * Figma: "Section / Logo Wall" (18483:18425)
* Index frames: 320–429 2×3 grid, 430–639 3×2 grid, 640+ space-between row. * Home uses the three pilot marks (street medics, Food Not Bombs, Mutual Aid Colorado).
* Logo heights from those instances (1× / 1.25 at lg / 1.5625 at xl).
*/ */
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
@@ -17,41 +16,20 @@ const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
const defaultLogos = useMemo( 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")), src: getAssetPath(partnerLogoPath("food-not-bombs")),
alt: t("partners.foodNotBombs"), alt: t("partners.foodNotBombs"),
size: "h-[48px] lg:h-[60px] xl:h-[74.6px]", 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")), src: getAssetPath(partnerLogoPath("mutual-aid-co")),
alt: t("partners.mutualAidCo"), alt: t("partners.mutualAidCo"),
size: "h-[51.1px] lg:h-[63.6px] xl:h-[79.5px]", 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], [t],
@@ -63,11 +41,7 @@ const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
); );
return ( return (
<LogoWallView <LogoWallView displayLogos={displayLogos} className={className} />
heading={t("heading")}
displayLogos={displayLogos}
className={className}
/>
); );
}); });
@@ -5,20 +5,17 @@ export interface LogoWallProps {
width?: number; width?: number;
height?: number; height?: number;
size?: string; size?: string;
order?: string;
}>; }>;
className?: string; className?: string;
} }
export interface LogoWallViewProps { export interface LogoWallViewProps {
heading: string;
displayLogos: Array<{ displayLogos: Array<{
src: string; src: string;
alt: string; alt: string;
width?: number; width?: number;
height?: number; height?: number;
size?: string; size?: string;
order?: string;
}>; }>;
className: string; className: string;
} }
@@ -4,41 +4,31 @@ import { memo } from "react";
import Image from "next/image"; import Image from "next/image";
import type { LogoWallViewProps } from "./LogoWall.types"; import type { LogoWallViewProps } from "./LogoWall.types";
function LogoWallView({ function LogoWallView({ displayLogos, className }: LogoWallViewProps) {
heading,
displayLogos,
className,
}: LogoWallViewProps) {
return ( return (
<section <section
className={`bg-[var(--color-surface-default-primary)] p-[var(--space-800)] md:px-[var(--space-600)] md:py-[var(--space-800)] lg:px-[var(--space-1800)] lg:py-[var(--space-1200)] xl:px-[160px] xl:py-[var(--space-1800)] ${className}`} className={`bg-[var(--color-surface-default-primary)] p-[var(--space-800)] md:px-[var(--space-600)] md:py-[var(--space-800)] lg:px-[var(--space-1800)] lg:py-[var(--space-1200)] xl:px-[160px] xl:py-[var(--space-1800)] ${className}`}
data-figma-node="18483-18425"
> >
<div className="flex flex-col items-center gap-[var(--space-800)] md:gap-[var(--space-600)] xl:gap-[var(--space-800)]"> <div className="w-full opacity-60">
<p className="font-inter text-center text-[10px] font-medium uppercase leading-[12px] text-[var(--color-content-default-primary)] xl:text-[14px] xl:leading-[12px]"> <div className="flex w-full flex-col items-center justify-center gap-[38.4px] sm:flex-row sm:flex-wrap sm:justify-center md:flex-nowrap lg:gap-[var(--space-1200)]">
{heading} {displayLogos.map((logo, index) => (
</p> <div
<div className="w-full opacity-60"> key={logo.alt || index}
<div className="grid grid-cols-2 grid-rows-3 sm:grid-cols-3 sm:grid-rows-2 md:flex md:w-full md:flex-nowrap md:items-center md:justify-between gap-[38.4px] lg:gap-[var(--space-1200)]"> className="flex shrink-0 items-center justify-center"
{displayLogos.map((logo, index) => ( >
<div <Image
key={index} src={logo.src}
className={`flex items-center justify-center md:shrink-0 ${ alt={logo.alt}
logo.order || "" className={`${logo.size || "h-8"} w-auto object-contain`}
}`} priority={index < 2}
> unoptimized
<Image width={0}
src={logo.src} height={0}
alt={logo.alt} sizes="100vw"
className={`${logo.size || "h-8"} w-auto object-contain`} />
priority={index < 2} // Prioritize first 2 logos for above-the-fold loading </div>
unoptimized // Skip optimization for local images ))}
width={0}
height={0}
sizes="100vw"
/>
</div>
))}
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -22,6 +22,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
alignment: alignmentProp = "center", alignment: alignmentProp = "center",
titleId, titleId,
titleContent, titleContent,
headingLevel: headingLevelProp = "h1",
}) => { }) => {
const variant = variantProp; const variant = variantProp;
const alignment = alignmentProp; const alignment = alignmentProp;
@@ -179,6 +180,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
alignment={alignment} alignment={alignment}
titleId={titleId} titleId={titleId}
titleContent={titleContent} titleContent={titleContent}
headingLevel={headingLevelProp}
styles={styles} styles={styles}
/> />
); );
@@ -13,6 +13,8 @@ export type ContentLockupVariantValue =
export type ContentLockupAlignmentValue = "center" | "left"; export type ContentLockupAlignmentValue = "center" | "left";
export type ContentLockupHeadingLevel = "h1" | "h2";
export interface ContentLockupProps { export interface ContentLockupProps {
title?: string; title?: string;
subtitle?: string; subtitle?: string;
@@ -37,6 +39,12 @@ export interface ContentLockupProps {
titleId?: string; titleId?: string;
/** Replaces the default title string when inline title markup is required. */ /** Replaces the default title string when inline title markup is required. */
titleContent?: ReactNode; 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 { export interface VariantStyle {
@@ -72,5 +80,6 @@ export interface ContentLockupViewProps {
alignment: "center" | "left"; alignment: "center" | "left";
titleId?: string; titleId?: string;
titleContent?: ReactNode; titleContent?: ReactNode;
headingLevel: ContentLockupHeadingLevel;
styles: VariantStyle; styles: VariantStyle;
} }
@@ -19,8 +19,11 @@ function ContentLockupView({
alignment, alignment,
titleId, titleId,
titleContent, titleContent,
headingLevel,
styles, styles,
}: ContentLockupViewProps) { }: ContentLockupViewProps) {
const TitleTag = headingLevel;
return ( return (
<div className={styles.container}> <div className={styles.container}>
{variant === "ask" || {variant === "ask" ||
@@ -43,9 +46,9 @@ function ContentLockupView({
}`} }`}
> >
{title ? ( {title ? (
<h1 id={titleId} className={styles.title}> <TitleTag id={titleId} className={styles.title}>
{title} {title}
</h1> </TitleTag>
) : null} ) : null}
</div> </div>
{subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null} {subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null}
@@ -61,16 +64,16 @@ function ContentLockupView({
{/* Title container */} {/* Title container */}
<div className={styles.titleContainer}> <div className={styles.titleContainer}>
{titleContent ? ( {titleContent ? (
<h1 <TitleTag
id={titleId} id={titleId}
className={`${styles.title} flex flex-wrap items-center gap-[var(--space-250)] xl:gap-[var(--space-300)]`} className={`${styles.title} flex flex-wrap items-center gap-[var(--space-250)] xl:gap-[var(--space-300)]`}
> >
{titleContent} {titleContent}
</h1> </TitleTag>
) : title ? ( ) : title ? (
<h1 id={titleId} className={styles.title}> <TitleTag id={titleId} className={styles.title}>
{title} {title}
</h1> </TitleTag>
) : null} ) : null}
{variant === "hero" && ( {variant === "hero" && (
<> <>
+3 -3
View File
@@ -123,19 +123,19 @@ export default function RootLayout({ children }: { children: ReactNode }) {
rel="preload" rel="preload"
as="image" as="image"
href={getAssetPath(ASSETS.AVATAR_1)} href={getAssetPath(ASSETS.AVATAR_1)}
type="image/svg+xml" type="image/png"
/> />
<link <link
rel="preload" rel="preload"
as="image" as="image"
href={getAssetPath(ASSETS.AVATAR_2)} href={getAssetPath(ASSETS.AVATAR_2)}
type="image/svg+xml" type="image/png"
/> />
<link <link
rel="preload" rel="preload"
as="image" as="image"
href={getAssetPath(ASSETS.AVATAR_3)} href={getAssetPath(ASSETS.AVATAR_3)}
type="image/svg+xml" type="image/png"
/> />
</head> </head>
<body <body
+1 -1
View File
@@ -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 | | 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. | | Content banner | **`ContentBanner.tsx`** | Single module at bucket root. |
| Logo wall | **`LogoWall/`** | **`container` / `view` / `types`** + **`index.tsx`**. | | Logo wall | **`LogoWall/`** | **`container` / `view` / `types`** + **`index.tsx`**. |
| Card steps (SectionCardSteps) | **`CardSteps/`** | Composes **`cards/Step`** (Figma **Card / Step** — not **`progress/Stepper`**). | | Card steps (SectionCardSteps) | **`CardSteps/`** | Composes **`cards/Step`** (Figma **Card / Step** — not **`progress/Stepper`**). |
+1 -1
View File
@@ -65,7 +65,7 @@ stage. Raster → SVG conversion is tracked in
| `marketing/feature-*.svg` (×4) | Mini cards | Panel art (`support`, `exercises`, `guidance`, `tools`) | | `marketing/feature-*.svg` (×4) | Mini cards | Panel art (`support`, `exercises`, `guidance`, `tools`) |
| `icons/icon-{git-branch,arrow-left-right,door-open,message-square-share}.svg` | FeatureGrid | Card glyphs from Figma Section/Feature-Grid (18632:10668) | | `icons/icon-{git-branch,arrow-left-right,door-open,message-square-share}.svg` | FeatureGrid | Card glyphs from Figma Section/Feature-Grid (18632:10668) |
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG | | `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG | | `marketing/avatar-*.png` (×3) | Avatar / ASSETS | **Done** — PNG portraits (not SVG wrappers) |
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster | | `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
| `marketing/og-image.png` | Default `og:image` / `twitter:image` | **Done** — 1200×630 hero crop (`ASSETS.OG_IMAGE`; `npm run generate:og-images`) | | `marketing/og-image.png` | Default `og:image` / `twitter:image` | **Done** — 1200×630 hero crop (`ASSETS.OG_IMAGE`; `npm run generate:og-images`) |
| `marketing/og-image-templates.png` | Templates `og:image` | **Done** — 1200×630 (`ASSETS.OG_IMAGE_TEMPLATES`) | | `marketing/og-image-templates.png` | Templates `og:image` | **Done** — 1200×630 (`ASSETS.OG_IMAGE_TEMPLATES`) |
+3 -3
View File
@@ -228,9 +228,9 @@ export const ASSETS = {
LOGO: "assets/logos/community-rule.svg", LOGO: "assets/logos/community-rule.svg",
// Avatars // Avatars
AVATAR_1: "assets/marketing/avatar-1.svg", AVATAR_1: "assets/marketing/avatar-1.png",
AVATAR_2: "assets/marketing/avatar-2.svg", AVATAR_2: "assets/marketing/avatar-2.png",
AVATAR_3: "assets/marketing/avatar-3.svg", AVATAR_3: "assets/marketing/avatar-3.png",
// Social media // Social media
BLUESKY_LOGO: "assets/logos/bluesky.svg", BLUESKY_LOGO: "assets/logos/bluesky.svg",
+2 -2
View File
@@ -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.", "_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 toolkit", "linkText": "Explore the patterns",
"linkHref": "/learn", "linkHref": "/learn",
"ariaLabel": "Feature tools and services", "ariaLabel": "Feature tools and services",
"features": { "features": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"navigation": { "navigation": {
"useCases": "Use cases", "workshops": "Workshops",
"learn": "Learn", "learn": "Learn",
"about": "About" "about": "About"
}, },
+46 -2
View File
@@ -1,4 +1,48 @@
{ {
"_comment": "HeroBanner component defaults (shared across pages)", "_comment": "HeroBanner defaults and the Home Rule card. Chip names follow the current deck and may change when redesigned decks are approved.",
"imageAlt": "Hero illustration" "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"]
}
}
}
} }
+3 -6
View File
@@ -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": { "partners": {
"boulderStreetMedics": "Boulder Street Medics",
"foodNotBombs": "Food Not Bombs", "foodNotBombs": "Food Not Bombs",
"metagov": "Metagov", "mutualAidCo": "Mutual Aid Colorado"
"mutualAidCo": "Mutual Aid CO",
"openCivics": "Open Civics",
"startCoop": "Start COOP",
"cuBoulder": "CU Boulder"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"_comment": "Navigation items", "_comment": "Navigation items",
"useCases": "Use cases", "workshops": "Workshops",
"learn": "Learn", "learn": "Learn",
"about": "About" "about": "About"
} }
+16 -20
View File
@@ -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": { "heroBanner": {
"title": "Collaborate", "title": "Build structure",
"subtitle": "with clarity", "subtitle": "before you need it",
"description": "Help your community make important decisions in a way that reflects its unique values.", "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."
"ctaText": "Learn how CommunityRule works",
"ctaHref": "/how-it-works"
}, },
"cardSteps": { "cardSteps": {
"title": "How CommunityRule works", "title": "How CommunityRule helps",
"headingDesktopLine1": "How", "headingDesktopLine1": "How",
"headingDesktopLine2": "CommunityRule", "headingDesktopLine2": "CommunityRule",
"headingDesktopLine3": "helps", "headingDesktopLine3": "helps",
"subtitle": "Here's a quick overview of the process, from start to finish.", "subtitle": "Here's a quick overview of the process, from start to finish.",
"cards": { "cards": {
"card1": { "card1": {
"text": "Document how your community makes decisions", "text": "See what has worked for other communities",
"_comment": "First step card" "_comment": "Pending design approval versus Section / Card Steps"
}, },
"card2": { "card2": {
"text": "Build an operating manual for a successful community", "text": "Decide how you will decide, while things are calm",
"_comment": "Second step card" "_comment": "Pending design approval versus Section / Card Steps"
}, },
"card3": { "card3": {
"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"
"_comment": "Third step card"
} }
} }
}, },
"featureGrid": { "featureGrid": {
"title": "Tools for the parts that get hard", "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": { "features": {
"decisionMaking": { "decisionMaking": {
"title": "Choose how you decide", "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": { "valuesAlignment": {
"title": "Name your values", "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": { "membershipGuidance": {
"title": "Define membership", "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": { "conflictResolution": {
"title": "Plan for conflict", "title": "Plan for conflict",
@@ -56,9 +53,8 @@
"buttonText": "Ask an organizer" "buttonText": "Ask an organizer"
}, },
"ruleStack": { "ruleStack": {
"title": "Popular templates", "title": "Get templates that help your community run smoothly",
"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.", "subtitle": "Ways other communities make decisions. Use one as it is or change it to fit yours.",
"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.",
"cards": { "cards": {
"consensusClusters": { "consensusClusters": {
"title": "Consensus clusters", "title": "Consensus clusters",
@@ -0,0 +1,56 @@
<svg width="73" height="73" viewBox="0 0 73 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Section / Logo Wall">
<g id="Section / Logo Wall_2">
<g id="Breakpoint=1440+">
<g id="Frame 34">
<g id="Boco Street Medics">
<path id="Ellipse 3" d="M36.2915 0.970154C55.7974 0.970316 71.6128 16.7595 71.6128 36.2397C71.6128 55.7199 55.7973 71.5091 36.2915 71.5092C16.7855 71.5092 0.970223 55.72 0.970215 36.2397C0.970215 16.7594 16.7855 0.970154 36.2915 0.970154Z" stroke="white" stroke-width="1.93988"/>
<path id="Ellipse 2" d="M50.3601 58.6317C62.6925 50.9019 66.4142 34.6534 58.6729 22.3395L14.0135 50.3313C21.7549 62.6452 38.0278 66.3614 50.3601 58.6317Z" fill="white"/>
<g id="Group 2">
<g id="Group 1">
<g id="path5">
<path d="M42.4067 14.4257V25.7801L52.1314 19.5682L58.0651 28.8299L47.0665 35.8555L58.0651 42.8813L52.1314 52.1429L42.4067 45.9309V58.153H31.3956V45.8657L21.1752 52.3942L15.2413 43.1327L26.6336 35.8555L15.2413 28.5784L21.1752 19.3168L31.3956 25.8453V14.4257C35.1573 14.4254 38.9192 14.425 42.681 14.4246" fill="white"/>
<path d="M42.4067 14.4257V25.7801L52.1314 19.5683L58.0651 28.8299L47.0665 35.8555L58.0651 42.8813L52.1314 52.1429L42.4067 45.9309V58.153H31.3956V45.8657L21.1752 52.3942L15.2413 43.1327L26.6336 35.8555L15.2413 28.5784L21.1752 19.3168L31.3956 25.8453V14.4257C35.1573 14.4254 38.9192 14.425 42.681 14.4246" stroke="black" stroke-width="1.93988" stroke-miterlimit="10"/>
</g>
<path id="Vector" d="M36.8186 53.3407C36.8739 52.554 37.0178 51.7872 37.2923 50.8392C37.4783 50.1962 37.7793 49.3631 38.0317 48.7864C38.0782 48.6759 38.1247 48.5389 38.1335 48.4836C38.149 48.3864 38.1468 48.382 38.0538 48.3069C37.8413 48.1367 37.0975 47.6682 35.7449 46.855C35.2956 46.5854 34.8993 46.3446 34.8661 46.3203C34.8041 46.2783 34.8041 46.2783 34.8041 46.4948C34.8041 47.065 34.8683 48.0174 34.9502 48.667C35.0078 49.1355 35.2336 50.2934 35.3797 50.8768C35.6475 51.9397 36.0858 52.9562 36.6703 53.8688C36.7854 54.0478 36.7854 54.0478 36.7898 53.871C36.792 53.7738 36.8053 53.5352 36.8186 53.3407ZM38.7312 49.1311C39.3201 48.4615 39.6942 47.8516 39.8934 47.2418C40.055 46.7446 40.0794 46.3601 39.9775 45.9689C39.8248 45.3855 39.3311 44.6497 38.7711 44.1702C38.6028 44.0287 38.5896 44.0089 38.5896 43.9337C38.5896 43.863 38.6095 43.8277 38.7091 43.7194C39.2426 43.1404 39.703 42.3979 39.8934 41.8101C40.055 41.3041 40.0794 40.9262 39.9753 40.5241C39.8425 40.0136 39.4064 39.3286 38.9238 38.8712C38.7379 38.6944 38.7401 38.6745 38.9792 38.3762C39.7296 37.437 40.1192 36.4691 40.0262 35.7709C39.9466 35.161 39.5592 34.4759 38.9637 33.8903C38.4567 33.3931 37.89 33.0175 36.7632 32.4363C35.4682 31.7668 34.9989 31.4375 34.6912 30.9801C34.565 30.7923 34.5318 30.653 34.5429 30.3812C34.5518 30.1868 34.5628 30.136 34.6248 30.0034C34.6647 29.9216 34.7222 29.8177 34.7532 29.778C34.8108 29.7028 34.8108 29.7028 34.7466 29.7426C34.2684 30.0431 33.8876 30.4939 33.7415 30.9271C33.653 31.19 33.6242 31.4132 33.6242 31.8397C33.6264 32.7567 33.9098 33.5257 34.503 34.2307C34.5916 34.3389 34.7023 34.4605 34.7466 34.5002C34.7908 34.5422 34.8263 34.5908 34.8263 34.6063C34.8263 34.6218 34.844 34.635 34.8661 34.635C34.8882 34.635 34.9347 34.666 34.9724 34.7035C35.0675 34.803 35.424 35.0704 35.6852 35.2405C36.0394 35.4703 36.4245 35.678 37.0842 35.9985C38.1557 36.5178 38.3903 36.6503 38.5873 36.847C38.7135 36.9752 38.76 37.0724 38.791 37.2669C38.8308 37.5188 38.7733 37.9674 38.6582 38.3364C38.6183 38.4624 38.6139 38.469 38.5408 38.4756C38.4833 38.48 38.4434 38.4646 38.3638 38.4049C38.056 38.1707 37.6111 37.9099 36.73 37.4481C35.9663 37.0459 35.7936 36.9487 35.5236 36.7697C35.1517 36.5244 34.8617 36.2592 34.6912 36.0117C34.4853 35.7156 34.4853 35.2339 34.6912 34.9024C34.7311 34.8361 34.7488 34.7897 34.7311 34.7897C34.6868 34.7897 34.4056 35.0107 34.2396 35.1764C34.0581 35.3554 33.9253 35.5433 33.8168 35.7686C33.5534 36.3233 33.5512 37.2735 33.8102 38.0447C33.9983 38.6148 34.2662 39.0281 34.7643 39.5252C34.9569 39.7175 35.114 39.8899 35.114 39.9075C35.114 39.9252 35.134 39.9385 35.1583 39.9385C35.1827 39.9385 35.3265 40.0224 35.4771 40.1263C35.9176 40.429 36.232 40.6058 36.9735 40.9682C38.2243 41.5825 38.3748 41.6665 38.5807 41.8698C38.7622 42.051 38.7999 42.1593 38.7976 42.5018C38.7976 42.641 38.7844 42.8311 38.7689 42.9217C38.7379 43.094 38.625 43.4874 38.5453 43.7039C38.501 43.821 38.4944 43.8277 38.4213 43.8277C38.366 43.8277 38.2951 43.7945 38.1512 43.6973C37.7351 43.4189 37.5336 43.3039 36.5087 42.767C35.6453 42.3118 35.2956 42.0842 34.979 41.7726C34.6248 41.4256 34.5385 41.2511 34.5385 40.8909C34.5385 40.6765 34.5982 40.4732 34.7045 40.3141C34.7466 40.2501 34.782 40.1948 34.782 40.1882C34.782 40.1727 34.5385 40.3495 34.3791 40.4799C34.0669 40.7384 33.7902 41.1671 33.6995 41.5317C33.5954 41.9427 33.5954 42.5681 33.6973 43.0631C33.9076 44.0774 34.5363 44.9392 35.5745 45.6353C35.9375 45.8805 36.3559 46.1081 37.2458 46.5435C38.5608 47.1909 38.7312 47.3213 38.791 47.7323C38.8463 48.1013 38.7423 48.6361 38.4921 49.2725C38.4147 49.467 38.4147 49.467 38.4722 49.4162C38.5032 49.3874 38.6205 49.2593 38.7312 49.1311ZM42.8 31.9634C42.7934 31.8463 42.7956 31.8308 42.8399 31.8176C42.8664 31.8087 42.8819 31.7977 42.8775 31.7933C42.8709 31.7866 42.769 31.769 42.6495 31.7513C42.5277 31.7336 42.3816 31.7093 42.3219 31.6961C42.2621 31.6828 42.2112 31.6762 42.2067 31.6806C42.2023 31.685 42.2842 31.7402 42.3905 31.8043C42.5366 31.8883 42.8067 32.0783 42.8067 32.0916C42.8067 32.0938 42.8022 32.0341 42.8 31.9634ZM38.0118 29.9128C38.0073 27.8842 38.0073 27.8842 38.853 27.8776C39.6964 27.8732 39.6964 27.8732 39.6964 27.6544C39.6964 27.4378 39.6964 27.4378 39.2935 27.4246C39.0744 27.4157 38.6914 27.4091 38.4457 27.4091C37.9985 27.4091 37.9985 27.4091 37.9852 25.5374C37.9764 24.5055 37.9697 22.9653 37.9697 22.1145C37.9697 20.5633 37.9697 20.5633 38.0472 20.4351C38.3128 19.9953 38.304 19.5689 38.0206 19.1402C37.8524 18.8838 37.569 18.6805 37.2702 18.6032C36.8761 18.5015 36.4245 18.6452 36.1191 18.9744C35.88 19.2286 35.7383 19.5887 35.7671 19.8716C35.7848 20.055 35.849 20.2495 35.9464 20.4086C36.0217 20.5323 36.0217 20.5323 36.0217 20.9301C36.0217 21.3278 36.0217 21.3278 36.0991 21.3543C36.2076 21.3897 36.3781 21.551 36.4423 21.677C36.4887 21.7698 36.4976 21.814 36.4976 22.0062C36.4976 22.1852 36.4887 22.2471 36.4511 22.3289C36.3825 22.4725 36.2784 22.5808 36.1412 22.6493C36.0217 22.7067 36.0217 22.7067 36.0217 22.8371C36.0217 22.9653 36.0217 22.9653 36.1412 23.0227CLine truncated
<path id="knife" d="M36.8191 53.3407C36.8744 52.554 37.0183 51.7872 37.2928 50.8392C37.4788 50.1962 37.7798 49.3631 38.0322 48.7864C38.0787 48.6759 38.1252 48.5389 38.134 48.4836C38.1495 48.3864 38.1473 48.382 38.0543 48.3069C37.8418 48.1367 37.098 47.6682 35.7454 46.855C35.2961 46.5854 34.8998 46.3446 34.8666 46.3203C34.8046 46.2783 34.8046 46.2783 34.8046 46.4948C34.8046 47.065 34.8688 48.0174 34.9507 48.667C35.0083 49.1355 35.2341 50.2934 35.3802 50.8768C35.648 51.9397 36.0864 52.9562 36.6708 53.8688C36.7859 54.0478 36.7859 54.0478 36.7903 53.871C36.7925 53.7738 36.8058 53.5352 36.8191 53.3407ZM38.0123 29.9128C38.0078 27.8842 38.0078 27.8842 38.8535 27.8776C39.6969 27.8732 39.6969 27.8732 39.6969 27.6544C39.6969 27.4378 39.6969 27.4378 39.294 27.4246C39.0749 27.4157 38.6919 27.4091 38.4462 27.4091C37.999 27.4091 37.999 27.4091 37.9857 25.5374C37.9769 24.5055 37.9702 22.9653 37.9702 22.1145C37.9702 20.5633 37.9702 20.5633 38.0477 20.4351C38.3133 19.9953 38.3045 19.5689 38.0211 19.1402C37.8529 18.8838 37.5695 18.6805 37.2707 18.6032C36.8766 18.5015 36.425 18.6452 36.1196 18.9744C35.8805 19.2286 35.7388 19.5887 35.7676 19.8716C35.7853 20.055 35.8495 20.2495 35.9469 20.4086C36.0222 20.5323 36.0222 20.5323 36.0222 20.9301C36.0222 21.3278 36.0222 21.3278 36.0996 21.3543C36.2081 21.3897 36.3786 21.551 36.4428 21.677C36.4892 21.7698 36.4981 21.814 36.4981 22.0062C36.4981 22.1852 36.4892 22.2471 36.4516 22.3289C36.383 22.4725 36.2789 22.5808 36.1417 22.6493C36.0222 22.7067 36.0222 22.7067 36.0222 22.8371C36.0222 22.9653 36.0222 22.9653 36.1417 23.0227C36.2789 23.0868 36.383 23.1951 36.4516 23.3409C36.5225 23.4956 36.518 23.8448 36.445 23.9928C36.3763 24.1298 36.248 24.2558 36.1218 24.3088C36.0222 24.3486 36.0222 24.3486 36.0222 24.5187C36.0222 24.6889 36.0222 24.6889 36.1284 24.7353C36.2657 24.7972 36.383 24.9143 36.4472 25.0535C36.4892 25.1463 36.4981 25.2038 36.4981 25.3872C36.4981 25.5728 36.4892 25.6236 36.4472 25.712C36.3852 25.8358 36.2081 26.0037 36.0996 26.0391C36.0222 26.0656 36.0222 26.0656 36.0222 26.7263C36.0222 27.2367 36.0155 27.3914 35.9956 27.3981C35.9801 27.4025 35.5484 27.4135 35.0371 27.4246C34.1073 27.4423 34.1073 27.4423 34.1073 27.6522C34.1073 27.8621 34.1073 27.8621 34.9242 27.8687C35.5462 27.8732 35.7477 27.8798 35.7609 27.9019C35.7698 27.9151 35.7786 28.5427 35.7786 29.2918C35.7786 30.3547 35.7853 30.6619 35.8074 30.6796C35.8428 30.7127 37.9769 31.9369 37.999 31.9369C38.0101 31.9391 38.0145 31.0265 38.0123 29.9128Z" fill="black"/>
</g>
<g id="Linked Path Group">
<g id="Linked Path Group_2">
<g id="Group 3">
<path id="[Copy] Boulder County" d="M10.9621 23.4851L6.66113 20.8469L7.76382 19.0546C7.9618 18.7328 8.17301 18.4914 8.39744 18.3304C8.62272 18.1679 8.85084 18.0824 9.08179 18.0737C9.31361 18.0637 9.53732 18.1248 9.75293 18.257C9.91674 18.3575 10.042 18.4815 10.1286 18.6291C10.2138 18.7758 10.2614 18.9351 10.2714 19.107C10.2814 19.2788 10.2545 19.4541 10.1908 19.6328L10.2328 19.6586C10.348 19.4902 10.4934 19.3588 10.6692 19.2643C10.8459 19.1684 11.0381 19.1224 11.2458 19.1265C11.4521 19.1297 11.6588 19.1949 11.866 19.322C12.097 19.4637 12.2674 19.6491 12.3771 19.8783C12.4854 20.1066 12.5236 20.3642 12.4917 20.6511C12.4585 20.9371 12.3442 21.2389 12.1488 21.5565L10.9621 23.4851ZM10.7633 21.9323L11.1585 21.2901C11.297 21.0649 11.3563 20.8729 11.3364 20.7141C11.3172 20.554 11.2314 20.4272 11.0788 20.3336C10.9682 20.2657 10.857 20.2332 10.7454 20.236C10.6323 20.238 10.5248 20.2732 10.4229 20.3417C10.3196 20.4094 10.227 20.5097 10.1452 20.6426L9.73849 21.3036L10.7633 21.9323ZM9.06856 20.8927L9.42235 20.3177C9.49208 20.2043 9.53515 20.092 9.55158 19.9807C9.56801 19.8694 9.55559 19.7673 9.51431 19.6745C9.4739 19.5804 9.40329 19.5024 9.30248 19.4405C9.15827 19.3521 9.01345 19.3336 8.86801 19.385C8.72256 19.4365 8.59647 19.549 8.48974 19.7225L8.12562 20.3143L9.06856 20.8927Z" fill="white"/>
<path id="[Copy] Boulder County_2" d="M14.6577 14.3452C15.0728 14.7137 15.3537 15.1058 15.5004 15.5216C15.647 15.9373 15.674 16.3479 15.5814 16.7533C15.4898 17.1574 15.2938 17.5282 14.9934 17.8656C14.692 18.2042 14.345 18.4421 13.9525 18.5791C13.56 18.7161 13.149 18.7368 12.7196 18.6412C12.29 18.5432 11.8682 18.3105 11.4543 17.943C11.0391 17.5745 10.7577 17.1829 10.6099 16.7684C10.4633 16.3527 10.4357 15.9428 10.5273 15.5386C10.6188 15.1344 10.8154 14.763 11.1169 14.4244C11.4173 14.087 11.7631 13.8493 12.1544 13.7111C12.5467 13.5718 12.9582 13.5505 13.3889 13.6472C13.8196 13.744 14.2425 13.9766 14.6577 14.3452ZM13.8285 15.2764C13.5829 15.0583 13.3442 14.909 13.1124 14.8285C12.8817 14.7467 12.6649 14.7321 12.4619 14.7848C12.26 14.8362 12.0782 14.9527 11.9165 15.1342C11.7559 15.3146 11.6613 15.5085 11.6328 15.716C11.6042 15.9234 11.6435 16.1374 11.7508 16.3578C11.8591 16.5769 12.0361 16.7956 12.2818 17.0136C12.5274 17.2317 12.7656 17.3816 12.9963 17.4634C13.228 17.5439 13.4454 17.5579 13.6484 17.5053C13.8514 17.4526 14.0332 17.3361 14.1938 17.1558C14.3555 16.9742 14.4501 16.7803 14.4775 16.5741C14.5061 16.3666 14.4662 16.1533 14.3579 15.9341C14.2507 15.7137 14.0742 15.4944 13.8285 15.2764Z" fill="white"/>
<path id="[Copy] Boulder County_3" d="M16.176 9.84974L17.1337 9.09842L19.1462 11.6561C19.3788 11.9517 19.511 12.2645 19.5428 12.5944C19.5735 12.9231 19.5101 13.2463 19.3526 13.5641C19.194 13.8805 18.9479 14.1696 18.6145 14.4312C18.2772 14.6958 17.9358 14.8676 17.5903 14.9466C17.2438 15.0242 16.9149 15.0088 16.6037 14.9001C16.2915 14.7902 16.019 14.5874 15.7865 14.2918L13.7739 11.7341L14.7336 10.9813L16.6806 13.4557C16.7883 13.5926 16.914 13.6912 17.0577 13.7517C17.2028 13.8112 17.3534 13.8287 17.5097 13.8042C17.666 13.7797 17.8159 13.7111 17.9594 13.5986C18.1029 13.4861 18.2044 13.3574 18.2639 13.2125C18.3247 13.0667 18.3434 12.9164 18.3198 12.7616C18.2963 12.6068 18.2307 12.461 18.123 12.3242L16.176 9.84974Z" fill="white"/>
<path id="[Copy] Boulder County_4" d="M20.8656 12.6842L18.4045 8.27934L19.4701 7.68579L21.448 11.226L23.2821 10.2044L23.7651 11.069L20.8656 12.6842Z" fill="white"/>
<path id="[Copy] Boulder County_5" d="M25.8648 10.1977L24.1145 10.8423L22.3661 6.10914L24.1141 5.46537C24.5966 5.28767 25.047 5.22943 25.4654 5.29065C25.8847 5.34975 26.2547 5.52157 26.5752 5.80611C26.8951 6.08912 27.1458 6.47637 27.3274 6.96786C27.5095 7.4609 27.5712 7.91952 27.5125 8.34372C27.4554 8.76735 27.2869 9.13835 27.0069 9.45673C26.7265 9.77357 26.3458 10.0206 25.8648 10.1977ZM24.8988 9.44549L25.4606 9.23856C25.7258 9.14092 25.9335 9.01364 26.084 8.85672C26.2354 8.69769 26.3205 8.49917 26.3394 8.26116C26.3593 8.02105 26.3063 7.73074 26.1806 7.39023C26.0548 7.04973 25.9068 6.79616 25.7367 6.62952C25.566 6.46134 25.3709 6.36691 25.1514 6.34624C24.9328 6.32346 24.6879 6.36203 24.4166 6.46194L23.8709 6.66291L24.8988 9.44549Z" fill="white"/>
<path id="[Copy] Boulder County_6" d="M28.9319 9.14136L27.99 4.18419L31.4471 3.52927L31.6319 4.5023L29.3732 4.93021L29.5664 5.94681L31.6484 5.55239L31.8337 6.52785L29.7518 6.92227L29.9454 7.9413L32.2041 7.5134L32.389 8.48643L28.9319 9.14136Z" fill="white"/>
<path id="[Copy] Boulder County_7" d="M32.9975 8.38103L32.8237 3.33804L34.907 3.26645C35.2846 3.25348 35.6128 3.31041 35.8916 3.43724C36.1721 3.56237 36.3913 3.74878 36.5494 3.99645C36.7075 4.24248 36.7924 4.53786 36.8043 4.8826C36.8164 5.23226 36.7503 5.53201 36.606 5.78185C36.4617 6.03006 36.2514 6.223 35.9751 6.3607C35.6988 6.49675 35.3685 6.57137 34.9844 6.58457L33.6669 6.62984L33.6338 5.66951L34.7247 5.63202C34.9086 5.6257 35.0612 5.59663 35.1827 5.54479C35.3058 5.49125 35.3976 5.41414 35.4583 5.31344C35.5188 5.2111 35.5464 5.08196 35.5411 4.92601C35.5357 4.77006 35.4992 4.64229 35.4317 4.54271C35.3641 4.44149 35.2671 4.36758 35.1405 4.32098C35.014 4.27273 34.8596 4.25174 34.6774 4.25801L34.0765 4.27865L34.2165 8.33914L32.9975 8.38103ZM35.7573 5.97866L37.0926 8.24031L35.7629 8.286L34.4522 6.02351L35.7573 5.97866Z" fill="white"/>
<path id="[Copy] Boulder County_8" d="M44.8704 6.23096L43.6659 5.97263C43.6764 5.84553 43.6675 5.72686 43.6392 5.61662C43.6108 5.50637 43.5633 5.40714 43.4966 5.31891C43.4298 5.23069 43.3443 5.15605 43.2398 5.095C43.1372 5.03269 43.0185 4.98707 42.8836 4.95814C42.6443 4.90682 42.4253 4.92117 42.2267 5.0012C42.0296 5.08158 41.8607 5.22258 41.7198 5.42422C41.5806 5.6262 41.4772 5.88457 41.4095 6.19933C41.339 6.52693 41.3265 6.81153 41.372 7.05312C41.4194 7.29345 41.5161 7.48808 41.6622 7.637C41.8102 7.78466 42.0015 7.88364 42.2359 7.93393C42.3676 7.96217 42.4909 7.97097 42.6058 7.96032C42.7222 7.95002 42.8283 7.92154 42.924 7.87487C43.0217 7.82694 43.1071 7.76208 43.1801 7.68031C43.2551 7.59727 43.3158 7.49857 43.3623 7.38421L44.5652 7.64977C44.4974 7.8637 44.3876 8.06528 44.236 8.25453C44.086 8.44412 43.9007 8.6068 43.6799 8.74258C43.4595 8.87675 43.2066 8.96698 42.9211 9.01327C42.6372 9.0599 42.3258 9.04688 41.987 8.9742C41.5405 8.87845 41.1617 8.69472 40.8505 8.42302C40.5412 8.15006 40.3252 7.80302 40.2025 7.38189C40.0797 6.96076 40.0765 6.4796 40.1929 5.93841C40.3097 5.39562 40.5122 4.95774 40.8006 4.62479C41.0889 4.29184 41.4309 4.06529 41.8263 3.94515C42.2218 3.825 42.6388 3.81195 43.0772 3.90598C43.3759 3.97005 43.6432 4.07105 43.879 4.20898C44.1151 4.3453 44.3124 4.51362 44.4709 4.71394C44.6298 4.91265 44.7448 5.13974 44.8158 5.39522C44.8869 5.65069 44.9051 5.92928 44.8704 6.23096Z" fill="white"/>
<path id="[Copy] Boulder County_9" d="M50.1047 9.02634C49.8897 9.53827 49.6091 9.93097 49.2626 10.2044C48.9162 10.4779 48.5361 10.6366 48.1221 10.6806C47.7097 10.7252 47.2952 10.6603 46.8785 10.4859C46.4603 10.3108 46.1229 10.0601 45.8663 9.73361C45.6097 9.40715 45.4573 9.02548 45.4092 8.58861C45.3632 8.15087 45.4473 7.67679 45.6616 7.16637C45.8766 6.65445 46.1565 6.26143 46.5014 5.98732C46.8478 5.71385 47.2272 5.55481 47.6397 5.5102C48.0521 5.46559 48.4674 5.53082 48.8855 5.70587C49.3022 5.88029 49.6391 6.12998 49.8964 6.45493C50.1552 6.78051 50.3083 7.1625 50.3558 7.60088C50.4033 8.03926 50.3196 8.51441 50.1047 9.02634ZM48.9547 8.54494C49.0819 8.24202 49.1462 7.96798 49.1476 7.72282C49.1505 7.47829 49.0942 7.26864 48.9788 7.09387C48.8649 6.91974 48.6958 6.78574 48.4715 6.69186C48.2488 6.59863 48.0346 6.57216 47.8288 6.61247C47.6231 6.65277 47.4331 6.75935 47.259 6.93221C47.0864 7.1057 46.9365 7.34391 46.8093 7.64683C46.6821 7.94974 46.6171 8.22347 46.6142 8.468C46.6128 8.71316 46.6698 8.92313 46.7852 9.09789C46.9007 9.27266 47.0698 9.40666 47.2925 9.4999C47.5167 9.59377 47.731 9.62024 47.9352 9.5793C48.141 9.53899 48.3301 9.4321 48.5027 9.2586C48.6769 9.08574 48.8275 8.84785 48.9547 8.54494Z" fill="white"/>
<path id="[Copy] Boulder County_10" d="M54.969 9.07482L55.9884 9.73977L54.2063 12.4636C54.0003 12.7784 53.7459 13.0037 53.4431 13.1396C53.1411 13.2741 52.8142 13.3178 52.4623 13.2706C52.1113 13.2221 51.7583 13.0821 51.4034 12.8506C51.0444 12.6164 50.7721 12.3485 50.5866 12.0471C50.402 11.7442 50.3114 11.4282 50.3148 11.099C50.3191 10.7684 50.4243 10.4457 50.6302 10.1309L52.4123 7.40707L53.4338 8.07337L51.7097 10.7085C51.6143 10.8542 51.561 11.0048 51.5496 11.1602C51.5396 11.3164 51.5713 11.4646 51.6445 11.6045C51.7178 11.7445 51.8308 11.8643 51.9835 11.9639C52.1362 12.0635 52.2908 12.1183 52.4472 12.1282C52.6051 12.1389 52.7536 12.1083 52.8929 12.0364C53.0323 11.9645 53.1496 11.8557 53.2449 11.71L54.969 9.07482Z" fill="white"/>
<path id="[Copy] Boulder County_11" d="M60.2086 13.4708L56.7649 17.1596L56.0075 16.4546L56.5253 12.9576L56.5018 12.9358L54.5143 15.0648L53.6216 14.2339L57.0653 10.5451L57.8336 11.2602L57.3029 14.7385L57.3336 14.767L59.3178 12.6416L60.2086 13.4708Z" fill="white"/>
<path id="[Copy] Boulder County_12" d="M60.4518 15.1649L61.241 14.5663L63.8231 17.9605L63.0338 18.5591L62.1075 17.3414L58.876 19.7924L58.1481 18.8355L61.3796 16.3845L60.4518 15.1649Z" fill="white"/>
<path id="[Copy] Boulder County_13" d="M64.4791 18.8953L65.1058 20.105L63.7204 22.0213L63.7408 22.0607L66.1077 22.0388L66.7344 23.2486L62.8994 23.1525L61.402 23.9259L60.8444 22.8496L62.3418 22.0762L64.4791 18.8953Z" fill="white"/>
</g>
<path id="[Copy] Boulder County_14" d="M14.0869 54.6979C14.2183 54.5728 14.2829 54.4284 14.2807 54.2646C14.2809 54.101 14.2068 53.926 14.0587 53.7396C13.9606 53.6161 13.8627 53.526 13.7651 53.4692C13.6676 53.4124 13.5741 53.3858 13.4847 53.3896C13.3967 53.3923 13.3161 53.4215 13.2429 53.4774C13.1817 53.5217 13.1387 53.5746 13.1141 53.636C13.0905 53.6987 13.0812 53.7689 13.0863 53.8466C13.0937 53.9246 13.1132 54.0098 13.1446 54.1022C13.1761 54.1946 13.218 54.2935 13.2702 54.3988L13.4627 54.7995C13.5755 55.0286 13.6558 55.2458 13.7035 55.4511C13.7523 55.6577 13.7681 55.8505 13.7509 56.0296C13.7348 56.21 13.684 56.3771 13.5984 56.5308C13.5129 56.6845 13.3914 56.8247 13.2341 56.9515C12.9832 57.1481 12.7188 57.2549 12.4407 57.272C12.1626 57.289 11.8829 57.2182 11.6017 57.0597C11.3215 56.9024 11.0505 56.6591 10.7889 56.3299C10.5263 55.9994 10.347 55.6721 10.251 55.3479C10.1549 55.0238 10.1538 54.7135 10.2477 54.417C10.3415 54.1205 10.5414 53.848 10.8473 53.5993L11.5739 54.5136C11.4525 54.6223 11.3734 54.7406 11.3364 54.8683C11.2995 54.9961 11.301 55.13 11.341 55.2702C11.3821 55.4116 11.4583 55.5524 11.5697 55.6926C11.6718 55.8212 11.7761 55.9167 11.8824 55.9792C11.9897 56.0429 12.0937 56.0748 12.1943 56.0747C12.2949 56.0746 12.3857 56.0436 12.4665 55.9816C12.5402 55.9212 12.5855 55.8475 12.6025 55.7607C12.6209 55.6728 12.612 55.5667 12.5761 55.4422C12.5424 55.318 12.4851 55.1706 12.4041 55.0001L12.1727 54.5108C11.9794 54.1065 11.8921 53.7366 11.9109 53.4011C11.9309 53.0646 12.0915 52.7781 12.3925 52.5418C12.6361 52.3467 12.9026 52.2424 13.192 52.229C13.4813 52.2156 13.7705 52.2851 14.0595 52.4376C14.3485 52.59 14.612 52.8161 14.8501 53.1157C15.0933 53.4218 15.2522 53.7286 15.3267 54.0363C15.4036 54.3442 15.3982 54.6345 15.3106 54.9072C15.223 55.1798 15.0573 55.4148 14.8134 55.6122L14.0869 54.6979Z" fill="white"/>
<path id="[Copy] Boulder County_15" d="M15.4831 55.6154L16.1516 54.8846L19.3029 57.7583L18.6344 58.4892L17.5039 57.4582L14.7666 60.4508L13.8782 59.6407L16.6155 56.6481L15.4831 55.6154Z" fill="white"/>
<path id="[Copy] Boulder County_16" d="M16.7981 62.1905L19.5647 57.9703L21.3094 59.1107C21.6257 59.3174 21.8612 59.5528 22.0161 59.8169C22.1732 60.0805 22.245 60.359 22.2314 60.6523C22.2188 60.9442 22.1179 61.2345 21.9288 61.523C21.7369 61.8156 21.51 62.0224 21.2479 62.1436C20.9868 62.2633 20.7035 62.2999 20.3982 62.2534C20.0937 62.2055 19.7806 62.0764 19.4589 61.8661L18.3555 61.145L18.8824 60.3413L19.796 60.9385C19.95 61.0391 20.0916 61.1032 20.2208 61.1308C20.3523 61.1579 20.4719 61.1477 20.5796 61.1003C20.6881 61.0516 20.7852 60.962 20.8708 60.8315C20.9563 60.701 21.0002 60.5756 21.0024 60.4554C21.0055 60.3338 20.9688 60.2175 20.8922 60.1067C20.8165 59.9944 20.7024 59.8884 20.5498 59.7886L20.0465 59.4597L17.819 62.8577L16.7981 62.1905ZM20.4404 61.8159L20.2279 64.4321L19.1142 63.7042L19.3474 61.1015L20.4404 61.8159Z" fill="white"/>
<path id="[Copy] Boulder County_17" d="M21.1692 64.94L23.2495 60.3426L26.4566 61.7895L26.0483 62.6919L23.9529 61.7466L23.5262 62.6894L25.4577 63.5608L25.0483 64.4654L23.1169 63.5941L22.6892 64.5391L24.7846 65.4844L24.3763 66.3869L21.1692 64.94Z" fill="white"/>
<path id="[Copy] Boulder County_18" d="M25.6 66.8569L26.9674 61.9996L30.3549 62.9504L30.0865 63.9038L27.8732 63.2826L27.5928 64.2787L29.6329 64.8514L29.3638 65.8072L27.3237 65.2346L27.0426 66.233L29.2559 66.8543L28.9875 67.8077L25.6 66.8569Z" fill="white"/>
<path id="[Copy] Boulder County_19" d="M30.5951 64.0589L30.7114 63.0752L34.947 63.5745L34.8307 64.5581L33.3112 64.379L32.835 68.4066L31.6409 68.2658L32.1171 64.2383L30.5951 64.0589Z" fill="white"/>
<path id="[Copy] Boulder County_20" d="M37.2242 63.6776L38.719 63.4613L40.4358 66.3695L40.4944 66.3611L41.3136 63.0858L42.8084 62.8695L43.5332 67.8631L42.3579 68.0332L41.9127 64.9658L41.8712 64.9718L41.1121 68.1812L40.361 68.2898L38.7182 65.4107L38.6767 65.4167L39.1244 68.5012L37.9491 68.6713L37.2242 63.6776Z" fill="white"/>
<path id="[Copy] Boulder County_21" d="M44.8842 67.4661L43.2487 62.6926L46.5782 61.5552L46.8993 62.4922L44.7239 63.2353L45.0593 64.2143L47.0645 63.5293L47.3863 64.4686L45.3811 65.1536L45.7173 66.1349L47.8928 65.3917L48.2138 66.3287L44.8842 67.4661Z" fill="white"/>
<path id="[Copy] Boulder County_22" d="M51.1068 64.9647L49.4633 65.8467L47.0715 61.4037L48.7128 60.5228C49.1658 60.2796 49.6038 60.1592 50.0266 60.1616C50.4502 60.1617 50.8405 60.2803 51.1976 60.5174C51.5539 60.753 51.8563 61.1016 52.1046 61.5629C52.3538 62.0258 52.4789 62.4713 52.4799 62.8995C52.4824 63.3269 52.3673 63.7177 52.1345 64.0719C51.9009 64.4247 51.5584 64.7223 51.1068 64.9647ZM50.0451 64.3544L50.5727 64.0713C50.8216 63.9377 51.0097 63.7827 51.1367 63.6064C51.2645 63.4278 51.3211 63.2194 51.3066 62.9811C51.2928 62.7406 51.1999 62.4605 51.0278 62.1409C50.8557 61.8212 50.6738 61.5908 50.4821 61.4495C50.2896 61.3067 50.0832 61.2404 49.8629 61.2505C49.6433 61.2584 49.4061 61.3307 49.1514 61.4674L48.639 61.7424L50.0451 64.3544Z" fill="white"/>
<path id="[Copy] Boulder County_23" d="M52.4244 58.2166L55.3652 62.3169L54.3731 63.0263L51.4323 58.926L52.4244 58.2166Z" fill="white"/>
<path id="[Copy] Boulder County_24" d="M57.5876 56.1498L56.6846 56.9878C56.5864 56.9064 56.4839 56.8458 56.3772 56.8059C56.2705 56.766 56.1618 56.7481 56.0512 56.7521C55.9405 56.7562 55.8302 56.7835 55.7201 56.8341C55.6102 56.8824 55.5047 56.9534 55.4035 57.0473C55.2241 57.2137 55.1103 57.4012 55.0619 57.6097C55.0148 57.817 55.0335 58.036 55.1181 58.2667C55.2039 58.4964 55.3565 58.729 55.5758 58.9646C55.8041 59.2099 56.0304 59.3831 56.2547 59.4842C56.4791 59.583 56.6942 59.6153 56.9002 59.581C57.1062 59.5444 57.2972 59.4445 57.4729 59.2814C57.5717 59.1898 57.6496 59.0939 57.7068 58.9938C57.7652 58.8925 57.8027 58.7894 57.8193 58.6843C57.8361 58.5769 57.8318 58.4699 57.8067 58.3633C57.7816 58.2544 57.7354 58.1482 57.6683 58.0447L58.5763 57.212C58.7129 57.3901 58.8153 57.5953 58.8836 57.8278C58.953 58.0591 58.9801 58.304 58.9648 58.5624C58.9484 58.8197 58.8773 59.0784 58.7515 59.3386C58.6268 59.5977 58.4375 59.8451 58.1834 60.0808C57.8487 60.3915 57.4806 60.5964 57.0792 60.6955C56.6779 60.7923 56.2692 60.7704 55.8533 60.6298C55.4374 60.4891 55.0409 60.2162 54.6638 59.811C54.2855 59.4047 54.0425 58.9881 53.9349 58.5614C53.8272 58.1348 53.8376 57.7252 53.9659 57.3327C54.0943 56.9402 54.3228 56.5914 54.6515 56.2863C54.8755 56.0785 55.1117 55.9176 55.3601 55.8036C55.6074 55.6883 55.8587 55.6232 56.114 55.6081C56.3681 55.5919 56.6204 55.6278 56.8707 55.7159C57.1211 55.8041 57.36 55.9487 57.5876 56.1498Z" fill="white"/>
<path id="[Copy] Boulder County_25" d="M59.5228 53.1991C59.369 53.1028 59.2129 53.0759 59.0544 53.1184C58.8956 53.1587 58.744 53.2735 58.5996 53.4628C58.5039 53.5882 58.4406 53.7051 58.4095 53.8136C58.3784 53.922 58.3757 54.019 58.4014 54.1046C58.4257 54.1891 58.474 54.2599 58.5462 54.3169C58.6044 54.3652 58.6663 54.3937 58.732 54.4024C58.7986 54.4098 58.8691 54.4014 58.9432 54.3773C59.017 54.3509 59.0949 54.3111 59.1769 54.2579C59.2588 54.2046 59.3445 54.1397 59.4338 54.0632L59.7753 53.7781C59.9698 53.6124 60.1608 53.4811 60.3483 53.3843C60.5368 53.2862 60.7201 53.2233 60.8981 53.1957C61.0772 53.1668 61.2519 53.1748 61.4221 53.2196C61.5924 53.2645 61.7585 53.3475 61.9203 53.4685C62.1729 53.6627 62.3418 53.8922 62.4269 54.1571C62.512 54.422 62.5122 54.7101 62.4277 55.0214C62.3441 55.3313 62.1748 55.6535 61.9198 55.9878C61.6637 56.3235 61.3903 56.5778 61.0993 56.7507C60.8083 56.9236 60.5074 57.0012 60.1966 56.9835C59.8857 56.9659 59.5719 56.8397 59.2551 56.605L59.9634 55.6764C60.0989 55.767 60.2331 55.8144 60.3662 55.8186C60.4993 55.8228 60.629 55.7883 60.7551 55.715C60.8823 55.6404 61.0001 55.532 61.1087 55.3896C61.2083 55.259 61.2754 55.1346 61.3098 55.0163C61.3452 54.8967 61.3505 54.7882 61.3256 54.6909C61.3007 54.5935 61.2482 54.5134 61.1681 54.4504C61.0913 54.3941 61.0087 54.3684 60.9202 54.3733C60.8304 54.3773 60.7296 54.412 60.6176 54.4775C60.5053 54.5407 60.3764 54.6325 60.2309 54.753L59.813 55.0976C59.4683 55.3844 59.1308 55.5601 58.8005 55.6247C58.469 55.6883 58.1514 55.6036 57.8478 55.3706C57.5984 55.183 57.4315 54.9508 57.3471 54.6741C57.2628 54.3974 57.259 54.1005 57.3357 53.7832C57.4124 53.466 57.5668 53.1552 57.799 52.8509C58.0361 52.54 58.2947 52.3106 58.575 52.1626C58.8549 52.0123 59.138 51.9458 59.4242 51.9633C59.7104 51.9808 59.9794 52.0832 60.2311 52.2705L59.5228 53.1991Z" fill="white"/>
</g>
</g>
<path id="lines" d="M66.7372 36.3885C66.7372 36.4373 66.7371 36.4861 66.7369 36.5349M5.62598 36.3885C5.62598 36.4375 5.62609 36.4864 5.62632 36.5353" stroke="white" stroke-width="2.19012" stroke-linecap="round" stroke-dasharray="0.11 10.26"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="64" height="64" href="data:image/png;base64,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"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="64" height="64" href="data:image/png;base64,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 truncated
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="64" height="64" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAOdEVYdFNvZnR3YXJlAEZpZ21hnrGWYwAAEbVJREFUeAHFW0l3FFeWvu9lSkIjEkgIJJASMQrbBRjjYx+MhfucOr3s7l9g6hzv7V73ouRNb8tee1H2P6he92BhsKuxAAkbg8VUCYh5kASaUcar74sbqYyIjEilBHLdcwRSTBn33u9+d3gvjayzuO8v9MqSOSHGO+REcsZ5OTGmlb+HrzMieXFu0hmbN0ZGxdlRybhRc+ztW7KOYmQdxH13YQD//it/4oquVozNXBLPO4ffvjYfvn1KXrO8NgO4b0daxXqfStZ+4gqF7bIOYhxQIplBybqh14WMVzbAsuLGfQZvt8pvIcbcdWK/ttZ99aqGeCUDuFMjULwwWFFx54m3uCTy8qXIUgE/+N3DHUte9LqshXMtMS9SmxVTW4OfWqn48gEizMDhb2SNsiYDwOs5lyn8Gb+eSL4ASs8viszOi1B552RNAjaUuhqRDbViG+rTL6MhsvbEWtCwagNU9DoVn54T4c9alU4TogPGMM2NYjKZ8vNOJvGJg5mBd76UVciqDOBODf/JGfNZ+Yl1VDxJaISGukRDuIL7PPPR0UGpUqo2QOHbH782Gftx/Lg3vyAyNY0LPPlNhYhobkgMDYeUmfnwyB+qeUxVBigM/ThqrD0Y/RR4/cWsev0fKU31YpoawJ02ctg4FFEDRw+vdPuKBkjyPPK8uGdTYPaCrEoWkQkmXmg2IHLwHJ/oslmf6ATMLy2NIjXZ1T0XaDDtrWUhUQ0SKhrAnTr/R2fcYOQYlX8yWR3kme4mYKgn+JkI7qnfINKAH6a4TOA1psZZGgT/E1X1dTBEk0jnZpGNTVKVpBjBiPnCfHjk39NuSzUA2d6ZwheRY9UqTw/ffSRyHz818Gp7G3IGFGlsUJKswUs+fa6epscj9y4pp/hGm/JrAunqFNm6WVaUNCQ4+SwtOyQagHleMoWRcKqrWvl7j0Vu3YOyIKeerXhCiyLB4qMuXwfUYYStm3DNXRzH9bt2KPwf49kPcW8zDNW7TZ/l4YLHEyK37+ur9qHC3rxRVm0EpEiTtYeS6oTEYPMyS0OATinPg/BWVJ6Fz9hN8AI8uL9PpK1Z4fzTGE4C6rkurf6WXqpiMyDPrBY5ariHIgf3wfsvNBxu3tb79uc0FMZx/jqOTQFNvV2l8IkL3tHnp82tJWI0cGTB+xq/fRS/vOwpgP7HUL43YhAqUkn5qRmR0TGF+KF+hTEJrhGxnoGSu+HlZ89VWWKOFWI30LGtQ4mRgheWyzdgmHk1xs5uvX8BxrgFBGxHGBzcC6Pys66owdME5OymZ6N6oWotnDr3afzSSAgQ+oH3lw3gzcJTk9OSKg+ewFvj6hXG6f2nCv/n0/rSPMds0QdYZ7Kq4FzwE7yZX//XwzjM6cjty8TpcF/rRjUSuYJGZQjQII/wOW/tKyEoSTa1iN1QF7bCpPEyO81HhyeLh6IhYL1Pwsoz7n0Yp8mjCYVlPyBfhxeZC5icRpvBfTcR591btB/I46UboFxjnYYHYW0D+5MYmRaJjL+Nq1+6OtQYV/NAyhY9vh1GvHZHZE+P3sfw+h1QEVYyLCBTx6YqFAqeXWIlO1i8ZBkBfoOT9c7gZbqLxzzCLc0AhOCFX/AyOfUKQ2Tsb4AuiCoPEtwHO87imodAyCYovKlVcz7TH2M/SfiMBRZWMMjDZ0ARPntLm6KJqfPOQ0UJGyQSLMnxEa47tD+9dkDZbGnIZUWjKChxgJWBsPIVvU+P/nwVHtmqcT8Gcr0CAtwBD40j9e3s0pcjoe0ic7ep8rR4DQyw5JT1Yz+mvkHDhH4hQnZv15ToBf1FF5DwHM/MguEfQ/FuhFg9DHL9jqQKkOi8EH+VUODLstk8s/S5CVGCo/fT5Na4Qr4nUHjLJv/JvsKEZx7Q3wIFmhqS72e6uzRWfnxrR5SVaDR6mkbgD9HA5zKsyDU3xhVpo79qOPJ8XGA8B94xIRQgJD6RIAx8BHCGF2d+WXgpiTINwzyCx/b2KlGBaOQJPvxBYAjGPeO3gzGekYpCBbd1yorSged2tGlx5UP/AfhmTrmEz8h1a9pcSinN4ygA0nVuuRwC3snw9T7zp6U9KrhjizZBzyaU5RFn8sZujdvt7T7Z2cYmsW0wwob0QQYNZd87rJBOEhqwZaPYFvBHW6samGRKo7Mo4t/kBRqH3EADJQlDaHExdsj7t5IBrByNnE3LsSSlBZzb2q5/0wis9MjmD54GTc0GwK1ZvF9viDf0AxyE8VbbpiC2YwLYOn9MFvecUSO2tvpc4p3/SQy5ZiM8XmNL15MHmD2YfRiO9x6losDFulYg/l981X329+SNyNmFpcSHyEN8QEuzMjnKTXlzt+bmjlZ9ke4O3wB+3r+Kombyubj/QWL59abYjW2aBq0p0zUiNCIVZ8F4BtPwnxHft++Ju8sM0KzeJOpYSU6+0OzwDCHZ2qz1x8Onye8Ow0TJ0OW4ZmElI5E+39HDLgH+vPkhIL+9o3SM7SvrdhIQYQjImoZGcaOXYZiAuT0a46Z4//2dmAVPTF0sJGIDJMvz1/Ii/3cGDdNE6cRPv6K+h/fnl0rK7+nVz+8OeIRofDolieIPYmOOdd4JyxWbyLGXKd5nuUtrNyYw+/SCWr8Ryo8/UJaPC3jFISS8i1cULRQwswnFv0Pj4337gwjCp0zgGHfxspiD/ZrzH02UX9PZppkoRQcXm1/AN4dhUhcxwHJtHpep58nKs0hpqfehb+tgoHyFnEy5iZrhel5JqzNAU/H/4RH1bprcua/vd2CvZqN4nVJXp06aSindX0Z1M8b02rKlq7Sh5nN8YEuCAdjkkJzAst7cjNjj76KD2y0rCmO/qCxTWYWxty/kDz6b9cfdB3qM06W4sGeYTivgogjA2CxnEYPR8XZaLmUaoXXjQhJkQ+K3uDPiTU6I2ZsT888DfkikiaEX0QB55AtKN2LZJrS4JM3dO8X+/piS6v+eRt3xTM8lFWsk6LQaJsZtGJm1MjflIkeT8j9RMbeoDF+mSSyHc3Ay8cxX3v7+uHg38ojpa2XzQ1aa9sA+cbMoUs4MayEVF+R+886bflPonR5Wtg/LXIKniZC0EIjrZoiAaqRIKrVBLp+ZDz0k5R6iYeIJ6ssuMf/0QXnFh9TmgQ8MoG0G3os2M/y9f4/YY0eQ/mCYoe/LlafMhzy9ENQuzExpPJYg1Rmg2IwwVkk8rL2LUmkhBOeYVQzjOzbcNAf2+BD2Tv1V01NtqEPEHNHAYH6R9PhJ6fMTnu8L32n4kv7th1H1izPVzZ+L4yfGOZuKI/3l5+LCYWgTyHHiuXh/vaDZIvzuqM/twQMIl0lxl65F2Z8pk3VAXy9C4Hfi2FIzNc7GMkSRM/x3ekMdRJhnqh+r8wl5SVI2okxWP6wIrfAAIr42wOvYBzQ06RD0zI9lyvuCdOmBzQ3jnJOdpH4eIeJOnfUnRoYZoKc7er4xlDnqg3dijZHWW8R1cyaPQl0mI4gxKUHNDyDU4tMXE7oZDGzhdQ8DVO/82VLBkyQ7d/gtsTd+T8yb/RoCSQUMvY5ewEF5g/Tq2DKzlebxJKMx16eNyWIGMOImrYkjoDZlWkNrz82XH+dCBw2DIoRdm8dByffDlZWnMM5PvOcXV6zwKhZAlNt3gQY0V6w5Du7XY+0JI/LpaR2mJomJGgALvXnUAS4fOZoGH1aBSemFQwjOA1AnOI68syvwahteOrdDK0bEq31zv5g9O/XcrpwumqQJ1wyYii8GJExjRDQCGqfn9bokiSGDm7HwtvZi5GDabK2tRSuseKHE7o2FB/P/9Asos0vnA3HhcwFhe+J9nRShZneYIXoYZBiuHMHAZl+fpszO9vL7ueDxNmqCy1cV/qz44uHIfiWrq8ZJUqabtTBAwQ5FX7SmDCq+EFZ8AFi9TDgO53gc6cxNTaFhiXbX0r4Jih3zFfMelBols3enX1t4bHlntKhxKIjMUTSob78VVXBPnzZqN4LFnZ5t5e/xGK1wc7MkCoLdlIW3G7WYjuYlxAOGrJAGQ46m7j8pP85e/MWc37LK2VGFWl9OjQllOPXhTEBO/X+0Tkex5NcIN26XjuXHkQJ/8Assv0BiuIB/bD/6i7MBWJsaFZFh4aD2KYqlbSlriDVR5QH/X7hU5rsabPiXyMmmlMaEw0h6KokLelDpvZj3WdjBo+bALjEohZkWPSp0q7xL5BKWu09ExAoXn/l/FnflOlDSJ/ajY+KNsQ6YVSbvz5V/PvsDEnJLSvzXR8PFc2aY/wdYz0QM4DcUSWFAgtzWriszYbl6S1d7+4LtgShcHHP4CKqzsyPlBUxR7tzVZfM0IfOfOeuXzHLlmh7r2xEspD4t8RFDg9Pp7pQBKxZKbayRs8b7xv+f//g7MK25Wzzph0FTSirhWgBRwAEoK0MugvgbHhbVOJzaUlgE3U8ZUhaF4VDs7NKEq0WXrylIGPdchOE9XBC5FBgFfYVPfO0pu/Vqo+THXWXm+NEhkVAvgINfRS5i2ktDwb4cqrQ7WgVy5EUP9HUpMghBLoxUK9WU7TZYGmenx1VqooAEyZRK5HE6tLM79XYT34OQyQwWfy2ZZsl+KZkCV0x8MxIFrmlD8urQJnxwJwjx0nVdsaUWbFjmgl1iXAY7CEOM5RMnzDYHZXZsLX8uFCyzBxdN+3o0hrkeQEOTcLniROL76aoiI219sCm2rc4A6cYNLf8ZvtadPv9H50pbYhxXVbhBoZAwJCH8/SXxel0k4WoQPcKMcPGqwpFze46qCdeiIcj6aS9LQRr1B6rMRFwU3dGpw1jmd6ZQlsJcreZxVp0cxuZSvA/FDLine truncated
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

+17 -17
View File
@@ -55,7 +55,7 @@ export default {
export const Default = { export const Default = {
args: { args: {
src: "assets/marketing/avatar-1.svg", src: "assets/marketing/avatar-1.png",
alt: "User Avatar", alt: "User Avatar",
size: "medium", size: "medium",
}, },
@@ -63,7 +63,7 @@ export const Default = {
export const Sizes = { export const Sizes = {
args: { args: {
src: "assets/marketing/avatar-1.svg", src: "assets/marketing/avatar-1.png",
alt: "User Avatar", alt: "User Avatar",
}, },
render: (args) => ( render: (args) => (
@@ -146,9 +146,9 @@ export const DifferentAvatars = {
render: (args) => ( render: (args) => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-x-4"> <div className="space-x-4">
<Avatar {...args} src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar {...args} src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar {...args} src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar {...args} src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar {...args} src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar {...args} src="assets/marketing/avatar-3.png" alt="User 3" />
</div> </div>
</div> </div>
), ),
@@ -168,36 +168,36 @@ export const AllSizesWithDifferentAvatars = {
<div> <div>
<h3 className="text-white font-semibold mb-3">Small Size</h3> <h3 className="text-white font-semibold mb-3">Small Size</h3>
<div className="space-x-4"> <div className="space-x-4">
<Avatar size="small" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="small" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="small" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="small" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="small" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="small" src="assets/marketing/avatar-3.png" alt="User 3" />
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Medium Size</h3> <h3 className="text-white font-semibold mb-3">Medium Size</h3>
<div className="space-x-4"> <div className="space-x-4">
<Avatar size="medium" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="medium" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="medium" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="medium" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="medium" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="medium" src="assets/marketing/avatar-3.png" alt="User 3" />
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Large Size</h3> <h3 className="text-white font-semibold mb-3">Large Size</h3>
<div className="space-x-4"> <div className="space-x-4">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="large" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="large" src="assets/marketing/avatar-3.png" alt="User 3" />
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">XLarge Size</h3> <h3 className="text-white font-semibold mb-3">XLarge Size</h3>
<div className="space-x-4"> <div className="space-x-4">
<Avatar size="xlarge" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="xlarge" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="xlarge" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="xlarge" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="xlarge" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="xlarge" src="assets/marketing/avatar-3.png" alt="User 3" />
</div> </div>
</div> </div>
</div> </div>
+38 -38
View File
@@ -33,9 +33,9 @@ export const Default = {
}, },
render: (args) => ( render: (args) => (
<AvatarContainer {...args}> <AvatarContainer {...args}>
<Avatar size={args.size} src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size={args.size} src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size={args.size} src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size={args.size} src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size={args.size} src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size={args.size} src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
), ),
}; };
@@ -47,36 +47,36 @@ export const Sizes = {
<div> <div>
<h3 className="text-white font-semibold mb-3">Small Size</h3> <h3 className="text-white font-semibold mb-3">Small Size</h3>
<AvatarContainer size="small"> <AvatarContainer size="small">
<Avatar size="small" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="small" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="small" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="small" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="small" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="small" src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Medium Size</h3> <h3 className="text-white font-semibold mb-3">Medium Size</h3>
<AvatarContainer size="medium"> <AvatarContainer size="medium">
<Avatar size="medium" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="medium" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="medium" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="medium" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="medium" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="medium" src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Large Size</h3> <h3 className="text-white font-semibold mb-3">Large Size</h3>
<AvatarContainer size="large"> <AvatarContainer size="large">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="large" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="large" src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">XLarge Size</h3> <h3 className="text-white font-semibold mb-3">XLarge Size</h3>
<AvatarContainer size="xlarge"> <AvatarContainer size="xlarge">
<Avatar size="xlarge" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="xlarge" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="xlarge" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="xlarge" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="xlarge" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="xlarge" src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
</div> </div>
</div> </div>
@@ -98,38 +98,38 @@ export const DifferentGroupSizes = {
<div> <div>
<h3 className="text-white font-semibold mb-3">2 Users</h3> <h3 className="text-white font-semibold mb-3">2 Users</h3>
<AvatarContainer size="large"> <AvatarContainer size="large">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
</AvatarContainer> </AvatarContainer>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">3 Users</h3> <h3 className="text-white font-semibold mb-3">3 Users</h3>
<AvatarContainer size="large"> <AvatarContainer size="large">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="large" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="large" src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">4 Users</h3> <h3 className="text-white font-semibold mb-3">4 Users</h3>
<AvatarContainer size="large"> <AvatarContainer size="large">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="large" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="large" src="assets/marketing/avatar-3.png" alt="User 3" />
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 4" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 4" />
</AvatarContainer> </AvatarContainer>
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">5 Users</h3> <h3 className="text-white font-semibold mb-3">5 Users</h3>
<AvatarContainer size="large"> <AvatarContainer size="large">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="large" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="large" src="assets/marketing/avatar-3.png" alt="User 3" />
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 4" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 4" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 5" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 5" />
</AvatarContainer> </AvatarContainer>
</div> </div>
</div> </div>
@@ -158,9 +158,9 @@ export const InContext = {
<h3 className="text-white font-semibold mb-4">Team Members</h3> <h3 className="text-white font-semibold mb-4">Team Members</h3>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<AvatarContainer size="medium"> <AvatarContainer size="medium">
<Avatar size="medium" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="medium" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="medium" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="medium" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="medium" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="medium" src="assets/marketing/avatar-3.png" alt="User 3" />
</AvatarContainer> </AvatarContainer>
<span className="text-white">3 team members</span> <span className="text-white">3 team members</span>
</div> </div>
@@ -172,10 +172,10 @@ export const InContext = {
</h3> </h3>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<AvatarContainer size="large"> <AvatarContainer size="large">
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="large" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="large" src="assets/marketing/avatar-2.png" alt="User 2" />
<Avatar size="large" src="assets/marketing/avatar-3.svg" alt="User 3" /> <Avatar size="large" src="assets/marketing/avatar-3.png" alt="User 3" />
<Avatar size="large" src="assets/marketing/avatar-1.svg" alt="User 4" /> <Avatar size="large" src="assets/marketing/avatar-1.png" alt="User 4" />
</AvatarContainer> </AvatarContainer>
<span className="text-white">4 contributors</span> <span className="text-white">4 contributors</span>
</div> </div>
@@ -185,8 +185,8 @@ export const InContext = {
<h3 className="text-white font-semibold mb-4">Small Team</h3> <h3 className="text-white font-semibold mb-4">Small Team</h3>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<AvatarContainer size="small"> <AvatarContainer size="small">
<Avatar size="small" src="assets/marketing/avatar-1.svg" alt="User 1" /> <Avatar size="small" src="assets/marketing/avatar-1.png" alt="User 1" />
<Avatar size="small" src="assets/marketing/avatar-2.svg" alt="User 2" /> <Avatar size="small" src="assets/marketing/avatar-2.png" alt="User 2" />
</AvatarContainer> </AvatarContainer>
<span className="text-white">2 members</span> <span className="text-white">2 members</span>
</div> </div>
+5 -5
View File
@@ -32,22 +32,22 @@ export default {
export const Default = { export const Default = {
args: { args: {
title: "How CommunityRule works", title: "How CommunityRule helps",
subtitle: "Here's a quick overview of the process, from start to finish.", subtitle: "Here's a quick overview of the process, from start to finish.",
headingDesktopLines: ["How", "CommunityRule", "helps"], headingDesktopLines: ["How", "CommunityRule", "helps"],
steps: [ steps: [
{ {
text: "Document how your community makes decisions", text: "See what has worked for other communities",
iconShape: "blob", iconShape: "blob",
iconColor: "green", iconColor: "green",
}, },
{ {
text: "Build an operating manual for a successful community", text: "Decide how you will decide, while things are calm",
iconShape: "gear", iconShape: "gear",
iconColor: "purple", 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", iconShape: "star",
iconColor: "orange", iconColor: "orange",
}, },
@@ -66,7 +66,7 @@ export const CustomContent = {
iconColor: "green", 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", iconShape: "gear",
iconColor: "purple", iconColor: "purple",
}, },
+1 -1
View File
@@ -38,7 +38,7 @@ export const Default = {
args: { args: {
title: "Tools for the parts that get hard", title: "Tools for the parts that get hard",
subtitle: 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: { parameters: {
docs: { docs: {
+5 -15
View File
@@ -8,7 +8,7 @@ export default {
docs: { docs: {
description: { description: {
component: 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" }, control: { type: "text" },
description: "The description 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"], tags: ["autodocs"],
}; };
export const Default = { export const Default = {
args: { args: {
title: "Collaborate", title: "Build structure",
subtitle: "with clarity", subtitle: "before you need it",
description: description:
"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.",
ctaText: "Learn how Community Rule works",
ctaHref: "#",
}, },
parameters: { parameters: {
docs: { docs: {
description: { description: {
story: story:
"Default hero banner with standard Community Rule messaging and branding.", "Default home hero with the team Rule card and breakpoint-specific CTAs.",
}, },
}, },
}, },
+7 -39
View File
@@ -7,49 +7,17 @@ export default {
layout: "fullscreen", layout: "fullscreen",
docs: { docs: {
description: { 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 ## Responsive Behavior
- **Mobile**: 3 rows × 2 columns grid with 38.4px gaps - **Mobile**: stacked, centered
- **SM**: 2 rows × 3 columns grid with 38.4px gaps - **SM+**: centered wrapping cluster
- **MD+**: Single space-between row (no wrap) - **MD+**: single centered row with a fixed gap
- **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\`.
## Props ## Props
- **logos** (optional): Array of logo objects with src, alt, and size properties. If not provided, uses default partner logos. - **logos** (optional): Array of logo objects with src, alt, and size properties. If not provided, uses the three pilot marks.`,
## Usage Examples
### Custom Logos
\`\`\`jsx
<LogoWall
logos={[
{
src: "assets/logos/partners/cu-boulder.svg",
alt: "CU Boulder",
size: "h-10 lg:h-12 xl:h-[60px]",
},
{
src: "assets/logos/partners/food-not-bombs.svg",
alt: "Food Not Bombs",
size: "h-11 lg:h-14 xl:h-[70px]",
}
]}
/>
\`\`\`
### Empty State
\`\`\`jsx
<LogoWall logos={[]} />
\`\`\`
This will fall back to the default partner logos.`,
}, },
}, },
}, },
@@ -58,7 +26,7 @@ This will fall back to the default partner logos.`,
logos: { logos: {
control: "object", control: "object",
description: 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: { docs: {
description: { description: {
story: 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.",
}, },
}, },
}, },
+3 -3
View File
@@ -40,9 +40,9 @@ componentTestSuite<AskOrganizerProps>(config);
describe("AskOrganizer (behavioral tests)", () => { describe("AskOrganizer (behavioral tests)", () => {
it("renders title", () => { it("renders title", () => {
render(<AskOrganizer title="Test Title" />); render(<AskOrganizer title="Test Title" />);
expect( const title = screen.getByRole("heading", { name: "Test Title" });
screen.getByRole("heading", { name: "Test Title" }), expect(title).toBeInTheDocument();
).toBeInTheDocument(); expect(title.tagName).toBe("H2");
}); });
it("renders subtitle when provided", () => { it("renders subtitle when provided", () => {
+3 -3
View File
@@ -15,7 +15,7 @@ import {
type AvatarProps = React.ComponentProps<typeof Avatar>; type AvatarProps = React.ComponentProps<typeof Avatar>;
const baseProps: AvatarProps = { const baseProps: AvatarProps = {
src: "/assets/marketing/avatar-1.svg", src: "/assets/marketing/avatar-1.png",
alt: "User avatar", alt: "User avatar",
size: "medium", size: "medium",
}; };
@@ -46,7 +46,7 @@ describe("Avatar (behavioral tests)", () => {
it("does not show initials while the image is loading", () => { it("does not show initials while the image is loading", () => {
render( render(
<Avatar <Avatar
src="/assets/marketing/avatar-1.svg" src="/assets/marketing/avatar-1.png"
alt="Ada Lovelace" alt="Ada Lovelace"
initials="AL" initials="AL"
/>, />,
@@ -57,7 +57,7 @@ describe("Avatar (behavioral tests)", () => {
it("applies the small photo ring after the image loads", () => { it("applies the small photo ring after the image loads", () => {
const { container } = render( const { container } = render(
<Avatar <Avatar
src="/assets/marketing/avatar-1.svg" src="/assets/marketing/avatar-1.png"
alt="User avatar" alt="User avatar"
size="small" size="small"
/>, />,
+5 -5
View File
@@ -38,9 +38,9 @@ componentTestSuite<FeatureGridProps>(config);
describe("FeatureGrid (behavioral tests)", () => { describe("FeatureGrid (behavioral tests)", () => {
it("renders title and subtitle", () => { it("renders title and subtitle", () => {
render(<FeatureGrid title="Test Title" subtitle="Test Subtitle" />); render(<FeatureGrid title="Test Title" subtitle="Test Subtitle" />);
expect( const title = screen.getByRole("heading", { name: "Test Title" });
screen.getByRole("heading", { name: "Test Title" }), expect(title).toBeInTheDocument();
).toBeInTheDocument(); expect(title.tagName).toBe("H2");
expect(screen.getByText("Test Subtitle")).toBeInTheDocument(); expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
expect( expect(
screen.queryByRole("heading", { name: "Test Subtitle" }), screen.queryByRole("heading", { name: "Test Subtitle" }),
@@ -58,10 +58,10 @@ describe("FeatureGrid (behavioral tests)", () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("sends Explore the toolkit to the Learn page", () => { it("sends Explore the patterns to the Learn page", () => {
render(<FeatureGrid title="Test" subtitle="Test" />); render(<FeatureGrid title="Test" subtitle="Test" />);
expect( expect(
screen.getByRole("link", { name: "Explore the toolkit" }), screen.getByRole("link", { name: "Explore the patterns" }),
).toHaveAttribute("href", "/learn"); ).toHaveAttribute("href", "/learn");
}); });
+16 -7
View File
@@ -20,8 +20,6 @@ const config: ComponentTestSuiteConfig<HeroBannerProps> = {
optionalProps: { optionalProps: {
subtitle: "Subtitle", subtitle: "Subtitle",
description: "Description", description: "Description",
ctaText: "CTA",
ctaHref: "/link",
}, },
primaryRole: "region", primaryRole: "region",
testCases: { testCases: {
@@ -50,18 +48,29 @@ describe("HeroBanner (behavioral tests)", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("renders hero image", () => { it("renders the team Rule card instead of a hero photograph", () => {
render(<HeroBanner title="Test" />); render(<HeroBanner title="Test" />);
expect( expect(
screen.getByRole("img", { name: "Hero illustration" }), screen.getByRole("heading", { name: "CommunityRule Creative Team" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(
screen.queryByRole("img", { name: "Hero illustration" }),
).not.toBeInTheDocument();
}); });
it("renders CTA with the inverse (black) filled palette", () => { it("sends Talk to a facilitator to the workshops page", () => {
render(<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />); render(<HeroBanner title="Test" />);
const cta = screen.getAllByRole("link", { name: "Get Started" })[0]; const cta = screen.getAllByRole("link", { name: "Talk to a facilitator" })[0];
expect(cta).toHaveAttribute("href", "/workshops");
expect(cta?.className).toContain( expect(cta?.className).toContain(
"bg-[var(--color-surface-default-primary)]", "bg-[var(--color-surface-default-primary)]",
); );
}); });
it("renders Create rule as a button for large screens", () => {
render(<HeroBanner title="Test" />);
expect(
screen.getAllByRole("button", { name: "Create rule" }).length,
).toBeGreaterThan(0);
});
}); });
+10 -2
View File
@@ -79,10 +79,18 @@ describe("Logo (behavioral tests)", () => {
expect(row?.className).toContain("h-[var(--space-1000)]"); 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(<Logo size="topNavFolderTop" wordmark />); render(<Logo size="topNavFolderTop" wordmark />);
const wordmark = screen.getByText("CommunityRule"); 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(<Logo size="topNavHeader" wordmark />);
const wordmark = screen.getByText("CommunityRule");
expect(wordmark.className).toContain("hidden");
expect(wordmark.className).toContain("sm:block");
}); });
it("renders with different size variants", () => { it("renders with different size variants", () => {
+18 -10
View File
@@ -131,7 +131,7 @@ describe("Top header chrome", () => {
); );
expect( expect(
screen.getAllByRole("menuitem", { screen.getAllByRole("menuitem", {
name: /navigate to use cases page/i, name: /navigate to workshops page/i,
hidden: true, hidden: true,
}), }),
).toHaveLength(1); ).toHaveLength(1);
@@ -146,7 +146,7 @@ describe("Top header chrome", () => {
name: /go to your profile/i, name: /go to your profile/i,
hidden: true, hidden: true,
}), }),
).toHaveLength(1); ).toHaveLength(2);
expect( expect(
screen.getAllByRole("button", { screen.getAllByRole("button", {
name: /create a new rule/i, name: /create a new rule/i,
@@ -157,11 +157,18 @@ describe("Top header chrome", () => {
} }
}); });
it("points Workshops at /workshops", () => {
renderWithProviders(<Top folderTop={false} />);
expect(
screen.getByRole("menuitem", { name: /navigate to workshops page/i }),
).toHaveAttribute("href", "/workshops");
});
it("shows Profile instead of Log in when signed in", () => { it("shows Profile instead of Log in when signed in", () => {
renderWithProviders(<Top folderTop={false} loggedIn />); renderWithProviders(<Top folderTop={false} loggedIn />);
expect( expect(
screen.getByRole("menuitem", { name: /go to your profile/i }), screen.getAllByRole("menuitem", { name: /go to your profile/i }).length,
).toBeInTheDocument(); ).toBeGreaterThan(0);
expect( expect(
screen.queryByRole("button", { name: /log in to your account/i }), screen.queryByRole("button", { name: /log in to your account/i }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
@@ -174,6 +181,7 @@ describe("Top header chrome", () => {
const headerNav = document.querySelector("header nav"); const headerNav = document.querySelector("header nav");
expect(logo?.className).not.toMatch(/\bz-20\b/); expect(logo?.className).not.toMatch(/\bz-20\b/);
expect(nav?.className).not.toMatch(/\babsolute\b/); expect(nav?.className).not.toMatch(/\babsolute\b/);
expect(headerNav?.className).toContain("flex");
expect(headerNav?.className).toContain( expect(headerNav?.className).toContain(
"md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]", "md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]",
); );
@@ -183,9 +191,8 @@ describe("Top header chrome", () => {
renderWithProviders(<Top folderTop />); renderWithProviders(<Top folderTop />);
const tab = document.querySelector(".HeaderTab"); const tab = document.querySelector(".HeaderTab");
const nav = document.querySelector("[data-top='nav']"); const nav = document.querySelector("[data-top='nav']");
expect(tab?.className).toContain("w-fit"); expect(tab?.className).toContain("flex-1");
expect(tab?.className).toContain("sm:w-auto"); expect(tab?.className).toContain("justify-between");
expect(tab?.className).toContain("sm:flex-1");
expect(nav?.className).toContain("md:absolute"); expect(nav?.className).toContain("md:absolute");
expect(nav?.className).toContain( expect(nav?.className).toContain(
"md:left-[calc(50vw-var(--space-400))]", "md:left-[calc(50vw-var(--space-400))]",
@@ -193,16 +200,17 @@ describe("Top header chrome", () => {
expect(nav?.className).not.toContain("ml-auto"); 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(<Top folderTop />); renderWithProviders(<Top folderTop />);
expect( expect(
screen.getAllByRole("menuitem", { screen.getAllByRole("menuitem", {
name: /navigate to use cases page/i, name: /navigate to workshops page/i,
hidden: true, hidden: true,
}), }),
).toHaveLength(1); ).toHaveLength(1);
const wordmark = screen.getByText("CommunityRule"); 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( expect(
screen.getByRole("button", { name: /create a new rule/i }).className, screen.getByRole("button", { name: /create a new rule/i }).className,
).toContain("md:text-x-small-label"); ).toContain("md:text-x-small-label");
+2 -2
View File
@@ -25,8 +25,8 @@ describe("TopWithPathname", () => {
it("renders Profile from the server session before /api/auth/session resolves", () => { it("renders Profile from the server session before /api/auth/session resolves", () => {
renderWithProviders(<TopWithPathname initialSignedIn />); renderWithProviders(<TopWithPathname initialSignedIn />);
expect( expect(
screen.getByRole("menuitem", { name: /go to your profile/i }), screen.getAllByRole("menuitem", { name: /go to your profile/i }).length,
).toBeInTheDocument(); ).toBeGreaterThan(0);
expect( expect(
screen.queryByRole("button", { name: /log in to your account/i }), screen.queryByRole("button", { name: /log in to your account/i }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
@@ -83,4 +83,27 @@ describe("ContentLockup", () => {
expect(link.className).not.toMatch(/focus:ring-2/); expect(link.className).not.toMatch(/focus:ring-2/);
expect(link.className).toMatch(/focus-visible:outline/); expect(link.className).toMatch(/focus-visible:outline/);
}); });
it("renders the title as h2 when headingLevel is h2", () => {
renderWithProviders(
<ContentLockup
variant="feature"
headingLevel="h2"
title="Tools for the parts that get hard"
/>,
);
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();
});
}); });
+22 -20
View File
@@ -7,40 +7,40 @@ test.describe("Critical User Journeys", () => {
test("complete user journey: learn about CommunityRule", async ({ page }) => { test("complete user journey: learn about CommunityRule", async ({ page }) => {
// 1. User lands on homepage // 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 // 2. User reads hero section
await expect( await expect(
page.locator("text=Help your community make important decisions"), page.locator("text=Strong communities agree on four things early"),
).toBeVisible(); ).toBeVisible();
// 3. User clicks CTA to learn more // 3. User clicks CTA to learn more
const learnCta = page const learnCta = page
.getByRole("link", { name: /Learn how CommunityRule works/i }) .getByRole("link", { name: /Talk to a facilitator/i })
.or(page.getByRole("button", { name: /Learn how CommunityRule works/i })) .or(page.getByRole("button", { name: /Talk to a facilitator/i }))
.first(); .first();
if ((await learnCta.count()) > 0 && (await learnCta.isVisible())) { if ((await learnCta.count()) > 0 && (await learnCta.isVisible())) {
await learnCta.click(); await learnCta.click();
await expect(page).toHaveURL(/\/how-it-works/); await expect(page).toHaveURL(/\/workshops/);
} }
// 4. User scrolls to CardSteps section (home) // 4. User scrolls to CardSteps section (home)
// Note: mobile title is one line; lg+ uses stacked "How / CommunityRule / helps" from home copy // Note: mobile title is one line; lg+ uses stacked "How / CommunityRule / helps" from home copy
const howItWorksHeading = page.locator( 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(); await expect(howItWorksHeading).toBeVisible();
// 5. User reads the process steps // 5. User reads the process steps
await expect( await expect(
page.locator("text=Document how your community makes decisions"), page.locator("text=See what has worked for other communities"),
).toBeVisible(); ).toBeVisible();
await expect( 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(); ).toBeVisible();
await expect( await expect(
page.locator( 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(); ).toBeVisible();
@@ -87,20 +87,22 @@ test.describe("Critical User Journeys", () => {
// Check main sections are present // Check main sections are present
await expect( await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }), page.locator("h1, h2").filter({ hasText: "Build structure" }),
).toBeVisible(); ).toBeVisible();
const howItWorksHeading = page.locator( 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(howItWorksHeading).toBeVisible();
await expect( 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(); ).toBeVisible();
// Check key components are rendered // 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('img[alt="Food Not Bombs"]')).toBeVisible();
await expect(page.locator("text=Jo Freeman")).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 }) => { test("feature grid section functionality", async ({ page }) => {
// Check section header // Check section header
await expect( 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(); ).toBeVisible();
await expect( await expect(
page.locator( 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(); ).toBeVisible();
@@ -127,12 +129,12 @@ test.describe("Critical User Journeys", () => {
featureSection.locator("text=Plan for conflict"), featureSection.locator("text=Plan for conflict"),
).toBeVisible(); ).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( await expect(
featureSection.locator('a[href="#decision-making"]'), featureSection.locator('a[href="#decision-making"]'),
).toHaveCount(0); ).toHaveCount(0);
await expect( await expect(
featureSection.getByRole("link", { name: "Explore the toolkit" }), featureSection.getByRole("link", { name: "Explore the patterns" }),
).toHaveAttribute("href", "/learn"); ).toHaveAttribute("href", "/learn");
}); });
@@ -186,19 +188,19 @@ test.describe("Critical User Journeys", () => {
// Test mobile viewport // Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 }); await page.setViewportSize({ width: 375, height: 667 });
await expect( await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }), page.locator("h1, h2").filter({ hasText: "Build structure" }),
).toBeVisible(); ).toBeVisible();
// Test tablet viewport // Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 }); await page.setViewportSize({ width: 768, height: 1024 });
await expect( await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }), page.locator("h1, h2").filter({ hasText: "Build structure" }),
).toBeVisible(); ).toBeVisible();
// Test desktop viewport // Test desktop viewport
await page.setViewportSize({ width: 1440, height: 900 }); await page.setViewportSize({ width: 1440, height: 900 });
await expect( await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }), page.locator("h1, h2").filter({ hasText: "Build structure" }),
).toBeVisible(); ).toBeVisible();
}); });
}); });
+17 -48
View File
@@ -18,7 +18,7 @@ test.describe("Edge Cases and Error Scenarios", () => {
await page.goto("/", { waitUntil: "domcontentloaded", timeout: 10000 }); await page.goto("/", { waitUntil: "domcontentloaded", timeout: 10000 });
// Page should still load eventually // Page should still load eventually
await expect(page.locator("text=Collaborate")).toBeVisible({ await expect(page.locator("text=Build structure")).toBeVisible({
timeout: 10000, timeout: 10000,
}); });
}); });
@@ -38,7 +38,7 @@ test.describe("Edge Cases and Error Scenarios", () => {
await expect(body).toBeVisible(); await expect(body).toBeVisible();
// Verify key content is still accessible // 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 }) => { test("handles JavaScript errors gracefully", async ({ page }) => {
@@ -59,30 +59,19 @@ test.describe("Edge Cases and Error Scenarios", () => {
}); });
// Page should continue to function // Page should continue to function
await expect(page.locator("text=Collaborate")).toBeVisible(); await expect(page.locator("text=Build structure")).toBeVisible();
const learnCta = page const createCta = page.getByRole("button", { name: "Create rule" });
.getByRole("link", { name: /Learn how CommunityRule works/i }) const facilitatorCta = page.getByRole("link", {
.or(page.getByRole("button", { name: /Learn how CommunityRule works/i })); name: /Talk to a facilitator/i,
const ctaCount = await learnCta.count(); });
let visibleCta = null; const visibleCreate = createCta.filter({ has: page.locator(":visible") });
if ((await visibleCreate.count()) > 0) {
for (let i = 0; i < ctaCount; i++) { await visibleCreate.first().click();
const cta = learnCta.nth(i); await expect(page).toHaveURL(/\/create/);
if (await cta.isVisible()) { } else {
visibleCta = cta; await expect(facilitatorCta.first()).toBeVisible();
break;
}
} }
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 }) => { test("handles missing images gracefully", async ({ page }) => {
@@ -96,29 +85,9 @@ test.describe("Edge Cases and Error Scenarios", () => {
await page.goto("/", { waitUntil: "domcontentloaded" }); await page.goto("/", { waitUntil: "domcontentloaded" });
// Page should still function without images // Page should still function without images
await expect(page.locator("text=Collaborate")).toBeVisible(); await expect(page.locator("text=Build structure")).toBeVisible();
await expect(
const learnCta = page page.getByRole("heading", { name: "CommunityRule Creative Team" }),
.getByRole("link", { name: /Learn how CommunityRule works/i }) ).toBeVisible();
.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/);
}); });
}); });
+30 -30
View File
@@ -11,11 +11,11 @@ describe("Page", () => {
render(<Page />); render(<Page />);
// Check that all main sections are rendered (using getAllByText since there are multiple instances) // Check that all main sections are rendered (using getAllByText since there are multiple instances)
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0);
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0); expect(screen.getAllByText("before you need it").length).toBeGreaterThan(0);
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
@@ -23,7 +23,7 @@ describe("Page", () => {
// Check CardSteps section (using getAllByText since there are multiple instances) // Check CardSteps section (using getAllByText since there are multiple instances)
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("How CommunityRule works").length, screen.getAllByText("How CommunityRule helps").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
@@ -41,7 +41,7 @@ describe("Page", () => {
}); });
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
@@ -58,20 +58,20 @@ describe("Page", () => {
render(<Page />); render(<Page />);
// Check hero banner content (using getAllByText since there are multiple instances) // Check hero banner content (using getAllByText since there are multiple instances)
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0);
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0); expect(screen.getAllByText("before you need it").length).toBeGreaterThan(0);
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
expect( expect(
screen.getAllByText("Learn how CommunityRule works").length, screen.getAllByText("Talk to a facilitator").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
const learnLinks = screen.getAllByRole("link", { 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 () => { test("renders CardSteps section with correct data", async () => {
@@ -80,7 +80,7 @@ describe("Page", () => {
// Wait for dynamically imported CardSteps component to load // Wait for dynamically imported CardSteps component to load
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("How CommunityRule works").length, screen.getAllByText("How CommunityRule helps").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
@@ -91,16 +91,16 @@ describe("Page", () => {
// Check individual card content (using getAllByText since there are multiple instances) // Check individual card content (using getAllByText since there are multiple instances)
expect( expect(
screen.getAllByText("Document how your community makes decisions").length, screen.getAllByText("See what has worked for other communities").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
expect( expect(
screen.getAllByText( screen.getAllByText(
"Build an operating manual for a successful community", "Decide how you will decide, while things are calm",
).length, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -117,11 +117,11 @@ describe("Page", () => {
}); });
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
const learnMore = screen.getAllByRole("link", { const learnMore = screen.getAllByRole("link", {
name: "Explore the toolkit", name: "Explore the patterns",
}); });
expect(learnMore[0]).toHaveAttribute("href", "/learn"); expect(learnMore[0]).toHaveAttribute("href", "/learn");
}); });
@@ -144,14 +144,14 @@ describe("Page", () => {
// Check that all major components are present by looking for their content // Check that all major components are present by looking for their content
// HeroBanner // HeroBanner
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); expect(screen.getAllByText("Build structure").length).toBeGreaterThan(0);
// Wait for dynamically imported components to load // Wait for dynamically imported components to load
// LogoWall - should be present (even if just the component structure) // LogoWall - should be present (even if just the component structure)
// CardSteps // CardSteps
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("How CommunityRule works").length, screen.getAllByText("How CommunityRule helps").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -174,7 +174,7 @@ describe("Page", () => {
test("has correct page structure", () => { test("has correct page structure", () => {
render(<Page />); render(<Page />);
const mainContainer = screen.getAllByText("Collaborate")[0].closest("div"); const mainContainer = screen.getAllByText("Build structure")[0].closest("div");
expect(mainContainer).toBeInTheDocument(); expect(mainContainer).toBeInTheDocument();
}); });
@@ -183,7 +183,7 @@ describe("Page", () => {
// Check CTA button in hero banner // Check CTA button in hero banner
expect( expect(
screen.getAllByText("Learn how CommunityRule works").length, screen.getAllByText("Talk to a facilitator").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
// Check CTA button in ask organizer section // Check CTA button in ask organizer section
@@ -196,7 +196,7 @@ describe("Page", () => {
// Check main description (using getAllByText since there are multiple instances) // Check main description (using getAllByText since there are multiple instances)
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
@@ -213,7 +213,7 @@ describe("Page", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -228,12 +228,12 @@ describe("Page", () => {
render(<Page />); render(<Page />);
// Check all section titles (using getAllByText since there are multiple instances) // 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 // Wait for dynamically imported components
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("How CommunityRule works").length, screen.getAllByText("How CommunityRule helps").length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
await waitFor(() => { await waitFor(() => {
@@ -254,18 +254,18 @@ describe("Page", () => {
await waitFor(() => { await waitFor(() => {
// Three Step tiles inside CardSteps // Three Step tiles inside CardSteps
expect( expect(
screen.getAllByText("Document how your community makes decisions") screen.getAllByText("See what has worked for other communities")
.length, .length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
screen.getAllByText( screen.getAllByText(
"Build an operating manual for a successful community", "Decide how you will decide, while things are calm",
).length, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -274,7 +274,7 @@ describe("Page", () => {
render(<Page />); render(<Page />);
// Check subtitles (using getAllByText since there are multiple instances) // 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 // Wait for dynamically imported components
await waitFor(() => { await waitFor(() => {
@@ -287,7 +287,7 @@ describe("Page", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText( 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, ).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
+28 -28
View File
@@ -28,33 +28,31 @@ describe("Page Flow Integration", () => {
// Hero Banner section // Hero Banner section
expect( expect(
screen.getByRole("heading", { name: "Collaborate" }), screen.getByRole("heading", { name: "Build structure" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("heading", { name: "with clarity" }), screen.getByRole("heading", { name: "before you need it" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText( 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(); ).toBeInTheDocument();
const ctaButtons = screen.getAllByRole("link", { const ctaButtons = screen.getAllByRole("link", {
name: "Learn how CommunityRule works", name: "Talk to a facilitator",
}); });
expect(ctaButtons.length).toBeGreaterThan(0); expect(ctaButtons.length).toBeGreaterThan(0);
await waitFor(() => { await waitFor(() => {
expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
}); });
expect(screen.getByAltText("Start COOP")).toBeInTheDocument(); expect(screen.getByAltText("Boulder Street Medics")).toBeInTheDocument();
expect(screen.getByAltText("Metagov")).toBeInTheDocument(); expect(screen.getByAltText("Mutual Aid Colorado")).toBeInTheDocument();
expect(screen.getByAltText("Open Civics")).toBeInTheDocument(); expect(screen.queryByAltText("Start COOP")).not.toBeInTheDocument();
expect(screen.getByAltText("Mutual Aid CO")).toBeInTheDocument();
expect(screen.getByAltText("CU Boulder")).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByRole("heading", { name: /How CommunityRule works/ }), screen.getByRole("heading", { name: /How CommunityRule helps/ }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
expect( expect(
@@ -63,25 +61,27 @@ describe("Page Flow Integration", () => {
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText("Document how your community makes decisions"), screen.getByText("See what has worked for other communities"),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText("Build an operating manual for a successful community"), screen.getByText("Decide how you will decide, while things are calm"),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText( 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(); ).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole("heading", { name: "Consensus" })).toBeInTheDocument(); expect(
screen.getByRole("heading", { name: "Devolution" }),
).toBeInTheDocument();
}); });
expect( expect(
screen.getByRole("heading", { name: "Do-ocracy" }), screen.getByRole("heading", { name: "Consensus" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("heading", { name: "Devolution" }), screen.getByRole("heading", { name: "Do-ocracy" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("heading", { name: "Quadratic Governance" }), screen.getByRole("heading", { name: "Quadratic Governance" }),
@@ -96,7 +96,7 @@ describe("Page Flow Integration", () => {
}); });
expect( expect(
screen.getByText( 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(); ).toBeInTheDocument();
@@ -120,11 +120,11 @@ describe("Page Flow Integration", () => {
renderPage(); renderPage();
const ctaLinks = screen.getAllByRole("link", { const ctaLinks = screen.getAllByRole("link", {
name: "Learn how CommunityRule works", name: "Talk to a facilitator",
}); });
const ctaLink = ctaLinks[0]; const ctaLink = ctaLinks[0];
expect(ctaLink).toBeInTheDocument(); expect(ctaLink).toBeInTheDocument();
expect(ctaLink).toHaveAttribute("href", "/how-it-works"); expect(ctaLink).toHaveAttribute("href", "/workshops");
await user.click(ctaLink); await user.click(ctaLink);
expect(ctaLink).toBeInTheDocument(); expect(ctaLink).toBeInTheDocument();
@@ -135,13 +135,13 @@ describe("Page Flow Integration", () => {
await waitFor(() => { await waitFor(() => {
const cards = screen.getAllByText( 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); expect(cards.length).toBeGreaterThan(0);
}); });
const cards = screen.getAllByText( 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); expect(cards.length).toBeGreaterThan(0);
@@ -153,11 +153,11 @@ describe("Page Flow Integration", () => {
renderPage(); renderPage();
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Consensus")).toBeInTheDocument(); expect(screen.getByText("Devolution")).toBeInTheDocument();
}); });
expect(screen.queryByText("Solidarity Network")).not.toBeInTheDocument(); expect(screen.queryByText("Solidarity Network")).not.toBeInTheDocument();
expect(screen.getByText("Do-ocracy")).toBeInTheDocument(); expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0);
expect(screen.getByText("Devolution")).toBeInTheDocument(); expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0);
expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); expect(screen.getByText("Quadratic Governance")).toBeInTheDocument();
const seeAll = screen.getByRole("link", { name: "See all templates" }); const seeAll = screen.getByRole("link", { name: "See all templates" });
@@ -189,7 +189,7 @@ describe("Page Flow Integration", () => {
expect(headings.length).toBeGreaterThan(4); expect(headings.length).toBeGreaterThan(4);
const mainContent = screen.getByText( const mainContent = screen.getByText(
/Help your community make important decisions/, /Strong communities agree on four things early/,
); );
expect(mainContent).toBeInTheDocument(); expect(mainContent).toBeInTheDocument();
}); });
@@ -220,15 +220,15 @@ describe("Page Flow Integration", () => {
renderPage(); renderPage();
expect( expect(
screen.getByText(/Help your community make important decisions/), screen.getByText(/Strong communities agree on four things early/),
).toBeInTheDocument(); ).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument(); expect(screen.getByText("How CommunityRule helps")).toBeInTheDocument();
}); });
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Consensus")).toBeInTheDocument(); expect(screen.getByText("Devolution")).toBeInTheDocument();
}); });
await waitFor(() => { await waitFor(() => {
+15 -18
View File
@@ -38,16 +38,16 @@ describe("User Journey Integration", () => {
renderPageWithFooter(); renderPageWithFooter();
expect( expect(
screen.getByText(/Help your community make important decisions/), screen.getByText(/Strong communities agree on four things early/),
).toBeInTheDocument(); ).toBeInTheDocument();
const learnLinks = screen.getAllByRole("link", { const learnLinks = screen.getAllByRole("link", {
name: "Learn how CommunityRule works", name: "Talk to a facilitator",
}); });
await user.click(learnLinks[0]); await user.click(learnLinks[0]);
await waitFor(() => { 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(); renderPage();
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Consensus")).toBeInTheDocument(); expect(screen.getByText("Devolution")).toBeInTheDocument();
}); });
expect(screen.getByText("Do-ocracy")).toBeInTheDocument(); expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0);
expect(screen.getByText("Devolution")).toBeInTheDocument(); expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0);
expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); expect(screen.getByText("Quadratic Governance")).toBeInTheDocument();
const seeHowLinks = screen.getAllByRole("link", { const seeHowLinks = screen.getAllByRole("link", {
@@ -109,15 +109,15 @@ describe("User Journey Integration", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("Document how your community makes decisions"), screen.getByText("See what has worked for other communities"),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
expect( expect(
screen.getByText("Build an operating manual for a successful community"), screen.getByText("Decide how you will decide, while things are calm"),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText( 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(); ).toBeInTheDocument();
@@ -156,7 +156,7 @@ describe("User Journey Integration", () => {
}); });
expect( expect(
screen.getByText( 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(); ).toBeInTheDocument();
@@ -173,11 +173,8 @@ describe("User Journey Integration", () => {
const partnerLogos = logoImages.filter( const partnerLogos = logoImages.filter(
(img) => (img) =>
img.alt?.includes("Food Not Bombs") || img.alt?.includes("Food Not Bombs") ||
img.alt?.includes("Start COOP") || img.alt?.includes("Boulder Street Medics") ||
img.alt?.includes("Metagov") || img.alt?.includes("Mutual Aid Colorado"),
img.alt?.includes("Open Civics") ||
img.alt?.includes("Mutual Aid CO") ||
img.alt?.includes("CU Boulder"),
); );
expect(partnerLogos.length).toBeGreaterThan(0); expect(partnerLogos.length).toBeGreaterThan(0);
}); });
@@ -193,11 +190,11 @@ describe("User Journey Integration", () => {
test("user completes the full journey from discovery to action", async () => { test("user completes the full journey from discovery to action", async () => {
renderPageWithFooter(); renderPageWithFooter();
expect(screen.getByText("Collaborate")).toBeInTheDocument(); expect(screen.getByText("Build structure")).toBeInTheDocument();
expect(screen.getByText("with clarity")).toBeInTheDocument(); expect(screen.getByText("before you need it")).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument(); expect(screen.getByText("How CommunityRule helps")).toBeInTheDocument();
}); });
await waitFor(() => { await waitFor(() => {
+23 -35
View File
@@ -7,16 +7,16 @@ afterEach(() => {
cleanup(); cleanup();
}); });
describe("LogoWall Component", () => { describe("LogoWall Component", () => {
test("renders with default logos", () => { test("renders the three pilot marks", () => {
render(<LogoWall />); render(<LogoWall />);
// Check for default logos expect(screen.getByAltText("Boulder Street Medics")).toBeInTheDocument();
expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
expect(screen.getByAltText("Start COOP")).toBeInTheDocument(); expect(screen.getByAltText("Mutual Aid Colorado")).toBeInTheDocument();
expect(screen.getByAltText("Metagov")).toBeInTheDocument(); expect(screen.queryByAltText("Start COOP")).not.toBeInTheDocument();
expect(screen.getByAltText("Open Civics")).toBeInTheDocument(); expect(screen.queryByAltText("Metagov")).not.toBeInTheDocument();
expect(screen.getByAltText("Mutual Aid CO")).toBeInTheDocument(); expect(screen.queryByAltText("Open Civics")).not.toBeInTheDocument();
expect(screen.getByAltText("CU Boulder")).toBeInTheDocument(); expect(screen.queryByAltText("CU Boulder")).not.toBeInTheDocument();
}); });
test("renders with custom logos", () => { test("renders with custom logos", () => {
@@ -25,13 +25,11 @@ describe("LogoWall Component", () => {
src: "assets/custom1.png", src: "assets/custom1.png",
alt: "Custom Logo 1", alt: "Custom Logo 1",
size: "h-8", size: "h-8",
order: "order-1",
}, },
{ {
src: "assets/custom2.png", src: "assets/custom2.png",
alt: "Custom Logo 2", alt: "Custom Logo 2",
size: "h-10", size: "h-10",
order: "order-2",
}, },
]; ];
@@ -62,21 +60,20 @@ describe("LogoWall Component", () => {
); );
}); });
test("applies responsive grid layout", () => { test("lays out the three marks as a centered cluster", () => {
render(<LogoWall />); render(<LogoWall />);
const grid = document.querySelector( const row = document.querySelector('[class*="flex w-full flex-col"]');
'[class*="grid grid-cols-2 grid-rows-3"]', expect(row).toBeInTheDocument();
); expect(row).toHaveClass(
expect(grid).toBeInTheDocument(); "justify-center",
expect(grid).toHaveClass( "sm:flex-row",
"sm:grid-cols-3", "sm:justify-center",
"sm:grid-rows-2",
"md:flex",
"md:justify-between",
"md:flex-nowrap", "md:flex-nowrap",
"gap-[38.4px]", "gap-[38.4px]",
); );
expect(row).not.toHaveClass("md:justify-between");
expect(row).not.toHaveClass("sm:justify-around");
}); });
test("renders logos with correct attributes", () => { test("renders logos with correct attributes", () => {
@@ -97,7 +94,6 @@ describe("LogoWall Component", () => {
test("handles empty logos array", () => { test("handles empty logos array", () => {
render(<LogoWall logos={[]} />); render(<LogoWall logos={[]} />);
// Should fall back to default logos
expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
}); });
@@ -112,15 +108,14 @@ describe("LogoWall Component", () => {
).toBe(0); ).toBe(0);
}); });
test("renders with proper semantic structure", () => { test("renders with proper semantic structure and no heading", () => {
render(<LogoWall />); render(<LogoWall />);
const section = document.querySelector("section"); const section = document.querySelector("section");
expect(section).toBeInTheDocument(); expect(section).toBeInTheDocument();
expect( expect(
screen.getByText("Trusted by leading cooperators"), screen.queryByText("Trusted by leading cooperators"),
).toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
test("renders partner logos at Figma 60% opacity", () => { test("renders partner logos at Figma 60% opacity", () => {
@@ -136,22 +131,15 @@ describe("LogoWall Component", () => {
const logos = screen.getAllByRole("img"); const logos = screen.getAllByRole("img");
logos.forEach((logo) => { logos.forEach((logo) => {
// Next.js Image attributes are not rendered as HTML attributes in JSDOM
// Just verify the images are present
expect(logo).toBeInTheDocument(); expect(logo).toBeInTheDocument();
}); });
}); });
test("prioritizes first two logos", () => { test("marks the section with the Figma node id", () => {
render(<LogoWall />); render(<LogoWall />);
const logos = screen.getAllByRole("img"); expect(
const foodNotBombsLogo = logos.find((img) => img.alt === "Food Not Bombs"); document.querySelector('[data-figma-node="18483-18425"]'),
const startCoopLogo = logos.find((img) => img.alt === "Start COOP"); ).toBeInTheDocument();
// 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();
}); });
}); });
+3 -1
View File
@@ -90,7 +90,9 @@ describe("RuleStack Component", () => {
test("defaults to home rule stack heading copy when namespace omitted", () => { test("defaults to home rule stack heading copy when namespace omitted", () => {
render(<RuleStack initialGridEntries={homeFeatured} />); render(<RuleStack initialGridEntries={homeFeatured} />);
const heading = screen.getByRole("heading", { level: 2 }); 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 () => { test("renders four featured governance template cards on the home row", async () => {