Update template cards
This commit is contained in:
+1
-1
@@ -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);
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
+6
-6
@@ -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"),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
|
||||
<Rule
|
||||
title="Consensus clusters"
|
||||
description="Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council."
|
||||
backgroundColor="bg-[var(--color-surface-default-brand-lime)]"
|
||||
icon={
|
||||
<Image
|
||||
src="assets/template-mark/consensus-clusters.svg"
|
||||
alt="Sociocracy"
|
||||
width={40}
|
||||
height={40}
|
||||
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log("Consensus clusters selected")}
|
||||
/>
|
||||
<Rule
|
||||
title="Consensus"
|
||||
description="Decisions that affect the group collectively should involve participation of all participants."
|
||||
backgroundColor="bg-[var(--color-surface-default-brand-rust)]"
|
||||
icon={
|
||||
<Image
|
||||
src="assets/template-mark/consensus.svg"
|
||||
alt="Consensus"
|
||||
width={40}
|
||||
height={40}
|
||||
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log("Consensus selected")}
|
||||
/>
|
||||
<Rule
|
||||
title="Elected Board"
|
||||
description="An elected board determines policies and organizes their implementation."
|
||||
backgroundColor="bg-[var(--color-surface-default-brand-red)]"
|
||||
icon={
|
||||
<Image
|
||||
src="assets/template-mark/elected-board.svg"
|
||||
alt="Elected Board"
|
||||
width={40}
|
||||
height={40}
|
||||
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log("Elected Board selected")}
|
||||
/>
|
||||
<Rule
|
||||
title="Petition"
|
||||
description="All participants can propose and vote on proposals for the group."
|
||||
backgroundColor="bg-[var(--color-surface-default-brand-teal)]"
|
||||
icon={
|
||||
<Image
|
||||
src="assets/template-mark/petition.svg"
|
||||
alt="Petition"
|
||||
width={40}
|
||||
height={40}
|
||||
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log("Petition selected")}
|
||||
/>
|
||||
{getGovernanceTemplatesForHome().map((entry) => (
|
||||
<Rule
|
||||
key={entry.slug}
|
||||
title={entry.title}
|
||||
description={entry.description}
|
||||
backgroundColor={entry.backgroundColor}
|
||||
templateGridFigmaShell
|
||||
icon={
|
||||
<Image
|
||||
src={getAssetPath(entry.iconPath)}
|
||||
alt={entry.title}
|
||||
width={40}
|
||||
height={40}
|
||||
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log(`${entry.slug} template selected`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(<RuleStack initialGridEntries={homeFeatured} />);
|
||||
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(<RuleStack />);
|
||||
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(<RuleStack />);
|
||||
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(<RuleStack />);
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user