From 2101676429125eb139cc486572d27f0b622eb61b Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:59:36 -0700 Subject: [PATCH 1/2] Rebuild the About page from the current marketing frames, and leave the unapproved sentences off the page. Co-authored-by: Cursor --- app/(marketing)/about/page.tsx | 90 ++---- .../CenteredHeader.container.tsx | 26 ++ .../CenteredHeader/CenteredHeader.types.ts | 12 + .../CenteredHeader/CenteredHeader.view.tsx | 103 +++++++ .../sections/CenteredHeader/index.tsx | 6 + .../sections/Credits/Credits.container.tsx | 18 ++ .../sections/Credits/Credits.types.ts | 9 + .../sections/Credits/Credits.view.tsx | 42 +++ app/components/sections/Credits/index.tsx | 2 + .../FourQuestions/FourQuestions.container.tsx | 29 ++ .../FourQuestions/FourQuestions.types.ts | 29 ++ .../FourQuestions/FourQuestions.view.tsx | 98 +++++++ .../sections/FourQuestions/index.tsx | 8 + .../sections/History/History.container.tsx | 21 ++ .../sections/History/History.types.ts | 24 ++ .../sections/History/History.view.tsx | 82 ++++++ app/components/sections/History/index.tsx | 2 + .../NarrativeBlock.container.tsx | 37 +++ .../NarrativeBlock/NarrativeBlock.types.ts | 10 + .../NarrativeBlock/NarrativeBlock.view.tsx | 77 ++++++ .../sections/NarrativeBlock/index.tsx | 2 + .../ParticipantSelection.container.tsx | 18 ++ .../ParticipantSelection.types.ts | 6 + .../ParticipantSelection.view.tsx | 89 ++++++ .../sections/ParticipantSelection/index.tsx | 2 + .../QuoteBlock/QuoteBlock.container.tsx | 2 + .../sections/QuoteBlock/QuoteBlock.types.ts | 6 + .../sections/QuoteBlock/QuoteBlock.view.tsx | 21 +- .../RebuildProcess.container.tsx | 18 ++ .../RebuildProcess/RebuildProcess.types.ts | 4 + .../RebuildProcess/RebuildProcess.view.tsx | 28 ++ .../sections/RebuildProcess/index.tsx | 2 + .../sections/YourTurn/YourTurn.container.tsx | 67 +++++ .../sections/YourTurn/YourTurn.types.ts | 15 + .../sections/YourTurn/YourTurn.view.tsx | 102 +++++++ app/components/sections/YourTurn/index.tsx | 2 + docs/figma-component-registry.md | 18 +- messages/en/metadata.json | 3 +- messages/en/pages/about.json | 259 ++++++++++++------ public/assets/shapes/about-header-arrow.svg | 3 + public/assets/shapes/about-header-ring.svg | 5 + .../shapes/about-participant-circles.svg | 5 + .../shapes/about-participant-clover.svg | 5 + .../assets/shapes/about-participant-rings.svg | 5 + stories/sections/Accordion.stories.js | 15 +- stories/sections/Book.stories.js | 23 +- stories/sections/CenteredHeader.stories.js | 12 + stories/sections/Credits.stories.js | 12 + stories/sections/FourQuestions.stories.js | 12 + stories/sections/History.stories.js | 12 + stories/sections/NarrativeBlock.stories.js | 12 + .../sections/ParticipantSelection.stories.js | 12 + stories/sections/RebuildProcess.stories.js | 12 + stories/sections/YourTurn.stories.js | 12 + stories/type/TripleTextBlock.stories.js | 28 +- tests/pages/about.test.tsx | 179 ++++++++---- 56 files changed, 1515 insertions(+), 238 deletions(-) create mode 100644 app/components/sections/CenteredHeader/CenteredHeader.container.tsx create mode 100644 app/components/sections/CenteredHeader/CenteredHeader.types.ts create mode 100644 app/components/sections/CenteredHeader/CenteredHeader.view.tsx create mode 100644 app/components/sections/CenteredHeader/index.tsx create mode 100644 app/components/sections/Credits/Credits.container.tsx create mode 100644 app/components/sections/Credits/Credits.types.ts create mode 100644 app/components/sections/Credits/Credits.view.tsx create mode 100644 app/components/sections/Credits/index.tsx create mode 100644 app/components/sections/FourQuestions/FourQuestions.container.tsx create mode 100644 app/components/sections/FourQuestions/FourQuestions.types.ts create mode 100644 app/components/sections/FourQuestions/FourQuestions.view.tsx create mode 100644 app/components/sections/FourQuestions/index.tsx create mode 100644 app/components/sections/History/History.container.tsx create mode 100644 app/components/sections/History/History.types.ts create mode 100644 app/components/sections/History/History.view.tsx create mode 100644 app/components/sections/History/index.tsx create mode 100644 app/components/sections/NarrativeBlock/NarrativeBlock.container.tsx create mode 100644 app/components/sections/NarrativeBlock/NarrativeBlock.types.ts create mode 100644 app/components/sections/NarrativeBlock/NarrativeBlock.view.tsx create mode 100644 app/components/sections/NarrativeBlock/index.tsx create mode 100644 app/components/sections/ParticipantSelection/ParticipantSelection.container.tsx create mode 100644 app/components/sections/ParticipantSelection/ParticipantSelection.types.ts create mode 100644 app/components/sections/ParticipantSelection/ParticipantSelection.view.tsx create mode 100644 app/components/sections/ParticipantSelection/index.tsx create mode 100644 app/components/sections/RebuildProcess/RebuildProcess.container.tsx create mode 100644 app/components/sections/RebuildProcess/RebuildProcess.types.ts create mode 100644 app/components/sections/RebuildProcess/RebuildProcess.view.tsx create mode 100644 app/components/sections/RebuildProcess/index.tsx create mode 100644 app/components/sections/YourTurn/YourTurn.container.tsx create mode 100644 app/components/sections/YourTurn/YourTurn.types.ts create mode 100644 app/components/sections/YourTurn/YourTurn.view.tsx create mode 100644 app/components/sections/YourTurn/index.tsx create mode 100644 public/assets/shapes/about-header-arrow.svg create mode 100644 public/assets/shapes/about-header-ring.svg create mode 100644 public/assets/shapes/about-participant-circles.svg create mode 100644 public/assets/shapes/about-participant-clover.svg create mode 100644 public/assets/shapes/about-participant-rings.svg create mode 100644 stories/sections/CenteredHeader.stories.js create mode 100644 stories/sections/Credits.stories.js create mode 100644 stories/sections/FourQuestions.stories.js create mode 100644 stories/sections/History.stories.js create mode 100644 stories/sections/NarrativeBlock.stories.js create mode 100644 stories/sections/ParticipantSelection.stories.js create mode 100644 stories/sections/RebuildProcess.stories.js create mode 100644 stories/sections/YourTurn.stories.js diff --git a/app/(marketing)/about/page.tsx b/app/(marketing)/about/page.tsx index 8ce856f..61e2af4 100644 --- a/app/(marketing)/about/page.tsx +++ b/app/(marketing)/about/page.tsx @@ -1,86 +1,50 @@ import type { Metadata } from "next"; import messages from "../../../messages/en/index"; -import { - ASSETS, - getAssetPath, - governanceBookletPath, - structureBeforeCrisisPath, -} from "../../../lib/assetUtils"; import { getTranslation } from "../../../lib/i18n/getTranslation"; import { routeMetadata } from "../../../lib/siteMetadata"; -import AboutHeader from "../../components/type/AboutHeader"; -import type { AboutHeaderSegment } from "../../components/type/AboutHeader"; -import Stats from "../../components/sections/Stats"; -import type { StatItem } from "../../components/sections/Stats"; -import TripleTextBlock from "../../components/type/TripleTextBlock"; -import type { TripleTextBlockColumn } from "../../components/type/TripleTextBlock"; -import Book from "../../components/sections/Book"; -import FaqAccordion from "../../components/sections/Accordion"; -import type { FaqAccordionItem } from "../../components/sections/Accordion"; +import CenteredHeader from "../../components/sections/CenteredHeader"; +import History from "../../components/sections/History"; +import ParticipantSelection from "../../components/sections/ParticipantSelection"; +import FourQuestions from "../../components/sections/FourQuestions"; +import RebuildProcess from "../../components/sections/RebuildProcess"; +import NarrativeBlock from "../../components/sections/NarrativeBlock"; +import YourTurn from "../../components/sections/YourTurn"; +import Credits from "../../components/sections/Credits"; import QuoteBlock from "../../components/sections/QuoteBlock"; import AskOrganizer from "../../components/sections/AskOrganizer"; export const metadata: Metadata = routeMetadata("/about", { title: messages.metadata.about.title, + description: messages.metadata.about.description, }); -function asArray(value: unknown): T[] { - return Array.isArray(value) ? value : []; -} - export default function AboutPage() { const t = (key: string) => getTranslation(messages, key); - const page = messages.pages.about; - const headerSegments = asArray(page.aboutHeader.segments); - const statsItems = asArray(page.stats.items); - - const faqItems = asArray(page.faq.items); - const tripleColumns = asArray(page.tripleTextBlock.columns); - - const askOrganizerData = { - title: t("pages.home.askOrganizer.title"), - subtitle: t("pages.home.askOrganizer.subtitle"), - buttonText: t("pages.home.askOrganizer.buttonText"), - }; - return (
- - - - - - + -
- + + + + + + + +
+
); diff --git a/app/components/sections/CenteredHeader/CenteredHeader.container.tsx b/app/components/sections/CenteredHeader/CenteredHeader.container.tsx new file mode 100644 index 0000000..8be3c41 --- /dev/null +++ b/app/components/sections/CenteredHeader/CenteredHeader.container.tsx @@ -0,0 +1,26 @@ +"use client"; + +/** + * Figma: "Section / Centered Header" (24214:25892) + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import CenteredHeaderView from "./CenteredHeader.view"; +import type { CenteredHeaderSegment } from "./CenteredHeader.types"; + +const CenteredHeaderContainer = memo(() => { + const copy = useMessages().pages.about.centeredHeader; + return ( + + ); +}); + +CenteredHeaderContainer.displayName = "CenteredHeader"; + +export default CenteredHeaderContainer; diff --git a/app/components/sections/CenteredHeader/CenteredHeader.types.ts b/app/components/sections/CenteredHeader/CenteredHeader.types.ts new file mode 100644 index 0000000..08f62b3 --- /dev/null +++ b/app/components/sections/CenteredHeader/CenteredHeader.types.ts @@ -0,0 +1,12 @@ +export type CenteredHeaderIcon = "arrow" | "ring"; + +export type CenteredHeaderSegment = + | { type: "word"; text: string } + | { type: "icon"; icon: CenteredHeaderIcon }; + +export interface CenteredHeaderViewProps { + accessibleName: string; + segments: CenteredHeaderSegment[]; + subhead: string; + credit: string; +} diff --git a/app/components/sections/CenteredHeader/CenteredHeader.view.tsx b/app/components/sections/CenteredHeader/CenteredHeader.view.tsx new file mode 100644 index 0000000..de72c4e --- /dev/null +++ b/app/components/sections/CenteredHeader/CenteredHeader.view.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { memo, type ReactNode } from "react"; +import { getAssetPath } from "../../../../lib/assetUtils"; +import type { + CenteredHeaderIcon, + CenteredHeaderSegment, + CenteredHeaderViewProps, +} from "./CenteredHeader.types"; + +const ICON_SRC: Record = { + arrow: "assets/shapes/about-header-arrow.svg", + ring: "assets/shapes/about-header-ring.svg", +}; + +const ICON_CLASS: Record = { + arrow: "size-10 md:size-[54px] lg:size-20", + ring: "size-8 md:size-[54px] lg:size-20", +}; + +/** Poster lines at 1024+, matching the desktop frame. Narrower widths wrap on their own. */ +const DESKTOP_LINES = [[0], [1], [2, 3], [4, 5], [6, 7, 8]] as const; + +function SegmentView({ segment }: { segment: CenteredHeaderSegment }) { + if (segment.type === "word") { + return ( + + {segment.text} + + ); + } + + return ( + + {/* eslint-disable-next-line @next/next/no-img-element -- decorative Figma mark */} + + + ); +} + +function CenteredHeaderView({ + accessibleName, + segments, + subhead, + credit, +}: CenteredHeaderViewProps) { + const desktopLines: ReactNode = DESKTOP_LINES.map((line) => ( +
+ {line.map((index) => { + const segment = segments[index]; + return segment ? : null; + })} +
+ )); + + return ( +
+

+ {accessibleName} +

+
+ {segments.map((segment, index) => ( + + ))} +
+
+ {desktopLines} +
+

+ {subhead} +

+
+

+ {credit} +

+
+ ); +} + +CenteredHeaderView.displayName = "CenteredHeaderView"; + +export default memo(CenteredHeaderView); diff --git a/app/components/sections/CenteredHeader/index.tsx b/app/components/sections/CenteredHeader/index.tsx new file mode 100644 index 0000000..0aefc21 --- /dev/null +++ b/app/components/sections/CenteredHeader/index.tsx @@ -0,0 +1,6 @@ +export { default } from "./CenteredHeader.container"; +export type { + CenteredHeaderIcon, + CenteredHeaderSegment, + CenteredHeaderViewProps, +} from "./CenteredHeader.types"; diff --git a/app/components/sections/Credits/Credits.container.tsx b/app/components/sections/Credits/Credits.container.tsx new file mode 100644 index 0000000..9e58b09 --- /dev/null +++ b/app/components/sections/Credits/Credits.container.tsx @@ -0,0 +1,18 @@ +"use client"; + +/** + * Figma: "Section / Credits" (24240:1547) + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import CreditsView from "./Credits.view"; + +const CreditsContainer = memo(() => { + const copy = useMessages().pages.about.credits; + return ; +}); + +CreditsContainer.displayName = "Credits"; + +export default CreditsContainer; diff --git a/app/components/sections/Credits/Credits.types.ts b/app/components/sections/Credits/Credits.types.ts new file mode 100644 index 0000000..27a55fb --- /dev/null +++ b/app/components/sections/Credits/Credits.types.ts @@ -0,0 +1,9 @@ +export type CreditRow = { + role: string; + name: string; +}; + +export interface CreditsViewProps { + heading: string; + rows: CreditRow[]; +} diff --git a/app/components/sections/Credits/Credits.view.tsx b/app/components/sections/Credits/Credits.view.tsx new file mode 100644 index 0000000..aafad1a --- /dev/null +++ b/app/components/sections/Credits/Credits.view.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { memo } from "react"; +import type { CreditsViewProps } from "./Credits.types"; + +function CreditsView({ heading, rows }: CreditsViewProps) { + return ( +
+
+

+ {heading} +

+
+ {rows.map((row) => ( +
+
+ {row.role} +
+
+ {row.name} +
+
+ ))} +
+
+
+ ); +} + +CreditsView.displayName = "CreditsView"; + +export default memo(CreditsView); diff --git a/app/components/sections/Credits/index.tsx b/app/components/sections/Credits/index.tsx new file mode 100644 index 0000000..e529029 --- /dev/null +++ b/app/components/sections/Credits/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./Credits.container"; +export type { CreditRow, CreditsViewProps } from "./Credits.types"; diff --git a/app/components/sections/FourQuestions/FourQuestions.container.tsx b/app/components/sections/FourQuestions/FourQuestions.container.tsx new file mode 100644 index 0000000..9e05f33 --- /dev/null +++ b/app/components/sections/FourQuestions/FourQuestions.container.tsx @@ -0,0 +1,29 @@ +"use client"; + +/** + * Figma: "Section / Four Questions" (24230:852) + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import FourQuestionsView from "./FourQuestions.view"; +import type { FourQuestionCard } from "./FourQuestions.types"; + +const FourQuestionsContainer = memo(() => { + const copy = useMessages().pages.about.fourQuestions; + return ( + + ); +}); + +FourQuestionsContainer.displayName = "FourQuestions"; + +export default FourQuestionsContainer; diff --git a/app/components/sections/FourQuestions/FourQuestions.types.ts b/app/components/sections/FourQuestions/FourQuestions.types.ts new file mode 100644 index 0000000..90083b5 --- /dev/null +++ b/app/components/sections/FourQuestions/FourQuestions.types.ts @@ -0,0 +1,29 @@ +export const FOUR_QUESTION_TONE_OPTIONS = [ + "yellow", + "teal", + "lavender", + "red", +] as const; + +export type FourQuestionTone = (typeof FOUR_QUESTION_TONE_OPTIONS)[number]; + +export type FourQuestionCard = { + tone: FourQuestionTone; + question: string; + line: string; +}; + +export type FourQuestionStat = { + value: string; + line: string; +}; + +export interface FourQuestionsViewProps { + heading: string; + cards: FourQuestionCard[]; + wishHeading: string; + wishParagraphs: string[]; + statsHeading: string; + statsSource: string; + stats: FourQuestionStat[]; +} diff --git a/app/components/sections/FourQuestions/FourQuestions.view.tsx b/app/components/sections/FourQuestions/FourQuestions.view.tsx new file mode 100644 index 0000000..8701aaa --- /dev/null +++ b/app/components/sections/FourQuestions/FourQuestions.view.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { memo } from "react"; +import type { + FourQuestionTone, + FourQuestionsViewProps, +} from "./FourQuestions.types"; + +const TONE_CLASS: Record = { + yellow: "bg-[var(--color-surface-invert-brand-secondary)]", + teal: "bg-[var(--color-surface-invert-brand-teal)]", + lavender: "bg-[var(--color-surface-invert-brand-lavender)]", + red: "bg-[var(--color-surface-invert-brand-red)]", +}; + +function FourQuestionsView({ + heading, + cards, + wishHeading, + wishParagraphs, + statsHeading, + statsSource, + stats, +}: FourQuestionsViewProps) { + return ( +
+

+ {heading} +

+
    + {cards.map((card) => ( +
  • +

    + {card.question} +

    +

    + {card.line} +

    +
  • + ))} +
+
+

+ {wishHeading} +

+
+ {wishParagraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+
+
+
+

+ {statsHeading} +

+

+ {statsSource} +

+
+
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
+ {stat.line} +
+
+ ))} +
+
+
+ ); +} + +FourQuestionsView.displayName = "FourQuestionsView"; + +export default memo(FourQuestionsView); diff --git a/app/components/sections/FourQuestions/index.tsx b/app/components/sections/FourQuestions/index.tsx new file mode 100644 index 0000000..82514e7 --- /dev/null +++ b/app/components/sections/FourQuestions/index.tsx @@ -0,0 +1,8 @@ +export { default } from "./FourQuestions.container"; +export type { + FourQuestionCard, + FourQuestionStat, + FourQuestionTone, + FourQuestionsViewProps, +} from "./FourQuestions.types"; +export { FOUR_QUESTION_TONE_OPTIONS } from "./FourQuestions.types"; diff --git a/app/components/sections/History/History.container.tsx b/app/components/sections/History/History.container.tsx new file mode 100644 index 0000000..3a175ff --- /dev/null +++ b/app/components/sections/History/History.container.tsx @@ -0,0 +1,21 @@ +"use client"; + +/** + * Figma: "Section / History" (24220:30635) + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import HistoryView from "./History.view"; +import type { HistoryItem } from "./History.types"; + +const HistoryContainer = memo(() => { + const copy = useMessages().pages.about.history; + return ( + + ); +}); + +HistoryContainer.displayName = "History"; + +export default HistoryContainer; diff --git a/app/components/sections/History/History.types.ts b/app/components/sections/History/History.types.ts new file mode 100644 index 0000000..2af6dcd --- /dev/null +++ b/app/components/sections/History/History.types.ts @@ -0,0 +1,24 @@ +export type HistoryAccent = "brand" | "positive"; + +export type HistoryRun = { + text: string; + emphasis?: boolean; +}; + +export type HistoryItem = + | { + kind: "entry"; + date: string; + accent: HistoryAccent; + body: HistoryRun[]; + } + | { + kind: "callout"; + title: string; + body: string; + }; + +export interface HistoryViewProps { + heading: string; + items: HistoryItem[]; +} diff --git a/app/components/sections/History/History.view.tsx b/app/components/sections/History/History.view.tsx new file mode 100644 index 0000000..df2513c --- /dev/null +++ b/app/components/sections/History/History.view.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { memo } from "react"; +import type { HistoryAccent, HistoryItem, HistoryViewProps } from "./History.types"; + +const ACCENT_CLASS: Record = { + brand: "bg-[var(--color-content-default-brand-primary)]", + positive: "bg-[var(--color-surface-default-positive-primary)]", +}; + +const DATE_CLASS = + "font-bricolage-grotesque text-display-xxs text-[var(--color-content-default-primary)]"; + +const BODY_CLASS = + "font-inter text-paragraph-m text-[var(--color-content-default-secondary)]"; + +function DateMark({ date, accent }: { date: string; accent: HistoryAccent }) { + return ( +
+

{date}

+ +
+ ); +} + +function HistoryItemView({ item }: { item: HistoryItem }) { + if (item.kind === "callout") { + return ( + + ); + } + + return ( +
+ +

+ {item.body.map((run, index) => + run.emphasis ? ( + + {run.text} + + ) : ( + {run.text} + ), + )} +

+
+ ); +} + +function HistoryView({ heading, items }: HistoryViewProps) { + return ( +
+

+ {heading} +

+
+ {items.map((item, index) => ( + + ))} +
+
+ ); +} + +HistoryView.displayName = "HistoryView"; + +export default memo(HistoryView); diff --git a/app/components/sections/History/index.tsx b/app/components/sections/History/index.tsx new file mode 100644 index 0000000..e391f7f --- /dev/null +++ b/app/components/sections/History/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./History.container"; +export type { HistoryItem, HistoryViewProps } from "./History.types"; diff --git a/app/components/sections/NarrativeBlock/NarrativeBlock.container.tsx b/app/components/sections/NarrativeBlock/NarrativeBlock.container.tsx new file mode 100644 index 0000000..81832f7 --- /dev/null +++ b/app/components/sections/NarrativeBlock/NarrativeBlock.container.tsx @@ -0,0 +1,37 @@ +"use client"; + +/** + * Figma: "Section / Narrative Block" (24237:1241) + */ + +import { memo, useCallback, useState } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import AskOrganizerInquiryModal from "../../modals/AskOrganizerInquiry"; +import NarrativeBlockView from "./NarrativeBlock.view"; + +const NarrativeBlockContainer = memo(() => { + const copy = useMessages().pages.about.narrativeBlock; + const [inquiryOpen, setInquiryOpen] = useState(false); + const openInquiry = useCallback(() => setInquiryOpen(true), []); + const closeInquiry = useCallback(() => setInquiryOpen(false), []); + + return ( + <> + + + + ); +}); + +NarrativeBlockContainer.displayName = "NarrativeBlock"; + +export default NarrativeBlockContainer; diff --git a/app/components/sections/NarrativeBlock/NarrativeBlock.types.ts b/app/components/sections/NarrativeBlock/NarrativeBlock.types.ts new file mode 100644 index 0000000..e27da97 --- /dev/null +++ b/app/components/sections/NarrativeBlock/NarrativeBlock.types.ts @@ -0,0 +1,10 @@ +export interface NarrativeBlockViewProps { + heading: string; + paragraphs: string[]; + addHeading: string; + addLine: string; + shareExperiences: string; + helpBuild: string; + helpBuildHref: string; + onShareExperiences: () => void; +} diff --git a/app/components/sections/NarrativeBlock/NarrativeBlock.view.tsx b/app/components/sections/NarrativeBlock/NarrativeBlock.view.tsx new file mode 100644 index 0000000..60fc9ab --- /dev/null +++ b/app/components/sections/NarrativeBlock/NarrativeBlock.view.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { memo } from "react"; +import Button from "../../buttons/Button"; +import type { NarrativeBlockViewProps } from "./NarrativeBlock.types"; + +function NarrativeBlockView({ + heading, + paragraphs, + addHeading, + addLine, + shareExperiences, + helpBuild, + helpBuildHref, + onShareExperiences, +}: NarrativeBlockViewProps) { + return ( +
+
+

+ {heading} +

+
+ {paragraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+
+
+

+ {addHeading} +

+

+ {addLine} +

+
+
+ + +
+
+
+
+ ); +} + +NarrativeBlockView.displayName = "NarrativeBlockView"; + +export default memo(NarrativeBlockView); diff --git a/app/components/sections/NarrativeBlock/index.tsx b/app/components/sections/NarrativeBlock/index.tsx new file mode 100644 index 0000000..78ce7bb --- /dev/null +++ b/app/components/sections/NarrativeBlock/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./NarrativeBlock.container"; +export type { NarrativeBlockViewProps } from "./NarrativeBlock.types"; diff --git a/app/components/sections/ParticipantSelection/ParticipantSelection.container.tsx b/app/components/sections/ParticipantSelection/ParticipantSelection.container.tsx new file mode 100644 index 0000000..a777f9b --- /dev/null +++ b/app/components/sections/ParticipantSelection/ParticipantSelection.container.tsx @@ -0,0 +1,18 @@ +"use client"; + +/** + * Figma: "Section / Participant Selection" (24227:30762) + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import ParticipantSelectionView from "./ParticipantSelection.view"; + +const ParticipantSelectionContainer = memo(() => { + const copy = useMessages().pages.about.participantSelection; + return ; +}); + +ParticipantSelectionContainer.displayName = "ParticipantSelection"; + +export default ParticipantSelectionContainer; diff --git a/app/components/sections/ParticipantSelection/ParticipantSelection.types.ts b/app/components/sections/ParticipantSelection/ParticipantSelection.types.ts new file mode 100644 index 0000000..786f542 --- /dev/null +++ b/app/components/sections/ParticipantSelection/ParticipantSelection.types.ts @@ -0,0 +1,6 @@ +export interface ParticipantSelectionViewProps { + heading: string; + body: string; + groups: string[]; + footnote: string; +} diff --git a/app/components/sections/ParticipantSelection/ParticipantSelection.view.tsx b/app/components/sections/ParticipantSelection/ParticipantSelection.view.tsx new file mode 100644 index 0000000..47f5ce4 --- /dev/null +++ b/app/components/sections/ParticipantSelection/ParticipantSelection.view.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { memo } from "react"; +import { getAssetPath } from "../../../../lib/assetUtils"; +import type { ParticipantSelectionViewProps } from "./ParticipantSelection.types"; + +function ParticipantArt() { + return ( +
+ {/* eslint-disable @next/next/no-img-element -- decorative Figma shapes */} + + + + {/* eslint-enable @next/next/no-img-element */} +
+ ); +} + +function ParticipantSelectionView({ + heading, + body, + groups, + footnote, +}: ParticipantSelectionViewProps) { + return ( +
+ +
+
+

+ {heading} +

+

+ {body} +

+
+
+
    + {groups.map((group, index) => ( +
  • + {group} +
  • + ))} +
+

+ {footnote} +

+
+
+
+ ); +} + +ParticipantSelectionView.displayName = "ParticipantSelectionView"; + +export default memo(ParticipantSelectionView); diff --git a/app/components/sections/ParticipantSelection/index.tsx b/app/components/sections/ParticipantSelection/index.tsx new file mode 100644 index 0000000..f05606b --- /dev/null +++ b/app/components/sections/ParticipantSelection/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./ParticipantSelection.container"; +export type { ParticipantSelectionViewProps } from "./ParticipantSelection.types"; diff --git a/app/components/sections/QuoteBlock/QuoteBlock.container.tsx b/app/components/sections/QuoteBlock/QuoteBlock.container.tsx index adc5c88..4336544 100644 --- a/app/components/sections/QuoteBlock/QuoteBlock.container.tsx +++ b/app/components/sections/QuoteBlock/QuoteBlock.container.tsx @@ -13,6 +13,7 @@ const QuoteBlockContainer = memo( className = "", quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.", quoteSecondary, + statementStacked = false, author = "Jo Freeman", source = "The Tyranny of Structurelessness", avatarSrc = getAssetPath(ASSETS.QUOTE_AVATAR), @@ -151,6 +152,7 @@ const QuoteBlockContainer = memo( className={className} quote={quote} quoteSecondary={quoteSecondary} + statementStacked={statementStacked} author={author} source={source} quoteId={quoteId} diff --git a/app/components/sections/QuoteBlock/QuoteBlock.types.ts b/app/components/sections/QuoteBlock/QuoteBlock.types.ts index 8558ba3..e451cd7 100644 --- a/app/components/sections/QuoteBlock/QuoteBlock.types.ts +++ b/app/components/sections/QuoteBlock/QuoteBlock.types.ts @@ -11,6 +11,11 @@ export interface QuoteBlockProps { quote?: string; /** Second paragraph for **`statement`** (Section/Quote); merged into one `

` from **`lg`**. */ quoteSecondary?: string; + /** + * Keep the two statement paragraphs stacked at every breakpoint. + * Default joins them into one paragraph from `lg`. + */ + statementStacked?: boolean; author?: string; source?: string; avatarSrc?: string; @@ -46,6 +51,7 @@ export interface QuoteBlockViewProps { className: string; quote: string; quoteSecondary?: string; + statementStacked: boolean; author: string; source?: string; quoteId: string; diff --git a/app/components/sections/QuoteBlock/QuoteBlock.view.tsx b/app/components/sections/QuoteBlock/QuoteBlock.view.tsx index 027326f..30cfd1c 100644 --- a/app/components/sections/QuoteBlock/QuoteBlock.view.tsx +++ b/app/components/sections/QuoteBlock/QuoteBlock.view.tsx @@ -11,6 +11,7 @@ function QuoteBlockView({ className, quote, quoteSecondary, + statementStacked, author, source, quoteId, @@ -41,13 +42,25 @@ function QuoteBlockView({

- {quote} + + {quote} + {quoteSecondary ? ( <> - {" "} - {quoteSecondary} + {statementStacked ? ( + " " + ) : ( + {" "} + )} + + {quoteSecondary} + ) : null}

diff --git a/app/components/sections/RebuildProcess/RebuildProcess.container.tsx b/app/components/sections/RebuildProcess/RebuildProcess.container.tsx new file mode 100644 index 0000000..b6fc8b4 --- /dev/null +++ b/app/components/sections/RebuildProcess/RebuildProcess.container.tsx @@ -0,0 +1,18 @@ +"use client"; + +/** + * Figma: "Section / Rebuild Process" (24237:1085) + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import RebuildProcessView from "./RebuildProcess.view"; + +const RebuildProcessContainer = memo(() => { + const copy = useMessages().pages.about.rebuildProcess; + return ; +}); + +RebuildProcessContainer.displayName = "RebuildProcess"; + +export default RebuildProcessContainer; diff --git a/app/components/sections/RebuildProcess/RebuildProcess.types.ts b/app/components/sections/RebuildProcess/RebuildProcess.types.ts new file mode 100644 index 0000000..22ef1bc --- /dev/null +++ b/app/components/sections/RebuildProcess/RebuildProcess.types.ts @@ -0,0 +1,4 @@ +export interface RebuildProcessViewProps { + heading: string; + body: string; +} diff --git a/app/components/sections/RebuildProcess/RebuildProcess.view.tsx b/app/components/sections/RebuildProcess/RebuildProcess.view.tsx new file mode 100644 index 0000000..eeae58a --- /dev/null +++ b/app/components/sections/RebuildProcess/RebuildProcess.view.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { memo } from "react"; +import type { RebuildProcessViewProps } from "./RebuildProcess.types"; + +function RebuildProcessView({ heading, body }: RebuildProcessViewProps) { + return ( +
+

+ {heading} +

+

+ {body} +

+
+ ); +} + +RebuildProcessView.displayName = "RebuildProcessView"; + +export default memo(RebuildProcessView); diff --git a/app/components/sections/RebuildProcess/index.tsx b/app/components/sections/RebuildProcess/index.tsx new file mode 100644 index 0000000..f771af5 --- /dev/null +++ b/app/components/sections/RebuildProcess/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./RebuildProcess.container"; +export type { RebuildProcessViewProps } from "./RebuildProcess.types"; diff --git a/app/components/sections/YourTurn/YourTurn.container.tsx b/app/components/sections/YourTurn/YourTurn.container.tsx new file mode 100644 index 0000000..1f0095f --- /dev/null +++ b/app/components/sections/YourTurn/YourTurn.container.tsx @@ -0,0 +1,67 @@ +"use client"; + +/** + * Figma: "Section / Your Turn" (24239:1397) + */ + +import { memo, useCallback, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useMessages } from "../../../contexts/MessagesContext"; +import { fetchAuthSession } from "../../../../lib/create/api"; +import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry"; +import AskOrganizerInquiryModal from "../../modals/AskOrganizerInquiry"; +import YourTurnView from "./YourTurn.view"; + +const YourTurnContainer = memo(() => { + const router = useRouter(); + const copy = useMessages().pages.about.yourTurn; + const [signedIn, setSignedIn] = useState(false); + const [inquiryOpen, setInquiryOpen] = useState(false); + + useEffect(() => { + let cancelled = false; + void fetchAuthSession() + .then(({ user }) => { + if (!cancelled) setSignedIn(Boolean(user)); + }) + .catch(() => { + /* guest fallback */ + }); + return () => { + cancelled = true; + }; + }, []); + + const onCreateRule = useCallback(() => { + prepareFreshCreateFlowEntrySync({ signedIn }); + router.push("/create/informational"); + }, [router, signedIn]); + + return ( + <> + setInquiryOpen(true)} + /> + setInquiryOpen(false)} + /> + + ); +}); + +YourTurnContainer.displayName = "YourTurn"; + +export default YourTurnContainer; diff --git a/app/components/sections/YourTurn/YourTurn.types.ts b/app/components/sections/YourTurn/YourTurn.types.ts new file mode 100644 index 0000000..17be077 --- /dev/null +++ b/app/components/sections/YourTurn/YourTurn.types.ts @@ -0,0 +1,15 @@ +export interface YourTurnViewProps { + heading: string; + body: string; + createRule: string; + facilitator: string; + facilitatorHref: string; + questionsLead: string; + faq: string; + faqHref: string; + or: string; + askOrganizer: string; + illustrationCaption: string; + onCreateRule: () => void; + onAskOrganizer: () => void; +} diff --git a/app/components/sections/YourTurn/YourTurn.view.tsx b/app/components/sections/YourTurn/YourTurn.view.tsx new file mode 100644 index 0000000..3bc5b10 --- /dev/null +++ b/app/components/sections/YourTurn/YourTurn.view.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { memo } from "react"; +import Button from "../../buttons/Button"; +import Link from "../../navigation/Link"; +import type { YourTurnViewProps } from "./YourTurn.types"; + +const COLOR_BARS = [ + "bg-[var(--color-lavender-lavender200)]", + "bg-[var(--color-surface-invert-brand-teal)]", + "bg-[var(--color-surface-invert-brand-secondary)]", + "bg-[var(--color-surface-invert-brand-red)]", +] as const; + +function YourTurnView({ + heading, + body, + createRule, + facilitator, + facilitatorHref, + questionsLead, + faq, + faqHref, + or, + askOrganizer, + illustrationCaption, + onCreateRule, + onAskOrganizer, +}: YourTurnViewProps) { + return ( +
+
+
+
+
+ {COLOR_BARS.map((bar) => ( + + ))} +
+

+ {heading} +

+

+ {body} +

+
+
+ + +
+

+ {questionsLead}{" "} + + {faq} + {" "} + {or}{" "} + + {askOrganizer} + +

+
+
+

+ {illustrationCaption} +

+
+
+
+ ); +} + +YourTurnView.displayName = "YourTurnView"; + +export default memo(YourTurnView); diff --git a/app/components/sections/YourTurn/index.tsx b/app/components/sections/YourTurn/index.tsx new file mode 100644 index 0000000..c40b288 --- /dev/null +++ b/app/components/sections/YourTurn/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./YourTurn.container"; +export type { YourTurnViewProps } from "./YourTurn.types"; diff --git a/docs/figma-component-registry.md b/docs/figma-component-registry.md index 46652c5..2d6892d 100644 --- a/docs/figma-component-registry.md +++ b/docs/figma-component-registry.md @@ -145,11 +145,19 @@ Inventory aligns with [**CR-104**](https://linear.app/community-rule/issue/CR-10 | Card steps (SectionCardSteps) | **`CardSteps/`** | Composes **`cards/Step`** (Figma **Card / Step** — not **`progress/Stepper`**). | | Rule stack | **`RuleStack/`** | | | Feature grid | **`FeatureGrid/`** | | -| Quote block | **`QuoteBlock/`** | Includes **`statement`** (Section/Quote **22137:890679**), shipped **`/about`** under FAQ; **`standard` / `compact` / `extended`** remain portrait + attribution. | +| Quote block | **`QuoteBlock/`** | Includes **`statement`** (Section/Quote **22137:890679**). About passes **`statementStacked`** for the opening statement. **`standard` / `compact` / `extended`** remain portrait + attribution. | | Ask organizer | **`AskOrganizer/`** | | -| Stats (about metrics) | **`Stats/`** | Composes **`cards/Stat`** + **`asset/Shapes`**; shipped on **`/about`**. | -| Book promo | **`Book/`** | **`/about`** download band — **`ContentLockup`** + **`Button`**. | -| FAQ accordion | **`Accordion/`** | Section wrapper over **`layout/Accordion`**; **`/about`**. | +| Centered header | **`CenteredHeader/`** | Figma [**24214:25892**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24214-25892). About hero. | +| History | **`History/`** | Figma [**24220:30635**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24220-30635). About timeline. | +| Participant selection | **`ParticipantSelection/`** | Figma [**24227:30762**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24227-30762). About study groups, unnamed. | +| Four questions | **`FourQuestions/`** | Figma [**24230:852**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24230-852). About questions and study stats. | +| Rebuild process | **`RebuildProcess/`** | Figma [**24237:1085**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24237-1085). | +| Narrative block | **`NarrativeBlock/`** | Figma [**24237:1241**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24237-1241). | +| Your turn | **`YourTurn/`** | Figma [**24239:1397**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24239-1397). About closing call to action. | +| Credits | **`Credits/`** | Figma [**24240:1547**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=24240-1547). | +| Stats (about metrics) | **`Stats/`** | Composes **`cards/Stat`** + **`asset/Shapes`**. | +| Book promo | **`Book/`** | Download band — **`ContentLockup`** + **`Button`**. | +| FAQ accordion | **`Accordion/`** | Section wrapper over **`layout/Accordion`**. | | Related content | **`RelatedArticles/`** | Article list / cards — confirm naming vs Figma “related slider” frames. | | Template grid (governance) | **`GovernanceTemplateGrid/`** | **`GovernanceTemplateGridSkeleton`** colocated. | | Use cases org strip | **`UseCasesOrgs/`** | Figma [**22112-873893**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22112-873893); composes **`cards/CaseStudy`**; **`/use-cases`**. | @@ -170,7 +178,7 @@ Inventory aligns with [**CR-104**](https://linear.app/community-rule/issue/CR-10 | --- | --- | --- | | Section header (1 vs 3 lines, responsive sizes) | **`SectionHeader/`** | Figma [**17411:10981**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=17411-10981). **`SectionHeader.tsx`** + **`index.tsx`**; **`default`** / **`multi-line`**; optional **`stackedDesktopLines`**. Composed by **`sections/CardSteps`**, **`sections/RuleStack`**, etc. | | Header lockup / content lockup | **`HeaderLockup/`**, **`ContentLockup/`** | **`container` / `view` / `types`** + **`index.tsx`** where split. **`ContentLockup`** **`about`** variant + optional **`titleContent`** for **`AboutHeader`**. | -| About header (inline word + shape lockup) | **`AboutHeader/`** | **`/about`** hero; composes **`ContentLockup`**. | +| About header (inline word + shape lockup) | **`AboutHeader/`** | Earlier About hero; composes **`ContentLockup`**. About now opens with **`sections/CenteredHeader`**. | | Marketing page header (title + body + optional CTA) | **`PageHeader/`** | Figma [**21004-15902**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=21004-15902); **`/use-cases`**. | | Triple text block | **`TripleTextBlock/`** | **`/about`** (columns only; optional title/CTA omitted per page). | | Three-step explainer + CTA | **`TripleStep/`** | Figma [**22084-859256**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22084-859256); **`/use-cases`** (illustration column deferred). | diff --git a/messages/en/metadata.json b/messages/en/metadata.json index 5b2d879..791a840 100644 --- a/messages/en/metadata.json +++ b/messages/en/metadata.json @@ -17,7 +17,8 @@ ] }, "about": { - "title": "About" + "title": "About", + "description": "Why the Media Economies Design Lab at CU Boulder rebuilt CommunityRule, what organizers told us and who builds it." }, "learn": { "title": "Learn" diff --git a/messages/en/pages/about.json b/messages/en/pages/about.json index 681abc8..9e690d3 100644 --- a/messages/en/pages/about.json +++ b/messages/en/pages/about.json @@ -1,125 +1,210 @@ { - "_comment": "About page content. Book download hrefs are wired in app/(marketing)/about/page.tsx (structureBeforeCrisisPath / governanceBookletPath).", - "aboutHeader": { + "_comment": "About page. Three sentences stay out until they are approved: cards and templates in the public domain, a Rule staying private unless it is shared, and co-op lawyers designing the Rule format.", + "centeredHeader": { + "accessibleName": "Every community has rules written or not", "segments": [ - { "type": "word", "text": "CommunityRule" }, + { "type": "word", "text": "Every" }, + { "type": "word", "text": "community" }, + { "type": "word", "text": "has" }, + { "type": "word", "text": "rules" }, { "type": "icon", "icon": "arrow" }, - { "type": "word", "text": "is" }, - { "type": "word", "text": "a" }, - { "type": "word", "text": "tool" }, - { "type": "word", "text": "that" }, - { "type": "word", "text": "helps" }, - { "type": "word", "text": "groups" }, - { "type": "icon", "icon": "about" }, - { "type": "word", "text": "define" }, - { "type": "word", "text": "who" }, - { "type": "word", "text": "they" }, - { "type": "word", "text": "want" }, - { "type": "word", "text": "to" }, - { "type": "word", "text": "be" } - ] + { "type": "word", "text": "written" }, + { "type": "word", "text": "or" }, + { "type": "word", "text": "not" }, + { "type": "icon", "icon": "ring" } + ], + "subhead": "CommunityRule helps communities write down, together, how they talk, who belongs, how they decide and what happens when they disagree.", + "credit": "From the Media Economies Design Lab at the University of Colorado Boulder" }, - "stats": { - "titlePrefix": "From", - "titleEmphasis": "projects", - "titleSuffix": "to communities", - "asOfPrefix": "as of", + "openingStatement": { + "paragraph1": "Too many of our communities end up with rules nobody chose, run by whoever set up the group chat. Other people often do all the work so the first real disagreement turns into a painful split.", + "paragraph2": "Our communities deserve better." + }, + "history": { + "heading": "The idea is old. Our first tries fell short", "items": [ { - "value": "420M+", - "label": "open source projects", - "shapeVariant": "yellow", - "asOf": "2025", - "sourceHref": "https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/", - "sourceName": "GitHub Octoverse" + "kind": "entry", + "date": "About 100 BCE", + "accent": "brand", + "body": [ + { + "text": "One of the Dead Sea Scrolls sets out how to join a Jewish community, how members should treat one another and what happens when someone breaks the rules. Scholars call it the Community Rule. Medieval monasteries called their own governing documents a " + }, + { "text": "regula", "emphasis": true }, + { "text": ", a rule. Our name is a nod to both." } + ] }, { - "value": "24%", - "label": "year over year growth in open source", - "shapeVariant": "purple", - "asOf": "2025", - "sourceHref": "https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/", - "sourceName": "GitHub Octoverse" + "kind": "entry", + "date": "1972", + "accent": "brand", + "body": [ + { + "text": "Jo Freeman, writing from inside the women's movement, argued that groups without written rules still have rules, just hidden ones. “The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.”" + } + ] }, { - "value": "45%", - "label": "adults in the US participate in community service", - "shapeVariant": "green", - "asOf": "2025", - "sourceHref": "https://www.americorps.gov/about/our-impact/volunteering-civic-life", - "sourceName": "AmeriCorps Volunteering and Civic Life in America" + "kind": "entry", + "date": "2020", + "accent": "brand", + "body": [ + { + "text": "Nathan Schneider started the first CommunityRule at the lab in the mutual aid surge during Covid. It was a palette of templates, from dictatorship to various flavors of democracy, that any community could adapt and adopt. Mutual aid communities were forming in weeks, so the lab offered to help them use it, for free." + } + ] }, { - "value": "800+", - "label": "mutual aid groups in the U.S.", - "shapeVariant": "orange", - "asOf": "2026", - "sourceHref": "https://www.nea.org/nea-today/all-news-articles/mutual-aid-act-resistance-and-community-support", - "sourceName": "Mutual Aid Hub via NEA" + "kind": "entry", + "date": "2021", + "accent": "brand", + "body": [ + { + "text": "Cassandra Dana, Drew Hornbein, Vincent Russell and Nathan Schneider turned the templates into a small book, Community Rules. Nine structures to choose from, combine or react against." + } + ] + }, + { + "kind": "entry", + "date": "2022 to 2024", + "accent": "brand", + "body": [ + { + "text": "The lab rebuilt it around modules with Deacon Rodda and Drew Hornbein, so a community could mix practices instead of picking one fixed model. Later, communities could log in, save a Rule and change it as they grew." + } + ] + }, + { + "kind": "callout", + "title": "We found that people came to read, and far fewer stayed to write. Many people tried the editor, often alone or in a workshop, and few communities are known to have adopted a Rule and kept it.", + "body": "Reading about governance was not the hard part. Something happened between the template and the room, and we did not know what." + }, + { + "kind": "entry", + "date": "2026", + "accent": "positive", + "body": [ + { + "text": "Two community fellows at the lab set out to learn what was holding people back and how to fix it. Dani Shipley designed a study, interviewed organizers and built prototypes of a new experience. In parallel, Andy DiLallo started rebuilding the tool around what organizers said." + } + ] } ] }, - "tripleTextBlock": { - "columns": [ + "participantSelection": { + "heading": "We went looking for communities that had figured it out", + "body": "We talked with the people who hold communities together about how they actually do it: organizers from worker and housing co-ops, mutual aid networks, street medic crews and open source projects, in different countries and at very different sizes. We are still talking with them.", + "groups": [ + "A mutual aid network that formed during a flood and never stopped", + "A decades-old worker co-op of co-ops started by staff", + "A community fridge organizer that serves a large metro area", + "An open source project with maintainers on three continents", + "A housing co-op on its second generation of members", + "A food share that has served every Saturday for at least nine years" + ], + "footnote": "Names appear only for pilot communities" + }, + "fourQuestions": { + "heading": "Across these conversations, the communities that lasted had settled four things early", + "cards": [ { - "title": "Share Leadership and Prevent Burnout", - "description": "Rotating roles and setting clear expectations for contributions can prevent a few people from taking on too much. A living document outlining responsibilities and group values helps distribute labor fairly and avoids misunderstandings.\n\nA transportation assistance group avoided burnout by introducing a weekly shift system, making participation more sustainable.", - "lgTitle": "Frameworks for Community Governance", - "lgDescription": "CommunityRule provides customizable templates that help communities define decision-making structures, leadership roles, and processes for collective action." + "tone": "yellow", + "question": "How do you talk?", + "line": "Where do discussions happen, how is information logged and what channels stay open to everyone?" }, { - "title": "Establish Transparent, Inclusive Decision-Making", - "description": "Deciding early on how your group makes choices—whether consensus, majority vote, or delegation—can prevent confusion. Transparency and regular check-ins help avoid unspoken hierarchies and keep everyone engaged.\n\nA disaster relief group sped up decisions by adopting a “consensus-minus-one” model, preventing gridlock while maintaining group alignment.", - "lgTitle": "Empowering Self-Governance", - "lgDescription": "The platform enables communities, mutual aid groups, and open-source projects to foster transparency and accountability without relying on top-down authority." + "tone": "teal", + "question": "Who belongs?", + "line": "How does someone join the community, how do they take on roles and what are the boundaries of membership?" }, { - "title": "Address Conflicts Before They Grow", - "description": "A basic conflict resolution process can prevent disputes from escalating. Clear guidelines for resource distribution reduce misunderstandings and maintain trust. Assume good faith and use structured discussions to resolve issues.\n\nMembers noticed frustration when some folks felt unheard in decisions about new projects. By anonymous feedback box, they created a space to address concerns early and make adjustments before conflicts escalated.", - "lgTitle": "Flexible & Open-Source Approach", - "lgDescription": "Users can modify existing governance templates or create their own, making it a versatile tool for diverse community needs, from co-ops to digital collectives." + "tone": "lavender", + "question": "How do you decide?", + "line": "Who has the authority to make decisions, what processes are used and how are agreements formalized?" + }, + { + "tone": "red", + "question": "What happens when you disagree?", + "line": "What are the steps for peer mediation, conflict management and handling core value collisions?" + } + ], + "wishHeading": "What did communities wish they had settled sooner?", + "wishParagraphs": [ + "Through interviews, workshops and digital ethnography we built empathy with people that we hoped would use the tool. This research produced many insights.", + "We found that written rules can harden too, and they can favor whoever writes well and has the most time. So a good Rule stays short, names who holds the keys, from the bank account to the group chat, and says how it changes and how someone can leave." + ], + "statsHeading": "What unwritten rules cost", + "statsSource": "From interviews with members of forty communities from April 2025 to February 2026.", + "stats": [ + { + "value": "9 in 10", + "line": "had lost an organizer to burnout, usually with little to no notice." + }, + { + "value": "2 in 3", + "line": "had never written down how they make hard or contentious decisions." + }, + { + "value": "1 in 2", + "line": "had split, or nearly split, over a decision nobody knew how to make." } ] }, - "structureBeforeCrisis": { - "title": "Get Structure Before Crisis", - "description": "Structure Before Crisis is a collection of organizing patterns that help mutual aid groups lay the groundwork to build new worlds.", - "buttonText": "Download Book", - "imageAlt": "Structure Before Crisis book cover" + "rebuildProcess": { + "heading": "We rebuilt the tool around what organizers told us", + "body": "Organizers told us the old tool was confusing. They could not see their options, and writing a whole Rule at once felt like too much. So we sketched each idea, put it in front of organizers, founders and co-op developers, ran a pilot with local mutual aid communities and co-ops and fixed whatever tripped people up. Now your options are laid out as cards other communities use. You pick the ones that fit and write your Rule one part at a time." }, - "communityRules": { - "title": "Get the Community Rules Book", - "description": "Community Rules is a simple tool to help make great communities even better and healthier. It includes nine templates for organizational structures that communities can choose from, combine, or react against.", - "buttonText": "Download Book", - "imageAlt": "Community Rules book cover" + "narrativeBlock": { + "heading": "What organizers gave, we give back", + "paragraphs": [ + "The study exists because organizers gave their time and their stories. We commit to ensuring they receive the direct value of our collective knowledge first, with access to the findings before anyone else, alongside a free, dedicated governance workshop for their community.", + "What it produced is shared. The tool is free for every community, and its code is open source under the GNU General Public License version 3. The guides, the zines and the findings are published under CC BY-SA 4.0, so you can copy them, adapt them and republish them. A tool that asks your community to be transparent should not be a black box itself.", + "Some things are not shared. The notes from each conversation stay private, and nothing a community tells us is quoted or used without its permission. Send your members a link, or export it and publish it yourself. With a community's consent, and only then, what we learn in workshops goes back into the templates and guides for the next community.", + "Our templates output plain text markdown that you can commit right into your git repository next to your code." + ], + "addHeading": "Add to it", + "addLine": "The patterns come from communities. Add yours.", + "shareExperiences": "Share your experiences", + "helpBuild": "Help build", + "helpBuildHref": "https://git.medlab.host" }, - "faq": { - "title": "Get answers to your questions", - "items": [ + "yourTurn": { + "heading": "Every community has rules. Write yours down, together", + "body": "It is also practice. Each time a community settles something together, it gets better at governing itself. The tool is free for every community, and the kit is free for volunteer-run communities. Try it together, then tell us how your community really works, so the next version fits it better.", + "createRule": "Create rule", + "facilitator": "Talk to a facilitator", + "facilitatorHref": "/workshops", + "questionsLead": "Questions first?", + "faq": "Read the FAQ", + "faqHref": "/learn#faq", + "or": "or", + "askOrganizer": "ask an organizer", + "illustrationCaption": "[People sitting around a circular table discussing drafts]" + }, + "credits": { + "heading": "Who made this", + "rows": [ { - "title": "What is CommunityRule, and who is it for?", - "answer": "CommunityRule helps groups write operating manuals that reflect their values and decision-making needs." + "role": "Started CommunityRule, advisor and guide", + "name": "Nathan Schneider" }, { - "title": "How does CommunityRule help with governance?", - "answer": "Templates and guided prompts help teams document roles, decision processes, and conflict resolution before issues escalate." + "role": "Mixed-method research, experience design and product management", + "name": "Dani Shipley" }, { - "title": "Do we need to have formal governance to use CommunityRule?", - "answer": "No. Many groups start with informal practices and use CommunityRule to clarify expectations as they grow." + "role": "Product engineering, design system workflow and infrastructure", + "name": "Andy DiLallo" }, { - "title": "What kinds of templates does CommunityRule offer?", - "answer": "The library includes structures such as consensus clusters, elected boards, do-ocracy, and other community-tested models." + "role": "Earlier contributors", + "name": "Cassandra Dana, Drew Hornbein, Deacon Rodda, Vincent Russell, Nathan Schneider" }, { - "title": "Is CommunityRule free to use?", - "answer": "Yes. You can explore templates and draft rules without a subscription." + "role": "Made at", + "name": "Media Economies Design Lab, University of Colorado Boulder" } ] - }, - "quote": { - "paragraph1": "Too many of our communities adopt default governance practices that rely on unchecked authority without even basic democratic features.", - "paragraph2": "Community Rule helps communities establish better norms for decision-making, stewardship, and culture." } } diff --git a/public/assets/shapes/about-header-arrow.svg b/public/assets/shapes/about-header-arrow.svg new file mode 100644 index 0000000..bfcffeb --- /dev/null +++ b/public/assets/shapes/about-header-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/shapes/about-header-ring.svg b/public/assets/shapes/about-header-ring.svg new file mode 100644 index 0000000..7ce71ca --- /dev/null +++ b/public/assets/shapes/about-header-ring.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/shapes/about-participant-circles.svg b/public/assets/shapes/about-participant-circles.svg new file mode 100644 index 0000000..8ddce46 --- /dev/null +++ b/public/assets/shapes/about-participant-circles.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/shapes/about-participant-clover.svg b/public/assets/shapes/about-participant-clover.svg new file mode 100644 index 0000000..84790d4 --- /dev/null +++ b/public/assets/shapes/about-participant-clover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/shapes/about-participant-rings.svg b/public/assets/shapes/about-participant-rings.svg new file mode 100644 index 0000000..96844c8 --- /dev/null +++ b/public/assets/shapes/about-participant-rings.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/stories/sections/Accordion.stories.js b/stories/sections/Accordion.stories.js index 22e52c5..0c945df 100644 --- a/stories/sections/Accordion.stories.js +++ b/stories/sections/Accordion.stories.js @@ -1,5 +1,4 @@ import FaqAccordion from "../../app/components/sections/Accordion"; -import about from "../../messages/en/pages/about.json"; export default { title: "Components/Sections/Accordion", @@ -32,7 +31,17 @@ export default { export const Default = { args: { - title: about.faq.title, - items: about.faq.items, + title: "Get answers to your questions", + items: [ + { + title: "What is CommunityRule, and who is it for?", + answer: + "CommunityRule helps groups write operating manuals that reflect their values and decision-making needs.", + }, + { + title: "Is CommunityRule free to use?", + answer: "Yes. You can explore templates and draft rules without a subscription.", + }, + ], }, }; diff --git a/stories/sections/Book.stories.js b/stories/sections/Book.stories.js index 3af7ec3..f932a1f 100644 --- a/stories/sections/Book.stories.js +++ b/stories/sections/Book.stories.js @@ -5,8 +5,6 @@ import { governanceBookletPath, structureBeforeCrisisPath, } from "../../lib/assetUtils"; -import messages from "../../messages/en/pages/about.json"; - export default { title: "Components/Sections/Book", component: Book, @@ -16,27 +14,26 @@ export default { }, }; -const structureBeforeCrisis = messages.structureBeforeCrisis; -const communityRules = messages.communityRules; - export const Default = { args: { - title: structureBeforeCrisis.title, - description: structureBeforeCrisis.description, - buttonText: structureBeforeCrisis.buttonText, + title: "Get Structure Before Crisis", + description: + "Structure Before Crisis is a collection of organizing patterns that help mutual aid groups lay the groundwork to build new worlds.", + buttonText: "Download Book", buttonHref: getAssetPath(structureBeforeCrisisPath()), imageSrc: getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER), - imageAlt: structureBeforeCrisis.imageAlt, + imageAlt: "Structure Before Crisis book cover", }, }; export const CommunityRules = { args: { - title: communityRules.title, - description: communityRules.description, - buttonText: communityRules.buttonText, + title: "Get the Community Rules Book", + description: + "Community Rules is a simple tool to help make great communities even better and healthier.", + buttonText: "Download Book", buttonHref: getAssetPath(governanceBookletPath()), imageSrc: getAssetPath(ASSETS.COMMUNITY_RULES_COVER), - imageAlt: communityRules.imageAlt, + imageAlt: "Community Rules book cover", }, }; diff --git a/stories/sections/CenteredHeader.stories.js b/stories/sections/CenteredHeader.stories.js new file mode 100644 index 0000000..72cca2c --- /dev/null +++ b/stories/sections/CenteredHeader.stories.js @@ -0,0 +1,12 @@ +import CenteredHeader from "../../app/components/sections/CenteredHeader"; + +export default { + title: "Components/Sections/CenteredHeader", + component: CenteredHeader, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/Credits.stories.js b/stories/sections/Credits.stories.js new file mode 100644 index 0000000..5baaaa0 --- /dev/null +++ b/stories/sections/Credits.stories.js @@ -0,0 +1,12 @@ +import Credits from "../../app/components/sections/Credits"; + +export default { + title: "Components/Sections/Credits", + component: Credits, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/FourQuestions.stories.js b/stories/sections/FourQuestions.stories.js new file mode 100644 index 0000000..5c751b8 --- /dev/null +++ b/stories/sections/FourQuestions.stories.js @@ -0,0 +1,12 @@ +import FourQuestions from "../../app/components/sections/FourQuestions"; + +export default { + title: "Components/Sections/FourQuestions", + component: FourQuestions, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/History.stories.js b/stories/sections/History.stories.js new file mode 100644 index 0000000..79d97cf --- /dev/null +++ b/stories/sections/History.stories.js @@ -0,0 +1,12 @@ +import History from "../../app/components/sections/History"; + +export default { + title: "Components/Sections/History", + component: History, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/NarrativeBlock.stories.js b/stories/sections/NarrativeBlock.stories.js new file mode 100644 index 0000000..cc7c449 --- /dev/null +++ b/stories/sections/NarrativeBlock.stories.js @@ -0,0 +1,12 @@ +import NarrativeBlock from "../../app/components/sections/NarrativeBlock"; + +export default { + title: "Components/Sections/NarrativeBlock", + component: NarrativeBlock, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/ParticipantSelection.stories.js b/stories/sections/ParticipantSelection.stories.js new file mode 100644 index 0000000..2570247 --- /dev/null +++ b/stories/sections/ParticipantSelection.stories.js @@ -0,0 +1,12 @@ +import ParticipantSelection from "../../app/components/sections/ParticipantSelection"; + +export default { + title: "Components/Sections/ParticipantSelection", + component: ParticipantSelection, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/RebuildProcess.stories.js b/stories/sections/RebuildProcess.stories.js new file mode 100644 index 0000000..455bbc4 --- /dev/null +++ b/stories/sections/RebuildProcess.stories.js @@ -0,0 +1,12 @@ +import RebuildProcess from "../../app/components/sections/RebuildProcess"; + +export default { + title: "Components/Sections/RebuildProcess", + component: RebuildProcess, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/sections/YourTurn.stories.js b/stories/sections/YourTurn.stories.js new file mode 100644 index 0000000..7477901 --- /dev/null +++ b/stories/sections/YourTurn.stories.js @@ -0,0 +1,12 @@ +import YourTurn from "../../app/components/sections/YourTurn"; + +export default { + title: "Components/Sections/YourTurn", + component: YourTurn, + parameters: { + layout: "fullscreen", + backgrounds: { default: "dark" }, + }, +}; + +export const Default = {}; diff --git a/stories/type/TripleTextBlock.stories.js b/stories/type/TripleTextBlock.stories.js index 9b5134b..d33a145 100644 --- a/stories/type/TripleTextBlock.stories.js +++ b/stories/type/TripleTextBlock.stories.js @@ -1,5 +1,4 @@ import TripleTextBlock from "../../app/components/type/TripleTextBlock"; -import messages from "../../messages/en/pages/about.json"; export default { title: "Components/Type/TripleTextBlock", @@ -9,6 +8,31 @@ export default { export const Default = { args: { - columns: messages.tripleTextBlock.columns, + columns: [ + { + title: "Share Leadership and Prevent Burnout", + description: + "Rotating roles and setting clear expectations for contributions can prevent a few people from taking on too much.", + lgTitle: "Frameworks for Community Governance", + lgDescription: + "CommunityRule provides customizable templates that help communities define decision-making structures.", + }, + { + title: "Establish Transparent, Inclusive Decision-Making", + description: + "Deciding early on how your group makes choices can prevent confusion.", + lgTitle: "Empowering Self-Governance", + lgDescription: + "The platform enables communities to foster transparency and accountability.", + }, + { + title: "Address Conflicts Before They Grow", + description: + "A basic conflict resolution process can prevent disputes from escalating.", + lgTitle: "Flexible & Open-Source Approach", + lgDescription: + "Users can modify existing governance templates or create their own.", + }, + ], }, }; diff --git a/tests/pages/about.test.tsx b/tests/pages/about.test.tsx index 98f430b..bedf08c 100644 --- a/tests/pages/about.test.tsx +++ b/tests/pages/about.test.tsx @@ -2,88 +2,147 @@ import { afterEach, describe, expect, test } from "vitest"; import { within } from "@testing-library/react"; import { cleanup, + fireEvent, renderWithProviders as render, screen, } from "../utils/test-utils"; import AboutPage from "../../app/(marketing)/about/page"; -import { - ASSETS, - getAssetPath, - governanceBookletPath, - structureBeforeCrisisPath, -} from "../../lib/assetUtils"; -import messages from "../../messages/en/index"; +import { testRouter } from "../mocks/navigation"; afterEach(() => { cleanup(); + testRouter.push.mockClear(); }); -function bookSection(title: string) { - const heading = screen.getByRole("heading", { name: title }); - const section = heading.closest("section"); - expect(section).not.toBeNull(); - return within(section as HTMLElement); -} - describe("AboutPage", () => { - test("pairs each book cover, heading, alt, and download with the same title", () => { + test("opens with the centered header and the new opening statement", () => { render(); - const page = messages.pages.about; - const structureBeforeCrisis = bookSection(page.structureBeforeCrisis.title); expect( - structureBeforeCrisis.getByText(page.structureBeforeCrisis.description), + screen.getByRole("heading", { + level: 1, + name: "Every community has rules written or not", + }), ).toBeInTheDocument(); expect( - structureBeforeCrisis.getByRole("img", { - name: page.structureBeforeCrisis.imageAlt, + screen.getByText( + "From the Media Economies Design Lab at the University of Colorado Boulder", + ), + ).toBeInTheDocument(); + expect( + screen.getByText("Our communities deserve better."), + ).toBeInTheDocument(); + expect( + screen.queryByRole("heading", { + name: /CommunityRule is a tool that helps groups/i, }), - ).toHaveAttribute( - "src", - getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER), + ).not.toBeInTheDocument(); + }); + + test("tells the study story without the old stats, books, or FAQ", () => { + render(); + + expect( + screen.getByRole("heading", { + name: "The idea is old. Our first tries fell short", + }), + ).toBeInTheDocument(); + expect(screen.getByText("regula").tagName).toBe("EM"); + expect( + screen.getByText(/far fewer stayed to write/), + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { + name: "We went looking for communities that had figured it out", + }), + ).toBeInTheDocument(); + const participant = screen + .getByRole("heading", { + name: "We went looking for communities that had figured it out", + }) + .closest("section"); + expect(participant).not.toBeNull(); + expect( + within(participant as HTMLElement).getByText( + "Names appear only for pilot communities", + ), + ).toBeInTheDocument(); + expect( + within(participant as HTMLElement).queryByText(/Colorado/), + ).not.toBeInTheDocument(); + expect(screen.getByText("How do you talk?")).toBeInTheDocument(); + expect(screen.getByText("What happens when you disagree?")).toBeInTheDocument(); + expect(screen.getByText("9 in 10")).toBeInTheDocument(); + expect(screen.getByText("2 in 3")).toBeInTheDocument(); + expect(screen.getByText("1 in 2")).toBeInTheDocument(); + expect( + screen.getByText( + "From interviews with members of forty communities from April 2025 to February 2026.", + ), + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { + name: "We rebuilt the tool around what organizers told us", + }), + ).toBeInTheDocument(); + + expect(screen.queryByText("420M+")).not.toBeInTheDocument(); + expect( + screen.queryByRole("heading", { name: "Get Structure Before Crisis" }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("heading", { name: "Get the Community Rules Book" }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("heading", { name: "Get answers to your questions" }), + ).not.toBeInTheDocument(); + }); + + test("keeps unapproved narrative sentences off the page and wires the approved actions", () => { + render(); + + expect( + screen.getByRole("heading", { name: "What organizers gave, we give back" }), + ).toBeInTheDocument(); + expect(screen.getByText(/GNU General Public License version 3/)).toBeInTheDocument(); + expect(screen.getByText(/CC BY-SA 4.0/)).toBeInTheDocument(); + expect(screen.queryByText(/CC0/)).not.toBeInTheDocument(); + expect(screen.queryByText(/public domain/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/stays private unless you share/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/co-op lawyers/i)).not.toBeInTheDocument(); + + expect(screen.getByRole("link", { name: "Help build" })).toHaveAttribute( + "href", + "https://git.medlab.host", ); expect( - structureBeforeCrisis.getByRole("link", { - name: page.structureBeforeCrisis.buttonText, - }), - ).toHaveAttribute("href", getAssetPath(structureBeforeCrisisPath())); + screen.getByRole("link", { name: "Talk to a facilitator" }), + ).toHaveAttribute("href", "/workshops"); + expect(screen.getByRole("link", { name: "Read the FAQ" })).toHaveAttribute( + "href", + "/learn#faq", + ); - const communityRules = bookSection(page.communityRules.title); - expect( - communityRules.getByText(page.communityRules.description), - ).toBeInTheDocument(); - expect( - communityRules.getByRole("img", { - name: page.communityRules.imageAlt, - }), - ).toHaveAttribute("src", getAssetPath(ASSETS.COMMUNITY_RULES_COVER)); - expect( - communityRules.getByRole("link", { - name: page.communityRules.buttonText, - }), - ).toHaveAttribute("href", getAssetPath(governanceBookletPath())); + fireEvent.click(screen.getByRole("button", { name: "Create rule" })); + expect(testRouter.push).toHaveBeenCalledWith("/create/informational"); + + fireEvent.click(screen.getByRole("button", { name: "Share your experiences" })); + expect(screen.getByRole("dialog", { name: "Ask an organizer" })).toBeInTheDocument(); }); - test("shows sourced About statistics with as-of years", () => { + test("names the people who made it", () => { render(); - const page = messages.pages.about; - expect(screen.getByText("420M+")).toBeInTheDocument(); - expect(screen.getByText("24%")).toBeInTheDocument(); - expect(screen.getByText("45%")).toBeInTheDocument(); - expect(screen.getByText("800+")).toBeInTheDocument(); - expect(screen.queryByText("8000+")).not.toBeInTheDocument(); - expect(screen.queryByText("27%")).not.toBeInTheDocument(); - - for (const item of page.stats.items) { - expect(screen.getByText(item.label)).toBeInTheDocument(); - if (item.sourceHref) { - expect( - screen.getAllByRole("link").some( - (link) => link.getAttribute("href") === item.sourceHref, - ), - ).toBe(true); - } - } + expect( + screen.getByRole("heading", { name: "Who made this" }), + ).toBeInTheDocument(); + expect(screen.getByText("Nathan Schneider")).toBeInTheDocument(); + expect(screen.getByText("Dani Shipley")).toBeInTheDocument(); + expect(screen.getByText("Andy DiLallo")).toBeInTheDocument(); + expect( + screen.getByText( + "Media Economies Design Lab, University of Colorado Boulder", + ), + ).toBeInTheDocument(); }); }); From 069cff2fcd1dd672a03b232693348babd6b016cd Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:22:17 -0700 Subject: [PATCH 2/2] Use the filled Create rule button on the standard header so it matches that navigation variant. Co-authored-by: Cursor --- .../navigation/Top/Top.container.tsx | 2 +- tests/components/Top.test.tsx | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/components/navigation/Top/Top.container.tsx b/app/components/navigation/Top/Top.container.tsx index 64f3964..6fdcb1a 100644 --- a/app/components/navigation/Top/Top.container.tsx +++ b/app/components/navigation/Top/Top.container.tsx @@ -170,7 +170,7 @@ const TopContainer = memo( return (