Pin the review card description under the sections and tint the card from the uploaded logo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilalloandCursor committed 2026-09-28 15:46:45 -06:00
1 parent 2653ae4602
commit 56adcd59fd
10 files changed
+508 -42

No files matched your search

@@ -0,0 +1,41 @@
"use client";
import { useEffect, useState } from "react";
import {
dominantColorFromImageUrl,
randomPreviewSurfaceColor,
} from "../../../../lib/create/previewSurfaceColor";
/**
* Fill for the review preview card. An uploaded logo contributes its most
* common color; no image uses a random light color. Unset until mount so the
* server and client share the fallback class.
*
* @param imageUrl Uploaded logo URL, or `null` when the user skipped the photo.
* @returns A CSS color, or `undefined` before the client picks one.
*/
export function usePreviewSurfaceColor(
imageUrl: string | null,
): string | undefined {
const [color, setColor] = useState<string | undefined>(undefined);
useEffect(() => {
if (!imageUrl) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- random fill is client-only; first paint stays on the fallback class
setColor(randomPreviewSurfaceColor());
return;
}
let cancelled = false;
void dominantColorFromImageUrl(imageUrl).then((next) => {
if (cancelled) return;
setColor(next ?? randomPreviewSurfaceColor());
});
return () => {
cancelled = true;
};
}, [imageUrl]);
return color;
}
@@ -26,6 +26,7 @@ import {
} from "../../components/FinalReviewChipEditModal";
import { FinalReviewCommunityContextEditModal } from "../../components/FinalReviewCommunityContextEditModal";
import { FinalReviewTitleEditModal } from "../../components/FinalReviewTitleEditModal";
import { usePreviewSurfaceColor } from "../../hooks/usePreviewSurfaceColor";
import { useCreateFlowNavigation } from "../../hooks/useCreateFlowNavigation";
import { createFlowStepForFacetGroup } from "../../utils/facetGroupToCreateFlowStep";
import {
@@ -234,6 +235,13 @@ export function FinalReviewScreen({
const descriptionEmptyHint =
variant === "editPublished" ? t("communityContextEditModal.emptyHint") : undefined;
const avatarUrl =
typeof state.communityAvatarUrl === "string" &&
state.communityAvatarUrl.trim().length > 0
? state.communityAvatarUrl.trim()
: null;
const surfaceColor = usePreviewSurfaceColor(avatarUrl);
return (
<CreateFlowLockupCardStepShell
lockupTitle={
@@ -271,8 +279,10 @@ export function FinalReviewScreen({
}
size={mdUp ? "L" : "M"}
expanded={true}
descriptionAtBottom
backgroundColor="bg-[var(--color-surface-invert-brand-teal)]"
logoUrl={getAssetPath(vectorMarkPath("mutual-aid"))}
surfaceColor={surfaceColor}
logoUrl={avatarUrl ?? getAssetPath(vectorMarkPath("mutual-aid"))}
logoAlt={ruleCardTitle}
categories={finalReviewCategories}
className={CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS}
@@ -49,6 +49,8 @@ const RuleContainer = memo<RuleProps>(
recommended = false,
templateGridFigmaShell = false,
fluidWidth = false,
descriptionAtBottom = false,
surfaceColor,
}) => {
const size = sizeProp ?? "L";
const t = useTranslation("ruleCard");
@@ -112,6 +114,8 @@ const RuleContainer = memo<RuleProps>(
recommended={recommended}
templateGridFigmaShell={templateGridFigmaShell}
fluidWidth={fluidWidth}
descriptionAtBottom={descriptionAtBottom}
surfaceColor={surfaceColor}
cardAriaLabel={cardAriaLabel}
recommendedLabel={recommendedLabel}
/>
+12
View File
@@ -84,6 +84,16 @@ export interface RuleProps {
templateGridFigmaShell?: boolean;
/** When true, expanded cards fill their container instead of a fixed Figma width. */
fluidWidth?: boolean;
/**
* Expanded cards: pin the description under the category rows (review preview).
* Default keeps the description between the header and the categories.
*/
descriptionAtBottom?: boolean;
/**
* Runtime CSS color for the card fill. When set, it overrides
* {@link backgroundColor}.
*/
surfaceColor?: string;
}
export interface RuleViewProps {
@@ -113,6 +123,8 @@ export interface RuleViewProps {
recommended?: boolean;
templateGridFigmaShell?: boolean;
fluidWidth?: boolean;
descriptionAtBottom?: boolean;
surfaceColor?: string;
/** Interactive card aria-label; supplied by the container from `ruleCard` messages. */
cardAriaLabel: string;
recommendedLabel: string;
+56 -38
View File
@@ -35,6 +35,8 @@ export function RuleView({
recommended = false,
templateGridFigmaShell = false,
fluidWidth = false,
descriptionAtBottom = false,
surfaceColor,
cardAriaLabel,
recommendedLabel,
}: RuleViewProps) {
@@ -267,8 +269,58 @@ export function RuleView({
}
const cardClassName = `${backgroundColor} ${cardPadding} ${cardGap} ${borderRadiusClass} shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] ${interactiveCard ? "hover:shadow-[0px_0px_64px_0px_rgba(0,0,0,0.15)] transition-shadow duration-200" : ""} flex flex-col items-start justify-center relative ${cardWidth || "w-full"} ${href ? "no-underline text-inherit" : ""} ${className || ""}`;
const surfaceStyle = surfaceColor
? { backgroundColor: surfaceColor }
: undefined;
const isLinkCard = Boolean(href) && interactiveCard;
const hasCategories = Boolean(categories && categories.length > 0);
const expandedDescriptionFrameClass = [
"relative w-full shrink-0",
hasCategories
? descriptionAtBottom
? "border-t border-solid border-[var(--color-content-invert-primary)] pt-[16px]"
: "border-b border-solid border-[var(--color-content-invert-primary)] pb-[16px]"
: "",
isLarge || isMedium ? "px-0" : "px-[12px]",
]
.filter(Boolean)
.join(" ");
const showExpandedDescription = Boolean(
description ||
(onDescriptionClick && typeof descriptionEmptyHint === "string"),
);
const expandedDescription = showExpandedDescription ? (
<div className={expandedDescriptionFrameClass}>
{onDescriptionClick ? (
<InlineTextButton
type="button"
underline={false}
data-testid="rule-description-edit"
ariaLabel={descriptionEditAriaLabel}
className={`${descriptionClass} w-full min-w-0 cursor-pointer whitespace-pre-wrap text-left text-[var(--color-content-invert-primary)] hover:!opacity-100 ${
!description && descriptionEmptyHint ? "opacity-70" : ""
}`.trim()}
onClick={(e) => {
e.stopPropagation();
onDescriptionClick();
}}
>
{description ?? descriptionEmptyHint ?? ""}
</InlineTextButton>
) : (
description && (
<p
className={`${descriptionClass} cursor-inherit text-[var(--color-content-invert-primary)]`}
>
{description}
</p>
)
)}
</div>
) : null;
const cardBody = (
<>
{/* Figma: Header = `border-b` row, `gap-px`, icon `pl-1 pr-2 py-2` + `border-l` on title. */}
@@ -382,43 +434,7 @@ export function RuleView({
</div>
) : expanded ? (
<>
{(description ||
(onDescriptionClick &&
typeof descriptionEmptyHint === "string")) && (
<div
className={`relative w-full shrink-0 ${
categories && categories.length > 0
? "border-b border-solid border-[var(--color-content-invert-primary)] pb-[16px]"
: ""
} ${expanded && (isLarge || isMedium) ? "px-0" : "px-[12px]"}`}
>
{onDescriptionClick ? (
<InlineTextButton
type="button"
underline={false}
data-testid="rule-description-edit"
ariaLabel={descriptionEditAriaLabel}
className={`${descriptionClass} w-full min-w-0 cursor-pointer whitespace-pre-wrap text-left text-[var(--color-content-invert-primary)] hover:!opacity-100 ${
!description && descriptionEmptyHint ? "opacity-70" : ""
}`.trim()}
onClick={(e) => {
e.stopPropagation();
onDescriptionClick();
}}
>
{description ?? descriptionEmptyHint ?? ""}
</InlineTextButton>
) : (
description && (
<p
className={`${descriptionClass} cursor-inherit text-[var(--color-content-invert-primary)]`}
>
{description}
</p>
)
)}
</div>
)}
{descriptionAtBottom ? null : expandedDescription}
{/* Categories Section - Using MultiSelect */}
{categories && categories.length > 0 && (
<div
@@ -459,6 +475,7 @@ export function RuleView({
))}
</div>
)}
{descriptionAtBottom ? expandedDescription : null}
</>
) : (
/* Collapsed State: Description */
@@ -488,6 +505,7 @@ export function RuleView({
<NextLink
href={href}
className={cardClassName}
style={surfaceStyle}
aria-label={ariaLabel}
onClick={onClick}
>
@@ -499,6 +517,7 @@ export function RuleView({
return (
<div
className={cardClassName}
style={surfaceStyle}
tabIndex={interactiveCard ? 0 : undefined}
role={interactiveCard ? "button" : "article"}
aria-label={ariaLabel}
@@ -510,4 +529,3 @@ export function RuleView({
</div>
);
}