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);
+ });
+});