Backend / staging cleanup, performance substrate, and create-flow polish #60
@@ -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>);
|
||||
|
||||
@@ -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,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,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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Worker’s coop" },
|
||||
{ "label": "Worker cooperative" },
|
||||
{ "label": "Mutual aid" },
|
||||
{ "label": "Open source project" },
|
||||
{ "label": "Nonprofit" },
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 239 B |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 375 B |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -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([
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||