Refine use cases rule examples

This commit is contained in:
adilallo committed 2026-05-19 22:16:08 -06:00
1 parent 7c46cbd87b
commit 2f2b5d0dc2
65 files changed
+3129 -252

No files matched your search

+1
View File
@@ -51,6 +51,7 @@ npm-cache/
/build
# misc
/tmp/
*.pem
# IDE and editor files
@@ -0,0 +1,109 @@
"use client";
/**
* Edit published rule: community name with the same 48-char limit as
* {@link CreateFlowTextFieldScreen} `community-name` step.
*/
import { useEffect, useMemo, useRef, useState } from "react";
import Create from "../../../components/modals/Create";
import TextInput from "../../../components/controls/TextInput";
import ContentLockup from "../../../components/type/ContentLockup";
import { useTranslation } from "../../../contexts/MessagesContext";
/** Matches `community-name` step (`CreateFlowTextFieldScreen` `maxLength={48}`). */
export const COMMUNITY_TITLE_FIELD_MAX_LENGTH = 48;
export interface FinalReviewTitleEditModalProps {
isOpen: boolean;
onClose: () => void;
initialValue: string;
onSave: (_value: string) => void;
}
export function FinalReviewTitleEditModal({
isOpen,
onClose,
initialValue,
onSave,
}: FinalReviewTitleEditModalProps) {
const tModal = useTranslation(
"create.reviewAndComplete.finalReview.titleEditModal",
);
const tField = useTranslation("create.community.communityName");
const tSave = useTranslation(
"create.reviewAndComplete.finalReview.chipEditModal",
);
const [draft, setDraft] = useState("");
const initialRef = useRef("");
const seededOpenRef = useRef(false);
useEffect(() => {
if (!isOpen) {
seededOpenRef.current = false;
return;
}
if (seededOpenRef.current) return;
seededOpenRef.current = true;
const seed = initialValue;
setDraft(seed);
initialRef.current = seed;
}, [isOpen, initialValue]);
const isDirty = useMemo(() => draft !== initialRef.current, [draft]);
const trimmedDraft = draft.trim();
const canSave = isDirty && trimmedDraft.length > 0;
const characterHint = tField("characterCountTemplate")
.replace("{current}", String(draft.length))
.replace("{max}", String(COMMUNITY_TITLE_FIELD_MAX_LENGTH));
const handleSave = () => {
if (!canSave) return;
const capped = trimmedDraft.slice(0, COMMUNITY_TITLE_FIELD_MAX_LENGTH);
onSave(capped);
onClose();
};
return (
<Create
isOpen={isOpen}
onClose={onClose}
backdropVariant="blurredYellow"
headerContent={
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={tModal("title")}
description={tModal("description")}
variant="modal"
alignment="left"
/>
</div>
}
showBackButton={false}
showNextButton
nextButtonText={tSave("saveButton")}
nextButtonDisabled={!canSave}
onNext={handleSave}
ariaLabel={tModal("title")}
>
<div className="pb-2">
<TextInput
className="!transition-none"
type="text"
placeholder={tField("placeholder")}
value={draft}
onChange={(e) => {
setDraft(e.target.value);
}}
inputSize="medium"
formHeader={false}
textHint={characterHint}
maxLength={COMMUNITY_TITLE_FIELD_MAX_LENGTH}
/>
</div>
</Create>
);
}
@@ -0,0 +1,20 @@
"use client";
import { useEffect, useState } from "react";
import { useMediaQuery } from "../../../hooks/useMediaQuery";
/** `--breakpoint-sm2` (440px); pairs with Tailwind `sm2:` on create-flow chrome. */
const CREATE_FLOW_MIN_WIDTH_SM2 = "(min-width: 440px)";
/** True at viewport ≥440px. */
export function useCreateFlowSm2Up(): boolean {
const [isMounted, setIsMounted] = useState(false);
const isSm2OrLarger = useMediaQuery(CREATE_FLOW_MIN_WIDTH_SM2);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- defer until mount for SSR/first-paint alignment
setIsMounted(true);
}, []);
return !isMounted || isSm2OrLarger;
}
@@ -163,10 +163,10 @@ export function CompletedScreen() {
<>
<div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] md:h-full">
<div
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:overflow-y-auto max-md:overscroll-y-contain max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:grid-rows-1 md:items-stretch md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
>
<div
className={`flex flex-col justify-start overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
className={`flex flex-col justify-start max-md:min-h-min max-md:overflow-visible min-h-0 overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<CreateFlowHeaderLockup
title={headerTitle}
@@ -177,7 +177,7 @@ export function CompletedScreen() {
/>
</div>
<div
className={`scrollbar-hide relative flex min-h-0 flex-col overflow-x-hidden md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
className={`scrollbar-hide relative flex min-h-0 flex-col self-stretch overflow-x-hidden md:max-h-full md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<div
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent md:block"
@@ -25,6 +25,7 @@ import {
type FinalReviewChipEditTarget,
} from "../../components/FinalReviewChipEditModal";
import { FinalReviewCommunityContextEditModal } from "../../components/FinalReviewCommunityContextEditModal";
import { FinalReviewTitleEditModal } from "../../components/FinalReviewTitleEditModal";
import { useCreateFlowNavigation } from "../../hooks/useCreateFlowNavigation";
import { createFlowStepForFacetGroup } from "../../utils/facetGroupToCreateFlowStep";
import {
@@ -114,6 +115,7 @@ export function FinalReviewScreen({
useState<TemplateChipDetail | null>(null);
const [communityContextModalOpen, setCommunityContextModalOpen] =
useState(false);
const [titleModalOpen, setTitleModalOpen] = useState(false);
const handleSave = useCallback(
(patch: FinalReviewChipEditPatch) => {
@@ -225,6 +227,9 @@ export function FinalReviewScreen({
const rawCommunityContextForModal =
typeof state.communityContext === "string" ? state.communityContext : "";
const rawTitleForModal =
typeof state.title === "string" ? state.title : "";
const descriptionEmptyHint =
variant === "editPublished" ? t("communityContextEditModal.emptyHint") : undefined;
@@ -242,6 +247,16 @@ export function FinalReviewScreen({
<Rule
title={ruleCardTitle}
description={ruleCardDescription}
onTitleClick={
variant === "editPublished"
? () => setTitleModalOpen(true)
: undefined
}
titleEditAriaLabel={
variant === "editPublished"
? t("titleEditModal.ariaEditTitle")
: undefined
}
onDescriptionClick={
variant === "editPublished"
? () => setCommunityContextModalOpen(true)
@@ -278,15 +293,26 @@ export function FinalReviewScreen({
detail={activeReadOnlyDetail}
/>
{variant === "editPublished" ? (
<FinalReviewCommunityContextEditModal
isOpen={communityContextModalOpen}
onClose={() => setCommunityContextModalOpen(false)}
initialValue={rawCommunityContextForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ communityContext: value, summary: value });
}}
/>
<>
<FinalReviewTitleEditModal
isOpen={titleModalOpen}
onClose={() => setTitleModalOpen(false)}
initialValue={rawTitleForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ title: value });
}}
/>
<FinalReviewCommunityContextEditModal
isOpen={communityContextModalOpen}
onClose={() => setCommunityContextModalOpen(false)}
initialValue={rawCommunityContextForModal}
onSave={(value) => {
markCreateFlowInteraction();
updateState({ communityContext: value, summary: value });
}}
/>
</>
) : null}
</CreateFlowLockupCardStepShell>
);
@@ -125,6 +125,7 @@ export default async function UseCaseDetailPage({ params }: PageProps) {
description: ruleCard.description,
backgroundColor: ruleCard.backgroundColor,
iconPath: ruleCard.iconPath,
href: `/use-cases/${slug}/rule`,
}}
/>
<article
+14
View File
@@ -0,0 +1,14 @@
import type { ReactNode } from "react";
/** Full-viewport case-study surfaces (completed rule demos) — no marketing footer. */
export default function MarketingCaseStudyLayout({
children,
}: {
children: ReactNode;
}) {
return (
<main className="flex h-dvh min-h-0 flex-col overflow-hidden">
{children}
</main>
);
}
@@ -0,0 +1,136 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import CommunityRule from "../../../../../components/type/CommunityRule";
import type { CommunityRuleSection } from "../../../../../components/type/CommunityRule/CommunityRule.types";
import CreateFlowTopNav from "../../../../../components/navigation/CreateFlowTopNav";
import Share from "../../../../../components/modals/Share";
import Alert from "../../../../../components/modals/Alert";
import { CreateFlowHeaderLockup } from "../../../../../(app)/create/components/CreateFlowHeaderLockup";
import {
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../../../../(app)/create/components/createFlowLayoutTokens";
import { useCreateFlowMdUp } from "../../../../../(app)/create/hooks/useCreateFlowMdUp";
import { useTranslation } from "../../../../../contexts/MessagesContext";
import type { UseCaseDetailSlug } from "../../../../../../lib/useCaseSyntheticPost";
import type { UseCaseCompletedRuleFixture } from "../../../../../../lib/useCaseCompletedRule";
import {
useUseCaseCompletedRuleActions,
type UseCaseCompletedRuleActionBanner,
} from "./useUseCaseCompletedRuleActions";
export type UseCaseCompletedRuleViewProps = {
slug: UseCaseDetailSlug;
fixture: UseCaseCompletedRuleFixture;
sections: CommunityRuleSection[];
};
/** Figma: Completed CR — use case demos (21995:39476, 21995:40092, 22015:42413). */
export function UseCaseCompletedRuleView({
slug,
fixture,
sections,
}: UseCaseCompletedRuleViewProps) {
const router = useRouter();
const mdUp = useCreateFlowMdUp();
const tTopNav = useTranslation("pages.useCasesCompletedRule.topNav");
const [shareModalOpen, setShareModalOpen] = useState(false);
const [actionBanner, setActionBanner] =
useState<UseCaseCompletedRuleActionBanner | null>(null);
const { copyPageLink, mailtoPageLink, handleDuplicate } =
useUseCaseCompletedRuleActions({
slug,
fixture,
setActionBanner,
});
const pageBg = fixture.pageBackground;
return (
<>
{/*
Mobile: grid scrolls (title sticky at top of scrollport).
Desktop: viewport-tall columns; rule scrolls in the right column only.
*/}
<div
className="flex min-h-0 w-full flex-1 flex-col overflow-hidden md:h-full"
style={{ background: pageBg }}
>
{actionBanner ? (
<div className="pointer-events-none fixed inset-x-0 top-0 z-20 flex justify-center px-5 pt-3">
<div className="pointer-events-auto w-full max-w-[639px]">
<Alert
type="banner"
status={actionBanner.status}
title={actionBanner.title}
description={actionBanner.description}
hasLeadingIcon
hasBodyText={Boolean(actionBanner.description)}
onClose={() => setActionBanner(null)}
className="w-full"
/>
</div>
</div>
) : null}
<Share
isOpen={shareModalOpen}
onClose={() => setShareModalOpen(false)}
onCopyLink={() => void copyPageLink()}
onEmailShare={mailtoPageLink}
onSignalShare={() => void copyPageLink()}
onSlackShare={() => void copyPageLink()}
onDiscordShare={() => void copyPageLink()}
/>
<CreateFlowTopNav
hasShare
hasDuplicate
duplicateLabel={tTopNav("duplicate")}
duplicateAriaLabel={tTopNav("duplicateAriaLabel")}
exitLabel={tTopNav("return")}
buttonPalette="inverse"
className="shrink-0 !bg-transparent"
onShare={() => setShareModalOpen(true)}
onDuplicate={() => void handleDuplicate()}
onExit={() => router.push(`/use-cases/${slug}`)}
/>
<div
className={`mx-auto grid w-full min-h-0 flex-1 grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:gap-6 max-md:overflow-y-auto max-md:overscroll-y-contain max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:flex-1 md:grid-cols-2 md:grid-rows-1 md:items-start md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
>
<div
className={`relative z-[1] flex flex-col justify-start max-md:sticky max-md:top-0 max-md:z-10 max-md:shrink-0 max-md:pb-4 md:sticky md:top-0 md:z-[1] md:flex md:h-[calc(100dvh-4rem)] md:max-h-[calc(100dvh-4rem)] md:flex-col md:justify-center md:self-start md:overflow-hidden md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
style={{ background: pageBg }}
>
<CreateFlowHeaderLockup
title={fixture.title}
description={fixture.summary}
justification="left"
palette="inverse"
/>
</div>
<div
className={`scrollbar-hide relative z-0 flex min-h-min flex-col overflow-x-hidden max-md:shrink-0 md:h-[calc(100dvh-4rem)] md:max-h-[calc(100dvh-4rem)] md:min-h-0 md:self-start md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<div
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 md:block"
style={{
backgroundImage: `linear-gradient(to bottom, color-mix(in srgb, ${pageBg} 55%, transparent), color-mix(in srgb, ${pageBg} 20%, transparent) 50%, transparent)`,
}}
aria-hidden
/>
<div className="w-full min-w-0 py-0 md:pb-8">
<CommunityRule
sections={sections}
useCardStyle={!mdUp}
cardAccentColor={pageBg}
className={mdUp ? "min-w-0" : "w-full min-w-0 p-4"}
/>
</div>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,122 @@
"use client";
import { useCallback, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useAuthModal } from "../../../../../contexts/AuthModalContext";
import { useTranslation } from "../../../../../contexts/MessagesContext";
import {
duplicateUseCaseTemplate,
fetchAuthSession,
} from "../../../../../../lib/create/api";
import type { UseCaseDetailSlug } from "../../../../../../lib/useCaseSyntheticPost";
import type { UseCaseCompletedRuleFixture } from "../../../../../../lib/useCaseCompletedRule";
export type UseCaseCompletedRuleActionBanner = {
key: string;
status: "positive" | "danger";
title: string;
description?: string;
};
export function useUseCaseCompletedRuleActions({
slug,
fixture,
setActionBanner,
}: {
slug: UseCaseDetailSlug;
fixture: UseCaseCompletedRuleFixture;
setActionBanner: (_: UseCaseCompletedRuleActionBanner | null) => void;
}) {
const router = useRouter();
const pathname = usePathname();
const { openLogin } = useAuthModal();
const t = useTranslation("pages.useCasesCompletedRule.topNav");
const [duplicateBusy, setDuplicateBusy] = useState(false);
const copyPageLink = useCallback(async () => {
if (typeof window === "undefined") return;
try {
await navigator.clipboard.writeText(window.location.href);
setActionBanner({
key: "shareCopied",
status: "positive",
title: t("shareLinkCopiedTitle"),
description: t("shareLinkCopiedDescription"),
});
} catch {
setActionBanner({
key: "shareCopyFailed",
status: "danger",
title: t("shareCopyFailedTitle"),
description: t("shareCopyFailedDescription"),
});
}
}, [setActionBanner, t]);
const mailtoPageLink = useCallback(() => {
if (typeof window === "undefined") return;
const url = window.location.href;
const subject = encodeURIComponent(fixture.title);
const body = encodeURIComponent(`${fixture.summary}\n\n${url}`);
window.location.href = `mailto:?subject=${subject}&body=${body}`;
}, [fixture.summary, fixture.title]);
const handleDuplicate = useCallback(async () => {
if (duplicateBusy) return;
setActionBanner(null);
const { user } = await fetchAuthSession();
if (!user) {
openLogin({
nextPath:
pathname && pathname.length > 0
? pathname
: `/use-cases/${slug}/rule`,
backdropVariant: "blurredYellow",
});
return;
}
setDuplicateBusy(true);
const res = await duplicateUseCaseTemplate(slug);
setDuplicateBusy(false);
if (res.ok === false) {
if (res.status === 401) {
openLogin({
nextPath:
pathname && pathname.length > 0
? pathname
: `/use-cases/${slug}/rule`,
backdropVariant: "blurredYellow",
});
return;
}
setActionBanner({
key: "duplicateFailed",
status: "danger",
title: t("duplicateFailedTitle"),
description:
res.status === 404 ? t("duplicateNotFoundDescription") : res.error,
});
return;
}
router.push("/profile");
}, [
duplicateBusy,
openLogin,
pathname,
router,
setActionBanner,
slug,
t,
]);
return {
copyPageLink,
mailtoPageLink,
handleDuplicate,
};
}
@@ -0,0 +1,66 @@
/**
* Figma: Completed CR — use case community rule demos
* (21995:39476, 21995:40092, 22015:42413)
*/
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import messages from "../../../../../messages/en/index";
import { resolveUseCaseCompletedRule } from "../../../../../lib/useCaseCompletedRule";
import {
USE_CASE_DETAIL_SLUGS,
useCaseContentKeyForSlug,
} from "../../../../../lib/useCaseSyntheticPost";
import { UseCaseCompletedRuleView } from "./_components/UseCaseCompletedRule.view";
type PageProps = {
params: Promise<{ slug: string }>;
};
export function generateStaticParams() {
return USE_CASE_DETAIL_SLUGS.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const resolved = resolveUseCaseCompletedRule(
slug,
messages.pages.useCasesCompletedRules,
);
if (!resolved) {
return {};
}
const contentKey = useCaseContentKeyForSlug(resolved.slug);
const meta = messages.metadata.useCasesCompletedRule[contentKey];
return {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
openGraph: {
title: meta.title,
description: meta.description,
type: "website",
siteName: "CommunityRule",
},
};
}
export default async function UseCaseCompletedRulePage({ params }: PageProps) {
const { slug } = await params;
const resolved = resolveUseCaseCompletedRule(
slug,
messages.pages.useCasesCompletedRules,
);
if (!resolved) {
notFound();
}
return (
<UseCaseCompletedRuleView
slug={resolved.slug}
fixture={resolved.fixture}
sections={resolved.sections}
/>
);
}
@@ -0,0 +1,67 @@
import type { Prisma } from "@prisma/client";
import { NextResponse } from "next/server";
import messages from "../../../../../messages/en/index";
import { prisma } from "../../../../../lib/server/db";
import { isDatabaseConfigured } from "../../../../../lib/server/env";
import {
dbUnavailable,
notFound,
unauthorized,
} from "../../../../../lib/server/responses";
import { getSessionUser } from "../../../../../lib/server/session";
import { apiRoute } from "../../../../../lib/server/apiRoute";
import { resolveUseCaseCompletedRule } from "../../../../../lib/useCaseCompletedRule";
import { isUseCaseDetailSlug } from "../../../../../lib/useCaseSyntheticPost";
import { normalizePublishedDocumentForEdit } from "../../../../../lib/create/normalizePublishedDocumentForEdit";
import { useCaseTemplateDuplicateTitle } from "../../../../../lib/useCaseTemplateDuplicate";
type RouteContext = { params: Promise<{ slug: string }> };
export const POST = apiRoute<RouteContext>(
"useCases.bySlug.duplicate",
async (_request, context) => {
if (!isDatabaseConfigured()) {
return dbUnavailable();
}
const user = await getSessionUser();
if (!user) {
return unauthorized();
}
const { slug } = await context.params;
if (!isUseCaseDetailSlug(slug)) {
return notFound();
}
const resolved = resolveUseCaseCompletedRule(
slug,
messages.pages.useCasesCompletedRules,
);
if (!resolved) {
return notFound();
}
const { fixture } = resolved;
const newRule = await prisma.publishedRule.create({
data: {
userId: user.id,
title: useCaseTemplateDuplicateTitle(fixture.title),
summary: fixture.summary,
document: normalizePublishedDocumentForEdit(
fixture.document,
) as Prisma.InputJsonValue,
},
});
return NextResponse.json({
rule: {
id: newRule.id,
title: newRule.title,
summary: newRule.summary,
createdAt: newRule.createdAt,
updatedAt: newRule.updatedAt,
},
});
},
);
@@ -10,11 +10,11 @@ const SURFACE_CLASS: Record<CaseStudyProps["surface"], string> = {
rose: "bg-[var(--color-surface-invert-brand-red)]",
};
/** Default art per tile: PNG composites (FNB/BCSM) or vector Mutual Aid logo. */
/** Default art per tile: Figma-exported SVG composites (305×305 incl. rounded bg). */
const SURFACE_ART: Record<CaseStudyProps["surface"], string> = {
lavender: "/assets/case-study/case-study-mutual-aid.svg",
neutral: "/assets/use-cases/case-study-food-not-bombs.png",
rose: "/assets/use-cases/case-study-boulder-county-street-medics.png",
neutral: "/assets/case-study/case-study-food-not-bombs.svg",
rose: "/assets/case-study/case-study-boulder-county-street-medics.svg",
};
/** Figma: ~23px corner (“Card / CaseStudy” shells). */
@@ -39,12 +39,8 @@ function CaseStudyView({
alt={imageAlt}
width={305}
height={305}
unoptimized={
SURFACE_ART[surface].endsWith(".svg") ? true : undefined
}
className={`pointer-events-none select-none ${
surface === "lavender" ? "object-contain object-center" : "object-cover"
}`}
unoptimized
className="pointer-events-none size-full select-none object-contain object-center"
draggable={false}
/>
)}
@@ -28,6 +28,8 @@ const RuleContainer = memo<RuleProps>(
onDescriptionClick,
descriptionEmptyHint,
descriptionEditAriaLabel,
onTitleClick,
titleEditAriaLabel,
icon,
backgroundColor = "bg-[var(--color-community-teal-100)]",
className = "",
@@ -84,6 +86,8 @@ const RuleContainer = memo<RuleProps>(
onDescriptionClick={onDescriptionClick}
descriptionEmptyHint={descriptionEmptyHint}
descriptionEditAriaLabel={descriptionEditAriaLabel}
onTitleClick={onTitleClick}
titleEditAriaLabel={titleEditAriaLabel}
icon={icon}
backgroundColor={backgroundColor}
className={className}
+9
View File
@@ -39,6 +39,13 @@ export interface RuleProps {
descriptionEditAriaLabel?: string;
/** Shown when {@link onDescriptionClick} is set and `description` is empty. */
descriptionEmptyHint?: string;
/**
* When set, the title in the card header is clickable — caller handles modal /
* navigation (e.g. edit published rule).
*/
onTitleClick?: () => void;
/** When {@link onTitleClick} is set, forwarded to the control’s `aria-label`. */
titleEditAriaLabel?: string;
icon?: React.ReactNode;
backgroundColor?: string;
className?: string;
@@ -80,6 +87,8 @@ export interface RuleViewProps {
onDescriptionClick?: () => void;
descriptionEmptyHint?: string;
descriptionEditAriaLabel?: string;
onTitleClick?: () => void;
titleEditAriaLabel?: string;
icon?: React.ReactNode;
backgroundColor: string;
className: string;
+23 -5
View File
@@ -14,6 +14,8 @@ export function RuleView({
onDescriptionClick,
descriptionEmptyHint,
descriptionEditAriaLabel,
onTitleClick,
titleEditAriaLabel,
icon,
backgroundColor,
className,
@@ -307,11 +309,27 @@ export function RuleView({
{t("recommendedLabel")}
</Tag>
) : null}
<h3
className={`${titleClass} cursor-inherit text-[var(--color-content-invert-primary)] overflow-hidden text-ellipsis w-full`}
>
{title}
</h3>
{onTitleClick ? (
<InlineTextButton
type="button"
underline={false}
data-testid="rule-title-edit"
ariaLabel={titleEditAriaLabel}
className={`${titleClass} w-full min-w-0 cursor-pointer text-left text-[var(--color-content-invert-primary)] hover:!opacity-100 overflow-hidden text-ellipsis`}
onClick={(e) => {
e.stopPropagation();
onTitleClick();
}}
>
{title}
</InlineTextButton>
) : (
<h3
className={`${titleClass} cursor-inherit text-[var(--color-content-invert-primary)] overflow-hidden text-ellipsis w-full`}
>
{title}
</h3>
)}
</div>
</div>
)}
@@ -2,6 +2,7 @@
import { memo } from "react";
import { usePathname } from "next/navigation";
import { isChromelessNavigationPath } from "../../../lib/navigationChromelessPath";
import TopWithPathname from "./Top/TopWithPathname";
export type ConditionalNavigationClientProps = {
@@ -15,10 +16,8 @@ export type ConditionalNavigationClientProps = {
const ConditionalNavigationClient = memo(
({ initialSignedIn }: ConditionalNavigationClientProps) => {
const pathname = usePathname();
const isCreateFlow = pathname?.startsWith("/create");
const isLogin = pathname === "/login";
if (isCreateFlow || isLogin) {
if (isChromelessNavigationPath(pathname)) {
return null;
}
@@ -16,17 +16,23 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
hasShare = false,
hasExport = false,
hasEdit = false,
hasDuplicate = false,
hasManageStakeholders = false,
saveDraftOnExit = false,
onShare,
onSelectExportFormat,
onEdit,
onDuplicate,
onManageStakeholders,
onExit,
exitLabel,
duplicateLabel,
duplicateAriaLabel,
buttonPalette,
className = "",
}) => {
const router = useRouter();
const t = useTranslation("create.topNav");
const tPopover = useTranslation("modals.popoverExport");
const handleExit = (options?: { saveDraft?: boolean }) => {
@@ -43,19 +49,26 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
hasShare={hasShare}
hasExport={hasExport}
hasEdit={hasEdit}
hasDuplicate={hasDuplicate}
hasManageStakeholders={hasManageStakeholders}
saveDraftOnExit={saveDraftOnExit}
onShare={onShare}
onSelectExportFormat={onSelectExportFormat}
onEdit={onEdit}
onDuplicate={onDuplicate}
onManageStakeholders={onManageStakeholders}
onExit={handleExit}
exitLabel={exitLabel}
duplicateLabel={duplicateLabel}
duplicateAriaLabel={duplicateAriaLabel}
buttonPalette={buttonPalette}
className={className}
exportPopoverMenuAriaLabel={tPopover("menuAriaLabel")}
exportPopoverPdfLabel={tPopover("downloadPdf")}
exportPopoverCsvLabel={tPopover("downloadCsv")}
exportPopoverMarkdownLabel={tPopover("downloadMarkdown")}
moreOptionsAriaLabel={t("moreOptionsAriaLabel")}
actionsMenuAriaLabel={t("actionsMenuAriaLabel")}
/>
);
},
@@ -21,6 +21,11 @@ export interface CreateFlowTopNavProps {
* @default false
*/
hasEdit?: boolean;
/**
* Whether to show Duplicate instead of Edit (marketing completed demos).
* @default false
*/
hasDuplicate?: boolean;
/**
* Whether to show **Manage Stakeholders** (published-rule invite management).
* Used on `/create/edit-rule` only.
@@ -45,6 +50,17 @@ export interface CreateFlowTopNavProps {
* Callback when Edit button is clicked
*/
onEdit?: () => void;
/**
* Callback when Duplicate button is clicked
*/
onDuplicate?: () => void;
/**
* Override exit button label (e.g. "Return" on marketing demos).
*/
exitLabel?: string;
/** Label for Duplicate when {@link hasDuplicate} is true. */
duplicateLabel?: string;
duplicateAriaLabel?: string;
/**
* Callback when Manage Stakeholders is clicked
*/
@@ -71,4 +87,6 @@ export type CreateFlowTopNavViewProps = CreateFlowTopNavProps & {
exportPopoverPdfLabel: string;
exportPopoverCsvLabel: string;
exportPopoverMarkdownLabel: string;
moreOptionsAriaLabel: string;
actionsMenuAriaLabel: string;
};
@@ -1,39 +1,178 @@
"use client";
import { useEffect, useId, useRef, useState } from "react";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import type { IconName } from "../../asset/icon";
import Logo from "../../asset/Logo";
import Button from "../../buttons/Button";
import ListItem from "../../layout/ListItem";
import Popover from "../../modals/Popover";
import { useCreateFlowSm2Up } from "../../../(app)/create/hooks/useCreateFlowSm2Up";
import { useTranslation } from "../../../contexts/MessagesContext";
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
const outlineButtonClass =
"md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]";
const exitButtonFigmaClass =
"!rounded-[var(--radius-measures-radius-full,9999px)] !border-[1.25px] !px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!text-[12px] md:!leading-[14px]";
type ActionMenuItem = {
id: string;
label: string;
leadingIcon: IconName;
onClick: () => void;
};
function KebabIcon({ className = "" }: { className?: string }) {
return (
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`shrink-0 md:h-[14px] md:w-[14px] ${className}`}
aria-hidden="true"
>
<circle cx="4" cy="8" r="1.5" fill="currentColor" />
<circle cx="8" cy="8" r="1.5" fill="currentColor" />
<circle cx="12" cy="8" r="1.5" fill="currentColor" />
</svg>
);
}
export function CreateFlowTopNavView({
hasShare = false,
hasExport = false,
hasEdit = false,
hasDuplicate = false,
hasManageStakeholders = false,
saveDraftOnExit = false,
onShare,
onSelectExportFormat,
onEdit,
onDuplicate,
onManageStakeholders,
onExit,
exitLabel,
duplicateLabel,
duplicateAriaLabel,
buttonPalette = "default",
className = "",
exportPopoverMenuAriaLabel,
exportPopoverPdfLabel,
exportPopoverCsvLabel,
exportPopoverMarkdownLabel,
moreOptionsAriaLabel,
actionsMenuAriaLabel,
}: CreateFlowTopNavViewProps) {
const t = useTranslation("create.topNav");
const exitButtonText = saveDraftOnExit ? t("saveAndExit") : t("exit");
const sm2Up = useCreateFlowSm2Up();
const exitButtonText =
exitLabel ?? (saveDraftOnExit ? t("saveAndExit") : t("exit"));
const [exportMenuOpen, setExportMenuOpen] = useState(false);
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
const exportWrapRef = useRef<HTMLDivElement>(null);
const actionsWrapRef = useRef<HTMLDivElement>(null);
const exportMenuId = useId();
const actionsMenuId = useId();
const hasSecondaryActions =
hasShare ||
hasExport ||
hasEdit ||
hasDuplicate ||
hasManageStakeholders;
const useKebabMenu = hasSecondaryActions && !sm2Up;
const actionMenuItems = useMemo((): ActionMenuItem[] => {
const items: ActionMenuItem[] = [];
if (hasShare && onShare) {
items.push({
id: "share",
label: t("share"),
leadingIcon: "mail",
onClick: onShare,
});
}
if (hasExport && onSelectExportFormat) {
items.push(
{
id: "export-pdf",
label: exportPopoverPdfLabel,
leadingIcon: "picture_as_pdf",
onClick: () => onSelectExportFormat("pdf"),
},
{
id: "export-csv",
label: exportPopoverCsvLabel,
leadingIcon: "csv",
onClick: () => onSelectExportFormat("csv"),
},
{
id: "export-markdown",
label: exportPopoverMarkdownLabel,
leadingIcon: "markdown_copy",
onClick: () => onSelectExportFormat("markdown"),
},
);
}
if (hasDuplicate && onDuplicate) {
items.push({
id: "duplicate",
label: duplicateLabel ?? t("edit"),
leadingIcon: "content_copy",
onClick: onDuplicate,
});
} else if (hasEdit && onEdit) {
items.push({
id: "edit",
label: t("edit"),
leadingIcon: "edit",
onClick: onEdit,
});
}
if (hasManageStakeholders && onManageStakeholders) {
items.push({
id: "manage-stakeholders",
label: t("manageStakeholders"),
leadingIcon: "tags",
onClick: onManageStakeholders,
});
}
items.push({
id: "exit",
label: exitButtonText,
leadingIcon: "log_out",
onClick: () => void onExit?.({ saveDraft: saveDraftOnExit }),
});
return items;
}, [
duplicateLabel,
exitButtonText,
exportPopoverCsvLabel,
exportPopoverMarkdownLabel,
exportPopoverPdfLabel,
hasDuplicate,
hasEdit,
hasExport,
hasManageStakeholders,
hasShare,
onDuplicate,
onEdit,
onExit,
onManageStakeholders,
onSelectExportFormat,
onShare,
saveDraftOnExit,
t,
]);
useEffect(() => {
if (!exportMenuOpen) return;
@@ -49,6 +188,20 @@ export function CreateFlowTopNavView({
return () => document.removeEventListener("mousedown", onDoc);
}, [exportMenuOpen]);
useEffect(() => {
if (!actionsMenuOpen) return;
const onDoc = (e: MouseEvent) => {
if (
actionsWrapRef.current &&
!actionsWrapRef.current.contains(e.target as Node)
) {
setActionsMenuOpen(false);
}
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [actionsMenuOpen]);
useEffect(() => {
if (!exportMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
@@ -58,6 +211,155 @@ export function CreateFlowTopNavView({
return () => window.removeEventListener("keydown", onKey);
}, [exportMenuOpen]);
useEffect(() => {
if (!actionsMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setActionsMenuOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [actionsMenuOpen]);
const inlineActions = (
<>
{hasShare && (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
onClick={onShare}
ariaLabel={t("shareAriaLabel")}
className={outlineButtonClass}
>
{t("share")}
</Button>
)}
{hasExport && onSelectExportFormat ? (
<div className="relative" ref={exportWrapRef}>
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
ariaLabel={t("exportAriaLabel")}
aria-haspopup="menu"
aria-expanded={exportMenuOpen}
aria-controls={exportMenuId}
onClick={() => setExportMenuOpen((o) => !o)}
className={`justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] ${outlineButtonClass}`}
>
<span>{t("export")}</span>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 md:h-[14px] md:w-[14px]"
aria-hidden="true"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</Button>
{exportMenuOpen ? (
<div className="absolute right-0 top-[calc(100%+var(--spacing-measures-spacing-200,8px))] z-[300]">
<Popover
id={exportMenuId}
menuAriaLabel={exportPopoverMenuAriaLabel}
>
<ListItem
showDivider
leadingIcon="picture_as_pdf"
label={exportPopoverPdfLabel}
onClick={() => {
onSelectExportFormat("pdf");
setExportMenuOpen(false);
}}
/>
<ListItem
showDivider
leadingIcon="csv"
label={exportPopoverCsvLabel}
onClick={() => {
onSelectExportFormat("csv");
setExportMenuOpen(false);
}}
/>
<ListItem
showDivider={false}
leadingIcon="markdown_copy"
label={exportPopoverMarkdownLabel}
onClick={() => {
onSelectExportFormat("markdown");
setExportMenuOpen(false);
}}
/>
</Popover>
</div>
) : null}
</div>
) : null}
{hasDuplicate && (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
onClick={onDuplicate}
ariaLabel={
duplicateAriaLabel ?? duplicateLabel ?? t("editAriaLabel")
}
className={outlineButtonClass}
>
{duplicateLabel ?? t("edit")}
</Button>
)}
{hasEdit && !hasDuplicate && (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
onClick={onEdit}
ariaLabel={t("editAriaLabel")}
className={outlineButtonClass}
>
{t("edit")}
</Button>
)}
{hasManageStakeholders && onManageStakeholders ? (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
onClick={onManageStakeholders}
ariaLabel={t("manageStakeholdersAriaLabel")}
className={outlineButtonClass}
>
{t("manageStakeholders")}
</Button>
) : null}
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
onClick={() => void onExit?.({ saveDraft: saveDraftOnExit })}
ariaLabel={exitButtonText}
className={`shrink-0 ${exitButtonFigmaClass} !text-[10px] !leading-[12px] !py-[6px] md:!py-[8px]`}
>
{exitButtonText}
</Button>
</>
);
return (
<header
className={`bg-black w-full ${className}`}
@@ -72,126 +374,56 @@ export function CreateFlowTopNavView({
<Logo size="createFlow" wordmark palette={buttonPalette} />
<div className="flex flex-wrap items-center justify-end gap-[var(--spacing-scale-012,12px)]">
{hasShare && (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
onClick={onShare}
ariaLabel={t("shareAriaLabel")}
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
>
{t("share")}
</Button>
)}
{hasExport && onSelectExportFormat ? (
<div className="relative" ref={exportWrapRef}>
{useKebabMenu ? (
<div className="relative" ref={actionsWrapRef}>
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
ariaLabel={t("exportAriaLabel")}
ariaLabel={moreOptionsAriaLabel}
aria-haspopup="menu"
aria-expanded={exportMenuOpen}
aria-controls={exportMenuId}
onClick={() => setExportMenuOpen((o) => !o)}
className="justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] !text-[10px] md:!text-[12px] !leading-[12px] md:!leading-[14px] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
aria-expanded={actionsMenuOpen}
aria-controls={actionsMenuId}
onClick={() => setActionsMenuOpen((open) => !open)}
className={`!px-[var(--spacing-scale-010,10px)] ${outlineButtonClass}`}
>
<span>{t("export")}</span>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 md:w-[14px] md:h-[14px]"
aria-hidden="true"
>
<path d="M19 9l-7 7-7-7" />
</svg>
<KebabIcon />
</Button>
{exportMenuOpen ? (
{actionsMenuOpen ? (
<div className="absolute right-0 top-[calc(100%+var(--spacing-measures-spacing-200,8px))] z-[300]">
<Popover
id={exportMenuId}
menuAriaLabel={exportPopoverMenuAriaLabel}
>
<ListItem
showDivider
leadingIcon="picture_as_pdf"
label={exportPopoverPdfLabel}
onClick={() => {
onSelectExportFormat("pdf");
setExportMenuOpen(false);
}}
/>
<ListItem
showDivider
leadingIcon="csv"
label={exportPopoverCsvLabel}
onClick={() => {
onSelectExportFormat("csv");
setExportMenuOpen(false);
}}
/>
<ListItem
showDivider={false}
leadingIcon="markdown_copy"
label={exportPopoverMarkdownLabel}
onClick={() => {
onSelectExportFormat("markdown");
setExportMenuOpen(false);
}}
/>
<Popover id={actionsMenuId} menuAriaLabel={actionsMenuAriaLabel}>
{actionMenuItems.map((item, index) => (
<ListItem
key={item.id}
showDivider={index < actionMenuItems.length - 1}
leadingIcon={item.leadingIcon}
label={item.label}
onClick={() => {
item.onClick();
setActionsMenuOpen(false);
}}
/>
))}
</Popover>
</div>
) : null}
</div>
) : null}
{hasEdit && (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
onClick={onEdit}
ariaLabel={t("editAriaLabel")}
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
>
{t("edit")}
</Button>
)}
{hasManageStakeholders && onManageStakeholders ? (
) : hasSecondaryActions ? (
inlineActions
) : (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
onClick={onManageStakeholders}
ariaLabel={t("manageStakeholdersAriaLabel")}
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
onClick={() => void onExit?.({ saveDraft: saveDraftOnExit })}
ariaLabel={exitButtonText}
className={`shrink-0 ${exitButtonFigmaClass} !text-[10px] !leading-[12px] !py-[6px] md:!py-[8px]`}
>
{t("manageStakeholders")}
{exitButtonText}
</Button>
) : null}
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
onClick={() => void onExit?.({ saveDraft: saveDraftOnExit })}
ariaLabel={exitButtonText}
className={`md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !py-[6px] md:!py-[8px] shrink-0 ${exitButtonFigmaClass}`}
>
{exitButtonText}
</Button>
)}
</div>
</nav>
</header>
@@ -36,7 +36,7 @@ const VARIANT_STYLES: Record<
},
};
/** Figma **Section/AskOrganizer** [18116:15960](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=18116-15960&m=dev) (`lg` shell + type + button). Use-case detail: [22015:42624](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22015-42624&m=dev). */
/** Figma **Section/AskOrganizer** — baseline default [17487:12288](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=17487-12288&m=dev), inverse [19189:8140](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19189-8140&m=dev); md+ [16306:14995](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=16306-14995&m=dev). Use-case detail instance: [22015:42624](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22015-42624&m=dev). */
const AskOrganizerContainer = memo<AskOrganizerProps>(
({
title,
@@ -61,9 +61,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
const sectionPadding =
resolvedVariant === "compact"
? "py-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)]"
: resolvedVariant === "use-case-detail"
? "w-full py-[var(--spacing-scale-096)] px-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-064)]"
: "py-[var(--spacing-scale-096)] px-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-064)]";
: resolvedVariant === "use-case-detail" || resolvedVariant === "inverse"
? "w-full py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)]"
: "py-[var(--spacing-scale-040)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)]";
const contentGap =
resolvedVariant === "compact"
@@ -38,7 +38,7 @@ function AskOrganizerView({
data-figma-node={isUseCaseDetail ? "22015-42624" : "18116-15960"}
>
<div
className={`mx-auto flex w-full min-w-[358px] max-w-[1280px] flex-col ${contentGap} ${isUseCaseDetail ? "items-center" : ""}`}
className={`mx-auto flex w-full min-w-0 max-w-[1280px] flex-col md:min-w-[358px] ${contentGap} ${isUseCaseDetail ? "items-center" : ""}`}
>
{/* Content Lockup */}
<ContentLockup
@@ -56,10 +56,10 @@ function AskOrganizerView({
>
<Button
{...(buttonHref ? { href: buttonHref } : {})}
size="large"
size="small"
buttonType="filled"
palette={buttonPalette}
className="!px-[var(--spacing-scale-016)] !py-[var(--spacing-scale-012)]"
className="!px-[var(--spacing-scale-010)] md:!px-[var(--spacing-scale-016)] md:!py-[var(--spacing-scale-012)] md:!text-[16px] md:!leading-[20px]"
onClick={onContactClick}
ariaLabel={ariaLabel}
data-testid="ask-organizer-cta"
@@ -9,6 +9,8 @@ export interface ContentBannerRulePreview {
description: string;
backgroundColor: string;
iconPath: string;
/** When set, the rule preview links to the completed community rule screen. */
href?: string;
}
export interface ContentBannerProps {
@@ -1,7 +1,9 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import ContentContainer from "../../content/ContentContainer";
import Rule from "../../cards/Rule";
import {
@@ -123,16 +125,23 @@ function ContentBannerArticleView({
function ContentBannerUseCaseView({
post,
rulePreview,
}: Pick<ContentBannerViewProps, "post" | "rulePreview">) {
contentTone = "inverse",
leadingImageSrc,
leadingImageAlt,
}: Pick<
ContentBannerViewProps,
| "post"
| "rulePreview"
| "contentTone"
| "leadingImageSrc"
| "leadingImageAlt"
>) {
const t = useTranslation("pages.useCasesCompletedRule");
if (!rulePreview) {
return null;
}
const { title, description, author, date } = post.frontmatter;
const formattedDate = new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
});
const { title } = post.frontmatter;
return (
<section
@@ -141,52 +150,77 @@ function ContentBannerUseCaseView({
>
<div
data-figma-node="22015:42621"
className="mx-auto flex w-full max-w-[1024px] flex-col items-center gap-[var(--space-800)] px-[var(--space-1200)] py-[var(--space-1000)] md:flex-row md:items-center"
className="mx-auto grid w-full max-w-[1024px] grid-cols-1 items-center gap-[var(--space-800)] px-[var(--space-1200)] py-[var(--space-1000)] lg:grid-cols-2 lg:items-center"
>
<div
data-node-id="19189:9171"
className="flex w-full max-w-[365px] shrink-0 flex-col gap-[var(--spacing-scale-024)]"
className="flex w-full min-w-0 shrink-0 flex-col lg:max-w-[365px]"
>
<div className="flex w-full flex-col gap-[var(--measures-spacing-016)]">
<div className="flex w-full flex-col gap-[var(--measures-spacing-004)] text-[var(--color-content-inverse-brand-royal)]">
<h1 className="w-full font-bricolage font-medium text-[32px] leading-[110%] sm:text-[40px] lg:text-[44px]">
{title}
</h1>
{description ? (
<p className="w-full font-inter font-normal text-[16px] leading-[130%] sm:text-[18px]">
{description}
</p>
) : null}
</div>
</div>
<div className="flex w-full items-end gap-[var(--measures-spacing-008)] font-inter text-[14px] leading-[20px] text-[var(--color-content-inverse-brand-royal)]">
<span>{author}</span>
<span>{formattedDate}</span>
</div>
<ContentContainer
post={post}
size="responsive"
tone={contentTone}
showLeadingImage={false}
leadingImageSrc={leadingImageSrc}
leadingImageAlt={leadingImageAlt}
/>
</div>
<div className="flex min-w-0 w-full flex-1">
<Rule
title={rulePreview.title}
description={rulePreview.description}
expanded
fluidWidth
size="L"
templateGridFigmaShell
backgroundColor={rulePreview.backgroundColor}
className="pointer-events-none w-full select-none rounded-[24px]"
icon={
<Image
src={getAssetPath(rulePreview.iconPath)}
alt=""
width={103}
height={103}
draggable={false}
unoptimized={rulePreview.iconPath.endsWith(".svg")}
className="aspect-square size-full max-h-[103px] max-w-[103px] object-contain mix-blend-luminosity"
<div className="flex min-w-0 w-full">
{rulePreview.href ? (
<Link
href={rulePreview.href}
className="block w-full rounded-[24px] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-brand-primary)] focus-visible:ring-offset-2"
aria-label={t("ruleCardLinkAriaLabel").replace(
"{title}",
rulePreview.title,
)}
>
<Rule
title={rulePreview.title}
description={rulePreview.description}
expanded
fluidWidth
size="L"
templateGridFigmaShell
backgroundColor={rulePreview.backgroundColor}
className="w-full cursor-pointer rounded-[24px] transition-opacity hover:opacity-95"
icon={
<Image
src={getAssetPath(rulePreview.iconPath)}
alt=""
width={103}
height={103}
draggable={false}
unoptimized={rulePreview.iconPath.endsWith(".svg")}
className="aspect-square size-full max-h-[103px] max-w-[103px] object-contain mix-blend-luminosity"
/>
}
/>
}
/>
</Link>
) : (
<Rule
title={rulePreview.title}
description={rulePreview.description}
expanded
fluidWidth
size="L"
templateGridFigmaShell
backgroundColor={rulePreview.backgroundColor}
className="pointer-events-none w-full select-none rounded-[24px]"
icon={
<Image
src={getAssetPath(rulePreview.iconPath)}
alt=""
width={103}
height={103}
draggable={false}
unoptimized={rulePreview.iconPath.endsWith(".svg")}
className="aspect-square size-full max-h-[103px] max-w-[103px] object-contain mix-blend-luminosity"
/>
}
/>
)}
</div>
</div>
</section>
@@ -39,6 +39,7 @@ export function RuleStackView({
subtitle={t("subtitle")}
variant="multi-line"
ruleStackDesktopTypeScale
twoColumnsFromMd={twoColumnsFromMd}
/>
{gridEntries === null ? (
@@ -28,6 +28,11 @@ export interface CommunityRuleSection {
export interface CommunityRuleProps {
sections: CommunityRuleSection[];
className?: string;
/** When true, wrap in white background with left teal bar (small breakpoint). */
/** When true, wrap in white background with left accent bar (small breakpoint). */
useCardStyle?: boolean;
/**
* Accent bar color when {@link useCardStyle} is true; should match the page
* surface behind the card (defaults to create-flow teal).
*/
cardAccentColor?: string;
}
@@ -17,17 +17,22 @@ function CommunityRuleView({
sections,
className = "",
useCardStyle = false,
cardAccentColor,
}: CommunityRuleProps) {
const accent = cardAccentColor ?? TEAL_BG;
const rootClass = useCardStyle
? `rounded-[12px] bg-white pl-3 border-l-4 ${className}`
? `rounded-[12px] bg-white pl-4 ${className}`
: className;
const rootStyle = useCardStyle ? { borderLeftColor: TEAL_BG } : undefined;
const rootStyle = useCardStyle
? {
gap: SECTION_GAP,
// Inset bar (not border) — Safari mishandles `border-left` + CSS variable accent colors.
boxShadow: `inset 4px 0 0 0 ${accent}`,
}
: { gap: SECTION_GAP };
return (
<div
className={`flex flex-col min-w-0 ${rootClass}`}
style={{ gap: SECTION_GAP, ...rootStyle }}
>
<div className={`flex flex-col min-w-0 ${rootClass}`} style={rootStyle}>
{sections.map((ruleSection, sectionIndex) => (
<Section
key={sectionIndex}
@@ -111,9 +111,9 @@ const ContentLockupContainer = memo<ContentLockupProps>(
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
title:
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] lg:text-[44px] lg:leading-[1.1] text-[var(--color-content-default-brand-primary)]",
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] tracking-[0] md:text-[44px] md:leading-[1.1] text-[var(--color-content-default-brand-primary)]",
subtitle:
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]",
"font-inter font-normal text-[18px] leading-[1.3] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]",
shape:
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
},
@@ -123,9 +123,9 @@ const ContentLockupContainer = memo<ContentLockupProps>(
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)] w-full",
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
title:
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] lg:text-[44px] lg:leading-[1.1] text-[var(--color-content-inverse-primary)]",
"font-bricolage-grotesque font-medium text-[32px] leading-[1.1] tracking-[0] md:text-[44px] md:leading-[1.1] text-[var(--color-content-inverse-primary)]",
subtitle:
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-invert-secondary)]",
"font-inter font-normal text-[18px] leading-[1.3] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-invert-secondary)]",
shape:
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
},
@@ -29,10 +29,10 @@ function HeaderLockupView({
}`}
>
{/* Title */}
<div className="flex items-center relative shrink-0 w-full">
<div className="flex w-full shrink-0 items-center">
<h1
id={titleId}
className={`flex-[1_0_0] min-h-px min-w-px overflow-hidden relative ${titleColorClass} text-ellipsis whitespace-pre-wrap ${
className={`relative w-full min-w-0 ${titleColorClass} whitespace-pre-wrap ${
isLeft ? "text-left" : "text-center"
} ${
isL
@@ -49,7 +49,7 @@ function HeaderLockupView({
!(typeof description === "string" && description.length === 0) &&
(typeof description === "string" ? (
<p
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
className={`font-inter font-normal max-w-[640px] overflow-visible relative shrink-0 ${descriptionColorClass} w-full whitespace-pre-wrap ${
isLeft ? "" : "text-center"
} ${
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
@@ -59,7 +59,7 @@ function HeaderLockupView({
</p>
) : (
<div
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
className={`font-inter font-normal max-w-[640px] overflow-visible relative shrink-0 ${descriptionColorClass} w-full whitespace-pre-wrap ${
isLeft ? "" : "text-center"
} ${
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
@@ -15,6 +15,11 @@ interface SectionHeaderProps {
* subtitle **18 / 1.3** at `lg`, **24/32** at `xl`, **left-aligned** in its column from `lg` (Figma **22085:860413**).
*/
ruleStackDesktopTypeScale?: boolean;
/**
* When true with `ruleStackDesktopTypeScale`, title and subtitle split into two columns from project **`md`** (640px),
* e.g. `/use-cases` Rule stack. Default keeps the split at **`lg`** (1024px).
*/
twoColumnsFromMd?: boolean;
}
/**
@@ -29,12 +34,18 @@ const SectionHeader = memo<SectionHeaderProps>(
variant: variantProp = "default",
stackedDesktopLines,
ruleStackDesktopTypeScale = false,
twoColumnsFromMd = false,
}) => {
const variant = variantProp;
const useStackedDesktop =
variant === "multi-line" && stackedDesktopLines != null;
const rowAlignClasses =
variant === "multi-line"
const splitFromMd =
twoColumnsFromMd &&
ruleStackDesktopTypeScale &&
variant === "multi-line";
const rowAlignClasses = splitFromMd
? "md:flex-row md:justify-between md:items-center xl:gap-[var(--spacing-scale-024)]"
: variant === "multi-line"
? "lg:flex-row lg:justify-between lg:items-center xl:gap-[var(--spacing-scale-024)]"
: "lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]";
@@ -42,11 +53,13 @@ const SectionHeader = memo<SectionHeaderProps>(
<div
className={`flex flex-col gap-[var(--spacing-scale-004)] w-full ${rowAlignClasses}`}
>
{/* Title — left column at lg+ */}
{/* Title — left column at md+ (use cases) or lg+ */}
<div
className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
? splitFromMd
? "md:w-[50%] md:h-[var(--spacing-scale-120)] md:flex md:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
: "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
: "lg:w-[369px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"
}
>
@@ -54,30 +67,38 @@ const SectionHeader = memo<SectionHeaderProps>(
className={
variant === "multi-line"
? ruleStackDesktopTypeScale
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:text-[32px] md:leading-[40px] lg:w-full lg:max-w-none lg:text-left lg:text-[32px] lg:leading-[40px] xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
? splitFromMd
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] text-[var(--color-content-default-primary)] md:w-full md:max-w-none md:text-left md:text-[32px] md:leading-[40px] xl:text-[40px] xl:leading-[52px]"
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:text-[32px] md:leading-[40px] lg:w-full lg:max-w-none lg:text-left lg:text-[32px] lg:leading-[40px] xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:text-[32px] md:leading-[40px] lg:w-[410px] lg:text-left xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
}
>
<span className="block lg:hidden">{title}</span>
<span className={splitFromMd ? "block md:hidden" : "block lg:hidden"}>
{title}
</span>
{useStackedDesktop ? (
<span className="hidden lg:block">
<span className={splitFromMd ? "hidden md:block" : "hidden lg:block"}>
<span className="block">{stackedDesktopLines[0]}</span>
<span className="block">{stackedDesktopLines[1]}</span>
<span className="block">{stackedDesktopLines[2]}</span>
</span>
) : (
<span className="hidden lg:block">{titleLg || title}</span>
<span className={splitFromMd ? "hidden md:block" : "hidden lg:block"}>
{titleLg || title}
</span>
)}
</h2>
</div>
{/* Subtitle — right column at lg+ (Figma X Large / Large / stacked small) */}
{/* Subtitle — right column at md+ (use cases) or lg+ */}
<div
className={
variant === "multi-line"
? ruleStackDesktopTypeScale
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-start lg:ml-[var(--spacing-scale-016)] xl:ml-0 xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-start"
? splitFromMd
? "md:w-[50%] md:h-[var(--spacing-scale-120)] md:flex md:items-center md:justify-start md:ml-[var(--spacing-scale-016)] xl:ml-0 xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-start"
: "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-start lg:ml-[var(--spacing-scale-016)] xl:ml-0 xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-start"
: "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end lg:ml-[var(--spacing-scale-016)] xl:ml-0 xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-end"
: "lg:w-[928px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end xl:h-[156px] xl:flex xl:items-center xl:justify-end"
}
@@ -86,7 +107,9 @@ const SectionHeader = memo<SectionHeaderProps>(
className={
variant === "multi-line"
? ruleStackDesktopTypeScale
? "font-inter font-normal text-[14px] leading-[20px] md:text-[18px] md:leading-[130%] lg:text-left lg:text-[18px] lg:leading-[130%] text-[var(--color-content-default-tertiary)] xl:text-[24px] xl:leading-[32px]"
? splitFromMd
? "font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-default-tertiary)] md:text-left md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px]"
: "font-inter font-normal text-[14px] leading-[20px] md:text-[18px] md:leading-[130%] lg:text-left lg:text-[18px] lg:leading-[130%] text-[var(--color-content-default-tertiary)] xl:text-[24px] xl:leading-[32px]"
: "font-inter font-normal text-[14px] leading-[20px] md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-default-tertiary)] lg:text-right"
: "font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"
}
@@ -6,7 +6,7 @@ import type { TripleTextBlockProps } from "./TripleTextBlock.types";
/**
* Figma: "Type / TripleTextBlock" — use cases **`lg` 22037-26994**, **`xl` 22085-860414**;
* **`md` 22085-862437**; stacked 22137:890676; lg 22128:888715; xl 22135:889705 (default).
* baseline **22112-871529**; **`md` 22085-862437**; stacked 22137:890676; lg 22128:888715; xl 22135:889705 (default).
*/
const TripleTextBlockContainer = memo<TripleTextBlockProps>((props) => {
const headingId = useId();
@@ -14,19 +14,19 @@ function columnUsesLargeBreakpointCopy(column: TripleTextBlockColumn): boolean {
function TripleTextUseCasesColumn({ column }: { column: TripleTextBlockColumn }) {
return (
<div className="flex w-full flex-col gap-[var(--spacing-scale-020)] lg:gap-0 xl:gap-[var(--spacing-scale-020)]">
<div className="flex flex-col gap-[var(--spacing-scale-008)] lg:gap-[var(--spacing-scale-004)] xl:gap-[var(--spacing-scale-008)]">
<h3 className="text-left font-bricolage-grotesque text-[24px] font-medium leading-8 text-[var(--color-content-default-primary,white)] md:text-[32px] md:leading-[1.1] lg:text-[18px] lg:leading-[var(--spacing-scale-022)] xl:text-[32px] xl:leading-[1.1]">
{column.title}
</h3>
<div className="flex flex-col gap-[var(--spacing-scale-024)] font-inter text-[16px] font-normal leading-6 text-[var(--color-content-default-secondary)] md:text-[24px] md:leading-8 lg:gap-[var(--spacing-scale-020)] lg:text-[14px] lg:leading-5 xl:gap-[var(--spacing-scale-032)] xl:text-[24px] xl:leading-8">
<p>{column.description}</p>
{column.descriptionSecondary ? (
<p>{column.descriptionSecondary}</p>
) : null}
</div>
<article className="flex w-full flex-col gap-[var(--spacing-scale-006)] md:gap-[var(--spacing-scale-008)] lg:gap-[var(--spacing-scale-004)] xl:gap-[var(--spacing-scale-008)]">
<h3 className="text-left font-bricolage-grotesque text-[24px] font-medium leading-8 text-[var(--color-content-default-primary,white)] md:text-[32px] md:leading-[1.1] lg:text-[18px] lg:leading-[var(--spacing-scale-022)] xl:text-[32px] xl:leading-[1.1]">
{column.title}
</h3>
<div className="flex flex-col font-inter text-[16px] font-normal leading-6 text-[var(--color-content-default-secondary)] md:text-[24px] md:leading-8 lg:text-[14px] lg:leading-5 xl:text-[24px] xl:leading-8">
<p>{column.description}</p>
{column.descriptionSecondary ? (
<p className="mt-[var(--spacing-scale-024)] md:mt-[var(--spacing-scale-032)] lg:mt-0 lg:pt-[var(--spacing-scale-020)] xl:mt-[var(--spacing-scale-032)]">
{column.descriptionSecondary}
</p>
) : null}
</div>
</div>
</article>
);
}
@@ -82,7 +82,7 @@ function TripleTextBlockColumnLockup({
* Section horizontal padding adds **+ Scale/096** below `xl` (outer frame inset); **use cases `xl`** uses **Scale/160** only ([22085:860414](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-860414&m=dev)).
*
* Figma: use cases **`lg`** [22037:26994](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22037-26994&m=dev);
* **`md`** [22085:862437](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-862437&m=dev); stacked **22137:890676**;
* baseline **22112:871529** / **22085:860366**; **`md`** [22085:862437](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22085-862437&m=dev); stacked **22137:890676**;
* lg 3-col **22128:888715**; xl **22135:889705** (default preset).
*/
function TripleTextBlockView({
@@ -102,11 +102,11 @@ function TripleTextBlockView({
<section
{...(isUseCases ? { "data-figma-node": "22085-860414" } : {})}
aria-labelledby={hasSectionTitle ? headingId : undefined}
className={`bg-black px-[calc(var(--spacing-scale-032)+var(--spacing-scale-096))] py-[var(--spacing-scale-064)] md:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] md:py-[var(--spacing-scale-064)] lg:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] lg:py-[var(--spacing-scale-064)] ${
className={`bg-black py-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-064)] ${
isUseCases
? "xl:px-[var(--spacing-scale-160)]"
: "xl:px-[calc(var(--spacing-scale-160)+var(--spacing-scale-096))]"
} xl:py-[var(--spacing-scale-064)] ${className}`.trim()}
? "px-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-096)] lg:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] xl:px-[var(--spacing-scale-160)]"
: "px-[calc(var(--spacing-scale-032)+var(--spacing-scale-096))] md:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] lg:px-[calc(var(--spacing-scale-096)+var(--spacing-scale-096))] xl:px-[calc(var(--spacing-scale-160)+var(--spacing-scale-096))]"
} ${className}`.trim()}
>
<div
className={
+38
View File
@@ -609,6 +609,44 @@ export async function duplicatePublishedRule(
}
}
export async function duplicateUseCaseTemplate(
slug: string,
): Promise<DuplicateRuleResult> {
try {
const res = await fetch(
`/api/use-cases/${encodeURIComponent(slug)}/duplicate`,
{
method: "POST",
credentials: "include",
},
);
const data = (await safeParseJsonResponse(res)) as {
rule?: { id: string; title: string };
} | null;
const rule = data && typeof data === "object" ? data.rule : undefined;
if (!res.ok || !rule) {
const fromBody =
data && typeof data === "object" ? readApiErrorMessage(data) : null;
const msg =
fromBody && fromBody !== "Request failed"
? fromBody
: PUBLISH_FAILED_FALLBACK;
return {
ok: false as const,
error: msg,
status: res.status,
};
}
return { ok: true, id: rule.id, title: rule.title };
} catch {
return {
ok: false as const,
error: DRAFT_SAVE_NETWORK_ERROR,
status: 0,
};
}
}
export type DeleteAccountResult = { ok: true } | { ok: false; error: string };
/**
+9 -1
View File
@@ -352,5 +352,13 @@ export function parseDocumentSectionsForDisplay(
if (!document || typeof document !== "object") return [];
const sections = (document as Record<string, unknown>).sections;
if (!Array.isArray(sections)) return [];
return sections.filter(isDocumentSection);
return sections
.filter(isDocumentSection)
.map((section) => ({
...section,
entries: section.entries.map((entry) => ({
...entry,
body: typeof entry.body === "string" ? entry.body : "",
})),
}));
}
+9 -1
View File
@@ -19,11 +19,19 @@ export function isDocumentEntry(x: unknown): x is CommunityRuleEntry {
if (typeof o.title !== "string" || o.title.trim().length === 0) {
return false;
}
if (typeof o.body !== "string") return false;
if (o.blocks !== undefined) {
if (!Array.isArray(o.blocks) || !o.blocks.every(isLabeledBlock)) {
return false;
}
}
const blocks = Array.isArray(o.blocks) ? o.blocks : [];
const hasBlocks = blocks.length > 0;
if (hasBlocks) {
if (o.body !== undefined && typeof o.body !== "string") {
return false;
}
return true;
}
if (typeof o.body !== "string") return false;
return true;
}
+21
View File
@@ -170,6 +170,27 @@ export function coreValuePresetFor(chipId: string): CoreValueDetailEntry {
};
}
/**
* Preset chip id for a core value label (`"1"` … `"n"` from
* `CoreValuesSelectScreen`), or `null` when the label is bespoke.
*/
export function resolveCoreValueChipIdFromLabel(label: string): string | null {
const t = label.trim();
if (!t) return null;
const values = (coreValuesMessages as { values?: unknown }).values;
if (!Array.isArray(values)) return null;
for (let i = 0; i < values.length; i++) {
const row = values[i];
if (typeof row === "string" && row.trim() === t) return String(i + 1);
if (!row || typeof row !== "object") continue;
const o = row as Record<string, unknown>;
if (typeof o.label === "string" && o.label.trim() === t) {
return String(i + 1);
}
}
return null;
}
/** Match `coreValues.json` row by trimmed label (custom chip id / drift fallbacks). */
export function coreValuePresetForLabel(label: string): CoreValueDetailEntry {
const t = label.trim();
@@ -0,0 +1,331 @@
import type { CommunityRuleSection } from "../../app/components/type/CommunityRule/CommunityRule.types";
import type { CommunityRuleLabeledBlock } from "../../app/components/type/CommunityRule/CommunityRule.types";
import type { PublishedMethodSelections } from "./buildPublishPayload";
import { parseDocumentSectionsForDisplay } from "./buildPublishPayload";
import { resolveMethodPresetIdFromLabel } from "./buildFinalReviewCategories";
import { resolveCoreValueChipIdFromLabel } from "./finalReviewChipPresets";
import {
communicationPresetFor,
conflictManagementPresetFor,
decisionApproachPresetFor,
membershipPresetFor,
mergeCoreValueDetailWithPresets,
} from "./finalReviewChipPresets";
import { templateCategoryToGroupKey } from "./templateReviewMapping";
import type { TemplateFacetGroupKey } from "./templateReviewMapping";
import { RULE_SECTION_CATEGORY } from "./ruleSectionsFromMethodSelections";
const COMM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle & Scope",
logisticsAdmin: "Logistics, Admin & Norms",
codeOfConduct: "Code of Conduct",
};
const MEM_LABELS: Record<string, string> = {
eligibility: "Eligibility & Philosophy",
joiningProcess: "Joining Process",
expectations: "Expectations & Removal",
};
const DEC_LABELS: Record<string, string> = {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
};
const CM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
processProtocol: "Process Protocol",
restorationFallbacks: "Restoration & Fallbacks",
};
const LABELS_BY_GROUP: Record<
Exclude<TemplateFacetGroupKey, "coreValues">,
Record<string, string>
> = {
communication: COMM_LABELS,
membership: MEM_LABELS,
decisionApproaches: DEC_LABELS,
conflictManagement: CM_LABELS,
};
function slugifyId(label: string): string {
const base = label
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return base.length > 0 ? base : "custom-method";
}
function keyForLabel(
label: string,
labelByKey: Record<string, string>,
): string | null {
const trimmed = label.trim();
for (const [key, displayLabel] of Object.entries(labelByKey)) {
if (displayLabel === trimmed) return key;
}
return null;
}
function parseConsensusPercent(body: string): number | null {
const m = body.trim().match(/^(\d+)\s*%?$/);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) ? n : null;
}
function sectionsRecordFromBlocks(
blocks: CommunityRuleLabeledBlock[],
labelByKey: Record<string, string>,
options?: { consensusLevelKey?: string },
): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const block of blocks) {
const key = keyForLabel(block.label, labelByKey);
if (!key) continue;
const body = block.body.trim();
if (options?.consensusLevelKey === key) {
const pct = parseConsensusPercent(body);
if (pct !== null) out[key] = pct;
continue;
}
if (key === "applicableScope" || key === "selectedApplicableScope") {
const parts = body
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
if (parts.length > 0) {
out.selectedApplicableScope = parts;
out.applicableScope = parts;
}
continue;
}
if (body.length > 0) out[key] = body;
}
return out;
}
function presetForMethod(
groupKey: Exclude<TemplateFacetGroupKey, "coreValues">,
id: string,
): Record<string, unknown> {
switch (groupKey) {
case "communication":
return { ...communicationPresetFor(id) } as Record<string, unknown>;
case "membership":
return { ...membershipPresetFor(id) } as Record<string, unknown>;
case "decisionApproaches":
return { ...decisionApproachPresetFor(id) } as Record<string, unknown>;
case "conflictManagement":
return { ...conflictManagementPresetFor(id) } as Record<string, unknown>;
}
}
function sectionsRecordFromEntry(
entry: CommunityRuleSection["entries"][number],
groupKey: Exclude<TemplateFacetGroupKey, "coreValues">,
presetId: string,
): Record<string, unknown> {
const labelByKey = LABELS_BY_GROUP[groupKey];
const consensusKey =
groupKey === "decisionApproaches" ? "consensusLevel" : undefined;
if (entry.blocks && entry.blocks.length > 0) {
const fromBlocks = sectionsRecordFromBlocks(entry.blocks, labelByKey, {
consensusLevelKey: consensusKey,
});
if (Object.keys(fromBlocks).length > 0) {
return { ...presetForMethod(groupKey, presetId), ...fromBlocks };
}
}
const body = (entry.body ?? "").trim();
if (body.length === 0) {
return presetForMethod(groupKey, presetId);
}
return { ...presetForMethod(groupKey, presetId), corePrinciple: body };
}
function coreValuesFromValuesSection(
section: CommunityRuleSection,
): Array<{ chipId: string; label: string; meaning: string; signals: string }> {
const out: Array<{
chipId: string;
label: string;
meaning: string;
signals: string;
}> = [];
for (const entry of section.entries) {
const label = entry.title.trim();
if (!label) continue;
const body = (entry.body ?? "").trim();
const parts = body.length > 0 ? body.split(/\n\n+/) : [];
const meaning = (parts[0] ?? "").trim();
const signals = parts.slice(1).join("\n\n").trim();
const merged = mergeCoreValueDetailWithPresets("", label, {
meaning,
signals,
});
const chipId =
resolveCoreValueChipIdFromLabel(label) ??
`hydrated-${label.toLowerCase()}`;
out.push({
chipId,
label,
meaning: merged.meaning,
signals: merged.signals,
});
}
return out;
}
type PublishedMethodRow = {
id: string;
label: string;
sections: Record<string, unknown>;
};
function methodSelectionsFromDisplaySections(
sections: CommunityRuleSection[],
): PublishedMethodSelections {
const out: PublishedMethodSelections = {};
const pushGroup = (
key: keyof PublishedMethodSelections,
groupKey: Exclude<TemplateFacetGroupKey, "coreValues">,
section: CommunityRuleSection,
) => {
const rows: PublishedMethodRow[] = [];
for (const entry of section.entries) {
const label = entry.title.trim();
if (!label) continue;
const id =
resolveMethodPresetIdFromLabel(label, groupKey) ??
`custom-${slugifyId(label)}`;
rows.push({
id,
label,
sections: sectionsRecordFromEntry(entry, groupKey, id),
});
}
if (rows.length > 0) {
switch (key) {
case "communication":
out.communication = rows as NonNullable<
PublishedMethodSelections["communication"]
>;
break;
case "membership":
out.membership = rows as NonNullable<
PublishedMethodSelections["membership"]
>;
break;
case "decisionApproaches":
out.decisionApproaches = rows as NonNullable<
PublishedMethodSelections["decisionApproaches"]
>;
break;
case "conflictManagement":
out.conflictManagement = rows as NonNullable<
PublishedMethodSelections["conflictManagement"]
>;
break;
default:
break;
}
}
};
for (const section of sections) {
const groupKey = templateCategoryToGroupKey(section.categoryName);
if (!groupKey || groupKey === "coreValues") continue;
switch (groupKey) {
case "communication":
pushGroup("communication", groupKey, section);
break;
case "membership":
pushGroup("membership", groupKey, section);
break;
case "decisionApproaches":
pushGroup("decisionApproaches", groupKey, section);
break;
case "conflictManagement":
pushGroup("conflictManagement", groupKey, section);
break;
default:
break;
}
}
return out;
}
function hasMethodSelections(ms: PublishedMethodSelections): boolean {
return Boolean(
ms.communication?.length ||
ms.membership?.length ||
ms.decisionApproaches?.length ||
ms.conflictManagement?.length,
);
}
/**
* Ensures a stored published `document` includes `methodSelections` and
* `coreValues` derived from display `sections` when missing (e.g. use-case
* template duplicates). Idempotent when the document is already normalized.
*/
export function normalizePublishedDocumentForEdit(
document: unknown,
): Record<string, unknown> {
if (!document || typeof document !== "object" || Array.isArray(document)) {
return {};
}
const doc = { ...(document as Record<string, unknown>) };
const sections = parseDocumentSectionsForDisplay(doc);
const existingMs = doc.methodSelections;
const hasMs =
existingMs &&
typeof existingMs === "object" &&
!Array.isArray(existingMs) &&
hasMethodSelections(existingMs as PublishedMethodSelections);
const existingCv = doc.coreValues;
const hasCv = Array.isArray(existingCv) && existingCv.length > 0;
if (!hasCv) {
const valuesSection = sections.find(
(s) =>
s.categoryName === RULE_SECTION_CATEGORY.values ||
templateCategoryToGroupKey(s.categoryName) === "coreValues",
);
if (valuesSection) {
const coreValues = coreValuesFromValuesSection(valuesSection);
if (coreValues.length > 0) {
doc.coreValues = coreValues;
}
}
}
if (!hasMs && sections.length > 0) {
const methodSelections = methodSelectionsFromDisplaySections(sections);
if (hasMethodSelections(methodSelections)) {
doc.methodSelections = methodSelections;
}
}
if (!Array.isArray(doc.sections) || doc.sections.length === 0) {
doc.sections = sections;
}
return doc;
}
@@ -8,6 +8,7 @@ import {
} from "./customRuleFacets";
import type { PublishedMethodSelections } from "./buildPublishPayload";
import type { StoredLastPublishedRule } from "./lastPublishedRule";
import { normalizePublishedDocumentForEdit } from "./normalizePublishedDocumentForEdit";
import { methodLabelFor } from "./finalReviewChipPresets";
import type { TemplateFacetGroupKey } from "./templateReviewMapping";
@@ -120,7 +121,9 @@ export function methodSectionsPinsFromPublishedHydratePatch(
export function createFlowStateFromPublishedRule(
rule: StoredLastPublishedRule,
): Partial<CreateFlowState> {
const doc = rule.document;
const doc = normalizePublishedDocumentForEdit(
rule.document,
) as StoredLastPublishedRule["document"];
const out: Partial<CreateFlowState> = {
title: rule.title,
editingPublishedRuleId: rule.id,
+15
View File
@@ -0,0 +1,15 @@
/**
* Routes that render product chrome only (`CreateFlowTopNav`), not marketing `Top`.
* Keep in sync with `ConditionalNavigationClient`.
*/
export function isChromelessNavigationPath(
pathname: string | null | undefined,
): boolean {
if (!pathname) {
return false;
}
if (pathname.startsWith("/create") || pathname === "/login") {
return true;
}
return /^\/use-cases\/[^/]+\/rule\/?$/.test(pathname);
}
+52
View File
@@ -0,0 +1,52 @@
import type { CommunityRuleSection } from "../app/components/type/CommunityRule/CommunityRule.types";
import { parsePublishedDocumentForCommunityRuleDisplay } from "./create/publishedDocumentToDisplaySections";
import type useCasesCompletedRules from "../messages/en/pages/useCasesCompletedRules.json";
import {
isUseCaseDetailSlug,
useCaseContentKeyForSlug,
type UseCaseDetailSlug,
} from "./useCaseSyntheticPost";
export type UseCasesCompletedRulesMessages = typeof useCasesCompletedRules;
export type UseCaseCompletedRuleFixture =
UseCasesCompletedRulesMessages[keyof UseCasesCompletedRulesMessages];
export function getUseCaseCompletedRuleFixture(
slug: UseCaseDetailSlug,
completedRules: UseCasesCompletedRulesMessages,
): UseCaseCompletedRuleFixture {
const contentKey = useCaseContentKeyForSlug(slug);
return completedRules[contentKey];
}
export function buildUseCaseCompletedRuleSections(
fixture: UseCaseCompletedRuleFixture,
): CommunityRuleSection[] {
return parsePublishedDocumentForCommunityRuleDisplay(fixture.document);
}
export function resolveUseCaseCompletedRule(
slug: string,
completedRules: UseCasesCompletedRulesMessages,
):
| {
slug: UseCaseDetailSlug;
fixture: UseCaseCompletedRuleFixture;
sections: CommunityRuleSection[];
}
| null {
if (!isUseCaseDetailSlug(slug)) {
return null;
}
const fixture = getUseCaseCompletedRuleFixture(slug, completedRules);
const sections = buildUseCaseCompletedRuleSections(fixture);
if (sections.length === 0) {
return null;
}
return {
slug,
fixture,
sections,
};
}
+7
View File
@@ -0,0 +1,7 @@
/** Title for a rule duplicated from a use-case completed demo (profile list). */
export function useCaseTemplateDuplicateTitle(sourceTitle: string): string {
const trimmed = sourceTitle.trim();
return trimmed.length > 0
? `${trimmed} Template (Copy)`
: "Community Rule Template (Copy)";
}
@@ -15,6 +15,11 @@
"emptyHint": "Add a community description",
"ariaEditDescription": "Edit community description"
},
"titleEditModal": {
"title": "Community name",
"description": "Update the name shown on your public CommunityRule.",
"ariaEditTitle": "Edit community name"
},
"categories": [
{
"name": "Values",
+2
View File
@@ -11,6 +11,8 @@
"exportAriaLabel": "Export",
"editAriaLabel": "Edit",
"manageStakeholdersAriaLabel": "Manage Stakeholders",
"moreOptionsAriaLabel": "More options",
"actionsMenuAriaLabel": "Create flow actions",
"leaveConfirmLoss": "Leave create flow? Your progress will be lost.",
"draftSaveBannerTitle": "Couldn't save draft",
"postLoginSaveFailedWithReason": "Could not save your draft to your account. Your progress is still stored on this device.\n\n{reason}"
+4
View File
@@ -18,6 +18,8 @@ import learn from "./pages/learn.json";
import about from "./pages/about.json";
import useCases from "./pages/useCases.json";
import useCasesDetail from "./pages/useCasesDetail.json";
import useCasesCompletedRules from "./pages/useCasesCompletedRules.json";
import useCasesCompletedRule from "./pages/useCasesCompletedRule.json";
import howItWorks from "./pages/howItWorks.json";
import monitor from "./pages/monitor.json";
import login from "./pages/login.json";
@@ -83,6 +85,8 @@ export default {
about,
useCases,
useCasesDetail,
useCasesCompletedRules,
useCasesCompletedRule,
howItWorks,
monitor,
login,
+17
View File
@@ -37,6 +37,23 @@
"keywords": ["street medics", "use case", "community governance", "operating manual"]
}
},
"useCasesCompletedRule": {
"mutualAidColorado": {
"title": "Mutual Aid Colorado community rule — CommunityRule",
"description": "Read the completed community rule Mutual Aid Colorado built with CommunityRule.",
"keywords": ["mutual aid", "community rule", "operating manual", "use case"]
},
"foodNotBombs": {
"title": "Food Not Bombs Boulder community rule — CommunityRule",
"description": "Read the completed community rule Food Not Bombs Boulder built with CommunityRule.",
"keywords": ["food not bombs", "community rule", "operating manual", "use case"]
},
"boulderCountyStreetMedics": {
"title": "Boulder County Street Medics community rule — CommunityRule",
"description": "Read the completed community rule Boulder County Street Medics built with CommunityRule.",
"keywords": ["street medics", "community rule", "operating manual", "use case"]
}
},
"howItWorks": {
"title": "A Guide to CommunityRule — CommunityRule",
"description": "CommunityRule is a modular governance toolkit designed to help democratic groups build, customize, and publish their own Operating Manual.",
@@ -0,0 +1,15 @@
{
"topNav": {
"duplicate": "Duplicate",
"return": "Return",
"duplicateAriaLabel": "Save a copy of this community rule to your profile",
"duplicateFailedTitle": "Could not duplicate",
"duplicateNotFoundDescription": "This use case template is no longer available.",
"returnAriaLabel": "Return to use case",
"shareLinkCopiedTitle": "Link copied",
"shareLinkCopiedDescription": "The link to this community rule is on your clipboard.",
"shareCopyFailedTitle": "Could not copy link",
"shareCopyFailedDescription": "Copy the URL from your browser address bar instead."
},
"ruleCardLinkAriaLabel": "View {title} community rule"
}
@@ -0,0 +1,644 @@
{
"mutualAidColorado": {
"pageBackground": "var(--color-surface-invert-brand-lavender)",
"title": "Mutual Aid Colorado",
"summary": "Mutual Aid Colorado is a statewide network that empowers frontline community efforts by connecting independent mutual aid groups and building shared logistical infrastructure like supply chains and print shops.",
"document": {
"sections": [
{
"categoryName": "Values",
"entries": [
{
"title": "Mutual Aid, Not Charity",
"body": "We act in political solidarity rather than charity. We are building systems of community care and resilience, recognizing that our liberation is bound together.\n\n“Change needs all of us.”\n\nFood Not Bombs is not a charity. It is a project of solidarity. Charity is vertical. It moves from those who have to those who have not and maintains the hierarchy between them. Solidarity is horizontal. It moves between equals who recognize that our liberation is bound together. We do not help the poor. We share resources among community members because access to food is a human right rather than a privilege of wealth."
},
{
"title": "Local Autonomy",
"body": "We fiercely protect the autonomy of individual local chapters. We exist to support and connect local work, not to dictate it, ensuring we do not transform into the very top-down bureaucracies we aim to subvert.\n\n“Everyone contributes, no one controls.”\n\nWe operate without bosses or managers. This does not mean we are disorganized. It means we are self-organized. Authority in this chapter is temporary and task-specific rather than permanent or personal. We believe the people doing the work should make the decisions about that work. By distributing responsibility we prevent burnout and ensure the movement survives beyond any single leader."
},
{
"title": "Action Over Bureaucracy",
"body": "We prioritize moving physical goods and supporting local groups efficiently over unnecessary administration. We use our explicit agreements to prevent operational bottlenecks and keep the work moving.\n\n“Relationships over algorithms.”\n\nWe use digital tools to coordinate but we build power in the physical world. An algorithm cannot cook a meal and a group chat cannot look someone in the eye. We prioritize face-to-face interaction and physical presence at distributions. We resist the temptation to let digital metrics replace tangible impact."
},
{
"title": "Durable Commons",
"body": "We explicitly document our resource sharing protocols and logistical agreements. This ensures that knowledge regarding supply routes and shared assets is accessible to any participating group, functioning as a true commons rather than being hoarded by a few central organizers.\n\n“Rescued food doesn't add carbon.”\n\nOur logistics are an ecological intervention. Food waste is a major driver of climate change. By intercepting food that would otherwise be discarded and redirecting it to hungry neighbors we close the loop. We view food recovery as stewarding a resource that the industrial food system has abandoned. Every pound of food we rescue is a pound of carbon kept out of the atmosphere."
},
{
"title": "Decentralized Power",
"body": "We are committed to building massive material power without centralizing authority. We engineer our structures to distribute responsibility and prevent the consolidation of control within the network.\n\n“Collective giving builds collective power.”\n\nOur budget is labor rather than capital. We rely on the time and care of our volunteers rather than large grants or corporate sponsorships. This independence gives us the political freedom to operate according to our values. When we pool our small individual capacities we create a collective power that money cannot buy."
}
]
},
{
"categoryName": "Membership",
"entries": [
{
"title": "Membership Agreement or Pledge",
"blocks": [
{
"label": "Eligibility & Philosophy",
"body": "We are a small, experimental group building civic infrastructure to connect mutual aid groups across Colorado. Access to shared physical infrastructure (like the print shop and supply vehicles) comes with the responsibility of stewardship."
},
{
"label": "Joining Process",
"body": "Participating groups and core organizers gain access by agreeing to follow the modular vocabulary of agreements we have designed."
},
{
"label": "Expectations & Removal",
"body": "Members are expected to actively steward and maintain these shared assets. This ensures that the equipment remains in good working order for the next autonomous group that needs it."
}
]
},
{
"title": "Contribution Based",
"blocks": [
{
"label": "Eligibility & Philosophy",
"body": "Roles are tiered between Volunteers and Core Members. Core Membership is not a status symbol, it grants access privileges (keys, door codes, independent pickups, internal Signal chats) required to facilitate logistics."
},
{
"label": "Joining Process",
"body": "Local groups and regional hubs become part of the network by actively participating in the supply chains, contributing labor to the print shops, or sharing their logistical resources."
},
{
"label": "Expectations & Removal",
"body": "Membership is inherently tied to active contribution. Maintaining standing requires continuous participation in the statewide infrastructure and shared operations."
}
]
}
]
},
{
"categoryName": "Decision-Making",
"entries": [
{
"title": "Consensus Decision-Making",
"blocks": [
{
"label": "Core Principle",
"body": "We make the big decisions together. For strategic changes, the core organizing team takes the time to have deep conversations and build genuine agreement. This ensures our infrastructure is guided by our collective values, rather than just being driven by whoever speaks the loudest."
},
{
"label": "Applicable Scope",
"body": "Strategic decisions, shared network values, and organization-wide policy changes."
},
{
"label": "Consensus Level",
"body": "100%"
},
{
"label": "Step-by-Step Instructions",
"body": "Core members openly discuss strategic issues and collaboratively shape proposals. Once a proposal is drafted, it is presented to the full core organizing team. We deliberate, address concerns, and refine the proposal together until every core member can consent to its adoption."
},
{
"label": "Objections & Deadlocks",
"body": "If a core member objects to a proposal, the group must make a good faith effort to resolve their concerns and modify the plan. However, we do not let the perfect be the enemy of the good. If multiple good faith attempts to find consensus fail, and a decision must be made to keep the infrastructure functioning, we trigger a fallback mechanism: the proposal can be passed with a 75% supermajority vote of the core organizing team."
}
]
},
{
"title": "Delegated Do-ocracy",
"blocks": [
{
"label": "Core Principle",
"body": "To manage a statewide logistics operation without bottlenecks, authority must be distributed. By formalizing structural relationships and empowering the volunteers running the shared infrastructure, we ensure the network can move quickly and resiliently."
},
{
"label": "Applicable Scope",
"body": "Statewide logistics, specialized operational domains, and day-to-day physical infrastructure (e.g., operating the print shop, driving supply routes, managing the warehouse floor)."
},
{
"label": "Step-by-Step Instructions",
"body": "We explicitly map our arenas of operation so specialized working groups have full authority over their specific domains. Within these domains, individual volunteers are empowered to make immediate operational choices (like organizing inventory or adjusting a supply route) to keep goods moving efficiently, without waiting for a statewide assembly's approval."
},
{
"label": "Objections & Deadlocks",
"body": "Authority has limits based on impact. If an individual's logistical experiment is irreversible, highly costly, or could permanently alter the shared infrastructure, they must pause and seek guidance from the relevant Working Group. If a Working Group's decision will have a material, network-wide impact or disrupt another team, they must coordinate with the affected groups or escalate to a broader coordination body."
}
]
}
]
},
{
"categoryName": "Conflict Management",
"entries": [
{
"title": "Peer Mediation",
"blocks": [
{
"label": "Core Principle",
"body": "Conflict and miscommunication are inevitable when coordinating logistics across a large region. The goal is to manage behavior and communication so the supply chain flows smoothly and infrastructure remains functional, without requiring everyone to be close friends."
},
{
"label": "Applicable Scope",
"body": "Day-to-day friction, miscommunications, or minor interpersonal disputes over operational tasks (e.g., scheduling conflicts, route overlaps, or minor workflow disagreements)."
},
{
"label": "Process Protocol",
"body": "We handle friction at the lowest possible level to prevent disrupting the supply chain.\n\nLevel 1: Direct Engagement between the organizers involved to clarify misunderstandings quickly.\n\nLevel 2: Supported Conversation with neutral witnesses acting purely to keep the conversation calm, clear, and focused on operational solutions."
},
{
"label": "Restoration & Fallbacks",
"body": "If the friction cannot be resolved at these lower levels and begins to threaten operational continuity, it escalates to Level 3 (Formal Facilitation by a designated Conflict Management Working Group)."
}
]
},
{
"title": "Conflict Resolution Council",
"blocks": [
{
"label": "Core Principle",
"body": "The Conflict Management Working Group acts as facilitators rather than judges. They prioritize the integrity of the shared infrastructure and the sustainable capacity of the volunteer base."
},
{
"label": "Applicable Scope",
"body": "Level 3 conflicts, severe organizer burnout, and the misuse of shared regional assets."
},
{
"label": "Process Protocol",
"body": "If a conflict reaches Level 3, the working group investigates the behavioral facts and how they are impacting operations. They draft a boundary plan and present a factual, redacted summary to the rest of the core organizers for collective ratification.\n\nCapacity & Burnout Clause: Operating statewide infrastructure can lead to severe exhaustion. The working group can mandate a temporary leave of absence for a core organizer as a mechanism of care to address burnout, preventing them from dropping the ball on critical logistics or becoming overwhelmed."
},
{
"label": "Restoration & Fallbacks",
"body": "Misuse of shared statewide assets for personal gain, hoarding supplies, or intentional sabotage of the supply chain bypasses the escalation ladder entirely. These severe breaches of trust trigger an Immediate Precautionary Suspension, temporarily restricting the organizer's access to warehouses, vehicles, and coordination channels while a formal investigation takes place."
}
]
}
]
},
{
"categoryName": "Communication",
"entries": [
{
"title": "Signal",
"blocks": [
{
"label": "Core Principle & Scope",
"body": "All communication for Mutual Aid Colorado happens on Signal. Protecting our network and local chapters is vital."
},
{
"label": "Logistics, Admin & Norms",
"body": "We maintain highly structured channels to keep our digital workspace organized. A designated Announcements channel serves as our persistent library for documenting explicit architecture, resource-sharing protocols, and agreements. Day-to-day management of regional supply chains (moving physical goods, coordinating print shop shifts, handling logistical emergencies) happens in specific, encrypted group chats or shared platforms."
},
{
"label": "Code of Conduct",
"body": "Tactical channels must be kept absolutely clear of policy debates or social chatter. This ensures drivers and logistics volunteers can quickly access the real-time information they need to keep people safe and supplied."
}
]
}
]
}
]
}
},
"foodNotBombs": {
"pageBackground": "var(--color-surface-invert-secondary)",
"title": "Food Not Bombs Boulder",
"summary": "Food Not Bombs Boulder is a mutual aid collective that recovers surplus food to share free, public meals with the community, protesting war and poverty while advocating for food as a fundamental human right.",
"document": {
"sections": [
{
"categoryName": "Values",
"entries": [
{
"title": "Solidarity Forever",
"body": "“Change needs all of us.”\n\nFood Not Bombs is not a charity. It is a project of solidarity. Charity is vertical. It moves from those who have to those who have not and maintains the hierarchy between them. Solidarity is horizontal. It moves between equals who recognize that our liberation is bound together. We do not help the poor. We share resources among community members because access to food is a human right rather than a privilege of wealth."
},
{
"title": "Shared Leadership",
"body": "We operate without bosses or managers. This does not mean we are disorganized. It means we are self-organized. Authority in this chapter is temporary and task-specific rather than permanent or personal. We believe the people doing the work should make the decisions about that work. By distributing responsibility we prevent burnout and ensure the movement survives beyond any single leader.\n\n“Everyone contributes, no one controls.”\n\nWe operate without bosses or managers. This does not mean we are disorganized. It means we are self-organized. Authority in this chapter is temporary and task-specific rather than permanent or personal. We believe the people doing the work should make the decisions about that work. By distributing responsibility we prevent burnout and ensure the movement survives beyond any single leader."
},
{
"title": "Organizing Offline",
"body": "We use digital tools to coordinate but we build power in the physical world. An algorithm cannot cook a meal and a group chat cannot look someone in the eye. We prioritize face-to-face interaction and physical presence at distributions. We resist the temptation to let digital metrics replace tangible impact.\n\n“Relationships over algorithms.”\n\nWe use digital tools to coordinate but we build power in the physical world. An algorithm cannot cook a meal and a group chat cannot look someone in the eye. We prioritize face-to-face interaction and physical presence at distributions. We resist the temptation to let digital metrics replace tangible impact."
},
{
"title": "Circular Food Systems",
"body": "Our logistics are an ecological intervention. Food waste is a major driver of climate change. By intercepting food that would otherwise be discarded and redirecting it to hungry neighbors we close the loop. We view food recovery as stewarding a resource that the industrial food system has abandoned. Every pound of food we rescue is a pound of carbon kept out of the atmosphere.\n\n“Rescued food doesn't add carbon.”\n\nOur logistics are an ecological intervention. Food waste is a major driver of climate change. By intercepting food that would otherwise be discarded and redirecting it to hungry neighbors we close the loop. We view food recovery as stewarding a resource that the industrial food system has abandoned. Every pound of food we rescue is a pound of carbon kept out of the atmosphere."
},
{
"title": "Powered by People",
"body": "“Collective giving builds collective power.”\n\nOur budget is labor rather than capital. We rely on the time and care of our volunteers rather than large grants or corporate sponsorships. This independence gives us the political freedom to operate according to our values. When we pool our small individual capacities we create a collective power that money cannot buy."
}
]
},
{
"categoryName": "Membership",
"entries": [
{
"title": "Consensus or Vote-Based Approval",
"blocks": [
{
"label": "Eligibility & Philosophy",
"body": "Access to critical resources is restricted to safeguard the project. Safety and accountability are prioritized."
},
{
"label": "Joining Process",
"body": "Volunteers who have completed two full Saturday distributions can submit a request via the Stewardship & Process Working Group form. The request is subject to a 15-day lazy consensus period. If an existing Core Member raises a concern, the volunteer must address it. If the objection stands, the volunteer can reapply after 15 days."
},
{
"label": "Expectations & Removal",
"body": "Any single Core Member may immediately ask a participant to leave for physical safety threats. For non-emergent issues:\n\nVolunteers: Exclusion requires agreement from at least five Core Members or a ratified conflict management plan.\n\nCore Members: Revoking membership requires a 3/4 supermajority vote of the full Core Membership, excluding the member in question."
}
]
},
{
"title": "Weighted or Tiered Membership",
"blocks": [
{
"label": "Eligibility & Philosophy",
"body": "Roles are tiered between Volunteers and Core Members. Core Membership is not a status symbol, it grants access privileges (keys, door codes, independent pickups, internal Signal chats) required to facilitate logistics."
},
{
"label": "Joining Process",
"body": "Volunteers advance to Core Membership through sustained contribution. Once a Core Member, individuals can sign up to \"bottom-line\" (take responsibility for) shifts via the roll call channel in the week leading up to a distribution."
},
{
"label": "Expectations & Removal",
"body": "Labor Requirements: Core Members must complete at least one shift, pickup, or two hours of labor (distribution, cleaning, admin, or working group participation) every month to maintain standing.\n\nShift Coverage & Bottom-Lining: Ensuring coverage is a collective responsibility; members must proactively monitor the schedule for gaps. Bottom-liners (experienced Core Members) must ensure all operational checklists are completed.\n\nHand-off Protocol: The last Core Member on-site must not leave until a new bottom-liner is confirmed or all checklist items are complete. If leaving early, they must ping the chat immediately to request coverage.\n\nLoss of Standing: The Stewardship & Process Working Group posts potentially inactive members to the chat. If an individual's activity is not verified, they revert to volunteer status and must restart the entry process."
}
]
}
]
},
{
"categoryName": "Decision-Making",
"entries": [
{
"title": "Lazy Consensus",
"blocks": [
{
"label": "Core Principle",
"body": "Consensus is assumed if no specific concerns are raised within a set timeframe."
},
{
"label": "Applicable Scope",
"body": "Decisions that cannot be easily reverted, Amendments to the governance manuial"
},
{
"label": "Consensus Level",
"body": "100%"
},
{
"label": "Step-by-Step Instructions",
"body": "Any Core Member can propose an amendment or global decision by posting it to the Core Member chat. If no concern is raised within 15 days, it is adopted. To protect shared time, facilitators strictly enforce this model during meetings, redirecting complex policy debates to async written deliberation or authorized ad-hoc breakout meetings."
},
{
"label": "Objections & Deadlocks",
"body": "If objections occur, the group must make a good faith effort to resolve them. If attempts fail and the decision is critical, the Stewardship & Process Working Group must certify that efforts are exhausted and organize a vote. A 3/4 supermajority of the full Core Membership is required to override the objection and pass the decision."
}
]
},
{
"title": "Do-ocracy",
"blocks": [
{
"label": "Core Principle",
"body": "Distribute authority and operate on trust to move efficiently. Decisions are made by those doing the work or by specific empowered groups. It is better to try a solution and adjust later than to do nothing."
},
{
"label": "Applicable Scope",
"body": "Reversible Implementation Details, Low-Risk Experiments"
},
{
"label": "Consensus Level",
"body": "0%"
},
{
"label": "Step-by-Step Instructions",
"body": "Core Members and Bottom-liners possess a broad mandate to act and can immediately try new approaches to solve problems. Simultaneously, Working Groups execute binding decisions within their domains independently, without seeking chapter-wide consent."
},
{
"label": "Objections & Deadlocks",
"body": "If an individual's experiment is permanent, costly, or hard to undo, they must seek advice first. Working Groups must seek consent from the full Core Membership if a decision has a material impact on the broader group (e.g., unbudgeted Finance expenses over $500, or actions that fundamentally change chapter operations or public reputation)."
}
]
}
]
},
{
"categoryName": "Conflict Management",
"entries": [
{
"title": "Peer Mediation",
"blocks": [
{
"label": "Core Principle",
"body": "Manage behavior so the project functions safely, establishing boundaries rather than forcing friendship. Handle conflicts at the lowest possible level to preserve capacity for serious issues."
},
{
"label": "Applicable Scope",
"body": "Lower-level disputes, initial conflicts."
},
{
"label": "Process Protocol",
"body": "Level 1 requires Direct Engagement to communicate clearly without triangulation. Level 2 utilizes Supported Conversations where each person brings a sympathetic third-party witness to help them remain calm and clear, not to argue or advocate."
},
{
"label": "Restoration & Fallbacks",
"body": "If direct engagement feels unsafe, or if third-party witnesses begin to argue, the meeting is considered a failure and the process escalates immediately to Formal Facilitation."
}
]
},
{
"title": "Judicial Committees",
"blocks": [
{
"label": "Core Principle",
"body": "The Conflict Management Working Group acts as facilitators and investigators, not judges. They do not unilaterally punish, but propose solutions for the broader group to ratify."
},
{
"label": "Applicable Scope",
"body": "Serious violations, conflicts that fail lower-level resolution."
},
{
"label": "Process Protocol",
"body": "The Working Group interviews parties to establish a behavioral timeline, focusing on facts rather than blame. They draft a Conflict Management Plan outlining specific consequences or boundaries. A dry, factual Redacted Summary, explicitly stating any disputed facts—is presented to the Core Membership for ratification under a standard 15-day lazy consensus."
},
{
"label": "Restoration & Fallbacks",
"body": "The Working Group can issue an immediate Precautionary Suspension (restricting access to keys/chats) if safety is a concern. If a plan faces unresolved objections or recommends removing a Core Member, ratification requires a 3/4 supermajority vote of the full Core Membership."
}
]
}
]
},
{
"categoryName": "Communication",
"entries": [
{
"title": "Signal",
"blocks": [
{
"label": "Core Principle & Scope",
"body": "Signal is the primary platform for operational communication. Group chats are strictly for logistics, sensitive coordination, and governance. Social conversations and community building should happen elsewhere."
},
{
"label": "Logistics, Admin & Norms",
"body": "Structure: Official channels are prepended with the 🥗 emoji. Public Channels are for active volunteers; Core Channels are restricted to Core Members.\n\nAdministration: All Core Members are admins (strictly a technical role). All channels use a standard 4-week disappearing message timer.\n\nAccess: The Stewardship & Process Working Group manages rosters, removing inactive volunteers with a \"soft touch\" message so they know they are welcome back.\n\nNorms: Use emoji reactions for acknowledgments to prevent notification spam. Only join channels where you actively contribute; do not lurk."
},
{
"label": "Code of Conduct",
"body": "The In-person Code of Conduct applies equally to all off-platform and synchronous interactions. Always assume good intentions and give others the benefit of the doubt before reacting."
}
]
},
{
"title": "In-Person Meetings",
"blocks": [
{
"label": "Core Principle & Scope",
"body": "Synchronous mediums are the dedicated spaces for social connection, community building, and navigating emotionally nuanced conversations."
},
{
"label": "Logistics, Admin & Norms",
"body": "Members are strongly encouraged to meet up outside of official operations to build friendship. If a text chat in Signal becomes heated or controversial, members must immediately transition the conversation to a synchronous medium (phone, video chat, or in-person meeting)."
},
{
"label": "Code of Conduct",
"body": "We don't have strict rules but we aspire to operate within these principles:\n\n1. We don't need to see eye to eye on everything, but the core point of unity that ties this group together is the belief that the world can be improved by both individual and collective action.\n\n2. Willfully spreading obviously false or misleading information will not be tolerated.\n\n3. We have zero tolerance for racism, sexism, and bigotry. We explicitly reject all identity-based hierarchies. No group is singled out for priority, special classification, or unique levels of responsibility. We do not create segregated spaces based on race, gender, or sexuality.\n\n4. Aspire to do no harm, especially to members of this community. We distinguish actual harm from intellectual discomfort. Vigorous disagreement is encouraged and does not constitute harm. We face difficult topics directly and minimize the use of content warnings."
}
]
}
]
}
]
}
},
"boulderCountyStreetMedics": {
"pageBackground": "var(--color-surface-invert-brand-red)",
"title": "BoCo Street Medics",
"summary": "Boulder County Street Medics is a grassroots, volunteer-run organization focused on providing first aid and medical support to marginalized communities and activists in the Boulder area.",
"document": {
"sections": [
{
"categoryName": "Values",
"entries": [
{
"title": "Radical Solidarity",
"body": "We provide care as an act of political solidarity rather than charity. We are not apolitical. Our presence is a commitment to community liberation.\n\nWe provide care as an act of political solidarity rather than charity. We are not apolitical. Our presence is a commitment to community liberation."
},
{
"title": "Integrity of Skill",
"body": "Good intentions do not stop bleeding. We only provide care we are trained to provide. Operating outside our scope is dangerous.\n\nGood intentions do not stop bleeding. We only provide care we are trained to provide. Operating outside our scope is dangerous."
},
{
"title": "Caring for Caregivers",
"body": "You cannot pour from an empty flush. We reject the martyr complex. Rest is mandatory rather than a weakness.\n\nYou cannot pour from an empty flush. We reject the martyr complex. Rest is mandatory rather than a weakness."
},
{
"title": "Consent and Autonomy",
"body": "We prioritize bodily autonomy. We secure consent before touching anyone.\n\nWe prioritize bodily autonomy. We secure consent before touching anyone."
},
{
"title": "Inter-Organizational Solidarity",
"body": "We coordinate with organizers but retain ultimate tactical control. We are healers rather than security guards.\n\nWe coordinate with organizers but retain ultimate tactical control. We are healers rather than security guards."
}
]
},
{
"categoryName": "Membership",
"entries": [
{
"title": "Tiered Membership",
"blocks": [
{
"label": "Eligibility & Philosophy",
"body": "Access is restricted to ensure safety; trust is verified rather than assumed. Roles are tiered based on age and experience. Field operations and gear cache access (which are strictly for logistical work, not social hubs) are limited to individuals 18 or older. Minors are restricted to off-site logistics. Access is a responsibility to steward resources, not a status symbol."
},
{
"label": "Joining Process",
"body": "New volunteers enter the pipeline as Provisional Members for a skill verification phase. Once proven, they can advance to Core Members, who steward the collective. Experienced Core Members can act as Bottom-Liners, serving as the tactical safety anchors who ensure buddy pairs, proper PPE, and comms."
},
{
"label": "Expectations & Removal",
"body": "Provisional Members: Never deploy alone. They must be tethered to a Core Member, operate from collective trainee bags, and do not vote or hold keys.\n\nBottom-Liners & Safety: The Bottom-Liner has absolute authority to pull a team out of danger and will immediately remove anyone under the influence of drugs or alcohol (zero-tolerance policy).\n\nRoster Maintenance: The Membership Working Group monitors activity to prevent burnout and ghosting. They manage sabbaticals, send check-in messages to inactive members, and facilitate graceful offboarding (retrieving keys/gear) if there is no response."
}
]
},
{
"title": "Skill-Based Evaluation",
"blocks": [
{
"label": "Eligibility & Philosophy",
"body": "Roles are tiered between Volunteers and Core Members. Core Membership is not a status symbol, it grants access privileges (keys, door codes, independent pickups, internal Signal chats) required to facilitate logistics."
},
{
"label": "Joining Process",
"body": "To move from Provisional to Core status, a volunteer must pass three strict skill-verification prerequisites:\n\nField Experience: Complete 5 field operations shadowed by a Core Member.\n\nSkill Verification: Gain approval from existing Core Members confirming tactical and medical competency.\n\nProof of Certification: Present a valid medical credential (e.g., 20-hour Street Medic course certificate, WFA, EMT, Paramedic, or Nursing license)."
},
{
"label": "Expectations & Removal",
"body": "You may only deploy if your skills are current and practiced. If your certification or skills have lapsed, you must refresh them before deploying again or temporarily transition to non-deployment roles."
}
]
}
]
},
{
"categoryName": "Decision-Making",
"entries": [
{
"title": "Lazy Consensus",
"blocks": [
{
"label": "Core Principle",
"body": "Move slowly on policy to build deep agreement. This prevents burnout and ensures decisions are not made only by the loudest voices."
},
{
"label": "Applicable Scope",
"body": "Strategic decisions (peacetime manual amendments, onboarding, broad policy changes). Standard Operating Procedures (packing lists, radio frequencies) live in a separate \"Living Archive\" and are exempt, updating instantly."
},
{
"label": "Consensus Level",
"body": "100%"
},
{
"label": "Step-by-Step Instructions",
"body": "Standard policy proposals are subjected to the 15-day model for review and adoption."
},
{
"label": "Objections & Deadlocks",
"body": "Emergency policies are strictly temporary. If they are not ratified through this standard 15-day process within two weeks, they automatically expire."
}
]
},
{
"title": "Supermajority Rule",
"blocks": [
{
"label": "Core Principle",
"body": "Move fast on tactics and crises to keep people safe, without allowing emergency powers to become permanent policy."
},
{
"label": "Applicable Scope",
"body": "Urgent crises that cannot wait 15 days (e.g., legal threats, lease losses)."
},
{
"label": "Consensus Level",
"body": "75%"
},
{
"label": "Step-by-Step Instructions",
"body": "Any Core Member can trigger an Emergency Vote. Voting takes place within a 24-hour window."
},
{
"label": "Objections & Deadlocks",
"body": "To prevent abuse, any decision passed via Emergency Vote must be formally ratified by the standard 15-day lazy consensus process within two weeks, or it is automatically voided."
}
]
},
{
"title": "Delegated Decision-Making",
"blocks": [
{
"label": "Core Principle",
"body": "Comply on the street, debrief in the living room. Rely on specialized groups for daily operations and an Incident Command System (ICS) for tactical safety. We explicitly reject funding that compromises political autonomy."
},
{
"label": "Applicable Scope",
"body": "Tactical field operations and daily operations (e.g., Finance Working Group managing routine purchasing via ethical, grassroots crowdfunding)."
},
{
"label": "Step-by-Step Instructions",
"body": "The Bottom-Liner acts as Incident Commander during deployments. Specialized Working Groups independently handle daily operations. Every action requires a mandated, rigorous debrief so the collective can learn and adapt."
},
{
"label": "Objections & Deadlocks",
"body": "If the Bottom-Liner loses the confidence of the entire deployment team, they must immediately designate a new leader on the spot to safely evacuate."
}
]
}
]
},
{
"categoryName": "Conflict Management",
"entries": [
{
"title": "Peer Mediation",
"blocks": [
{
"label": "Core Principle",
"body": "Conflict is inevitable in high-stress environments. The goal is to manage behavior to keep the project functional rather than forcing friendship."
},
{
"label": "Applicable Scope",
"body": "Initial interpersonal friction, handled at the lowest level possible."
},
{
"label": "Process Protocol",
"body": "Level 1: Direct Engagement.\n\nLevel 2: Supported Conversation with neutral witnesses."
},
{
"label": "Restoration & Fallbacks",
"body": "If friction cannot be managed at lower levels, it escalates to Level 3 (Formal Facilitation)."
}
]
},
{
"title": "Conflict Resolution Council",
"blocks": [
{
"label": "Core Principle",
"body": "The Working Group acts as facilitators and a mechanism of care rather than judges, relying on collective ratification for boundary plans while maintaining strict zero-tolerance for severe clinical or ethical violations."
},
{
"label": "Applicable Scope",
"body": "Level 3 Formal Facilitation, burnout/trauma concerns, formal external reviews (community complaints against medics), and clinical malpractice."
},
{
"label": "Process Protocol",
"body": "The group investigates facts and drafts a boundary plan. This group can mandate a leave of absence to address secondary trauma and burnout. They also handle external reviews and grievances. Once drafted, a dry, factual Redacted Summary of the boundary plan is presented to the broader Core Membership for ratification through consensus."
},
{
"label": "Restoration & Fallbacks",
"body": "Operating outside of scope, practicing without a license, or violating patient privacy completely bypasses the standard escalation ladder. These severe violations trigger an Immediate Precautionary Suspension rather than standard mediation."
}
]
}
]
},
{
"categoryName": "Communication",
"entries": [
{
"title": "Signal",
"blocks": [
{
"label": "Core Principle & Scope",
"body": "Digital security is physical safety. We operate in a legally precarious environment; these protocols exist to protect our patients and ourselves from surveillance and criminalization."
},
{
"label": "Logistics, Admin & Norms",
"body": "Official channels use the 🩹 emoji. Deployments utilize a dedicated Off-Site Dispatcher who holds legal names and triggers jail support protocols if a medic misses a check-in."
},
{
"label": "Code of Conduct",
"body": "Patient Health Information is never broadcast unencrypted. Field photography is strictly forbidden."
}
]
},
{
"title": "In-Person Meetings",
"blocks": [
{
"label": "Core Principle & Scope",
"body": "Prioritize patient safety and maintain absolute operational security during active deployments."
},
{
"label": "Logistics, Admin & Norms",
"body": "Radio & Chatter: Radio operation requires proper training and licensing (inform the Bottom-Liner immediately if you cannot confidently use issued gear). Tactical channels must remain absolutely silent during deployments except for vital updates, as clutter hides calls for help.\n\nState EMS: The Bottom-Liner manages or delegates dispatch communication when activating state EMS to minimize police interception risks (unless it is a time-sensitive emergency)."
},
{
"label": "Code of Conduct",
"body": "Media: Strict No Comment policy in the field. Medics treat patients rather than give soundbites.\n\nLaw Enforcement: We do not consent to warrantless searches and we do not volunteer patient identities to police."
}
]
}
]
}
]
}
}
}
+4 -4
View File
@@ -41,10 +41,10 @@
"title": "Food Not Bombs Boulder",
"description": "Food Not Bombs Boulder is a mutual aid collective that recovers surplus food to share free, public meals with the community, protesting war and poverty while advocating for food as a fundamental human right.",
"backgroundColor": "bg-[var(--color-surface-invert-secondary)]",
"iconPath": "assets/use-cases/case-study-food-not-bombs.png"
"iconPath": "assets/case-study/case-study-food-not-bombs.svg"
},
"leadingImage": {
"src": "assets/use-cases/case-study-food-not-bombs.png",
"src": "assets/case-study/case-study-food-not-bombs.svg",
"alt": "Food Not Bombs logo"
},
"bodyMarkdown": "Food Not Bombs operates on a fundamentally decentralized model. However it is a well documented phenomenon that organizations relying entirely on informal networks often develop unspoken power dynamics. When rules remain unstated the individuals with the most experience or social capital tend to direct operations by default. The Boulder chapter utilized CommunityRule to address this structural vulnerability. The platform offers a modular approach to organizational design that allows groups to select and adapt established governance patterns. By applying this toolkit the chapter successfully translated their implicit cultural norms into an explicit operating manual without adopting a rigid corporate hierarchy.\n\nA central advantage of this approach is the ability to construct a polycentric governance system. Rather than relying on a single central committee the organization distributes authority across multiple overlapping domains. CommunityRule helped the Boulder chapter map out these distinct operational needs by defining different speeds of decision making. For example broad constitutional amendments require a slow lazy consensus period to ensure comprehensive agreement. In contrast daily logistical tasks are delegated to autonomous working groups like the Finance team. This structural differentiation allows the chapter to maintain democratic participation while efficiently managing the routine demands of food recovery and distribution.\n\nFinally formalizing these operational agreements provides a critical foundation for organizational continuity. Grassroots initiatives frequently experience high participant turnover which can lead to a rapid loss of operational knowledge. By using CommunityRule to document their financial protocols and conflict escalation pathways in a legible format the chapter created a highly resilient shared resource. This explicit documentation functions as a stabilizing technology that outlasts the tenure of any individual founder or core member. It offers a practical template for how horizontal organizations can balance their ideological commitments to shared leadership with the practical necessity of maintaining a reliable infrastructure over time.",
@@ -71,10 +71,10 @@
"title": "BoCo Street Medics",
"description": "Boulder County Street Medics is a grassroots, volunteer-run organization focused on providing first aid and medical support to marginalized communities and activists in the Boulder area.",
"backgroundColor": "bg-[var(--color-surface-invert-brand-red)]",
"iconPath": "assets/use-cases/case-study-boulder-county-street-medics.png"
"iconPath": "assets/case-study/case-study-boulder-county-street-medics.svg"
},
"leadingImage": {
"src": "assets/use-cases/case-study-boulder-county-street-medics.png",
"src": "assets/case-study/case-study-boulder-county-street-medics.svg",
"alt": "Boulder County Street Medics logo"
},
"bodyMarkdown": "When communities like the BoCo Street Medics operate in high-stakes, legally precarious environments, the classic \"tyranny of structurelessness\" isn't just an academic critique. It is a massive operational risk. What these medics recognized is that relying on implicit norms and unspoken hierarchies quickly leads to burnout, fragmented trust, and compromised safety. This is exactly where a tool like CommunityRule becomes vital. By providing a legible, modular framework for democratic design, it allowed the collective to step back from the adrenaline of the streets and intentionally translate their core values of radical solidarity into explicit, accountable processes. They didn't have to reinvent the wheel of governance because they could select and adapt proven patterns to fit their unique reality.\n\nWhat CommunityRule facilitates so beautifully is the understanding that a single community can and often must operate at different speeds of democracy. The BoCo Street Medics utilized this modular approach to map out their distinct operational modes, making it crystal clear to every member when to rely on the slow, deeply participatory \"15-Day Lazy Consensus\" for strategic policy, and when to pivot to a rigid Incident Command System for tactical safety during a deployment. By charting these different pathways, the medics created a constitution that balances the deep, deliberate work of egalitarian community-building with the rapid, authoritative action required to keep people safe on the ground.\n\nUltimately, formalizing these processes through CommunityRule is an act of institutional care. It shifts the burden of conflict management, onboarding, and decision-making away from the exhausted shoulders of a few founders and distributes it into a resilient, shared architecture. By defining clear boundaries around membership access, escalation ladders for conflict, and even mandated leaves of absence, the BoCo Street Medics have built a culture of true stewardship. They've proven that radical, grassroots work doesn't have to be chaotic. When we design our governance with intention, we build a solidarity that can actually outlast us.",
@@ -0,0 +1,35 @@
<svg width="261" height="257" viewBox="0 0 261 257" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M237.187 128.398C237.187 128.718 237.187 129.038 237.186 129.358M23.5743 128.398C23.5743 128.719 23.5747 129.04 23.5756 129.361" stroke="black" stroke-width="7.26321" stroke-linecap="round" stroke-dasharray="0.36 34.03"/>
<path d="M130.381 4.90393C198.572 4.90393 253.875 59.9672 253.875 127.918C253.875 195.868 198.572 250.93 130.381 250.93C62.189 250.93 6.88667 195.868 6.88647 127.918C6.88647 59.9673 62.1889 4.90408 130.381 4.90393Z" stroke="black" stroke-width="6.43333"/>
<circle cx="130.862" cy="127.917" r="90.2838" stroke="black" stroke-width="3.21666"/>
<path d="M180.016 206.533C223.18 179.509 236.206 122.703 209.111 79.6534L52.8018 177.514C79.8967 220.564 136.852 233.556 180.016 206.533Z" fill="black"/>
<path d="M152.177 51.9861V91.6816L186.214 69.9647L206.982 102.344L168.487 126.906L206.982 151.468L186.214 183.848L152.177 162.13V204.859H113.638V161.902L77.8664 184.726L57.0977 152.347L96.971 126.906L57.0977 101.465L77.8664 69.0858L113.638 91.9097V51.9861C126.804 51.9851 139.971 51.9836 153.138 51.9822" fill="black"/>
<path d="M152.177 51.9861V91.6816L186.214 69.9647L206.982 102.344L168.487 126.906L206.982 151.468L186.214 183.848L152.177 162.13V204.859H113.638V161.902L77.8664 184.726L57.0977 152.347L96.971 126.906L57.0977 101.465L77.8664 69.0858L113.638 91.9097V51.9861C126.804 51.9851 139.971 51.9836 153.138 51.9822" stroke="#FDA8B7" stroke-width="6.43333" stroke-miterlimit="10"/>
<path d="M132.619 188.035C132.813 185.285 133.316 182.604 134.277 179.29C134.928 177.042 135.982 174.129 136.865 172.113C137.028 171.726 137.19 171.247 137.221 171.054C137.276 170.714 137.268 170.699 136.942 170.436C136.199 169.841 133.595 168.204 128.861 165.361C127.288 164.418 125.901 163.576 125.785 163.491C125.568 163.344 125.568 163.344 125.568 164.101C125.568 166.095 125.793 169.424 126.08 171.696C126.281 173.333 127.071 177.382 127.583 179.421C128.52 183.137 130.054 186.691 132.1 189.881C132.503 190.507 132.503 190.507 132.518 189.889C132.526 189.549 132.573 188.715 132.619 188.035ZM139.313 173.318C141.374 170.977 142.684 168.845 143.381 166.713C143.947 164.974 144.032 163.63 143.675 162.263C143.141 160.223 141.413 157.651 139.453 155.974C138.864 155.48 138.817 155.41 138.817 155.148C138.817 154.9 138.887 154.777 139.236 154.398C141.103 152.374 142.715 149.778 143.381 147.723C143.947 145.954 144.032 144.633 143.668 143.227C143.203 141.443 141.676 139.048 139.987 137.449C139.337 136.831 139.344 136.761 140.181 135.718C142.808 132.435 144.171 129.051 143.846 126.61C143.567 124.477 142.211 122.083 140.127 120.035C138.353 118.297 136.369 116.984 132.425 114.952C127.893 112.611 126.25 111.46 125.173 109.861C124.731 109.204 124.615 108.718 124.654 107.767C124.685 107.087 124.724 106.91 124.941 106.446C125.08 106.16 125.282 105.797 125.39 105.658C125.592 105.396 125.592 105.396 125.367 105.535C123.693 106.585 122.361 108.161 121.849 109.675C121.539 110.595 121.439 111.375 121.439 112.866C121.446 116.072 122.438 118.761 124.515 121.225C124.824 121.604 125.212 122.029 125.367 122.168C125.522 122.314 125.646 122.484 125.646 122.538C125.646 122.592 125.708 122.639 125.785 122.639C125.863 122.639 126.025 122.747 126.157 122.878C126.49 123.226 127.738 124.161 128.652 124.756C129.892 125.559 131.24 126.285 133.549 127.405C137.299 129.221 138.12 129.684 138.81 130.372C139.251 130.82 139.414 131.16 139.522 131.84C139.662 132.721 139.461 134.289 139.058 135.579C138.918 136.019 138.903 136.043 138.647 136.066C138.446 136.081 138.306 136.027 138.027 135.818C136.95 135 135.393 134.088 132.309 132.473C129.636 131.067 129.032 130.727 128.086 130.102C126.785 129.244 125.77 128.317 125.173 127.452C124.453 126.417 124.453 124.732 125.173 123.574C125.313 123.342 125.375 123.18 125.313 123.18C125.158 123.18 124.174 123.952 123.593 124.532C122.957 125.157 122.492 125.814 122.113 126.602C121.191 128.541 121.183 131.863 122.089 134.559C122.748 136.552 123.686 137.997 125.429 139.735C126.103 140.407 126.653 141.01 126.653 141.072C126.653 141.134 126.723 141.18 126.808 141.18C126.893 141.18 127.397 141.474 127.924 141.837C129.466 142.895 130.566 143.513 133.161 144.78C137.539 146.928 138.066 147.221 138.786 147.932C139.422 148.565 139.553 148.944 139.546 150.141C139.546 150.628 139.499 151.293 139.445 151.609C139.337 152.212 138.941 153.587 138.662 154.344C138.508 154.754 138.484 154.777 138.229 154.777C138.035 154.777 137.787 154.661 137.283 154.321C135.827 153.348 135.122 152.946 131.534 151.069C128.513 149.477 127.288 148.681 126.18 147.592C124.941 146.379 124.639 145.769 124.639 144.51C124.639 143.76 124.848 143.05 125.22 142.493C125.367 142.269 125.491 142.076 125.491 142.053C125.491 141.999 124.639 142.617 124.081 143.073C122.988 143.977 122.02 145.475 121.702 146.75C121.338 148.187 121.338 150.373 121.694 152.104C122.43 155.65 124.631 158.663 128.265 161.096C129.535 161.954 131 162.749 134.114 164.271C138.717 166.535 139.313 166.991 139.522 168.428C139.716 169.718 139.352 171.587 138.477 173.812C138.205 174.492 138.205 174.492 138.407 174.314C138.515 174.214 138.926 173.766 139.313 173.318ZM153.554 113.299C153.531 112.889 153.539 112.835 153.694 112.789C153.787 112.758 153.841 112.719 153.825 112.704C153.802 112.681 153.446 112.619 153.027 112.557C152.601 112.495 152.09 112.41 151.881 112.364C151.671 112.318 151.493 112.294 151.478 112.31C151.462 112.325 151.749 112.518 152.121 112.742C152.632 113.036 153.577 113.7 153.577 113.747C153.577 113.755 153.562 113.546 153.554 113.299ZM136.795 106.129C136.78 99.0375 136.78 99.0375 139.739 99.0143C142.691 98.9989 142.691 98.9989 142.691 98.2341C142.691 97.477 142.691 97.477 141.281 97.4306C140.514 97.3997 139.174 97.3765 138.314 97.3765C136.749 97.3765 136.749 97.3765 136.702 90.8331C136.671 87.2253 136.648 81.8406 136.648 78.8663C136.648 73.443 136.648 73.443 136.919 72.9949C137.849 71.4576 137.818 69.9666 136.826 68.4678C136.237 67.5717 135.246 66.8609 134.2 66.5905C132.82 66.2352 131.24 66.7373 130.171 67.8884C129.334 68.7768 128.838 70.0361 128.939 71.025C129.001 71.6662 129.225 72.346 129.566 72.9022C129.83 73.3349 129.83 73.3349 129.83 74.7255C129.83 76.116 129.83 76.116 130.101 76.2087C130.481 76.3324 131.077 76.8963 131.302 77.3367C131.465 77.6611 131.496 77.8156 131.496 78.4878C131.496 79.1135 131.465 79.3298 131.333 79.6157C131.093 80.1178 130.728 80.4964 130.248 80.7359C129.83 80.9367 129.83 80.9367 129.83 81.3925C129.83 81.8406 129.83 81.8406 130.248 82.0415C130.728 82.Line truncated
<path d="M132.621 188.035C132.815 185.285 133.318 182.604 134.279 179.29C134.93 177.042 135.983 174.129 136.867 172.113C137.029 171.726 137.192 171.247 137.223 171.054C137.277 170.714 137.27 170.699 136.944 170.436C136.2 169.841 133.597 168.204 128.863 165.361C127.29 164.418 125.903 163.576 125.787 163.491C125.57 163.344 125.57 163.344 125.57 164.101C125.57 166.095 125.795 169.424 126.081 171.696C126.283 173.333 127.073 177.382 127.585 179.421C128.522 183.137 130.056 186.691 132.102 189.881C132.505 190.507 132.505 190.507 132.52 189.889C132.528 189.549 132.574 188.715 132.621 188.035ZM136.797 106.129C136.782 99.0375 136.782 99.0375 139.741 99.0143C142.693 98.9989 142.693 98.9989 142.693 98.2341C142.693 97.477 142.693 97.477 141.283 97.4306C140.516 97.3997 139.176 97.3765 138.316 97.3765C136.751 97.3765 136.751 97.3765 136.704 90.8331C136.673 87.2253 136.65 81.8406 136.65 78.8663C136.65 73.443 136.65 73.443 136.921 72.9949C137.851 71.4576 137.82 69.9666 136.828 68.4678C136.239 67.5717 135.247 66.8609 134.201 66.5905C132.822 66.2352 131.242 66.7373 130.172 67.8884C129.336 68.7768 128.84 70.0361 128.94 71.025C129.002 71.6662 129.227 72.346 129.568 72.9022C129.832 73.3349 129.832 73.3349 129.832 74.7255C129.832 76.116 129.832 76.116 130.103 76.2087C130.482 76.3324 131.079 76.8963 131.304 77.3367C131.466 77.6611 131.497 77.8156 131.497 78.4878C131.497 79.1135 131.466 79.3298 131.335 79.6157C131.094 80.1178 130.73 80.4964 130.25 80.7359C129.832 80.9367 129.832 80.9367 129.832 81.3925C129.832 81.8406 129.832 81.8406 130.25 82.0415C130.73 82.2655 131.094 82.6441 131.335 83.1539C131.583 83.6947 131.567 84.9153 131.311 85.433C131.071 85.9119 130.622 86.3523 130.18 86.5377C129.832 86.6768 129.832 86.6768 129.832 87.2716C129.832 87.8665 129.832 87.8665 130.203 88.0287C130.684 88.245 131.094 88.6545 131.319 89.1412C131.466 89.4657 131.497 89.6665 131.497 90.3077C131.497 90.9567 131.466 91.1344 131.319 91.4434C131.102 91.876 130.482 92.4631 130.103 92.5867C129.832 92.6794 129.832 92.6794 129.832 94.9894C129.832 96.7739 129.808 97.3147 129.739 97.3379C129.684 97.3534 128.173 97.392 126.384 97.4306C123.129 97.4924 123.129 97.4924 123.129 98.2263C123.129 98.9603 123.129 98.9603 125.988 98.9834C128.166 98.9989 128.871 99.0221 128.917 99.0993C128.948 99.1457 128.979 101.34 128.979 103.959C128.979 107.675 129.002 108.748 129.08 108.81C129.204 108.926 136.673 113.206 136.751 113.206C136.789 113.214 136.805 110.023 136.797 106.129Z" fill="#FDA8B7"/>
<path d="M42.9432 84.2425L28.6759 75.5007L32.3359 69.5585C32.9931 68.4916 33.6941 67.6911 34.4388 67.1571C35.1864 66.6184 35.9433 66.3347 36.7096 66.3058C37.4787 66.2723 38.2209 66.4746 38.9361 66.9128C39.4795 67.2458 39.8948 67.6568 40.1821 68.1459C40.4647 68.6321 40.6225 69.1601 40.6554 69.7298C40.6884 70.2994 40.5991 70.8805 40.3877 71.473L40.5271 71.5583C40.9092 71.0002 41.392 70.5644 41.9753 70.2509C42.5614 69.9328 43.1991 69.7804 43.8882 69.7937C44.5726 69.8042 45.2585 70.02 45.9459 70.4411C46.7122 70.9106 47.2772 71.5252 47.641 72.2848C48.0001 73.0415 48.1267 73.8954 48.0207 74.8463C47.9101 75.7945 47.5305 76.795 46.8819 77.848L42.9432 84.2425ZM42.2851 79.0951L43.5965 76.966C44.0565 76.2191 44.2535 75.5827 44.1873 75.0565C44.124 74.5258 43.8393 74.1053 43.333 73.7951C42.9661 73.5703 42.5974 73.4626 42.227 73.4721C41.8519 73.4786 41.4952 73.5955 41.157 73.8228C40.8141 74.0471 40.5069 74.3797 40.2355 74.8203L38.8854 77.0121L42.2851 79.0951ZM36.6631 75.6505L37.8374 73.744C38.0689 73.3682 38.2119 72.9958 38.2665 72.6268C38.3211 72.2577 38.28 71.9194 38.1431 71.6119C38.0091 71.2998 37.7749 71.0413 37.4405 70.8364C36.9622 70.5433 36.4817 70.4821 35.9991 70.6529C35.5165 70.8236 35.0981 71.1966 34.7438 71.7718L33.5352 73.734L36.6631 75.6505Z" fill="black"/>
<path d="M55.5351 53.3753C56.9126 54.5967 57.8445 55.8964 58.331 57.2743C58.8174 58.6521 58.9068 60.013 58.5992 61.3567C58.2953 62.6963 57.6448 63.9254 56.6479 65.0438C55.6473 66.1663 54.4959 66.9547 53.1934 67.409C51.891 67.8634 50.5273 67.9321 49.1024 67.6152C47.6771 67.2907 46.2777 66.5196 44.9043 65.3018C43.5268 64.0804 42.593 62.7828 42.103 61.409C41.6165 60.0311 41.5253 58.6723 41.8293 57.3327C42.1332 55.993 42.7855 54.7619 43.7861 53.6394C44.783 52.521 45.9306 51.7328 47.229 51.2749C48.531 50.8129 49.8965 50.7421 51.3254 51.0626C52.7544 51.383 54.1576 52.154 55.5351 53.3753ZM52.7836 56.4622C51.9685 55.7395 51.1765 55.2447 50.4074 54.9778C49.642 54.7069 48.9225 54.6587 48.2489 54.8333C47.5789 55.0037 46.9756 55.3899 46.4391 55.9919C45.9062 56.5897 45.5923 57.2325 45.4974 57.9201C45.4025 58.6078 45.5329 59.3169 45.8887 60.0473C46.248 60.7738 46.8353 61.4983 47.6504 62.221C48.4654 62.9438 49.2557 63.4406 50.0211 63.7115C50.7901 63.9783 51.5114 64.0245 52.1851 63.85C52.8587 63.6754 53.4619 63.2892 53.9948 62.6914C54.5314 62.0895 54.8453 61.4467 54.9366 60.7631C55.0315 60.0755 54.8993 59.3684 54.5399 58.642C54.1841 57.9115 53.5987 57.1849 52.7836 56.4622Z" fill="black"/>
<path d="M61.0729 37.8893L64.2506 35.3993L70.9292 43.8773C71.701 44.8571 72.1397 45.8938 72.2452 46.9874C72.3474 48.0768 72.1371 49.1481 71.6143 50.2013C71.0882 51.2502 70.2719 52.2081 69.1655 53.0751C68.0462 53.9522 66.9134 54.5215 65.7671 54.7832C64.6174 55.0405 63.5262 54.9891 62.4935 54.629C61.4573 54.2645 60.5534 53.5924 59.7815 52.6126L53.103 44.1346L56.2871 41.6395L62.7483 49.8416C63.1055 50.2951 63.5227 50.6221 63.9997 50.8227C64.481 51.0199 64.9809 51.0779 65.4996 50.9967C66.0182 50.9155 66.5155 50.6884 66.9915 50.3154C67.4675 49.9424 67.8043 49.5159 68.0018 49.0359C68.2036 48.5525 68.2655 48.0543 68.1873 47.5413C68.1091 47.0282 67.8913 46.5449 67.5341 46.0914L61.0729 37.8893Z" fill="black"/>
<path d="M77.2204 46.819L69.0522 32.2158L72.587 30.249L79.1519 41.9857L85.2362 38.6004L86.8395 41.4668L77.2204 46.819Z" fill="black"/>
<path d="M94.25 38.3035L88.4442 40.4392L82.6404 24.7453L88.4385 22.6123C90.0388 22.0236 91.533 21.8309 92.9209 22.0342C94.312 22.2305 95.5393 22.8004 96.6027 23.7441C97.6643 24.6826 98.4964 25.9668 99.099 27.5964C99.7036 29.2312 99.9087 30.7518 99.7144 32.1582C99.5251 33.5626 98.9663 34.7926 98.0381 35.8479C97.1079 36.8981 95.8452 37.7166 94.25 38.3035ZM91.0447 35.8089L92.9084 35.1233C93.7878 34.7998 94.4769 34.3779 94.9758 33.8578C95.4779 33.3307 95.7602 32.6726 95.8227 31.8835C95.8884 31.0874 95.7125 30.1249 95.295 28.9958C94.8775 27.8668 94.3865 27.026 93.822 26.4734C93.2556 25.9156 92.6083 25.6024 91.88 25.5337C91.155 25.458 90.3425 25.5856 89.4427 25.9167L87.6327 26.5825L91.0447 35.8089Z" fill="black"/>
<path d="M105.139 34.5293L102.012 18.0907L113.478 15.921L114.092 19.1477L106.6 20.5653L107.242 23.9365L114.147 22.6298L114.762 25.8646L107.857 27.1712L108.5 30.5505L115.991 29.1329L116.605 32.3596L105.139 34.5293Z" fill="black"/>
<path d="M119.212 31.8789L118.635 15.1548L125.544 14.9177C126.796 14.8747 127.885 15.0635 128.81 15.4842C129.74 15.8992 130.467 16.5174 130.991 17.3388C131.515 18.1547 131.797 19.1343 131.837 20.2776C131.877 21.4371 131.658 22.4312 131.179 23.2597C130.701 24.0828 130.003 24.7227 129.087 25.1793C128.17 25.6304 127.075 25.8779 125.801 25.9216L121.432 26.0716L121.322 22.8868L124.94 22.7626C125.55 22.7417 126.056 22.6453 126.459 22.4734C126.867 22.2959 127.172 22.0401 127.373 21.7062C127.574 21.3668 127.665 20.9385 127.647 20.4214C127.63 19.9042 127.509 19.4805 127.285 19.1502C127.06 18.8145 126.739 18.5694 126.319 18.4148C125.899 18.2548 125.387 18.1852 124.783 18.2059L122.79 18.2743L123.255 31.7402L119.212 31.8789ZM128.365 23.9123L132.793 31.4128L128.383 31.5642L124.036 24.0609L128.365 23.9123Z" fill="black"/>
<path d="M159.575 24.7121L155.58 23.8563C155.615 23.4349 155.585 23.0413 155.491 22.6758C155.397 22.3102 155.24 21.9812 155.019 21.6887C154.797 21.3962 154.514 21.1487 154.167 20.9464C153.827 20.7398 153.433 20.5886 152.986 20.4928C152.192 20.3228 151.466 20.3705 150.807 20.6361C150.153 20.9027 149.593 21.3704 149.126 22.0392C148.664 22.7091 148.321 23.5659 148.096 24.6097C147.862 25.6961 147.82 26.6399 147.971 27.441C148.128 28.2379 148.449 28.8833 148.933 29.377C149.424 29.8666 150.058 30.1946 150.836 30.3612C151.272 30.4548 151.681 30.4839 152.062 30.4485C152.449 30.4142 152.8 30.3197 153.118 30.1649C153.442 30.0059 153.725 29.7907 153.967 29.5195C154.216 29.2441 154.418 28.9168 154.572 28.5375L158.561 29.4172C158.336 30.1267 157.972 30.7952 157.469 31.4229C156.971 32.0517 156.357 32.5913 155.624 33.0417C154.893 33.4868 154.054 33.7862 153.107 33.9399C152.166 34.0948 151.133 34.0518 150.009 33.811C148.528 33.4938 147.272 32.8849 146.24 31.9841C145.215 31.0792 144.499 29.9285 144.092 28.5321C143.685 27.1357 143.675 25.5401 144.061 23.7454C144.449 21.9454 145.121 20.4932 146.078 19.3889C147.034 18.2846 148.168 17.5331 149.48 17.1344C150.792 16.7357 152.175 16.6921 153.629 17.0036C154.619 17.2158 155.506 17.5505 156.288 18.0078C157.071 18.4596 157.725 19.0177 158.251 19.6818C158.777 20.3406 159.159 21.0936 159.394 21.9408C159.63 22.7879 159.69 23.7117 159.575 24.7121Z" fill="black"/>
<path d="M177.681 34.1064C176.968 35.8039 176.036 37.1062 174.887 38.0133C173.737 38.9203 172.476 39.447 171.103 39.5932C169.735 39.7415 168.36 39.5267 166.978 38.9489C165.591 38.3689 164.472 37.5379 163.621 36.4557C162.77 35.3736 162.265 34.1084 162.106 32.66C161.953 31.2087 162.233 29.6368 162.945 27.9443C163.658 26.2468 164.587 24.9435 165.732 24.0343C166.881 23.1273 168.14 22.5996 169.508 22.4513C170.876 22.303 172.254 22.5188 173.641 23.0987C175.022 23.6766 176.14 24.5041 176.993 25.5812C177.851 26.6604 178.359 27.9267 178.516 29.3801C178.673 30.8335 178.395 32.4089 177.681 34.1064ZM173.867 32.5115C174.289 31.5071 174.503 30.5984 174.508 29.7856C174.518 28.9749 174.331 28.2798 173.948 27.7005C173.571 27.1233 173.01 26.6792 172.266 26.3682C171.527 26.0593 170.817 25.9718 170.134 26.1056C169.452 26.2395 168.821 26.593 168.244 27.1663C167.671 27.7417 167.173 28.5316 166.751 29.5361C166.329 30.5405 166.113 31.4481 166.103 32.2588C166.098 33.0717 166.287 33.7678 166.67 34.3471C167.053 34.9264 167.613 35.3705 168.352 35.6794C169.096 35.9904 169.807 36.0779 170.484 35.942C171.167 35.8081 171.794 35.4535 172.367 34.8781C172.945 34.3048 173.445 33.516 173.867 32.5115Z" fill="black"/>
<path d="M194.565 34.5512L197.947 36.7545L192.031 45.7851C191.348 46.8287 190.504 47.5759 189.499 48.0265C188.497 48.4726 187.412 48.6177 186.245 48.4617C185.08 48.3012 183.909 47.8373 182.732 47.0702C181.541 46.2941 180.637 45.4065 180.022 44.4074C179.41 43.4037 179.11 42.3562 179.121 41.2649C179.136 40.169 179.485 39.0992 180.169 38.0556L186.084 29.025L189.472 31.2328L183.75 39.9694C183.433 40.4525 183.256 40.9516 183.218 41.4666C183.185 41.9846 183.29 42.4756 183.533 42.9396C183.776 43.4036 184.151 43.8006 184.657 44.1306C185.164 44.4607 185.677 44.642 186.196 44.6746C186.719 44.7102 187.212 44.6087 187.674 44.3702C188.137 44.1317 188.526 43.7709 188.842 43.2878L194.565 34.5512Z" fill="black"/>
<path d="M212.546 49.4778L201.118 61.7054L198.605 59.369L200.324 47.778L200.247 47.7057L193.651 54.7629L190.689 52.0092L202.117 39.7816L204.666 42.1514L202.904 53.6804L203.006 53.7749L209.591 46.7297L212.546 49.4778Z" fill="black"/>
<path d="M213.921 55.595L216.54 53.6111L225.108 64.862L222.49 66.8458L219.416 62.8096L208.694 70.9325L206.278 67.7607L217 59.6377L213.921 55.595Z" fill="black"/>
<path d="M227.71 68.4754L229.79 72.4861L225.195 78.8377L225.262 78.9682L233.115 78.8975L235.195 82.9082L222.472 82.5868L217.504 85.1497L215.654 81.5814L220.621 79.0185L227.71 68.4754Z" fill="black"/>
<path d="M51.4135 193.365C51.8495 192.95 52.0638 192.472 52.0565 191.929C52.0568 191.387 51.8111 190.806 51.3194 190.188C50.9938 189.779 50.6691 189.48 50.3453 189.292C50.0215 189.104 49.7113 189.016 49.4148 189.028C49.1226 189.037 48.8552 189.134 48.6126 189.319C48.4093 189.466 48.2668 189.641 48.1852 189.845C48.1069 190.053 48.0762 190.285 48.0931 190.543C48.1177 190.801 48.1822 191.084 48.2867 191.39C48.3911 191.696 48.53 192.024 48.7033 192.373L49.3423 193.701C49.7168 194.461 49.9833 195.181 50.1417 195.861C50.3035 196.546 50.3561 197.185 50.2993 197.779C50.246 198.377 50.0773 198.93 49.7935 199.44C49.5096 199.949 49.1067 200.414 48.5846 200.834C47.7525 201.486 46.8751 201.84 45.9523 201.896C45.0296 201.952 44.1016 201.718 43.1683 201.192C42.2383 200.671 41.3393 199.864 40.4711 198.773C39.5996 197.677 39.0044 196.592 38.6856 195.518C38.3668 194.443 38.3631 193.415 38.6743 192.432C38.9855 191.449 39.6486 190.546 40.6636 189.722L43.0748 192.753C42.6722 193.113 42.4097 193.505 42.2871 193.929C42.1646 194.352 42.1697 194.796 42.3026 195.26C42.4388 195.729 42.6917 196.196 43.0614 196.661C43.4005 197.087 43.7464 197.404 44.0992 197.611C44.4553 197.822 44.8003 197.928 45.1342 197.927C45.4681 197.927 45.7692 197.824 46.0374 197.619C46.2818 197.419 46.4322 197.175 46.4887 196.887C46.5494 196.596 46.52 196.244 46.4006 195.831C46.2889 195.419 46.0987 194.931 45.8299 194.366L45.0617 192.744C44.4202 191.403 44.1305 190.177 44.1926 189.065C44.259 187.95 44.7916 187.001 45.7903 186.217C46.5985 185.571 47.4828 185.225 48.4429 185.181C49.4031 185.137 50.3626 185.367 51.3216 185.873C52.2805 186.378 53.1551 187.128 53.9452 188.121C54.7523 189.135 55.2796 190.153 55.5271 191.172C55.7822 192.193 55.7645 193.155 55.474 194.059C55.1836 194.963 54.6338 195.742 53.8247 196.396L51.4135 193.365Z" fill="black"/>
<path d="M56.7361 197.251L58.9545 194.829L69.4107 204.354L67.1923 206.776L63.4411 203.359L54.3578 213.278L51.41 210.593L60.4933 200.674L56.7361 197.251Z" fill="black"/>
<path d="M61.9331 219.786L71.1152 205.796L76.9035 209.574C77.9526 210.259 78.734 211.039 79.2477 211.915C79.7689 212.789 80.0069 213.712 79.9616 214.684C79.9194 215.652 79.5845 216.614 78.9568 217.57C78.3201 218.54 77.567 219.226 76.6975 219.628C75.8309 220.025 74.891 220.146 73.8779 219.992C72.8678 219.834 71.829 219.406 70.7617 218.709L67.1012 216.32L68.8498 213.655L71.8808 215.634C72.3916 215.968 72.8615 216.18 73.2902 216.271C73.7266 216.361 74.1234 216.327 74.4806 216.17C74.8408 216.009 75.1629 215.711 75.4468 215.279C75.7308 214.846 75.8765 214.43 75.8839 214.032C75.8943 213.629 75.7725 213.244 75.5185 212.876C75.2675 212.504 74.8888 212.153 74.3825 211.822L72.7131 210.732L65.3199 221.997L61.9331 219.786ZM74.0184 218.542L73.3113 227.215L69.6166 224.803L70.3922 216.175L74.0184 218.542Z" fill="black"/>
<path d="M77.3637 229.487L84.2692 214.244L94.9072 219.038L93.5517 222.03L86.6013 218.898L85.1851 222.024L91.5917 224.911L90.2328 227.91L83.8262 225.023L82.4067 228.157L89.3571 231.289L88.0016 234.281L77.3637 229.487Z" fill="black"/>
<path d="M93.0112 236.253L97.5509 220.146L108.786 223.296L107.895 226.458L100.554 224.4L99.6233 227.703L106.39 229.6L105.496 232.769L98.73 230.872L97.7968 234.184L105.137 236.242L104.246 239.403L93.0112 236.253Z" fill="black"/>
<path d="M110.594 227.242L110.98 223.98L125.027 225.634L124.641 228.896L119.601 228.303L118.02 241.659L114.06 241.193L115.641 227.837L110.594 227.242Z" fill="black"/>
<path d="M134.25 226.094L139.207 225.377L144.902 235.023L145.096 234.995L147.812 224.134L152.769 223.417L155.176 239.977L151.278 240.541L149.799 230.369L149.662 230.388L147.146 241.031L144.655 241.391L139.205 231.842L139.068 231.862L140.554 242.091L136.656 242.654L134.25 226.094Z" fill="black"/>
<path d="M161.116 238.407L155.687 222.578L166.73 218.81L167.796 221.917L160.581 224.379L161.694 227.625L168.345 225.356L169.413 228.471L162.763 230.74L163.879 233.994L171.094 231.532L172.16 234.639L161.116 238.407Z" fill="black"/>
<path d="M182.752 229.764L177.3 232.687L169.361 217.956L174.806 215.037C176.308 214.231 177.761 213.833 179.164 213.841C180.569 213.842 181.864 214.235 183.049 215.021C184.232 215.803 185.235 216.959 186.059 218.488C186.886 220.023 187.302 221.5 187.306 222.919C187.314 224.336 186.933 225.632 186.161 226.806C185.386 227.975 184.25 228.961 182.752 229.764ZM179.229 227.74L180.979 226.802C181.805 226.36 182.429 225.846 182.85 225.262C183.274 224.67 183.462 223.979 183.413 223.189C183.367 222.392 183.059 221.463 182.487 220.403C181.916 219.343 181.313 218.579 180.676 218.111C180.038 217.637 179.353 217.417 178.622 217.451C177.893 217.477 177.107 217.716 176.262 218.169L174.562 219.08L179.229 227.74Z" fill="black"/>
<path d="M188.13 206.819L197.89 220.412L194.598 222.763L184.838 209.17L188.13 206.819Z" fill="black"/>
<path d="M205.74 199.638L202.744 202.415C202.418 202.145 202.078 201.944 201.724 201.812C201.37 201.68 201.009 201.62 200.642 201.634C200.275 201.647 199.909 201.738 199.544 201.906C199.179 202.065 198.829 202.301 198.493 202.612C197.898 203.164 197.52 203.785 197.36 204.476C197.203 205.163 197.266 205.889 197.546 206.654C197.831 207.415 198.338 208.186 199.065 208.967C199.823 209.78 200.574 210.354 201.318 210.689C202.063 211.017 202.777 211.124 203.46 211.01C204.144 210.889 204.777 210.558 205.36 210.017C205.688 209.714 205.947 209.396 206.136 209.064C206.33 208.729 206.455 208.387 206.51 208.038C206.565 207.682 206.551 207.328 206.468 206.974C206.384 206.613 206.231 206.261 206.008 205.918L209.021 203.159C209.475 203.749 209.814 204.429 210.041 205.2C210.271 205.966 210.361 206.778 210.311 207.635C210.256 208.487 210.02 209.345 209.603 210.207C209.189 211.066 208.561 211.886 207.718 212.667C206.607 213.697 205.386 214.376 204.054 214.704C202.722 215.025 201.367 214.952 199.986 214.486C198.606 214.02 197.291 213.115 196.039 211.772C194.784 210.425 193.978 209.045 193.62 207.631C193.263 206.216 193.297 204.859 193.723 203.558C194.149 202.257 194.907 201.101 195.998 200.09C196.741 199.401 197.525 198.868 198.349 198.49C199.17 198.108 200.003 197.892 200.851 197.842C201.694 197.788 202.531 197.908 203.362 198.2C204.192 198.492 204.985 198.971 205.74 199.638Z" fill="black"/>
<path d="M213.061 188.988C212.55 188.669 212.032 188.58 211.507 188.721C210.98 188.854 210.477 189.235 209.997 189.862C209.68 190.278 209.47 190.666 209.366 191.025C209.263 191.384 209.254 191.706 209.339 191.989C209.42 192.27 209.58 192.504 209.82 192.693C210.013 192.853 210.218 192.948 210.436 192.977C210.657 193.001 210.891 192.973 211.137 192.893C211.382 192.806 211.641 192.674 211.912 192.497C212.184 192.321 212.469 192.106 212.765 191.852L213.898 190.907C214.544 190.357 215.178 189.922 215.8 189.601C216.425 189.276 217.033 189.067 217.624 188.976C218.218 188.88 218.798 188.906 219.363 189.055C219.928 189.204 220.479 189.479 221.016 189.879C221.854 190.523 222.414 191.284 222.696 192.162C222.979 193.04 222.979 193.995 222.699 195.026C222.421 196.054 221.859 197.122 221.013 198.23C220.163 199.343 219.256 200.186 218.29 200.759C217.325 201.332 216.326 201.59 215.295 201.531C214.263 201.473 213.222 201.055 212.171 200.277L214.522 197.199C214.971 197.499 215.416 197.656 215.858 197.67C216.3 197.684 216.73 197.569 217.149 197.326C217.57 197.079 217.962 196.72 218.322 196.248C218.653 195.815 218.875 195.402 218.989 195.01C219.107 194.614 219.125 194.254 219.042 193.932C218.959 193.609 218.785 193.343 218.52 193.135C218.265 192.948 217.991 192.863 217.697 192.879C217.399 192.892 217.064 193.007 216.693 193.225C216.32 193.434 215.893 193.739 215.409 194.138L214.023 195.28C212.879 196.231 211.759 196.814 210.663 197.028C209.563 197.239 208.509 196.958 207.502 196.186C206.674 195.564 206.121 194.795 205.841 193.878C205.561 192.961 205.549 191.976 205.803 190.925C206.058 189.873 206.571 188.843 207.341 187.834C208.128 186.804 208.986 186.043 209.916 185.553C210.845 185.054 211.785 184.834 212.734 184.892C213.684 184.95 214.576 185.289 215.411 185.91L213.061 188.988Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1,101 @@
<svg width="305" height="305" viewBox="0 0 305 305" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_22877_2962)">
<path d="M0 23.0933C0 10.3392 10.3392 0 23.0933 0H281.907C294.661 0 305 10.3392 305 23.0933V281.907C305 294.661 294.661 305 281.907 305H23.0933C10.3392 305 0 294.661 0 281.907V23.0933Z" fill="#E5E5E5"/>
<path d="M300.076 111.766C299.913 110.885 299.26 110.173 298.518 109.714C298.968 109.007 299.818 108.39 299.579 107.477C299.506 107.195 299.267 106.928 299.032 106.768C298.364 106.313 297.805 106.21 298.398 104.88C298.613 104.397 298.828 103.914 299.051 103.433C299.122 103.28 299.097 103.076 298.929 102.993C298.681 102.672 298.432 102.351 298.183 102.031C297.854 101.475 297.917 101.213 298.37 101.245C298.582 101.335 298.715 101.564 298.946 101.633C299.233 101.717 299.591 101.798 299.842 101.557C300.122 101.289 300.378 100.914 300.33 100.496C300.288 100.127 300.279 100.075 300.155 99.7041C299.556 97.9185 301.635 97.748 302.727 97.9317C302.737 97.9333 302.743 97.9207 302.735 97.9153C301.7 97.3244 300.789 96.9375 299.737 97.724C299.488 97.9103 299.23 98.3241 299.237 98.6465C299.245 99.0433 299.855 101.107 298.72 100.629C296.854 99.8429 297.237 102.508 298.354 103.388C297.899 104.258 297.03 105.566 297.53 106.446C297.717 106.774 298.109 106.987 298.412 107.182C299.404 107.818 297.989 109.087 297.745 109.677C297.675 109.847 297.692 110.06 297.877 110.155C299.154 110.812 299.609 111.951 299.385 113.337C299.267 114.064 299.169 114.712 299.333 115.441C299.365 115.582 299.551 115.535 299.566 115.411C299.713 114.123 300.321 113.091 300.076 111.766Z" fill="#2D2D2D"/>
<path d="M308.483 108.562C306.697 107.823 304.742 107.538 302.971 106.747C302.951 106.738 302.933 106.769 302.954 106.778C304.725 107.556 306.475 108.542 308.44 108.724C308.524 108.732 308.563 108.594 308.483 108.562Z" fill="#2D2D2D"/>
<path d="M252.431 26.5132C252.979 29.0186 253.523 31.5267 253.817 34.0769C254.256 37.8798 254.017 41.7389 253.699 45.5407C253.379 49.3611 252.94 53.1705 252.858 57.0067C252.816 58.9589 252.905 60.909 253.138 62.8476C253.337 64.5102 253.566 66.3903 254.356 67.8873C254.477 68.1158 254.888 68.0283 254.86 67.754C254.678 65.97 254.124 64.226 253.915 62.4344C253.69 60.4953 253.663 58.5348 253.703 56.5853C253.782 52.7513 254.245 48.9331 254.562 45.1144C254.881 41.2662 255.029 37.4037 254.565 33.561C254.292 31.2906 253.784 29.0558 253.266 26.8313C252.675 24.2893 251.803 21.8321 251.195 19.3054C251.19 19.2885 251.165 19.2956 251.168 19.3125C251.672 21.6927 251.91 24.1254 252.431 26.5132Z" fill="#2D2D2D"/>
<path d="M208.081 7.08557C208.065 7.06152 208.026 7.08339 208.042 7.10743C209.51 9.351 211.128 11.4284 212.874 13.4621C214.715 15.6057 216.378 17.8771 218.186 20.0387C218.325 20.2054 218.54 19.9939 218.467 19.8245C217.499 17.5705 215.605 15.6538 213.975 13.8638C211.961 11.6525 209.736 9.59312 208.081 7.08557Z" fill="#2D2D2D"/>
<path d="M224.482 0.393887C225.024 2.62325 225.658 4.78484 226.567 6.89396C226.603 6.97539 226.737 6.91855 226.711 6.83548C226.025 4.67279 225.266 2.53197 224.603 0.362187C224.58 0.285124 224.463 0.315731 224.482 0.393887Z" fill="#2D2D2D"/>
<path d="M207.364 68.5148C211.039 67.7535 214.905 67.355 218.636 66.9446C222.425 66.5276 226.242 66.3745 230.052 66.4735C233.808 66.5708 237.548 66.9757 241.239 67.6693C243.021 68.0038 244.783 68.4339 246.527 68.9242C248.415 69.4554 250.241 70.255 252.147 70.7037C252.397 70.7628 252.543 70.4168 252.317 70.2862C249.247 68.505 245.315 67.7404 241.873 67.0348C238.135 66.268 234.319 65.8788 230.508 65.7455C226.546 65.6072 222.575 65.8121 218.636 66.2374C214.763 66.6549 211.122 67.5616 207.352 68.4716C207.324 68.4787 207.336 68.5208 207.364 68.5148Z" fill="#2D2D2D"/>
<path d="M246.946 70.1402C245.056 69.3482 242.847 69.1345 240.829 68.8492C238.577 68.5306 236.325 68.5229 234.065 68.3355C234.028 68.3327 234.028 68.3907 234.065 68.3934C236.277 68.5771 238.484 69.0771 240.675 69.4395C242.71 69.7762 244.797 70.3938 246.86 70.4654C247.025 70.4708 247.11 70.209 246.946 70.1402Z" fill="#2D2D2D"/>
<path d="M240.266 85.0702C243.514 84.4854 246.664 83.6082 249.963 83.2699C253.224 82.9348 256.642 82.69 259.905 83.078C259.934 83.0813 259.934 83.037 259.905 83.0332C253.387 82.2336 246.262 81.892 240.134 84.7466C239.942 84.8363 240.081 85.1035 240.266 85.0702Z" fill="#2D2D2D"/>
<path d="M190.611 51.24C190.581 51.245 190.581 51.2947 190.611 51.3002C192.679 51.6827 194.964 51.4838 197.056 51.3871C199.39 51.2789 201.718 51.0132 204.032 50.6919C206.323 50.3738 208.598 49.9518 210.852 49.4343C213.08 48.9221 215.457 48.4002 217.532 47.4202C217.695 47.3432 217.576 47.1196 217.42 47.1459C215.318 47.5006 213.245 48.1963 211.168 48.6849C208.918 49.214 206.645 49.6529 204.363 50.0125C201.94 50.3951 199.5 50.6323 197.056 50.8285C194.93 50.999 192.712 50.8946 190.611 51.24Z" fill="#2D2D2D"/>
<path d="M209.245 50.7749C208.742 50.9274 208.907 51.7193 209.352 51.4521C209.365 51.4444 209.366 51.4247 209.352 51.4171C209.687 51.2761 210.034 51.2023 210.395 51.1946C210.678 51.1662 210.961 51.1345 211.244 51.099C211.81 51.0318 212.376 50.9651 212.942 50.893C213.95 50.7656 214.994 50.6907 215.97 50.4032C216.204 50.3344 216.153 50.0048 215.917 49.9912C214.789 49.9245 213.637 50.1267 212.517 50.2584C211.437 50.3863 210.289 50.459 209.245 50.7749Z" fill="#2D2D2D"/>
<path d="M268.073 71.7948C268.037 72.0188 268.327 72.0904 268.41 71.8828C270.191 67.3814 270.579 62.3608 271.509 57.6359C272.43 52.9608 273.847 48.1146 276.358 44.028C276.374 44.0029 276.335 43.9717 276.316 43.9958C273.425 47.7107 271.96 51.8786 270.921 56.4406C269.771 61.4918 268.908 66.6835 268.073 71.7948Z" fill="#2D2D2D"/>
<path d="M272.086 69.1849C272.098 69.204 272.11 69.2231 272.122 69.2422C272.139 69.2707 272.176 69.2619 272.187 69.2335C271.631 70.6469 271.238 72.4188 271.246 73.9628C271.247 74.1628 271.506 74.171 271.571 74.0048C271.953 73.0314 272.087 71.9733 272.256 70.9469C272.33 70.4998 272.664 69.4297 272.314 69.0504C272.271 69.0034 272.205 69.0143 272.16 69.0504C272.139 69.0679 272.118 69.0848 272.097 69.1018C272.074 69.1204 272.071 69.1608 272.086 69.1849Z" fill="#2D2D2D"/>
<path d="M301.209 124.527C301.209 123.995 300.857 123.296 300.739 122.764C300.558 121.952 300.478 121.115 300.453 120.284C300.398 118.538 300.899 116.951 301.249 115.268C301.294 115.046 300.994 114.913 300.876 115.117C299.496 117.496 299.527 120.769 300.1 123.378C300.321 124.382 300.334 124.584 299.559 125.323C298.663 126.178 297.535 126.756 296.379 127.165C295.517 127.468 294.295 127.579 294.164 128.727C294.051 129.717 293.996 130.559 293.473 131.447C292.836 132.532 291.607 132.95 290.46 133.213C288.9 133.571 287.421 133.79 285.955 134.473C284.58 135.112 280.552 137.732 281.965 139.64C282.017 139.711 282.132 139.674 282.112 139.58C281.766 137.958 284.373 136.292 285.483 135.591C286.94 134.67 288.563 134.347 290.228 134.052C292.71 133.613 294.599 132.229 294.875 129.598C295.05 127.938 297.592 127.603 298.86 126.82C299.486 126.433 301.209 125.324 301.209 124.527Z" fill="#2D2D2D"/>
<path d="M298.685 130.689C298.378 130.452 298.051 130.24 297.731 130.021C297.693 129.996 297.658 130.057 297.696 130.084C298.014 130.301 298.326 130.529 298.656 130.728C298.679 130.742 298.708 130.706 298.685 130.689Z" fill="#2D2D2D"/>
<path d="M314.404 151.411C312.488 151.111 311.012 149.61 309.804 148.212C307.46 145.5 305.403 142.622 302.88 140.053C301.323 138.468 299.518 136.598 297.215 136.534C297.7 135.671 298.574 134.987 299.441 134.541C300.907 133.784 302.601 133.959 304.16 133.584C304.345 133.54 304.361 133.233 304.16 133.2C301.779 132.808 299 133.382 297.238 135.082C295.268 136.983 296.2 139.236 296.949 141.429C297.687 143.59 296.273 145.027 294.094 145.064C292.318 145.094 290.415 144.222 288.709 143.803C286.748 143.32 283.869 142.563 282.776 144.727C282.052 144.404 280.513 143.664 279.44 143.576C279.65 143.45 279.853 143.296 280.041 143.106C280.157 142.989 280.042 142.738 279.87 142.807C278.765 143.247 277.672 143.351 276.603 142.757C274.823 141.768 275.318 139.544 276.394 138.31C277.705 136.806 278.649 135.787 278.664 133.706C278.665 133.693 278.664 133.678 278.664 133.664C278.893 133.728 279.164 133.609 279.165 133.289C279.168 132.658 278.912 132.203 278.52 131.862C278.343 130.519 278.107 129.197 278.466 127.823C278.563 127.452 278.701 127.113 278.87 126.801C279.021 126.725 279.089 126.583 279.091 126.439C280.402 124.485 283.059 123.664 284.895 122.339C284.911 122.327 284.908 122.294 284.884 122.297C282.375 122.595 279.844 123.953 278.39 126.055C277.629 126.108 277.476 125.028 277.546 124.551C277.634 123.943 278.377 123.361 278.811 122.967C279.416 122.418 279.957 121.898 280.387 121.202C281.09 120.063 281.08 118.633 280.859 117.364C280.315 114.238 277.783 111.782 275.259 110.107C272.379 108.198 268.726 107.251 265.287 107.539C262.464 107.776 259.381 109.348 258.987 112.408C258.806 113.821 259.385 115.246 259.784 116.57C260.315 118.329 260.018 122.342 257.179 120.944C256.101 120.414 255.254 119.642 254.004 120.12C252.953 120.522 252.813 121.794 252.758 122.759C252.64 124.805 252.608 126.488 250.25 127.067C247.611 127.715 245.559 125.27 244.245 123.364C242.294 120.536 240.491 117.172 236.654 116.867C234.532 116.697 232.416 117.21 230.566 118.256C229.666 118.764 229.068 119.577 228.616 120.483C227.95 121.818 227.696 123.485 226.092 124.025C224.851 124.444 223.467 123.657 222.64 122.797C221.451 121.561 221.122 119.63 220.803 118.016C220.64 117.191 220.678 116.545 221.156 115.819C221.328 115.558 221.512 115.237 221.547 114.92C221.661 113.863 221.061 113.097 220.453 112.289C217.765 108.724 214.327 105.028 209.442 106.07C207.653 106.452 206.233 107.664 205.56 109.343C205.264 110.081 205.291 111.135 204.228 111.259C203.544 111.339 202.633 110.92 202.039 110.652C200.6 110.001 199.5 108.808 198.756 107.443C198.604 107.163 198.216 107.123 197.994 107.339C196.583 108.713 194.598 108.77 192.849 108.036C191.13 107.315 190.12 105.628 189.584 103.925C188.65 100.96 190.212 97.7184 188.849 94.919C188.27 93.7275 186.888 92.9558 185.72 92.4814C183.937 91.7577 182.1 91.2658 180.411 90.3181C177.088 88.4544 174.515 83.6842 177.784 80.5153C177.982 80.3235 177.958 80.0234 177.784 79.8294C176.591 78.4996 175.456 77.083 174.8 75.4034C174.493 74.6192 174.383 73.7731 174.498 72.9391C174.626 72.011 175.287 71.324 175.568 70.4594C175.806 69.727 175.882 68.993 175.96 68.2306C176.101 66.8467 176.717 65.7471 177.695 64.7824C179.148 63.3488 181.25 62.6104 183.22 62.0333C183.24 62.4077 183.363 62.7782 183.559 63.0958C184.586 64.7589 187.109 64.7059 188.155 66.3548C186.041 68.5202 184.01 71.2278 185.564 74.3196C185.84 74.8673 186.328 75.2608 186.827 75.5909C188.038 76.3938 187.722 77.0398 187.604 78.1569C187.456 79.5528 188.253 80.541 189.407 81.2252C190.729 82.009 192.465 81.8275 193.93 81.898C194.81 81.9407 196.241 82.0751 195.934 83.6333C195.871 83.9563 195.828 84.2099 195.791 84.5362C195.708 85.2489 195.96 85.9676 196.233 86.6093C197.3 89.1092 200.795 89.7311 202.752 87.9122C203.293 87.4094 203.646 86.8902 203.929 86.2048C204.349 85.1872 204.536 83.9574 205.854 83.841C207.597 83.6869 209.646 85.7091 210.241 87.1782C210.688 88.2817 209.419 89.1267 208.884 89.9514C208.594 90.4012 208.518 90.9598 208.471 91.479C208.351 92.7989 209.111 94.1084 209.878 95.1075C211.85 97.6741 215.521 98.7382 218.619 98.4256C220.561 98.2299 222.458 97.514 224.369 98.2906C226.237 99.0498 225.658 101.351 227.276 102.361C227.526 102.517 227.799 102.625 228.087 102.694C227.022 104.106 226.626 106.379 226.214 107.888C225.924 108.948 225.78 109.982 225.886 111.078C225.996 112.215 226.278 113.273 225.951 114.402C225.913 114.534 226.106 114.611 226.172 114.492C227.106 112.825 226.345 110.808 226.706 108.999C226.929 107.882 227.308 106.776 227.66 105.695C227.988 104.688 228.582 103.767 228.972 102.792C231.051 102.788 233.613 101.38 235.121 100.741C236.037 100.354 236.938 99.9352 237.825 99.4908C239.5 99.2181 241.166 98.8672 242.854 98.6699C243.477 98.5967 244.103 98.5387 244.728 98.4928C242.568 100.58 241.324 103.611 240.97 106.551C240.622 109.445 240.497 113.717 242.61 116.016C242.629 116.037 242.663 116.01 242.649 115.987C241.807 114.625 241.789 112.887 241.637 111.342C241.497 109.907 241.53 108.459 241.674 107.027C242.008 103.689 243.526 100.782 245.898 98.4551C245.907 98.4458 245.914 98Line truncated
<path d="M253.097 99.9534C253.224 99.8212 253.101 99.5566 252.909 99.625C251.074 100.275 249.866 102.132 249.117 103.831C249.093 103.886 249.164 103.927 249.199 103.877C250.296 102.302 251.795 101.309 253.097 99.9534Z" fill="#2D2D2D"/>
<path d="M226.786 102.459C226.106 103.33 225.577 104.308 224.915 105.189C224.894 105.218 224.94 105.251 224.964 105.226C225.764 104.417 226.497 103.614 226.924 102.539C226.954 102.464 226.836 102.396 226.786 102.459Z" fill="#2D2D2D"/>
<path d="M192.593 3.56395C192.293 3.02724 191.894 2.55557 191.55 2.04674C191.204 1.53408 190.945 0.973321 190.615 0.452463C190.558 0.362283 190.42 0.417484 190.443 0.522421C190.699 1.68547 191.454 3.02342 192.425 3.73119C192.528 3.80607 192.648 3.66233 192.593 3.56395Z" fill="#2D2D2D"/>
<path d="M181.327 31.0075C179.838 30.8921 178.331 31.1304 176.847 31.2512C175.55 31.3567 174.011 31.1993 172.84 31.8311C172.824 31.8398 172.82 31.8699 172.84 31.8776C174.064 32.3908 175.558 32.0617 176.847 31.965C178.354 31.8519 179.919 31.7901 181.37 31.3365C181.556 31.278 181.515 31.0222 181.327 31.0075Z" fill="#2D2D2D"/>
<path d="M75.7789 111.66V113.622H52.9503V111.66H57.5986V37.079H52.9503V35.1163H90.757V64.9692H89.3109L88.4845 59.7011C84.9724 39.0416 81.7702 37.079 74.5394 37.079H71.0273V72.6132C75.6757 72.6132 78.9812 67.5516 80.0141 55.0527L80.1174 53.8131H81.5636V93.9957H80.1174L80.0141 91.8265C79.3943 79.4308 75.5724 74.266 71.0273 74.266V111.66H75.7789ZM122.046 85.4221C122.046 107.941 115.332 114.758 107.068 114.758C98.7012 114.758 92.0902 107.941 92.0902 85.4221C92.0902 62.9033 98.7012 56.0857 107.068 56.0857C115.332 56.0857 122.046 62.9033 122.046 85.4221ZM110.684 108.767V62.0769C110.684 59.2879 109.444 57.7384 107.068 57.7384C104.589 57.7384 103.453 59.2879 103.453 62.0769V108.767C103.453 111.66 104.589 113.106 107.068 113.106C109.444 113.106 110.684 111.66 110.684 108.767ZM157.656 85.4221C157.656 107.941 150.941 114.758 142.678 114.758C134.311 114.758 127.699 107.941 127.699 85.4221C127.699 62.9033 134.311 56.0857 142.678 56.0857C150.941 56.0857 157.656 62.9033 157.656 85.4221ZM146.293 108.767V62.0769C146.293 59.2879 145.053 57.7384 142.678 57.7384C140.198 57.7384 139.062 59.2879 139.062 62.0769V108.767C139.062 111.66 140.198 113.106 142.678 113.106C145.053 113.106 146.293 111.66 146.293 108.767ZM192.129 35.1163V111.66H195.228V113.622H181.283V109.18C179.526 113.002 177.461 114.345 174.878 114.345C168.68 114.345 163.309 108.044 163.309 84.8023C163.309 60.734 168.887 56.0857 174.362 56.0857C176.531 56.0857 178.39 56.8087 180.663 60.0109V37.079H176.634V35.1163H192.129ZM180.663 104.429V66.622C180.663 62.4901 178.803 59.0813 176.634 59.0813C174.775 59.0813 174.671 62.1802 174.671 63.4198V105.875C174.671 109.903 175.808 111.04 177.151 111.04C178.7 111.04 180.663 108.871 180.663 104.429Z" fill="#2D2D2D"/>
<path d="M137.661 167.847V131.775C137.661 127.102 136.514 126.528 133.48 126.036V124.478H144.056V126.036C140.285 126.528 139.629 127.184 139.629 131.775V186.784H132.251L118.314 129.151V179.488C118.314 184.079 119.051 184.817 122.741 185.227V186.784H112.739V185.227C115.772 184.817 116.428 184.161 116.428 179.488V126.036H112.739V124.478H127.168L137.661 167.847ZM170.539 164.403C170.539 182.275 165.21 187.686 158.652 187.686C152.011 187.686 146.765 182.275 146.765 164.403C146.765 146.531 152.011 141.121 158.652 141.121C165.21 141.121 170.539 146.531 170.539 164.403ZM161.521 182.931V145.875C161.521 143.662 160.537 142.432 158.652 142.432C156.684 142.432 155.783 143.662 155.783 145.875V182.931C155.783 185.227 156.684 186.375 158.652 186.375C160.537 186.375 161.521 185.227 161.521 182.931ZM185.766 127.102H186.503V142.022H192.734V143.58H186.503V182.275C186.503 184.161 187.241 184.899 188.307 184.899C189.783 184.899 191.668 181.947 191.668 172.438V171.29H193.226C193.226 171.782 193.226 172.848 193.226 172.848C193.226 185.309 190.193 187.686 185.11 187.686C181.011 187.686 177.403 185.145 177.403 179.406V143.58H175.026V142.596C180.109 138.661 184.126 132.184 185.766 127.102Z" fill="#2D2D2D"/>
<path d="M96.4542 240.711C96.4542 256.42 88.1052 263.78 77.2295 263.78H55.8076V261.693H60.7511V182.377H55.8076V180.29H76.4605C87.9953 180.29 95.7951 186.991 95.7951 200.284C95.7951 212.148 90.522 217.311 81.2941 219.948C91.1811 223.573 96.4542 227.528 96.4542 240.711ZM82.3927 213.576V187.98C82.3927 184.135 80.7448 182.377 77.0097 182.377C77.0097 182.377 75.5816 182.377 75.0323 182.377V219.179H77.0097C80.7448 219.179 82.3927 217.421 82.3927 213.576ZM82.3927 256.09V226.649C82.3927 222.914 80.7448 221.156 77.0097 221.156H75.0323V261.693H77.0097C80.7448 261.693 82.3927 259.935 82.3927 256.09ZM134.333 233.79C134.333 257.738 127.192 264.989 118.404 264.989C109.506 264.989 102.475 257.738 102.475 233.79C102.475 209.841 109.506 202.591 118.404 202.591C127.192 202.591 134.333 209.841 134.333 233.79ZM122.249 258.617V208.962C122.249 205.996 120.931 204.348 118.404 204.348C115.767 204.348 114.559 205.996 114.559 208.962V258.617C114.559 261.693 115.767 263.231 118.404 263.231C120.931 263.231 122.249 261.693 122.249 258.617ZM173.961 261.693H175.499V263.78H160.119V261.693H161.657V210.061C161.657 208.084 160.778 206.985 159.46 206.985C158.032 206.985 155.725 209.731 155.725 212.917V261.693H157.373V263.78H140.345V261.693H143.531V205.886H140.345V203.799H155.725V207.864C158.801 203.799 161.327 202.591 164.623 202.591C169.127 202.591 172.203 204.458 173.302 208.303C176.597 204.129 179.564 202.591 182.749 202.591C188.682 202.591 192.087 205.886 192.087 212.368V261.693H195.273V263.78H178.245V261.693H179.783V210.061C179.783 208.084 178.904 206.985 177.586 206.985C176.158 206.985 173.961 209.622 173.961 212.917V261.693ZM201.388 180.29H216.768V207.095C219.294 203.579 221.382 202.591 223.579 202.591C229.401 202.591 235.333 207.534 235.333 233.131C235.333 257.848 229.621 264.549 223.029 264.549C220.173 264.549 217.976 263.121 216.109 259.057V263.78H201.388V261.693C201.608 261.693 204.574 261.693 204.574 261.693V182.377H201.388V180.29ZM216.768 213.796V254.003C216.768 258.727 218.855 261.034 220.503 261.034C221.931 261.034 223.249 259.826 223.249 255.541V210.391C223.249 209.072 223.139 205.777 221.162 205.777C218.745 205.777 216.768 209.402 216.768 213.796ZM267.882 251.586C267.882 259.716 262.938 264.989 255.248 264.989C252.282 264.989 249.975 263.78 246.789 260.265L242.395 264.989H241.296V237.635H242.615L243.054 240.381C245.141 257.299 249.975 262.902 254.15 262.902C256.676 262.902 258.983 261.254 258.983 257.628C258.983 253.015 254.809 245.105 252.172 240.711L247.998 234.009C243.713 227.198 241.626 222.365 241.626 217.092C241.626 208.303 246.24 202.591 252.941 202.591C255.907 202.591 258.214 203.799 260.961 207.205L264.696 202.591H265.904V227.858H264.476L264.146 225.111C262.059 208.303 257.335 204.458 254.479 204.458C253.051 204.458 250.964 206.326 250.964 210.061C250.964 214.455 253.93 219.618 256.347 223.903L261.18 232.362C264.696 238.733 267.882 245.434 267.882 251.586Z" fill="#2D2D2D"/>
<path d="M1.84173 232.561C1.95541 232.693 2.16802 232.553 2.1243 232.396C1.58595 230.456 -0.35593 228.908 -1.6977 227.506C-3.52754 225.594 -5.34371 223.724 -7.39544 222.047C-7.43643 222.014 -7.51186 222.058 -7.47524 222.108C-4.69769 225.885 -1.20909 229.023 1.84173 232.561Z" fill="#2D2D2D"/>
<path d="M-1.93029 224.054C-1.22688 224.922 -0.538233 225.866 0.354823 226.547C0.530811 226.68 0.714997 226.431 0.632468 226.269C0.178289 225.373 -0.545885 224.601 -1.19081 223.837C-1.91116 222.986 -2.63478 222.155 -3.43821 221.381C-3.60053 221.225 -3.88801 221.429 -3.75739 221.627C-3.19335 222.477 -2.57302 223.262 -1.93029 224.054Z" fill="#2D2D2D"/>
<path d="M16.3003 228.659C18.0323 230.43 19.8282 232.137 21.552 233.917C23.2458 235.666 24.6203 237.82 26.4004 239.454C26.589 239.627 26.9809 239.46 26.8688 239.182C26.1255 237.325 24.4864 235.734 23.1501 234.286C22.9228 234.04 22.6888 233.8 22.4582 233.558C21.7521 232.074 20.6174 230.756 19.8632 229.272C19.0844 227.739 18.4318 226.141 17.7043 224.583C16.2981 221.572 14.5994 219.188 11.6973 217.535C11.683 217.527 11.6727 217.547 11.6847 217.555C14.4032 219.54 16.033 222.954 17.3671 225.943C18.0722 227.522 18.7493 229.107 19.5599 230.636C19.0095 230.096 18.4586 229.556 17.9109 229.014C16.2779 227.395 14.6885 225.736 13.2331 223.954C11.7377 222.124 10.7687 219.772 9.15855 218.071C9.13614 218.048 9.09024 218.065 9.09242 218.099C9.21157 220.042 10.6058 221.893 11.7109 223.409C13.0811 225.29 14.6765 226.998 16.3003 228.659Z" fill="#2D2D2D"/>
<path d="M40.605 267.878C40.6256 267.878 40.6256 267.845 40.605 267.845C40.5839 267.845 40.5839 267.878 40.605 267.878Z" fill="#2D2D2D"/>
<path d="M43.5049 268.452C42.5052 268.135 41.4274 268.208 40.4016 268.049C40.3983 268.048 40.3972 268.053 40.3999 268.053C41.428 268.213 42.432 268.61 43.483 268.613C43.5775 268.613 43.5923 268.479 43.5049 268.452Z" fill="#2D2D2D"/>
<path d="M25.6545 260.421C27.3751 260.988 29.2475 261.229 31.026 261.566C32.6028 261.865 34.0533 262.161 35.6618 262.016C35.6771 262.015 35.6826 261.992 35.6656 261.988C32.3831 261.297 29.0825 260.141 25.7354 259.824C25.378 259.79 25.3441 260.319 25.6545 260.421Z" fill="#2D2D2D"/>
<path d="M38.3383 263.74C38.3416 263.74 38.3416 263.735 38.3383 263.735H38.1317C38.1285 263.735 38.1285 263.74 38.1317 263.74H38.3383Z" fill="#2D2D2D"/>
<path d="M52.922 292.441C52.9056 293.122 53.2374 293.792 53.4609 294.425C53.679 295.042 53.8501 295.787 54.249 296.31C54.2562 296.319 54.2747 296.321 54.2775 296.306C54.3972 295.64 54.1386 294.926 53.9971 294.277C53.8539 293.621 53.632 292.839 53.1652 292.341C53.0778 292.247 52.9248 292.322 52.922 292.441Z" fill="#2D2D2D"/>
<path d="M50.7173 285.534C50.6036 285.249 50.1445 285.361 50.2467 285.664C50.4599 286.299 50.6381 286.944 50.855 287.578C51.0327 288.097 51.1748 288.642 51.5448 289.058C51.6738 289.202 51.9033 289.083 51.9077 288.907C51.9213 288.374 51.6874 287.89 51.4961 287.401C51.2496 286.773 50.9665 286.161 50.7173 285.534Z" fill="#2D2D2D"/>
<path d="M68.6378 279.177C68.6045 279.268 68.6914 279.377 68.7859 279.372C69.1991 279.346 69.5839 278.846 69.8621 278.576C70.1681 278.279 70.7606 277.839 70.7398 277.372C70.7344 277.262 70.648 277.158 70.5294 277.162C70.0856 277.181 69.7599 277.689 69.5003 278.006C69.3598 278.179 69.2183 278.35 69.0854 278.527C68.9985 278.643 68.9144 278.759 68.8362 278.881C68.7974 278.942 68.7023 279.013 68.7193 279.058C68.7171 279.058 68.7143 279.057 68.7122 279.059C68.6488 279.111 68.6712 279.086 68.6378 279.177Z" fill="#2D2D2D"/>
<path d="M56.152 285.808C56.1651 285.861 56.2307 285.861 56.258 285.822C56.8248 285.017 57.0483 284.033 57.55 283.178C58.3157 281.873 59.4258 281.472 60.7572 280.955C61.8841 280.516 63.121 279.423 63.1893 278.162C63.198 278.005 62.9963 277.939 62.8925 278.039C62.1722 278.731 61.7437 279.541 60.8659 280.097C60.0149 280.636 58.924 280.659 58.1512 281.352C57.0111 282.375 55.7557 284.211 56.152 285.808Z" fill="#2D2D2D"/>
<path d="M72.6487 263.992C71.5425 265.266 70.8757 267.012 69.911 268.417C68.7562 270.099 67.3969 271.584 66.0787 273.131C65.977 273.25 66.1142 273.468 66.2568 273.362C67.6544 272.326 68.9229 271.022 69.9777 269.64C71.1391 268.117 72.7482 266.178 73.1051 264.257C73.1504 264.012 72.8438 263.767 72.6487 263.992Z" fill="#2D2D2D"/>
<path d="M32.3086 231.875C31.192 230.458 30.6187 229.474 30.4706 227.637C30.2744 225.198 29.6518 222.818 28.7593 220.546C27.8209 218.158 26.3261 216.418 24.7854 214.431C24.7624 214.401 24.722 214.442 24.7444 214.472C26.2414 216.403 27.253 218.987 28.1051 221.257C28.9954 223.628 29.3534 226.082 29.5863 228.59C29.7847 230.725 31.2789 232.04 32.4769 233.69C34.0286 235.827 34.6161 237.996 35.1583 240.524C35.2211 240.816 35.6895 240.819 35.7502 240.524C36.1831 238.42 35.1889 236.276 34.1587 234.491C33.6247 233.565 32.9688 232.713 32.3086 231.875Z" fill="#2D2D2D"/>
<path d="M40.5314 258.28C40.48 258.458 40.5719 258.684 40.8123 258.684C41.1998 258.684 41.2053 258.098 40.8336 258.074C40.5937 256.448 39.8865 254.896 39.3356 253.357C38.6999 251.583 38.1069 249.79 37.4013 248.042C36.1153 244.855 33.5384 242.461 31.826 239.526C31.6877 239.288 31.3483 239.496 31.4631 239.738C32.9792 242.928 35.5136 245.465 36.8652 248.752C37.5123 250.326 38.0435 251.954 38.6218 253.554C39.1924 255.132 39.6373 256.847 40.5314 258.28Z" fill="#2D2D2D"/>
<path d="M60.1894 255.468C60.3217 255.456 60.4206 255.25 60.2665 255.184C58.024 254.227 55.3066 254.232 52.9034 254.081C51.3589 253.984 49.8958 254.179 48.8486 252.824C48.3496 252.179 47.6353 251.851 46.8346 251.917C46.81 251.919 46.8094 251.958 46.8346 251.956C48.2676 251.851 48.3217 253.869 49.3421 254.362C50.1018 254.729 50.8457 254.751 51.683 254.803C54.4616 254.976 57.4217 255.724 60.1894 255.468Z" fill="#2D2D2D"/>
<path d="M61.7684 239.796C61.605 240.967 61.3033 242.129 61.212 243.308C61.1995 243.472 61.4383 243.526 61.5137 243.39C62.1226 242.295 62.1554 241.004 61.9406 239.796C61.9258 239.712 61.7804 239.709 61.7684 239.796Z" fill="#2D2D2D"/>
<path d="M102.523 207.946C102.402 207.341 102.353 206.36 101.993 205.842C101.77 205.521 101.604 205.244 101.178 205.16C99.8189 204.893 99.9653 204.007 99.8495 202.938C99.8293 202.752 99.539 202.605 99.4439 202.828C99.1811 203.445 98.9892 204.446 99.4314 205.045C99.674 205.375 99.7325 205.433 100.073 205.666C100.23 205.736 100.394 205.787 100.564 205.82C101.47 206.084 101.465 206.913 101.628 207.617C101.865 208.636 102.387 210.206 101.551 211.106C101.256 211.424 101.138 211.86 101.049 212.269C100.785 213.481 101.707 215.244 100.796 216.277C100.773 216.303 100.81 216.34 100.833 216.315C101.484 215.624 101.705 214.857 101.73 213.914C101.756 212.903 101.655 212.109 102.432 211.296C102.71 211.005 102.955 210.592 102.905 210.167C102.816 209.419 102.671 208.684 102.523 207.946Z" fill="#2D2D2D"/>
<path d="M48.5354 223.102C48.6677 223.043 48.6524 222.837 48.5354 222.773C48.2179 222.599 47.8659 222.539 47.5587 222.334C47.2188 222.108 46.9668 221.792 46.8417 221.403C46.7531 221.129 46.3706 221.244 46.4176 221.52C46.5646 222.385 47.5609 223.541 48.5354 223.102Z" fill="#2D2D2D"/>
<path d="M62.8596 208.357C62.4333 208.419 61.7802 208.437 61.4922 208.836C60.8112 209.778 60.3925 210.22 59.2076 210.519C58.0888 210.801 56.9198 210.913 56.1223 211.833C55.69 212.331 55.66 213.087 55.5545 213.702C55.2878 215.253 53.1295 215.323 51.9642 215.404C51.9221 215.407 51.9096 215.469 51.9538 215.48C53.0764 215.753 54.0837 215.833 55.1511 215.328C55.5009 215.163 55.901 214.717 56.0059 214.345C56.3524 213.112 56.2874 211.987 57.674 211.456C58.8605 211.001 60.1624 211.06 61.2118 210.263C61.7441 209.858 61.8157 209.227 62.5705 209.062C63.803 208.792 65.2989 208.734 66.4575 209.286C66.5843 209.347 66.7456 209.176 66.6188 209.077C65.4677 208.183 64.2817 208.149 62.8596 208.357Z" fill="#2D2D2D"/>
<path d="M63.7235 216.158C63.7132 216.333 63.9088 216.404 64.0433 216.341C64.4324 216.16 64.61 215.752 64.8456 215.406C65.0746 215.071 65.2675 214.718 65.3708 214.325C65.5452 213.664 65.1948 213.078 64.5384 212.97C64.4925 212.962 64.4674 213.018 64.5067 213.045C65.1079 213.458 64.7882 214.292 64.4942 214.763C64.2143 215.212 63.7563 215.612 63.7235 216.158Z" fill="#2D2D2D"/>
<path d="M45.5084 234.395C45.5756 234.657 45.8314 234.758 46.0757 234.717C47.3951 234.495 47.8121 235.857 48.2832 236.645C48.328 236.721 48.3909 236.757 48.4559 236.768C48.4849 237.358 48.515 237.948 48.51 238.526C48.4942 240.426 48.6352 242.481 48.3936 244.356C48.3887 244.396 48.4587 244.406 48.4652 244.365C49.0058 240.98 50.1114 237.195 49.0866 233.766C49.0074 233.501 48.5439 233.45 48.4991 233.766C48.4428 234.167 48.4177 234.573 48.4106 234.983C47.9602 234.274 47.1546 233.81 46.2834 233.798C45.8014 232.378 44.7356 231.381 43.2474 230.995C42.8566 230.894 41.9766 230.967 42.1116 230.192C42.1832 229.783 42.4833 229.316 42.6472 228.923C42.8757 228.374 42.7943 227.846 42.5172 227.344C42.2586 226.874 41.5749 226.684 41.1136 226.531C39.1417 225.882 40.0823 224.189 40.2031 222.796C40.2299 222.487 40.1763 222.204 40.0659 221.962C40.766 222.076 41.4798 221.979 42.1023 221.541C42.2417 221.443 42.2166 221.148 42.0034 221.176C41.1317 221.292 39.4024 221.659 39.2516 220.447C39.2308 220.279 39.1319 220.158 38.9548 220.149C38.2082 220.113 37.4791 220.143 36.8621 220.599C36.8047 220.642 36.8232 220.73 36.8987 220.734C37.4862 220.767 38.0612 220.694 38.6465 220.72C38.6389 220.866 38.6296 221.011 38.6285 221.157C38.4749 221.181 38.3197 221.228 38.165 221.309C38.1333 221.325 38.1574 221.366 38.1874 221.362C38.3809 221.334 38.5394 221.346 38.6689 221.392C38.6995 221.45 38.7449 221.5 38.8007 221.529C38.8542 221.557 38.9089 221.583 38.9635 221.609C39.6462 222.505 38.5635 225.436 39.1433 226.266C39.9123 227.368 42.333 227.481 41.5219 229.213C41.1257 230.059 40.9617 230.817 41.7208 231.484C42.0521 231.775 42.5893 231.812 43.003 231.883C44.3087 232.107 45.1931 233.169 45.5084 234.395Z" fill="#2D2D2D"/>
<path d="M49.249 185.119C50.9417 185.274 52.0987 184.357 53.2432 183.205C55.2036 181.232 57.0947 179.856 60.0078 179.924C61.1719 179.952 62.2486 180.458 63.4084 180.533C64.1883 180.582 64.995 180.136 65.6596 179.791C67.2145 178.984 68.4246 178.657 70.0916 179.404C71.3934 179.989 72.5335 181.001 73.6665 181.849C75.8614 183.492 78.0564 185.134 80.271 186.751C82.1577 188.127 84.581 187.937 86.6503 188.931C87.6849 189.428 88.4407 190.224 88.7282 191.345C88.9966 192.392 89.2431 192.822 90.2416 193.345C92.6781 194.622 94.984 196.289 97.456 197.458C97.8217 197.631 98.2288 197.183 97.9069 196.874C96.0672 195.107 93.4957 193.884 91.2494 192.717C90.6707 192.417 90.0279 192.233 89.7973 191.513C89.6601 191.085 89.6519 190.652 89.4371 190.243C89.0419 189.49 88.5036 188.947 87.8182 188.451C85.9857 187.125 83.6672 187.354 81.6876 186.479C80.3448 185.885 79.1221 184.703 77.9449 183.838C76.6758 182.907 75.4023 181.983 74.1278 181.059C72.2564 179.701 70.3774 178.046 67.9458 178.04C66.9779 178.037 66.0471 178.607 65.2284 179.053C63.5472 179.97 62.3115 179.406 60.574 179.169C58.6239 178.903 56.6542 179.495 55.0266 180.55C53.1213 181.785 51.7899 184.807 49.249 184.747C49.0058 184.742 49.0146 185.098 49.249 185.119Z" fill="#2D2D2D"/>
<path d="M94.005 208.92C93.8422 210.398 93.4093 211.988 93.5886 213.468C93.6115 213.657 93.8979 213.804 93.9985 213.579C94.4904 212.48 94.5554 211.245 94.7145 210.059C94.9232 208.501 95.2758 206.874 95.1675 205.299C95.0692 203.873 93.4984 202.889 92.4151 204.008C92.2227 204.207 92.4824 204.576 92.7196 204.403C94.0263 203.45 94.3373 205.764 94.2526 206.639C94.1788 207.4 94.0887 208.16 94.005 208.92Z" fill="#2D2D2D"/>
<path d="M76.5375 221.25C77.136 220.057 79.2445 219.853 80.3535 219.296C80.8295 219.057 81.182 218.649 81.4799 218.221C81.9986 217.476 81.6712 216.332 82.4845 215.736C82.5194 215.711 82.4861 215.652 82.4506 215.678C81.3509 216.465 81.0809 217.868 79.9119 218.54C78.8543 219.149 77.5049 219.199 76.5353 219.98C75.4045 220.891 75.4269 222.145 75.1187 223.465C74.67 225.389 71.4809 226.374 70.0172 227.365C69.8101 227.505 69.9369 227.832 70.1883 227.771C71.8153 227.38 73.3834 226.44 74.7006 225.43C76.1751 224.299 75.786 222.747 76.5375 221.25Z" fill="#2D2D2D"/>
<path d="M46.4474 195.679C46.4763 195.653 46.4386 195.606 46.4064 195.626C44.605 196.734 42.9238 197.908 43.1484 200.261C43.3342 202.201 42.1739 202.29 40.819 203.001C40.2834 203.282 39.8883 203.761 39.9025 204.373C39.9074 204.602 40.1845 204.652 40.3075 204.483C41.2317 203.211 43.1255 203.317 43.8529 201.862C44.2716 201.024 43.7715 199.913 43.9305 199.034C44.1628 197.746 45.5013 196.506 46.4474 195.679Z" fill="#2D2D2D"/>
<path d="M36.8054 204.471C36.2298 205.034 35.9593 206.406 36.3107 207.134C36.3698 207.256 36.5774 207.258 36.6359 207.134C36.8092 206.768 36.8294 206.359 36.8693 205.961C36.9158 205.503 37.0857 205.029 37.0404 204.568C37.0289 204.452 36.8972 204.38 36.8054 204.471Z" fill="#2D2D2D"/>
<path d="M65.8096 193.309C65.8637 193.425 66.0446 193.413 66.1014 193.309C66.8688 191.91 65.3467 190.263 64.3055 189.518C64.2667 189.491 64.2219 189.551 64.2552 189.582C64.8307 190.116 65.1723 190.83 65.4062 191.57C65.5893 192.149 65.5522 192.756 65.8096 193.309Z" fill="#2D2D2D"/>
<path d="M74.1219 190.35C74.0486 191.341 74.2935 192.879 75.3401 193.307C75.5494 193.393 75.6948 193.153 75.6669 192.98C75.5986 192.552 75.3297 192.153 75.1876 191.744C75.0297 191.291 74.9777 190.827 75.0012 190.35C75.0291 189.78 74.1634 189.791 74.1219 190.35Z" fill="#2D2D2D"/>
<path d="M90.8203 218.395C90.8285 218.677 90.7612 218.929 90.6913 219.199C90.6213 219.473 90.4738 219.748 90.4885 220.036C90.4967 220.189 90.6252 220.281 90.7733 220.252C91.1192 220.185 91.3619 219.833 91.4783 219.524C91.6297 219.121 91.6171 218.715 91.5466 218.297C91.4761 217.881 90.8082 217.984 90.8203 218.395Z" fill="#2D2D2D"/>
<path d="M85.872 194.894C85.8463 194.904 85.8321 194.936 85.8551 194.958C86.1863 195.278 86.412 195.66 86.5907 196.086C86.7782 196.532 86.964 197.023 87.375 197.311C87.5073 197.404 87.6494 197.252 87.6237 197.119C87.4816 196.398 86.9257 194.471 85.872 194.894Z" fill="#2D2D2D"/>
<path d="M101.219 219.014C101.242 219.014 101.242 218.978 101.219 218.978C101.196 218.978 101.196 219.014 101.219 219.014Z" fill="#2D2D2D"/>
<path d="M101.622 220.229C101.618 220.253 101.654 220.264 101.66 220.239C102.208 217.734 104.579 216.522 106.662 215.51C106.782 215.451 106.707 215.297 106.601 215.285C105.303 215.129 104.13 215.803 103.222 216.706C102.197 217.726 101.858 218.855 101.622 220.229Z" fill="#2D2D2D"/>
<path d="M117.237 208.623C117.193 208.153 117.24 207.713 117.156 207.245C116.926 205.951 115.379 204.849 114.21 204.526C114.033 204.478 113.9 204.704 114.031 204.834C114.589 205.383 115.269 205.77 115.835 206.313C116.81 207.249 116.34 208.448 116.78 209.561C117.006 210.132 117.794 210.413 118.286 210.653C119.282 211.139 120.319 211.572 121.393 211.85C121.407 211.853 121.412 211.835 121.402 211.828C120.527 211.16 119.574 210.626 118.611 210.097C117.965 209.742 117.326 209.585 117.237 208.623Z" fill="#2D2D2D"/>
<path d="M101.018 182.3C101.021 182.276 100.986 182.265 100.98 182.289C100.838 182.846 100.513 183.293 99.9753 183.523C99.9573 183.53 99.9704 183.561 99.9879 183.553C100.509 183.339 100.936 182.869 101.018 182.3Z" fill="#2D2D2D"/>
<path d="M101.423 181.397C100.894 181.461 100.438 181.512 99.9065 181.458C99.7207 181.439 99.5934 181.594 99.5305 181.744C98.5981 183.972 96.0862 185.636 93.6164 185.292C93.5715 185.286 93.5502 185.354 93.5972 185.362C95.2456 185.675 96.5923 185.551 98.0641 184.663C98.7074 184.275 99.2212 183.722 99.6743 183.132C99.8688 182.878 99.9869 182.246 100.421 182.293C100.9 182.345 101.182 182.146 101.563 181.914C101.781 181.782 101.712 181.361 101.423 181.397Z" fill="#2D2D2D"/>
<path d="M134.14 216.058C133.957 215.186 133.616 214.47 133.048 213.777C132.544 213.162 132.019 212.593 131.756 211.823C131.529 211.157 132.085 210.062 132.263 209.406C133.609 204.445 129.154 199.266 126.587 195.511C123.586 191.122 120.338 186.772 116.166 183.422C114.614 182.175 112.488 180.733 110.261 179.919C109.834 179.236 109.542 178.573 109.484 177.71C109.423 176.799 108.824 176.16 108.624 175.242C108.415 174.28 108.067 173.399 107.935 172.41C107.879 171.992 107.761 171.352 107.394 171.067C107.014 170.772 106.672 170.607 106.213 170.463C104.977 170.077 104.529 169.33 104.068 168.205C103.452 166.705 102.779 165.528 101.523 164.463C101.012 164.03 100.441 163.668 99.8783 163.31C98.6694 162.541 97.6113 161.884 97.8217 160.27C97.9878 159 97.3013 157.975 96.358 157.205C95.248 156.299 93.7816 155.998 92.4453 155.597C91.5938 155.341 90.8942 154.978 90.258 154.461C88.8829 152.587 86.6656 151.21 85.197 149.523C84.1739 148.347 83.77 146.767 82.5009 145.787C81.1252 144.726 79.4818 144.188 77.8733 143.582C73.6982 142.009 69.5232 140.435 65.3475 138.864C61.5736 137.444 58.1621 136.198 54.069 136.618C50.0984 137.025 46.1354 137.927 42.2035 138.607C38.4968 139.248 34.465 139.531 31.0452 141.196C27.8824 142.736 25.3907 145.405 22.3016 147.094C22.2201 147.139 22.1781 147.21 22.1633 147.286C21.2189 147.444 20.2936 147.765 19.5661 148.078C18.244 148.648 17.1569 149.722 16.025 150.58C14.6467 151.625 13.048 151.99 11.3444 152.049C8.71445 152.141 6.52935 152.33 4.48582 154.153C3.11016 155.38 1.85365 156.216 0.0615273 156.724C-1.37043 157.131 -2.83243 157.372 -4.24033 157.869C-6.35874 158.617 -8.03501 160.199 -9.18275 162.094C-9.81948 163.147 -10.1813 164.353 -10.3649 165.56C-10.6136 167.194 -10.2933 168.651 -11.6652 169.884C-13.6874 171.699 -15.6795 173.127 -16.6393 175.794C-17.0497 176.936 -16.9759 178.24 -16.6349 179.504C-16.6606 179.501 -16.6874 179.499 -16.7169 179.505C-18.1078 179.793 -19.1927 180.44 -20.0082 181.618C-20.4476 182.253 -20.599 183.033 -20.7285 183.776C-20.9827 185.236 -21.8916 185.616 -22.9246 186.467C-24.1772 187.499 -24.9211 188.83 -25.4381 190.352C-26.1749 192.522 -25.6311 195.224 -27.7227 196.673C-29.0278 197.577 -30.3002 198.072 -31.1309 199.501C-31.9999 200.995 -31.9852 202.929 -32.3525 204.578C-32.7859 206.526 -34.3304 207.735 -35.1983 209.451C-35.9788 210.993 -35.4651 212.798 -35.0163 214.349C-34.7157 215.389 -34.4529 216.389 -34.4698 217.482C-34.4851 218.447 -35.0562 219.312 -35.1737 220.257C-35.3585 221.738 -34.7496 222.765 -33.6144 223.672C-32.265 224.75 -30.6232 225.365 -29.9449 227.085C-29.2033 228.967 -29.7001 230.925 -28.4961 232.682C-27.6943 233.852 -26.3399 234.528 -24.9697 234.723C-23.0071 235.003 -21.1696 233.788 -19.2266 233.745C-17.5875 233.709 -16.4835 234.637 -14.8258 234.014C-13.5076 233.519 -12.4369 232.437 -11.3307 231.604C-10.4092 230.909 -9.41284 230.394 -8.23886 230.693C-8.02242 230.748 -7.86611 230.648 -7.79342 230.502C-2.47061 231.88 2.0832 235.118 6.1976 238.916C6.0719 238.897 5.94401 238.885 5.81174 238.886C5.79917 238.886 5.79261 238.902 5.80136 238.91C7.2508 240.346 8.34662 241.743 9.43807 243.479C10.5088 245.183 11.8451 246.264 13.5694 247.276C18.1123 249.944 20.551 255.001 23.2263 259.376C22.1999 260.023 21.7703 261.188 21.8572 262.429C21.9622 263.932 23.2586 265.072 24.449 265.798C25.1327 266.216 25.8913 266.498 26.6794 266.704C26.798 267.187 26.9193 267.67 27.039 268.153C27.3254 269.305 27.6113 270.457 27.8977 271.609C28.5136 274.084 29.1443 276.556 29.763 279.03C30.1483 280.571 30.1035 281.802 29.3958 283.243C28.9995 284.051 28.6705 284.807 28.4754 285.69C28.1551 287.139 28.3945 288.636 28.9591 289.986C27.6206 290.267 26.5231 291.242 26.0285 292.57C25.2453 294.673 25.9334 296.604 27.2642 298.31C28.8645 300.359 31.4622 301.674 33.8107 302.636C35.839 303.467 38.4875 304.205 40.8087 304.03C40.8136 304.181 40.8377 304.325 40.9634 304.451C41.5793 305.066 43.53 304.729 44.3176 304.755C47.3476 304.853 50.3804 305.015 53.394 304.598C54.4702 304.448 55.4321 304.532 55.7649 303.431C56.0776 302.398 55.8655 301.175 55.3255 300.269C55.1282 299.938 54.6205 300.158 54.7118 300.527C54.9752 301.594 55.5545 303.261 54.1182 303.563C52.7486 303.853 51.3456 303.93 49.9497 303.964C47.3405 304.026 44.716 303.869 42.1073 303.803C42.0789 303.802 42.0554 303.8 42.0302 303.797C42.174 303.65 42.1368 303.348 41.904 303.299C41.7881 303.275 41.6723 303.256 41.5558 303.235C41.5236 303.115 41.4711 302.986 41.3525 302.861C40.571 302.04 39.6353 301.769 39.1991 300.6C38.4558 298.604 37.2157 297.302 35.6663 295.894C35.657 295.886 35.6439 295.899 35.6526 295.907C36.6845 296.949 37.3064 298.249 37.9547 299.545C38.417 300.471 38.5411 301.759 39.4232 302.421C39.6867 302.618 40.078 302.808 40.3775 303.056C39.1275 302.899 37.8689 302.806 36.6287 302.512C34.5715 302.024 32.6357 301.137 30.8403 300.034C29.0432 298.929 27.4194 297.423 26.7925 295.357C26.199 293.4 27.5774 290.937 29.7144 290.919C30.1144 290.915 30.3292 290.493 30.1538 290.153C29.0481 288.014 29.217 285.867 30.2779 283.758C31.1299 282.065 31.166Line truncated
<path d="M2.21192 199.364C2.15945 199.375 2.17421 199.452 2.22394 199.455C3.95649 199.526 5.69834 199.364 7.40356 199.749C9.41375 200.202 9.85044 201.974 10.9818 203.384C11.8836 204.508 13.3751 204.851 14.8841 205.166C14.7617 205.893 14.648 206.471 14.1031 207.038C13.7796 207.375 13.5839 208.022 13.5806 208.47C13.5653 210.513 14.2709 212.858 12.1159 214.159C12.1044 214.166 12.1121 214.185 12.1252 214.181C13.4697 213.801 14.1403 212.782 14.3862 211.439C14.5868 210.341 14.1873 209.258 14.3813 208.157C14.6704 206.518 14.9147 207.248 15.1847 207.141C16.3035 208.843 17.3354 210.613 18.317 212.39C18.3279 212.409 18.3574 212.396 18.3525 212.375C17.7879 210.136 16.8391 208.159 15.5312 206.249C15.54 206.181 15.5487 206.114 15.5553 206.045C16.1193 206.788 16.8025 207.481 17.2556 208.303C18.0891 209.817 18.6291 211.494 19.2581 213.099C20.5841 216.483 22.0051 219.889 24.0694 222.882C24.0825 222.901 24.1175 222.889 24.1087 222.865C22.8353 219.108 21.0825 215.525 19.6741 211.808C19.1122 210.324 18.5569 208.85 17.8049 207.45C17.4808 206.847 17.0561 206.111 16.521 205.538C17.1747 205.716 17.7896 205.945 18.3115 206.293C18.481 206.407 18.6968 206.184 18.5892 206.016C17.8535 204.866 16.6861 204.508 15.3979 204.228C14.6021 204.054 13.8113 203.909 13.0357 203.656C11.8131 203.259 11.2245 202.279 10.6216 201.22C8.89836 198.193 5.0955 198.777 2.21192 199.364Z" fill="#2D2D2D"/>
<path d="M20.7924 206.023C20.7192 206.053 20.6722 206.151 20.7421 206.213C21.2149 206.636 21.8921 206.76 22.4774 206.976C23.3754 207.309 24.2105 207.757 24.9986 208.297C25.009 208.407 25.0199 208.518 25.0374 208.627C25.0828 208.917 25.3752 209.436 25.6463 209.564C26.0475 209.753 26.1589 209.799 26.5913 209.906C27.5745 210.151 27.169 211.425 27.0241 211.987C26.8383 212.709 26.6601 213.814 27.222 214.433C27.5505 214.795 27.8259 214.913 28.3069 214.996C29.6175 215.223 29.2682 216.439 29.3306 217.412C29.3409 217.576 29.5 217.706 29.6519 217.714C30.0318 218.445 30.8084 219.009 31.6151 219.111C32.1158 219.174 32.635 219.09 32.8383 219.674C32.9137 219.892 33.1657 220.172 33.3755 220.27C34.1423 220.627 34.7632 220.508 35.4464 220.028C35.5503 219.955 35.5136 219.796 35.3819 219.789C35.0928 219.768 34.8031 219.76 34.5135 219.765C32.9869 219.755 33.6986 219.361 33.3608 218.785C33.1946 218.5 32.9307 218.441 32.6388 218.398C31.9955 218.304 31.5539 218.393 30.9516 218.06C30.5504 217.838 30.2712 217.487 30.0465 217.1C30.228 216.431 30.2455 215.612 29.8913 215.051C29.6563 214.679 29.4415 214.464 29.0496 214.259C28.8911 214.184 28.7223 214.144 28.5424 214.142C27.4767 213.947 27.756 212.833 27.9035 212.229C28.0861 211.481 28.4413 210.155 27.7286 209.537C27.2837 209.151 26.1027 208.841 25.9196 208.384C25.4255 207.148 26.4612 205.548 27.2214 204.68C28.3566 203.386 30.0449 203.111 31.2626 202.031C31.702 201.64 31.8075 201.37 31.907 200.785C32.1136 199.572 31.9097 198.842 32.7738 197.797C33.7947 196.561 35.5891 196.88 36.774 195.912C37.8162 195.061 38.3606 193.976 38.336 192.623C38.3305 192.305 37.8097 192.209 37.7621 192.545C37.6211 193.541 37.2347 194.323 36.5247 195.047C35.8 195.785 34.6331 195.861 33.6964 196.155C32.6557 196.483 31.8955 197.363 31.4085 198.294C31.1937 198.705 31.0445 199.235 31.0309 199.699C30.985 201.258 30.3122 201.562 29.1813 202.153C27.7609 202.894 26.5459 203.894 25.6818 205.248C25.2965 205.851 25.0784 206.53 25.003 207.227C23.8088 206.469 22.0216 205.53 20.7924 206.023Z" fill="#2D2D2D"/>
<path d="M33.5442 159.847C32.8971 162.399 32.2636 164.519 33.4015 167.051C33.965 168.305 35.2396 168.974 35.8911 170.167C36.6857 171.623 37.2405 173.183 38.2532 174.516C38.3642 174.661 38.6047 174.582 38.5582 174.387C38.0734 172.343 37.0661 169.727 35.5178 168.264C33.2031 166.075 33.1796 162.846 33.5677 159.851C33.5699 159.837 33.5475 159.834 33.5442 159.847Z" fill="#2D2D2D"/>
<path d="M41.8899 175.062C43.0371 176.049 44.2876 176.908 45.5381 177.757C46.7044 178.549 48.0642 179.693 49.4721 179.935C49.7126 179.976 49.8635 179.725 49.705 179.535C48.8813 178.553 47.516 177.995 46.4432 177.306C45.044 176.407 43.7137 175.413 42.3321 174.489C41.9888 174.259 41.5756 174.792 41.8899 175.062Z" fill="#2D2D2D"/>
<path d="M91.5321 166.405C91.5109 166.405 91.5109 166.438 91.5321 166.438C91.5527 166.438 91.5527 166.405 91.5321 166.405Z" fill="#2D2D2D"/>
<path d="M91.2161 169.082C90.9406 170.132 90.9275 171.383 91.1554 172.441C91.1904 172.603 91.4112 172.578 91.4582 172.441C91.8222 171.391 91.7522 170.074 91.256 169.077C91.2472 169.06 91.2205 169.065 91.2161 169.082Z" fill="#2D2D2D"/>
<path d="M-0.353915 171.575C-0.698786 173.589 -0.961128 175.394 0.804762 176.793C1.59397 177.419 2.52966 177.8 3.27132 178.495C4.13979 179.308 3.79929 180.442 3.71785 181.517C3.55717 181.808 3.38227 182.091 3.16912 182.353C2.55371 183.11 1.51691 183.378 0.627681 183.659C0.592702 183.67 0.597075 183.728 0.636426 183.725C1.81369 183.654 2.98165 183.451 3.77852 182.555C3.7971 182.628 3.81732 182.701 3.84465 182.771C3.92062 182.967 4.23762 183.016 4.29173 182.771C4.39393 182.312 4.48903 181.841 4.5497 181.372C4.68196 181.129 4.8104 180.884 4.94157 180.638C5.31486 179.942 5.82479 179.553 6.37024 179.017C6.49923 178.891 6.39975 178.681 6.22868 178.675C5.48046 178.648 4.96562 179.248 4.58522 179.877C4.49777 179.081 4.20264 178.339 3.53039 177.73C2.85978 177.122 1.97164 176.752 1.22287 176.251C-0.291609 175.236 -0.402558 173.229 -0.292156 171.583C-0.28997 171.548 -0.347903 171.54 -0.353915 171.575Z" fill="#2D2D2D"/>
<path d="M8.31752 185.006C8.30441 184.975 8.25795 184.968 8.24756 185.006C8.03824 185.786 8.26287 186.599 8.78482 187.221C9.11329 187.611 9.4926 187.905 9.91398 188.188C10.3436 188.477 10.7365 188.888 11.2066 189.097C11.3951 189.181 11.5891 189.046 11.5476 188.835C11.4121 188.149 10.6256 187.784 10.0878 187.43C9.17341 186.829 8.72743 185.989 8.31752 185.006Z" fill="#2D2D2D"/>
<path d="M14.9743 172.356C14.9448 172.398 14.9935 172.44 15.0344 172.434C16.4762 172.209 17.783 171.088 19.0341 170.372C20.5945 169.479 22.2439 169.033 23.9142 168.426C24.1525 168.339 24.1055 168.017 23.8557 167.994C22.1505 167.84 20.5813 168.645 19.1144 169.431C17.7322 170.172 15.8849 171.048 14.9743 172.356Z" fill="#2D2D2D"/>
<path d="M32.33 179.928C33.3291 180.183 32.9739 181.082 33.1078 181.739C33.2389 182.385 33.5013 182.974 33.7057 183.597C34.079 184.735 32.9843 185.699 33.5947 186.82C33.7057 187.024 33.9757 186.909 33.9954 186.712C34.1478 185.183 34.8431 184.056 34.0916 182.591C33.604 181.64 33.5226 180.163 32.3442 179.877C32.3109 179.869 32.2967 179.92 32.33 179.928Z" fill="#2D2D2D"/>
<path d="M38.7288 183.938C38.4282 184.809 38.2905 185.739 38.0834 186.637C37.8724 187.547 37.5729 188.429 37.3712 189.343C37.334 189.51 37.5461 189.594 37.6407 189.457C38.6758 187.954 39.2486 185.738 38.7709 183.938C38.7649 183.916 38.7354 183.919 38.7288 183.938Z" fill="#2D2D2D"/>
<path d="M43.5031 141.611C39.1051 141.735 38.7001 147.388 36.7352 150.278C36.7199 150.301 36.7555 150.329 36.773 150.308C37.9781 148.893 38.9509 147.728 39.709 145.988C40.0091 145.299 40.3102 144.628 40.7064 143.988C41.3705 142.912 42.5844 142.581 43.596 141.953C43.7463 141.86 43.6911 141.606 43.5031 141.611Z" fill="#2D2D2D"/>
<path d="M48.0806 145.049C46.3617 145.888 44.6718 148.619 44.7986 150.531C44.8095 150.696 45.0341 150.78 45.1227 150.619C45.6075 149.744 45.8485 148.774 46.3049 147.881C46.8175 146.879 47.6182 146.113 48.203 145.172C48.2533 145.09 48.1566 145.012 48.0806 145.049Z" fill="#2D2D2D"/>
<path d="M73.6289 152.588C73.5978 151.922 73.642 150.923 72.9807 150.494C72.4281 150.136 71.8636 150.121 71.3821 150.586C71.1301 150.829 71.5089 151.204 71.7652 150.969C72.9015 149.923 72.8042 153.011 72.8342 153.175C72.9506 153.811 73.1529 154.216 73.5808 154.688C74.5952 155.809 76.2961 155.966 76.8606 157.547C76.9147 157.698 77.1858 157.744 77.2148 157.547C77.5886 154.986 73.7426 155.028 73.6289 152.588Z" fill="#2D2D2D"/>
<path d="M85.3513 170.161C84.3058 172.259 81.7184 173.029 80.1318 174.626C79.6361 175.126 79.3388 175.754 79.1327 176.415C78.8725 177.251 78.9955 177.369 78.3533 177.804C77.8467 178.147 77.4062 178.468 76.9831 178.905C76.8219 179.072 76.9815 179.415 77.2253 179.321C77.9549 179.04 79.0245 178.6 79.5213 177.986C80.1646 177.191 79.9842 176.062 80.6909 175.186C81.4451 174.252 82.6377 173.666 83.5701 172.919C84.5457 172.137 84.8441 171.261 85.3928 170.186C85.4071 170.158 85.365 170.133 85.3513 170.161Z" fill="#2D2D2D"/>
<path d="M84.1728 176.734C83.7191 176.4 83.0381 175.688 82.4386 175.726C82.3375 175.732 82.2156 175.778 82.1708 175.879C82.0746 176.098 82.08 176.329 82.3233 176.442C82.4495 176.5 82.5643 176.457 82.6342 176.377C82.6987 176.767 83.4961 177.191 83.7814 177.403C84.299 177.788 84.7598 178.181 85.3943 178.342C85.555 178.383 85.7025 178.207 85.6124 178.058C85.2582 177.474 84.7138 177.134 84.1728 176.734Z" fill="#2D2D2D"/>
<path d="M107.508 194.622C107.968 194.981 108.347 195.507 108.904 195.711C109.079 195.775 109.214 195.586 109.183 195.431C109.058 194.804 108.442 194.398 107.965 194.03C107.349 193.555 106.602 193.23 105.874 192.97C105.478 192.828 105.318 193.448 105.702 193.594C106.358 193.845 106.954 194.191 107.508 194.622Z" fill="#2D2D2D"/>
<path d="M114.886 218.302C114.815 218.462 114.931 218.691 115.134 218.625C115.933 218.361 116.453 217.445 116.542 216.642C116.581 216.295 116.643 215.875 116.336 215.635C116.154 215.494 115.914 215.533 115.801 215.734C115.745 215.834 115.786 215.931 115.87 215.996C115.995 216.091 115.551 217.07 115.499 217.187C115.325 217.575 115.056 217.913 114.886 218.302Z" fill="#2D2D2D"/>
<path d="M120.627 198.033C120.77 198.16 121.009 198.079 121.01 197.874C121.013 197.115 120.452 196.404 120.024 195.817C119.533 195.143 118.915 194.575 118.271 194.051C118.032 193.857 117.694 194.174 117.926 194.396C118.489 194.938 118.96 195.538 119.41 196.175C119.827 196.764 120.083 197.555 120.627 198.033Z" fill="#2D2D2D"/>
<path d="M46.4928 152.188C46.3026 153.527 46.1643 155.011 46.5933 156.31C46.6431 156.461 46.8803 156.482 46.9196 156.31C47.3322 154.498 47.1705 152.548 47.4727 150.701C47.5902 149.983 48.3128 148.505 47.711 147.892C47.6498 147.83 47.5629 147.841 47.5006 147.892C47.3907 147.981 47.3989 147.989 47.4 148.134C47.4006 148.158 47.417 148.183 47.441 148.188C47.4662 148.193 47.4907 148.198 47.5159 148.203C47.5214 148.204 47.5257 148.2 47.5306 148.198C47.4399 148.421 46.9453 149.813 46.9065 149.956C46.7081 150.687 46.5988 151.44 46.4928 152.188Z" fill="#2D2D2D"/>
<path d="M29.5108 149.29C30.018 148.249 30.5635 147.263 30.9832 146.182C31.0313 146.058 30.8504 145.964 30.7651 146.055C29.3004 147.617 28.0597 150.114 27.957 152.282C27.9559 152.31 27.9931 152.316 28.0067 152.295C28.6178 151.367 29.0244 150.289 29.5108 149.29Z" fill="#2D2D2D"/>
<path d="M19.4487 154.503C19.8668 154.123 20.2576 153.673 20.5909 153.216C20.909 152.779 21.2424 152.011 21.7627 151.803C21.9529 151.727 21.959 151.484 21.7627 151.41C21.2348 151.211 20.8287 151.783 20.5489 152.142C20.0286 152.811 19.5241 153.515 19.1716 154.289C19.0896 154.468 19.294 154.643 19.4487 154.503Z" fill="#2D2D2D"/>
<path d="M13.7147 155.493C13.765 155.366 13.5863 155.287 13.5005 155.368C12.1953 156.607 11.698 158.23 10.724 159.683C10.7207 159.687 10.7289 159.692 10.7322 159.687C11.7286 158.232 13.0403 157.177 13.7147 155.493Z" fill="#2D2D2D"/>
<path d="M7.34083 222.472C7.87535 223.32 8.42299 224.158 8.9526 225.009C9.58714 226.029 10.4671 226.943 10.9333 228.055C10.9437 228.079 10.9847 228.07 10.9754 228.043C10.3058 226.08 9.27943 224.432 8.24154 222.642C7.3228 221.057 6.42263 218.753 4.8617 217.714C4.73107 217.627 4.51628 217.773 4.58132 217.93C5.2492 219.532 6.41498 221.004 7.34083 222.472Z" fill="#2D2D2D"/>
<path d="M4.30897 212.933C4.79758 213.681 5.49552 215.349 6.60337 214.309C6.72033 214.199 6.6378 213.957 6.46509 213.974C5.09873 214.115 5.17032 212.049 4.03351 211.704C3.47002 211.532 2.91309 211.532 2.33211 211.446C1.60739 211.337 0.857531 210.771 0.205501 210.453C0.188558 210.444 0.168882 210.461 0.17872 210.479C0.561302 211.141 0.981596 211.632 1.66369 211.991C2.53761 212.451 3.66951 211.955 4.30897 212.933Z" fill="#2D2D2D"/>
<path d="M3.87052 188.233C3.84155 188.165 3.74809 188.121 3.67814 188.123C3.6448 188.123 3.612 188.124 3.57867 188.125C3.43328 188.129 3.33327 188.218 3.28462 188.349C2.79383 189.67 4.23835 191.185 5.41779 191.504C5.55443 191.541 5.6938 191.383 5.60854 191.256C5.32215 190.829 4.95815 190.51 4.59852 190.148C4.3717 189.919 4.18752 189.659 4.03612 189.375C3.98092 189.25 3.93665 189.121 3.90277 188.989C3.90605 189 3.89074 188.742 3.84046 188.556C3.93009 188.479 3.92682 188.366 3.87052 188.233Z" fill="#2D2D2D"/>
<path d="M52.3447 171.196C53.0913 171.438 54.5134 171.524 54.9774 170.751C55.0523 170.626 54.9889 170.418 54.8315 170.392C54.1494 170.281 53.4897 170.611 52.7689 170.475C51.8791 170.307 51.221 169.723 50.6718 169.043C50.4936 168.822 50.0995 169.059 50.1952 169.32C50.551 170.291 51.3866 170.887 52.3447 171.196Z" fill="#2D2D2D"/>
<path d="M63.207 168.338C64.5936 168.285 66.1474 168.713 67.2662 167.681C67.3985 167.559 67.2575 167.367 67.1012 167.398C65.6266 167.697 64.2444 167.6 62.7567 167.676C62.2828 167.7 61.7395 167.839 61.3236 168.071C60.5371 168.511 59.8315 168.736 58.9609 168.907C58.933 168.912 58.927 168.957 58.9532 168.969C60.3398 169.62 61.7964 168.393 63.207 168.338Z" fill="#2D2D2D"/>
<path d="M71.2933 164.732C72.6339 165.222 74.5949 165.584 76.0072 165.271C76.1734 165.234 76.1641 164.987 76.0072 164.944C75.2929 164.752 74.5447 164.767 73.8101 164.723C72.9794 164.672 72.1524 164.539 71.3211 164.528C71.1976 164.526 71.1872 164.693 71.2933 164.732Z" fill="#2D2D2D"/>
<path d="M24.9032 178.344C25.7351 177.87 26.1608 176.766 26.5625 175.94C26.9795 175.082 27.5173 174.072 27.5458 173.108C27.5512 172.913 27.302 172.875 27.2036 173.015C26.5997 173.875 26.3308 175.001 25.8613 175.946C25.488 176.697 24.8901 177.441 24.8048 178.287C24.7999 178.338 24.8639 178.366 24.9032 178.344Z" fill="#2D2D2D"/>
<path d="M44.6541 188.449C45.2592 187.939 45.9937 187.493 46.4244 186.817C46.5069 186.687 46.3714 186.527 46.2304 186.565C45.5002 186.76 44.9066 187.321 44.3431 187.803C43.8917 188.189 42.9265 188.823 42.9527 189.487C42.9538 189.517 42.9751 189.536 43.0036 189.538C43.596 189.581 44.2355 188.803 44.6541 188.449Z" fill="#2D2D2D"/>
<path d="M54.1713 236.754C54.7063 235.302 54.9326 233.736 55.3376 232.242C55.8262 230.442 56.4777 228.687 57.2193 226.976C58.5316 223.951 60.6855 221.372 61.988 218.375C62.0071 218.331 61.9617 218.261 61.912 218.299C59.3509 220.244 57.7708 223.687 56.4738 226.541C55.1053 229.554 53.5668 233.33 53.7012 236.691C53.7127 236.969 54.0789 237.004 54.1713 236.754Z" fill="#2D2D2D"/>
<path d="M65.7772 233.322C66.2189 232.957 66.8889 232.477 66.9277 231.858C66.9348 231.738 66.8124 231.606 66.6883 231.619C66.162 231.676 65.7986 232.163 65.4269 232.5C64.9651 232.92 64.4158 233.197 63.9316 233.575C63.99 231.848 64.7918 229.73 65.1099 228.596C65.6248 226.762 66.5058 224.862 66.5211 222.94C66.5216 222.898 66.4708 222.891 66.449 222.92C65.1974 224.604 60.4583 237.117 65.1443 236.744C65.5045 236.716 65.5641 236.252 65.228 236.127C64.3453 235.8 63.9955 235.025 63.9376 234.073C63.9704 234.09 64.0053 234.102 64.0425 234.103C64.6738 234.121 65.3072 233.71 65.7772 233.322Z" fill="#2D2D2D"/>
</g>
<defs>
<clipPath id="clip0_22877_2962">
<path d="M0 23.0933C0 10.3392 10.3392 0 23.0933 0H281.907C294.661 0 305 10.3392 305 23.0933V281.907C305 294.661 294.661 305 281.907 305H23.0933C10.3392 305 0 294.661 0 281.907V23.0933Z" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 94 KiB

+7 -1
View File
@@ -38,7 +38,13 @@ export default {
},
useCardStyle: {
control: "boolean",
description: "When true, wraps the rule body in a white card with a teal bar",
description:
"When true, wraps the rule body in a white card with a left accent bar",
},
cardAccentColor: {
control: "text",
description:
"Accent bar color when useCardStyle is true (match page background)",
},
},
};
+27 -3
View File
@@ -91,11 +91,35 @@ describe("AskOrganizer (behavioral tests)", () => {
variant="use-case-detail"
/>,
);
expect(
container.querySelector('[data-figma-node="22015-42624"]'),
).toBeInTheDocument();
const section = container.querySelector('[data-figma-node="22015-42624"]');
expect(section).toBeInTheDocument();
expect(section).toHaveClass("py-[var(--spacing-scale-032)]");
expect(section).not.toHaveClass("py-[var(--spacing-scale-096)]");
expect(
screen.getByRole("heading", { name: "Still have questions?" }),
).toBeInTheDocument();
});
it("centered variant uses baseline section padding per Figma 17487-12288", () => {
const { container } = render(
<AskOrganizer
title="Still have questions?"
subtitle="Get answers from an experienced organizer"
variant="centered"
/>,
);
const section = container.querySelector('[data-figma-node="18116-15960"]');
expect(section).toHaveClass(
"py-[var(--spacing-scale-040)]",
"px-[var(--spacing-scale-032)]",
);
expect(section).not.toHaveClass("py-[var(--spacing-scale-096)]");
});
it("does not force 358px min-width below md (fits 320px baseline)", () => {
const { container } = render(<AskOrganizer title="Test Title" />);
const inner = container.querySelector("section > div");
expect(inner).toHaveClass("min-w-0", "md:min-w-[358px]");
expect(inner?.className).not.toContain(" min-w-[358px]");
});
});
+17 -1
View File
@@ -1,4 +1,5 @@
import { describe } from "vitest";
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import {
componentTestSuite,
type ComponentTestSuiteConfig,
@@ -34,4 +35,19 @@ const config: ComponentTestSuiteConfig<Props> = {
describe("CommunityRule", () => {
componentTestSuite<Props>(config);
it("uses cardAccentColor for the card left border when useCardStyle is true", () => {
const { container } = render(
<CommunityRule
sections={sampleSections}
useCardStyle
cardAccentColor="var(--color-surface-invert-brand-lavender)"
/>,
);
const root = container.firstElementChild as HTMLElement;
expect(root.style.boxShadow).toBe(
"inset 4px 0 0 0 var(--color-surface-invert-brand-lavender)",
);
expect(screen.getByText("How proposals pass")).toBeInTheDocument();
});
});
+33 -6
View File
@@ -62,6 +62,28 @@ describe("ContentBanner", () => {
expect(screen.getByText("Test description")).toBeInTheDocument();
});
it("renders useCase variant rule preview as link when href is set", () => {
const { container } = render(
<ContentBanner
post={mockPost}
variant="useCase"
rulePreview={{
title: "Sample Operating Manual",
description: "Governance preview for the case study.",
backgroundColor: "bg-[var(--color-surface-invert-brand-lavender)]",
iconPath: "assets/case-study/case-study-mutual-aid.svg",
href: "/use-cases/mutual-aid-colorado/rule",
}}
/>,
);
const link = screen.getByRole("link", {
name: /view sample operating manual community rule/i,
});
expect(link).toHaveAttribute("href", "/use-cases/mutual-aid-colorado/rule");
expect(container.querySelector(".pointer-events-none")).toBeNull();
});
it("renders useCase variant with ContentContainer copy and rule preview", () => {
const { container } = render(
<ContentBanner
@@ -76,13 +98,18 @@ describe("ContentBanner", () => {
/>,
);
expect(
screen.getByRole("heading", { name: "Test Article" }),
).toBeInTheDocument();
const title = screen.getByRole("heading", { name: "Test Article" });
expect(title).toBeInTheDocument();
expect(title).toHaveClass("sm:text-[24px]", "md:text-[32px]");
expect(screen.getByText("Sample Operating Manual")).toBeInTheDocument();
expect(
container.querySelector('[data-figma-node="22015:42621"]'),
).toBeInTheDocument();
const copyColumn = container.querySelector('[data-node-id="19189:9171"]');
expect(copyColumn).toHaveClass("lg:max-w-[365px]");
expect(copyColumn).not.toHaveClass("max-w-[365px]");
const bannerRow = container.querySelector(
'[data-figma-node="22015:42621"]',
);
expect(bannerRow).toBeInTheDocument();
expect(bannerRow).toHaveClass("lg:grid-cols-2");
});
it("renders guide variant with left-aligned copy and logo mark", () => {
+129 -2
View File
@@ -1,6 +1,10 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
import {
renderWithProviders as render,
screen,
waitFor,
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import CreateFlowTopNav from "../../app/components/navigation/CreateFlowTopNav";
@@ -150,6 +154,25 @@ describe("CreateFlowTopNav (behavioral tests)", () => {
expect(handler).toHaveBeenCalledTimes(1);
});
it("renders Duplicate button when hasDuplicate is true", () => {
render(
<CreateFlowTopNav
hasDuplicate={true}
duplicateLabel="Duplicate"
duplicateAriaLabel="Duplicate"
onDuplicate={vi.fn()}
/>,
);
expect(
screen.getByRole("button", { name: "Duplicate" }),
).toBeInTheDocument();
});
it("uses exitLabel override when provided", () => {
render(<CreateFlowTopNav exitLabel="Return" />);
expect(screen.getByRole("button", { name: "Return" })).toBeInTheDocument();
});
it("calls onExit when Exit button is clicked", async () => {
const user = userEvent.setup();
const handleExit = vi.fn();
@@ -161,3 +184,107 @@ describe("CreateFlowTopNav (behavioral tests)", () => {
expect(handleExit).toHaveBeenCalledTimes(1);
});
});
describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
const defaultInnerWidth = 1200;
beforeEach(() => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 320,
});
});
afterEach(() => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: defaultInnerWidth,
});
});
const completedScreenProps = {
hasShare: true,
hasExport: true,
hasEdit: true,
saveDraftOnExit: true,
onShare: vi.fn(),
onSelectExportFormat: vi.fn(),
onEdit: vi.fn(),
onExit: vi.fn(),
} as const;
it("collapses secondary actions into a kebab menu", async () => {
render(<CreateFlowTopNav {...completedScreenProps} />);
await waitFor(() => {
expect(
screen.getByRole("button", { name: "More options" }),
).toBeInTheDocument();
});
expect(
screen.queryByRole("button", { name: "Share" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Export" }),
).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Edit" })).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Save & Exit" }),
).not.toBeInTheDocument();
});
it("opens kebab menu with share, export formats, edit, and save & exit", async () => {
const user = userEvent.setup();
render(<CreateFlowTopNav {...completedScreenProps} />);
const kebab = await screen.findByRole("button", { name: "More options" });
await user.click(kebab);
expect(screen.getByRole("menuitem", { name: "Share" })).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Download PDF" }),
).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Download CSV" }),
).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Download Markdown" }),
).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Edit" })).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Save & Exit" }),
).toBeInTheDocument();
});
it("invokes handlers from kebab menu items", async () => {
const user = userEvent.setup();
const handleShare = vi.fn();
const handleEdit = vi.fn();
const handleExit = vi.fn();
render(
<CreateFlowTopNav
{...completedScreenProps}
onShare={handleShare}
onEdit={handleEdit}
onExit={handleExit}
/>,
);
const kebab = await screen.findByRole("button", { name: "More options" });
await user.click(kebab);
await user.click(screen.getByRole("menuitem", { name: "Share" }));
expect(handleShare).toHaveBeenCalledTimes(1);
await user.click(kebab);
await user.click(screen.getByRole("menuitem", { name: "Edit" }));
expect(handleEdit).toHaveBeenCalledTimes(1);
await user.click(kebab);
await user.click(screen.getByRole("menuitem", { name: "Save & Exit" }));
expect(handleExit).toHaveBeenCalledTimes(1);
});
});
+33 -2
View File
@@ -702,17 +702,48 @@ function FinalReviewEditPublishedWithStateProbe({
return <FinalReviewScreen variant="editPublished" />;
}
describe("FinalReviewScreen — edit published description", () => {
it("does not expose click-to-edit description on default final review", () => {
describe("FinalReviewScreen — edit published title and description", () => {
it("does not expose click-to-edit title or description on default final review", () => {
render(
<FinalReviewWithFlowState
title="Oak"
communityContext="Visible body"
/>,
);
expect(screen.queryByTestId("rule-title-edit")).not.toBeInTheDocument();
expect(screen.queryByTestId("rule-description-edit")).not.toBeInTheDocument();
});
it("opens Save modal from title click and updates title", async () => {
let latest: CreateFlowState = {};
render(
<FinalReviewEditPublishedWithStateProbe
onState={(s) => {
latest = s;
}}
initial={{
title: "Oak Park Commons",
communityContext: "Original",
selectedCommunicationMethodIds: ["signal"],
}}
/>,
);
fireEvent.click(await screen.findByTestId("rule-title-edit"));
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByText(/Community name/i)).toBeInTheDocument();
const input = within(dialog).getByRole("textbox");
fireEvent.change(input, { target: { value: "Renamed Commons" } });
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
await waitFor(() => {
expect(latest.title).toBe("Renamed Commons");
});
});
it("opens Save modal from description click and updates communityContext + summary", async () => {
let latest: CreateFlowState = {};
render(
+21
View File
@@ -1,4 +1,6 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import SectionHeader from "../../app/components/type/SectionHeader";
import { componentTestSuite } from "../utils/componentTestSuite";
@@ -21,3 +23,22 @@ componentTestSuite<SectionHeaderProps>({
errorState: false,
},
});
describe("SectionHeader twoColumnsFromMd", () => {
it("splits rule stack header at md when twoColumnsFromMd is set", () => {
const { container } = render(
<SectionHeader
title="Popular templates"
subtitle="Start from a proven pattern."
variant="multi-line"
ruleStackDesktopTypeScale
twoColumnsFromMd
/>,
);
expect(
screen.getByRole("heading", { name: /popular templates/i }),
).toBeInTheDocument();
expect(container.firstElementChild).toHaveClass("md:flex-row");
});
});
@@ -66,9 +66,10 @@ describe("TripleTextBlock", () => {
/>,
);
expect(
container.querySelector('[data-figma-node="22085-860414"]'),
).toBeTruthy();
const section = container.querySelector('[data-figma-node="22085-860414"]');
expect(section).toBeTruthy();
expect(section).toHaveClass("px-[var(--spacing-scale-032)]");
expect(section).not.toHaveClass("px-[calc(var(--spacing-scale-032)+var(--spacing-scale-096))]");
expect(
screen.getByRole("heading", {
@@ -84,6 +85,9 @@ describe("TripleTextBlock", () => {
).toBeInTheDocument();
expect(screen.getByText("First paragraph.")).toBeInTheDocument();
expect(screen.getByText("Second paragraph.")).toBeInTheDocument();
expect(screen.getByText("Second paragraph.")).toHaveClass(
"mt-[var(--spacing-scale-024)]",
);
expect(screen.getByRole("link", { name: "Setup your community" })).toHaveAttribute(
"href",
"/create",
@@ -0,0 +1,153 @@
import { describe, test, expect, vi } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders as render } from "../utils/test-utils";
import UseCaseCompletedRulePage from "../../app/(marketing-case-study)/use-cases/[slug]/rule/page";
import messages from "../../messages/en/index";
import { USE_CASE_DETAIL_SLUGS } from "../../lib/useCaseSyntheticPost";
const mockPush = vi.fn();
const mockOpenLogin = vi.fn();
const mockFetchAuthSession = vi.fn();
const mockDuplicateUseCaseTemplate = vi.fn();
vi.mock("next/navigation", () => ({
notFound: vi.fn(),
useRouter: () => ({ push: mockPush }),
usePathname: () => "/use-cases/food-not-bombs/rule",
}));
vi.mock("../../app/contexts/AuthModalContext", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
useAuthModal: () => ({
openLogin: mockOpenLogin,
closeLogin: vi.fn(),
}),
};
});
vi.mock("../../lib/create/api", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
fetchAuthSession: () => mockFetchAuthSession(),
duplicateUseCaseTemplate: (slug) => mockDuplicateUseCaseTemplate(slug),
};
});
vi.mock(
"../../app/(app)/create/hooks/useCreateFlowMdUp",
() => ({
useCreateFlowMdUp: () => true,
}),
);
describe("UseCaseCompletedRulePage", () => {
test.each(USE_CASE_DETAIL_SLUGS)(
"renders completed rule for %s",
async (slug) => {
const contentKey =
slug === "mutual-aid-colorado"
? "mutualAidColorado"
: slug === "food-not-bombs"
? "foodNotBombs"
: "boulderCountyStreetMedics";
const fixture = messages.pages.useCasesCompletedRules[contentKey];
render(
await UseCaseCompletedRulePage({
params: Promise.resolve({ slug }),
}),
);
expect(
screen.getByRole("heading", { name: fixture.title }),
).toBeInTheDocument();
if (slug === "mutual-aid-colorado") {
expect(
screen.getByText(/Food Not Bombs is not a charity/),
).toBeInTheDocument();
}
if (slug === "boulder-county-street-medics") {
expect(screen.getByText("Membership")).toBeInTheDocument();
expect(screen.getByText(/Tiered Membership/)).toBeInTheDocument();
}
expect(screen.getByText("Values")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /return/i }),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: messages.pages.useCasesCompletedRule.topNav.duplicateAriaLabel,
}),
).toBeInTheDocument();
},
);
test("Duplicate opens login when signed out", async () => {
const user = userEvent.setup();
mockOpenLogin.mockClear();
mockFetchAuthSession.mockResolvedValue({ user: null });
render(
await UseCaseCompletedRulePage({
params: Promise.resolve({ slug: "food-not-bombs" }),
}),
);
await user.click(
screen.getByRole("button", {
name: messages.pages.useCasesCompletedRule.topNav.duplicateAriaLabel,
}),
);
expect(mockOpenLogin).toHaveBeenCalledWith(
expect.objectContaining({
nextPath: "/use-cases/food-not-bombs/rule",
}),
);
expect(mockDuplicateUseCaseTemplate).not.toHaveBeenCalled();
});
test("Duplicate saves to profile when signed in", async () => {
const user = userEvent.setup();
mockPush.mockClear();
mockFetchAuthSession.mockResolvedValue({
user: { id: "u1", email: "a@b.c" },
});
mockDuplicateUseCaseTemplate.mockResolvedValue({
ok: true,
id: "rule-copy",
title: "Food Not Bombs Boulder Template (Copy)",
});
render(
await UseCaseCompletedRulePage({
params: Promise.resolve({ slug: "food-not-bombs" }),
}),
);
await user.click(
screen.getByRole("button", {
name: messages.pages.useCasesCompletedRule.topNav.duplicateAriaLabel,
}),
);
expect(mockDuplicateUseCaseTemplate).toHaveBeenCalledWith("food-not-bombs");
expect(mockPush).toHaveBeenCalledWith("/profile");
});
test("Return navigates to use case detail", async () => {
const user = userEvent.setup();
mockPush.mockClear();
render(
await UseCaseCompletedRulePage({
params: Promise.resolve({ slug: "mutual-aid-colorado" }),
}),
);
await user.click(screen.getByRole("button", { name: /return/i }));
expect(mockPush).toHaveBeenCalledWith("/use-cases/mutual-aid-colorado");
});
});
+6
View File
@@ -18,6 +18,9 @@ vi.mock("../../app/components/sections/ContentBanner", () => ({
<>
<p>{rulePreview.title}</p>
<p>{rulePreview.description}</p>
{rulePreview.href ? (
<a href={rulePreview.href}>View community rule</a>
) : null}
</>
) : null}
</section>
@@ -61,6 +64,9 @@ describe("UseCaseDetailPage", () => {
screen.getByRole("heading", { name: entry.banner.title }),
).toBeInTheDocument();
expect(screen.getByText(entry.ruleCard.description)).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /view community rule/i }),
).toHaveAttribute("href", `/use-cases/${slug}/rule`);
const bodySnippet =
slug === "mutual-aid-colorado"
+16
View File
@@ -106,6 +106,22 @@ describe("Rule Component", () => {
).not.toBeInTheDocument();
});
it("clicking editable title calls onTitleClick and does not fire card onClick", () => {
const onCard = vi.fn();
const onTitle = vi.fn();
render(
<Rule
{...defaultProps}
expanded={true}
onClick={onCard}
onTitleClick={onTitle}
/>,
);
fireEvent.click(screen.getByTestId("rule-title-edit"));
expect(onTitle).toHaveBeenCalledTimes(1);
expect(onCard).not.toHaveBeenCalled();
});
it("clicking editable description calls onDescriptionClick and does not fire card onClick", () => {
const onCard = vi.fn();
const onDesc = vi.fn();
+34
View File
@@ -314,6 +314,40 @@ describe("parseDocumentSectionsForDisplay", () => {
expect(parseDocumentSectionsForDisplay(doc)).toEqual(doc.sections);
});
it("accepts entries with labeled blocks and omits body in JSON (normalized to \"\")", () => {
const doc = {
sections: [
{
categoryName: "Membership",
entries: [
{
title: "Open membership",
blocks: [
{ label: "Eligibility", body: "Anyone may join." },
{ label: "Process", body: "Sign the sheet." },
],
},
],
},
],
};
expect(parseDocumentSectionsForDisplay(doc)).toEqual([
{
categoryName: "Membership",
entries: [
{
title: "Open membership",
body: "",
blocks: [
{ label: "Eligibility", body: "Anyone may join." },
{ label: "Process", body: "Sign the sheet." },
],
},
],
},
]);
});
it("accepts entries with labeled blocks and empty body", () => {
const doc = {
sections: [
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { isChromelessNavigationPath } from "../../../lib/navigationChromelessPath";
describe("isChromelessNavigationPath", () => {
it.each([
["/create", true],
["/create/completed", true],
["/login", true],
["/use-cases/mutual-aid-colorado/rule", true],
["/use-cases/food-not-bombs/rule/", true],
["/", false],
["/use-cases", false],
["/use-cases/mutual-aid-colorado", false],
["/use-cases/mutual-aid-colorado/rule/extra", false],
] as const)("returns %s -> %s", (pathname, expected) => {
expect(isChromelessNavigationPath(pathname)).toBe(expected);
});
it("returns false for null or undefined", () => {
expect(isChromelessNavigationPath(null)).toBe(false);
expect(isChromelessNavigationPath(undefined)).toBe(false);
});
});
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import useCasesCompletedRules from "../../messages/en/pages/useCasesCompletedRules.json";
import { createFlowStateFromPublishedRule } from "../../lib/create/publishedDocumentToCreateFlowState";
import { normalizePublishedDocumentForEdit } from "../../lib/create/normalizePublishedDocumentForEdit";
describe("normalizePublishedDocumentForEdit", () => {
it("derives methodSelections and coreValues from use-case display sections", () => {
const fixture = useCasesCompletedRules.mutualAidColorado;
const normalized = normalizePublishedDocumentForEdit(fixture.document);
expect(Array.isArray(normalized.coreValues)).toBe(true);
expect((normalized.coreValues as unknown[]).length).toBeGreaterThan(0);
const ms = normalized.methodSelections as Record<string, unknown>;
expect(Array.isArray(ms.membership)).toBe(true);
expect((ms.membership as unknown[]).length).toBeGreaterThan(0);
expect(Array.isArray(ms.decisionApproaches)).toBe(true);
expect((ms.decisionApproaches as unknown[]).length).toBeGreaterThan(0);
});
it("is idempotent when methodSelections already exist", () => {
const once = normalizePublishedDocumentForEdit(
useCasesCompletedRules.mutualAidColorado.document,
);
const twice = normalizePublishedDocumentForEdit(once);
expect(twice.methodSelections).toEqual(once.methodSelections);
expect(twice.coreValues).toEqual(once.coreValues);
});
});
describe("createFlowStateFromPublishedRule with section-only documents", () => {
it("hydrates method ids from normalized use-case duplicate shape", () => {
const doc = normalizePublishedDocumentForEdit(
useCasesCompletedRules.mutualAidColorado.document,
);
const patch = createFlowStateFromPublishedRule({
id: "rule-1",
title: "Mutual Aid Colorado Template (Copy)",
summary: "Summary",
document: doc as Record<string, unknown>,
});
expect(patch.selectedMembershipMethodIds?.length).toBeGreaterThan(0);
expect(patch.selectedDecisionApproachIds?.length).toBeGreaterThan(0);
expect(patch.selectedCoreValueIds?.length).toBeGreaterThan(0);
expect(patch.sections).toEqual([]);
});
});
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { useCaseTemplateDuplicateTitle } from "../../lib/useCaseTemplateDuplicate";
describe("useCaseTemplateDuplicateTitle", () => {
it("appends Template (Copy) to the source title", () => {
expect(useCaseTemplateDuplicateTitle("BoCo Street Medics")).toBe(
"BoCo Street Medics Template (Copy)",
);
});
it("falls back when the source title is empty", () => {
expect(useCaseTemplateDuplicateTitle(" ")).toBe(
"Community Rule Template (Copy)",
);
});
});
+98
View File
@@ -0,0 +1,98 @@
import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
const isDatabaseConfiguredMock = vi.fn();
const createMock = vi.fn();
const getSessionUserMock = vi.fn();
vi.mock("../../lib/server/env", () => ({
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
}));
vi.mock("../../lib/server/db", () => ({
prisma: {
publishedRule: {
create: (...args: unknown[]) => createMock(...args),
},
},
}));
vi.mock("../../lib/server/session", () => ({
getSessionUser: () => getSessionUserMock(),
}));
import { POST } from "../../app/api/use-cases/[slug]/duplicate/route";
function makeContext(slug: string) {
return { params: Promise.resolve({ slug }) };
}
beforeEach(() => {
isDatabaseConfiguredMock.mockReset();
createMock.mockReset();
getSessionUserMock.mockReset();
});
describe("POST /api/use-cases/[slug]/duplicate", () => {
it("returns 401 when not signed in", async () => {
isDatabaseConfiguredMock.mockReturnValue(true);
getSessionUserMock.mockResolvedValue(null);
const res = await POST(
new NextRequest("https://x.test/api/use-cases/food-not-bombs/duplicate"),
makeContext("food-not-bombs"),
);
expect(res.status).toBe(401);
});
it("returns 404 for an unknown slug", async () => {
isDatabaseConfiguredMock.mockReturnValue(true);
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
const res = await POST(
new NextRequest("https://x.test/api/use-cases/unknown/duplicate"),
makeContext("unknown"),
);
expect(res.status).toBe(404);
expect(createMock).not.toHaveBeenCalled();
});
it("creates a published rule from the use-case fixture", async () => {
isDatabaseConfiguredMock.mockReturnValue(true);
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
createMock.mockResolvedValueOnce({
id: "r-new",
title: "Food Not Bombs Boulder Template (Copy)",
summary: "Summary",
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z"),
});
const res = await POST(
new NextRequest(
"https://x.test/api/use-cases/food-not-bombs/duplicate",
),
makeContext("food-not-bombs"),
);
expect(res.status).toBe(200);
const body = (await res.json()) as { rule: { id: string; title: string } };
expect(body.rule.id).toBe("r-new");
expect(createMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
userId: "u1",
title: "Food Not Bombs Boulder Template (Copy)",
document: expect.objectContaining({
methodSelections: expect.objectContaining({
membership: expect.arrayContaining([
expect.objectContaining({ id: expect.any(String), label: expect.any(String) }),
]),
}),
coreValues: expect.arrayContaining([
expect.objectContaining({ chipId: expect.any(String), label: expect.any(String) }),
]),
}),
}),
}),
);
});
});