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,
description: post.frontmatter.description,
author: {
"@type": "Person",
"@type": "Organization",
name: post.frontmatter.author,
},
publisher: {
@@ -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<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`. */
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 */}
<img
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
alt="Bluesky"
alt=""
width={24}
height={22}
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
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`}
aria-label={t("social.gitlab.ariaLabel")}
aria-label={t("social.gitea.ariaLabel")}
>
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
<img
src={getAssetPath(ASSETS.GITLAB_ICON)}
alt="GitLab"
src={getAssetPath(ASSETS.GITEA_ICON)}
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.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>
</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)]",
};
// 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
@@ -20,37 +20,31 @@ const LogoWallContainer = memo<LogoWallProps>(({ 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],
@@ -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}`}
>
<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
className={`transition-opacity duration-500 ${
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) => (
<div
key={index}
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: "Your Article Title Here"
description: "A brief, compelling description of what this article covers"
author: "Author Name"
author: "CommunityRule"
date: "2025-01-15"
related: ["slug-of-related-article-1", "slug-of-related-article-2"]
---
@@ -1,7 +1,7 @@
---
title: "Avoiding Burnout: Sustainability in the Ruins"
description: "Building a practice of resistance that doesn't consume you"
author: "Author name"
author: "CommunityRule"
date: "2025-08-12"
related:
- "resolving-active-conflicts"
@@ -1,7 +1,7 @@
---
title: "Digital Mediation and the Death of Nuance"
description: "How corporate platforms undermine solidarity and what to build instead"
author: "Author name"
author: "CommunityRule"
date: "2025-08-18"
related:
- "operational-security-mutual-aid"
@@ -1,7 +1,7 @@
---
title: "How Chaos Concentrates Control"
description: "How to limit informal hierarchies inevitably emerging in horizontal groups"
author: "Author name"
author: "CommunityRule"
date: "2025-08-15"
related:
- "making-decisions-without-hierarchy"
@@ -1,7 +1,7 @@
---
title: "Integrating New Members Without Dilution"
description: "How to Bring New People In Without Everything Falling Apart"
author: "Author name"
author: "CommunityRule"
date: "2025-08-05"
related:
- "making-decisions-without-hierarchy"
@@ -1,7 +1,7 @@
---
title: "Knowledge Management and Institutional Amnesia"
description: "Preserving what we learn without surveillance infrastructure"
author: "Author name"
author: "CommunityRule"
date: "2025-08-20"
related:
- "integrating-new-members-without-dilution"
@@ -1,7 +1,7 @@
---
title: "Making decisions without hierarchy"
description: "A brief guide to collaborative nonhierarchical decision making"
author: "Author name"
author: "CommunityRule"
date: "2025-08-01"
related:
- "resolving-active-conflicts"
@@ -1,7 +1,7 @@
---
title: "Operational Security for Mutual Aid"
description: "Why protecting information isn't paranoia: it's care work in a hostile world"
author: "Author name"
author: "CommunityRule"
date: "2025-08-10"
related:
- "resolving-active-conflicts"
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: "Resolving Active Conflicts"
description: "Practical steps for resolving conflicts while maintaining trust, cooperation, and shared goals"
author: "Author name"
author: "CommunityRule"
date: "2025-04-15"
related:
- "operational-security-mutual-aid"
+2 -1
View File
@@ -209,7 +209,8 @@ export const ASSETS = {
// Social media
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_SHAPE_1: "assets/shapes/content-shape-1.svg",
+2 -1
View File
@@ -11,7 +11,8 @@
},
"ariaLabels": {
"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",
"askOrganizerContact": "Ask an organizer - Contact an organizer for help"
}
+11 -6
View File
@@ -7,14 +7,19 @@
},
"social": {
"bluesky": {
"handle": "medlabboulder",
"label": "Bluesky",
"ariaLabel": "Follow us on Bluesky",
"url": "https://bsky.app/profile/medlabboulder"
"url": "https://bsky.app/profile/medlabboulder.bsky.social"
},
"gitlab": {
"handle": "medlabboulder",
"ariaLabel": "Follow us on GitLab",
"url": "https://gitlab.com/medlabboulder"
"gitea": {
"label": "Gitea",
"ariaLabel": "View source on Gitea",
"url": "https://git.medlab.host/CommunityRule/community-rule"
},
"mastodon": {
"label": "Mastodon",
"ariaLabel": "Follow us on Mastodon",
"url": "https://social.medlab.host/@medlab"
}
},
"navigation": {
@@ -3,7 +3,7 @@
"description": "We need your email to save your CommunityRule progress\nand make it accessible to you later.",
"placeholder": "email@domain.com",
"characterCountTemplate": "{current}/{max}",
"magicLinkSuccessTitle": "Check your email to log in!",
"magicLinkSuccessDescription": "Your account is created, now just check your email for a magic link",
"magicLinkSuccessTitle": "Check your email to log in",
"magicLinkSuccessDescription": "Your account has been created. A login link has been emailed to you.",
"magicLinkErrorTitle": "Could not send link"
}
@@ -16,7 +16,7 @@
"addButtonText": "Add maturity"
},
"organizationTypes": [
{ "label": "Workers coop" },
{ "label": "Worker cooperative" },
{ "label": "Mutual aid" },
{ "label": "Open source project" },
{ "label": "Nonprofit" },
+1 -1
View File
@@ -29,7 +29,7 @@
"title": "Who is this for?",
"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."
},
{
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`.
* 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([
+2 -2
View File
@@ -46,7 +46,7 @@ const WorkerCoopIcon = () => (
export const Default = {
args: {
icon: <WorkerCoopIcon />,
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: <WorkerCoopIcon />,
title: "Worker's cooperatives",
title: "Worker cooperatives",
description: "Short description",
},
};
@@ -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));
});
},
+1 -1
View File
@@ -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.",
},
+4 -6
View File
@@ -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.",
},
},
};
+5 -2
View File
@@ -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(<Footer />);
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");
});
+3 -3
View File
@@ -12,7 +12,7 @@ type IconProps = React.ComponentProps<typeof Icon>;
const baseProps: IconProps = {
icon: <div data-testid="test-icon">Icon</div>,
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
icon={<div data-testid="icon">Icon</div>}
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();
});
+24
View File
@@ -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(
<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 () => {
const handleClose = vi.fn();
const customOptions = [
+1 -3
View File
@@ -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();
});
+2 -2
View File
@@ -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",
+11 -5
View File
@@ -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 () => {
+7 -21
View File
@@ -42,14 +42,6 @@ describe("LogoWall Component", () => {
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", () => {
render(<LogoWall />);
@@ -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(<LogoWall />);
const foodNotBombsContainer = screen
.getByAltText("Food Not Bombs")
.closest("div");
expect(foodNotBombsContainer).toHaveClass("order-1", "sm:order-4");
});
test("handles empty logos array", () => {
render(<LogoWall logos={[]} />);
@@ -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", () => {