Keep the review card’s fallback color on the draft so a reload does not pick a new one.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilalloandCursor committed 2026-09-29 14:01:38 -06:00
1 parent 56adcd59fd
commit 6d536880fe
8 files changed
+165 -8

No files matched your search

@@ -3,39 +3,60 @@
import { useEffect, useState } from "react";
import {
dominantColorFromImageUrl,
isPreviewSurfaceColor,
randomPreviewSurfaceColor,
} from "../../../../lib/create/previewSurfaceColor";
function storedOrRandom(
storedFallback: string | undefined,
persistFallback: (_color: string) => void,
): string {
if (storedFallback && isPreviewSurfaceColor(storedFallback)) {
return storedFallback;
}
const next = randomPreviewSurfaceColor();
persistFallback(next);
return next;
}
/**
* 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.
* common color; no image uses a random light color stored on the draft so
* reload keeps the same fill. 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.
* @param storedFallback Draft `previewSurfaceColor`, if already picked.
* @param persistFallback Writes the fallback onto the draft once.
* @returns A CSS color, or `undefined` before the client picks one.
*
* @example
* const color = usePreviewSurfaceColor(avatarUrl, state.previewSurfaceColor, persist);
*/
export function usePreviewSurfaceColor(
imageUrl: string | null,
storedFallback: string | undefined,
persistFallback: (_color: string) => void,
): 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());
setColor(storedOrRandom(storedFallback, persistFallback));
return;
}
let cancelled = false;
void dominantColorFromImageUrl(imageUrl).then((next) => {
if (cancelled) return;
setColor(next ?? randomPreviewSurfaceColor());
setColor(next ?? storedOrRandom(storedFallback, persistFallback));
});
return () => {
cancelled = true;
};
}, [imageUrl]);
}, [imageUrl, persistFallback, storedFallback]);
return color;
}
@@ -33,6 +33,7 @@ import {
getAssetPath,
vectorMarkPath,
} from "../../../../../lib/assetUtils";
import { isPreviewSurfaceColor } from "../../../../../lib/create/previewSurfaceColor";
const FACET_FALLBACK_ORDER: readonly TemplateFacetGroupKey[] = [
"coreValues",
@@ -240,7 +241,26 @@ export function FinalReviewScreen({
state.communityAvatarUrl.trim().length > 0
? state.communityAvatarUrl.trim()
: null;
const surfaceColor = usePreviewSurfaceColor(avatarUrl);
const storedFallback =
typeof state.previewSurfaceColor === "string"
? state.previewSurfaceColor
: undefined;
const persistFallback = useCallback(
(color: string) => {
replaceState((prev) =>
prev.previewSurfaceColor &&
isPreviewSurfaceColor(prev.previewSurfaceColor)
? prev
: { ...prev, previewSurfaceColor: color },
);
},
[replaceState],
);
const surfaceColor = usePreviewSurfaceColor(
avatarUrl,
storedFallback,
persistFallback,
);
return (
<CreateFlowLockupCardStepShell
+5
View File
@@ -118,6 +118,11 @@ export interface CreateFlowState {
* Set after successful `POST /api/uploads` with purpose `communityAvatar`.
*/
communityAvatarUrl?: string;
/**
* Light `hsl(...)` fill for the review card when no logo is uploaded.
* Picked once and kept on the draft so reload does not change it.
*/
previewSurfaceColor?: string;
/** Selected chip ids from `community-size` (MultiSelect). */
selectedCommunitySizeIds?: string[];
/** Selected chip ids from `community-structure` (organization types). */