diff --git a/app/tailwind.css b/app/tailwind.css index deefd79..9ba2e02 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -771,7 +771,7 @@ --color-surface-inverse-brand-accent: var(--color-yellow-yellow100); --color-surface-inverse-brand-primary: var(--color-yellow-yellow50); - --color-surface-inverse-brand-secondary: var(--color-rust-rust300); + --color-surface-inverse-brand-secondary: var(--color-yellow-yellow200); --color-surface-inverse-primary: var(--color-gray-000); --color-surface-inverse-secondary: var(--color-gray-100); --color-surface-inverse-tertiary: var(--color-gray-200); diff --git a/lib/templates/governanceTemplateCatalog.ts b/lib/templates/governanceTemplateCatalog.ts index c35593b..3b9334e 100644 --- a/lib/templates/governanceTemplateCatalog.ts +++ b/lib/templates/governanceTemplateCatalog.ts @@ -140,13 +140,14 @@ const bySlug = new Map( ); /** - * Order for the home “Popular templates” row (four cards). Must match catalog slugs. + * Order for the home “Popular templates” row (four cards). + * Figma Section/RuleStack [22083:855584](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22083-855584). */ export const GOVERNANCE_TEMPLATE_HOME_SLUGS: readonly string[] = [ - "consensus-clusters", "consensus", - "elected-board", - "petition", + "do-ocracy", + "devolution", + "quadratic-governance", ]; export function getGovernanceTemplatesForHome(): GovernanceTemplateCatalogEntry[] { diff --git a/prisma/seed.ts b/prisma/seed.ts index cee12e2..12af21d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -234,7 +234,7 @@ const TEMPLATES: { description: "Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council.", sortOrder: 0, - featured: true, + featured: false, body: bodyFromSlugComposition("consensus-clusters"), }, { @@ -254,7 +254,7 @@ const TEMPLATES: { description: "An elected board determines policies and organizes their implementation.", sortOrder: 2, - featured: true, + featured: false, body: bodyFromSlugComposition("elected-board"), }, { @@ -264,7 +264,7 @@ const TEMPLATES: { description: "Any participant can propose a rule change. If enough sign it, it goes to a general vote.", sortOrder: 3, - featured: true, + featured: false, body: bodyFromSlugComposition("petition"), }, { @@ -304,7 +304,7 @@ const TEMPLATES: { description: "Authority is granted to those doing the work. If you do the task, you decide how it gets done.", sortOrder: 7, - featured: false, + featured: true, body: bodyFromSlugComposition("do-ocracy"), }, { @@ -314,7 +314,7 @@ const TEMPLATES: { description: "Voting cost is squared (V²), preventing a majority from steamrolling a passionate minority.", sortOrder: 8, - featured: false, + featured: true, body: bodyFromSlugComposition("quadratic-governance"), }, { @@ -334,7 +334,7 @@ const TEMPLATES: { description: "Starts as a Dictatorship for speed, moving to a Board, and finally to full community ownership.", sortOrder: 10, - featured: false, + featured: true, body: bodyFromSlugComposition("devolution"), }, { diff --git a/stories/cards/Rule.stories.js b/stories/cards/Rule.stories.js index 3bf7304..70705b1 100644 --- a/stories/cards/Rule.stories.js +++ b/stories/cards/Rule.stories.js @@ -1,5 +1,7 @@ import Rule from "../../app/components/cards/Rule"; import Image from "next/image"; +import { getAssetPath } from "../../lib/assetUtils"; +import { getGovernanceTemplatesForHome } from "../../lib/templates/governanceTemplateCatalog"; export default { title: "Components/Cards/Rule", @@ -323,66 +325,25 @@ export const AllVariants = { // eslint-disable-next-line no-unused-vars render: (_args) => (
- - } - onClick={() => console.log("Consensus clusters selected")} - /> - - } - onClick={() => console.log("Consensus selected")} - /> - - } - onClick={() => console.log("Elected Board selected")} - /> - - } - onClick={() => console.log("Petition selected")} - /> + {getGovernanceTemplatesForHome().map((entry) => ( + + } + onClick={() => console.log(`${entry.slug} template selected`)} + /> + ))}
), parameters: { diff --git a/tests/e2e/critical-journeys.spec.ts b/tests/e2e/critical-journeys.spec.ts index 73a349f..9e3f334 100644 --- a/tests/e2e/critical-journeys.spec.ts +++ b/tests/e2e/critical-journeys.spec.ts @@ -45,10 +45,10 @@ test.describe("Critical User Journeys", () => { ).toBeVisible(); // 6. User explores rule templates - await page.locator("text=Circles").first().click(); - await page.locator("text=Consensus").nth(1).click(); - await page.locator("text=Elected Board").first().click(); - await page.locator("text=Petition").first().click(); + await page.locator("text=Consensus").first().click(); + await page.locator("text=Do-ocracy").first().click(); + await page.locator("text=Devolution").first().click(); + await page.locator("text=Quadratic Governance").first().click(); // 7. User checks out features const features = [ diff --git a/tests/pages/page-flow.test.jsx b/tests/pages/page-flow.test.jsx index 6e510b4..72cf0a7 100644 --- a/tests/pages/page-flow.test.jsx +++ b/tests/pages/page-flow.test.jsx @@ -75,16 +75,16 @@ describe("Page Flow Integration", () => { ).toBeInTheDocument(); await waitFor(() => { - expect(screen.getByRole("heading", { name: "Circles" })).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "Consensus" })).toBeInTheDocument(); }); expect( - screen.getByRole("heading", { name: "Elected Board" }), + screen.getByRole("heading", { name: "Do-ocracy" }), ).toBeInTheDocument(); expect( - screen.getByRole("heading", { name: "Consensus" }), + screen.getByRole("heading", { name: "Devolution" }), ).toBeInTheDocument(); expect( - screen.getByRole("heading", { name: "Petition" }), + screen.getByRole("heading", { name: "Quadratic Governance" }), ).toBeInTheDocument(); await waitFor(() => { @@ -153,12 +153,12 @@ describe("Page Flow Integration", () => { renderPage(); await waitFor(() => { - expect(screen.getByText("Circles")).toBeInTheDocument(); + expect(screen.getByText("Consensus")).toBeInTheDocument(); }); expect(screen.queryByText("Solidarity Network")).not.toBeInTheDocument(); - expect(screen.getByText("Elected Board")).toBeInTheDocument(); - expect(screen.getByText("Consensus")).toBeInTheDocument(); - expect(screen.getByText("Petition")).toBeInTheDocument(); + expect(screen.getByText("Do-ocracy")).toBeInTheDocument(); + expect(screen.getByText("Devolution")).toBeInTheDocument(); + expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); const seeAll = screen.getByRole("link", { name: "See all templates" }); expect(seeAll).toHaveAttribute("href", "/templates"); @@ -228,7 +228,7 @@ describe("Page Flow Integration", () => { }); await waitFor(() => { - expect(screen.getByText("Circles")).toBeInTheDocument(); + expect(screen.getByText("Consensus")).toBeInTheDocument(); }); await waitFor(() => { diff --git a/tests/pages/user-journey.test.jsx b/tests/pages/user-journey.test.jsx index fac5802..2b7caba 100644 --- a/tests/pages/user-journey.test.jsx +++ b/tests/pages/user-journey.test.jsx @@ -56,11 +56,11 @@ describe("User Journey Integration", () => { renderPage(); await waitFor(() => { - expect(screen.getByText("Circles")).toBeInTheDocument(); + expect(screen.getByText("Consensus")).toBeInTheDocument(); }); - expect(screen.getByText("Elected Board")).toBeInTheDocument(); - expect(screen.getByText("Consensus")).toBeInTheDocument(); - expect(screen.getByText("Petition")).toBeInTheDocument(); + expect(screen.getByText("Do-ocracy")).toBeInTheDocument(); + expect(screen.getByText("Devolution")).toBeInTheDocument(); + expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); const seeHowLinks = screen.getAllByRole("link", { name: "See how it works", @@ -195,7 +195,7 @@ describe("User Journey Integration", () => { }); await waitFor(() => { - expect(screen.getAllByText(/Circles/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Consensus/i).length).toBeGreaterThan(0); }); await waitFor(() => { diff --git a/tests/unit/RuleStack.test.jsx b/tests/unit/RuleStack.test.jsx index 74fa516..7760893 100644 --- a/tests/unit/RuleStack.test.jsx +++ b/tests/unit/RuleStack.test.jsx @@ -61,7 +61,7 @@ afterEach(() => { async function waitForRuleStackCards() { await waitFor(() => { - expect(screen.getByText("Circles")).toBeInTheDocument(); + expect(screen.getByText("Consensus")).toBeInTheDocument(); }); } @@ -70,7 +70,7 @@ describe("RuleStack Component", () => { const fetchMock = vi.mocked(global.fetch); const callsBefore = fetchMock.mock.calls.length; render(); - expect(screen.getByText("Circles")).toBeInTheDocument(); + expect(screen.getByText("Consensus")).toBeInTheDocument(); expect(fetchMock.mock.calls.length).toBe(callsBefore); }); @@ -120,20 +120,24 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - expect( - screen.getByText(/Units called Circles have the ability to decide/), - ).toBeInTheDocument(); expect( screen.getByText( /Important decisions require unanimous agreement\. Proposals pass only if no serious objections remain\./, ), ).toBeInTheDocument(); expect( - screen.getByText(/An elected board determines policies/), + screen.getByText( + /Authority is granted to those doing the work\. If you do the task, you decide how it gets done\./, + ), ).toBeInTheDocument(); expect( screen.getByText( - /Any participant can propose a rule change\. If enough sign it/, + /Starts as a Dictatorship for speed, moving to a Board, and finally to full community ownership\./, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + /Voting cost is squared \(V²\), preventing a majority from steamrolling a passionate minority\./, ), ).toBeInTheDocument(); }); @@ -143,24 +147,19 @@ describe("RuleStack Component", () => { await waitForRuleStackCards(); const imgs = container.querySelectorAll("img"); - const circles = [...imgs].find((el) => { - const s = el.getAttribute("src") ?? ""; - return ( - s.includes("template-mark/consensus-clusters") || - s.includes("template-mark%2Fconsensus-clusters") - ); - }); const consensus = [...imgs].find((el) => { const s = el.getAttribute("src") ?? ""; return ( - s.includes("consensus") && - !s.includes("consensus-clusters") && - !s.includes("elected") && - !s.includes("petition") + s.includes("template-mark/consensus") && + !s.includes("consensus-clusters") ); }); - expect(circles).toBeTruthy(); + const doOcracy = [...imgs].find((el) => { + const s = el.getAttribute("src") ?? ""; + return s.includes("template-mark/do-ocracy"); + }); expect(consensus).toBeTruthy(); + expect(doOcracy).toBeTruthy(); }); test("renders see-all-templates link to full templates page", async () => { @@ -203,15 +202,15 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - const circlesCard = screen - .getByText("Circles") - .closest('[class*="bg-[var(--color-surface-invert-brand-teal)]"]'); - expect(circlesCard).toBeInTheDocument(); - const consensusCard = screen .getByText("Consensus") .closest('[class*="bg-[var(--color-surface-invert-positive-secondary)]"]'); expect(consensusCard).toBeInTheDocument(); + + const doOcracyCard = screen + .getByText("Do-ocracy") + .closest('[class*="bg-[var(--color-surface-invert-brand-royal)]"]'); + expect(doOcracyCard).toBeInTheDocument(); }); test("handles template click events for featured templates", async () => { @@ -284,31 +283,31 @@ describe("RuleStack Component", () => { await waitForRuleStackCards(); const imgs = container.querySelectorAll("img"); - const circlesIcon = [...imgs].find((el) => { + const doOcracyIcon = [...imgs].find((el) => { const s = el.getAttribute("src") ?? ""; return ( - s.includes("template-mark/consensus-clusters") || - s.includes("template-mark%2Fconsensus-clusters") + s.includes("template-mark/do-ocracy") || + s.includes("template-mark%2Fdo-ocracy") ); }); - expect(circlesIcon).toBeTruthy(); - expect(circlesIcon?.getAttribute("src")).toMatch( - /template-mark(?:%2F|\/)consensus-clusters/, + expect(doOcracyIcon).toBeTruthy(); + expect(doOcracyIcon?.getAttribute("src")).toMatch( + /template-mark(?:%2F|\/)do-ocracy/, ); - expect(circlesIcon?.className).toMatch( + expect(doOcracyIcon?.className).toMatch( /min-\[640px\]:max-\[1023px\]:w-\[56px\]/, ); - expect(circlesIcon?.className).toMatch( + expect(doOcracyIcon?.className).toMatch( /min-\[640px\]:max-\[1023px\]:h-\[56px\]/, ); - expect(circlesIcon?.className).toMatch( + expect(doOcracyIcon?.className).toMatch( /min-\[1024px\]:max-\[1439px\]:w-\[90px\]/, ); - expect(circlesIcon?.className).toMatch( + expect(doOcracyIcon?.className).toMatch( /min-\[1024px\]:max-\[1439px\]:h-\[90px\]/, ); - expect(circlesIcon?.className).toMatch(/min-\[1440px\]:w-\[90px\]/); - expect(circlesIcon?.className).toMatch(/min-\[1440px\]:h-\[90px\]/); + expect(doOcracyIcon?.className).toMatch(/min-\[1440px\]:w-\[90px\]/); + expect(doOcracyIcon?.className).toMatch(/min-\[1440px\]:h-\[90px\]/); }); test("applies different background colors to featured cards", async () => { @@ -370,14 +369,14 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - const electedBoardCard = screen.getByText("Elected Board").closest("div"); - await user.click(electedBoardCard); + const doOcracyCard = screen.getByText("Do-ocracy").closest("div"); + await user.click(doOcracyCard); expect(gtagSpy).toHaveBeenCalledWith("event", "template_click", { - template_slug: "elected-board", + template_slug: "do-ocracy", }); expect(analyticsSpy).toHaveBeenCalledWith("Template Clicked", { - templateSlug: "elected-board", + templateSlug: "do-ocracy", }); }); }); diff --git a/tests/unit/governanceTemplateCatalog.test.ts b/tests/unit/governanceTemplateCatalog.test.ts new file mode 100644 index 0000000..81b79c4 --- /dev/null +++ b/tests/unit/governanceTemplateCatalog.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { + GOVERNANCE_TEMPLATE_CATALOG, + GOVERNANCE_TEMPLATE_HOME_SLUGS, + getGovernanceTemplatesForHome, +} from "../../lib/templates/governanceTemplateCatalog"; + +/** + * Figma Community-Rule-System node 21764-16435 — Card / Rule template surfaces. + * Token names and hex fallbacks from Dev Mode (May 2026). + */ +const FIGMA_TEMPLATE_SURFACE_BY_SLUG: Record = { + consensus: "--color-surface-invert-positive-secondary", + "consensus-clusters": "--color-surface-invert-brand-teal", + "solidarity-network": "--color-surface-invert-positive-primary", + "sortition-jury": "--color-surface-invert-brand-lavender", + "liquid-democracy": "--color-surface-invert-brand-kiwi", + "do-ocracy": "--color-surface-invert-brand-royal", + "quadratic-governance": "--color-surface-invert-brand-secondary", + "federated-clusters": "--color-surface-invert-brand-primary", + devolution: "--color-surface-invert-negative-secondary", + "benevolent-dictator": "--color-surface-invert-negative-primary", + petition: "--color-surface-invert-brand-teal", + "self-appointed-board": "--color-surface-invert-brand-rust", + "elected-board": "--color-surface-invert-warning-secondary", +}; + +describe("governanceTemplateCatalog (Figma 21764-16435)", () => { + it("maps every catalog slug to the Figma invert surface token", () => { + for (const entry of GOVERNANCE_TEMPLATE_CATALOG) { + const expected = FIGMA_TEMPLATE_SURFACE_BY_SLUG[entry.slug]; + expect(expected, `missing Figma mapping for ${entry.slug}`).toBeTruthy(); + expect(entry.backgroundColor).toBe(`bg-[var(${expected})]`); + } + }); + + it("covers all thirteen Figma template variants", () => { + expect(GOVERNANCE_TEMPLATE_CATALOG).toHaveLength(13); + expect(Object.keys(FIGMA_TEMPLATE_SURFACE_BY_SLUG)).toHaveLength(13); + }); + + it("orders the home RuleStack row per Figma 22083-855584", () => { + expect([...GOVERNANCE_TEMPLATE_HOME_SLUGS]).toEqual([ + "consensus", + "do-ocracy", + "devolution", + "quadratic-governance", + ]); + expect(getGovernanceTemplatesForHome()).toHaveLength(4); + }); +});