diff --git a/app/(marketing)/blog/[slug]/page.tsx b/app/(marketing)/blog/[slug]/page.tsx index 00584fb..1ce8706 100644 --- a/app/(marketing)/blog/[slug]/page.tsx +++ b/app/(marketing)/blog/[slug]/page.tsx @@ -126,7 +126,7 @@ export default async function BlogPostPage({ params }: PageProps) { headline: post.frontmatter.title, description: post.frontmatter.description, author: { - "@type": "Person", + "@type": "Organization", name: post.frontmatter.author, }, publisher: { diff --git a/app/components/controls/Chip/Chip.view.tsx b/app/components/controls/Chip/Chip.view.tsx index 7620104..d41a91e 100644 --- a/app/components/controls/Chip/Chip.view.tsx +++ b/app/components/controls/Chip/Chip.view.tsx @@ -155,6 +155,7 @@ function ChipView({ tabIndex={0} onClick={handleClick} onKeyDown={(e) => { + if (e.target instanceof HTMLInputElement) return; if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(e as unknown as React.MouseEvent); diff --git a/app/components/navigation/Footer.tsx b/app/components/navigation/Footer.tsx index 02a7529..a897281 100644 --- a/app/components/navigation/Footer.tsx +++ b/app/components/navigation/Footer.tsx @@ -25,7 +25,7 @@ const Footer = memo(() => { /** Figma 18411:62925 (1024+): org name is one line, `w-full whitespace-nowrap`. */ const orgNameClass = `${bodyTextClass} lg:whitespace-nowrap`; - const primaryLinkClass = `inline-flex w-fit max-w-full self-start text-[var(--color-content-default-primary)] font-inter text-base font-medium leading-5 tracking-[0%] ${linkFocusClass} p-2 -m-2 cursor-pointer lg:text-2xl lg:font-normal lg:leading-7`; + const primaryLinkClass = `inline-flex w-fit max-w-full shrink-0 whitespace-nowrap self-start md:self-end text-[var(--color-content-default-primary)] font-inter text-base font-medium leading-5 tracking-[0%] ${linkFocusClass} p-2 -m-2 cursor-pointer lg:text-2xl lg:font-normal lg:leading-7`; /** Figma 18411:62944: 40px gaps, w-[396px] link block; `p-2` on links overruns 396px—tighten x at `md+` row. */ const legalLinkClass = `inline-flex w-fit max-w-full self-start text-[var(--color-content-default-secondary)] font-inter text-sm font-normal leading-5 tracking-[0%] ${linkFocusClass} p-2 -m-2 cursor-pointer underline decoration-solid [text-decoration-skip-ink:none] md:self-auto md:px-0 md:py-1 md:mx-0 md:text-xs md:leading-4 md:whitespace-nowrap md:no-underline md:text-[var(--color-content-default-primary)] lg:text-sm lg:leading-5 lg:text-[var(--color-content-default-primary)]`; @@ -37,7 +37,11 @@ const Footer = memo(() => { name: t("organization.name"), email: t("organization.email"), url: t("organization.url"), - sameAs: [t("social.bluesky.url"), t("social.gitlab.url")], + sameAs: [ + t("social.bluesky.url"), + t("social.gitea.url"), + t("social.mastodon.url"), + ], }; return ( @@ -104,27 +108,42 @@ const Footer = memo(() => { {/* eslint-disable-next-line @next/next/no-img-element -- social logo */} Bluesky -
{t("social.bluesky.handle")}
+
{t("social.bluesky.label")}
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */} GitLab -
{t("social.gitlab.handle")}
+
{t("social.gitea.label")}
+
+ + {/* eslint-disable-next-line @next/next/no-img-element -- social icon */} + +
{t("social.mastodon.label")}
diff --git a/app/components/navigation/MenuItem/MenuItem.container.tsx b/app/components/navigation/MenuItem/MenuItem.container.tsx index 0101668..b340ee7 100644 --- a/app/components/navigation/MenuItem/MenuItem.container.tsx +++ b/app/components/navigation/MenuItem/MenuItem.container.tsx @@ -70,15 +70,15 @@ const MenuItemContainer = memo( "border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]", }; - // State styles for Inverse mode (black text on yellow background) + // State styles for Inverse mode (black text on yellow HeaderTab / inverse surfaces) const inverseModeStyles: Record<"default" | "hover" | "selected", string> = { default: - "bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]", + "bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-inverse-brand-secondary)] hover:text-[var(--color-content-inverse-primary,black)]", hover: - "bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]", + "bg-[var(--color-surface-inverse-brand-secondary)] text-[var(--color-content-inverse-primary,black)]", selected: - "border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]", + "border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-inverse-brand-secondary)]", }; // Get state styles based on mode diff --git a/app/components/sections/LogoWall/LogoWall.container.tsx b/app/components/sections/LogoWall/LogoWall.container.tsx index 29e4168..4e9c079 100644 --- a/app/components/sections/LogoWall/LogoWall.container.tsx +++ b/app/components/sections/LogoWall/LogoWall.container.tsx @@ -20,37 +20,31 @@ const LogoWallContainer = memo(({ logos, className = "" }) => { src: getAssetPath(partnerLogoPath("food-not-bombs")), alt: t("partners.foodNotBombs"), size: "h-11 lg:h-14 xl:h-[70px]", - order: "order-1 sm:order-4", }, { src: getAssetPath(partnerLogoPath("start-coop")), alt: t("partners.startCoop"), size: "h-[42px] lg:h-[53px] xl:h-[66px]", - order: "order-2 sm:order-2", }, { src: getAssetPath(partnerLogoPath("metagov")), alt: t("partners.metagov"), size: "h-6 lg:h-8 xl:h-[41px]", - order: "order-3 sm:order-1", }, { src: getAssetPath(partnerLogoPath("open-civics")), alt: t("partners.openCivics"), size: "h-8 lg:h-10 xl:h-[50px]", - order: "order-4 sm:order-5 md:order-6", }, { src: getAssetPath(partnerLogoPath("mutual-aid-co")), alt: t("partners.mutualAidCo"), size: "h-11 lg:h-14 xl:h-[70px]", - order: "order-5 sm:order-6 md:order-5", }, { src: getAssetPath(partnerLogoPath("cu-boulder")), alt: t("partners.cuBoulder"), size: "h-10 lg:h-12 xl:h-[60px]", - order: "order-6 sm:order-3", }, ], [t], diff --git a/app/components/sections/LogoWall/LogoWall.view.tsx b/app/components/sections/LogoWall/LogoWall.view.tsx index 2384436..5da57d7 100644 --- a/app/components/sections/LogoWall/LogoWall.view.tsx +++ b/app/components/sections/LogoWall/LogoWall.view.tsx @@ -14,18 +14,12 @@ function LogoWallView({ className={`p-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-032)] lg:px-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-048)] xl:px-[160px] xl:py-[var(--spacing-scale-064)] ${className}`} >
- {/* Label */} -

- Trusted by leading cooperators -

- - {/* Logo Grid Container */}
-
+
{displayLogos.map((logo, index) => (
+ + + + + + + + + + \ No newline at end of file diff --git a/public/assets/logos/mastodon.svg b/public/assets/logos/mastodon.svg new file mode 100644 index 0000000..7a2c52c --- /dev/null +++ b/public/assets/logos/mastodon.svg @@ -0,0 +1,6 @@ + + + diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png index b1dcb03..abed7ee 100644 Binary files a/public/favicon-16x16.png and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png index 1cc4463..2407f5b 100644 Binary files a/public/favicon-32x32.png and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 08f3640..58c1c93 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/scripts/generate-favicons.mjs b/scripts/generate-favicons.mjs index 40df89c..06832b4 100644 --- a/scripts/generate-favicons.mjs +++ b/scripts/generate-favicons.mjs @@ -3,6 +3,8 @@ * Regenerate root favicon binaries from `public/assets/logos/community-rule.svg`. * Safari and iOS need PNG/ICO fallbacks; SVG alone shows a letter fallback in Safari. * + * Cream mark (#FFFDD2) on a transparent canvas — matches the brand SVG. + * * Run: npm run generate:favicons */ import fs from "node:fs/promises"; @@ -14,42 +16,24 @@ import pngToIco from "png-to-ico"; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const PUBLIC = path.join(ROOT, "public"); const SVG_PATH = path.join(PUBLIC, "assets/logos/community-rule.svg"); -const LOGO_FILL = "#FFFDD2"; -const MARK_ON_LIGHT = "#000000"; async function readLogoSvg() { return fs.readFile(SVG_PATH, "utf8"); } -async function markPng(svg, size, fill) { - const tinted = svg.replaceAll(LOGO_FILL, fill); - return sharp(Buffer.from(tinted)) - .resize(size, size, { fit: "contain" }) - .png() - .toBuffer(); -} - -async function creamMarkOnBlack(svg, size) { - const logoSize = Math.round(size * 0.75); - const logo = await markPng(svg, logoSize, LOGO_FILL); - return sharp({ - create: { - width: size, - height: size, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 1 }, - }, - }) - .composite([{ input: logo, gravity: "center" }]) +/** Resize the logo SVG to a PNG with alpha (transparent background). */ +async function creamMarkTransparent(svg, size) { + return sharp(Buffer.from(svg)) + .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) .png() .toBuffer(); } async function main() { const svg = await readLogoSvg(); - const png16 = await markPng(svg, 16, MARK_ON_LIGHT); - const png32 = await markPng(svg, 32, MARK_ON_LIGHT); - const appleTouch = await creamMarkOnBlack(svg, 180); + const png16 = await creamMarkTransparent(svg, 16); + const png32 = await creamMarkTransparent(svg, 32); + const appleTouch = await creamMarkTransparent(svg, 180); const faviconIco = await pngToIco([png16, png32]); await Promise.all([ diff --git a/stories/cards/Icon.stories.js b/stories/cards/Icon.stories.js index eb9a76d..e8d0cb3 100644 --- a/stories/cards/Icon.stories.js +++ b/stories/cards/Icon.stories.js @@ -46,7 +46,7 @@ const WorkerCoopIcon = () => ( export const Default = { args: { icon: , - title: "Worker's cooperatives", + title: "Worker cooperatives", description: "Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations.", }, @@ -64,7 +64,7 @@ export const WithLongTitle = { export const WithShortDescription = { args: { icon: , - title: "Worker's cooperatives", + title: "Worker cooperatives", description: "Short description", }, }; diff --git a/stories/navigation/Footer.responsive.stories.js b/stories/navigation/Footer.responsive.stories.js index e1036c1..8d9dbf0 100644 --- a/stories/navigation/Footer.responsive.stories.js +++ b/stories/navigation/Footer.responsive.stories.js @@ -222,10 +222,15 @@ export const Interactive = { }); await userEvent.click(blueskyLink); - const gitlabLink = canvas.getByRole("link", { - name: /follow us on gitlab/i, + const giteaLink = canvas.getByRole("link", { + name: /view source on gitea/i, }); - await userEvent.click(gitlabLink); + await userEvent.click(giteaLink); + + const mastodonLink = canvas.getByRole("link", { + name: /follow us on mastodon/i, + }); + await userEvent.click(mastodonLink); }); }, }; @@ -264,10 +269,15 @@ export const HoverStates = { await userEvent.hover(blueskyLink); await new Promise((resolve) => setTimeout(resolve, 100)); - const gitlabLink = canvas.getByRole("link", { - name: /follow us on gitlab/i, + const giteaLink = canvas.getByRole("link", { + name: /view source on gitea/i, }); - await userEvent.hover(gitlabLink); + await userEvent.hover(giteaLink); + + const mastodonLink = canvas.getByRole("link", { + name: /follow us on mastodon/i, + }); + await userEvent.hover(mastodonLink); await new Promise((resolve) => setTimeout(resolve, 100)); }); }, diff --git a/stories/sections/Groups.stories.js b/stories/sections/Groups.stories.js index 0bac78b..d76951c 100644 --- a/stories/sections/Groups.stories.js +++ b/stories/sections/Groups.stories.js @@ -20,7 +20,7 @@ const sampleItems = [ width={36} /> ), - title: "Worker's cooperatives", + title: "Worker cooperatives", description: "Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations.", }, diff --git a/stories/sections/LogoWall.stories.js b/stories/sections/LogoWall.stories.js index d7861e1..c4fd586 100644 --- a/stories/sections/LogoWall.stories.js +++ b/stories/sections/LogoWall.stories.js @@ -13,9 +13,9 @@ export default { - **Mobile**: 3 rows × 2 columns grid with 32px gaps - **SM**: 2 rows × 3 columns grid with 48px row gap and 32px column gap -- **MD**: Single row with space-between layout and 24px gap between text and logos +- **MD+**: Centered flex-wrap row of logos - **LG**: Larger logo sizes and 64px horizontal padding -- **XL**: Largest logo sizes, 160px horizontal padding, and 14px label text +- **XL**: Largest logo sizes and 160px horizontal padding ## Animations & Transitions @@ -28,7 +28,7 @@ export default { ## Props -- **logos** (optional): Array of logo objects with src, alt, size, and order properties. If not provided, uses default partner logos. +- **logos** (optional): Array of logo objects with src, alt, and size properties. If not provided, uses default partner logos. ## Usage Examples @@ -40,13 +40,11 @@ export default { src: "assets/logos/partners/cu-boulder.svg", alt: "CU Boulder", size: "h-10 lg:h-12 xl:h-[60px]", - order: "order-1 sm:order-2" }, { src: "assets/logos/partners/food-not-bombs.svg", alt: "Food Not Bombs", size: "h-11 lg:h-14 xl:h-[70px]", - order: "order-2 sm:order-1" } ]} /> @@ -65,7 +63,7 @@ This will fall back to the default partner logos.`, logos: { control: "object", description: - "Array of logo objects with src, alt, size, and order properties. If not provided, uses default partner logos.", + "Array of logo objects with src, alt, and size properties. If not provided, uses default partner logos.", }, }, }; diff --git a/tests/components/Footer.test.tsx b/tests/components/Footer.test.tsx index 827d699..3b0b055 100644 --- a/tests/components/Footer.test.tsx +++ b/tests/components/Footer.test.tsx @@ -54,7 +54,10 @@ describe("Footer (behavioral tests)", () => { screen.getAllByRole("link", { name: "Follow us on Bluesky" }).length, ).toBeGreaterThan(0); expect( - screen.getAllByRole("link", { name: "Follow us on GitLab" }).length, + screen.getAllByRole("link", { name: "View source on Gitea" }).length, + ).toBeGreaterThan(0); + expect( + screen.getAllByRole("link", { name: "Follow us on Mastodon" }).length, ).toBeGreaterThan(0); }); @@ -74,7 +77,7 @@ describe("Footer (behavioral tests)", () => { it("renders navigation links with baseline width-fit focus targets", () => { render(
); const useCases = screen.getAllByRole("link", { name: "Use cases" })[0]; - expect(useCases).toHaveClass("w-fit", "self-start"); + expect(useCases).toHaveClass("w-fit", "self-start", "md:self-end"); expect(useCases).not.toHaveClass("w-full"); }); diff --git a/tests/components/Icon.test.tsx b/tests/components/Icon.test.tsx index 94a5322..a419e06 100644 --- a/tests/components/Icon.test.tsx +++ b/tests/components/Icon.test.tsx @@ -12,7 +12,7 @@ type IconProps = React.ComponentProps; const baseProps: IconProps = { icon:
Icon
, - title: "Worker's cooperatives", + title: "Worker cooperatives", description: "Employee-owned businesses often need to clarify how power is shared", }; @@ -89,12 +89,12 @@ describe("Icon (behavioral tests)", () => { render( Icon
} - title="Worker's cooperatives" + title="Worker cooperatives" description="Employee-owned businesses" />, ); expect(screen.getByTestId("icon")).toBeInTheDocument(); - expect(screen.getByText("Worker's cooperatives")).toBeInTheDocument(); + expect(screen.getByText("Worker cooperatives")).toBeInTheDocument(); expect(screen.getByText("Employee-owned businesses")).toBeInTheDocument(); }); diff --git a/tests/components/MultiSelect.test.tsx b/tests/components/MultiSelect.test.tsx index 464ddb0..7696b68 100644 --- a/tests/components/MultiSelect.test.tsx +++ b/tests/components/MultiSelect.test.tsx @@ -166,6 +166,30 @@ describe("MultiSelect – behaviour specifics", () => { expect(handleConfirm).toHaveBeenCalledWith("custom-1", "NewOption"); }); + it("allows spaces in custom chip labels", async () => { + const handleConfirm = vi.fn(); + const customOptions = [ + { id: "custom-1", label: "", state: "custom" as const }, + ]; + render( + , + ); + + const input = screen.getByPlaceholderText("Type to add"); + await userEvent.type(input, "Mutual aid network"); + + const checkButton = screen.getByRole("button", { name: "Confirm" }); + await userEvent.click(checkButton); + + expect(handleConfirm).toHaveBeenCalledWith( + "custom-1", + "Mutual aid network", + ); + }); + it("handles custom chip close", async () => { const handleClose = vi.fn(); const customOptions = [ diff --git a/tests/e2e/critical-journeys.spec.ts b/tests/e2e/critical-journeys.spec.ts index ad7736a..d17fcc3 100644 --- a/tests/e2e/critical-journeys.spec.ts +++ b/tests/e2e/critical-journeys.spec.ts @@ -101,9 +101,7 @@ test.describe("Critical User Journeys", () => { // Check key components are rendered await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible(); - await expect( - page.locator("text=Trusted by leading cooperators"), - ).toBeVisible(); + await expect(page.locator('img[alt="Food Not Bombs"]')).toBeVisible(); await expect(page.locator("text=Jo Freeman")).toBeVisible(); }); diff --git a/tests/pages/learn.test.tsx b/tests/pages/learn.test.tsx index 560a66d..8fac465 100644 --- a/tests/pages/learn.test.tsx +++ b/tests/pages/learn.test.tsx @@ -31,7 +31,7 @@ const mockPosts = [ frontmatter: { title: "Resolving Active Conflicts", description: "Practical steps for resolving conflicts", - author: "Author name", + author: "CommunityRule", date: "2025-04-15", thumbnail: { vertical: "resolving-active-conflicts-vertical.svg", @@ -48,7 +48,7 @@ const mockPosts = [ frontmatter: { title: "Operational Security for Mutual Aid", description: "Tactics to protect members", - author: "Author name", + author: "CommunityRule", date: "2025-04-10", thumbnail: { vertical: "operational-security-mutual-aid-vertical.svg", diff --git a/tests/pages/user-journey.test.jsx b/tests/pages/user-journey.test.jsx index 2b7caba..cdf4164 100644 --- a/tests/pages/user-journey.test.jsx +++ b/tests/pages/user-journey.test.jsx @@ -134,12 +134,16 @@ describe("User Journey Integration", () => { const blueskyLink = screen.getByRole("link", { name: "Follow us on Bluesky", }); - const gitlabLink = screen.getByRole("link", { - name: "Follow us on GitLab", + const giteaLink = screen.getByRole("link", { + name: "View source on Gitea", + }); + const mastodonLink = screen.getByRole("link", { + name: "Follow us on Mastodon", }); expect(blueskyLink).toBeInTheDocument(); - expect(gitlabLink).toBeInTheDocument(); + expect(giteaLink).toBeInTheDocument(); + expect(mastodonLink).toBeInTheDocument(); }); test("user explores features and benefits", async () => { @@ -179,9 +183,11 @@ describe("User Journey Integration", () => { }); const blueskyLink = screen.getByRole("link", { name: /Bluesky/i }); - const gitlabLink = screen.getByRole("link", { name: /GitLab/i }); + const giteaLink = screen.getByRole("link", { name: /Gitea/i }); + const mastodonLink = screen.getByRole("link", { name: /Mastodon/i }); expect(blueskyLink).toBeInTheDocument(); - expect(gitlabLink).toBeInTheDocument(); + expect(giteaLink).toBeInTheDocument(); + expect(mastodonLink).toBeInTheDocument(); }); test("user completes the full journey from discovery to action", async () => { diff --git a/tests/unit/LogoWall.test.jsx b/tests/unit/LogoWall.test.jsx index be366a2..904cdab 100644 --- a/tests/unit/LogoWall.test.jsx +++ b/tests/unit/LogoWall.test.jsx @@ -42,14 +42,6 @@ describe("LogoWall Component", () => { expect(screen.queryByAltText("Food Not Bombs")).not.toBeInTheDocument(); }); - test("renders section label", () => { - render(); - - expect( - screen.getByText("Trusted by leading cooperators"), - ).toBeInTheDocument(); - }); - test("applies correct CSS classes", () => { render(); @@ -74,7 +66,12 @@ describe("LogoWall Component", () => { '[class*="grid grid-cols-2 grid-rows-3"]', ); expect(grid).toBeInTheDocument(); - expect(grid).toHaveClass("sm:grid-cols-3", "sm:grid-rows-2", "md:flex"); + expect(grid).toHaveClass( + "sm:grid-cols-3", + "sm:grid-rows-2", + "md:flex", + "md:justify-center", + ); }); test("renders logos with correct attributes", () => { @@ -88,15 +85,6 @@ describe("LogoWall Component", () => { expect(foodNotBombsLogo).toHaveClass("h-11", "lg:h-14", "xl:h-[70px]"); }); - test("applies order classes for responsive layout", () => { - render(); - - const foodNotBombsContainer = screen - .getByAltText("Food Not Bombs") - .closest("div"); - expect(foodNotBombsContainer).toHaveClass("order-1", "sm:order-4"); - }); - test("handles empty logos array", () => { render(); @@ -119,9 +107,7 @@ describe("LogoWall Component", () => { const section = document.querySelector("section"); expect(section).toBeInTheDocument(); - // Check for the label - const label = screen.getByText("Trusted by leading cooperators"); - expect(label).toBeInTheDocument(); + expect(screen.queryByText("Trusted by leading cooperators")).not.toBeInTheDocument(); }); test("applies transition effects", () => {