Refine use cases rule examples

This commit is contained in:
adilallo
2026-05-19 22:16:08 -06:00
parent 7c46cbd87b
commit 2f2b5d0dc2
65 changed files with 3129 additions and 252 deletions
@@ -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 controls `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={