Backend / staging cleanup, performance substrate, and create-flow polish #60

Merged
an.di merged 16 commits from adilallo/Backend/StagingCleanup into main 2026-05-26 15:11:47 +00:00
39 changed files with 167 additions and 124 deletions
Showing only changes of commit 6b45a2e5d0 - Show all commits
+1 -1
View File
@@ -126,7 +126,7 @@ export default async function BlogPostPage({ params }: PageProps) {
headline: post.frontmatter.title, headline: post.frontmatter.title,
description: post.frontmatter.description, description: post.frontmatter.description,
author: { author: {
"@type": "Person", "@type": "Organization",
name: post.frontmatter.author, name: post.frontmatter.author,
}, },
publisher: { publisher: {
@@ -155,6 +155,7 @@ function ChipView({
tabIndex={0} tabIndex={0}
onClick={handleClick} onClick={handleClick}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.target instanceof HTMLInputElement) return;
if (e.key === "Enter" || e.key === " ") { if (e.key === "Enter" || e.key === " ") {
e.preventDefault(); e.preventDefault();
handleClick(e as unknown as React.MouseEvent<HTMLButtonElement>); handleClick(e as unknown as React.MouseEvent<HTMLButtonElement>);
+28 -9
View File
@@ -25,7 +25,7 @@ const Footer = memo(() => {
/** Figma 18411:62925 (1024+): org name is one line, `w-full whitespace-nowrap`. */ /** Figma 18411:62925 (1024+): org name is one line, `w-full whitespace-nowrap`. */
const orgNameClass = `${bodyTextClass} lg: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. */ /** 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)]`; 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"), name: t("organization.name"),
email: t("organization.email"), email: t("organization.email"),
url: t("organization.url"), 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 ( return (
@@ -104,27 +108,42 @@ const Footer = memo(() => {
{/* eslint-disable-next-line @next/next/no-img-element -- social logo */} {/* eslint-disable-next-line @next/next/no-img-element -- social logo */}
<img <img
src={getAssetPath(ASSETS.BLUESKY_LOGO)} src={getAssetPath(ASSETS.BLUESKY_LOGO)}
alt="Bluesky" alt=""
width={24} width={24}
height={22} height={22}
className="h-[21px] w-[24px] flex-shrink-0 transition-transform group-hover:scale-110" className="h-[21px] w-[24px] flex-shrink-0 transition-transform group-hover:scale-110"
/> />
<div className={bodyTextClass}>{t("social.bluesky.handle")}</div> <div className={bodyTextClass}>{t("social.bluesky.label")}</div>
</a> </a>
<a <a
href={t("social.gitlab.url")} href={t("social.gitea.url")}
className={`group inline-flex w-fit max-w-full items-center gap-[var(--spacing-measures-spacing-06,6px)] ${linkFocusClass} p-2 -m-2 cursor-pointer`} className={`group inline-flex w-fit max-w-full items-center gap-[var(--spacing-measures-spacing-06,6px)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
aria-label={t("social.gitlab.ariaLabel")} aria-label={t("social.gitea.ariaLabel")}
> >
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */} {/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
<img <img
src={getAssetPath(ASSETS.GITLAB_ICON)} src={getAssetPath(ASSETS.GITEA_ICON)}
alt="GitLab" alt=""
width={22} width={22}
height={22} height={22}
className="h-5 w-[22px] flex-shrink-0 grayscale transition-transform group-hover:scale-110" className="h-5 w-[22px] flex-shrink-0 grayscale transition-transform group-hover:scale-110"
/> />
<div className={bodyTextClass}>{t("social.gitlab.handle")}</div> <div className={bodyTextClass}>{t("social.gitea.label")}</div>
</a>
<a
href={t("social.mastodon.url")}
className={`group inline-flex w-fit max-w-full items-center gap-[var(--spacing-measures-spacing-06,6px)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
aria-label={t("social.mastodon.ariaLabel")}
>
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
<img
src={getAssetPath(ASSETS.MASTODON_LOGO)}
alt=""
width={22}
height={22}
className="h-5 w-[22px] flex-shrink-0 grayscale transition-transform group-hover:scale-110"
/>
<div className={bodyTextClass}>{t("social.mastodon.label")}</div>
</a> </a>
</div> </div>
</div> </div>
@@ -70,15 +70,15 @@ const MenuItemContainer = memo<MenuItemProps>(
"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)]", "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> = const inverseModeStyles: Record<"default" | "hover" | "selected", string> =
{ {
default: 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: 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: 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 // Get state styles based on mode
@@ -20,37 +20,31 @@ const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
src: getAssetPath(partnerLogoPath("food-not-bombs")), src: getAssetPath(partnerLogoPath("food-not-bombs")),
alt: t("partners.foodNotBombs"), alt: t("partners.foodNotBombs"),
size: "h-11 lg:h-14 xl:h-[70px]", size: "h-11 lg:h-14 xl:h-[70px]",
order: "order-1 sm:order-4",
}, },
{ {
src: getAssetPath(partnerLogoPath("start-coop")), src: getAssetPath(partnerLogoPath("start-coop")),
alt: t("partners.startCoop"), alt: t("partners.startCoop"),
size: "h-[42px] lg:h-[53px] xl:h-[66px]", size: "h-[42px] lg:h-[53px] xl:h-[66px]",
order: "order-2 sm:order-2",
}, },
{ {
src: getAssetPath(partnerLogoPath("metagov")), src: getAssetPath(partnerLogoPath("metagov")),
alt: t("partners.metagov"), alt: t("partners.metagov"),
size: "h-6 lg:h-8 xl:h-[41px]", size: "h-6 lg:h-8 xl:h-[41px]",
order: "order-3 sm:order-1",
}, },
{ {
src: getAssetPath(partnerLogoPath("open-civics")), src: getAssetPath(partnerLogoPath("open-civics")),
alt: t("partners.openCivics"), alt: t("partners.openCivics"),
size: "h-8 lg:h-10 xl:h-[50px]", size: "h-8 lg:h-10 xl:h-[50px]",
order: "order-4 sm:order-5 md:order-6",
}, },
{ {
src: getAssetPath(partnerLogoPath("mutual-aid-co")), src: getAssetPath(partnerLogoPath("mutual-aid-co")),
alt: t("partners.mutualAidCo"), alt: t("partners.mutualAidCo"),
size: "h-11 lg:h-14 xl:h-[70px]", size: "h-11 lg:h-14 xl:h-[70px]",
order: "order-5 sm:order-6 md:order-5",
}, },
{ {
src: getAssetPath(partnerLogoPath("cu-boulder")), src: getAssetPath(partnerLogoPath("cu-boulder")),
alt: t("partners.cuBoulder"), alt: t("partners.cuBoulder"),
size: "h-10 lg:h-12 xl:h-[60px]", size: "h-10 lg:h-12 xl:h-[60px]",
order: "order-6 sm:order-3",
}, },
], ],
[t], [t],
@@ -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}`} 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}`}
> >
<div className="flex flex-col gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-024)] xl:gap-[var(--spacing-scale-032)]"> <div className="flex flex-col gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-024)] xl:gap-[var(--spacing-scale-032)]">
{/* Label */}
<p className="font-inter font-medium text-[10px] leading-[12px] xl:text-[14px] xl:leading-[12px] uppercase text-[var(--color-content-default-secondary)] text-center">
Trusted by leading cooperators
</p>
{/* Logo Grid Container */}
<div <div
className={`transition-opacity duration-500 ${ className={`transition-opacity duration-500 ${
isVisible ? "opacity-60" : "opacity-0" isVisible ? "opacity-60" : "opacity-0"
}`} }`}
> >
<div className="grid grid-cols-2 grid-rows-3 sm:grid-cols-3 sm:grid-rows-2 md:flex md:justify-between md:items-center gap-x-[var(--spacing-scale-032)] gap-y-[var(--spacing-scale-032)] sm:gap-y-[var(--spacing-scale-048)]"> <div className="grid grid-cols-2 grid-rows-3 sm:grid-cols-3 sm:grid-rows-2 md:flex md:flex-wrap md:justify-center md:items-center gap-x-[var(--spacing-scale-032)] gap-y-[var(--spacing-scale-032)] sm:gap-y-[var(--spacing-scale-048)]">
{displayLogos.map((logo, index) => ( {displayLogos.map((logo, index) => (
<div <div
key={index} key={index}
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
title: "Your Article Title Here" title: "Your Article Title Here"
description: "A brief, compelling description of what this article covers" description: "A brief, compelling description of what this article covers"
author: "Author Name" author: "CommunityRule"
date: "2025-01-15" date: "2025-01-15"
related: ["slug-of-related-article-1", "slug-of-related-article-2"] related: ["slug-of-related-article-1", "slug-of-related-article-2"]
--- ---
@@ -1,7 +1,7 @@
--- ---
title: "Avoiding Burnout: Sustainability in the Ruins" title: "Avoiding Burnout: Sustainability in the Ruins"
description: "Building a practice of resistance that doesn't consume you" description: "Building a practice of resistance that doesn't consume you"
author: "Author name" author: "CommunityRule"
date: "2025-08-12" date: "2025-08-12"
related: related:
- "resolving-active-conflicts" - "resolving-active-conflicts"
@@ -1,7 +1,7 @@
--- ---
title: "Digital Mediation and the Death of Nuance" title: "Digital Mediation and the Death of Nuance"
description: "How corporate platforms undermine solidarity and what to build instead" description: "How corporate platforms undermine solidarity and what to build instead"
author: "Author name" author: "CommunityRule"
date: "2025-08-18" date: "2025-08-18"
related: related:
- "operational-security-mutual-aid" - "operational-security-mutual-aid"
@@ -1,7 +1,7 @@
--- ---
title: "How Chaos Concentrates Control" title: "How Chaos Concentrates Control"
description: "How to limit informal hierarchies inevitably emerging in horizontal groups" description: "How to limit informal hierarchies inevitably emerging in horizontal groups"
author: "Author name" author: "CommunityRule"
date: "2025-08-15" date: "2025-08-15"
related: related:
- "making-decisions-without-hierarchy" - "making-decisions-without-hierarchy"
@@ -1,7 +1,7 @@
--- ---
title: "Integrating New Members Without Dilution" title: "Integrating New Members Without Dilution"
description: "How to Bring New People In Without Everything Falling Apart" description: "How to Bring New People In Without Everything Falling Apart"
author: "Author name" author: "CommunityRule"
date: "2025-08-05" date: "2025-08-05"
related: related:
- "making-decisions-without-hierarchy" - "making-decisions-without-hierarchy"
@@ -1,7 +1,7 @@
--- ---
title: "Knowledge Management and Institutional Amnesia" title: "Knowledge Management and Institutional Amnesia"
description: "Preserving what we learn without surveillance infrastructure" description: "Preserving what we learn without surveillance infrastructure"
author: "Author name" author: "CommunityRule"
date: "2025-08-20" date: "2025-08-20"
related: related:
- "integrating-new-members-without-dilution" - "integrating-new-members-without-dilution"
@@ -1,7 +1,7 @@
--- ---
title: "Making decisions without hierarchy" title: "Making decisions without hierarchy"
description: "A brief guide to collaborative nonhierarchical decision making" description: "A brief guide to collaborative nonhierarchical decision making"
author: "Author name" author: "CommunityRule"
date: "2025-08-01" date: "2025-08-01"
related: related:
- "resolving-active-conflicts" - "resolving-active-conflicts"
@@ -1,7 +1,7 @@
--- ---
title: "Operational Security for Mutual Aid" title: "Operational Security for Mutual Aid"
description: "Why protecting information isn't paranoia: it's care work in a hostile world" description: "Why protecting information isn't paranoia: it's care work in a hostile world"
author: "Author name" author: "CommunityRule"
date: "2025-08-10" date: "2025-08-10"
related: related:
- "resolving-active-conflicts" - "resolving-active-conflicts"
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
title: "Resolving Active Conflicts" title: "Resolving Active Conflicts"
description: "Practical steps for resolving conflicts while maintaining trust, cooperation, and shared goals" description: "Practical steps for resolving conflicts while maintaining trust, cooperation, and shared goals"
author: "Author name" author: "CommunityRule"
date: "2025-04-15" date: "2025-04-15"
related: related:
- "operational-security-mutual-aid" - "operational-security-mutual-aid"
+2 -1
View File
@@ -209,7 +209,8 @@ export const ASSETS = {
// Social media // Social media
BLUESKY_LOGO: "assets/logos/bluesky.svg", BLUESKY_LOGO: "assets/logos/bluesky.svg",
GITLAB_ICON: "assets/logos/gitlab.svg", GITEA_ICON: "assets/logos/gitea.svg",
MASTODON_LOGO: "assets/logos/mastodon.svg",
// Content page decorative shapes // Content page decorative shapes
CONTENT_SHAPE_1: "assets/shapes/content-shape-1.svg", CONTENT_SHAPE_1: "assets/shapes/content-shape-1.svg",
+2 -1
View File
@@ -11,7 +11,8 @@
}, },
"ariaLabels": { "ariaLabels": {
"followBluesky": "Follow us on Bluesky", "followBluesky": "Follow us on Bluesky",
"followGitlab": "Follow us on GitLab", "viewSourceGitea": "View source on Gitea",
"followMastodon": "Follow us on Mastodon",
"featureToolsAndServices": "Feature tools and services", "featureToolsAndServices": "Feature tools and services",
"askOrganizerContact": "Ask an organizer - Contact an organizer for help" "askOrganizerContact": "Ask an organizer - Contact an organizer for help"
} }
+11 -6
View File
@@ -7,14 +7,19 @@
}, },
"social": { "social": {
"bluesky": { "bluesky": {
"handle": "medlabboulder", "label": "Bluesky",
"ariaLabel": "Follow us on Bluesky", "ariaLabel": "Follow us on Bluesky",
"url": "https://bsky.app/profile/medlabboulder" "url": "https://bsky.app/profile/medlabboulder.bsky.social"
}, },
"gitlab": { "gitea": {
"handle": "medlabboulder", "label": "Gitea",
"ariaLabel": "Follow us on GitLab", "ariaLabel": "View source on Gitea",
"url": "https://gitlab.com/medlabboulder" "url": "https://git.medlab.host/CommunityRule/community-rule"
},
"mastodon": {
"label": "Mastodon",
"ariaLabel": "Follow us on Mastodon",
"url": "https://social.medlab.host/@medlab"
} }
}, },
"navigation": { "navigation": {
@@ -3,7 +3,7 @@
"description": "We need your email to save your CommunityRule progress\nand make it accessible to you later.", "description": "We need your email to save your CommunityRule progress\nand make it accessible to you later.",
"placeholder": "email@domain.com", "placeholder": "email@domain.com",
"characterCountTemplate": "{current}/{max}", "characterCountTemplate": "{current}/{max}",
"magicLinkSuccessTitle": "Check your email to log in!", "magicLinkSuccessTitle": "Check your email to log in",
"magicLinkSuccessDescription": "Your account is created, now just check your email for a magic link", "magicLinkSuccessDescription": "Your account has been created. A login link has been emailed to you.",
"magicLinkErrorTitle": "Could not send link" "magicLinkErrorTitle": "Could not send link"
} }
@@ -16,7 +16,7 @@
"addButtonText": "Add maturity" "addButtonText": "Add maturity"
}, },
"organizationTypes": [ "organizationTypes": [
{ "label": "Workers coop" }, { "label": "Worker cooperative" },
{ "label": "Mutual aid" }, { "label": "Mutual aid" },
{ "label": "Open source project" }, { "label": "Open source project" },
{ "label": "Nonprofit" }, { "label": "Nonprofit" },
+1 -1
View File
@@ -29,7 +29,7 @@
"title": "Who is this for?", "title": "Who is this for?",
"items": [ "items": [
{ {
"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." "description": "Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations."
}, },
{ {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<style>
.st1{fill:#fff}
</style>
<g id="Icon">
<circle cx="512" cy="512" r="512" style="fill:#609926"/>
<path class="st1" d="M762.2 350.3c-100.9 5.3-160.7 8-212 8.5v114.1l-16-7.9-.1-106.1c-58.9 0-110.7-3.1-209.1-8.6-12.3-.1-29.5-2.4-47.9-2.5-47.1-.1-110.2 33.5-106.7 118C175.8 597.6 296 609.9 344 610.9c5.3 24.7 61.8 110.1 103.6 114.6H631c109.9-8.2 192.3-373.8 131.2-375.2zm-546 117.3c-4.7-36.6 11.8-74.8 73.2-73.2C296.1 462 307 501.5 329 561.9c-56.2-7.4-104-25.7-112.8-94.3zm415.6 83.5-51.3 105.6c-6.5 13.4-22.7 19-36.2 12.5l-105.6-51.3c-13.4-6.5-19-22.7-12.5-36.2l51.3-105.6c6.5-13.4 22.7-19 36.2-12.5l105.6 51.3c13.4 6.6 19 22.8 12.5 36.2z"/>
<path class="st1" d="M555 609.9c.1-.2.2-.3.2-.5 17.2-35.2 24.3-49.8 19.8-62.4-3.9-11.1-15.5-16.6-36.7-26.6-.8-.4-1.7-.8-2.5-1.2.2-2.3-.1-4.7-1-7-.8-2.3-2.1-4.3-3.7-6l13.6-27.8-11.9-5.8-13.7 28.4c-2 0-4.1.3-6.2 1-8.9 3.2-13.5 13-10.3 21.9.7 1.9 1.7 3.5 2.8 5l-23.6 48.4c-1.9 0-3.8.3-5.7 1-8.9 3.2-13.5 13-10.3 21.9 3.2 8.9 13 13.5 21.9 10.3 8.9-3.2 13.5-13 10.3-21.9-.9-2.5-2.3-4.6-4-6.3l23-47.2c2.5.2 5 0 7.5-.9 2.1-.8 3.9-1.9 5.5-3.3.9.4 1.9.9 2.7 1.3 17.4 8.2 27.9 13.2 30 19.1 2.6 7.5-5.1 23.4-19.3 52.3-.1.2-.2.5-.4.7-2.2-.1-4.4.2-6.5 1-8.9 3.2-13.5 13-10.3 21.9 3.2 8.9 13 13.5 21.9 10.3 8.9-3.2 13.5-13 10.3-21.9-.6-2-1.9-4-3.4-5.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="22" height="22" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill="#949494"
d="M44.9955 16.2709c0 -9.76202 -6.3993 -12.62323 -6.3993 -12.62323 -3.2287 -1.4803 -8.7692 -2.10373 -14.5254 -2.1506h-0.1415c-5.7562 0.04687 -11.293 0.6703 -14.51985 2.1506 0 0 -6.40027 2.86121 -6.40027 12.62323 0 2.2359 -0.04312 4.9078 0.02719 7.7427 0.2325 9.5455 1.75123 18.9551 10.58333 21.2913 4.0715 1.0772 7.5683 1.3022 10.3836 1.1475 5.1065 -0.2812 7.9687 -1.8206 7.9687 -1.8206l-0.1679 -3.7031s-3.6496 1.1503 -7.7474 1.0097c-4.0602 -0.1387 -8.3436 -0.4378 -8.9998 -5.4196 -0.0634 -0.4629 -0.0947 -0.9296 -0.0938 -1.3969 2.9714 0.6634 5.995 1.0664 9.0365 1.2047 3.089 0.1416 5.9849 -0.1809 8.9267 -0.5316 5.6418 -0.6731 10.5543 -4.1474 11.1711 -7.3208 0.9769 -5.0005 0.8981 -12.2033 0.8981 -12.2033ZM37.445 28.8483h-4.6874V17.3762c0 -2.4187 -1.0181 -3.6459 -3.0544 -3.6459 -2.2499 0 -3.3805 1.456 -3.3805 4.335v6.2812h-4.6556v-6.2812c0 -2.879 -1.125 -4.335 -3.3806 -4.335 -2.0362 0 -3.0543 1.2272 -3.0543 3.6459v11.4721h-4.6875V17.0284c0 -2.4156 0.6172 -4.334 1.8516 -5.7552 1.275 -1.4203 2.9437 -2.14873 5.0165 -2.14873 2.3981 0 4.214 0.92063 5.414 2.76373l1.169 1.9546 1.1672 -1.9546c1.2009 -1.8431 3.0159 -2.76373 5.414 -2.76373 2.0728 0 3.7415 0.72843 5.0165 2.14873 1.2368 1.42 1.8539 3.3384 1.8515 5.7552v11.8199Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

+9 -25
View File
@@ -3,6 +3,8 @@
* Regenerate root favicon binaries from `public/assets/logos/community-rule.svg`. * 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. * 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 * Run: npm run generate:favicons
*/ */
import fs from "node:fs/promises"; 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 ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const PUBLIC = path.join(ROOT, "public"); const PUBLIC = path.join(ROOT, "public");
const SVG_PATH = path.join(PUBLIC, "assets/logos/community-rule.svg"); const SVG_PATH = path.join(PUBLIC, "assets/logos/community-rule.svg");
const LOGO_FILL = "#FFFDD2";
const MARK_ON_LIGHT = "#000000";
async function readLogoSvg() { async function readLogoSvg() {
return fs.readFile(SVG_PATH, "utf8"); return fs.readFile(SVG_PATH, "utf8");
} }
async function markPng(svg, size, fill) { /** Resize the logo SVG to a PNG with alpha (transparent background). */
const tinted = svg.replaceAll(LOGO_FILL, fill); async function creamMarkTransparent(svg, size) {
return sharp(Buffer.from(tinted)) return sharp(Buffer.from(svg))
.resize(size, size, { fit: "contain" }) .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
.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" }])
.png() .png()
.toBuffer(); .toBuffer();
} }
async function main() { async function main() {
const svg = await readLogoSvg(); const svg = await readLogoSvg();
const png16 = await markPng(svg, 16, MARK_ON_LIGHT); const png16 = await creamMarkTransparent(svg, 16);
const png32 = await markPng(svg, 32, MARK_ON_LIGHT); const png32 = await creamMarkTransparent(svg, 32);
const appleTouch = await creamMarkOnBlack(svg, 180); const appleTouch = await creamMarkTransparent(svg, 180);
const faviconIco = await pngToIco([png16, png32]); const faviconIco = await pngToIco([png16, png32]);
await Promise.all([ await Promise.all([
+2 -2
View File
@@ -46,7 +46,7 @@ const WorkerCoopIcon = () => (
export const Default = { export const Default = {
args: { args: {
icon: <WorkerCoopIcon />, icon: <WorkerCoopIcon />,
title: "Worker's cooperatives", title: "Worker cooperatives",
description: description:
"Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations.", "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 = { export const WithShortDescription = {
args: { args: {
icon: <WorkerCoopIcon />, icon: <WorkerCoopIcon />,
title: "Worker's cooperatives", title: "Worker cooperatives",
description: "Short description", description: "Short description",
}, },
}; };
@@ -222,10 +222,15 @@ export const Interactive = {
}); });
await userEvent.click(blueskyLink); await userEvent.click(blueskyLink);
const gitlabLink = canvas.getByRole("link", { const giteaLink = canvas.getByRole("link", {
name: /follow us on gitlab/i, 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 userEvent.hover(blueskyLink);
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
const gitlabLink = canvas.getByRole("link", { const giteaLink = canvas.getByRole("link", {
name: /follow us on gitlab/i, 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)); await new Promise((resolve) => setTimeout(resolve, 100));
}); });
}, },
+1 -1
View File
@@ -20,7 +20,7 @@ const sampleItems = [
width={36} width={36}
/> />
), ),
title: "Worker's cooperatives", title: "Worker cooperatives",
description: description:
"Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations.", "Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations.",
}, },
+4 -6
View File
@@ -13,9 +13,9 @@ export default {
- **Mobile**: 3 rows × 2 columns grid with 32px gaps - **Mobile**: 3 rows × 2 columns grid with 32px gaps
- **SM**: 2 rows × 3 columns grid with 48px row gap and 32px column gap - **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 - **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 ## Animations & Transitions
@@ -28,7 +28,7 @@ export default {
## Props ## 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 ## Usage Examples
@@ -40,13 +40,11 @@ export default {
src: "assets/logos/partners/cu-boulder.svg", src: "assets/logos/partners/cu-boulder.svg",
alt: "CU Boulder", alt: "CU Boulder",
size: "h-10 lg:h-12 xl:h-[60px]", size: "h-10 lg:h-12 xl:h-[60px]",
order: "order-1 sm:order-2"
}, },
{ {
src: "assets/logos/partners/food-not-bombs.svg", src: "assets/logos/partners/food-not-bombs.svg",
alt: "Food Not Bombs", alt: "Food Not Bombs",
size: "h-11 lg:h-14 xl:h-[70px]", 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: { logos: {
control: "object", control: "object",
description: 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.",
}, },
}, },
}; };
+5 -2
View File
@@ -54,7 +54,10 @@ describe("Footer (behavioral tests)", () => {
screen.getAllByRole("link", { name: "Follow us on Bluesky" }).length, screen.getAllByRole("link", { name: "Follow us on Bluesky" }).length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
expect( 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); ).toBeGreaterThan(0);
}); });
@@ -74,7 +77,7 @@ describe("Footer (behavioral tests)", () => {
it("renders navigation links with baseline width-fit focus targets", () => { it("renders navigation links with baseline width-fit focus targets", () => {
render(<Footer />); render(<Footer />);
const useCases = screen.getAllByRole("link", { name: "Use cases" })[0]; 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"); expect(useCases).not.toHaveClass("w-full");
}); });
+3 -3
View File
@@ -12,7 +12,7 @@ type IconProps = React.ComponentProps<typeof Icon>;
const baseProps: IconProps = { const baseProps: IconProps = {
icon: <div data-testid="test-icon">Icon</div>, icon: <div data-testid="test-icon">Icon</div>,
title: "Worker's cooperatives", title: "Worker cooperatives",
description: description:
"Employee-owned businesses often need to clarify how power is shared", "Employee-owned businesses often need to clarify how power is shared",
}; };
@@ -89,12 +89,12 @@ describe("Icon (behavioral tests)", () => {
render( render(
<Icon <Icon
icon={<div data-testid="icon">Icon</div>} icon={<div data-testid="icon">Icon</div>}
title="Worker's cooperatives" title="Worker cooperatives"
description="Employee-owned businesses" description="Employee-owned businesses"
/>, />,
); );
expect(screen.getByTestId("icon")).toBeInTheDocument(); 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(); expect(screen.getByText("Employee-owned businesses")).toBeInTheDocument();
}); });
+24
View File
@@ -166,6 +166,30 @@ describe("MultiSelect behaviour specifics", () => {
expect(handleConfirm).toHaveBeenCalledWith("custom-1", "NewOption"); 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(
<MultiSelect
options={customOptions}
onCustomChipConfirm={handleConfirm}
/>,
);
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 () => { it("handles custom chip close", async () => {
const handleClose = vi.fn(); const handleClose = vi.fn();
const customOptions = [ const customOptions = [
+1 -3
View File
@@ -101,9 +101,7 @@ test.describe("Critical User Journeys", () => {
// Check key components are rendered // Check key components are rendered
await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible(); await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible();
await expect( await expect(page.locator('img[alt="Food Not Bombs"]')).toBeVisible();
page.locator("text=Trusted by leading cooperators"),
).toBeVisible();
await expect(page.locator("text=Jo Freeman")).toBeVisible(); await expect(page.locator("text=Jo Freeman")).toBeVisible();
}); });
+2 -2
View File
@@ -31,7 +31,7 @@ const mockPosts = [
frontmatter: { frontmatter: {
title: "Resolving Active Conflicts", title: "Resolving Active Conflicts",
description: "Practical steps for resolving conflicts", description: "Practical steps for resolving conflicts",
author: "Author name", author: "CommunityRule",
date: "2025-04-15", date: "2025-04-15",
thumbnail: { thumbnail: {
vertical: "resolving-active-conflicts-vertical.svg", vertical: "resolving-active-conflicts-vertical.svg",
@@ -48,7 +48,7 @@ const mockPosts = [
frontmatter: { frontmatter: {
title: "Operational Security for Mutual Aid", title: "Operational Security for Mutual Aid",
description: "Tactics to protect members", description: "Tactics to protect members",
author: "Author name", author: "CommunityRule",
date: "2025-04-10", date: "2025-04-10",
thumbnail: { thumbnail: {
vertical: "operational-security-mutual-aid-vertical.svg", vertical: "operational-security-mutual-aid-vertical.svg",
+11 -5
View File
@@ -134,12 +134,16 @@ describe("User Journey Integration", () => {
const blueskyLink = screen.getByRole("link", { const blueskyLink = screen.getByRole("link", {
name: "Follow us on Bluesky", name: "Follow us on Bluesky",
}); });
const gitlabLink = screen.getByRole("link", { const giteaLink = screen.getByRole("link", {
name: "Follow us on GitLab", name: "View source on Gitea",
});
const mastodonLink = screen.getByRole("link", {
name: "Follow us on Mastodon",
}); });
expect(blueskyLink).toBeInTheDocument(); expect(blueskyLink).toBeInTheDocument();
expect(gitlabLink).toBeInTheDocument(); expect(giteaLink).toBeInTheDocument();
expect(mastodonLink).toBeInTheDocument();
}); });
test("user explores features and benefits", async () => { test("user explores features and benefits", async () => {
@@ -179,9 +183,11 @@ describe("User Journey Integration", () => {
}); });
const blueskyLink = screen.getByRole("link", { name: /Bluesky/i }); 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(blueskyLink).toBeInTheDocument();
expect(gitlabLink).toBeInTheDocument(); expect(giteaLink).toBeInTheDocument();
expect(mastodonLink).toBeInTheDocument();
}); });
test("user completes the full journey from discovery to action", async () => { test("user completes the full journey from discovery to action", async () => {
+7 -21
View File
@@ -42,14 +42,6 @@ describe("LogoWall Component", () => {
expect(screen.queryByAltText("Food Not Bombs")).not.toBeInTheDocument(); expect(screen.queryByAltText("Food Not Bombs")).not.toBeInTheDocument();
}); });
test("renders section label", () => {
render(<LogoWall />);
expect(
screen.getByText("Trusted by leading cooperators"),
).toBeInTheDocument();
});
test("applies correct CSS classes", () => { test("applies correct CSS classes", () => {
render(<LogoWall />); render(<LogoWall />);
@@ -74,7 +66,12 @@ describe("LogoWall Component", () => {
'[class*="grid grid-cols-2 grid-rows-3"]', '[class*="grid grid-cols-2 grid-rows-3"]',
); );
expect(grid).toBeInTheDocument(); 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", () => { test("renders logos with correct attributes", () => {
@@ -88,15 +85,6 @@ describe("LogoWall Component", () => {
expect(foodNotBombsLogo).toHaveClass("h-11", "lg:h-14", "xl:h-[70px]"); expect(foodNotBombsLogo).toHaveClass("h-11", "lg:h-14", "xl:h-[70px]");
}); });
test("applies order classes for responsive layout", () => {
render(<LogoWall />);
const foodNotBombsContainer = screen
.getByAltText("Food Not Bombs")
.closest("div");
expect(foodNotBombsContainer).toHaveClass("order-1", "sm:order-4");
});
test("handles empty logos array", () => { test("handles empty logos array", () => {
render(<LogoWall logos={[]} />); render(<LogoWall logos={[]} />);
@@ -119,9 +107,7 @@ describe("LogoWall Component", () => {
const section = document.querySelector("section"); const section = document.querySelector("section");
expect(section).toBeInTheDocument(); expect(section).toBeInTheDocument();
// Check for the label expect(screen.queryByText("Trusted by leading cooperators")).not.toBeInTheDocument();
const label = screen.getByText("Trusted by leading cooperators");
expect(label).toBeInTheDocument();
}); });
test("applies transition effects", () => { test("applies transition effects", () => {